Files
logos-tutorial/tutorial-cpp-ui-app.md

26 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 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 — 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

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
├── module.yaml
├── 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:

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: module.yaml

name: calc_ui_cpp
version: 1.0.0
type: ui
category: tools
description: "Calculator C++ UI — widget frontend for calc_module"

dependencies:
  - calc_module

nix_packages:
  build: []
  runtime: []

external_libraries: []

cmake:
  find_packages: []
  extra_sources: []
  proto_files: []

Step 3: metadata.json

{
  "name": "calc_ui_cpp",
  "version": "1.0.0",
  "description": "Calculator C++ UI — widget frontend for calc_module",
  "type": "ui",
  "main": "calc_ui_cpp_plugin",
  "dependencies": ["calc_module"],
  "category": "tools"
}

Step 4: 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
)

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 5: Interface Header (src/calc_ui_cpp_interface.h)

#ifndef CALC_UI_CPP_INTERFACE_H
#define CALC_UI_CPP_INTERFACE_H

#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 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 module.yaml — 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 module.yaml declares dependencies: [calc_module] and calc_module is passed as a flake input via moduleInputs, 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:

m_logos->calc_module.add(3, 5)   // typed: int add(int, int) over IPC

This is the same pattern used in production modules such as logos-storage-ui.

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 module.yaml 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
)

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 . --override-input calc_module path:../logos-calc-module -- \
  --modules-dir ./modules

What still requires a rebuild:

  • Changes to .cpp / .h files (backend logic, plugin interface)
  • Changes to CMakeLists.txt or module.yaml

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.

src/calc_ui_cpp_plugin.h

#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

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

Same pattern as Part 2 — mkLogosModule for the build, apps merged on top for nix run.

Important — moduleInputs: Because module.yaml 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 moduleInputs provides. 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";
    nixpkgs.follows = "logos-module-builder/nixpkgs";

    logos-standalone-app.url = "github:logos-co/logos-standalone-app";
    logos-standalone-app.inputs.logos-liblogos.inputs.nixpkgs.follows =
      "logos-module-builder/nixpkgs";

    calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module";
  };

  outputs = { self, logos-module-builder, logos-standalone-app, nixpkgs, calc_module }:
    let
      systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
      forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
      moduleOutputs = logos-module-builder.lib.mkLogosModule {
        src = ./.;
        configFile = ./module.yaml;
        moduleInputs = { inherit calc_module; };
      };
    in
      moduleOutputs // {
        apps = forAllSystems (system:
          let
            pkgs = import nixpkgs { inherit system; };
            standalone = logos-standalone-app.packages.${system}.default;
            plugin = moduleOutputs.packages.${system}.default;
            pluginDir = pkgs.runCommand "calc-ui-cpp-plugin-dir" {} ''
              mkdir -p $out
              cp ${plugin}/lib/*_plugin.*  $out/
              cp ${./metadata.json} $out/metadata.json
            '';
            run = pkgs.writeShellScript "run-calc-ui-cpp-standalone" ''
              exec ${standalone}/bin/logos-standalone-app "${pluginDir}" "$@"
            '';
          in { default = { type = "app"; program = "${run}"; }; }
        );
      };
}

Step 10: Build and Test

10.1 Build

git add -A
nix build --override-input calc_module path:../logos-calc-module

Inspect the output:

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 . --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).

Why --override-input? calc_module.url in flake.nix points to the published GitHub URL. For local development, --override-input redirects it to the local sibling directory. This is the same mechanism ws build --local / ws build --auto-local uses throughout the workspace.

10.3 Full functionality (with modules)

nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm
mkdir -p modules

# Install capability_module
nix bundle --bundler 'github:logos-co/nix-bundle-lgx' \
  'github:logos-co/logos-capability-module' -o lgx-capability
./pm/bin/lgpm --modules-dir ./modules install --file lgx-capability/*.lgx

# Bundle and install calc_module (from Part 1)
cd ../logos-calc-module
nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.#lib' -o lgx-result
cd ../logos-calc-ui-cpp
./pm/bin/lgpm --modules-dir ./modules install --file ../logos-calc-module/lgx-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

# Package calc_module (from Part 1)
cd ../logos-calc-module
nix bundle --bundler 'github:logos-co/nix-bundle-lgx#portable' '.#lib' -o lgx-calc-module
cd ../logos-calc-ui-cpp

# Package the C++ UI plugin
nix bundle --bundler 'github:logos-co/nix-bundle-lgx#portable' '.' -o lgx-calc-ui-cpp

11.2 Install via logos-basecamp UI

  1. Open logos-basecamp
  2. Go to Package Manager
  3. Click Install from file
  4. Select lgx-calc-module/*.lgx — installs calc_module
  5. Repeat for lgx-calc-ui-cpp/*.lgx — installs calc_ui_cpp

The "Calculator" tab appears in the sidebar.

11.3 Install via CLI (alternative)

nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm
./pm/bin/lgpm install --file lgx-calc-module/*.lgx
./pm/bin/lgpm install --file lgx-calc-ui-cpp/*.lgx

11.4 Build logos-basecamp from source

Build a local logos-basecamp binary, then use lgpm to populate a modules directory and run it:

# Build logos-basecamp
nix build 'github:logos-co/logos-basecamp' -o basecamp-result

# Create module directories
mkdir -p modules ui-plugins

# Build lgpm CLI
nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm

# Install capability_module (required by all UI plugins)
nix bundle --bundler 'github:logos-co/nix-bundle-lgx' \
  'github:logos-co/logos-capability-module' -o lgx-capability
./pm/bin/lgpm --modules-dir ./modules install --file lgx-capability/*.lgx

# Bundle and install calc_module (local, not portable)
cd ../logos-calc-module
nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.#lib' -o lgx-calc-module-local
cd ../logos-calc-ui-cpp
./pm/bin/lgpm --modules-dir ./modules install --file ../logos-calc-module/lgx-calc-module-local/*.lgx

# Bundle and install the C++ UI plugin
nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.' -o lgx-calc-ui-cpp-local
./pm/bin/lgpm --modules-dir ./ui-plugins install --file lgx-calc-ui-cpp-local/*.lgx

# Run basecamp with the populated directories
./basecamp-result/bin/logos-basecamp \
  --modules-dir ./modules \
  --ui-plugins-dir ./ui-plugins

Local vs portable: A locally-built logos-basecamp (via nix build) expects local .lgx packages (built without #portable). Portable builds (AppImage, macOS app bundle) expect portable .lgx packages.


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
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 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