2026-05-27 15:03:26 +08:00
---
title : Build a Logos C++ UI module
doc_type : procedure
product : core
topics : core
2026-06-01 08:59:09 -04:00
steps_layout : sectioned
2026-06-17 20:47:13 -04:00
authors : iurimatias, kashepavadan
2026-05-27 15:03:26 +08:00
owner : logos
doc_version : 1
slug : build-a-logos-cpp-ui-module
2026-07-22 09:14:51 -04:00
sidebar_position : 4
2026-05-27 15:03:26 +08:00
---
# Build a Logos C++ UI module
2026-07-06 09:17:31 +00:00
#### Get started building a ui\_qml module with a C++ backend that runs in a separate process.
2026-06-01 08:59:09 -04:00
2026-07-02 22:07:41 -04:00
This guide covers building a [module ](https://docs.logos.co/get-started/glossary#module ) that pairs a QML user interface with a C++ backend plugin. The backend runs in a separate `ui-host` process while the QML view loads inside the host app (`logos-basecamp` or `logos-standalone-app` ), so a backend crash cannot bring down the host. This guide is intended for developers who have completed [Part 1 ](wrap-a-c-library-as-a-logos-core-module.md ) and want typed, process-isolated inter-module calls from their UI layer.
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
**Before you start** , make sure you have the following:
2026-06-01 08:59:09 -04:00
2026-07-07 19:48:26 +08:00
- Completed [Part 1 ](wrap-a-c-library-as-a-logos-core-module.md ) — a working `calc_module` with the shared library built in `logos-calc-module/lib/` .
- Nix with flakes enabled.
- Basic familiarity with [QML ](https://doc.qt.io/qt-6/qmlapplications.html )
2026-06-01 08:59:09 -04:00
## What to expect
2026-07-07 19:48:26 +08:00
- You can scaffold, configure, and build a `calc_ui_cpp` module with a process-isolated C++ backend.
- You can call `calc_module` methods from QML using a typed replica and `logos.watch()` .
- You will be able to build, run, and live-reload the module using `nix run` .
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
## Step 1: Scaffold the project
2026-06-01 08:59:09 -04:00
2026-06-22 19:26:29 +08:00
Create a new directory and initialise it from the C++ backend UI template.
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
1. Create and enter the project directory:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
```bash
mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp
` ``
2. Initialise from the template:
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
nix flake init -t github:logos-co/logos-module-builder/0.2.0#ui-qml-backend
` ``
3. Initialise a Git repository and stage all generated files:
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
git init && git add -A
` ``
4. Remove the template's example sources. The scaffolded template includes ` ui_example` files with mismatched class names and IIDs; leaving them causes build errors or plugin-load failures at runtime:
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
rm -f src/ui_example.rep src/ui_example_interface.h src/ui_example_plugin.h src/ui_example_plugin.cpp
` ``
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
## Step 2: Configure the module metadata
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
Replace the template ` metadata.json` with your plugin's details.
2026-06-01 08:59:09 -04:00
2026-07-22 09:14:51 -04:00
:::info
2026-06-17 20:47:13 -04:00
The ` calc_module.url` input attribute name in ` flake.nix` must match the dependency name declared here in ` "dependencies"`.
2026-07-22 09:14:51 -04:00
:::
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
1. Replace ` metadata.json` with the following:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``json
{
"name": "calc_ui_cpp",
"version": "1.0.0",
"type": "ui_qml",
"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"],
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
"nix": {
"packages": { "build": [], "runtime": [] },
"external_libraries": [],
"cmake": {
"find_packages": [],
"extra_sources": [],
"extra_include_dirs": [],
"extra_link_libraries": []
}
}
}
` ``
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
Key fields:
2026-06-01 08:59:09 -04:00
2026-07-07 19:48:26 +08:00
- ` "type": "ui_qml"` — tells the builder this is a QML view module.
- ` "main": "calc_ui_cpp_plugin"` — the backend Qt plugin library name (without extension).
- ` "view": "qml/Main.qml"` — the QML entry point.
- ` "dependencies": ["calc_module"]` — [core modules](https://docs.logos.co/get-started/glossary#core-module) the backend calls.
2026-07-06 09:17:31 +00:00
2. Create the icons directory and add a placeholder icon (displayed in the ` logos-basecamp` sidebar when the module is loaded):
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
mkdir -p icons
# Copy any PNG here - or generate a 64x64 placeholder:
echo "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAmElEQVR4nO3QMREAIBDAsFeEN3ziCWRkoEP2XmedfX82OkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAO0BN/SiO/PatoIAAAAASUVORK5CYII=" | base64 -d > icons/calc.png
` ``
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
## Step 3: Define the remote interface
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
The ` .rep` file is the single source of truth for the interface between the QML view and the C++ backend. ` repc` compiles it at build time into typed source and replica headers.
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
1. Create ` src/calc_ui_cpp.rep`:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``rep
class CalcUiCpp
{
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())
}
` ``
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` repc` generates two headers from this file:
2026-06-01 08:59:09 -04:00
2026-07-07 19:48:26 +08:00
- ` rep_calc_ui_cpp_source.h` — ` CalcUiCppSimpleSource` with virtual slots the backend overrides.
- ` rep_calc_ui_cpp_replica.h` — ` CalcUiCppReplica` with typed methods the QML view calls.
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
## Step 4: Write the interface header
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
Create ` src/calc_ui_cpp_interface.h`:
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
` ``cpp
#ifndef CALC_UI_CPP_INTERFACE_H
#define CALC_UI_CPP_INTERFACE_H
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
#include <QObject>
#include <QString>
#include "interface.h"
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
class CalcUiCppInterface : public PluginInterface
{
public:
virtual ~CalcUiCppInterface() = default;
};
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
#define CalcUiCppInterface_iid "org.logos.CalcUiCppInterface"
Q_DECLARE_INTERFACE(CalcUiCppInterface, CalcUiCppInterface_iid)
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
#endif // CALC_UI_CPP_INTERFACE_H
` ``
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
## Step 5: Configure the CMake build
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
Create ` CMakeLists.txt`:
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
` ``cmake
cmake_minimum_required(VERSION 3.14)
project(CalcUiCppPlugin LANGUAGES CXX)
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
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()
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
logos_module(
NAME calc_ui_cpp
REP_FILE src/calc_ui_cpp.rep
SOURCES
src/calc_ui_cpp_interface.h
src/calc_ui_cpp_plugin.h
src/calc_ui_cpp_plugin.cpp
)
` ``
` REP_FILE` tells ` logos_module()` to run ` repc` to generate source/replica headers, generate ` CalcUiCppViewPluginBase`, and build a separate ` calc_ui_cpp_replica_factory` shared library.
## Step 6: Write the C++ backend plugin
The backend plugin inherits three base classes:
2026-07-07 19:48:26 +08:00
- ` CalcUiCppSimpleSource` — generated from ` .rep`, provides the typed source for Qt Remote Objects.
- ` CalcUiCppInterface` — standard Logos plugin interface (` name()`, ` version()`).
- ` CalcUiCppViewPluginBase` — generated, provides ` setBackend()` and ` enableRemoting()`.
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
1. Create ` src/calc_ui_cpp_plugin.h`:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``cpp
#ifndef CALC_UI_CPP_PLUGIN_H
#define CALC_UI_CPP_PLUGIN_H
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
#include <QString>
#include <QVariantList>
#include "calc_ui_cpp_interface.h"
#include "LogosViewPluginBase.h"
#include "rep_calc_ui_cpp_source.h"
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
class LogosAPI;
class LogosModules;
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
class CalcUiCppPlugin : public CalcUiCppSimpleSource,
public CalcUiCppInterface,
public CalcUiCppViewPluginBase
{
Q_OBJECT
Q_PLUGIN_METADATA(IID CalcUiCppInterface_iid FILE "metadata.json")
Q_INTERFACES(CalcUiCppInterface)
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
public:
explicit CalcUiCppPlugin(QObject* parent = nullptr);
~CalcUiCppPlugin() override;
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
QString name() const override { return "calc_ui_cpp"; }
QString version() const override { return "1.0.0"; }
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
Q_INVOKABLE void initLogos(LogosAPI* api);
// Slots from calc_ui_cpp.rep — return values directly. The QML replica
// receives QRemoteObjectPendingReply; use logos.watch() in QML to get the value.
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;
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
signals:
void eventResponse(const QString& eventName, const QVariantList& args);
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
private:
LogosAPI* m_logosAPI = nullptr;
LogosModules* m_logos = nullptr;
};
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
#endif // CALC_UI_CPP_PLUGIN_H
` ``
2026-06-17 20:47:13 -04:00
2026-07-22 09:14:51 -04:00
:::info
If the interface filename or IID symbol here doesn't match the names in ` src/calc_ui_cpp_interface.h`, you will get build errors or plugin-load failures at runtime.
:::
2026-07-06 09:17:31 +00:00
2. Create ` src/calc_ui_cpp_plugin.cpp`:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``cpp
#include "calc_ui_cpp_plugin.h"
#include "logos_api.h"
#include "logos_sdk.h"
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : CalcUiCppSimpleSource(parent) {}
CalcUiCppPlugin::~CalcUiCppPlugin() { delete m_logos; }
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
void CalcUiCppPlugin::initLogos(LogosAPI* api)
{
if (m_logos) return;
m_logosAPI = api;
m_logos = new LogosModules(api);
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
// Register this object as the Remote Objects source so the QML replica
// can see its properties and call its slots.
setBackend(this);
}
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
int CalcUiCppPlugin::add(int a, int b) { return m_logos->calc_module.add(a, b); }
int CalcUiCppPlugin::multiply(int a, int b) { return m_logos->calc_module.multiply(a, b); }
int CalcUiCppPlugin::factorial(int n) { return m_logos->calc_module.factorial(n); }
int CalcUiCppPlugin::fibonacci(int n) { return m_logos->calc_module.fibonacci(n); }
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
QString CalcUiCppPlugin::libVersion()
{
return m_logos->calc_module.libVersion();
}
` ``
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
## Step 7: Write the QML view
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
1. Create ` src/qml/Main.qml`:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
Item {
id: root
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
property string result: ""
property string errorText: ""
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
// Typed replica of the backend running in ui-host (generated from calc_ui_cpp.rep).
readonly property var backend: logos.module("calc_ui_cpp")
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
// 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: false
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
Connections {
target: logos
function 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")
}
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
// logos.watch() delivers the result of a replica slot call via callbacks.
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) }
)
}
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
ColumnLayout {
anchors.fill: parent
anchors.margins: 24
spacing: 16
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
Text {
text: "Logos Calculator (C++ backend)"
font.pixelSize: 20
color: "#ffffff"
Layout.alignment: Qt.AlignHCenter
}
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
// Reactive backend-connection indicator.
Text {
text: root.ready ? "Connected" : "Connecting to backend..."
color: root.ready ? "#56d364" : "#f0883e"
font.pixelSize: 12
Layout.alignment: Qt.AlignHCenter
}
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
RowLayout {
spacing: 12
Layout.fillWidth: true
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
TextField { id: inputA; placeholderText: "a"; Layout.preferredWidth: 80; validator: IntValidator {} }
TextField { id: inputB; placeholderText: "b"; Layout.preferredWidth: 80; validator: IntValidator {} }
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
Button {
text: "Add"; enabled: root.ready
onClicked: root.callCalc("add", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])
}
Button {
text: "Multiply"; enabled: root.ready
onClicked: root.callCalc("multiply", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])
}
}
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
RowLayout {
spacing: 12
Layout.fillWidth: true
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
TextField { id: inputN; placeholderText: "n"; Layout.preferredWidth: 80; validator: IntValidator { bottom: 0 } }
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
Button { text: "Factorial"; enabled: root.ready; onClicked: root.callCalc("factorial", [parseInt(inputN.text) || 0]) }
Button { text: "Fibonacci"; enabled: root.ready; onClicked: root.callCalc("fibonacci", [parseInt(inputN.text) || 0]) }
Button { text: "libcalc version"; enabled: root.ready; onClicked: root.callCalc("libVersion", []) }
}
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
Rectangle {
Layout.fillWidth: true
height: 56
color: root.errorText.length > 0 ? "#3d1a1a" : "#1a2d1a"
radius: 8
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
Text {
anchors.centerIn: parent
text: 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
}
}
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
Item { Layout.fillHeight: true }
}
}
` ``
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
Key patterns:
2026-06-01 08:59:09 -04:00
2026-07-07 19:48:26 +08:00
- ` logos.module("calc_ui_cpp")` — gets the typed replica, with auto-synced properties.
- ` logos.watch(backend.add(1, 2), ...)` — delivers a ` SLOT` return value as a JS Promise.
- The ` logos` object is injected by the host at runtime — no ` QtRemoteObjects` import is needed.
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
### Step 7.5: Use the Logos Design System in your QML (Optional)
The QML view runs inside the [` logos-standalone-app`](https://github.com/logos-co/logos-standalone-app) host app, which already has [` logos-design-system`](https://github.com/logos-co/logos-design-system) on its import path. Use its themed components directly to automatically give your module a polished, consistent look.
2026-07-06 09:17:31 +00:00
1. In ` src/qml/Main.qml`, add the necessary imports and replace raw ` Button` and ` TextField` elements with design system equivalents:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``qml
import Logos.Theme
import Logos.Controls
import Logos.Icons // optional shared icon assets
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
...
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
// Instead of Button:
LogosButton {
text: qsTr("Add")
onClicked: root.callCalc("add", [parseInt(inputA.text) || 0,
parseInt(inputB.text) || 0])
}
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
...
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
// Instead of TextField:
LogosTextField {
id: inputA
placeholderText: qsTr("a")
}
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
...
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
// Use theme colors instead of hardcoded hex values:
Rectangle {
color: Theme.palette.backgroundSecondary
radius: Theme.spacing.radiusSmall
LogosText { text: qsTr("Result"); color: Theme.palette.text }
}
` ``
2. Explore available components by running the design system storybook in the logos-design-system repo:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
git clone https://github.com/logos-co/logos-design-system.git
cd logos-design-system && nix run
` ``
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
The sidebar splits components into:
2026-06-01 08:59:09 -04:00
2026-07-07 19:48:26 +08:00
- **Controls** — designed per Figma, production-ready (` LogosButton`, ` LogosBadge`, ` LogosCheckbox`, ` LogosComboBox`, ` LogosIconButton`, ` LogosPaginator`, ` LogosSearchBar`, ` LogosTabBar`, ` LogosTable`, ` LogosText`, ` LogosTextField`, ` LogosToolTip`, …).
- **Controls (not designed)** — placeholders with stable APIs but unstyled visuals (` LogosDialog`, ` LogosDrawer`, ` LogosScrollView`, ` LogosSpinner`, ` LogosTextArea`, ` LogosSwitch`, …). You can ship with them; they'll get the polished look applied later without you having to change your QML.
2026-07-06 09:17:31 +00:00
**Theme tokens** (use these instead of hex literals or manual font sizes):
2026-07-07 19:48:26 +08:00
- ` 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`, ` radiusLarge`
- ` Theme.typography.*` — ` pageTitleText` (36), ` titleText` (30), ` panelTitleText` (24), ` subtitleText` (16), ` primaryText` (14), ` secondaryText` (12); ` weightRegular` / ` weightMedium` / ` weightBold`; ` publicSans`
- ` Logos.Icons.LogosIcons.*` — ` arrowLeft`, ` arrowRight`, ` refresh`, ` install`, ` trash`, ` more`, ` search`, …
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
## Step 8: Configure the Nix flake
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
The template already wires everything up. Update the description and point ` calc_module` at your dependency.
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
1. Replace ` flake.nix` with the following:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``nix
{
description = "Calculator C++ UI plugin for Logos - QML view with process-isolated backend for calc_module";
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0";
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
# Keep this placeholder as is - it gets locked to your real path in the next step.
calc_module.url = "path:/path/to/your/calc_module";
};
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
outputs = inputs@{ logos-module-builder, calc_module, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
` ``
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
To point at a published repo instead of a local path, change the ` calc_module.url` input to a ` github:` URL, for example ` calc_module.url = "github:<your-org>/<your-calc-module>";`.
2026-06-17 20:47:13 -04:00
## Step 9: Build and run the module
Before building, confirm the ` calc_module` shared library is present from [Part 1](wrap-a-c-library-as-a-logos-core-module.md).
2026-07-06 09:17:31 +00:00
1. Confirm the shared library exists:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
# Linux
ls ../logos-calc-module/lib/libcalc.so
2026-06-17 20:47:13 -04:00
2026-07-06 09:17:31 +00:00
# macOS
ls ../logos-calc-module/lib/libcalc.dylib
` ``
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
If the file is missing, build it first:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
cd ../logos-calc-module/lib
gcc -shared -fPIC -o libcalc.so libcalc.c # Linux
# gcc -shared -fPIC -o libcalc.dylib libcalc.c # macOS
cd ../../logos-calc-ui-cpp
` ``
2. Stage all files, then lock ` calc_module` to your local Part 1 checkout. The ` --override-input` flag resolves ` ../logos-calc-module` to an absolute path and records it in ` flake.lock`:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
git add -A
nix flake update --override-input calc_module path:../logos-calc-module
git add flake.lock
` ``
3. Build and run the app. After the lock is in place, no override flag is needed on subsequent commands:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
nix run
` ``
4. Confirm the view loads with all controls visible, then click **Add** with values in the input fields to test it out:
2026-06-01 08:59:09 -04:00
2026-07-22 09:14:51 -04:00

2026-06-17 20:47:13 -04:00
2026-07-22 09:14:51 -04:00

2026-06-17 20:47:13 -04:00
## Step 10: Update view with live reloading (Optional)
To enable live updates to the UI, set ` DEV_QML_PATH` to the directory that contains your view entry's basename. This tutorial sets ` "view": "qml/Main.qml"` in ` metadata.json`, so the directory must contain ` Main.qml` (here: ` src/qml/`).
2026-07-06 09:17:31 +00:00
1. Run with ` DEV_QML_PATH` set so that QML is loaded from your source tree at runtime:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
DEV_QML_PATH=$PWD/src/qml nix run .
` ``
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
## Step 11: Add UI integration tests (Optional)
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
Add automated UI tests using the [logos-qt-mcp](https://github.com/logos-co/logos-qt-mcp) test framework.
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
1. Create ` tests/ui-tests.mjs`:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``javascript
import { resolve } from "node:path";
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
// CI sets LOGOS_QT_MCP automatically; for interactive use: nix build .#test-framework -o result-mcp
const 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")
);
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
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" },
);
});
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
test("calc_ui_cpp: operation buttons visible", async (app) => {
await app.expectTexts(["Add", "Multiply", "Factorial", "Fibonacci"]);
});
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
run();
` ``
2. Stage the test file and run the hermetic CI test:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
git add tests/
nix build .#integration-test -L
` ``
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
The ` integration-test` output launches ` logos-standalone-app` with ` QT_QPA_PLATFORM=offscreen` (no display needed), connects to the QML inspector, and runs all ` .mjs` files in ` tests/`.
3. To run tests interactively against an already-running app, build the test framework and run the app and tests in separate terminals:
2026-06-01 08:59:09 -04:00
2026-07-06 09:17:31 +00:00
` ``bash
nix build .#test-framework -o result-mcp
nix run . # terminal 1 — app with inspector on :3768
node tests/ui-tests.mjs # terminal 2
` ``
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
## Troubleshooting the C++ UI module build
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
### The plugin fails to load at runtime with no clear error
2026-06-01 08:59:09 -04:00
2026-06-17 20:47:13 -04:00
Confirm the ` Q_PLUGIN_METADATA` IID and ` Q_DECLARE_INTERFACE` macro in ` calc_ui_cpp_interface.h` both use ` CalcUiCppInterface_iid`. A mismatch causes silent load failures.
### Linker errors during ` nix build`
Confirm ` ../logos-calc-module/lib/libcalc.so` for Linux (or ` .dylib` on macOS) exists and that ` flake.lock` was updated with ` --override-input calc_module path:../logos-calc-module`. A stale or placeholder lock file is the most common cause.
### ` DEV_QML_PATH` does not seem to take effect
Confirm the path points at the directory containing ` Main.qml` directly — not a parent directory. The host looks for the basename from ` "view"` in ` metadata.json` inside the directory you provide.