16 KiB
Tutorial Part 2: Building a QML UI for Your Logos Module
This is Part 2 of the Logos module tutorial series. In Part 1 you wrapped a C library as a Logos core module. Now you'll build a QML user interface that calls that module — first isolated with nix run, then packaged and loaded into logos-basecamp.
What you'll build: A calc_ui QML plugin with input fields and buttons that call calc_module methods (add, multiply, factorial, fibonacci) through the Logos bridge.
What you'll learn:
- How QML UI plugins work in the Logos platform
- The
logos.callModule()bridge that connects QML to core modules - The project structure and metadata for a QML plugin
- How to package and install your UI into
logos-basecamp
Prerequisites:
- Completed Part 1 — you have a working
calc_module - Nix with flakes enabled (same as Part 1)
- Basic familiarity with QML (Qt's declarative UI language)
How QML UI Plugins Work
Before writing code, let's understand the architecture:
+-------------------+ logos.callModule() +-------------------+
| calc_ui | --------------------------> | calc_module |
| Main.qml (QML) | IPC (Qt Remote Objects) | C++ plugin |
+-------------------+ +-------------------+
^ ^
└──────────────── loaded by ───────────────────────┘
logos-basecamp / logos-standalone-app
Key points:
- No compilation. A QML plugin is just
.qmlfiles and ametadata.json. - Sandboxed. No network access, no filesystem access outside the module directory.
- The
logosbridge is injected by the host. Call core modules withlogos.callModule("module", "method", [args]). - Entry point is always
Main.qml.
Step 1: Scaffold
Use the QML module template from logos-module-builder:
mkdir logos-calc-ui && cd logos-calc-ui
nix flake init -t github:logos-co/logos-module-builder#ui-qml-module
git init && git add -A
This gives you:
logos-calc-ui/
├── flake.nix # Nix build + nix run support
├── metadata.json # Plugin metadata
└── Main.qml # Your UI (starter template)
Step 2: Update metadata.json
Replace the template contents with your plugin's details:
{
"name": "calc_ui",
"version": "1.0.0",
"description": "Calculator UI - QML frontend for the calc_module",
"type": "ui_qml",
"main": "Main.qml",
"dependencies": ["calc_module"],
"category": "tools",
"icon": "icons/calc.png"
}
The dependencies field tells the host to load calc_module before showing your UI.
Step 3: Write Main.qml
Replace the starter file with the calculator UI:
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"
font.pixelSize: 20
font.weight: Font.DemiBold
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: callTwoOp("add", inputA.text, inputB.text)
}
Button {
text: "Multiply"
onClicked: callTwoOp("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: callOneOp("factorial", inputN.text)
}
Button {
text: "Fibonacci"
onClicked: callOneOp("fibonacci", inputN.text)
}
Button {
text: "libcalc version"
onClicked: callModule("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 }
}
// ── Logos bridge helpers ───────────────────────────────────
function callModule(method, args) {
root.errorText = ""
root.result = ""
if (typeof logos === "undefined" || !logos.callModule) {
root.errorText = "Logos bridge not available"
return
}
root.result = String(logos.callModule("calc_module", method, args))
}
function callTwoOp(method, a, b) {
if (a === "" || b === "") { root.errorText = "Enter values for a and b"; return }
callModule(method, [parseInt(a), parseInt(b)])
}
function callOneOp(method, n) {
if (n === "") { root.errorText = "Enter a value for n"; return }
callModule(method, [parseInt(n)])
}
}
The logos object is injected by the host at runtime. The callModule helper checks for it and routes calls through the IPC bridge to calc_module.
Step 4: Update flake.nix
The template already has logos-standalone-app wired up. The only change needed is to update the description and pname:
{
description = "Calculator QML UI Plugin for Logos - frontend for calc_module";
inputs = {
logos-nix.url = "github:logos-co/logos-nix";
nixpkgs.follows = "logos-nix/nixpkgs";
logos-standalone-app.url = "github:logos-co/logos-standalone-app";
logos-standalone-app.inputs.logos-liblogos.inputs.nixpkgs.follows =
"logos-nix/nixpkgs";
};
outputs = { self, nixpkgs, logos-standalone-app, ... }:
let
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
pkgs = import nixpkgs { inherit system; };
});
in {
packages = forAllSystems ({ pkgs }: let
plugin = pkgs.stdenv.mkDerivation {
pname = "logos-calc-ui-plugin";
version = "1.0.0";
src = ./.;
phases = [ "unpackPhase" "installPhase" ];
installPhase = ''
mkdir -p $out/lib/icons
cp $src/Main.qml $out/lib/Main.qml
cp $src/metadata.json $out/lib/metadata.json
if [ -f "$src/icons/calc.png" ]; then
cp $src/icons/calc.png $out/lib/icons/calc.png
fi
'';
};
in { default = plugin; lib = plugin; });
apps = forAllSystems ({ pkgs }:
let
standalone = logos-standalone-app.packages.${pkgs.system}.default;
plugin = self.packages.${pkgs.system}.default;
run = pkgs.writeShellScript "run-calc-ui-standalone" ''
exec ${standalone}/bin/logos-standalone "${plugin}/lib" "$@"
'';
in { default = { type = "app"; program = "${run}"; }; }
);
};
}
The installPhase copies only Main.qml and metadata.json to $out/lib/. The apps output wires up logos-standalone-app so nix run . works.
Step 5: Test with nix run
5.1 UI only (layout preview)
git add -A
nix run .
The app opens immediately. No modules are loaded, so clicking buttons shows "Logos bridge not available" — but you can verify the layout and styling look correct.
5.2 Full functionality (with modules)
To test actual calls to calc_module, you need a modules directory with capability_module (required by all UI plugins) and calc_module installed via lgpm. Do this once:
nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm
mkdir -p modules
# 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 (from Part 1)
cd ../logos-calc-module
nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.#lib' -o lgx-result
cd ../logos-calc-ui
./pm/bin/lgpm --modules-dir ./modules install --file ../logos-calc-module/lgx-result/*.lgx
Then run with the modules directory:
nix run . -- --modules-dir ./modules
Clicking Add, Multiply, Factorial, or Fibonacci now calls the real module.
Step 6: Using the Logos Design System
logos-basecamp has logos-design-system on its QML import path. You can use its themed components directly without any extra setup in your module.
import Logos.Theme 1.0
import Logos.Controls 1.0
Replace the plain Button and TextField with the styled equivalents:
// Instead of Button:
LogosButton {
text: "Add"
onClicked: callTwoOp("add", inputA.text, inputB.text)
}
// Instead of TextField:
LogosTextField {
id: inputA
placeholderText: "a"
}
// Use theme colors instead of hardcoded hex values:
Rectangle {
color: Theme.palette.backgroundSecondary
// ...
Text { color: Theme.palette.text }
}
Available components: LogosButton, LogosTextField, LogosText, LogosTabButton.
Available theme tokens via Theme.palette:
- Colors:
background,backgroundSecondary,backgroundMuted,text,textMuted,border,overlayOrange - Spacing:
Theme.spacing.radiusSmall,Theme.spacing.radiusXlarge - Typography:
Theme.typography.secondaryText,Theme.typography.weightMedium
Note:
Logos.ThemeandLogos.Controlsare only available when running insidelogos-basecamp. They are not available inlogos-standalone-app. Use them only if you know your module will run in basecamp, or guard the import.
Step 7: Load in logos-basecamp
7.1 Create LGX packages
Bundle both modules as portable LGX files:
# 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
# Package the QML UI plugin
nix bundle --bundler 'github:logos-co/nix-bundle-lgx#portable' '.' -o lgx-calc-ui
7.2 Install via logos-basecamp UI
- Open
logos-basecamp - Go to Package Manager
- Click Install from file
- Select
lgx-calc-module/*.lgx— installscalc_module - Repeat for
lgx-calc-ui/*.lgx— installscalc_ui
The "Calculator UI" tab appears in the sidebar. Clicking it loads your Main.qml.
7.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/*.lgx
7.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
./pm/bin/lgpm --modules-dir ./modules install --file ../logos-calc-module/lgx-calc-module-local/*.lgx
# Bundle and install the QML UI plugin
nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.' -o lgx-calc-ui-local
./pm/bin/lgpm --modules-dir ./ui-plugins install --file lgx-calc-ui-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(vianix build) expects local.lgxpackages (built without#portable). Portable builds (AppImage, macOS app bundle) expect portable.lgxpackages.
7.5 Live reloading
For rapid iteration on QML, use development mode. This watches your source files and reloads on change:
QML_UI=$(pwd) logos-basecamp
Edit Main.qml, save, and the UI updates without rebuilding.
7.6 Testing without logos-basecamp
You can open Main.qml in any QML viewer (e.g., qml from Qt) to test the layout. The logos bridge won't be available, so clicking buttons will show "Logos bridge not available" — but you can verify the layout and styling work correctly.
# If you have Qt installed
qml Main.qml
Step 8: Package for Distribution (Optional)
The LGX packages created in Step 7.2 are local packages — they contain /nix/store references and work on the machine that built them. To create portable packages for distribution to other machines, use the #portable bundler:
# Portable core module
cd ../logos-calc-module
nix bundle --bundler 'github:logos-co/nix-bundle-lgx#portable' '.#lib' -o lgx-portable
# Portable QML UI plugin
cd ../logos-calc-ui
nix bundle --bundler 'github:logos-co/nix-bundle-lgx#portable' '.' -o lgx-portable
Portable LGX packages are fully self-contained and can be installed on any machine with the Logos Package Manager:
nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm
./pm/bin/lgpm --modules-dir ./modules install --file calc_module.lgx
./pm/bin/lgpm --modules-dir ./ui-plugins install --file calc_ui.lgx
Local vs portable: Local builds of
logos-basecamp(vianix build) expect local.lgxpackages. Portable builds (AppImage, macOS app bundle) expect portable.lgxpackages. See the logos-basecamp README for details.
Recap
| Core Module (Part 1) | QML UI Plugin (Part 2) | |
|---|---|---|
| Language | C++ | QML / JavaScript |
| Files | .cpp, .h, CMakeLists.txt, module.yaml |
Main.qml, metadata.json |
| Compilation | Yes (CMake → .so) |
No (file copy) |
metadata.type |
"core" |
"ui_qml" |
| Test command | logoscore -m ./result/lib -l calc_module |
nix run . |
| Calls other modules | Via LogosAPI* (C++) |
Via logos.callModule() (JS) |
What's Next
- Add more methods to
calc_moduleand call them from QML - Use Logos Design System styled components for consistent look and feel
- Build a C++ UI module for cases where QML sandboxing is too restrictive — see Developer Guide, Section 7.2