Files
logos-tutorial/tutorial-cpp-ui-app.md
T
Khushboo Mehta 22410a8a7b fix: improvements
1. Adapt to changed made in standalone app to use capability module
   directy so it doesnt have to be loaded manually
2. adapt to using nix deriuvation for lgx packages directly from modules
2026-03-26 12:04:09 +01:00

785 lines
27 KiB
Markdown

# Tutorial Part 3: Building a C++ UI Module
This is Part 3 of the Logos module tutorial series. In [Part 2](tutorial-qml-ui-app.md) 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 `QQuickWidget` inside the plugin loading the same `Main.qml` as the QML plugin, with `CalcBackend` exposed as a context property — plus dev mode for editing QML without rebuilding
- **Option B — Pure Qt widget:** `QPushButton`, `QLineEdit`, `QLabel` wired 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 also fixes the type coercion issue from Part 2 — `int` arguments stay `int` all the way to the module.
**Prerequisites:**
- Completed [Part 1](tutorial-wrapping-c-library.md) — you have a working `calc_module`
- 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
```bash
mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp
nix flake init -t github:logos-co/logos-module-builder#ui-module
git init && git add -A
```
This gives you:
```
logos-calc-ui-cpp/
├── flake.nix
├── metadata.json
├── CMakeLists.txt
└── src/
├── ui_example_interface.h
├── ui_example_plugin.h
└── ui_example_plugin.cpp
```
Rename the source files to match your module:
```bash
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).
```json
{
"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:
```bash
mkdir -p icons
# Copy any PNG here — or use a placeholder:
convert -size 64x64 xc:'#4a90d9' icons/calc.png 2>/dev/null \
|| printf '\x89PNG\r\n\x1a\n' > icons/calc.png
```
> **Naming convention:** Each entry in `dependencies` must match the `name` field in that module's own `metadata.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?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
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
)
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 QuickWidgets` and `qt_add_resources` — covered in [Step 7](#step-7-option-a--qml-loaded-from-c).
---
## Step 4: Interface Header (`src/calc_ui_cpp_interface.h`)
```cpp
#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
```
---
## 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:
```cpp
#ifndef CALC_UI_CPP_PLUGIN_H
#define CALC_UI_CPP_PLUGIN_H
#include <QObject>
#include <QWidget>
#include <QVariantList>
#include "calc_ui_cpp_interface.h"
class LogosAPI;
class CalcUiCppPlugin : public QObject, public CalcUiCppInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID CalcUiCppInterface_iid FILE "metadata.json")
Q_INTERFACES(CalcUiCppInterface PluginInterface)
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 — no `QString`/`int` coercion issues.
### 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` — the `LogosModules` umbrella class with one typed member per dependency
- `calc_module_api.h` / `calc_module_api.cpp` — the per-module wrapper included by `logos_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:
```cpp
m_logos->calc_module.add(3, 5) // typed: int add(int, int) over IPC
```
### `src/calc_backend.h`
```cpp
#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`
```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(...)`:
```qml
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
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
)
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:
```cpp
#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.
```bash
# Run with dev mode enabled
QML_PATH=$PWD/src/qml \
nix run .
```
> **What still requires a rebuild:**
> - Changes to `.cpp` / `.h` files (backend logic, plugin interface)
> - Changes to `CMakeLists.txt` or `metadata.json`
>
> **What does not require a rebuild:**
> - Any `.qml` change — 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`
```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`
Pass `logosStandalone` to `mkLogosModule` and you get `apps.default` (i.e. `nix run`) for free — no manual `apps` block required.
**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`.
```nix
{
description = "Calculator C++ UI plugin for Logos - widget frontend for calc_module";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
logos-standalone-app.url = "github:logos-co/logos-standalone-app";
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module";
};
outputs = inputs@{ logos-module-builder, logos-standalone-app, calc_module, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
logosStandalone = logos-standalone-app;
};
}
```
`logosStandalone` tells `mkLogosModule` to wire up `apps.default` automatically. It stages the compiled plugin alongside `metadata.json` and any icon files into a Nix store directory, then produces a shell script that calls `logos-standalone-app` with that directory — exactly what `nix run` executes.
---
## Step 10: Build and Test
### 10.1 Build
```bash
git add -A
nix build --override-input calc_module path:../logos-calc-module
```
Inspect the output with `lm` (the module inspector from `logos-module`):
```bash
nix build 'github:logos-co/logos-module#cli' --out-link ./lm-cli
./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)
```bash
nix run . --override-input calc_module path:../logos-calc-module
```
The widget opens. No backend connected yet, so button clicks will silently return 0 (CalcBackend logs a warning when `calc_module` is not connected).
> **When do you need `--override-input`?** `calc_module.url` in `flake.nix` points to the published GitHub URL. If your local `logos-calc-module` has unpushed changes or differs from what is on GitHub, you must use `--override-input calc_module path:../logos-calc-module` so nix uses your local copy. If your `calc_module` is already pushed and matches the GitHub URL, you can run `nix build` / `nix run` without the override. This is the same mechanism `ws build --local` / `ws build --auto-local` uses throughout the workspace.
### 10.3 Full functionality (with modules)
The `capability_module` is loaded automatically by the standalone app. You only need to install `calc_module`:
```bash
nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm
mkdir -p modules
# Bundle and install calc_module (from Part 1)
cd ../logos-calc-module
nix build '.#lgx'
cd ../logos-calc-ui-cpp
./pm/bin/lgpm --modules-dir ./modules install --file ../logos-calc-module/result/*.lgx
nix run . --override-input calc_module path:../logos-calc-module -- --modules-dir ./modules
```
---
## Step 11: Load in `logos-basecamp`
### 11.1 Create LGX packages
```bash
# Package calc_module (from Part 1)
cd ../logos-calc-module
nix build '.#lgx-dual'
# Package the C++ UI plugin
cd ../logos-calc-ui-cpp
nix build '.#lgx-dual'
```
> For more bundling options (standalone bundler syntax, cross-platform packaging), see the [Developer Guide — Bundling with nix-bundle-lgx](logos-developer-guide.md#32-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-basecamp` does not accept `--modules-dir` or `--ui-plugins-dir` CLI flags. It manages its own data directory and preinstalls bundled modules (main_ui, package_manager, etc.) on first launch.
```bash
# Build logos-basecamp
nix build 'github:logos-co/logos-basecamp' -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:
```bash
# 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` (substitute `BASECAMP_DIR` with the actual path you found above):
```bash
# Build lgpm CLI
nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm
# Install core module
./pm/bin/lgpm --modules-dir BASECAMP_DIR/modules \
install --file ../logos-calc-module/result/*.lgx
# Install UI plugin
./pm/bin/lgpm --modules-dir BASECAMP_DIR/plugins \
install --file result/*.lgx
# Launch basecamp -- your modules appear alongside the built-in ones
./basecamp-result/bin/logos-basecamp
```
### 11.3 Install via logos-basecamp UI
Instead of using `lgpm` on the command line, you can install modules through the basecamp UI:
1. Launch `logos-basecamp`
2. Go to **Package Manager**
3. Click **Install from file**
4. Select `../logos-calc-module/result/*.lgx` — installs `calc_module`
5. Repeat for `result/*.lgx` — installs `calc_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:
```bash
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:
```bash
# Remove basecamp's data directory (find yours under Application Support or .local/share)
# macOS (typical):
rm -rf ~/Library/Application\ Support/Logos/LogosBasecampDev
# Linux (typical):
# 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`, use `logos-cpp-generator` to generate a typed `CalcModuleClient` class. See [Developer Guide](logos-developer-guide.md) Section 6.2
- **Events** — core modules emit `eventResponse` signals; connect to them from your backend class via `LogosAPIClient`
- **Use the Logos Design System** in Option B QML — `import Logos.Theme` and `import Logos.Controls` are available when running inside `logos-basecamp`