Build a Logos C++ UI module
Get started building a ui_qml module with a C++ backend that runs in a separate process.
This document is accurate for Testnet v0.3.
This guide covers building a UI module that pairs a QML view with a C++ backend. The backend runs in a separate ui-host process while the view loads inside the host app (logos-standalone-app or Logos Basecamp), so a backend crash cannot bring down the host. The backend calls the calc_module from Wrap a C library as a Logos module through a typed, generated API.
You write two things: a .rep file, the contract between the view and the backend, and a C++ backend class that implements it. The build generates the Qt plugin and interface classes around them. This guide is intended for developers who want typed, process-isolated calls from their UI to other modules.
-
A supported OS:
- Linux x86_64 or aarch64
- macOS arm64 (Apple Silicon)
-
The
logos-calc-moduleproject from Wrap a C library as a Logos module, with its shared library built inlogos-calc-module/lib/. You create this guide's project next to it. -
Basic familiarity with QML.
-
Nix with flakes enabled.
- Install from nixos.org, then enable flakes:
mkdir -p ~/.config/nixecho 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
What to expect
- You can build a
calc_ui_cppmodule whose C++ backend runs in its own process, and run it inlogos-standalone-appwithnix run. - You can connect a QML view to the backend's slots, properties and signals, and to an event that
calc_moduleemits. - You can reload the view as you edit it, and check it with automated UI tests.
Step 1: Scaffold the project
-
Create the project directory next to
logos-calc-module, and enter it:mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp -
Initialise the project from the C++ backend UI template:
nix flake init -t github:logos-co/logos-module-builder/0.3.2#ui-qml-backend- The template follows the universal authoring model:
metadata.jsonsets"interface": "universal", and the only sources are an example.repfile and one backend class. The plugin and interface classes are generated.
- The template follows the universal authoring model:
-
Remove the template's example
.repfile and backend, which the following steps replace:rm -f src/ui_example.rep src/ui_example_backend.h src/ui_example_backend.cpp -
Initialise a Git repository and stage the files:
git init && git add -A
Step 2: Configure the module metadata
-
Replace
metadata.jsonwith the following:{"name": "calc_ui_cpp","version": "1.0.0","type": "ui_qml","interface": "universal","category": "tools","description": "Calculator C++ UI — QML view with process-isolated backend for calc_module","main": "calc_ui_cpp_plugin","view": "qml/Main.qml","icon": "icons/calc.png","dependencies": ["calc_module"],"codegen": { "rep": "src/calc_ui_cpp.rep" },"nix": {"packages": {"build": [],"runtime": []},"external_libraries": [],"cmake": {"find_packages": [],"extra_sources": [],"extra_include_dirs": [],"extra_link_libraries": []}}}Field What it does type"ui_qml"marks a module with a QML view.interface"universal"selects the universal authoring model: you write the.repfile and a backend class, and the build generates the plugin around them. Without it, the build expects a hand-written plugin.codegen.repYour .repfile.backend_classandbackend_headercan be set too; they default toCalcUiCppBackendandcalc_ui_cpp_backend.h.mainThe name of the generated backend plugin library, without its extension. viewThe QML entry point. dependenciesThe core modules that the backend calls. Each one is also a flake input of the same name, set up in Step 8. -
Create the icon that the Logos Basecamp sidebar shows for the module. LGX packaging accepts only a 256×256 PNG, and this command writes a placeholder of that size:
mkdir -p icons# Copy any PNG here — or generate a 256×256 placeholder:echo "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUuzHEuzHEVOa2oAAAAH0lEQVR42u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAvg0hAAABYOSdlwAAAABJRU5ErkJggg==" | base64 -d > icons/calc.png
Step 3: Define the remote interface
The .rep file is the contract between the QML view and the C++ backend, and the one Qt-typed file you write. At build time, Qt's repc compiler generates CalcUiCppSimpleSource, the base class that your backend implements, and CalcUiCppReplica, the typed replica that the view uses.
-
Create
src/calc_ui_cpp.rep:class CalcUiCpp{// ── SLOTs — call-and-return; each reply reaches QML via logos.watch() ──SLOT(int add(int a, int b))SLOT(int multiply(int a, int b))SLOT(int factorial(int n))SLOT(int fibonacci(int n))SLOT(QString libVersion())// Void slot — fire-and-forget. Asks calc_module to (re-)announce// its version as a `versionReady` event; there's no return value// to await, the answer comes back through the PROP below.SLOT(void announceVersion())// ── PROPs — auto-synced backend → every QML replica, no polling ──// QString, event-fed: the typed `versionReady` subscription the// backend arms in onContextReady() writes it. Starts empty.PROP(QString versionEvent="" READONLY)// int, slot-driven: the backend bumps it after each calculation,// so the view shows a live tally without ever polling.PROP(int computeCount=0 READONLY)// int, READWRITE: a memory register the QML view both *reads* and// *writes* (Store / Clear buttons), and the backend may set too.// A write round-trips QML → replica → source → back to every replica.PROP(int memory=0 READWRITE)// ── SIGNAL — backend → view push, distinct from a return value ──// Emitted after each calculation. QML catches it with a// Connections block (not logos.watch(), not a PROP read).SIGNAL(computed(QString op, int result))}Each kind of member reaches the view in its own way:
Member Backend QML view SLOTwith a return value, such asaddOverrides int add(int a, int b)logos.watch(backend.add(1, 2), ...)delivers the replySLOTreturningvoid, such asannounceVersionOverrides void announceVersion()Calls backend.announceVersion(), with no reply to wait forREADONLYPROP, such ascomputeCountWrites it with the generated setComputeCount()Reads backend.computeCount, which updates without pollingREADWRITEPROP, such asmemoryCan write it with setMemory()Reads backend.memoryand can assign it, for examplebackend.memory = 8SIGNAL, such ascomputedEmits it with emit computed(op, result)Handles it in a Connectionsblock, inonComputed
Step 4: Configure the CMake build
-
Replace
CMakeLists.txtwith the following:cmake_minimum_required(VERSION 3.14)project(CalcUiCppPlugin LANGUAGES CXX)if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)else()message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")endif()# Derive the module name from metadata.json — single source of truth.file(READ "${CMAKE_CURRENT_SOURCE_DIR}/metadata.json" METADATA_JSON)string(JSON MODULE_NAME GET ${METADATA_JSON} name)logos_module(NAME ${MODULE_NAME}REP_FILE src/calc_ui_cpp.repSOURCESsrc/calc_ui_cpp_backend.hsrc/calc_ui_cpp_backend.cppINCLUDE_DIRSsrc)- List only your two source files.
REP_FILEtellslogos_module()to runrepc, generate the plugin and interface classes around your backend, and build a separatecalc_ui_cpp_replica_factorylibrary that the host uses to create typed replicas.
- List only your two source files.
Step 5: Write the C++ backend
The backend is the only C++ that you write. It derives CalcUiCppSimpleSource, generated from the .rep file, and LogosUiPluginContext, which provides modules(): typed calls and event subscriptions for each module in dependencies.
-
Create
src/calc_ui_cpp_backend.h:#pragma once#include "rep_calc_ui_cpp_source.h"#include "logos_ui_plugin_context.h"// The whole hand-written backend. Derives:// - CalcUiCppSimpleSource — generated from calc_ui_cpp.rep; override its// slots (the QML replica gets each return value via Qt Remote Objects).// - LogosUiPluginContext — supplies modules() (Qt-typed callers + typed event// subscriptions for "dependencies") and onContextReady(). A UI plugin is a// view, not a module, so that is all the context carries.// The *Plugin / *Interface classes (Q_PLUGIN_METADATA, initLogos wiring,// QtRO registration) are generated around it.class CalcUiCppBackend : public CalcUiCppSimpleSource,public LogosUiPluginContext{public:// Slots from calc_ui_cpp.rep — each delegates to calc_module.int add(int a, int b) override;int multiply(int a, int b) override;int factorial(int n) override;int fibonacci(int n) override;QString libVersion() override;// Tells calc_module to emit its `versionReady` event.void announceVersion() override;// Fires once when ui-host hands the plugin its LogosAPI — the// typed dependency surface is live, so we arm the event// subscription here (before the view's first call).void onContextReady() override;private:// Feeds the non-slot surfaces of the .rep after each calculation:// bumps the computeCount PROP (setComputeCount, generated) and// emits the `computed` SIGNAL. The READWRITE `memory` PROP is// driven from QML, so the backend doesn't have to touch it.void record(const QString& op, int result);};- There is no
Q_OBJECT,Q_PLUGIN_METADATA,initLogos,name()orversion()to write. The build generates them.
- There is no
-
Create
src/calc_ui_cpp_backend.cpp:#include "calc_ui_cpp_backend.h"// Generated umbrella: LogosModules (behind modules()) from// metadata.json#dependencies — typed wrappers + typed event accessors.#include "logos_sdk.h"int CalcUiCppBackend::add(int a, int b){int result = modules().calc_module.add(a, b);record("add", result);return result;}int CalcUiCppBackend::multiply(int a, int b){int result = modules().calc_module.multiply(a, b);record("multiply", result);return result;}int CalcUiCppBackend::factorial(int n){int result = modules().calc_module.factorial(n);record("factorial", result);return result;}int CalcUiCppBackend::fibonacci(int n){int result = modules().calc_module.fibonacci(n);record("fibonacci", result);return result;}QString CalcUiCppBackend::libVersion(){// A UI plugin is Qt-typed: modules().calc_module's wrapper returns QString// (api-style qt), matching the .rep slot — no conversion needed.return modules().calc_module.libVersion();}void CalcUiCppBackend::record(const QString& op, int result){// PROP: bump the slot-driven counter. setComputeCount() is the// generated setter; Qt Remote Objects syncs the new value to every// replica, so the view's "Computations" label updates with no polling.setComputeCount(computeCount() + 1);// SIGNAL: a backend → view push, distinct from the return value the// QML side gets via logos.watch(). `computed` is declared on the// generated SimpleSource, so we just emit it; the typed replica// re-emits it and the view's Connections block catches it.emit computed(op, result);}void CalcUiCppBackend::announceVersion(){// Fire-and-forget call into calc_module: it looks up the library// version and emits it as a `versionReady` event. We don't read a// return value here — the event comes back through the subscription// armed in onContextReady() below.modules().calc_module.libVersionNotify();}void CalcUiCppBackend::onContextReady(){// Typed module-event subscription. `versionReady` is calc_module's// event (Part 1's `logos_events:` block); the generated wrapper// exposes it as on<Event> + a Qt-typed callback (QString, because a// UI plugin is api-style qt). Push each payload into the versionEvent// PROP — Qt Remote Objects then auto-syncs it to the QML replica.modules().calc_module.onVersionReady([this](const QString& version) {setVersionEvent(version);});}modules().calc_module.add(a, b)callscalc_modulethrough the generated, typed API. A UI plugin gets Qt types (int,QString) that match its.repslots, even wherecalc_moduledeclaresstd::string; the generated wrapper converts them.record()updates thecomputeCountproperty and emits thecomputedsignal after each calculation.onContextReady()subscribes to theversionReadyevent ofcalc_moduleand writes each payload to theversionEventproperty. Subscribe there, not in the constructor:modules()is ready only once the framework callsonContextReady().
Step 6: Write the QML view
-
Create
src/qml/Main.qml:import QtQuickimport QtQuick.Controlsimport QtQuick.LayoutsItem {id: rootproperty string result: ""property string errorText: ""// Last payload from the backend's `computed` SIGNAL (see Connections below).property string lastSignal: "(none)"// Typed replica of the backend running in ui-host (generated from calc_ui_cpp.rep).readonly property var backend: logos.module("calc_ui_cpp")// The ui-host backend connects asynchronously, so the replica isn't// immediately usable. Track readiness reactively: isViewModuleReady()// is a Q_INVOKABLE (not a property), so we re-check it on the// onViewModuleReadyChanged signal and once at startup — never via a// plain property binding, which would not re-evaluate.property bool ready: falseConnections {target: logosfunction onViewModuleReadyChanged(moduleName, isReady) {if (moduleName === "calc_ui_cpp")root.ready = isReady && root.backend !== null}}Component.onCompleted: {root.ready = root.backend !== null && logos.isViewModuleReady("calc_ui_cpp")}// SIGNAL from the .rep: the backend emits `computed(op, result)` after// each calculation. The typed replica re-emits it, so we catch it with// a Connections block — no logos.watch(), no property read. This is the// backend → view push path, distinct from the slot return value above.Connections {target: root.backendfunction onComputed(op, result) {root.lastSignal = op + " = " + result}}// logos.watch() delivers the result of a replica slot call via callbacks.// No QtRemoteObjects import needed — the bridge handles it.function callCalc(method, args) {if (!root.ready) {root.errorText = "Backend not ready"return}root.errorText = ""root.result = "..."logos.watch(backend[method].apply(backend, args),function(value) { root.result = String(value) },function(error) { root.errorText = String(error) })}ColumnLayout {anchors.fill: parentanchors.margins: 24spacing: 16Text {text: "Logos Calculator (C++ backend)"font.pixelSize: 20color: "#ffffff"Layout.alignment: Qt.AlignHCenter}// Reactive backend-connection indicator.Text {text: root.ready ? "Connected" : "Connecting to backend..."color: root.ready ? "#56d364" : "#f0883e"font.pixelSize: 12Layout.alignment: Qt.AlignHCenter}RowLayout {spacing: 12Layout.fillWidth: trueTextField {id: inputAplaceholderText: "a"Layout.preferredWidth: 80validator: IntValidator {}}TextField {id: inputBplaceholderText: "b"Layout.preferredWidth: 80validator: IntValidator {}}Button {text: "Add"enabled: root.readyonClicked: root.callCalc("add", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])}Button {text: "Multiply"enabled: root.readyonClicked: root.callCalc("multiply", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])}}RowLayout {spacing: 12Layout.fillWidth: trueTextField {id: inputNplaceholderText: "n"Layout.preferredWidth: 80validator: IntValidator { bottom: 0 }}Button {text: "Factorial"enabled: root.readyonClicked: root.callCalc("factorial", [parseInt(inputN.text) || 0])}Button {text: "Fibonacci"enabled: root.readyonClicked: root.callCalc("fibonacci", [parseInt(inputN.text) || 0])}Button {text: "libcalc version"enabled: root.readyonClicked: root.callCalc("libVersion", [])}Button {// Fires the event path: asks calc_module to emit// versionReady. No logos.watch() — the result comes// back through the versionEvent PROP, not a return value.text: "Announce version (event)"enabled: root.readyonClicked: root.backend.announceVersion()}}Rectangle {Layout.fillWidth: trueheight: 56color: root.errorText.length > 0 ? "#3d1a1a" : "#1a2d1a"radius: 8Text {anchors.centerIn: parenttext: root.errorText.length > 0 ? root.errorText: (root.result.length > 0 ? root.result : "Enter values and press a button")color: root.errorText.length > 0 ? "#f85149" : "#56d364"font.pixelSize: 15}}// Slot-driven PROP: bumped by the backend's record() after each// calculation. A plain property read — auto-syncs, no polling.Text {text: "Computations: " + ((root.ready && root.backend) ? root.backend.computeCount : 0)color: "#cdd6f4"font.pixelSize: 14Layout.alignment: Qt.AlignHCenter}// SIGNAL payload, captured by the Connections block above.Text {text: "Last op (signal): " + root.lastSignalcolor: "#94e2d5"font.pixelSize: 14Layout.alignment: Qt.AlignHCenter}// READWRITE PROP: the memory register. The label *reads*// backend.memory; the buttons *write* it. A write round-trips// QML → replica → backend source → back to every replica, so the// label updates once the new value syncs home.RowLayout {spacing: 12Layout.alignment: Qt.AlignHCenterText {text: "Memory: " + ((root.ready && root.backend) ? root.backend.memory : 0)color: "#cdd6f4"font.pixelSize: 14}Button {text: "Store (MS)"enabled: root.readyonClicked: root.backend.memory = parseInt(root.result) || 0}Button {text: "Clear (MC)"enabled: root.readyonClicked: root.backend.memory = 0}}// Event-fed label: the versionEvent PROP auto-syncs from the// backend's typed versionReady subscription. No polling — it// updates the moment calc_module emits.Text {readonly property string ev: (root.ready && root.backend) ? root.backend.versionEvent : ""text: "Version event: " + (ev.length > 0 ? ev : "(none yet)")color: "#f9e2af"font.pixelSize: 15Layout.alignment: Qt.AlignHCenter}Item { Layout.fillHeight: true }}}logos.module("calc_ui_cpp")returns the typed replica of the backend. The host injects thelogosobject, so noQtRemoteObjectsimport is needed.- The backend connects asynchronously from its own process.
logos.isViewModuleReady()is a method, not a property, so the view checks it once at startup and again inonViewModuleReadyChanged, and enables the buttons only when the backend is ready. - Slot replies arrive through
logos.watch(), properties are read and written directly, and aConnectionsblock on the backend handles thecomputedsignal.
Step 7: Use the Logos Design System in your QML (optional)
The host app has logos-design-system on its QML import path. Use its themed components instead of raw controls and hard-coded colours, so the module keeps the Logos look as the design system evolves.
-
In
src/qml/Main.qml, import the design system and replace raw controls with its components, for example:import Logos.Themeimport Logos.Controlsimport Logos.Icons // optional shared icon assetsLogosButton {text: qsTr("Add")onClicked: root.callCalc("add", [parseInt(inputA.text) || 0,parseInt(inputB.text) || 0])}LogosTextField {id: inputAplaceholderText: qsTr("a")}Rectangle {color: Theme.palette.backgroundSecondaryradius: Theme.spacing.radiusSmallLogosText { text: qsTr("Result"); color: Theme.palette.text }} -
Build the design system's storybook, then run it to browse the available components:
nix build github:logos-co/logos-design-system#bin-bundle-dir --out-link ../logos-storybook../logos-storybook/bin/LogosStorybook- Controls are designed and ready for production, for example
LogosButton,LogosBadge,LogosCheckbox,LogosComboBox,LogosIconButton,LogosPaginator,LogosSearchBar,LogosTabBar,LogosTable,LogosText,LogosTextFieldandLogosToolTip. - Controls (not designed) have stable APIs but unstyled visuals, for example
LogosDialog,LogosDrawer,LogosScrollView,LogosSpinner,LogosTextAreaandLogosSwitch. You can ship with them; they get the designed look later without changes to your QML.
Use theme tokens instead of colour literals and font sizes:
Token Values Theme.palette.*background,backgroundSecondary,surface,text,textSecondary,border,primary,success,warning,error,info,hover,pressed, …Theme.spacing.*tiny,small,medium,large,xlarge,xxlarge,radiusSmall,radiusMedium,radiusLargeTheme.typography.*pageTitleText(36),titleText(30),panelTitleText(24),subtitleText(16),primaryText(14),secondaryText(12),weightRegular,weightMedium,weightBold,publicSansLogosIcons.*(fromLogos.Icons)arrowLeft,arrowRight,refresh,install,trash,more,search, … - Controls are designed and ready for production, for example
Step 8: Configure the Nix flake
-
Replace
flake.nixwith the following:{description = "Calculator C++ UI plugin for Logos - QML view with process-isolated backend for calc_module";inputs = {logos-module-builder.url = "github:logos-co/logos-module-builder/0.3.2";# Points at your local calc_module checkout. This is a placeholder —# you lock it to your actual path in the next step with# `nix flake update --override-input` (see "Lock and build" below).calc_module.url = "path:/path/to/your/calc_module";};outputs = inputs@{ logos-module-builder, calc_module, ... }:logos-module-builder.lib.mkLogosQmlModule {src = ./.;configFile = ./metadata.json;flakeInputs = inputs;};}- The
calc_moduleinput has the same name as the dependency inmetadata.json. - Leave the placeholder
path:/path/to/your/calc_moduleas it is. Nix doesn't accept a relative path such as../logos-calc-moduleinflake.nix, so Step 9 records the real path inflake.lockinstead. - To fetch
calc_modulefrom a repository instead, use agithub:URL, for examplecalc_module.url = "github:<your-org>/<your-calc-module>";. mkLogosQmlModulecompiles the backend, bundles the view, builds the LGX packages and sets upnix run.
- The
Step 9: Build and run the module
-
Confirm that the shared library of
calc_moduleexists:# Linuxls ../logos-calc-module/lib/libcalc.so# macOSls ../logos-calc-module/lib/libcalc.dylib-
If it is missing, build it as in Wrap a C library as a Logos module:
cd ../logos-calc-module/libgcc -shared -fPIC -o libcalc.so libcalc.c # Linux# gcc -shared -fPIC -o libcalc.dylib libcalc.c # macOScd ../../logos-calc-ui-cpp
-
-
Stage the files and lock
calc_moduleto your local project:git add -Anix flake update --override-input calc_module path:../logos-calc-modulegit add flake.lock--override-inputresolves../logos-calc-moduleto an absolute path and records it inflake.lock, so later commands need no override.- The lock keeps a copy of
calc_moduleas it is now. After you changecalc_module, run thenix flake updatecommand again to build against the change.
-
Build and run the app:
nix run- The first run builds the backend, the replica factory and
logos-standalone-app, which can take several minutes.
Expected result:
logos-standalone-appopens the calculator view, which shows Connected once the backend is ready:
- The first run builds the backend, the replica factory and
-
Enter
3and5in the first two fields, then click Add.Expected result: the result shows
8, followed byComputations: 1andLast op (signal): add = 8:
8is the slot's reply,Computations: 1is thecomputeCountproperty andLast op (signal): add = 8is thecomputedsignal.
-
Click Store (MS), then Announce version (event).
Expected result: the view shows
Memory: 8andVersion event: 1.0.0.Memoryis theREADWRITEproperty: the view wrote it, and the value synced back from the backend.Version eventarrives through the event path: the button madecalc_moduleemitversionReady, and the backend's subscription wrote the payload to theversionEventproperty.
Step 10: Reload the view as you edit it (optional)
nix run rebuilds the plugin whenever any tracked file changes, including the QML. The development launcher reloads the view from your source tree instead, without a rebuild.
-
Build the development launcher once, then start it from the project root:
nix build .#ui-dev./result/bin/run-logos-standalone-uiIt reports where it reloads QML from:
run-logos-standalone-ui: hot-reloading QML from /path/to/logos-calc-ui-cpp/src/qml(export DEV_QML_PATH to override, or LOGOS_QML_HOT_RELOAD=0 to disable) -
Edit and save a
.qmlor.jsfile undersrc/qml/.Expected result: the view re-renders with your change, and the launcher logs
QML reloadedwith the file's path.- The backend keeps running in its own process, so its state and connections survive a reload. QML state, such as the text in a field, resets.
- A QML syntax error is logged with its line number and blanks the view until the next save that compiles.
- Changes to C++, the
.repfile,metadata.jsonor CMake still need a rebuild: runnix build .#ui-devagain and restart the launcher.
Step 11: Add UI integration tests (optional)
Add automated UI tests with the logos-qt-mcp test framework. The tests connect to the QML inspector in logos-standalone-app and can find elements, click buttons and check text.
-
Create
tests/ui-tests.mjs:import { resolve } from "node:path";// CI sets LOGOS_QT_MCP automatically; for interactive use: nix build .#test-framework -o result-mcpconst root =process.env.LOGOS_QT_MCP ||new URL("../result-mcp", import.meta.url).pathname;const { test, run } = await import(resolve(root, "test-framework/framework.mjs"));test("calc_ui_cpp: loads and shows title", async (app) => {await app.waitFor(async () => {await app.expectTexts(["Logos Calculator (C++ backend)"]);},{ timeout: 15000, interval: 500, description: "UI to load" },);});test("calc_ui_cpp: operation buttons visible", async (app) => {await app.expectTexts(["Add", "Multiply", "Factorial", "Fibonacci"]);});run(); -
Stage the test file and run the tests headlessly:
git add tests/nix build .#integration-test -Lintegration-testlauncheslogos-standalone-appwithQT_QPA_PLATFORM=offscreen, so no display is needed, and runs every.mjsfile intests/.
-
To run the tests against a running app instead, build the test framework and start the app:
nix build .#test-framework -o result-mcpnix run .Then, in a second terminal in the project directory, run the tests with Node.js:
node tests/ui-tests.mjs- The app serves its QML inspector on port 3768.
Troubleshooting the C++ UI module build
Linker errors during nix build
Confirm that ../logos-calc-module/lib/libcalc.so (Linux) or ../logos-calc-module/lib/libcalc.dylib (macOS) exists, and that flake.lock was updated with nix flake update --override-input calc_module path:../logos-calc-module. A missing library or a lock that still holds the placeholder path is the most common cause.
The development launcher doesn't find your QML
The launcher looks for the view entry from metadata.json under src/<viewDir>/, then under <viewDir>/. If your project is laid out differently, set DEV_QML_PATH to the directory that contains the entry file itself, here Main.qml, not a parent directory.
Known constraints
Reloading works only with logos-standalone-app. Logos Basecamp loads UI plugins from its own data directory, so it shows your changes only after you rebuild and reinstall the LGX package.