2026-03-18 14:23:44 +01:00
# Tutorial Part 2: Building a QML UI for Your Logos Module
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
This is Part 2 of the Logos module tutorial series. In [Part 1 ](tutorial-wrapping-c-library.md ) 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` .
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
**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.
2026-03-06 15:55:00 +00:00
**What you'll learn:**
2026-03-06 16:45:49 +00:00
2026-03-06 15:55:00 +00:00
- 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
2026-03-18 14:23:44 +01:00
- How to package and install your UI into `logos-basecamp`
2026-03-06 15:55:00 +00:00
**Prerequisites:**
2026-03-06 16:45:49 +00:00
2026-03-06 15:55:00 +00:00
- Completed [Part 1 ](tutorial-wrapping-c-library.md ) — 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:
```
2026-03-18 14:23:44 +01:00
+-------------------+ logos.callModule() +-------------------+
| calc_ui | --------------------------> | calc_module |
| Main.qml (QML) | IPC (Qt Remote Objects) | C++ plugin |
+-------------------+ +-------------------+
^ ^
└──────────────── loaded by ───────────────────────┘
logos-basecamp / logos-standalone-app
2026-03-06 15:55:00 +00:00
```
Key points:
2026-03-18 14:23:44 +01:00
- **No compilation.** A QML plugin is just `.qml` files and a `metadata.json` .
- **Sandboxed.** No network access, no filesystem access outside the module directory.
- **The `logos` bridge** is injected by the host. Call core modules with `logos.callModule("module", "method", [args])` .
- **Entry point** is always `Main.qml` .
2026-03-06 15:55:00 +00:00
---
2026-03-18 14:23:44 +01:00
## Step 1: Scaffold
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
Use the QML module template from `logos-module-builder` :
2026-03-06 15:55:00 +00:00
```bash
mkdir logos-calc-ui && cd logos-calc-ui
2026-03-18 14:23:44 +01:00
nix flake init -t github:logos-co/logos-module-builder#ui-qml-module
git init && git add -A
2026-03-06 15:55:00 +00:00
```
2026-03-18 14:23:44 +01:00
This gives you:
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
```
logos-calc-ui/
├── flake.nix # Nix build + nix run support
├── metadata.json # Plugin metadata
└── Main.qml # Your UI (starter template)
```
2026-03-06 15:55:00 +00:00
---
2026-03-18 14:23:44 +01:00
## Step 2: Update `metadata.json`
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
Replace the template contents with your plugin's details:
2026-03-06 15:55:00 +00:00
```json
{
"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"
}
```
2026-03-18 14:23:44 +01:00
The `dependencies` field tells the host to load `calc_module` before showing your UI.
2026-03-06 15:55:00 +00:00
---
## Step 3: Write `Main.qml`
2026-03-18 14:23:44 +01:00
Replace the starter file with the calculator UI:
2026-03-06 15:55:00 +00:00
```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"
font.pixelSize: 20
font.weight: Font . DemiBold
2026-03-18 14:23:44 +01:00
color: "#ffffff"
2026-03-06 15:55:00 +00:00
Layout.alignment: Qt . AlignHCenter
}
2026-03-18 14:23:44 +01:00
// ── Two-operand operations ─────────────────────────────
RowLayout {
spacing: 12
2026-03-06 15:55:00 +00:00
Layout.fillWidth: true
2026-03-18 14:23:44 +01:00
TextField {
id: inputA
placeholderText: "a"
Layout.preferredWidth: 80
validator: IntValidator {}
}
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
TextField {
id: inputB
placeholderText: "b"
Layout.preferredWidth: 80
validator: IntValidator {}
}
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
Button {
text: "Add"
onClicked: callTwoOp ( "add" , inputA . text , inputB . text )
}
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
Button {
text: "Multiply"
onClicked: callTwoOp ( "multiply" , inputA . text , inputB . text )
2026-03-06 15:55:00 +00:00
}
}
2026-03-18 14:23:44 +01:00
// ── Single-operand operations ──────────────────────────
RowLayout {
spacing: 12
2026-03-06 15:55:00 +00:00
Layout.fillWidth: true
2026-03-18 14:23:44 +01:00
TextField {
id: inputN
placeholderText: "n"
Layout.preferredWidth: 80
validator: IntValidator { bottom: 0 }
2026-03-06 15:55:00 +00:00
}
2026-03-18 14:23:44 +01:00
Button {
text: "Factorial"
onClicked: callOneOp ( "factorial" , inputN . text )
2026-03-06 15:55:00 +00:00
}
2026-03-18 14:23:44 +01:00
Button {
text: "Fibonacci"
onClicked: callOneOp ( "fibonacci" , inputN . text )
}
Button {
text: "libcalc version"
onClicked: callModule ( "libVersion" , [])
2026-03-06 15:55:00 +00:00
}
}
// ── Result display ─────────────────────────────────────
Rectangle {
Layout.fillWidth: true
2026-03-18 14:23:44 +01:00
height: 56
color: root . errorText . length > 0 ? "#3d1a1a" : "#1a2d1a"
2026-03-06 15:55:00 +00:00
radius: 8
2026-03-18 14:23:44 +01: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-03-06 15:55:00 +00:00
}
}
Item { Layout.fillHeight: true }
}
2026-03-18 14:23:44 +01:00
// ── Logos bridge helpers ───────────────────────────────────
2026-03-06 15:55:00 +00:00
function callModule ( method , args ) {
root . errorText = ""
root . result = ""
if ( typeof logos === "undefined" || ! logos . callModule ) {
2026-03-18 14:23:44 +01:00
root . errorText = "Logos bridge not available"
2026-03-06 15:55:00 +00:00
return
}
2026-03-18 14:23:44 +01:00
root . result = String ( logos . callModule ( "calc_module" , method , args ))
2026-03-06 15:55:00 +00:00
}
function callTwoOp ( method , a , b ) {
2026-03-18 14:23:44 +01:00
if ( a === "" || b === "" ) { root . errorText = "Enter values for a and b" ; return }
2026-03-06 15:55:00 +00:00
callModule ( method , [ parseInt ( a ), parseInt ( b )])
}
function callOneOp ( method , n ) {
2026-03-18 14:23:44 +01:00
if ( n === "" ) { root . errorText = "Enter a value for n" ; return }
2026-03-06 15:55:00 +00:00
callModule ( method , [ parseInt ( n )])
}
}
```
2026-03-18 14:23:44 +01:00
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` .
2026-03-06 15:55:00 +00:00
---
2026-03-18 14:23:44 +01:00
## Step 4: Update `flake.nix`
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
The template already has `logos-standalone-app` wired up. The only change needed is to update the description and pname:
2026-03-06 15:55:00 +00:00
```nix
{
description = "Calculator QML UI Plugin for Logos - frontend for calc_module" ;
inputs = {
2026-03-20 20:07:17 +01:00
logos-nix . url = "github:logos-co/logos-nix" ;
nixpkgs . follows = "logos-nix/nixpkgs" ;
2026-03-18 14:23:44 +01:00
logos-standalone-app . url = "github:logos-co/logos-standalone-app" ;
logos-standalone-app . inputs . logos-liblogos . inputs . nixpkgs . follows =
2026-03-20 20:07:17 +01:00
"logos-nix/nixpkgs" ;
2026-03-06 15:55:00 +00:00
};
2026-03-20 20:07:17 +01:00
outputs = { self , nixpkgs , logos-standalone-app , ... }:
2026-03-06 15:55:00 +00:00
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 ; };
});
2026-03-18 14:23:44 +01:00
in {
2026-03-06 15:55:00 +00:00
packages = forAllSystems ({ pkgs }: let
plugin = pkgs . stdenv . mkDerivation {
pname = "logos-calc-ui-plugin" ;
version = "1.0.0" ;
src = ./. ;
phases = [ "unpackPhase" "installPhase" ];
installPhase = ''
2026-03-18 14:23:44 +01:00
mkdir -p $out/lib/icons
cp $src/Main.qml $out/lib/Main.qml
cp $src/metadata.json $out/lib/metadata.json
2026-03-06 15:55:00 +00:00
if [ -f "$src/icons/calc.png" ]; then
2026-03-18 14:23:44 +01:00
cp $src/icons/calc.png $out/lib/icons/calc.png
2026-03-06 15:55:00 +00:00
fi
'' ;
};
2026-03-18 14:23:44 +01:00
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 } " ; }; }
);
2026-03-06 15:55:00 +00:00
};
}
```
2026-03-18 14:23:44 +01:00
The `installPhase` copies only `Main.qml` and `metadata.json` to `$out/lib/` . The `apps` output wires up `logos-standalone-app` so `nix run .` works.
2026-03-06 15:55:00 +00:00
---
2026-03-18 14:23:44 +01:00
## Step 5: Test with `nix run`
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
### 5.1 UI only (layout preview)
2026-03-06 15:55:00 +00:00
```bash
git add -A
2026-03-18 14:23:44 +01:00
nix run .
2026-03-06 15:55:00 +00:00
```
2026-03-18 14:23:44 +01:00
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:
2026-03-06 15:55:00 +00:00
```bash
2026-03-18 14:23:44 +01:00
nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm
mkdir -p modules
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
# 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
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
# Bundle and install calc_module (from Part 1)
2026-03-07 15:04:37 +00:00
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
2026-03-06 15:55:00 +00:00
```
2026-03-18 14:23:44 +01:00
Then run with the modules directory:
2026-03-06 15:55:00 +00:00
```bash
2026-03-18 14:23:44 +01:00
nix run . -- --modules-dir ./modules
2026-03-06 15:55:00 +00:00
```
2026-03-18 14:23:44 +01:00
Clicking **Add** , **Multiply** , **Factorial** , or **Fibonacci** now calls the real module.
2026-03-06 15:55:00 +00:00
---
2026-03-18 14:23:44 +01:00
## Step 6: Using the Logos Design System
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
`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.
2026-03-06 15:55:00 +00:00
2026-03-18 14:23:44 +01:00
```qml
import Logos . Theme 1.0
import Logos . Controls 1.0
```
Replace the plain `Button` and `TextField` with the styled equivalents:
```qml
// 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.Theme` and `Logos.Controls` are only available when running inside `logos-basecamp`. They are not available in `logos-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:
2026-03-06 15:55:00 +00:00
```bash
2026-03-18 14:23:44 +01:00
# 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
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/*.lgx` — installs `calc_ui`
The "Calculator UI" tab appears in the sidebar. Clicking it loads your `Main.qml` .
### 7.3 Install via CLI (alternative)
```bash
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:
```bash
# 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 \
2026-03-06 15:55:00 +00:00
--modules-dir ./modules \
--ui-plugins-dir ./ui-plugins
```
2026-03-18 14:23:44 +01:00
> **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.
### 7.5 Live reloading
For rapid iteration on QML, use development mode. This watches your source files and reloads on change:
```bash
QML_UI = $( pwd ) logos-basecamp
```
2026-03-06 15:55:00 +00:00
Edit `Main.qml` , save, and the UI updates without rebuilding.
2026-03-18 14:23:44 +01:00
### 7.6 Testing without `logos-basecamp`
2026-03-06 15:55:00 +00:00
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.
```bash
# If you have Qt installed
qml Main.qml
```
---
## Step 8: Package for Distribution (Optional)
2026-03-18 14:23:44 +01:00
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:
2026-03-06 15:55:00 +00:00
```bash
2026-03-07 15:04:37 +00:00
# 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
2026-03-06 15:55:00 +00:00
```
2026-03-07 15:04:37 +00:00
Portable LGX packages are fully self-contained and can be installed on any machine with the Logos Package Manager:
2026-03-06 15:55:00 +00:00
```bash
nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm
2026-03-07 15:04:37 +00:00
./pm/bin/lgpm --modules-dir ./modules install --file calc_module.lgx
2026-03-06 15:55:00 +00:00
./pm/bin/lgpm --modules-dir ./ui-plugins install --file calc_ui.lgx
```
2026-03-18 14:23:44 +01:00
> **Local vs portable:** Local builds of `logos-basecamp` (via `nix build`) expect **local** `.lgx` packages. Portable builds (AppImage, macOS app bundle) expect **portable** `.lgx` packages. See the [logos-basecamp README](https://github.com/logos-co/logos-basecamp/blob/master/README.md) for details.
2026-03-07 15:04:37 +00:00
2026-03-06 15:55:00 +00:00
---
2026-03-18 14:23:44 +01:00
## Recap
2026-03-06 16:45:49 +00:00
2026-03-18 14:23:44 +01:00
| | 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) |
2026-03-06 15:55:00 +00:00
---
## What's Next
2026-03-18 14:23:44 +01:00
- **Add more methods** to `calc_module` and 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 ](logos-developer-guide.md ), Section 7.2