32 KiB
Tutorial Part 3: Building a C++ UI Module
This is Part 3 of the Logos module tutorial series. In Part 2 you built a QML UI plugin. Now you'll build a native C++ Qt widget plugin that calls calc_module through a typed backend class.
What you'll build: A calc_ui_cpp C++ plugin with two options for the UI:
- Option A — QML loaded from C++: A
QQuickWidgetinside the plugin loading the sameMain.qmlas the QML plugin, withCalcBackendexposed as a context property — plus dev mode for editing QML without rebuilding - Option B — Pure Qt widget:
QPushButton,QLineEdit,QLabelwired directly to a backend class
Why C++ over QML-only?
| QML plugin (Part 2) | C++ UI plugin (Part 3) | |
|---|---|---|
| Compilation | No | Yes (CMake) |
| Backend calls | Via logos.callModule() IPC bridge |
Via LogosAPI* directly in C++ |
| Type safety | Weak — all args travel as QVariant |
Strong — C++ types preserved |
| Sandboxing | Yes | No |
| QML support | Native | Optional via QQuickWidget |
The C++ backend class provides type-safe calls via generated SDK wrappers — int arguments stay int all the way to the module without relying on runtime coercion.
Prerequisites:
- Completed Part 1 — you have a working
calc_module, and its shared library exists inlogos-calc-module/lib/(.soon Linux,.dylibon macOS) - Nix with flakes enabled
How It Works
+----------------------+ CalcBackend::add(3, 5) +-------------------+
| calc_ui_cpp | --------------------------------> | calc_module |
| C++ Qt plugin | LogosAPI* / invokeRemoteMethod | C++ plugin |
| createWidget() | | add(int, int) |
+----------------------+ +-------------------+
^
| loaded by
v
logos-standalone-app / logos-basecamp
The plugin implements createWidget() which returns a QWidget*. The widget is shown in the host app's window. A CalcBackend class holds LogosAPI* and makes typed calls to calc_module.
Step 1: Scaffold
mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp
nix flake init -t github:logos-co/logos-module-builder/tutorial-v1#ui-module
git init && git add -A
Note: The generated
flake.nixuses an unpinnedlogos-module-builderURL. Replace it with the pinned version shown in Step 9 to ensure reproducible builds.
This gives you:
logos-calc-ui-cpp/
├── flake.nix
├── metadata.json
├── CMakeLists.txt
├── interfaces/
│ └── IComponent.h
└── src/
├── ui_example_interface.h
├── ui_example_plugin.h
└── ui_example_plugin.cpp
Rename the source files to match your module:
mv src/ui_example_interface.h src/calc_ui_cpp_interface.h
mv src/ui_example_plugin.h src/calc_ui_cpp_plugin.h
mv src/ui_example_plugin.cpp src/calc_ui_cpp_plugin.cpp
Step 2: metadata.json
metadata.json is the single source of truth — it contains both the runtime metadata (embedded into the plugin binary by Qt) and the build configuration (read by logos-module-builder via the nix section).
{
"name": "calc_ui_cpp",
"version": "1.0.0",
"type": "ui",
"category": "tools",
"description": "Calculator C++ UI — widget frontend for calc_module",
"main": "calc_ui_cpp_plugin",
"icon": "icons/calc.png",
"dependencies": ["calc_module"],
"nix": {
"packages": {
"build": [],
"runtime": []
},
"external_libraries": [],
"cmake": {
"find_packages": [],
"extra_sources": [],
"extra_include_dirs": [],
"extra_link_libraries": []
}
}
}
Create the icon directory and add a placeholder icon. The icon is displayed in the logos-basecamp sidebar when the module is loaded:
mkdir -p icons
# Copy any PNG here — or generate a 64×64 placeholder:
echo "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAeklEQVR4nO3PUQkAIBTAwFfPdtazjSH8OITBAtxm7fN1wwUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY0oAUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY0oAUNaEEDWtCAFjSgBQ1oQQNa0IAWPHYBic8hlloAWpEAAAAASUVORK5CYII=" | base64 -d > icons/calc.png
Naming convention: Each entry in
dependenciesmust match thenamefield in that module's ownmetadata.json. When adding a dependency as a flake input, the input attribute name must also match — e.g.,calc_module.url = "github:logos-co/logos-tutorial/tutorial-v1?dir=logos-calc-module". The URL can point to any repo, but the attribute name is how the builder resolves dependencies.
Step 3: CMakeLists.txt
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()
logos_module(
NAME calc_ui_cpp
SOURCES
src/calc_ui_cpp_interface.h
src/calc_ui_cpp_plugin.h
src/calc_ui_cpp_plugin.cpp
src/calc_backend.h
src/calc_backend.cpp
INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/interfaces
)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
target_link_libraries(calc_ui_cpp_module_plugin PRIVATE Qt6::Widgets)
For Option A (QML inside the plugin) you will add
Quick QuickWidgetsandqt_add_resources— covered in Step 7.
Step 4: Interface Header (src/calc_ui_cpp_interface.h)
#ifndef CALC_UI_CPP_INTERFACE_H
#define CALC_UI_CPP_INTERFACE_H
#include <QObject>
#include <QString>
#include "interface.h"
class CalcUiCppInterface : public PluginInterface
{
public:
virtual ~CalcUiCppInterface() = default;
};
#define CalcUiCppInterface_iid "org.logos.CalcUiCppInterface"
Q_DECLARE_INTERFACE(CalcUiCppInterface, CalcUiCppInterface_iid)
#endif // CALC_UI_CPP_INTERFACE_H
The template also scaffolds interfaces/IComponent.h — the widget interface that logos-basecamp uses to load C++ UI plugins. You don't need to modify this file:
// interfaces/IComponent.h (scaffolded by the template — do not modify)
#pragma once
#include <QObject>
#include <QWidget>
#include <QtPlugin>
class LogosAPI;
class IComponent {
public:
virtual ~IComponent() = default;
virtual QWidget* createWidget(LogosAPI* logosAPI = nullptr) = 0;
virtual void destroyWidget(QWidget* widget) = 0;
};
#define IComponent_iid "com.logos.component.IComponent"
Q_DECLARE_INTERFACE(IComponent, IComponent_iid)
Step 5: Plugin Header (src/calc_ui_cpp_plugin.h)
Replace the scaffolded plugin header. This header is the same for both Option A and Option B — only the .cpp implementation differs.
Key difference from core modules: C++ UI plugins must inherit from
IComponent(frominterfaces/IComponent.h, scaffolded by the template) and useIComponent_iidas the plugin metadata IID. This is howlogos-basecampidentifies and loads widget-based UI plugins. WithoutIComponent, basecamp will fail with "Failed to cast plugin to IComponent".
#ifndef CALC_UI_CPP_PLUGIN_H
#define CALC_UI_CPP_PLUGIN_H
#include <QObject>
#include <QWidget>
#include <QVariantList>
#include <IComponent.h>
#include "calc_ui_cpp_interface.h"
class CalcUiCppPlugin : public QObject, public CalcUiCppInterface, public IComponent
{
Q_OBJECT
Q_PLUGIN_METADATA(IID IComponent_iid FILE "metadata.json")
Q_INTERFACES(CalcUiCppInterface PluginInterface IComponent)
public:
explicit CalcUiCppPlugin(QObject* parent = nullptr);
~CalcUiCppPlugin() override;
QString name() const override { return "calc_ui_cpp"; }
QString version() const override { return "1.0.0"; }
Q_INVOKABLE void initLogos(LogosAPI* api);
Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr);
Q_INVOKABLE void destroyWidget(QWidget* widget);
signals:
void eventResponse(const QString& eventName, const QVariantList& args);
private:
LogosAPI* m_logosAPI = nullptr;
};
#endif // CALC_UI_CPP_PLUGIN_H
Step 6: Backend Class
The backend class is the key addition over the QML plugin. It holds a LogosModules* wrapper — a typed C++ SDK generated at build time from metadata.json — and exposes Q_INVOKABLE methods that call calc_module through it. Because the calls go through a generated typed class, argument types are preserved at compile time — no runtime coercion needed.
How the generated SDK works
When metadata.json declares "dependencies": ["calc_module"] and calc_module is passed as a flake input via flakeInputs, the build system runs logos-cpp-generator before compilation. This produces:
logos_sdk.h/logos_sdk.cpp— theLogosModulesumbrella class with one typed member per dependencycalc_module_api.h/calc_module_api.cpp— the per-module wrapper included bylogos_sdk.h
LogosModules is constructed with a LogosAPI* and provides a member named after each declared dependency (snake_case). All IPC routing happens inside the generated code — your backend just calls methods directly:
m_logos->calc_module.add(3, 5) // typed: int add(int, int) over IPC
src/calc_backend.h
#ifndef CALC_BACKEND_H
#define CALC_BACKEND_H
#include <QObject>
#include <QString>
#include "logos_sdk.h" // generated at build time from metadata.json dependencies
class LogosAPI;
class CalcBackend : public QObject
{
Q_OBJECT
public:
explicit CalcBackend(LogosAPI* api, QObject* parent = nullptr);
Q_INVOKABLE int add(int a, int b);
Q_INVOKABLE int multiply(int a, int b);
Q_INVOKABLE int factorial(int n);
Q_INVOKABLE int fibonacci(int n);
Q_INVOKABLE QString libVersion();
private:
LogosModules* m_logos; // generated umbrella wrapper
};
#endif // CALC_BACKEND_H
src/calc_backend.cpp
#include "calc_backend.h"
CalcBackend::CalcBackend(LogosAPI* api, QObject* parent)
: QObject(parent), m_logos(new LogosModules(api)) {}
int CalcBackend::add(int a, int b) { return m_logos->calc_module.add(a, b); }
int CalcBackend::multiply(int a, int b) { return m_logos->calc_module.multiply(a, b); }
int CalcBackend::factorial(int n) { return m_logos->calc_module.factorial(n); }
int CalcBackend::fibonacci(int n) { return m_logos->calc_module.fibonacci(n); }
QString CalcBackend::libVersion() { return m_logos->calc_module.libVersion(); }
LogosModules is constructed once with LogosAPI*. Each member (calc_module) is a generated proxy that routes calls to the corresponding module process over Qt Remote Objects IPC. No raw invokeRemoteMethod, no string method names, no manual QVariant unwrapping.
Step 7: Option A — QML Loaded from C++
The plugin loads src/qml/Main.qml into a QQuickWidget and exposes CalcBackend as a QML context property. The QML is identical in structure to logos-calc-ui/Main.qml (Part 2), but calls backend.* methods directly instead of routing through the logos.callModule() IPC bridge — so argument types are preserved and there is no sandboxing overhead.
7.1 Add the QML file
Create src/qml/Main.qml. The structure mirrors logos-calc-ui/Main.qml exactly; the only difference is that buttons call backend.* methods directly instead of routing through logos.callModule(...):
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
property string result: ""
property string errorText: ""
ColumnLayout {
anchors.fill: parent
anchors.margins: 24
spacing: 16
// ── Title ──────────────────────────────────────────────
Text {
text: "Logos Calculator (C++ backend)"
font.pixelSize: 20
color: "#ffffff"
Layout.alignment: Qt.AlignHCenter
}
// ── Two-operand operations ─────────────────────────────
RowLayout {
spacing: 12
Layout.fillWidth: true
TextField { id: inputA; placeholderText: "a"; Layout.preferredWidth: 80; validator: IntValidator {} }
TextField { id: inputB; placeholderText: "b"; Layout.preferredWidth: 80; validator: IntValidator {} }
Button {
text: "Add"
onClicked: root.result = String(backend.add(inputA.text, inputB.text))
}
Button {
text: "Multiply"
onClicked: root.result = String(backend.multiply(inputA.text, inputB.text))
}
}
// ── Single-operand operations ──────────────────────────
RowLayout {
spacing: 12
Layout.fillWidth: true
TextField { id: inputN; placeholderText: "n"; Layout.preferredWidth: 80; validator: IntValidator { bottom: 0 } }
Button { text: "Factorial"; onClicked: root.result = String(backend.factorial(inputN.text)) }
Button { text: "Fibonacci"; onClicked: root.result = String(backend.fibonacci(inputN.text)) }
Button { text: "libcalc version"; onClicked: root.result = backend.libVersion() }
}
// ── Result display ─────────────────────────────────────
Rectangle {
Layout.fillWidth: true
height: 56
color: root.errorText.length > 0 ? "#3d1a1a" : "#1a2d1a"
radius: 8
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
}
}
Item { Layout.fillHeight: true }
}
}
7.2 Update CMakeLists.txt
Add Quick and QuickWidgets, and embed the QML as a Qt resource:
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()
logos_module(
NAME calc_ui_cpp
SOURCES
src/calc_ui_cpp_interface.h
src/calc_ui_cpp_plugin.h
src/calc_ui_cpp_plugin.cpp
src/calc_backend.h
src/calc_backend.cpp
INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/interfaces
)
find_package(Qt6 REQUIRED COMPONENTS Widgets Quick QuickWidgets)
target_link_libraries(calc_ui_cpp_module_plugin PRIVATE
Qt6::Widgets
Qt6::Quick
Qt6::QuickWidgets
)
qt_add_resources(calc_ui_cpp_module_plugin "qml_resources"
PREFIX "/"
FILES
src/qml/Main.qml
)
7.3 createWidget() — load QML
Replace calc_ui_cpp_plugin.cpp with:
#include "calc_ui_cpp_plugin.h"
#include "calc_backend.h"
#include "logos_api.h"
#include <QDebug>
#include <QDir>
#include <QQuickWidget>
#include <QQmlContext>
#include <QUrl>
CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : QObject(parent) {}
CalcUiCppPlugin::~CalcUiCppPlugin() {}
void CalcUiCppPlugin::initLogos(LogosAPI* api)
{
m_logosAPI = api;
}
QWidget* CalcUiCppPlugin::createWidget(LogosAPI* logosAPI)
{
auto* backend = new CalcBackend(logosAPI);
auto* quickWidget = new QQuickWidget();
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
quickWidget->rootContext()->setContextProperty("backend", backend);
// Dev mode: set QML_PATH to the directory containing Main.qml to load
// from the filesystem without rebuilding. Example: export QML_PATH=$PWD/src/qml
QString devSource = qgetenv("QML_PATH");
QUrl qmlUrl = devSource.isEmpty()
? QUrl("qrc:/src/qml/Main.qml")
: QUrl::fromLocalFile(QDir(devSource).filePath("Main.qml"));
quickWidget->setSource(qmlUrl);
if (quickWidget->status() == QQuickWidget::Error) {
qWarning() << "CalcUiCppPlugin: failed to load QML";
for (const auto& e : quickWidget->errors())
qWarning() << e.toString();
}
return quickWidget;
}
void CalcUiCppPlugin::destroyWidget(QWidget* widget)
{
delete widget;
}
7.4 Dev Mode
When QML_PATH is set, the plugin loads Main.qml from disk instead of the embedded resource. You can edit QML layout, styling, and property bindings without a Nix rebuild — just restart the app to pick up changes.
# Run with dev mode enabled
QML_PATH=$PWD/src/qml \
nix run .
What still requires a rebuild:
- Changes to
.cpp/.hfiles (backend logic, plugin interface)- Changes to
CMakeLists.txtormetadata.jsonWhat does not require a rebuild:
- Any
.qmlchange — layout, styling, property bindings, JS logic
Step 8: Option B — Pure Qt Widget
The plugin creates a standard Qt widget using layouts and connects button clicks to the backend. No QML, no additional Qt modules — just Qt6::Widgets.
Replace src/calc_ui_cpp_plugin.cpp with:
src/calc_ui_cpp_plugin.cpp
#include "calc_ui_cpp_plugin.h"
#include "calc_backend.h"
#include "logos_api.h"
#include <QDebug>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : QObject(parent) {}
CalcUiCppPlugin::~CalcUiCppPlugin() {}
void CalcUiCppPlugin::initLogos(LogosAPI* api)
{
m_logosAPI = api;
}
QWidget* CalcUiCppPlugin::createWidget(LogosAPI* logosAPI)
{
auto* backend = new CalcBackend(logosAPI);
auto* widget = new QWidget();
auto* layout = new QVBoxLayout(widget);
layout->setContentsMargins(24, 24, 24, 24);
layout->setSpacing(16);
// ── Title ──────────────────────────────────────────────────
auto* title = new QLabel("Logos Calculator (C++)");
title->setAlignment(Qt::AlignHCenter);
layout->addWidget(title);
// ── Two-operand row ────────────────────────────────────────
auto* twoOpRow = new QHBoxLayout();
auto* inputA = new QLineEdit(); inputA->setPlaceholderText("a"); inputA->setMaximumWidth(80);
auto* inputB = new QLineEdit(); inputB->setPlaceholderText("b"); inputB->setMaximumWidth(80);
auto* addBtn = new QPushButton("Add");
auto* mulBtn = new QPushButton("Multiply");
twoOpRow->addWidget(inputA);
twoOpRow->addWidget(inputB);
twoOpRow->addWidget(addBtn);
twoOpRow->addWidget(mulBtn);
twoOpRow->addStretch();
layout->addLayout(twoOpRow);
// ── Single-operand row ─────────────────────────────────────
auto* oneOpRow = new QHBoxLayout();
auto* inputN = new QLineEdit(); inputN->setPlaceholderText("n"); inputN->setMaximumWidth(80);
auto* facBtn = new QPushButton("Factorial");
auto* fibBtn = new QPushButton("Fibonacci");
auto* verBtn = new QPushButton("libcalc version");
oneOpRow->addWidget(inputN);
oneOpRow->addWidget(facBtn);
oneOpRow->addWidget(fibBtn);
oneOpRow->addWidget(verBtn);
oneOpRow->addStretch();
layout->addLayout(oneOpRow);
// ── Result display ─────────────────────────────────────────
auto* resultLabel = new QLabel("Enter values and press a button");
resultLabel->setAlignment(Qt::AlignHCenter);
layout->addWidget(resultLabel);
layout->addStretch();
// ── Wire up buttons ────────────────────────────────────────
auto show = [resultLabel](const QString& v) { resultLabel->setText(v); };
QObject::connect(addBtn, &QPushButton::clicked, [=] {
show(QString::number(backend->add(inputA->text().toInt(), inputB->text().toInt())));
});
QObject::connect(mulBtn, &QPushButton::clicked, [=] {
show(QString::number(backend->multiply(inputA->text().toInt(), inputB->text().toInt())));
});
QObject::connect(facBtn, &QPushButton::clicked, [=] {
show(QString::number(backend->factorial(inputN->text().toInt())));
});
QObject::connect(fibBtn, &QPushButton::clicked, [=] {
show(QString::number(backend->fibonacci(inputN->text().toInt())));
});
QObject::connect(verBtn, &QPushButton::clicked, [=] {
show(backend->libVersion());
});
return widget;
}
void CalcUiCppPlugin::destroyWidget(QWidget* widget)
{
delete widget;
}
Step 9: flake.nix
Since metadata.json declares "type": "ui", mkLogosModule automatically wires up apps.default (i.e. nix run) for free — no manual apps block or logosStandalone parameter required. The standalone app runner is bundled with logos-module-builder.
Important — flakeInputs: Because metadata.json declares "dependencies": ["calc_module"], the build system runs logos-cpp-generator before compiling your C++ sources. The generator introspects calc_module's built plugin to produce logos_sdk.h / logos_sdk.cpp (and per-module calc_module_api.h / calc_module_api.cpp). These are the files your backend includes as #include "logos_sdk.h". For this to work, calc_module must be available as a built Nix package at code-generation time — that is what flakeInputs provides (the builder discovers dependency inputs by matching their names against the dependencies array in metadata.json). Without it, the build fails with 'logos_sdk.h' file not found.
{
description = "Calculator C++ UI plugin for Logos - widget frontend for calc_module";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder/tutorial-v1";
calc_module.url = "github:logos-co/logos-tutorial/tutorial-v1?dir=logos-calc-module";
# calc_module.url = "path:../logos-calc-module"; # local checkout (development)
};
outputs = inputs@{ logos-module-builder, calc_module, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
Because metadata.json declares "type": "ui", mkLogosModule automatically wires up apps.default. It stages the compiled plugin alongside metadata.json and any icon files into a Nix store directory, bundles all module dependencies (direct and transitive) from their LGX packages, then produces a shell script that calls logos-standalone-app with that directory — exactly what nix run executes. All required backend modules are self-contained; no external setup is needed.
The calc_module.url can be either:
github:— use the published tutorial-v1 repo.path:— use your local checkout (for development).
Important: Whichever URL scheme you use,
calc_modulemust be built with its shared library (.soon Linux,.dylibon macOS) present inlib/.
Step 10: Build and Test
10.1 Build
If libcalc is missing from logos-calc-module/lib, rebuild it first:
# Check:
ls ../logos-calc-module/lib/libcalc.so # Linux
ls ../logos-calc-module/lib/libcalc.dylib # macOS
# Rebuild if missing:
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
Then build the UI module:
git add -A
nix flake update
git add flake.lock
# If flake.nix uses path:../logos-calc-module
nix build
# If flake.nix uses github: — override to local checkout
nix build --override-input calc_module path:../logos-calc-module
Inspect the output with lm (the module inspector from logos-module):
nix build 'github:logos-co/logos-module/tutorial-v1#lm' --out-link ./lm-cli
# Linux
./lm-cli/bin/lm ./result/lib/calc_ui_cpp_plugin.so
# macOS
# ./lm-cli/bin/lm ./result/lib/calc_ui_cpp_plugin.dylib
You should see createWidget and destroyWidget in the methods list.
10.2 UI only (layout preview)
nix run .
The widget opens. No backend module is loaded, so button clicks will silently return 0 (CalcBackend logs a warning when calc_module is not connected) — but you can verify the layout looks correct.
10.3 Full functionality (with modules)
The standalone app automatically bundles and loads all module dependencies declared in metadata.json. To test with your local calc_module from Part 1:
Run with local override:
nix run . --override-input calc_module path:../logos-calc-module
Clicking Add, Multiply, Factorial, or Fibonacci now calls the real module.
When do you need
--override-input?calc_module.urlinflake.nixpoints to the published GitHub URL. If your locallogos-calc-modulehas unpushed changes or differs from what is on GitHub, you must use--override-input calc_module path:../logos-calc-moduleso nix uses your local copy. If yourcalc_moduleis already pushed and matches the GitHub URL, you can runnix build/nix runwithout the override. This is the same mechanismws build --local/ws build --auto-localuses throughout the workspace.
If flake.nix already uses calc_module.url = "path:../logos-calc-module", you can run nix build / nix run without --override-input.
Step 11: Load in logos-basecamp
11.1 Create LGX packages
Use --out-link to avoid overwriting the result symlink:
# Package calc_module (from Part 1)
cd ../logos-calc-module
nix build '.#lgx' --out-link result-lgx
nix build '.#lgx-portable' --out-link result-lgx-portable
# Package the C++ UI plugin
cd ../logos-calc-ui-cpp
nix build '.#lgx' --out-link result-lgx
nix build '.#lgx-portable' --out-link result-lgx-portable
For more bundling options (standalone bundler syntax, cross-platform packaging), see the Developer Guide — Bundling with nix-bundle-lgx.
11.2 Build and run logos-basecamp
Build logos-basecamp, launch it once to preinstall its bundled modules, then install your modules.
Note:
logos-basecampdoes not accept--modules-diror--ui-plugins-dirCLI flags. It manages its own data directory and preinstalls bundled modules (main_ui, package_manager, etc.) on first launch.
# Build logos-basecamp
nix build 'github:logos-co/logos-basecamp/tutorial-v1' -o basecamp-result
# Launch once to preinstall bundled modules, then close it
./basecamp-result/bin/logos-basecamp
Basecamp creates its data directory on first launch. To find where it is, check the log output for plugins directory or look for the directory that contains modules/ and plugins/ subdirectories:
# macOS (typical path, may vary):
ls ~/Library/Application\ Support/Logos/
# Linux (typical path, may vary):
ls ~/.local/share/Logos/
The dev build directory is named LogosBasecampDev (portable builds use LogosBasecamp).
Install your modules using lgpm. First, set BASECAMP_DIR to your platform's path:
# macOS:
BASECAMP_DIR="$HOME/Library/Application Support/Logos/LogosBasecampDev"
# Linux:
BASECAMP_DIR="$HOME/.local/share/Logos/LogosBasecampDev"
# Build lgpm CLI
nix build 'github:logos-co/logos-package-manager/tutorial-v1#cli' --out-link ./pm
# Install core module
./pm/bin/lgpm --modules-dir "$BASECAMP_DIR/modules" \
install --file ../logos-calc-module/result-lgx/*.lgx
# Install UI plugin
./pm/bin/lgpm --ui-plugins-dir "$BASECAMP_DIR/plugins" \
install --file result-lgx/*.lgx
# Launch basecamp -- your modules appear alongside the built-in ones
./basecamp-result/bin/logos-basecamp
11.3 Portable basecamp build (optional)
The dev build above depends on nix store paths at runtime. For a self-contained portable build that works without nix:
# Build portable basecamp (bundles all Qt frameworks/libraries)
nix build 'github:logos-co/logos-basecamp/tutorial-v1#bin-bundle-dir' -o basecamp-portable
# Launch once to preinstall bundled modules
./basecamp-portable/bin/logos-basecamp
The portable build uses a different data directory (LogosBasecamp instead of LogosBasecampDev). Set BASECAMP_DIR to your platform's path:
# macOS:
BASECAMP_DIR="$HOME/Library/Application Support/Logos/LogosBasecamp"
# Linux:
BASECAMP_DIR="$HOME/.local/share/Logos/LogosBasecamp"
Install your modules using the portable .lgx variants:
# Install core module (use portable variant)
./pm/bin/lgpm --modules-dir "$BASECAMP_DIR/modules" \
install --file ../logos-calc-module/result-lgx-portable/*.lgx
# Install UI plugin (use portable variant)
./pm/bin/lgpm --ui-plugins-dir "$BASECAMP_DIR/plugins" \
install --file result-lgx-portable/*.lgx
# Launch
./basecamp-portable/bin/logos-basecamp
Important: Portable basecamp requires portable
.lgxvariants (result-lgx-portable), and the dev build requires dev variants (result-lgx). Mixing them will cause loading failures.
11.4 Install via logos-basecamp UI
Instead of using lgpm on the command line, you can install modules through the basecamp UI:
- Launch
logos-basecamp - Go to Package Manager
- Click Install from file
- Select
../logos-calc-module/result-lgx/*.lgx— installscalc_module - Repeat for
result-lgx/*.lgx— installscalc_ui_cpp
The "Calculator" tab appears in the sidebar.
Known Limitations
QML changes not appearing after rebuild (Option A only)
Qt caches compiled QML on disk. If you update your Main.qml, rebuild and reinstall the .lgx, but the old UI still appears, the cache is stale. Fix by disabling the cache before launching:
QML_DISABLE_DISK_CACHE=1 ./basecamp-result/bin/logos-basecamp
UI module not loading or basecamp behaving unexpectedly
When switching between portable and dev builds of basecamp, or running multiple basecamp instances, the data directory can get into a bad state (stale modules, mixed variants, corrupted preinstall). Clear it and let basecamp re-preinstall on next launch:
# Remove basecamp's data directory
# macOS:
rm -rf ~/Library/Application\ Support/Logos/LogosBasecampDev
# Linux:
rm -rf ~/.local/share/Logos/LogosBasecampDev
# Relaunch — basecamp will re-preinstall its bundled modules
./basecamp-result/bin/logos-basecamp
Then reinstall your custom modules.
Recap: Three Module Types
| Core (Part 1) | QML UI (Part 2) | C++ UI (Part 3) | |
|---|---|---|---|
| Language | C++ | QML / JS | C++ (+ optional QML) |
| Compilation | Yes | No | Yes |
| Backend calls | Exposed via Q_INVOKABLE |
logos.callModule() IPC |
LogosAPI* → invokeRemoteMethod() |
| Type safety | Strong | Weak (QVariant/QString) | Strong |
| Async support | — | logos.callModuleAsync() |
LogosAPIClient::invokeRemoteMethodAsync() |
| Sandboxed | No | Yes | No |
| QML support | — | Native | Via QQuickWidget |
| Template | #default |
#ui-qml-module |
#ui-module |
What's Next
- Generated type-safe wrappers — instead of raw
invokeRemoteMethod, uselogos-cpp-generatorto generate a typedCalcModuleClientclass. See Developer Guide Section 6.2 - Events — core modules emit
eventResponsesignals; connect to them from your backend class viaLogosAPIClient - Use the Logos Design System in Option B QML —
import Logos.Themeandimport Logos.Controlsare available when running insidelogos-basecamp