mirror of
https://github.com/logos-co/logos-tutorial.git
synced 2026-08-31 04:41:08 +00:00
Merge pull request #50 from logos-co/executable_tutorials
add executable tutorials support; convert tutorial-wrapping-c-library to an executable tutorial
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
name: Tutorial Tests
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# One-time setup required for the clickable report links to work:
|
||||
#
|
||||
# 1. Repo Settings → Pages → "Build and deployment" → Source: "Deploy from a
|
||||
# branch", Branch: `gh-pages` / `(root)`. (The publish-report job creates
|
||||
# the gh-pages branch on its first run.)
|
||||
# 2. Nothing else — GITHUB_TOKEN already has the permissions granted below.
|
||||
#
|
||||
# Each run publishes the two-column HTML report to:
|
||||
# https://<owner>.github.io/<repo>/pr-<N>/<os>/ (pull requests)
|
||||
# https://<owner>.github.io/<repo>/main/<os>/ (pushes to main/master)
|
||||
# and (for PRs) posts/updates a comment with the links.
|
||||
#
|
||||
# Note: pull requests opened from forks get a read-only GITHUB_TOKEN, so the
|
||||
# Pages push and PR comment are skipped for them — the downloadable artifact is
|
||||
# still produced. PRs from branches in this repo get the full clickable links.
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
push:
|
||||
branches: [master, main]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tutorial-tests:
|
||||
name: Tutorial Tests (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Setup Cachix
|
||||
uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: logos-co
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
|
||||
- name: Install PyYAML
|
||||
run: pip3 install --break-system-packages pyyaml
|
||||
|
||||
- name: Test - QML UI App (includes C Library tutorial via requires)
|
||||
run: |
|
||||
# --continue-on-fail so the run walks the whole Part 1 -> 2 -> 3 chain
|
||||
# and the published report is complete. The job still fails (non-zero
|
||||
# exit) if any step failed; this only changes whether we stop early.
|
||||
python3 tools/tutorial_runner.py run \
|
||||
tests/tutorial-cpp-ui-app.test.yaml \
|
||||
--verbose \
|
||||
--continue-on-fail \
|
||||
--report "${{ runner.temp }}/tutorial-report.html"
|
||||
|
||||
- name: Stage report for upload
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p report-out
|
||||
# Name it index.html so the published directory URL renders directly.
|
||||
if [ -f "${{ runner.temp }}/tutorial-report.html" ]; then
|
||||
cp "${{ runner.temp }}/tutorial-report.html" report-out/index.html
|
||||
else
|
||||
echo "<h1>No report produced</h1>" > report-out/index.html
|
||||
fi
|
||||
|
||||
- name: Upload tutorial execution report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tutorial-report-${{ matrix.os }}
|
||||
path: report-out/index.html
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Verify markdown generation
|
||||
run: |
|
||||
python3 tools/tutorial_runner.py generate \
|
||||
tests/tutorial-wrapping-c-library.test.yaml \
|
||||
-o /tmp/gen-part1.md
|
||||
python3 tools/tutorial_runner.py generate \
|
||||
tests/tutorial-qml-ui-app.test.yaml \
|
||||
-o /tmp/gen-part2.md
|
||||
echo "Generated markdown successfully"
|
||||
|
||||
publish-report:
|
||||
name: Publish report to GitHub Pages
|
||||
needs: tutorial-tests
|
||||
# Run even when tests fail — a failing run is exactly when you want to open
|
||||
# the report. Skip on forks, where GITHUB_TOKEN can't push or comment.
|
||||
if: ${{ always() && github.event.pull_request.head.repo.fork != true }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write # push to the gh-pages branch
|
||||
pull-requests: write # post/update the PR comment
|
||||
|
||||
# Serialize Pages pushes so two refs can't race on the gh-pages branch.
|
||||
concurrency:
|
||||
group: gh-pages-publish
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- name: Download all reports
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
# No `name:` → downloads every artifact into artifacts/<name>/...
|
||||
|
||||
- name: Arrange site directory
|
||||
id: arrange
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
BASE="pr-${{ github.event.pull_request.number }}"
|
||||
else
|
||||
BASE="main"
|
||||
fi
|
||||
echo "base=$BASE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
mkdir -p "site/$BASE"
|
||||
found=""
|
||||
for os in ubuntu-latest macos-latest; do
|
||||
src="artifacts/tutorial-report-$os/index.html"
|
||||
if [ -f "$src" ]; then
|
||||
mkdir -p "site/$BASE/$os"
|
||||
cp "$src" "site/$BASE/$os/index.html"
|
||||
found="$found $os"
|
||||
fi
|
||||
done
|
||||
echo "found=$found" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Landing page for this ref linking to each OS report.
|
||||
{
|
||||
echo "<!doctype html><meta charset=utf-8>"
|
||||
echo "<title>Tutorial reports — $BASE</title>"
|
||||
echo "<style>body{font:16px system-ui;margin:40px;max-width:640px}a{color:#2563eb}</style>"
|
||||
echo "<h1>Tutorial execution reports</h1>"
|
||||
echo "<p><strong>$BASE</strong> · commit <code>${GITHUB_SHA::7}</code></p><ul>"
|
||||
for os in ubuntu-latest macos-latest; do
|
||||
if [ -d "site/$BASE/$os" ]; then
|
||||
echo "<li><a href=\"./$os/\">$os</a></li>"
|
||||
fi
|
||||
done
|
||||
echo "</ul>"
|
||||
} > "site/$BASE/index.html"
|
||||
|
||||
- name: Deploy to gh-pages
|
||||
if: steps.arrange.outputs.found != ''
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./site
|
||||
keep_files: true # don't wipe other PRs' directories
|
||||
commit_message: "Publish tutorial report for ${{ steps.arrange.outputs.base }} (${{ github.sha }})"
|
||||
|
||||
- name: Comment on PR with report links
|
||||
if: ${{ github.event_name == 'pull_request' && steps.arrange.outputs.found != '' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const base = "${{ steps.arrange.outputs.base }}";
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const root = `https://${owner}.github.io/${repo}/${base}`;
|
||||
const oses = "${{ steps.arrange.outputs.found }}".trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
const links = oses.map(os => `- [\`${os}\` report](${root}/${os}/)`).join("\n");
|
||||
const marker = "<!-- tutorial-report-links -->";
|
||||
const body =
|
||||
`${marker}\n` +
|
||||
`### 📊 Tutorial execution report\n\n` +
|
||||
`Rendered tutorial alongside the commands actually run and their output ` +
|
||||
`(updated each run, commit \`${context.sha.slice(0,7)}\`):\n\n` +
|
||||
`${links}\n\n` +
|
||||
`_Pages can take a minute to update after the run finishes._`;
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner, repo, issue_number: context.issue.number, per_page: 100,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: context.issue.number, body });
|
||||
}
|
||||
@@ -20,6 +20,35 @@ Step-by-step tutorials that build on each other. Each creates a working module y
|
||||
|
||||
- **logos-dev-boost:** [Scaffolding Modules with logos-dev-boost](tutorial-dev-boost.md) — use the `logos-dev-boost` CLI to auto-generate modules from C library directories. Wraps libcalc (source-only) and sqlcipher (pre-built `.so`), including integration tests that create encrypted databases. Covers `--type module`, `--type full-app`, and `--lib-dir`.
|
||||
|
||||
## Executable Tutorials
|
||||
|
||||
Tutorials have YAML specs in `tests/` that can be both **executed** (to verify they work) and used to **generate** the `.md` files. See [docs/spec.md](docs/spec.md) for the full format reference.
|
||||
|
||||
```bash
|
||||
# Run a tutorial end-to-end, writing to a directory you can inspect afterwards
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-wrapping-c-library.test.yaml \
|
||||
--workdir /tmp/my-tutorial-test --verbose
|
||||
|
||||
# Run only specific phases (scaffold, files, build, inspect, logoscore)
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-wrapping-c-library.test.yaml \
|
||||
--phase scaffold,files,build --verbose
|
||||
|
||||
# Re-run a single phase against a previous build
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-wrapping-c-library.test.yaml \
|
||||
--workdir /tmp/my-tutorial-test --phase inspect --verbose
|
||||
|
||||
# Generate the .md tutorial from the YAML spec
|
||||
python3 tools/tutorial_runner.py generate tests/tutorial-wrapping-c-library.test.yaml
|
||||
|
||||
# Pin all GitHub URLs to a specific release tag
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-wrapping-c-library.test.yaml --release tutorial-v2
|
||||
python3 tools/tutorial_runner.py generate tests/tutorial-wrapping-c-library.test.yaml --release tutorial-v2
|
||||
```
|
||||
|
||||
The `--workdir` flag lets you point the runner at a directory of your choice — all files, builds, and artifacts end up there so you can inspect or re-use them. Without it, a temp directory is created and deleted after the run (use `--keep-workdir` to preserve it).
|
||||
|
||||
The `--release` flag (or the `release` field in the YAML) pins all `{release}` placeholders in GitHub URLs to a git tag, so `github:logos-co/repo{release}#output` becomes `github:logos-co/repo/tutorial-v2#output`. Set it to `""` or omit it for latest.
|
||||
|
||||
## Example Modules
|
||||
|
||||
Working module source code used by the tutorials:
|
||||
|
||||
+471
@@ -0,0 +1,471 @@
|
||||
# Tutorial YAML Spec Format
|
||||
|
||||
This document describes the YAML format used by `tools/tutorial_runner.py` to define executable tutorials. Each `.test.yaml` file is the **single source of truth** — it drives both:
|
||||
|
||||
- **Execution** (`run`): steps are executed in a temp directory, commands run, outputs verified
|
||||
- **Markdown generation** (`generate`): a `.md` tutorial is produced from the same YAML
|
||||
|
||||
## Quick example
|
||||
|
||||
```yaml
|
||||
name: "My Tutorial"
|
||||
output: my-tutorial.md
|
||||
release: ""
|
||||
|
||||
intro: |
|
||||
One-paragraph description of what this tutorial covers.
|
||||
|
||||
what_you_build: "A short sentence describing the end result."
|
||||
|
||||
what_you_learn:
|
||||
- First learning objective
|
||||
- Second learning objective
|
||||
|
||||
prerequisites:
|
||||
- "**Nix** with flakes enabled."
|
||||
|
||||
sections:
|
||||
- title: "Set Up the Project"
|
||||
phase: scaffold
|
||||
text: |
|
||||
Intro paragraph for this section.
|
||||
steps:
|
||||
- title: "Create the directory"
|
||||
run: "mkdir -p my-project"
|
||||
|
||||
- title: "Write the config"
|
||||
text: "Create `config.json`:"
|
||||
file:
|
||||
path: config.json
|
||||
language: json
|
||||
content: |
|
||||
{ "name": "example" }
|
||||
|
||||
- title: "Build"
|
||||
run: "nix build"
|
||||
expect_contains:
|
||||
- "Build successful"
|
||||
```
|
||||
|
||||
## Top-level fields
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `name` | yes | string | Tutorial title. Used as the `# heading` in generated markdown and in runner output. |
|
||||
| `output` | no | string | Default output filename for `generate` (relative to the tutorial directory, e.g., `tutorial-wrapping-c-library.md`). Can be overridden with `-o`. |
|
||||
| `project_name` | no | string | Directory name for this tutorial's project (e.g., `logos-calc-module`). Used when chaining tutorials via `requires:` — each tutorial runs in a subdirectory of a shared parent. Ignored when running standalone. |
|
||||
| `requires` | no | list of strings | Paths to prerequisite `.test.yaml` specs (relative to the current spec). The runner executes each prerequisite first in a sibling subdirectory (named by its `project_name`), then runs the current tutorial. Prerequisites are resolved **transitively** — if A requires B and B requires C, the run order is C, B, A. Shared prerequisites run once (deduped), and circular `requires:` are reported as an error. This enables cross-tutorial references like `../logos-calc-module`. Requires `project_name` on both the current and prerequisite specs. |
|
||||
| `intro` | no | string | Introductory paragraph(s). Rendered after the title in the markdown. Supports full markdown. |
|
||||
| `what_you_build` | no | string | One-line summary prefixed with "**What you'll build:**" in the markdown. |
|
||||
| `what_you_learn` | no | list of strings | Bullet list prefixed with "**What you'll learn:**". |
|
||||
| `comparison` | no | string | Free-form markdown block rendered after the learning objectives (useful for comparison tables). |
|
||||
| `prerequisites` | no | list of strings | Rendered as a bullet list under a prerequisites heading. Each item can contain markdown (code blocks, links, etc.). |
|
||||
| `release` | no | string | Git tag applied to all `{release}` placeholders in GitHub URLs (e.g., `tutorial-v2`). See [Release tags](#release-tags). |
|
||||
| `build_overrides` | no | map | Nix `--override-input` flags for the runner. Keys are input names, values are relative paths to local repos. Only affects execution, not generation. |
|
||||
| `sections` | yes | list | The tutorial content. See below. |
|
||||
|
||||
## Sections
|
||||
|
||||
Each section becomes a `## heading` in the markdown. Sections with `step: true` get auto-numbered as "Step N: Title".
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `title` | yes | string | Section heading. |
|
||||
| `step` | no | boolean | If `true`, this section is numbered as "Step N: Title" in markdown and gets a `---` separator after it (except the last section). Sections without `step` render as plain `## Title`. |
|
||||
| `text` | no | string | Introductory prose rendered before the steps. Supports full markdown (tables, blockquotes, code blocks, etc.). |
|
||||
| `steps` | no | list | Ordered list of steps. See below. |
|
||||
|
||||
Sections without `steps` are prose-only — the `text` is rendered as-is. This is useful for reference sections like "Troubleshooting" or "Common Patterns".
|
||||
|
||||
## Steps
|
||||
|
||||
Steps are the core building blocks. Each step can combine multiple fields. The rendering order in the generated markdown is:
|
||||
|
||||
1. `title` → `### heading`
|
||||
2. `text` → prose paragraph
|
||||
3. `file` → code block with file contents
|
||||
4. `run` → bash code block
|
||||
5. `post_text` → prose after the action
|
||||
6. `extra_run` → additional command block (no heading)
|
||||
|
||||
A step without a `title` renders its content inline under the previous heading — useful for continuation content like "Then run:" followed by a code block.
|
||||
|
||||
### Step fields
|
||||
|
||||
#### `title` (string, optional)
|
||||
|
||||
Rendered as a `### heading` in the markdown. Steps without a title don't get a heading — their content flows under the previous step's heading.
|
||||
|
||||
#### `text` (string, optional)
|
||||
|
||||
Prose rendered before any action. Supports full markdown.
|
||||
|
||||
#### `file` (object, optional)
|
||||
|
||||
Writes a file to disk during execution and renders it as a code block in the markdown.
|
||||
|
||||
| Subfield | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `path` | string | Relative path within the project (e.g., `src/main.cpp`, `lib/libcalc.h`). |
|
||||
| `content` | string | The file contents. |
|
||||
| `language` | string | Syntax highlighting hint for the markdown code block. Auto-detected from extension if omitted (`.c` → `c`, `.h`/`.cpp` → `cpp`, `.json` → `json`, `.nix` → `nix`, etc.). |
|
||||
| `encoding` | string | Set to `base64` for binary files. Renders as `*Binary file: \`path\`*` instead of a code block. |
|
||||
|
||||
**Runner behavior:** Creates parent directories and writes the file to disk.
|
||||
|
||||
**Generator behavior:** Renders ` ```language ` code block with the content.
|
||||
|
||||
#### `run` (string, optional)
|
||||
|
||||
A shell command to execute and display.
|
||||
|
||||
| Subfield | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| (value) | string | The command to execute. Supports `{ext}` (expands to `so` or `dylib`) and `{shared_flags}` (expands to `-shared -fPIC` or `-dynamiclib`). |
|
||||
|
||||
**Runner behavior:** Expands platform placeholders, injects nix overrides if applicable, and runs the command. Checks exit code (0 = pass, non-zero = fail).
|
||||
|
||||
**Generator behavior:** Renders the command in a ` ```bash ` block. If `code_block` is present on the step, renders that instead (see below).
|
||||
|
||||
#### `code_block` (string, optional)
|
||||
|
||||
The exact content to show in the generated markdown, used **instead of** the `run` command. This is for cases where:
|
||||
|
||||
- The executed command differs from what readers should see (e.g., `&&`-chained commands displayed as separate lines)
|
||||
- Platform-specific variants should be shown (Linux and macOS versions)
|
||||
- The display should include comments, blank lines, or additional context
|
||||
|
||||
The runner ignores `code_block` — it only uses `run` for execution.
|
||||
|
||||
```yaml
|
||||
- title: "Build the shared library"
|
||||
run: "cd lib && gcc {shared_flags} -o libcalc.{ext} libcalc.c && cd .."
|
||||
code_block: |
|
||||
cd lib
|
||||
|
||||
# Linux
|
||||
gcc -shared -fPIC -o libcalc.so libcalc.c
|
||||
|
||||
# macOS
|
||||
# gcc -shared -fPIC -o libcalc.dylib libcalc.c
|
||||
|
||||
cd ..
|
||||
```
|
||||
|
||||
#### `expect_contains` (list of strings, optional)
|
||||
|
||||
Assertions checked by the runner against command output. Not rendered in the markdown.
|
||||
|
||||
```yaml
|
||||
run: "./lm/bin/lm metadata result/lib/calc_module_plugin.{ext}"
|
||||
expect_contains:
|
||||
- "Name: calc_module"
|
||||
- "Version: 1.0.0"
|
||||
```
|
||||
|
||||
**Runner behavior:** Captures stdout+stderr, checks that every string appears in the output.
|
||||
|
||||
**Generator behavior:** Ignored — not rendered. Use `post_text` to show expected output to readers.
|
||||
|
||||
#### `check_file` (string, optional)
|
||||
|
||||
Verifies a file exists. Runner-only, not rendered in the markdown.
|
||||
|
||||
```yaml
|
||||
- check_file: "result/lib/calc_module_plugin.{ext}"
|
||||
```
|
||||
|
||||
**Runner behavior:** Expands `{ext}`, globs for the file, passes if found.
|
||||
|
||||
**Generator behavior:** Ignored — not rendered.
|
||||
|
||||
#### `ui_test` (object, optional)
|
||||
|
||||
Runs headless UI tests against a Qt app using [logos-qt-mcp](https://github.com/logos-co/logos-qt-mcp). The app is launched with `QT_QPA_PLATFORM=offscreen` (no display needed) and tests connect to the QML inspector to verify elements, click buttons, and check results.
|
||||
|
||||
Two modes:
|
||||
- **Launch mode** (preferred): `launch` runs the app as a background process, tests connect to its inspector, app is killed when done. The `launch` command is rendered in the generated markdown.
|
||||
- **Binary mode**: `build` + `binary` let the test framework manage the app via `--ci`. Not rendered in markdown.
|
||||
|
||||
| Subfield | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `launch` | string | Command to launch the app (e.g., `nix run .`). Launched as a background process with offscreen Qt. **Rendered** in generated markdown as a bash code block. |
|
||||
| `build` | string | Command to build the app binary (binary mode only, not rendered). |
|
||||
| `binary` | string | Path to the app binary or `nix-app` to auto-resolve from flake (binary mode only). |
|
||||
| `qt_mcp` | string | Path to the logos-qt-mcp package, relative to workdir (e.g., `result-mcp`). Falls back to `--qt-mcp` CLI flag or `LOGOS_QT_MCP` env var. |
|
||||
| `setup` | list of strings | Commands to run before testing (e.g., `nix build 'github:logos-co/logos-qt-mcp' -o result-mcp`). |
|
||||
| `inspector_port` | integer | TCP port for the QML inspector (default: 3768). |
|
||||
| `tests` | list of objects | Test actions to execute. See below. |
|
||||
|
||||
**Test actions:**
|
||||
|
||||
| Action | Fields | What it does |
|
||||
|--------|--------|--------------|
|
||||
| `click` | `target` | Find element by text and click it |
|
||||
| `wait_for` | `texts`, `timeout` (ms, default 10000), `name` | Poll until all texts are visible |
|
||||
| `expect_texts` | `texts` | Assert all texts are visible now |
|
||||
| `set_text` | `find_by`, `find_value`, `value` | Find element by property and set its `text` property |
|
||||
| `sleep` | `ms` | Wait a fixed duration |
|
||||
|
||||
**Runner behavior (launch mode):** Runs setup commands, launches the app in the background with `QT_QPA_PLATFORM=offscreen`, waits for the QML inspector to be available, generates a `.mjs` test file, runs it, then kills the app. Reports pass/fail.
|
||||
|
||||
**Runner behavior (binary mode):** Runs setup + build, generates a `.mjs` test file, runs it via `node test.mjs --ci <binary> --verbose`. Reports pass/fail.
|
||||
|
||||
**Generator behavior:** In launch mode, `launch` is rendered as a ` ```bash ` code block. In binary mode, nothing is rendered. Use `text:` and `post_text:` for additional user-facing prose.
|
||||
|
||||
```yaml
|
||||
# Launch mode (preferred) — what's shown is what's executed
|
||||
- ui_test:
|
||||
launch: "nix run ."
|
||||
setup:
|
||||
- "nix build 'github:logos-co/logos-qt-mcp' -o result-mcp"
|
||||
qt_mcp: "result-mcp"
|
||||
tests:
|
||||
- name: "Title visible"
|
||||
action: wait_for
|
||||
texts: ["Logos Calculator"]
|
||||
timeout: 15000
|
||||
- name: "Enter number"
|
||||
action: set_text
|
||||
find_by: "placeholderText"
|
||||
find_value: "a"
|
||||
value: "3"
|
||||
- name: "Click Add"
|
||||
action: click
|
||||
target: "Add"
|
||||
- name: "Result shows 3"
|
||||
action: wait_for
|
||||
texts: ["3"]
|
||||
timeout: 10000
|
||||
```
|
||||
|
||||
#### `post_text` (string, optional)
|
||||
|
||||
Prose rendered **after** the step's action (file, run). Supports full markdown including code blocks, tables, blockquotes.
|
||||
|
||||
Use this for:
|
||||
- Expected output blocks
|
||||
- Explanations of what just happened
|
||||
- Callout boxes and tips
|
||||
|
||||
```yaml
|
||||
- title: "View metadata"
|
||||
run: "./lm/bin/lm metadata result/lib/plugin.{ext}"
|
||||
post_text: |
|
||||
Output:
|
||||
|
||||
```
|
||||
Plugin Metadata:
|
||||
================
|
||||
Name: my_module
|
||||
Version: 1.0.0
|
||||
```
|
||||
```
|
||||
|
||||
#### `extra_run` (object, optional)
|
||||
|
||||
A continuation command rendered under the same step heading (no separate `###`). Useful when a step has two related commands (e.g., build then verify).
|
||||
|
||||
| Subfield | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `run` | string | Command to execute. |
|
||||
| `code_block` | string | Display override (same as step-level `code_block`). |
|
||||
| `post_text` | string | Prose after the extra command. |
|
||||
|
||||
```yaml
|
||||
- title: "Build the shared library"
|
||||
run: "cd lib && gcc {shared_flags} -o libcalc.{ext} libcalc.c && cd .."
|
||||
code_block: |
|
||||
cd lib
|
||||
gcc -shared -fPIC -o libcalc.so libcalc.c
|
||||
cd ..
|
||||
post_text: "Verify the symbols are exported:"
|
||||
extra_run:
|
||||
run: "nm -gU lib/libcalc.{ext} | grep calc"
|
||||
code_block: |
|
||||
# Linux
|
||||
nm -D lib/libcalc.so | grep calc
|
||||
|
||||
# macOS
|
||||
# nm -gU lib/libcalc.dylib | grep calc
|
||||
post_text: |
|
||||
You should see symbols marked with `T`.
|
||||
```
|
||||
|
||||
## Platform placeholders
|
||||
|
||||
These placeholders are expanded at execution time by the runner and at generation time in rendered content:
|
||||
|
||||
| Placeholder | Linux | macOS |
|
||||
|-------------|-------|-------|
|
||||
| `{ext}` | `so` | `dylib` |
|
||||
| `{shared_flags}` | `-shared -fPIC` | `-dynamiclib` |
|
||||
|
||||
Platform placeholders work in `run`, `check_file`, `extra_run.run`, and `file.content` fields. They are **not** expanded in `code_block`, `text`, or `post_text` — those are rendered verbatim.
|
||||
|
||||
When a command uses platform placeholders, provide a `code_block` showing both platform variants for the markdown.
|
||||
|
||||
## Release tags
|
||||
|
||||
The `release` field lets you pin all GitHub URLs to a specific git tag. This avoids updating every URL individually when you want all `nix build 'github:logos-co/...'` commands to use the same release.
|
||||
|
||||
Use the `{release}` placeholder in `run` commands, `code_block`, and `file.content`:
|
||||
|
||||
```yaml
|
||||
release: "tutorial-v2"
|
||||
|
||||
sections:
|
||||
- title: "Set Up"
|
||||
steps:
|
||||
- run: "nix flake init -t github:logos-co/logos-module-builder{release}#with-external-lib"
|
||||
- run: "nix build 'github:logos-co/logos-module{release}#lm' --out-link ./lm"
|
||||
```
|
||||
|
||||
When `release` is set to `"tutorial-v2"`, `{release}` expands to `/tutorial-v2`:
|
||||
|
||||
```bash
|
||||
nix build 'github:logos-co/logos-module/tutorial-v2#lm' --out-link ./lm
|
||||
```
|
||||
|
||||
When `release` is empty or omitted, `{release}` expands to nothing:
|
||||
|
||||
```bash
|
||||
nix build 'github:logos-co/logos-module#lm' --out-link ./lm
|
||||
```
|
||||
|
||||
The `--release` CLI flag overrides the YAML field:
|
||||
|
||||
```bash
|
||||
# Use a specific tag (overrides whatever is in the YAML)
|
||||
python3 tools/tutorial_runner.py run spec.yaml --release tutorial-v3
|
||||
|
||||
# Generate markdown with a tag
|
||||
python3 tools/tutorial_runner.py generate spec.yaml --release tutorial-v2
|
||||
|
||||
# Clear the tag even if the YAML sets one
|
||||
python3 tools/tutorial_runner.py run spec.yaml --release ""
|
||||
```
|
||||
|
||||
## Runner behavior
|
||||
|
||||
- Creates a fresh temp directory (or uses `--workdir`) — all files, builds, and commands happen there
|
||||
- Walks sections and steps in order
|
||||
- Executes `file`, `run`, `check_file`, `ui_test` actions
|
||||
- Tracks pass/fail/skip counts
|
||||
- **Stops on first failure** by default (use `--continue-on-fail` to override)
|
||||
- Prints a summary report at the end
|
||||
- By default the temp directory is **deleted** when the run finishes
|
||||
|
||||
### Working directory
|
||||
|
||||
The runner needs a directory to work in. There are three modes:
|
||||
|
||||
1. **Default (temp dir, auto-deleted):** A fresh `/tmp/tutorial-test-XXXXX/` is created and removed after the run.
|
||||
2. **`--keep-workdir`:** Same temp dir, but it's kept after the run so you can inspect the results (built artifacts, installed modules, etc.).
|
||||
3. **`--workdir <path>`:** Use your own directory. It is never deleted. Useful for re-running specific phases against a previous build or for debugging.
|
||||
|
||||
The workdir path is printed at the top of every run:
|
||||
|
||||
```
|
||||
workdir : /tmp/tutorial-test-abc123
|
||||
```
|
||||
|
||||
### Tutorial chaining (`requires`)
|
||||
|
||||
When a spec has `requires:`, the runner creates a shared parent directory and runs each prerequisite before the main tutorial. Each tutorial gets its own subdirectory named by `project_name`:
|
||||
|
||||
```
|
||||
# Part 2 requires Part 1:
|
||||
requires:
|
||||
- tutorial-wrapping-c-library.test.yaml
|
||||
project_name: logos-calc-ui
|
||||
```
|
||||
|
||||
```
|
||||
# Running Part 2 automatically runs Part 1 first:
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-qml-ui-app.test.yaml
|
||||
|
||||
# Resulting directory structure:
|
||||
/tmp/tutorial-chain-XXXXX/
|
||||
├── logos-calc-module/ # Part 1 (prerequisite)
|
||||
└── logos-calc-ui/ # Part 2 (main)
|
||||
```
|
||||
|
||||
Commands like `../logos-calc-module` in Part 2 resolve to Part 1's output. Results are cumulative — a failure in any tutorial stops the chain (unless `--continue-on-fail`).
|
||||
|
||||
`requires:` is resolved **transitively**. A spec only needs to declare its *direct* prerequisites; their prerequisites are pulled in automatically. For the three-part series, Part 3 can simply declare Part 2:
|
||||
|
||||
```
|
||||
# Part 3 requires Part 2, which itself requires Part 1:
|
||||
requires:
|
||||
- tutorial-qml-ui-app.test.yaml
|
||||
project_name: logos-calc-ui-cpp
|
||||
```
|
||||
|
||||
```
|
||||
# Running Part 3 resolves the whole graph and runs in dependency order:
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-cpp-ui-app.test.yaml
|
||||
|
||||
# Resulting directory structure (Part 1 → Part 2 → Part 3):
|
||||
/tmp/tutorial-chain-XXXXX/
|
||||
├── logos-calc-module/ # Part 1 (transitive prerequisite, runs first)
|
||||
├── logos-calc-ui/ # Part 2 (direct prerequisite, runs second)
|
||||
└── logos-calc-ui-cpp/ # Part 3 (main, runs last)
|
||||
```
|
||||
|
||||
The graph is walked depth-first in post-order, so a prerequisite always runs before the spec that needs it. A prerequisite shared by multiple specs runs only once, and a circular `requires:` chain is reported as an error rather than looping forever.
|
||||
|
||||
When running standalone (no `requires:`), `project_name` is ignored and the runner behaves as before.
|
||||
|
||||
## Generator behavior
|
||||
|
||||
- Walks the same YAML structure
|
||||
- Produces markdown with proper headings, code blocks, and prose
|
||||
- Sections with `phase` get numbered as "Step 1:", "Step 2:", etc.
|
||||
- Sections without `phase` get plain `## Title` headings
|
||||
- `expect_contains` and `check_file` are not rendered (runner-only)
|
||||
- `code_block` overrides the `run` value for display
|
||||
- `file` content is rendered as a fenced code block with syntax highlighting
|
||||
- Triple blank lines are collapsed to double
|
||||
|
||||
## File layout
|
||||
|
||||
```
|
||||
repos/logos-tutorial/
|
||||
├── tests/
|
||||
│ └── tutorial-wrapping-c-library.test.yaml # The spec
|
||||
├── tools/
|
||||
│ ├── tutorial_runner.py # Runner + generator
|
||||
│ └── run-tutorial # Nix wrapper script
|
||||
├── tutorial-wrapping-c-library.md # Generated output
|
||||
└── docs/
|
||||
└── spec.md # This file
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Run a tutorial (all phases)
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-wrapping-c-library.test.yaml --verbose
|
||||
|
||||
# Run specific phases
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-wrapping-c-library.test.yaml --phase scaffold,files,build
|
||||
|
||||
# Run into a specific directory (kept after run, useful for inspecting results)
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-wrapping-c-library.test.yaml --workdir /tmp/my-tutorial-test --verbose
|
||||
|
||||
# Keep the auto-generated temp directory for debugging
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-wrapping-c-library.test.yaml --keep-workdir --verbose
|
||||
|
||||
# Re-run just the logoscore phase against a previous build
|
||||
python3 tools/tutorial_runner.py run tests/tutorial-wrapping-c-library.test.yaml --workdir /tmp/my-tutorial-test --phase logoscore --verbose
|
||||
|
||||
# Generate markdown
|
||||
python3 tools/tutorial_runner.py generate tests/tutorial-wrapping-c-library.test.yaml
|
||||
|
||||
# Generate to a specific file
|
||||
python3 tools/tutorial_runner.py generate tests/tutorial-wrapping-c-library.test.yaml -o my-output.md
|
||||
|
||||
# Use nix wrapper (ensures python3 + pyyaml are available)
|
||||
./tools/run-tutorial run tests/tutorial-wrapping-c-library.test.yaml --verbose
|
||||
```
|
||||
@@ -0,0 +1,776 @@
|
||||
name: "Tutorial Part 3: Building a C++ UI Module (Process-Isolated)"
|
||||
output: tutorial-cpp-ui-app.md
|
||||
project_name: logos-calc-ui-cpp
|
||||
requires:
|
||||
- tutorial-qml-ui-app.test.yaml
|
||||
release: ""
|
||||
|
||||
intro: |
|
||||
This is Part 3 of the Logos module tutorial series. In [Part 2](tutorial-qml-ui-app.md) you built a QML-only UI plugin. Now you'll build a **ui_qml module with a C++ backend** — the backend runs in a separate `ui-host` process while the QML view loads in the host app (basecamp / standalone).
|
||||
|
||||
what_you_build: |
|
||||
A `calc_ui_cpp` module with:
|
||||
|
||||
- A `.rep` file defining the remote interface (slots)
|
||||
- A C++ backend plugin that inherits from the generated `SimpleSource` base class
|
||||
- A QML view that calls the backend via a typed replica using `logos.watch()`
|
||||
- Process isolation: backend crashes can't bring down the host app
|
||||
|
||||
comparison: |
|
||||
**Why C++ backend over QML-only?**
|
||||
|
||||
| | QML-only (Part 2) | C++ backend (Part 3) |
|
||||
| ----------------- | ----------------------------------------------------------------- | --------------------------------------- |
|
||||
| Compilation | None | CMake + Qt |
|
||||
| Process isolation | No (QML runs in-process) | Yes (C++ in separate `ui-host` process) |
|
||||
| Backend calls | `logos.callModule()` / `logos.callModuleAsync()` to other modules | `LogosModules` typed SDK in C++ |
|
||||
| Type safety | Args travel as `QVariant` | C++ types preserved |
|
||||
| QML ↔ backend | Direct bridge | Qt Remote Objects (typed replica) |
|
||||
| `.rep` file | Not needed | Required — defines the remote interface |
|
||||
|
||||
prerequisites:
|
||||
- "Completed [Part 1](tutorial-wrapping-c-library.md) — you have a working `calc_module` with the shared library built (`.so` on Linux, `.dylib` on macOS in `logos-calc-module/lib/`)"
|
||||
- Nix with flakes enabled
|
||||
|
||||
sections:
|
||||
# ── Architecture (prose only) ───────────────────────────────────────────────
|
||||
- title: "Architecture"
|
||||
text: |
|
||||
```
|
||||
logos-basecamp / logos-standalone-app
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ QML View (Main.qml) │
|
||||
│ readonly property var backend: │
|
||||
│ logos.module("calc_ui_cpp") │
|
||||
│ logos.watch(backend.add(1,2))│
|
||||
│ │ │
|
||||
│ │ Qt Remote Objects (socket) │
|
||||
└──────────┼──────────────────────────────────┘
|
||||
│
|
||||
ui-host process (separate)
|
||||
┌──────────┼──────────────────────────────────┐
|
||||
│ ▼ │
|
||||
│ CalcUiCppPlugin (backend) │
|
||||
│ : CalcUiCppSimpleSource │
|
||||
│ : CalcUiCppViewPluginBase │
|
||||
│ int add(int a, int b) { │
|
||||
│ return m_logos->calc_module.add(a,b); │
|
||||
│ } │
|
||||
│ │ │
|
||||
│ │ LogosModules typed SDK │
|
||||
│ ▼ │
|
||||
│ calc_module (loaded in ui-host) │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The `.rep` file declares the interface. At build time, Qt's `repc` compiler generates:
|
||||
|
||||
- **`CalcUiCppSimpleSource`** — base class the backend inherits from
|
||||
- **`CalcUiCppReplica`** — typed replica the QML view uses
|
||||
- **`calc_ui_cpp_replica_factory`** — separate plugin that the host loads to create typed replicas
|
||||
|
||||
# ── Step 1: Scaffold ────────────────────────────────────────────────────────
|
||||
- title: "Scaffold"
|
||||
step: true
|
||||
text: |
|
||||
Create a new directory and initialise it from the C++ backend UI template:
|
||||
|
||||
`mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp`
|
||||
steps:
|
||||
- run: "nix flake init -t github:logos-co/logos-module-builder{release}#ui-qml-backend"
|
||||
code_block: |
|
||||
nix flake init -t github:logos-co/logos-module-builder{release}#ui-qml-backend
|
||||
post_text: |
|
||||
This creates the template. We'll customize it for our calculator.
|
||||
|
||||
- run: "git init && git add -A"
|
||||
|
||||
# ── Step 2: metadata.json ───────────────────────────────────────────────────
|
||||
- title: "`metadata.json`"
|
||||
step: true
|
||||
text: |
|
||||
Replace the template contents with your plugin's details:
|
||||
steps:
|
||||
- file:
|
||||
path: metadata.json
|
||||
language: json
|
||||
content: |
|
||||
{
|
||||
"name": "calc_ui_cpp",
|
||||
"version": "1.0.0",
|
||||
"type": "ui_qml",
|
||||
"category": "tools",
|
||||
"description": "Calculator C++ UI — QML view with process-isolated backend for calc_module",
|
||||
"main": "calc_ui_cpp_plugin",
|
||||
"view": "qml/Main.qml",
|
||||
"icon": "icons/calc.png",
|
||||
"dependencies": ["calc_module"],
|
||||
|
||||
"nix": {
|
||||
"packages": {
|
||||
"build": [],
|
||||
"runtime": []
|
||||
},
|
||||
"external_libraries": [],
|
||||
"cmake": {
|
||||
"find_packages": [],
|
||||
"extra_sources": [],
|
||||
"extra_include_dirs": [],
|
||||
"extra_link_libraries": []
|
||||
}
|
||||
}
|
||||
}
|
||||
post_text: |
|
||||
Create the icon directory and add a placeholder icon (displayed in the `logos-basecamp` sidebar when the module is loaded):
|
||||
|
||||
- run: "mkdir -p icons && echo 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAmElEQVR4nO3QMREAIBDAsFeEN3ziCWRkoEP2XmedfX82OkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAO0BN/SiO/PatoIAAAAASUVORK5CYII=' | base64 -d > icons/calc.png"
|
||||
code_block: |
|
||||
mkdir -p icons
|
||||
# Copy any PNG here — or generate a 64×64 placeholder:
|
||||
echo "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAmElEQVR4nO3QMREAIBDAsFeEN3ziCWRkoEP2XmedfX82OkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAO0BN/SiO/PatoIAAAAASUVORK5CYII=" | base64 -d > icons/calc.png
|
||||
post_text: |
|
||||
Key fields:
|
||||
|
||||
- `"type": "ui_qml"` — tells the builder this is a QML view module
|
||||
- `"main": "calc_ui_cpp_plugin"` — the backend Qt plugin library (without extension)
|
||||
- `"view": "qml/Main.qml"` — the QML entry point
|
||||
- `"dependencies": ["calc_module"]` — core modules the backend calls
|
||||
|
||||
# ── Step 3: The .rep File ─────────────────────────────────────────────────────
|
||||
- title: "The `.rep` File"
|
||||
step: true
|
||||
text: |
|
||||
Create `src/calc_ui_cpp.rep`:
|
||||
steps:
|
||||
- file:
|
||||
path: src/calc_ui_cpp.rep
|
||||
language: rep
|
||||
content: |
|
||||
class CalcUiCpp
|
||||
{
|
||||
SLOT(int add(int a, int b))
|
||||
SLOT(int multiply(int a, int b))
|
||||
SLOT(int factorial(int n))
|
||||
SLOT(int fibonacci(int n))
|
||||
SLOT(QString libVersion())
|
||||
}
|
||||
post_text: |
|
||||
This is the **single source of truth** for the remote interface. `repc` generates:
|
||||
|
||||
- `rep_calc_ui_cpp_source.h` — `CalcUiCppSimpleSource` with virtual slots the backend overrides
|
||||
- `rep_calc_ui_cpp_replica.h` — `CalcUiCppReplica` with typed methods
|
||||
|
||||
**SLOT** return values are delivered as `QRemoteObjectPendingReply` — use `logos.watch()` in QML to get them as JS Promises. You can also declare **PROP** entries (e.g. `PROP(QString status READWRITE)`) which auto-sync from the backend to the QML replica.
|
||||
|
||||
# ── Step 4: Interface header ──────────────────────────────────────────────────
|
||||
- title: "Interface header"
|
||||
step: true
|
||||
text: |
|
||||
The scaffolded template creates a set of `ui_example` files (`src/ui_example.rep`, `src/ui_example_interface.h`, `src/ui_example_plugin.{h,cpp}`). We replace them with `calc_ui_cpp` equivalents, so remove the example sources first — leaving them around with mismatched class/IID names just invites build errors or plugin-load failures at runtime:
|
||||
steps:
|
||||
- run: "rm -f src/ui_example.rep src/ui_example_interface.h src/ui_example_plugin.h src/ui_example_plugin.cpp"
|
||||
post_text: |
|
||||
Now create `src/calc_ui_cpp_interface.h`:
|
||||
- file:
|
||||
path: src/calc_ui_cpp_interface.h
|
||||
language: cpp
|
||||
content: |
|
||||
#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
|
||||
post_text: |
|
||||
Your plugin header should then include `calc_ui_cpp_interface.h` and use:
|
||||
|
||||
- `Q_PLUGIN_METADATA(IID CalcUiCppInterface_iid FILE "metadata.json")`
|
||||
- `Q_INTERFACES(CalcUiCppInterface)`
|
||||
|
||||
If the interface filename or IID symbol doesn't match, you'll typically get build errors (missing header/symbol) or plugin-load failures at runtime.
|
||||
|
||||
# ── Step 5: CMakeLists.txt ────────────────────────────────────────────────────
|
||||
- title: "`CMakeLists.txt`"
|
||||
step: true
|
||||
steps:
|
||||
- file:
|
||||
path: CMakeLists.txt
|
||||
language: cmake
|
||||
content: |
|
||||
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
|
||||
REP_FILE src/calc_ui_cpp.rep
|
||||
SOURCES
|
||||
src/calc_ui_cpp_interface.h
|
||||
src/calc_ui_cpp_plugin.h
|
||||
src/calc_ui_cpp_plugin.cpp
|
||||
)
|
||||
post_text: |
|
||||
`REP_FILE` tells `logos_module()` to:
|
||||
|
||||
1. Run `repc` to generate source/replica headers
|
||||
2. Generate `CalcUiCppViewPluginBase` (typed remoting base class)
|
||||
3. Build a separate `calc_ui_cpp_replica_factory` shared library
|
||||
|
||||
# ── Step 6: C++ Backend Plugin ────────────────────────────────────────────────
|
||||
- title: "C++ Backend Plugin"
|
||||
step: true
|
||||
steps:
|
||||
- title: "`src/calc_ui_cpp_plugin.h`"
|
||||
file:
|
||||
path: src/calc_ui_cpp_plugin.h
|
||||
language: cpp
|
||||
content: |
|
||||
#ifndef CALC_UI_CPP_PLUGIN_H
|
||||
#define CALC_UI_CPP_PLUGIN_H
|
||||
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
#include "calc_ui_cpp_interface.h"
|
||||
#include "LogosViewPluginBase.h"
|
||||
#include "rep_calc_ui_cpp_source.h"
|
||||
|
||||
class LogosAPI;
|
||||
class LogosModules;
|
||||
|
||||
// Inherits CalcUiCppSimpleSource (generated from calc_ui_cpp.rep) so
|
||||
// enableRemoting() can publish the typed source and QML replicas get
|
||||
// auto-synced properties + callable slots.
|
||||
class CalcUiCppPlugin : public CalcUiCppSimpleSource,
|
||||
public CalcUiCppInterface,
|
||||
public CalcUiCppViewPluginBase
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID CalcUiCppInterface_iid FILE "metadata.json")
|
||||
Q_INTERFACES(CalcUiCppInterface)
|
||||
|
||||
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);
|
||||
|
||||
// Slots from calc_ui_cpp.rep — return values directly. The QML replica
|
||||
// receives QRemoteObjectPendingReply; use logos.watch() in QML to get the value.
|
||||
int add(int a, int b) override;
|
||||
int multiply(int a, int b) override;
|
||||
int factorial(int n) override;
|
||||
int fibonacci(int n) override;
|
||||
QString libVersion() override;
|
||||
|
||||
signals:
|
||||
void eventResponse(const QString& eventName, const QVariantList& args);
|
||||
|
||||
private:
|
||||
LogosAPI* m_logosAPI = nullptr;
|
||||
LogosModules* m_logos = nullptr;
|
||||
};
|
||||
|
||||
#endif // CALC_UI_CPP_PLUGIN_H
|
||||
post_text: |
|
||||
Three base classes:
|
||||
|
||||
- **`CalcUiCppSimpleSource`** — generated from `.rep`, provides the typed source for Qt Remote Objects
|
||||
- **`CalcUiCppInterface`** — standard Logos plugin interface (`name()`, `version()`)
|
||||
- **`CalcUiCppViewPluginBase`** — generated, provides `setBackend()` and `enableRemoting()`
|
||||
|
||||
- title: "`src/calc_ui_cpp_plugin.cpp`"
|
||||
file:
|
||||
path: src/calc_ui_cpp_plugin.cpp
|
||||
language: cpp
|
||||
content: |
|
||||
#include "calc_ui_cpp_plugin.h"
|
||||
#include "logos_api.h"
|
||||
#include "logos_sdk.h"
|
||||
|
||||
CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : CalcUiCppSimpleSource(parent) {}
|
||||
CalcUiCppPlugin::~CalcUiCppPlugin() { delete m_logos; }
|
||||
|
||||
void CalcUiCppPlugin::initLogos(LogosAPI* api)
|
||||
{
|
||||
if (m_logos) return;
|
||||
m_logosAPI = api;
|
||||
m_logos = new LogosModules(api);
|
||||
// Register this object as the Remote Objects source so the QML replica
|
||||
// can see its properties and call its slots.
|
||||
setBackend(this);
|
||||
}
|
||||
|
||||
int CalcUiCppPlugin::add(int a, int b)
|
||||
{
|
||||
return m_logos->calc_module.add(a, b);
|
||||
}
|
||||
|
||||
int CalcUiCppPlugin::multiply(int a, int b)
|
||||
{
|
||||
return m_logos->calc_module.multiply(a, b);
|
||||
}
|
||||
|
||||
int CalcUiCppPlugin::factorial(int n)
|
||||
{
|
||||
return m_logos->calc_module.factorial(n);
|
||||
}
|
||||
|
||||
int CalcUiCppPlugin::fibonacci(int n)
|
||||
{
|
||||
return m_logos->calc_module.fibonacci(n);
|
||||
}
|
||||
|
||||
QString CalcUiCppPlugin::libVersion()
|
||||
{
|
||||
return m_logos->calc_module.libVersion();
|
||||
}
|
||||
post_text: |
|
||||
Key points:
|
||||
|
||||
- Constructor calls `CalcUiCppSimpleSource(parent)` — not `QObject(parent)`
|
||||
- `initLogos()` calls `setBackend(this)` to register with the Remote Objects host
|
||||
- Slots return values directly — they travel back to the QML replica via Qt Remote Objects
|
||||
- `m_logos->calc_module.add(a, b)` uses the generated typed SDK (type-safe, no QVariant)
|
||||
|
||||
# ── Step 7: QML View ──────────────────────────────────────────────────────────
|
||||
- title: "QML View"
|
||||
step: true
|
||||
text: |
|
||||
Create `src/qml/Main.qml`:
|
||||
steps:
|
||||
- file:
|
||||
path: src/qml/Main.qml
|
||||
language: qml
|
||||
content: |
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string result: ""
|
||||
property string errorText: ""
|
||||
|
||||
// Typed replica of the backend running in ui-host (generated from calc_ui_cpp.rep).
|
||||
readonly property var backend: logos.module("calc_ui_cpp")
|
||||
|
||||
// logos.watch() delivers the result of a replica slot call via callbacks.
|
||||
// No QtRemoteObjects import needed — the bridge handles it.
|
||||
function callCalc(method, args) {
|
||||
if (!backend) {
|
||||
root.errorText = "Backend not available"
|
||||
return
|
||||
}
|
||||
root.errorText = ""
|
||||
root.result = "..."
|
||||
logos.watch(backend[method].apply(backend, args),
|
||||
function(value) { root.result = String(value) },
|
||||
function(error) { root.errorText = String(error) }
|
||||
)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 24
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Logos Calculator (C++ backend)"
|
||||
font.pixelSize: 20
|
||||
color: "#ffffff"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
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.callCalc("add", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Multiply"
|
||||
onClicked: root.callCalc("multiply", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
Layout.fillWidth: true
|
||||
|
||||
TextField {
|
||||
id: inputN
|
||||
placeholderText: "n"
|
||||
Layout.preferredWidth: 80
|
||||
validator: IntValidator { bottom: 0 }
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Factorial"
|
||||
onClicked: root.callCalc("factorial", [parseInt(inputN.text) || 0])
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Fibonacci"
|
||||
onClicked: root.callCalc("fibonacci", [parseInt(inputN.text) || 0])
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "libcalc version"
|
||||
onClicked: root.callCalc("libVersion", [])
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
post_text: |
|
||||
Key patterns:
|
||||
|
||||
- `logos.module("calc_ui_cpp")` — gets the typed replica (auto-synced properties)
|
||||
- `logos.watch(backend.add(1, 2), ...)` — SLOT return value as JS Promise
|
||||
- The `logos` object is injected by the host at runtime — no `QtRemoteObjects` import needed
|
||||
|
||||
# ── Step 8: Use the Logos Design System (prose only) ──────────────────────────
|
||||
- title: "Use the Logos Design System in your QML"
|
||||
step: true
|
||||
text: |
|
||||
The QML you load above runs inside the host (`logos-basecamp` / `logos-standalone-app`), which already has `logos-design-system` on the QML import path. Use its themed components rather than rolling your own visuals — your module gets the polished look automatically as the design system evolves.
|
||||
|
||||
```qml
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
import Logos.Icons // optional shared icon assets
|
||||
|
||||
LogosButton {
|
||||
text: qsTr("Add")
|
||||
onClicked: root.callCalc("add", [parseInt(inputA.text) || 0,
|
||||
parseInt(inputB.text) || 0])
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: inputA
|
||||
placeholderText: qsTr("a")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusSmall
|
||||
LogosText { text: qsTr("Result"); color: Theme.palette.text }
|
||||
}
|
||||
```
|
||||
|
||||
**Discover what's available** by running the storybook:
|
||||
|
||||
```bash
|
||||
cd repos/logos-design-system && nix run
|
||||
```
|
||||
|
||||
The sidebar splits components into:
|
||||
|
||||
- **Controls** — designed per Figma, production-ready (`LogosButton`, `LogosBadge`, `LogosCheckbox`, `LogosComboBox`, `LogosIconButton`, `LogosPaginator`, `LogosSearchBar`, `LogosTabBar`, `LogosTable`, `LogosText`, `LogosTextField`, `LogosToolTip`, …).
|
||||
- **Controls (not designed)** — placeholders with stable APIs but unstyled visuals (`LogosDialog`, `LogosDrawer`, `LogosScrollView`, `LogosSpinner`, `LogosTextArea`, `LogosSwitch`, …). You can ship with them; they'll get the polished look applied later without you having to change your QML.
|
||||
|
||||
**Theme tokens** (use these instead of hex literals or magic font sizes):
|
||||
|
||||
- `Theme.palette.*` — `background`, `backgroundSecondary`, `surface`, `text`, `textSecondary`, `border`, `primary`, `success`, `warning`, `error`, `info`, `hover`, `pressed`, …
|
||||
- `Theme.spacing.*` — `tiny`, `small`, `medium`, `large`, `xlarge`, `xxlarge`, `radiusSmall`, `radiusMedium`, `radiusLarge`
|
||||
- `Theme.typography.*` — `pageTitleText` (36), `titleText` (30), `panelTitleText` (24), `subtitleText` (16), `primaryText` (14), `secondaryText` (12); `weightRegular` / `weightMedium` / `weightBold`; `publicSans`
|
||||
- `Logos.Icons.LogosIcons.*` — `arrowLeft`, `arrowRight`, `refresh`, `install`, `trash`, `more`, `search`, …
|
||||
|
||||
**Feedback and contributions**
|
||||
|
||||
Feel free to report bugs, file feature requests, or contribute components / theme tokens upstream — all welcome at `logos-co/logos-design-system`. The same fix lifts every consumer, so upstreaming is the most impactful path. If you can sketch the public API you'd like to use in a feature request, it makes review and implementation much faster.
|
||||
|
||||
# ── Step 9: flake.nix ─────────────────────────────────────────────────────────
|
||||
- title: "`flake.nix`"
|
||||
step: true
|
||||
text: |
|
||||
The template already wires everything up. Update the description and point `calc_module` at your dependency:
|
||||
steps:
|
||||
- file:
|
||||
path: flake.nix
|
||||
language: nix
|
||||
content: |
|
||||
{
|
||||
description = "Calculator C++ UI plugin for Logos - QML view with process-isolated backend for calc_module";
|
||||
|
||||
inputs = {
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
|
||||
|
||||
# Option A: point to a remote repo (for CI or when calc_module is published)
|
||||
calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module";
|
||||
|
||||
# Option B: point to your local checkout (for local development)
|
||||
# calc_module.url = "path:../logos-calc-module";
|
||||
};
|
||||
|
||||
outputs = inputs@{ logos-module-builder, calc_module, ... }:
|
||||
logos-module-builder.lib.mkLogosQmlModule {
|
||||
src = ./.;
|
||||
configFile = ./metadata.json;
|
||||
flakeInputs = inputs;
|
||||
};
|
||||
}
|
||||
post_text: |
|
||||
The `calc_module` input attribute name must match the dependency name in `metadata.json`. The URL can be:
|
||||
|
||||
- **`github:`** — fetches from a remote GitHub repo. Use for CI or when `calc_module` is published.
|
||||
- **`path:`** — points to a local directory on disk (e.g., `path:../logos-calc-module`). Use during local development.
|
||||
|
||||
> **Important:** Whichever URL scheme you use, `calc_module` must be built with its shared library (`.so` on Linux, `.dylib` on macOS) present in `lib/`. If it's missing, the nix build will fail with linker errors. See [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library).
|
||||
|
||||
`mkLogosQmlModule` handles everything: compiles the C++ backend (because `main` is set), bundles the QML view, generates LGX packages, and wires up `nix run`.
|
||||
|
||||
# ── Step 10: Build and Run ────────────────────────────────────────────────────
|
||||
- title: "Build and Run"
|
||||
step: true
|
||||
text: |
|
||||
First, make sure your local `calc_module` is built and its shared library is present in `lib/` (see [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library)):
|
||||
steps:
|
||||
- title: "Ensure `calc_module` is built"
|
||||
run: "ls ../logos-calc-module/lib/libcalc.{ext}"
|
||||
code_block: |
|
||||
ls ../logos-calc-module/lib/libcalc.so # Linux
|
||||
ls ../logos-calc-module/lib/libcalc.dylib # macOS
|
||||
post_text: |
|
||||
If the file is missing, build it first (as covered in [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library)):
|
||||
extra_run:
|
||||
run: "cd ../logos-calc-module/lib && gcc {shared_flags} -o libcalc.{ext} libcalc.c && cd ../../logos-calc-ui-cpp"
|
||||
code_block: |
|
||||
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
|
||||
|
||||
- title: "Lock and build"
|
||||
text: |
|
||||
Stage your files and lock the flake. Then build and run — choose the approach that matches your `flake.nix` setup.
|
||||
run: "git add -A"
|
||||
- run: "nix flake update"
|
||||
- run: "git add flake.lock"
|
||||
post_text: |
|
||||
```bash
|
||||
# If flake.nix uses path:../logos-calc-module — just run directly:
|
||||
nix run
|
||||
|
||||
# If flake.nix uses github: — override to use your local checkout:
|
||||
nix run --override-input calc_module path:../logos-calc-module
|
||||
|
||||
# Or from the workspace:
|
||||
./scripts/ws run logos-calc-ui-cpp --local logos-calc-ui-cpp logos-calc-module
|
||||
```
|
||||
|
||||
- title: "Launch and verify the UI"
|
||||
text: |
|
||||
Launch the app and confirm the view loads with all of its controls. The backend runs in a separate `ui-host` process; clicking **Add** sends the call over Qt Remote Objects and the result comes back through `logos.watch()`.
|
||||
ui_test:
|
||||
launch: "nix run . --override-input calc_module path:../logos-calc-module"
|
||||
setup:
|
||||
- "nix build 'github:logos-co/logos-qt-mcp{release}' -o result-mcp"
|
||||
qt_mcp: "result-mcp"
|
||||
tests:
|
||||
- name: "App window opens with title"
|
||||
action: wait_for
|
||||
texts: ["Logos Calculator (C++ backend)"]
|
||||
timeout: 30000
|
||||
- name: "Operation buttons visible"
|
||||
action: wait_for
|
||||
texts: ["Add", "Multiply", "Factorial", "Fibonacci"]
|
||||
timeout: 5000
|
||||
- name: "Enter operands"
|
||||
action: set_text
|
||||
find_by: "placeholderText"
|
||||
find_value: "a"
|
||||
value: "3"
|
||||
- name: "Set second operand"
|
||||
action: set_text
|
||||
find_by: "placeholderText"
|
||||
find_value: "b"
|
||||
value: "5"
|
||||
- name: "Click Add"
|
||||
action: click
|
||||
target: "Add"
|
||||
- name: "Result of 3 + 5 shows 8"
|
||||
action: wait_for
|
||||
texts: ["8"]
|
||||
timeout: 10000
|
||||
post_text: |
|
||||
The result `8` comes from `calc_module.add(3, 5)` executed in the C++ backend — proof the full path (QML replica → Qt Remote Objects → ui-host backend → typed SDK → `calc_module`) works end to end.
|
||||
|
||||
# ── Step 11: Live reloading (prose only) ──────────────────────────────────────
|
||||
- title: "Live reloading QML with `DEV_QML_PATH`"
|
||||
step: true
|
||||
text: |
|
||||
For QML iteration, point `DEV_QML_PATH` at the directory that contains your view entry's **basename** (from `metadata.json` `"view"`). This tutorial sets `"view": "qml/Main.qml"`, so the directory must contain `Main.qml` (here: `src/qml/`):
|
||||
|
||||
```bash
|
||||
DEV_QML_PATH=$PWD/src/qml nix run .
|
||||
```
|
||||
|
||||
When `DEV_QML_PATH` is set, `logos-standalone-app` loads QML from your source tree at runtime instead of the installed copy — so edits to `Main.qml` (and any QML under that tree) are picked up on the next relaunch without you having to re-sync files.
|
||||
|
||||
**Important — what this does *not* skip.** `nix run` always re-evaluates the flake and rehashes the source tree before launching. By default `src = ./.` includes every tracked file, including `*.qml` — so:
|
||||
|
||||
- **Any source change, including QML edits, rebuilds the plugin** before the app starts. `DEV_QML_PATH` only kicks in *after* the build is done; it doesn't shortcut the rebuild itself.
|
||||
- **C++ / `.rep` / `metadata.json` / CMake changes** rebuild as normal.
|
||||
- The flake-evaluation overhead on each `nix run` is fixed and unavoidable while invoking through nix.
|
||||
|
||||
For the absolute fastest loop (no nix involvement after the first build), do the build once and run the resulting binary directly:
|
||||
|
||||
```bash
|
||||
# Build once — populates result/ in the nix store
|
||||
nix build .
|
||||
|
||||
# Subsequent runs: invoke the bundled standalone wrapper directly,
|
||||
# skipping nix entirely. DEV_QML_PATH still redirects QML loading.
|
||||
DEV_QML_PATH=$PWD/src/qml ./result/bin/run-logos-standalone-ui
|
||||
```
|
||||
|
||||
(Adjust the binary name to whatever `ls result/bin/` shows on your build.)
|
||||
|
||||
> **Naming:** Only `DEV_QML_PATH` is honored by `logos-standalone-app`. See `repos/logos-standalone-app/README.md`.
|
||||
|
||||
> This does not work with `logos-basecamp` — Basecamp loads QML plugins from its own install tree, so source edits are not picked up until you rebuild and reinstall the `.lgx`.
|
||||
|
||||
# ── Step 12: How the Pieces Connect (prose only) ──────────────────────────────
|
||||
- title: "How the Pieces Connect"
|
||||
step: true
|
||||
text: |
|
||||
1. `nix build` → compiles the C++ plugin + replica factory, bundles QML view
|
||||
2. `nix run` → launches `logos-standalone-app` which:
|
||||
- Loads `calc_module` (dependency)
|
||||
- Spawns a `ui-host` child process with `calc_ui_cpp_plugin.so`
|
||||
- `ui-host` calls `initLogos()` → `setBackend(this)` → `enableRemoting(host)`
|
||||
- Backend is now accessible over a local socket
|
||||
3. Host app loads `calc_ui_cpp_replica_factory.dylib` → creates a typed replica
|
||||
4. QML gets the replica via `logos.module("calc_ui_cpp")`
|
||||
5. `backend.add(1, 2)` → Qt Remote Objects sends call to ui-host → backend runs → returns result
|
||||
|
||||
# ── Step 13: UI Integration Tests ─────────────────────────────────────────────
|
||||
- title: "UI Integration Tests"
|
||||
step: true
|
||||
text: |
|
||||
Add automated UI tests using the [logos-qt-mcp](https://github.com/logos-co/logos-qt-mcp) test framework. Just create `.mjs` files in `tests/` and `logos-module-builder` auto-wires `nix build .#integration-test`.
|
||||
|
||||
Tests connect to the QML inspector inside `logos-standalone-app` and can find elements, click buttons, verify text, and take screenshots.
|
||||
steps:
|
||||
- title: "Create a test file"
|
||||
text: |
|
||||
Create `tests/ui-tests.mjs`:
|
||||
file:
|
||||
path: tests/ui-tests.mjs
|
||||
language: javascript
|
||||
content: |
|
||||
import { resolve } from "node:path";
|
||||
|
||||
// CI sets LOGOS_QT_MCP automatically; for interactive use: nix build .#test-framework -o result-mcp
|
||||
const root =
|
||||
process.env.LOGOS_QT_MCP ||
|
||||
new URL("../result-mcp", import.meta.url).pathname;
|
||||
const { test, run } = await import(
|
||||
resolve(root, "test-framework/framework.mjs")
|
||||
);
|
||||
|
||||
test("calc_ui_cpp: loads and shows title", async (app) => {
|
||||
await app.waitFor(
|
||||
async () => {
|
||||
await app.expectTexts(["Logos Calculator (C++ backend)"]);
|
||||
},
|
||||
{ timeout: 15000, interval: 500, description: "UI to load" },
|
||||
);
|
||||
});
|
||||
|
||||
test("calc_ui_cpp: operation buttons visible", async (app) => {
|
||||
await app.expectTexts(["Add", "Multiply", "Factorial", "Fibonacci"]);
|
||||
});
|
||||
|
||||
run();
|
||||
|
||||
- title: "Run the tests"
|
||||
run: "git add tests/"
|
||||
- run: "nix build .#integration-test -L --override-input calc_module path:../logos-calc-module"
|
||||
code_block: |
|
||||
# Hermetic CI test
|
||||
nix build .#integration-test -L
|
||||
post_text: |
|
||||
The `integration-test` output launches `logos-standalone-app` with `QT_QPA_PLATFORM=offscreen` (no display needed), connects to the QML inspector, and runs all `.mjs` files in `tests/`.
|
||||
|
||||
To run tests interactively (against an already-running app):
|
||||
|
||||
```bash
|
||||
nix build .#test-framework -o result-mcp
|
||||
nix run . # app with inspector on :3768
|
||||
node tests/ui-tests.mjs # in another terminal
|
||||
```
|
||||
|
||||
# ── Comparison: .rep Interface Patterns (prose only) ──────────────────────────
|
||||
- title: "Comparison: .rep Interface Patterns"
|
||||
text: |
|
||||
| Pattern | .rep declaration | Backend C++ | QML usage |
|
||||
| ---------------- | ------------------------------------ | ------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| **Return value** | `SLOT(int add(int a, int b))` | `int add(...) override { return ...; }` | `logos.watch(backend.add(1,2), cb)` |
|
||||
| **Property** | `PROP(QString status READWRITE)` | `setStatus("Ready")` (inherited) | `backend.status` (auto-syncs) |
|
||||
| **Signal** | `SIGNAL(errorOccurred(QString msg))` | `emit errorOccurred("fail")` | `Connections { target: backend; function onErrorOccurred(msg) {...} }` |
|
||||
| **Model** | (use Q_PROPERTY on backend) | `Q_PROPERTY(QAbstractItemModel* items ...)` | `logos.model("calc_ui_cpp", "items")` |
|
||||
|
||||
# ── Next Steps (prose only) ───────────────────────────────────────────────────
|
||||
- title: "Next Steps"
|
||||
text: |
|
||||
- Add more `.rep` properties/signals for richer UI state
|
||||
- Use `logos.model()` for list views backed by `QAbstractItemModel`
|
||||
- Package as `.lgx` for distribution: `nix build .#lgx`
|
||||
- **Use the Logos Design System** in your QML — see [Step 8](#step-8-use-the-logos-design-system-in-your-qml). Browse components in the storybook (`cd repos/logos-design-system && nix run`); file issues at `logos-co/logos-design-system`.
|
||||
- See [logos-package-manager-ui](https://github.com/logos-co/logos-package-manager-ui) for a production example
|
||||
@@ -0,0 +1,883 @@
|
||||
name: "Tutorial Part 2: Building a QML UI for Your Logos Module"
|
||||
output: tutorial-qml-ui-app.md
|
||||
project_name: logos-calc-ui
|
||||
requires:
|
||||
- tutorial-wrapping-c-library.test.yaml
|
||||
release: ""
|
||||
|
||||
intro: |
|
||||
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`.
|
||||
|
||||
what_you_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_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](tutorial-wrapping-c-library.md) — you have a working `calc_module` with the shared library built (`.so` on Linux, `.dylib` on macOS in `logos-calc-module/lib/`)"
|
||||
- Nix with flakes enabled (same as Part 1)
|
||||
- "Basic familiarity with QML (Qt's declarative UI language)"
|
||||
|
||||
sections:
|
||||
# ── How QML UI Plugins Work (prose only) ────────────────────────────────────
|
||||
- title: "How QML UI Plugins Work"
|
||||
text: |
|
||||
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 `.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 defined by the required `"view"` field in `metadata.json` (for this tutorial it is `Main.qml`).
|
||||
|
||||
# ── Step 1: Scaffold ───────────────────────────────────────────────────────
|
||||
- title: "Scaffold"
|
||||
step: true
|
||||
text: |
|
||||
Create a new directory and initialise it from the QML module template:
|
||||
|
||||
`mkdir logos-calc-ui && cd logos-calc-ui`
|
||||
steps:
|
||||
- run: "nix flake init -t github:logos-co/logos-module-builder{release}#ui-qml"
|
||||
post_text: |
|
||||
> **Note:** The generated `flake.nix` uses an unpinned `logos-module-builder` URL. Replace it with the pinned version shown in [Step 4](#step-4-update-flakenix) to ensure reproducible builds.
|
||||
|
||||
- run: "git init"
|
||||
- run: "git add -A"
|
||||
post_text: |
|
||||
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 ───────────────────────────────────────────
|
||||
- title: "Update `metadata.json`"
|
||||
step: true
|
||||
text: |
|
||||
Replace the template contents with your plugin's details. The template may generate an extra `nix` section — keep it as-is, it's used by the builder:
|
||||
steps:
|
||||
- file:
|
||||
path: metadata.json
|
||||
language: json
|
||||
content: |
|
||||
{
|
||||
"name": "calc_ui",
|
||||
"version": "1.0.0",
|
||||
"description": "Calculator UI - QML frontend for the calc_module",
|
||||
"type": "ui_qml",
|
||||
"view": "Main.qml",
|
||||
"dependencies": ["calc_module"],
|
||||
"category": "tools",
|
||||
"icon": "icons/calc.png",
|
||||
|
||||
"nix": {
|
||||
"packages": {
|
||||
"build": [],
|
||||
"runtime": []
|
||||
},
|
||||
"external_libraries": [],
|
||||
"cmake": {
|
||||
"find_packages": [],
|
||||
"extra_sources": [],
|
||||
"extra_include_dirs": [],
|
||||
"extra_link_libraries": []
|
||||
}
|
||||
}
|
||||
}
|
||||
post_text: |
|
||||
Create the icon directory and add a placeholder icon. The icon is displayed in the `logos-basecamp` sidebar when the module is loaded:
|
||||
|
||||
- run: "mkdir -p icons && echo 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAmElEQVR4nO3QMREAIBDAsFeEN3ziCWRkoEP2XmedfX82OkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAO0BN/SiO/PatoIAAAAASUVORK5CYII=' | base64 -d > icons/calc.png"
|
||||
code_block: |
|
||||
mkdir -p icons
|
||||
# Copy any PNG here — or generate a 64×64 placeholder:
|
||||
echo "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAmElEQVR4nO3QMREAIBDAsFeEN3ziCWRkoEP2XmedfX82OkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAO0BN/SiO/PatoIAAAAASUVORK5CYII=" | base64 -d > icons/calc.png
|
||||
post_text: |
|
||||
The `view` field tells the host which QML file to load for the UI. The `dependencies` field tells the host to load `calc_module` before showing your UI.
|
||||
|
||||
> **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 the dependency name — 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: Write Main.qml ─────────────────────────────────────────────────
|
||||
- title: "Write `Main.qml`"
|
||||
step: true
|
||||
text: |
|
||||
Replace the starter file with the calculator UI. This demonstrates two communication patterns:
|
||||
|
||||
1. **Direct calls** — `logos.callModule()` sends a request and returns the result immediately
|
||||
2. **Event-based** — `logos.callModule()` fires-and-forgets, the module emits an event, and QML receives it via `logos.onModuleEvent()`
|
||||
steps:
|
||||
- file:
|
||||
path: Main.qml
|
||||
language: qml
|
||||
content: |
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string result: ""
|
||||
property string errorText: ""
|
||||
property string versionFromEvent: ""
|
||||
|
||||
// ── Event subscription ────────────────────────────────────
|
||||
// Subscribe to "versionReady" events pushed from calc_module.
|
||||
Component.onCompleted: {
|
||||
if (typeof logos !== "undefined" && logos.onModuleEvent)
|
||||
logos.onModuleEvent("calc_module", "versionReady")
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: typeof logos !== "undefined" ? logos : null
|
||||
function onModuleEventReceived(moduleName, eventName, data) {
|
||||
if (eventName === "versionReady")
|
||||
root.versionFromEvent = data[0]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ── Pattern 1: Direct call (request -> response) ──────
|
||||
Text {
|
||||
text: "Direct calls (logos.callModule -> returns result)"
|
||||
color: "#8b949e"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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", [])
|
||||
}
|
||||
}
|
||||
|
||||
// Direct call result
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pattern 2: Event-based (fire-and-forget -> event) ─
|
||||
Text {
|
||||
text: "Event-based (fire-and-forget call -> result via event)"
|
||||
color: "#8b949e"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
Layout.fillWidth: true
|
||||
|
||||
Button {
|
||||
text: "libcalc version (event)"
|
||||
onClicked: {
|
||||
if (typeof logos !== "undefined" && logos.callModule)
|
||||
logos.callModule("calc_module", "libVersionNotify", [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event result
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 56
|
||||
color: "#1a1a2d"
|
||||
radius: 8
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.versionFromEvent.length > 0
|
||||
? ("Version (via event): " + root.versionFromEvent)
|
||||
: "Press the event button — result arrives via event"
|
||||
color: "#7ab8ff"
|
||||
font.pixelSize: 15
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
|
||||
// ── Direct call 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)])
|
||||
}
|
||||
}
|
||||
post_text: |
|
||||
The UI demonstrates two communication patterns:
|
||||
|
||||
- **Green section (direct calls):** `logos.callModule("calc_module", "libVersion", [])` sends a request to `calc_module` and returns the result synchronously. Simple request/response.
|
||||
|
||||
- **Blue section (event-based):** `logos.callModule("calc_module", "libVersionNotify", [])` calls the module but ignores the return value. Instead, the module emits a `"versionReady"` event via `eventResponse`, and the QML receives it through the `logos.onModuleEvent()` subscription set up in `Component.onCompleted`.
|
||||
|
||||
The `logos` object is injected by the host at runtime.
|
||||
|
||||
# ── Step 4: Update flake.nix ───────────────────────────────────────────────
|
||||
- title: "Update `flake.nix`"
|
||||
step: true
|
||||
text: |
|
||||
The template already has everything wired up. Update the description and add `calc_module` as a dependency input:
|
||||
steps:
|
||||
- file:
|
||||
path: flake.nix
|
||||
language: nix
|
||||
content: |
|
||||
{
|
||||
description = "Calculator QML UI Plugin for Logos - frontend for calc_module";
|
||||
|
||||
inputs = {
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
|
||||
|
||||
# Option A: point to a remote repo (for CI or when calc_module is published)
|
||||
calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module";
|
||||
|
||||
# Option B: point to your local checkout (for local development)
|
||||
# calc_module.url = "path:../logos-calc-module";
|
||||
};
|
||||
|
||||
outputs = inputs@{ logos-module-builder, ... }:
|
||||
logos-module-builder.lib.mkLogosQmlModule {
|
||||
src = ./.;
|
||||
configFile = ./metadata.json;
|
||||
flakeInputs = inputs;
|
||||
};
|
||||
}
|
||||
post_text: |
|
||||
The input attribute name (`calc_module`) must match the dependency name in `metadata.json`.
|
||||
|
||||
The `calc_module.url` can be either:
|
||||
|
||||
- **`github:`** — fetches from a remote GitHub repo. Use this for CI or when `calc_module` has been published.
|
||||
- **`path:`** — points to a local directory on disk. Use this during development when both repos live side by side (e.g., `path:../logos-calc-module`).
|
||||
|
||||
> **Important:** Whichever URL scheme you use, `calc_module` must be built with its shared library (`.so` on Linux, `.dylib` on macOS) present in `lib/`. If the library is missing, the nix build will fail with linker errors. See [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library) for build instructions.
|
||||
|
||||
`mkLogosQmlModule` handles everything — it stages QML files, metadata, and icons into a plugin directory, bundles all module dependencies (direct and transitive) from their LGX packages, and automatically wires up `apps.default` so `nix run .` launches the UI in a standalone window with all required backend modules self-contained. `flakeInputs = inputs` passes all inputs so that dependencies declared in `metadata.json` are resolved automatically.
|
||||
|
||||
> **Tip:** Even if `flake.nix` uses a `github:` URL, you can override it at build time with `--override-input calc_module path:../logos-calc-module` to use your local checkout without editing `flake.nix`. This is covered in [Step 5.2](#52-full-functionality-with-modules).
|
||||
|
||||
# ── Step 5: Test with nix run ──────────────────────────────────────────────
|
||||
- title: "Test with `nix run`"
|
||||
step: true
|
||||
steps:
|
||||
- title: "UI only (layout preview)"
|
||||
run: "git add -A"
|
||||
- run: "nix flake update"
|
||||
- run: "git add flake.lock"
|
||||
- ui_test:
|
||||
launch: "nix run ."
|
||||
setup:
|
||||
- "nix build 'github:logos-co/logos-qt-mcp{release}' -o result-mcp"
|
||||
qt_mcp: "result-mcp"
|
||||
tests:
|
||||
- name: "App window opens with title"
|
||||
action: wait_for
|
||||
texts: ["Logos Calculator"]
|
||||
timeout: 15000
|
||||
- name: "Add button visible"
|
||||
action: wait_for
|
||||
texts: ["Add"]
|
||||
timeout: 5000
|
||||
- name: "Multiply button visible"
|
||||
action: wait_for
|
||||
texts: ["Multiply"]
|
||||
timeout: 5000
|
||||
- name: "Factorial button visible"
|
||||
action: wait_for
|
||||
texts: ["Factorial"]
|
||||
timeout: 5000
|
||||
- name: "Fibonacci button visible"
|
||||
action: wait_for
|
||||
texts: ["Fibonacci"]
|
||||
timeout: 5000
|
||||
post_text: |
|
||||
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.
|
||||
|
||||
# ── Step 5b: Full functionality (with modules) ───────────────────────────
|
||||
# Requires ../logos-calc-module from Part 1. Use --phase modules to enable.
|
||||
- title: "Full functionality (with modules)"
|
||||
step: true
|
||||
text: |
|
||||
The standalone app automatically bundles and loads all module dependencies declared in `metadata.json`. To test with your local `calc_module` from Part 1, you first need to make sure it has been built and its shared library (`.so` on Linux, `.dylib` on macOS) is present.
|
||||
steps:
|
||||
- title: "Ensure `calc_module` is built"
|
||||
text: |
|
||||
Go back to your `logos-calc-module` directory and verify the shared library exists:
|
||||
run: "ls ../logos-calc-module/lib/libcalc.{ext}"
|
||||
code_block: |
|
||||
ls ../logos-calc-module/lib/libcalc.so # Linux
|
||||
ls ../logos-calc-module/lib/libcalc.dylib # macOS
|
||||
post_text: |
|
||||
If the file is missing, build it first (as covered in [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library)):
|
||||
extra_run:
|
||||
run: "cd ../logos-calc-module/lib && gcc {shared_flags} -o libcalc.{ext} libcalc.c && cd ../../logos-calc-ui"
|
||||
code_block: |
|
||||
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
|
||||
post_text: |
|
||||
Also make sure the module itself builds successfully:
|
||||
|
||||
- run: "cd ../logos-calc-module && git add -A && nix build && cd ../logos-calc-ui"
|
||||
code_block: |
|
||||
cd ../logos-calc-module
|
||||
git add -A
|
||||
nix build
|
||||
cd ../logos-calc-ui
|
||||
post_text: |
|
||||
The `nix build` produces `result/lib/calc_module_plugin.so` (or `.dylib`), which is the compiled Qt plugin. The `lib/libcalc.so` (or `.dylib`) inside the source tree is the underlying C library that gets linked in during the build.
|
||||
|
||||
- title: "Option A: Use `--override-input` (quick, no flake.nix edits)"
|
||||
text: |
|
||||
If your `flake.nix` points to a `github:` URL, you can override it at build time to use your local checkout:
|
||||
ui_test:
|
||||
launch: "nix run . --override-input calc_module path:../logos-calc-module"
|
||||
setup:
|
||||
- "nix build 'github:logos-co/logos-qt-mcp{release}' -o result-mcp"
|
||||
qt_mcp: "result-mcp"
|
||||
tests:
|
||||
- name: "App title visible"
|
||||
action: wait_for
|
||||
texts: ["Logos Calculator"]
|
||||
timeout: 15000
|
||||
- name: "All operation buttons visible"
|
||||
action: wait_for
|
||||
texts: ["Add", "Multiply", "Factorial", "Fibonacci"]
|
||||
timeout: 5000
|
||||
post_text: |
|
||||
This tells nix to resolve the `calc_module` flake input from your local directory instead of from the remote URL. Any changes you've made to `calc_module` locally (including the built `.so`/`.dylib` in `lib/`) are picked up immediately — no need to push to GitHub first.
|
||||
|
||||
- title: "Option B: Set `path:` in `flake.nix` (persistent local development)"
|
||||
text: |
|
||||
If you're iterating on both repos side by side, you can point the flake input directly to your local `calc_module` checkout. In `flake.nix`, change:
|
||||
|
||||
```nix
|
||||
# From remote:
|
||||
calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module";
|
||||
# To local:
|
||||
calc_module.url = "path:../logos-calc-module";
|
||||
```
|
||||
|
||||
Then run normally without overrides:
|
||||
|
||||
```bash
|
||||
nix flake update # re-lock with the local path
|
||||
git add flake.lock
|
||||
nix run .
|
||||
```
|
||||
|
||||
This is convenient when you always want to build against the local copy. Switch back to `github:` when you're ready to pin to a published version.
|
||||
|
||||
- title: "Option C: Pin to the remote repo"
|
||||
text: |
|
||||
If `calc_module` has been pushed to the remote repository (with the `.so`/`.dylib` committed in `lib/`), the `github:` URL in `flake.nix` already points to it. A plain `nix run .` fetches and builds `calc_module` from the remote:
|
||||
|
||||
```bash
|
||||
nix run .
|
||||
```
|
||||
|
||||
> **Important:** The remote repo must contain the built `.so`/`.dylib` in `lib/` (or the nix build must produce it). If the shared library is missing, the `calc_module` build will fail with linker errors.
|
||||
|
||||
Whichever option you choose, clicking **Add**, **Multiply**, **Factorial**, or **Fibonacci** now calls the real module.
|
||||
|
||||
# ── Step 6: Using the Logos Design System ──────────────────────────────────
|
||||
- title: "Using the Logos Design System"
|
||||
step: true
|
||||
text: |
|
||||
`logos-basecamp` (and `logos-standalone-app`) has `logos-design-system` on its QML import path. Use its themed components directly — no extra setup in your module.
|
||||
|
||||
```qml
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
import Logos.Icons // optional: shared icon assets (LogosIcons.search, .install, .refresh, …)
|
||||
```
|
||||
|
||||
### Why use it
|
||||
|
||||
Hardcoding colors, font sizes, or rolling your own button means your module looks subtly different from every other module in basecamp, drifts as the design evolves, and re-implements work the design system already does. Using `Logos.Controls` + `Theme` tokens means your module gets the polished look automatically as the design system is updated — no churn on your side.
|
||||
|
||||
### What's available
|
||||
|
||||
Run the storybook to browse every component interactively with live property editors:
|
||||
|
||||
```bash
|
||||
cd repos/logos-design-system
|
||||
nix run # or: ws run logos-design-system
|
||||
```
|
||||
|
||||
The sidebar splits components into two sections:
|
||||
|
||||
- **Controls** — *designed per Figma, production-ready*. Use these directly. Examples: `LogosButton`, `LogosBadge`, `LogosCheckbox`, `LogosComboBox`, `LogosIconButton`, `LogosPaginator`, `LogosSearchBar`, `LogosTabBar` / `LogosTabButton`, `LogosTable` / `LogosTableColumn`, `LogosText`, `LogosTextField`, `LogosToolTip`.
|
||||
- **Controls (not designed)** — *placeholders with stable APIs but unstyled visuals*. Functional, you can ship with them, and you'll inherit the polished look automatically when each gets its design pass — no QML changes on your side. Examples: `LogosDialog`, `LogosDrawer`, `LogosFrame`, `LogosGroupBox`, `LogosItemDelegate`, `LogosMenu`, `LogosProgressBar`, `LogosRadioButton`, `LogosScrollBar` / `LogosScrollView`, `LogosSlider`, `LogosSpinBox`, `LogosSpinner`, `LogosStackView`, `LogosSwitch`, `LogosTextArea`, `LogosToolBar`.
|
||||
|
||||
Each storybook page exposes a `designed: true/false` flag if you want to see at a glance which it is.
|
||||
|
||||
### Replace raw Qt controls with Logos equivalents
|
||||
|
||||
```qml
|
||||
// Instead of Button:
|
||||
LogosButton {
|
||||
text: qsTr("Add")
|
||||
onClicked: callTwoOp("add", inputA.text, inputB.text)
|
||||
}
|
||||
|
||||
// Instead of TextField:
|
||||
LogosTextField {
|
||||
id: inputA
|
||||
placeholderText: qsTr("a")
|
||||
}
|
||||
|
||||
// Use theme colors instead of hardcoded hex values:
|
||||
Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
Text { color: Theme.palette.text }
|
||||
}
|
||||
```
|
||||
|
||||
### Theme tokens — avoid hardcoding magic numbers
|
||||
|
||||
```qml
|
||||
// Palette — Theme.palette.*
|
||||
// background, backgroundSecondary, backgroundMuted, surface,
|
||||
// text, textSecondary, textMuted, textTertiary,
|
||||
// border, borderSubtle, primary, success, warning, error, info, hover, pressed, …
|
||||
|
||||
// Spacing — Theme.spacing.*
|
||||
// tiny, small, medium, large, xlarge, xxlarge,
|
||||
// radiusSmall, radiusMedium, radiusLarge
|
||||
|
||||
// Typography — Theme.typography.*
|
||||
// pageTitleText (36), titleText (30), panelTitleText (24),
|
||||
// subtitleText (16), primaryText (14), secondaryText (12),
|
||||
// weightRegular (400), weightMedium (500), weightBold (700),
|
||||
// publicSans (font family)
|
||||
|
||||
// Icons — Logos.Icons.LogosIcons.*
|
||||
// arrowLeft, arrowRight, refresh, install, trash, more, search, …
|
||||
```
|
||||
|
||||
If a token you need is missing, file a feature issue — don't inline a hex literal or a magic number; that just stores up drift.
|
||||
|
||||
### Feedback and contributions
|
||||
|
||||
Feel free to report bugs, file feature requests, or contribute components / theme tokens upstream — all welcome at `logos-co/logos-design-system`. The same fix lifts every consumer, so upstreaming is the most impactful path. If you can sketch the public API you'd like to use in a feature request, it makes review and implementation much faster.
|
||||
|
||||
# ── Step 7: Load in logos-basecamp ─────────────────────────────────────────
|
||||
- title: "Load in `logos-basecamp`"
|
||||
step: true
|
||||
steps:
|
||||
- title: "Bundle as LGX packages"
|
||||
text: |
|
||||
Create `.lgx` packages for both dev and portable variants. Use `--out-link` to avoid overwriting the `result` symlink:
|
||||
run: "cd ../logos-calc-module && nix build '.#lgx' --out-link result-lgx && nix build '.#lgx-portable' --out-link result-lgx-portable && cd ../logos-calc-ui"
|
||||
code_block: |
|
||||
# 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 QML UI plugin
|
||||
cd ../logos-calc-ui
|
||||
nix build '.#lgx' --out-link result-lgx
|
||||
nix build '.#lgx-portable' --out-link result-lgx-portable
|
||||
extra_run:
|
||||
run: "nix build '.#lgx' --out-link result-lgx && nix build '.#lgx-portable' --out-link result-lgx-portable"
|
||||
post_text: |
|
||||
> 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).
|
||||
|
||||
- title: "Build and run logos-basecamp"
|
||||
text: |
|
||||
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.
|
||||
run: "nix build 'github:logos-co/logos-basecamp{release}' -o basecamp-result"
|
||||
post_text: |
|
||||
```bash
|
||||
# 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`).
|
||||
|
||||
- title: "Install modules with lgpm"
|
||||
text: |
|
||||
Install your modules using `lgpm`. First, set `BASECAMP_DIR` to your platform's path:
|
||||
|
||||
```bash
|
||||
# macOS:
|
||||
BASECAMP_DIR="$HOME/Library/Application Support/Logos/LogosBasecampDev"
|
||||
|
||||
# Linux:
|
||||
BASECAMP_DIR="$HOME/.local/share/Logos/LogosBasecampDev"
|
||||
```
|
||||
run: "nix build 'github:logos-co/logos-package-manager{release}#cli' --out-link ./pm"
|
||||
post_text: |
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
- title: "Portable basecamp build (optional)"
|
||||
text: |
|
||||
The dev build above depends on nix store paths at runtime. For a self-contained portable build that works without nix:
|
||||
run: "nix build 'github:logos-co/logos-basecamp{release}#bin-bundle-dir' -o basecamp-portable"
|
||||
post_text: |
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
# 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 `.lgx` variants (`result-lgx-portable`), and the dev build requires dev variants (`result-lgx`). Mixing them will cause loading failures.
|
||||
|
||||
- title: "Install via logos-basecamp UI"
|
||||
text: |
|
||||
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/*.lgx` — installs `calc_module`
|
||||
5. Repeat for `result-lgx/*.lgx` — installs `calc_ui`
|
||||
|
||||
The "Calculator UI" tab appears in the sidebar. Clicking it loads your `Main.qml`.
|
||||
|
||||
- title: "Live reloading with `logos-standalone-app`"
|
||||
text: |
|
||||
For QML iteration, set `DEV_QML_PATH` to the directory that contains your view entry file (the basename from `metadata.json` `view` must exist under that directory). For this tutorial's layout (`view`: `Main.qml` at repo root):
|
||||
|
||||
```bash
|
||||
DEV_QML_PATH=$PWD nix run .
|
||||
```
|
||||
|
||||
When `DEV_QML_PATH` is set, `logos-standalone-app` loads QML from your source tree at runtime instead of the installed copy — so edits in `Main.qml` are picked up on the next relaunch without you having to manually re-sync files.
|
||||
|
||||
**Important — what this does *not* skip.** `nix run` always re-evaluates the flake and rehashes the source tree before launching. By default `src = ./.` includes every tracked file, including `*.qml` — so:
|
||||
|
||||
- **Any source change, including QML edits, rebuilds the plugin** before the app starts. `DEV_QML_PATH` only kicks in *after* the build is done; it doesn't shortcut the rebuild itself.
|
||||
- **C++ / `.rep` / `metadata.json` / CMake changes** rebuild as normal.
|
||||
- The flake-evaluation overhead on each `nix run` is fixed and unavoidable while invoking through nix.
|
||||
|
||||
For the absolute fastest loop (no nix involvement after the first build), do the build once and run the resulting binary directly:
|
||||
|
||||
```bash
|
||||
# Build once — populates result/ in the nix store
|
||||
nix build .
|
||||
|
||||
# Subsequent runs: invoke the bundled standalone wrapper directly,
|
||||
# skipping nix entirely. DEV_QML_PATH still redirects QML loading.
|
||||
DEV_QML_PATH=$PWD ./result/bin/run-logos-standalone-ui
|
||||
```
|
||||
|
||||
(Adjust the binary name to whatever `ls result/bin/` shows on your build.)
|
||||
|
||||
> **Naming:** Only `DEV_QML_PATH` is honored. See `repos/logos-standalone-app/README.md`.
|
||||
|
||||
> This does not work with `logos-basecamp`. Basecamp loads QML plugins from its own data directory, so changes to your source files are not reflected until you rebuild and reinstall the `.lgx` package.
|
||||
|
||||
- title: "Testing without any runtime"
|
||||
text: |
|
||||
You can open `Main.qml` in any QML viewer (e.g., `qml` from Qt) to test the layout.
|
||||
|
||||
#### Install
|
||||
|
||||
You'll need to have QML and any included modules (`QtQuick` and submodules `Controls`, and `Layout`).
|
||||
|
||||
Eg, to simply install on linux (apt package manager):
|
||||
|
||||
```bash
|
||||
sudo apt install qml-qt6 qml6-module-qtquick qml6-module-qtquick-controls qml6-module-qtquick-layouts
|
||||
```
|
||||
|
||||
#### Viewing the QML
|
||||
|
||||
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 and included modules installed
|
||||
# macOS:
|
||||
qml Main.qml
|
||||
|
||||
# Linux:
|
||||
qml6 Main.qml
|
||||
```
|
||||
|
||||
# ── Step 8: UI Integration Tests ─────────────────────────────────────────
|
||||
- title: "UI Integration Tests"
|
||||
step: true
|
||||
text: |
|
||||
You can add automated UI tests that verify your QML plugin renders correctly. The test infrastructure is built into `logos-module-builder` — just add `.mjs` test files to a `tests/` directory and you get `nix build .#integration-test` for free.
|
||||
|
||||
Tests use the [logos-qt-mcp](https://github.com/logos-co/logos-qt-mcp) test framework, which connects to the QML inspector inside `logos-standalone-app` and can find elements, click buttons, verify text, and take screenshots.
|
||||
steps:
|
||||
- title: "Create a test file"
|
||||
text: |
|
||||
Create `tests/ui-tests.mjs`:
|
||||
file:
|
||||
path: tests/ui-tests.mjs
|
||||
language: javascript
|
||||
content: |
|
||||
import { resolve } from "node:path";
|
||||
|
||||
// CI sets LOGOS_QT_MCP automatically; for interactive use: nix build .#test-framework -o result-mcp
|
||||
const root =
|
||||
process.env.LOGOS_QT_MCP ||
|
||||
new URL("../result-mcp", import.meta.url).pathname;
|
||||
const { test, run } = await import(
|
||||
resolve(root, "test-framework/framework.mjs")
|
||||
);
|
||||
|
||||
test("calc_ui: loads and shows title", async (app) => {
|
||||
await app.waitFor(
|
||||
async () => {
|
||||
await app.expectTexts(["Logos Calculator"]);
|
||||
},
|
||||
{ timeout: 15000, interval: 500, description: "calc_ui to load" },
|
||||
);
|
||||
});
|
||||
|
||||
test("calc_ui: add button visible", async (app) => {
|
||||
await app.expectTexts(["Add"]);
|
||||
});
|
||||
|
||||
test("calc_ui: click add shows validation", async (app) => {
|
||||
await app.click("Add");
|
||||
await app.waitFor(
|
||||
async () => {
|
||||
await app.expectTexts(["Enter values for a and b"]);
|
||||
},
|
||||
{ timeout: 5000, interval: 500, description: "validation message to appear" },
|
||||
);
|
||||
});
|
||||
|
||||
run();
|
||||
|
||||
- title: "Run the tests"
|
||||
run: "git add tests/"
|
||||
- run: "nix build .#integration-test -L --override-input calc_module path:../logos-calc-module"
|
||||
code_block: |
|
||||
nix build .#integration-test -L
|
||||
post_text: |
|
||||
The `integration-test` output launches `logos-standalone-app` with `QT_QPA_PLATFORM=offscreen` (no display needed), connects to the QML inspector, and runs all `.mjs` files in `tests/`.
|
||||
|
||||
You can have multiple test files (e.g., `tests/smoke.mjs`, `tests/interactions.mjs`) — they are all discovered and run automatically.
|
||||
|
||||
To run tests interactively (against an already-running app):
|
||||
|
||||
```bash
|
||||
nix build .#test-framework -o result-mcp
|
||||
nix run . # start the app with inspector on :3768
|
||||
node tests/ui-tests.mjs # in another terminal
|
||||
```
|
||||
|
||||
# ── Known Limitations (prose only) ─────────────────────────────────────────
|
||||
- title: "Known Limitations"
|
||||
text: |
|
||||
### QML-to-C++ type coercion
|
||||
|
||||
When calling C++ module methods from QML via `logos.callModule()`, arguments are passed through IPC as `QVariant` values. The runtime automatically coerces mismatched types to match the target method signature — for example, a `double` sent from QML will be converted to `int` if the method expects `int`, and numeric strings will be converted to their numeric types.
|
||||
|
||||
This means you can define methods with their natural parameter types (`int`, `bool`, `double`, etc.) and calls from QML will work without manual conversion:
|
||||
|
||||
```cpp
|
||||
// This works — the runtime coerces arguments automatically
|
||||
Q_INVOKABLE int add(int a, int b) { return a + b; }
|
||||
```
|
||||
|
||||
> **Note:** Type coercion uses `QVariant::convert()`, which rounds (not truncates) when converting `double` to `int` — e.g., `3.7` becomes `4`.
|
||||
|
||||
### QML changes not appearing after rebuild
|
||||
|
||||
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
|
||||
# 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 (prose only) ─────────────────────────────────────────────────────
|
||||
- title: "Recap"
|
||||
text: |
|
||||
| | Core Module (Part 1) | QML UI Plugin (Part 2) |
|
||||
| ------------------- | ----------------------------------------------- | ----------------------------- |
|
||||
| Language | C++ | QML / JavaScript |
|
||||
| Files | `.cpp`, `.h`, `CMakeLists.txt`, `metadata.json` | `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 (prose only) ───────────────────────────────────────────────
|
||||
- title: "What's Next"
|
||||
text: |
|
||||
- **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
|
||||
File diff suppressed because it is too large
Load Diff
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wrapper that ensures python3 + pyyaml are available via nix-shell.
|
||||
# Usage: run-tutorial run <spec.yaml> [OPTIONS]
|
||||
# run-tutorial generate <spec.yaml> [-o output.md]
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# If python3 + yaml are already available, run directly
|
||||
if python3 -c "import yaml" 2>/dev/null; then
|
||||
exec python3 "$SCRIPT_DIR/tutorial_runner.py" "$@"
|
||||
fi
|
||||
|
||||
# Otherwise, use nix-shell to provide them
|
||||
exec nix-shell -p python3 python3Packages.pyyaml --run \
|
||||
"python3 \"$SCRIPT_DIR/tutorial_runner.py\" $*"
|
||||
File diff suppressed because it is too large
Load Diff
+181
-80
@@ -4,7 +4,7 @@ This is Part 3 of the Logos module tutorial series. In [Part 2](tutorial-qml-ui-
|
||||
|
||||
**What you'll build:** A `calc_ui_cpp` module with:
|
||||
|
||||
- A `.rep` file defining the remote interface (slots + properties)
|
||||
- A `.rep` file defining the remote interface (slots)
|
||||
- A C++ backend plugin that inherits from the generated `SimpleSource` base class
|
||||
- A QML view that calls the backend via a typed replica using `logos.watch()`
|
||||
- Process isolation: backend crashes can't bring down the host app
|
||||
@@ -20,7 +20,7 @@ This is Part 3 of the Logos module tutorial series. In [Part 2](tutorial-qml-ui-
|
||||
| QML ↔ backend | Direct bridge | Qt Remote Objects (typed replica) |
|
||||
| `.rep` file | Not needed | Required — defines the remote interface |
|
||||
|
||||
**Prerequisites:**
|
||||
## Prerequisites
|
||||
|
||||
- Completed [Part 1](tutorial-wrapping-c-library.md) — you have a working `calc_module` with the shared library built (`.so` on Linux, `.dylib` on macOS in `logos-calc-module/lib/`)
|
||||
- Nix with flakes enabled
|
||||
@@ -63,21 +63,27 @@ The `.rep` file declares the interface. At build time, Qt's `repc` compiler gene
|
||||
- **`CalcUiCppReplica`** — typed replica the QML view uses
|
||||
- **`calc_ui_cpp_replica_factory`** — separate plugin that the host loads to create typed replicas
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Scaffold
|
||||
|
||||
Create a new directory and initialise it from the C++ backend UI template:
|
||||
|
||||
`mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp`
|
||||
|
||||
```bash
|
||||
mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp
|
||||
nix flake init -t github:logos-co/logos-module-builder#ui-qml-backend
|
||||
git init && git add -A
|
||||
```
|
||||
|
||||
This creates the template. We'll customize it for our calculator.
|
||||
|
||||
```bash
|
||||
git init && git add -A
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: metadata.json
|
||||
## Step 2: `metadata.json`
|
||||
|
||||
Replace the template contents with your plugin's details:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -85,20 +91,36 @@ This creates the template. We'll customize it for our calculator.
|
||||
"version": "1.0.0",
|
||||
"type": "ui_qml",
|
||||
"category": "tools",
|
||||
"description": "Calculator C++ UI — QML view with process-isolated backend",
|
||||
"description": "Calculator C++ UI — QML view with process-isolated backend for calc_module",
|
||||
"main": "calc_ui_cpp_plugin",
|
||||
"view": "qml/Main.qml",
|
||||
"icon": "icons/calc.png",
|
||||
"dependencies": ["calc_module"],
|
||||
|
||||
"nix": {
|
||||
"packages": { "build": [], "runtime": [] },
|
||||
"packages": {
|
||||
"build": [],
|
||||
"runtime": []
|
||||
},
|
||||
"external_libraries": [],
|
||||
"cmake": { "find_packages": [], "extra_sources": [] }
|
||||
"cmake": {
|
||||
"find_packages": [],
|
||||
"extra_sources": [],
|
||||
"extra_include_dirs": [],
|
||||
"extra_link_libraries": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create the icon directory and add a placeholder icon (displayed in the `logos-basecamp` sidebar when the module is loaded):
|
||||
|
||||
```bash
|
||||
mkdir -p icons
|
||||
# Copy any PNG here — or generate a 64×64 placeholder:
|
||||
echo "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAmElEQVR4nO3QMREAIBDAsFeEN3ziCWRkoEP2XmedfX82OkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAO0BN/SiO/PatoIAAAAASUVORK5CYII=" | base64 -d > icons/calc.png
|
||||
```
|
||||
|
||||
Key fields:
|
||||
|
||||
- `"type": "ui_qml"` — tells the builder this is a QML view module
|
||||
@@ -115,8 +137,6 @@ Create `src/calc_ui_cpp.rep`:
|
||||
```rep
|
||||
class CalcUiCpp
|
||||
{
|
||||
PROP(QString status READWRITE)
|
||||
|
||||
SLOT(int add(int a, int b))
|
||||
SLOT(int multiply(int a, int b))
|
||||
SLOT(int factorial(int n))
|
||||
@@ -128,25 +148,25 @@ class CalcUiCpp
|
||||
This is the **single source of truth** for the remote interface. `repc` generates:
|
||||
|
||||
- `rep_calc_ui_cpp_source.h` — `CalcUiCppSimpleSource` with virtual slots the backend overrides
|
||||
- `rep_calc_ui_cpp_replica.h` — `CalcUiCppReplica` with typed methods and auto-synced properties
|
||||
- `rep_calc_ui_cpp_replica.h` — `CalcUiCppReplica` with typed methods
|
||||
|
||||
**PROP** values auto-sync from backend to QML replica. **SLOT** return values are delivered as `QRemoteObjectPendingReply` — use `logos.watch()` in QML to get them as JS Promises.
|
||||
**SLOT** return values are delivered as `QRemoteObjectPendingReply` — use `logos.watch()` in QML to get them as JS Promises. You can also declare **PROP** entries (e.g. `PROP(QString status READWRITE)`) which auto-sync from the backend to the QML replica.
|
||||
|
||||
---
|
||||
|
||||
## Step 3.1: Update the interface header
|
||||
## Step 4: Interface header
|
||||
|
||||
The scaffolded template may create an interface file like `src/ui_example_interface.h`. Rename it to match this tutorial and make sure the class/IID names are updated, or the plugin metadata wiring will break.
|
||||
The scaffolded template creates a set of `ui_example` files (`src/ui_example.rep`, `src/ui_example_interface.h`, `src/ui_example_plugin.{h,cpp}`). We replace them with `calc_ui_cpp` equivalents, so remove the example sources first — leaving them around with mismatched class/IID names just invites build errors or plugin-load failures at runtime:
|
||||
|
||||
```bash
|
||||
# If your scaffold created ui_example files, rename the interface header:
|
||||
mv src/ui_example_interface.h src/calc_ui_cpp_interface.h
|
||||
rm -f src/ui_example.rep src/ui_example_interface.h src/ui_example_plugin.h src/ui_example_plugin.cpp
|
||||
```
|
||||
|
||||
Set `src/calc_ui_cpp_interface.h` to:
|
||||
Now create `src/calc_ui_cpp_interface.h`:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#ifndef CALC_UI_CPP_INTERFACE_H
|
||||
#define CALC_UI_CPP_INTERFACE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
@@ -160,9 +180,12 @@ public:
|
||||
|
||||
#define CalcUiCppInterface_iid "org.logos.CalcUiCppInterface"
|
||||
Q_DECLARE_INTERFACE(CalcUiCppInterface, CalcUiCppInterface_iid)
|
||||
|
||||
#endif // CALC_UI_CPP_INTERFACE_H
|
||||
```
|
||||
|
||||
Your plugin header should then include `calc_ui_cpp_interface.h` and use:
|
||||
|
||||
- `Q_PLUGIN_METADATA(IID CalcUiCppInterface_iid FILE "metadata.json")`
|
||||
- `Q_INTERFACES(CalcUiCppInterface)`
|
||||
|
||||
@@ -170,7 +193,7 @@ If the interface filename or IID symbol doesn't match, you'll typically get buil
|
||||
|
||||
---
|
||||
|
||||
## Step 4: CMakeLists.txt
|
||||
## Step 5: `CMakeLists.txt`
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
@@ -195,17 +218,18 @@ logos_module(
|
||||
`REP_FILE` tells `logos_module()` to:
|
||||
|
||||
1. Run `repc` to generate source/replica headers
|
||||
2. Generate `LogosViewPluginBase` (typed remoting base class)
|
||||
2. Generate `CalcUiCppViewPluginBase` (typed remoting base class)
|
||||
3. Build a separate `calc_ui_cpp_replica_factory` shared library
|
||||
|
||||
---
|
||||
|
||||
## Step 5: C++ Backend Plugin
|
||||
## Step 6: C++ Backend Plugin
|
||||
|
||||
### `src/calc_ui_cpp_plugin.h`
|
||||
### 6.1 `src/calc_ui_cpp_plugin.h`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#ifndef CALC_UI_CPP_PLUGIN_H
|
||||
#define CALC_UI_CPP_PLUGIN_H
|
||||
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
@@ -216,6 +240,9 @@ logos_module(
|
||||
class LogosAPI;
|
||||
class LogosModules;
|
||||
|
||||
// Inherits CalcUiCppSimpleSource (generated from calc_ui_cpp.rep) so
|
||||
// enableRemoting() can publish the typed source and QML replicas get
|
||||
// auto-synced properties + callable slots.
|
||||
class CalcUiCppPlugin : public CalcUiCppSimpleSource,
|
||||
public CalcUiCppInterface,
|
||||
public CalcUiCppViewPluginBase
|
||||
@@ -233,7 +260,8 @@ public:
|
||||
|
||||
Q_INVOKABLE void initLogos(LogosAPI* api);
|
||||
|
||||
// Slots from .rep — override the generated virtuals
|
||||
// Slots from calc_ui_cpp.rep — return values directly. The QML replica
|
||||
// receives QRemoteObjectPendingReply; use logos.watch() in QML to get the value.
|
||||
int add(int a, int b) override;
|
||||
int multiply(int a, int b) override;
|
||||
int factorial(int n) override;
|
||||
@@ -247,6 +275,8 @@ private:
|
||||
LogosAPI* m_logosAPI = nullptr;
|
||||
LogosModules* m_logos = nullptr;
|
||||
};
|
||||
|
||||
#endif // CALC_UI_CPP_PLUGIN_H
|
||||
```
|
||||
|
||||
Three base classes:
|
||||
@@ -255,23 +285,23 @@ Three base classes:
|
||||
- **`CalcUiCppInterface`** — standard Logos plugin interface (`name()`, `version()`)
|
||||
- **`CalcUiCppViewPluginBase`** — generated, provides `setBackend()` and `enableRemoting()`
|
||||
|
||||
### `src/calc_ui_cpp_plugin.cpp`
|
||||
### 6.2 `src/calc_ui_cpp_plugin.cpp`
|
||||
|
||||
```cpp
|
||||
#include "calc_ui_cpp_plugin.h"
|
||||
#include "logos_api.h"
|
||||
#include "logos_sdk.h"
|
||||
|
||||
CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent)
|
||||
: CalcUiCppSimpleSource(parent) {}
|
||||
|
||||
CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : CalcUiCppSimpleSource(parent) {}
|
||||
CalcUiCppPlugin::~CalcUiCppPlugin() { delete m_logos; }
|
||||
|
||||
void CalcUiCppPlugin::initLogos(LogosAPI* api)
|
||||
{
|
||||
if (m_logos) return;
|
||||
m_logosAPI = api;
|
||||
m_logos = new LogosModules(api);
|
||||
// Register this object as the Remote Objects source
|
||||
// Register this object as the Remote Objects source so the QML replica
|
||||
// can see its properties and call its slots.
|
||||
setBackend(this);
|
||||
}
|
||||
|
||||
@@ -310,7 +340,7 @@ Key points:
|
||||
|
||||
---
|
||||
|
||||
## Step 6: QML View
|
||||
## Step 7: QML View
|
||||
|
||||
Create `src/qml/Main.qml`:
|
||||
|
||||
@@ -319,19 +349,17 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string result: ""
|
||||
property string errorText: ""
|
||||
|
||||
// Typed replica of the backend running in ui-host
|
||||
// Typed replica of the backend running in ui-host (generated from calc_ui_cpp.rep).
|
||||
readonly property var backend: logos.module("calc_ui_cpp")
|
||||
|
||||
// "status" property from the .rep — auto-synced via Qt Remote Objects
|
||||
readonly property string status: backend ? backend.status : ""
|
||||
|
||||
// logos.watch() delivers the result of a replica slot call via callbacks.
|
||||
// No QtRemoteObjects import needed — the bridge handles it.
|
||||
function callCalc(method, args) {
|
||||
if (!backend) {
|
||||
root.errorText = "Backend not available"
|
||||
@@ -339,7 +367,6 @@ Item {
|
||||
}
|
||||
root.errorText = ""
|
||||
root.result = "..."
|
||||
// logos.watch() wraps the pending reply in a JS Promise
|
||||
logos.watch(backend[method].apply(backend, args),
|
||||
function(value) { root.result = String(value) },
|
||||
function(error) { root.errorText = String(error) }
|
||||
@@ -352,52 +379,84 @@ Item {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Calculator (C++ backend)"
|
||||
text: "Logos Calculator (C++ backend)"
|
||||
font.pixelSize: 20
|
||||
color: "#ffffff"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
Layout.fillWidth: true
|
||||
|
||||
TextField {
|
||||
id: inputA; placeholderText: "a"
|
||||
id: inputA
|
||||
placeholderText: "a"
|
||||
Layout.preferredWidth: 80
|
||||
validator: IntValidator {}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: inputB; placeholderText: "b"
|
||||
id: inputB
|
||||
placeholderText: "b"
|
||||
Layout.preferredWidth: 80
|
||||
validator: IntValidator {}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Add"
|
||||
onClicked: root.callCalc("add", [parseInt(inputA.text) || 0,
|
||||
parseInt(inputB.text) || 0])
|
||||
onClicked: root.callCalc("add", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Multiply"
|
||||
onClicked: root.callCalc("multiply", [parseInt(inputA.text) || 0,
|
||||
parseInt(inputB.text) || 0])
|
||||
onClicked: root.callCalc("multiply", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
Layout.fillWidth: true
|
||||
|
||||
TextField {
|
||||
id: inputN
|
||||
placeholderText: "n"
|
||||
Layout.preferredWidth: 80
|
||||
validator: IntValidator { bottom: 0 }
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Factorial"
|
||||
onClicked: root.callCalc("factorial", [parseInt(inputN.text) || 0])
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Fibonacci"
|
||||
onClicked: root.callCalc("fibonacci", [parseInt(inputN.text) || 0])
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "libcalc version"
|
||||
onClicked: root.callCalc("libVersion", [])
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true; height: 56
|
||||
color: root.errorText ? "#3d1a1a" : "#1a2d1a"
|
||||
Layout.fillWidth: true
|
||||
height: 56
|
||||
color: root.errorText.length > 0 ? "#3d1a1a" : "#1a2d1a"
|
||||
radius: 8
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.errorText || root.result || "Press a button"
|
||||
color: root.errorText ? "#f85149" : "#56d364"
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Backend status: " + root.status
|
||||
color: "#8b949e"; font.pixelSize: 13
|
||||
}
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -405,13 +464,12 @@ Item {
|
||||
Key patterns:
|
||||
|
||||
- `logos.module("calc_ui_cpp")` — gets the typed replica (auto-synced properties)
|
||||
- `backend.status` — PROP from `.rep`, updates automatically
|
||||
- `logos.watch(backend.add(1, 2), ...)` — SLOT return value as JS Promise
|
||||
- ``— required for`logos.watch()`
|
||||
- The `logos` object is injected by the host at runtime — no `QtRemoteObjects` import needed
|
||||
|
||||
---
|
||||
|
||||
## Step 6.5: Use the Logos Design System in your QML
|
||||
## Step 8: Use the Logos Design System in your QML
|
||||
|
||||
The QML you load above runs inside the host (`logos-basecamp` / `logos-standalone-app`), which already has `logos-design-system` on the QML import path. Use its themed components rather than rolling your own visuals — your module gets the polished look automatically as the design system evolves.
|
||||
|
||||
@@ -462,11 +520,13 @@ Feel free to report bugs, file feature requests, or contribute components / them
|
||||
|
||||
---
|
||||
|
||||
## Step 7: flake.nix
|
||||
## Step 9: `flake.nix`
|
||||
|
||||
The template already wires everything up. Update the description and point `calc_module` at your dependency:
|
||||
|
||||
```nix
|
||||
{
|
||||
description = "Calculator C++ UI plugin — QML view with process-isolated backend";
|
||||
description = "Calculator C++ UI plugin for Logos - QML view with process-isolated backend for calc_module";
|
||||
|
||||
inputs = {
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
||||
@@ -478,7 +538,7 @@ Feel free to report bugs, file feature requests, or contribute components / them
|
||||
# calc_module.url = "path:../logos-calc-module";
|
||||
};
|
||||
|
||||
outputs = inputs@{ logos-module-builder, ... }:
|
||||
outputs = inputs@{ logos-module-builder, calc_module, ... }:
|
||||
logos-module-builder.lib.mkLogosQmlModule {
|
||||
src = ./.;
|
||||
configFile = ./metadata.json;
|
||||
@@ -498,20 +558,43 @@ The `calc_module` input attribute name must match the dependency name in `metada
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Build and Run
|
||||
## Step 10: Build and Run
|
||||
|
||||
First, make sure your local `calc_module` is built and its `.so`/`.dylib` is present in `lib/` (see [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library)):
|
||||
First, make sure your local `calc_module` is built and its shared library is present in `lib/` (see [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library)):
|
||||
|
||||
### 10.1 Ensure `calc_module` is built
|
||||
|
||||
```bash
|
||||
ls ../logos-calc-module/lib/libcalc.so # Linux
|
||||
ls ../logos-calc-module/lib/libcalc.dylib # macOS
|
||||
```
|
||||
|
||||
Then build and run. Choose the approach that matches your `flake.nix` setup:
|
||||
If the file is missing, build it first (as covered in [Part 1, Step 1.5](tutorial-wrapping-c-library.md#15-build-the-shared-library)):
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
### 10.2 Lock and build
|
||||
|
||||
Stage your files and lock the flake. Then build and run — choose the approach that matches your `flake.nix` setup.
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
```
|
||||
|
||||
```bash
|
||||
nix flake update
|
||||
```
|
||||
|
||||
```bash
|
||||
git add flake.lock
|
||||
```
|
||||
|
||||
```bash
|
||||
# If flake.nix uses path:../logos-calc-module — just run directly:
|
||||
nix run
|
||||
|
||||
@@ -522,12 +605,24 @@ nix run --override-input calc_module path:../logos-calc-module
|
||||
./scripts/ws run logos-calc-ui-cpp --local logos-calc-ui-cpp logos-calc-module
|
||||
```
|
||||
|
||||
### Live reloading QML with `DEV_QML_PATH`
|
||||
### 10.3 Launch and verify the UI
|
||||
|
||||
For QML iteration, point `DEV_QML_PATH` at the directory that contains your view entry's **basename** (from `metadata.json` `"view"`). This tutorial sets `"view": "qml/Main.qml"`, so the directory must contain `Main.qml` (here: `qml/` at the repo root):
|
||||
Launch the app and confirm the view loads with all of its controls. The backend runs in a separate `ui-host` process; clicking **Add** sends the call over Qt Remote Objects and the result comes back through `logos.watch()`.
|
||||
|
||||
```bash
|
||||
DEV_QML_PATH=$PWD/qml nix run .
|
||||
nix run . --override-input calc_module path:../logos-calc-module
|
||||
```
|
||||
|
||||
The result `8` comes from `calc_module.add(3, 5)` executed in the C++ backend — proof the full path (QML replica → Qt Remote Objects → ui-host backend → typed SDK → `calc_module`) works end to end.
|
||||
|
||||
---
|
||||
|
||||
## Step 11: Live reloading QML with `DEV_QML_PATH`
|
||||
|
||||
For QML iteration, point `DEV_QML_PATH` at the directory that contains your view entry's **basename** (from `metadata.json` `"view"`). This tutorial sets `"view": "qml/Main.qml"`, so the directory must contain `Main.qml` (here: `src/qml/`):
|
||||
|
||||
```bash
|
||||
DEV_QML_PATH=$PWD/src/qml nix run .
|
||||
```
|
||||
|
||||
When `DEV_QML_PATH` is set, `logos-standalone-app` loads QML from your source tree at runtime instead of the installed copy — so edits to `Main.qml` (and any QML under that tree) are picked up on the next relaunch without you having to re-sync files.
|
||||
@@ -546,7 +641,7 @@ nix build .
|
||||
|
||||
# Subsequent runs: invoke the bundled standalone wrapper directly,
|
||||
# skipping nix entirely. DEV_QML_PATH still redirects QML loading.
|
||||
DEV_QML_PATH=$PWD/qml ./result/bin/run-logos-standalone-ui
|
||||
DEV_QML_PATH=$PWD/src/qml ./result/bin/run-logos-standalone-ui
|
||||
```
|
||||
|
||||
(Adjust the binary name to whatever `ls result/bin/` shows on your build.)
|
||||
@@ -557,7 +652,7 @@ DEV_QML_PATH=$PWD/qml ./result/bin/run-logos-standalone-ui
|
||||
|
||||
---
|
||||
|
||||
## Step 9: How the Pieces Connect
|
||||
## Step 12: How the Pieces Connect
|
||||
|
||||
1. `nix build` → compiles the C++ plugin + replica factory, bundles QML view
|
||||
2. `nix run` → launches `logos-standalone-app` which:
|
||||
@@ -568,14 +663,17 @@ DEV_QML_PATH=$PWD/qml ./result/bin/run-logos-standalone-ui
|
||||
3. Host app loads `calc_ui_cpp_replica_factory.dylib` → creates a typed replica
|
||||
4. QML gets the replica via `logos.module("calc_ui_cpp")`
|
||||
5. `backend.add(1, 2)` → Qt Remote Objects sends call to ui-host → backend runs → returns result
|
||||
6. `backend.status` auto-syncs whenever the backend calls `setStatus(...)`
|
||||
|
||||
---
|
||||
|
||||
## Step 10: UI Integration Tests (Optional)
|
||||
## Step 13: UI Integration Tests
|
||||
|
||||
Add automated UI tests using the [logos-qt-mcp](https://github.com/logos-co/logos-qt-mcp) test framework. Just create `.mjs` files in `tests/` and `logos-module-builder` auto-wires `nix build .#integration-test`.
|
||||
|
||||
Tests connect to the QML inspector inside `logos-standalone-app` and can find elements, click buttons, verify text, and take screenshots.
|
||||
|
||||
### 13.1 Create a test file
|
||||
|
||||
Create `tests/ui-tests.mjs`:
|
||||
|
||||
```javascript
|
||||
@@ -592,30 +690,35 @@ const { test, run } = await import(
|
||||
test("calc_ui_cpp: loads and shows title", async (app) => {
|
||||
await app.waitFor(
|
||||
async () => {
|
||||
await app.expectTexts(["UI Example (C++ backend)"]);
|
||||
await app.expectTexts(["Logos Calculator (C++ backend)"]);
|
||||
},
|
||||
{ timeout: 15000, interval: 500, description: "UI to load" },
|
||||
);
|
||||
});
|
||||
|
||||
test("calc_ui_cpp: shows connection status", async (app) => {
|
||||
await app.expectTexts(["Connecting to backend..."]);
|
||||
});
|
||||
|
||||
test("calc_ui_cpp: add button visible", async (app) => {
|
||||
await app.expectTexts(["Add"]);
|
||||
test("calc_ui_cpp: operation buttons visible", async (app) => {
|
||||
await app.expectTexts(["Add", "Multiply", "Factorial", "Fibonacci"]);
|
||||
});
|
||||
|
||||
run();
|
||||
```
|
||||
|
||||
### 13.2 Run the tests
|
||||
|
||||
```bash
|
||||
git add tests/
|
||||
```
|
||||
|
||||
```bash
|
||||
# Hermetic CI test
|
||||
nix build .#integration-test -L
|
||||
```
|
||||
|
||||
# Interactive
|
||||
The `integration-test` output launches `logos-standalone-app` with `QT_QPA_PLATFORM=offscreen` (no display needed), connects to the QML inspector, and runs all `.mjs` files in `tests/`.
|
||||
|
||||
To run tests interactively (against an already-running app):
|
||||
|
||||
```bash
|
||||
nix build .#test-framework -o result-mcp
|
||||
nix run . # app with inspector on :3768
|
||||
node tests/ui-tests.mjs # in another terminal
|
||||
@@ -632,12 +735,10 @@ node tests/ui-tests.mjs # in another terminal
|
||||
| **Signal** | `SIGNAL(errorOccurred(QString msg))` | `emit errorOccurred("fail")` | `Connections { target: backend; function onErrorOccurred(msg) {...} }` |
|
||||
| **Model** | (use Q_PROPERTY on backend) | `Q_PROPERTY(QAbstractItemModel* items ...)` | `logos.model("calc_ui_cpp", "items")` |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Add more `.rep` properties/signals for richer UI state
|
||||
- Use `logos.model()` for list views backed by `QAbstractItemModel`
|
||||
- Package as `.lgx` for distribution: `nix build .#lgx`
|
||||
- **Use the Logos Design System** in your QML — see [Step 6.5](#step-65-use-the-logos-design-system-in-your-qml). Browse components in the storybook (`cd repos/logos-design-system && nix run`); file issues at `logos-co/logos-design-system` (bugs on designed components, *feature* type for new components / variants / theme tokens).
|
||||
- **Use the Logos Design System** in your QML — see [Step 8](#step-8-use-the-logos-design-system-in-your-qml). Browse components in the storybook (`cd repos/logos-design-system && nix run`); file issues at `logos-co/logos-design-system`.
|
||||
- See [logos-package-manager-ui](https://github.com/logos-co/logos-package-manager-ui) for a production example
|
||||
|
||||
+66
-41
@@ -11,7 +11,7 @@ This is Part 2 of the Logos module tutorial series. In [Part 1](tutorial-wrappin
|
||||
- The project structure and metadata for a QML plugin
|
||||
- How to package and install your UI into `logos-basecamp`
|
||||
|
||||
**Prerequisites:**
|
||||
## Prerequisites
|
||||
|
||||
- Completed [Part 1](tutorial-wrapping-c-library.md) — you have a working `calc_module` with the shared library built (`.so` on Linux, `.dylib` on macOS in `logos-calc-module/lib/`)
|
||||
- Nix with flakes enabled (same as Part 1)
|
||||
@@ -40,20 +40,26 @@ Key points:
|
||||
- **The `logos` bridge** is injected by the host. Call core modules with `logos.callModule("module", "method", [args])`.
|
||||
- **Entry point** is defined by the required `"view"` field in `metadata.json` (for this tutorial it is `Main.qml`).
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Scaffold
|
||||
|
||||
Use the QML module template from `logos-module-builder`:
|
||||
Create a new directory and initialise it from the QML module template:
|
||||
|
||||
`mkdir logos-calc-ui && cd logos-calc-ui`
|
||||
|
||||
```bash
|
||||
mkdir logos-calc-ui && cd logos-calc-ui
|
||||
nix flake init -t github:logos-co/logos-module-builder#ui-qml
|
||||
git init && git add -A
|
||||
```
|
||||
|
||||
> **Note:** The generated `flake.nix` uses an unpinned `logos-module-builder` URL. Replace it with the pinned version shown in [Step 4](#step-4-update-flakenix) to ensure reproducible builds.
|
||||
|
||||
```bash
|
||||
git init
|
||||
```
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
```
|
||||
|
||||
This gives you:
|
||||
|
||||
```
|
||||
@@ -361,18 +367,29 @@ The `calc_module.url` can be either:
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
nix flake update # regenerate flake.lock to match the pinned inputs in flake.nix
|
||||
```
|
||||
|
||||
```bash
|
||||
nix flake update
|
||||
```
|
||||
|
||||
```bash
|
||||
git add flake.lock
|
||||
```
|
||||
|
||||
```bash
|
||||
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)
|
||||
---
|
||||
|
||||
## Step 6: 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, you first need to make sure it has been built and its shared library (`.so` on Linux, `.dylib` on macOS) is present.
|
||||
|
||||
#### Ensure `calc_module` is built
|
||||
### 6.1 Ensure `calc_module` is built
|
||||
|
||||
Go back to your `logos-calc-module` directory and verify the shared library exists:
|
||||
|
||||
@@ -401,7 +418,7 @@ cd ../logos-calc-ui
|
||||
|
||||
The `nix build` produces `result/lib/calc_module_plugin.so` (or `.dylib`), which is the compiled Qt plugin. The `lib/libcalc.so` (or `.dylib`) inside the source tree is the underlying C library that gets linked in during the build.
|
||||
|
||||
#### Option A: Use `--override-input` (quick, no flake.nix edits)
|
||||
### 6.2 Option A: Use `--override-input` (quick, no flake.nix edits)
|
||||
|
||||
If your `flake.nix` points to a `github:` URL, you can override it at build time to use your local checkout:
|
||||
|
||||
@@ -411,7 +428,7 @@ nix run . --override-input calc_module path:../logos-calc-module
|
||||
|
||||
This tells nix to resolve the `calc_module` flake input from your local directory instead of from the remote URL. Any changes you've made to `calc_module` locally (including the built `.so`/`.dylib` in `lib/`) are picked up immediately — no need to push to GitHub first.
|
||||
|
||||
#### Option B: Set `path:` in `flake.nix` (persistent local development)
|
||||
### 6.3 Option B: Set `path:` in `flake.nix` (persistent local development)
|
||||
|
||||
If you're iterating on both repos side by side, you can point the flake input directly to your local `calc_module` checkout. In `flake.nix`, change:
|
||||
|
||||
@@ -432,7 +449,7 @@ nix run .
|
||||
|
||||
This is convenient when you always want to build against the local copy. Switch back to `github:` when you're ready to pin to a published version.
|
||||
|
||||
#### Option C: Pin to the remote repo
|
||||
### 6.4 Option C: Pin to the remote repo
|
||||
|
||||
If `calc_module` has been pushed to the remote repository (with the `.so`/`.dylib` committed in `lib/`), the `github:` URL in `flake.nix` already points to it. A plain `nix run .` fetches and builds `calc_module` from the remote:
|
||||
|
||||
@@ -446,7 +463,7 @@ Whichever option you choose, clicking **Add**, **Multiply**, **Factorial**, or *
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Using the Logos Design System
|
||||
## Step 7: Using the Logos Design System
|
||||
|
||||
`logos-basecamp` (and `logos-standalone-app`) has `logos-design-system` on its QML import path. Use its themed components directly — no extra setup in your module.
|
||||
|
||||
@@ -528,9 +545,9 @@ Feel free to report bugs, file feature requests, or contribute components / them
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Load in `logos-basecamp`
|
||||
## Step 8: Load in `logos-basecamp`
|
||||
|
||||
### 7.1 Bundle as LGX packages
|
||||
### 8.1 Bundle as LGX packages
|
||||
|
||||
Create `.lgx` packages for both dev and portable variants. Use `--out-link` to avoid overwriting the `result` symlink:
|
||||
|
||||
@@ -548,16 +565,21 @@ 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](logos-developer-guide.md#32-bundling-with-nix-bundle-lgx).
|
||||
|
||||
### 7.2 Build and run logos-basecamp
|
||||
```bash
|
||||
nix build '.#lgx' --out-link result-lgx && nix build '.#lgx-portable' --out-link result-lgx-portable
|
||||
```
|
||||
|
||||
### 8.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
|
||||
```
|
||||
|
||||
```bash
|
||||
# Launch once to preinstall bundled modules, then close it
|
||||
./basecamp-result/bin/logos-basecamp
|
||||
```
|
||||
@@ -574,6 +596,8 @@ ls ~/.local/share/Logos/
|
||||
|
||||
The dev build directory is named `LogosBasecampDev` (portable builds use `LogosBasecamp`).
|
||||
|
||||
### 8.3 Install modules with lgpm
|
||||
|
||||
Install your modules using `lgpm`. First, set `BASECAMP_DIR` to your platform's path:
|
||||
|
||||
```bash
|
||||
@@ -585,9 +609,10 @@ BASECAMP_DIR="$HOME/.local/share/Logos/LogosBasecampDev"
|
||||
```
|
||||
|
||||
```bash
|
||||
# Build lgpm CLI
|
||||
nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm
|
||||
```
|
||||
|
||||
```bash
|
||||
# Install core module
|
||||
./pm/bin/lgpm --modules-dir "$BASECAMP_DIR/modules" \
|
||||
install --file ../logos-calc-module/result-lgx/*.lgx
|
||||
@@ -600,14 +625,15 @@ nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm
|
||||
./basecamp-result/bin/logos-basecamp
|
||||
```
|
||||
|
||||
### 7.3 Portable basecamp build (optional)
|
||||
### 8.4 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:
|
||||
|
||||
```bash
|
||||
# Build portable basecamp (bundles all Qt frameworks/libraries)
|
||||
nix build 'github:logos-co/logos-basecamp#bin-bundle-dir' -o basecamp-portable
|
||||
```
|
||||
|
||||
```bash
|
||||
# Launch once to preinstall bundled modules
|
||||
./basecamp-portable/bin/logos-basecamp
|
||||
```
|
||||
@@ -639,7 +665,7 @@ Install your modules using the **portable** `.lgx` variants:
|
||||
|
||||
> **Important:** Portable basecamp requires portable `.lgx` variants (`result-lgx-portable`), and the dev build requires dev variants (`result-lgx`). Mixing them will cause loading failures.
|
||||
|
||||
### 7.4 Install via logos-basecamp UI
|
||||
### 8.5 Install via logos-basecamp UI
|
||||
|
||||
Instead of using `lgpm` on the command line, you can install modules through the basecamp UI:
|
||||
|
||||
@@ -651,7 +677,7 @@ Instead of using `lgpm` on the command line, you can install modules through the
|
||||
|
||||
The "Calculator UI" tab appears in the sidebar. Clicking it loads your `Main.qml`.
|
||||
|
||||
### 7.5 Live reloading with `logos-standalone-app`
|
||||
### 8.6 Live reloading with `logos-standalone-app`
|
||||
|
||||
For QML iteration, set `DEV_QML_PATH` to the directory that contains your view entry file (the basename from `metadata.json` `view` must exist under that directory). For this tutorial's layout (`view`: `Main.qml` at repo root):
|
||||
|
||||
@@ -684,7 +710,7 @@ DEV_QML_PATH=$PWD ./result/bin/run-logos-standalone-ui
|
||||
|
||||
> This does not work with `logos-basecamp`. Basecamp loads QML plugins from its own data directory, so changes to your source files are not reflected until you rebuild and reinstall the `.lgx` package.
|
||||
|
||||
### 7.6 Testing without any runtime
|
||||
### 8.7 Testing without any runtime
|
||||
|
||||
You can open `Main.qml` in any QML viewer (e.g., `qml` from Qt) to test the layout.
|
||||
|
||||
@@ -713,13 +739,13 @@ qml6 Main.qml
|
||||
|
||||
---
|
||||
|
||||
## Step 8: UI Integration Tests (Optional)
|
||||
## Step 9: UI Integration Tests
|
||||
|
||||
You can add automated UI tests that verify your QML plugin renders correctly. The test infrastructure is built into `logos-module-builder` — just add `.mjs` test files to a `tests/` directory and you get `nix build .#integration-test` for free.
|
||||
|
||||
Tests use the [logos-qt-mcp](https://github.com/logos-co/logos-qt-mcp) test framework, which connects to the QML inspector inside `logos-standalone-app` and can find elements, click buttons, verify text, and take screenshots.
|
||||
|
||||
### 8.1 Create a test file
|
||||
### 9.1 Create a test file
|
||||
|
||||
Create `tests/ui-tests.mjs`:
|
||||
|
||||
@@ -737,7 +763,7 @@ const { test, run } = await import(
|
||||
test("calc_ui: loads and shows title", async (app) => {
|
||||
await app.waitFor(
|
||||
async () => {
|
||||
await app.expectTexts(["Calculator"]);
|
||||
await app.expectTexts(["Logos Calculator"]);
|
||||
},
|
||||
{ timeout: 15000, interval: 500, description: "calc_ui to load" },
|
||||
);
|
||||
@@ -747,38 +773,41 @@ test("calc_ui: add button visible", async (app) => {
|
||||
await app.expectTexts(["Add"]);
|
||||
});
|
||||
|
||||
test("calc_ui: click add and check result", async (app) => {
|
||||
test("calc_ui: click add shows validation", async (app) => {
|
||||
await app.click("Add");
|
||||
// Verify the result appears (depends on your UI)
|
||||
await app.waitFor(
|
||||
async () => {
|
||||
await app.expectTexts(["Result:"]);
|
||||
await app.expectTexts(["Enter values for a and b"]);
|
||||
},
|
||||
{ timeout: 5000, interval: 500, description: "result to appear" },
|
||||
{ timeout: 5000, interval: 500, description: "validation message to appear" },
|
||||
);
|
||||
});
|
||||
|
||||
run();
|
||||
```
|
||||
|
||||
### 8.2 Run the tests
|
||||
### 9.2 Run the tests
|
||||
|
||||
```bash
|
||||
git add tests/
|
||||
```
|
||||
|
||||
# Hermetic CI test (builds everything, runs headless)
|
||||
```bash
|
||||
nix build .#integration-test -L
|
||||
|
||||
# Interactive: build test framework, run against a running app
|
||||
nix build .#test-framework -o result-mcp
|
||||
nix run . # start the app with inspector on :3768
|
||||
node tests/ui-tests.mjs # in another terminal
|
||||
```
|
||||
|
||||
The `integration-test` output launches `logos-standalone-app` with `QT_QPA_PLATFORM=offscreen` (no display needed), connects to the QML inspector, and runs all `.mjs` files in `tests/`.
|
||||
|
||||
You can have multiple test files (e.g., `tests/smoke.mjs`, `tests/interactions.mjs`) — they are all discovered and run automatically.
|
||||
|
||||
To run tests interactively (against an already-running app):
|
||||
|
||||
```bash
|
||||
nix build .#test-framework -o result-mcp
|
||||
nix run . # start the app with inspector on :3768
|
||||
node tests/ui-tests.mjs # in another terminal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
@@ -822,8 +851,6 @@ rm -rf ~/.local/share/Logos/LogosBasecampDev
|
||||
|
||||
Then reinstall your custom modules.
|
||||
|
||||
---
|
||||
|
||||
## Recap
|
||||
|
||||
| | Core Module (Part 1) | QML UI Plugin (Part 2) |
|
||||
@@ -835,8 +862,6 @@ Then reinstall your custom modules.
|
||||
| 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_module` and call them from QML
|
||||
|
||||
+116
-94
@@ -13,16 +13,15 @@ This tutorial walks you through wrapping a C shared library (`.so` on Linux, `.d
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes globally:
|
||||
```bash
|
||||
# Add to ~/.config/nix/nix.conf (create the file if it doesn't exist):
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
||||
```
|
||||
Verify it works:
|
||||
```bash
|
||||
nix flake --help >/dev/null 2>&1 && echo "Flakes enabled" || echo "Flakes NOT enabled — check nix.conf"
|
||||
```
|
||||
- **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
||||
```
|
||||
|
||||
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
|
||||
|
||||
- **A C compiler** (gcc or clang) for building the C library. Only needed if you're building the `.so`/`.dylib` yourself rather than using a pre-built library.
|
||||
- Basic familiarity with C and C++.
|
||||
|
||||
@@ -34,28 +33,36 @@ Before writing any C code, scaffold the Logos module project using the official
|
||||
|
||||
### 1.1 Create the project using the module builder template
|
||||
|
||||
For a module that wraps an external C library:
|
||||
|
||||
`mkdir logos-calc-module && cd logos-calc-module`
|
||||
|
||||
```bash
|
||||
# For a module that wraps an external C library:
|
||||
mkdir logos-calc-module && cd logos-calc-module
|
||||
nix flake init -t github:logos-co/logos-module-builder#with-external-lib
|
||||
|
||||
# Or for a plain module (no external library):
|
||||
# nix flake init -t github:logos-co/logos-module-builder
|
||||
```
|
||||
|
||||
> **Note:** The generated `flake.nix` uses an unpinned `logos-module-builder` URL. Replace it with the pinned version shown in [Step 2.3](#23-flakenix--nix-build-config) to ensure reproducible builds.
|
||||
|
||||
This generates the skeleton files (`flake.nix`, `metadata.json`, `CMakeLists.txt`, etc.) pre-configured for the logos-module-builder. You then customize them for your specific library.
|
||||
|
||||
> **Note:** The generated `flake.nix` uses an unpinned `logos-module-builder` URL. Replace it with the pinned version shown in the flake.nix step below to ensure reproducible builds.
|
||||
|
||||
> **Alternative approach:** You can also create the C library as a separate project, build it there, then copy the resulting `.so`/`.dylib` and header files into the module's `lib/` directory. This can be cleaner for larger libraries with their own build systems.
|
||||
|
||||
### 1.2 Create the lib directory
|
||||
---
|
||||
|
||||
## Step 2: Write the C Library
|
||||
|
||||
Create the C library that your module will wrap. Place the header and implementation in the `lib/` directory.
|
||||
|
||||
### 2.1 Create the lib directory
|
||||
|
||||
```bash
|
||||
mkdir -p lib
|
||||
```
|
||||
|
||||
### 1.3 Write the C header
|
||||
### 2.2 Write the C header
|
||||
|
||||
Create `lib/libcalc.h`:
|
||||
|
||||
@@ -91,7 +98,7 @@ const char* calc_version(void);
|
||||
|
||||
The `extern "C"` block is essential — it prevents C++ name mangling so the Logos module can find the symbols.
|
||||
|
||||
### 1.4 Write the C implementation
|
||||
### 2.3 Write the C implementation
|
||||
|
||||
Create `lib/libcalc.c`:
|
||||
|
||||
@@ -139,7 +146,7 @@ const char* calc_version(void)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 Build the shared library
|
||||
### 2.4 Build the shared library
|
||||
|
||||
```bash
|
||||
cd lib
|
||||
@@ -177,9 +184,11 @@ You should see each symbol marked with `T` (text/code section). Addresses will v
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Configure the Logos Module
|
||||
## Step 3: Configure the Logos Module
|
||||
|
||||
The template from Step 1.1 generated skeleton files with placeholder names (`external_lib`, `example_lib`). Now rename and customize them for your library. You need to edit **every generated file**:
|
||||
The template generated skeleton files with placeholder names (`external_lib`, `example_lib`). Now rename and customize them for your library. You need to edit **every generated file**.
|
||||
|
||||
After editing, your project should look like this:
|
||||
|
||||
| File | What to change |
|
||||
| ---------------------- | ----------------------------------------------------------------- |
|
||||
@@ -188,24 +197,21 @@ The template from Step 1.1 generated skeleton files with placeholder names (`ext
|
||||
| `flake.nix` | Description (and dependency inputs if needed) |
|
||||
| `src/*.h`, `src/*.cpp` | Rename files, replace class/method names, add your wrapping logic |
|
||||
|
||||
After renaming and editing, your project should look like this:
|
||||
|
||||
```
|
||||
logos-calc-module/
|
||||
├── flake.nix # Nix build configuration (~10 lines)
|
||||
├── metadata.json # Module metadata, build settings, and runtime config (~25 lines)
|
||||
├── CMakeLists.txt # CMake build file (~20 lines)
|
||||
├── metadata.json # Module metadata, build settings, and runtime config
|
||||
├── CMakeLists.txt # CMake build file
|
||||
├── lib/
|
||||
│ ├── libcalc.h # C library header
|
||||
│ ├── libcalc.c # C library source
|
||||
│ └── libcalc.so # Pre-built shared library
|
||||
│ └── libcalc.c # C library source (compiled by CMake)
|
||||
└── src/
|
||||
├── calc_module_interface.h # Interface declaration
|
||||
├── calc_module_plugin.h # Plugin header
|
||||
└── calc_module_plugin.cpp # Plugin implementation (wrapping logic)
|
||||
```
|
||||
|
||||
### 2.1 `metadata.json` — Module Configuration
|
||||
### 3.1 `metadata.json` — Module Configuration
|
||||
|
||||
> **Edit:** Change `name`, `description`, `main`, `nix.external_libraries[].name`, and `nix.cmake.extra_include_dirs` to match your module and library.
|
||||
|
||||
@@ -247,11 +253,10 @@ This is the single source of truth for your module. It is embedded into the plug
|
||||
| Field | What it does |
|
||||
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `name` | Module name — must be a valid C identifier (used in filenames, method calls) |
|
||||
| `nix.external_libraries[].name` | Library name **without the `lib` prefix** — the builder looks for `lib<name>.so` / `lib<name>.dylib` in the directory specified by `vendor_path`. So `name: calc` matches the file `libcalc.so` / `libcalc.dylib`. This follows the standard Unix library naming convention where `-lcalc` links against `libcalc`. |
|
||||
| `nix.external_libraries[].vendor_path` | Where to find the pre-built library. `"lib"` means the `lib/` directory in your project root |
|
||||
| `nix.cmake.extra_include_dirs` | Added to the CMake include path so your C++ code can `#include "lib/libcalc.h"` |
|
||||
| `nix.external_libraries` | Declares C/C++ libraries vendored in the repo. Each entry has a `name` (used for the Nix derivation and CMake target) and `vendor_path` (directory containing the source). The build system compiles the library and makes it available as a CMake target |
|
||||
| `nix.cmake.extra_include_dirs` | Added to the CMake include path so your C++ code can `#include "libcalc.h"` |
|
||||
|
||||
### 2.2 `CMakeLists.txt` — Build File
|
||||
### 3.2 `CMakeLists.txt` — Build File
|
||||
|
||||
> **Edit:** Change `project()` name, `NAME`, `SOURCES` filenames, and `EXTERNAL_LIBS` to match your module and library.
|
||||
|
||||
@@ -293,9 +298,9 @@ The `if/elseif/else` block above it is boilerplate — don't change it.
|
||||
|
||||
**How `EXTERNAL_LIBS calc` works:** The `logos_module()` CMake function searches `lib/` for `libcalc.so` (Linux) or `libcalc.dylib` (macOS), links it to your plugin, and sets up RPATH so the library is found at runtime.
|
||||
|
||||
### 2.3 `flake.nix` — Nix Build Config
|
||||
### 3.3 `flake.nix` — Nix Build Config
|
||||
|
||||
> **Edit:** Change `description`. Add flake inputs here if your module depends on other modules or fetches a library from source (see [Advanced: Wrapping a Library from a Flake Input](#advanced-wrapping-a-library-from-a-flake-input)).
|
||||
Change `description`. Add flake inputs here if your module depends on other modules or fetches a library from source.
|
||||
|
||||
```nix
|
||||
{
|
||||
@@ -316,13 +321,11 @@ The `if/elseif/else` block above it is boilerplate — don't change it.
|
||||
|
||||
That's it — `mkLogosModule` handles all the Nix complexity (fetching Qt, the SDK, the code generator, setting up include paths, etc.). Note that `configFile` points to `metadata.json` (the single source of truth) and `flakeInputs = inputs` passes all flake inputs to the builder so that dependencies declared in `metadata.json` are resolved automatically.
|
||||
|
||||
> **Naming flake inputs:** When adding module dependencies, the flake input attribute name **must match** the `name` field in that dependency's `metadata.json`. For example, if you depend on a module whose `metadata.json` has `"name": "waku_module"`, your flake input must be `waku_module.url = "github:logos-co/logos-waku-module"`. The URL can point to any repo, but the attribute name is how the builder resolves dependencies.
|
||||
> **Naming flake inputs:** When adding module dependencies, the flake input attribute name **must match** the `name` field in that dependency's `metadata.json`. For example, if you depend on a module whose `metadata.json` has `"name": "waku_module"`, your flake input must be `waku_module.url = "github:logos-co/logos-waku-module"`.
|
||||
|
||||
### 2.4 `src/calc_module_interface.h` — Interface Declaration
|
||||
### 3.4 `src/calc_module_interface.h` — Interface Declaration
|
||||
|
||||
> **Edit:** Rename from `external_lib_interface.h`. Replace the class name, interface ID, include guard, and declare your module's methods as `Q_INVOKABLE virtual` pure-virtual functions.
|
||||
|
||||
This declares the methods your module exposes. It inherits from `PluginInterface` (provided by the Logos C++ SDK).
|
||||
This declares the methods your module exposes. It inherits from `PluginInterface` (provided by the Logos C++ SDK). Every method you want callable by other modules must be `Q_INVOKABLE` and `virtual`.
|
||||
|
||||
```cpp
|
||||
#ifndef CALC_MODULE_INTERFACE_H
|
||||
@@ -356,9 +359,7 @@ Q_DECLARE_INTERFACE(CalcModuleInterface, CalcModuleInterface_iid)
|
||||
- Supported parameter/return types: `int`, `bool`, `QString`, `QByteArray`, `QVariant`, `QJsonArray`, `QStringList`, `LogosResult`
|
||||
- The interface ID string (e.g., `"org.logos.CalcModuleInterface"`) must be unique across all modules
|
||||
|
||||
### 2.5 `src/calc_module_plugin.h` — Plugin Header
|
||||
|
||||
> **Edit:** Rename from `external_lib_plugin.h`. Replace class name, interface references, `name()`/`version()` return values, and declare your `Q_INVOKABLE` wrapper methods. Add `#include` for your C library header.
|
||||
### 3.5 `src/calc_module_plugin.h` — Plugin Header
|
||||
|
||||
This is the actual plugin class. It inherits from both `QObject` (for Qt's meta-object system) and your interface.
|
||||
|
||||
@@ -385,15 +386,13 @@ public:
|
||||
explicit CalcModulePlugin(QObject* parent = nullptr);
|
||||
~CalcModulePlugin() override;
|
||||
|
||||
// PluginInterface — required by every module
|
||||
// PluginInterface
|
||||
QString name() const override { return "calc_module"; }
|
||||
QString version() const override { return "1.0.0"; }
|
||||
|
||||
// Called by the Logos host when the module is loaded.
|
||||
// NOT marked override — it is invoked reflectively via QMetaObject.
|
||||
Q_INVOKABLE void initLogos(LogosAPI* api);
|
||||
|
||||
// CalcModuleInterface — each wraps a libcalc C function
|
||||
// CalcModuleInterface
|
||||
Q_INVOKABLE int add(int a, int b) override;
|
||||
Q_INVOKABLE int multiply(int a, int b) override;
|
||||
Q_INVOKABLE int factorial(int n) override;
|
||||
@@ -416,11 +415,9 @@ signals:
|
||||
- `initLogos` must be `Q_INVOKABLE` but **not** `override` — the base class `PluginInterface` does not declare it as virtual; the Logos host calls it reflectively via `QMetaObject::invokeMethod`
|
||||
- `eventResponse` signal is required for event forwarding between modules. Emit it to push data to subscribers (e.g., QML UIs listening via `logos.onModuleEvent()`)
|
||||
- `name()` must return the same string as the `name` field in `metadata.json`
|
||||
- **No `m_logosAPI` member variable** — the `LogosAPI`\* pointer is stored in the global `logosAPI` variable defined in `liblogos`, not in a class member. See the `initLogos` implementation below.
|
||||
- **No `m_logosAPI` member variable** — the `LogosAPI*` pointer is stored in the global `logosAPI` variable defined in `liblogos`, not in a class member. See the `initLogos` implementation below.
|
||||
|
||||
### 2.6 `src/calc_module_plugin.cpp` — Plugin Implementation
|
||||
|
||||
> **Edit:** Rename from `external_lib_plugin.cpp`. Replace the placeholder implementations with actual calls to your C library functions.
|
||||
### 3.6 `src/calc_module_plugin.cpp` — Plugin Implementation
|
||||
|
||||
This is where the wrapping happens. Each method calls the corresponding C function.
|
||||
|
||||
@@ -442,16 +439,12 @@ CalcModulePlugin::~CalcModulePlugin()
|
||||
|
||||
void CalcModulePlugin::initLogos(LogosAPI* api)
|
||||
{
|
||||
// IMPORTANT: Use the global `logosAPI` variable from liblogos, NOT a class member.
|
||||
// `logosAPI` is defined in the Logos SDK headers and is used by the API
|
||||
// internally. Storing the pointer in a local `m_logosAPI` member will NOT work.
|
||||
logosAPI = api;
|
||||
qDebug() << "CalcModulePlugin: LogosAPI initialized";
|
||||
}
|
||||
|
||||
int CalcModulePlugin::add(int a, int b)
|
||||
{
|
||||
// Call the C library function
|
||||
int result = calc_add(a, b);
|
||||
qDebug() << "CalcModulePlugin::add" << a << "+" << b << "=" << result;
|
||||
return result;
|
||||
@@ -498,20 +491,20 @@ void CalcModulePlugin::libVersionNotify()
|
||||
**The wrapping pattern** is always the same:
|
||||
|
||||
1. Call the C function with the arguments
|
||||
2. Convert the C result to a Qt type if needed (e.g., `const char`\* → `QString`)
|
||||
2. Convert the C result to a Qt type if needed (e.g., `const char*` → `QString`)
|
||||
3. Return the Qt type
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Build the Module
|
||||
## Step 4: Build the Module
|
||||
|
||||
### 3.1 Initialize the Git repo
|
||||
### 4.1 Initialize the Git repo
|
||||
|
||||
Nix flakes require a git repository.
|
||||
|
||||
Before staging files, create a `.gitignore` to exclude build artifacts:
|
||||
|
||||
```
|
||||
```text
|
||||
# Nix build output
|
||||
result
|
||||
result-*
|
||||
@@ -523,28 +516,42 @@ build/
|
||||
Then initialise the repo:
|
||||
|
||||
```bash
|
||||
cd logos-calc-module
|
||||
git init
|
||||
```
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
```
|
||||
|
||||
```bash
|
||||
nix flake update
|
||||
```
|
||||
|
||||
```bash
|
||||
git add flake.lock
|
||||
```
|
||||
|
||||
### 3.2 Build with Nix
|
||||
### 4.2 Build the plugin library
|
||||
|
||||
Build just the plugin library (`.so` / `.dylib`):
|
||||
|
||||
```bash
|
||||
# Build just the plugin library (.so / .dylib)
|
||||
nix build '.#lib'
|
||||
```
|
||||
|
||||
# Build everything (library + generated SDK headers)
|
||||
> **Quoting matters:** Use `'.#lib'` (with quotes) rather than bare `nix build .#lib`. Some shells (especially zsh) may interpret the `#` as a comment character.
|
||||
|
||||
The first build takes a while (5–15 minutes) as Nix downloads Qt, the Logos SDK, and other dependencies. Subsequent builds are fast due to caching.
|
||||
|
||||
### 4.3 Build the full package
|
||||
|
||||
Build everything (library + generated SDK headers):
|
||||
|
||||
```bash
|
||||
nix build
|
||||
```
|
||||
|
||||
> **Quoting matters:** Use `'.#lib'` (with quotes) rather than bare `nix build .#lib`. Some shells (especially zsh) may interpret the `#` as a comment character, causing the command to silently build the wrong thing or fail.
|
||||
|
||||
The first build takes a while (5-15 minutes) as Nix downloads Qt, the Logos SDK, and other dependencies. Subsequent builds are fast due to caching.
|
||||
|
||||
### 3.3 Inspect the output
|
||||
### 4.4 Inspect the output
|
||||
|
||||
```bash
|
||||
ls -la result/lib/
|
||||
@@ -566,17 +573,19 @@ Both library files are placed together so the plugin can find the C library at r
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Inspect the Module
|
||||
## Step 5: Inspect the Module
|
||||
|
||||
### 4.1 Build the `lm` tool
|
||||
Use the `lm` CLI tool (from `logos-module`) to inspect the compiled module binary.
|
||||
|
||||
The `lm` CLI tool (from `logos-module`) inspects compiled module binaries:
|
||||
### 5.1 Build the `lm` tool
|
||||
|
||||
The `lm` CLI inspects compiled module binaries. Build it from the `logos-module` repo:
|
||||
|
||||
```bash
|
||||
nix build 'github:logos-co/logos-module#lm' --out-link ./lm
|
||||
```
|
||||
|
||||
### 4.2 View metadata
|
||||
### 5.2 View metadata
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
@@ -599,7 +608,7 @@ Type: core
|
||||
Dependencies: (none)
|
||||
```
|
||||
|
||||
### 4.3 List methods
|
||||
### 5.3 List methods
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
@@ -646,7 +655,7 @@ QString libVersion()
|
||||
|
||||
All five wrapping methods are visible and invokable. The `initLogos` method is automatically called by the Logos host when loading the module.
|
||||
|
||||
### 4.4 JSON output
|
||||
### 5.4 JSON output
|
||||
|
||||
For scripting and CI, use `--json`:
|
||||
|
||||
@@ -676,25 +685,31 @@ For scripting and CI, use `--json`:
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Test with `logoscore`
|
||||
## Step 6: Test with `logoscore`
|
||||
|
||||
### 5.1 Build logoscore
|
||||
### 6.1 Build logoscore
|
||||
|
||||
```bash
|
||||
nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos
|
||||
```
|
||||
|
||||
### 5.2 Set up the modules directory
|
||||
### 6.2 Set up the modules directory
|
||||
|
||||
`logoscore` expects modules in subdirectories, each with a `manifest.json`. Rather than copying files and writing the manifest manually, use the Nix derivation to create an LGX package and install it with the package manager:
|
||||
|
||||
```bash
|
||||
# Bundle the module into an LGX package
|
||||
nix build '.#lgx'
|
||||
```
|
||||
|
||||
# Install it into a modules directory using the Logos Package Manager
|
||||
```bash
|
||||
nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm
|
||||
```
|
||||
|
||||
```bash
|
||||
mkdir -p modules
|
||||
```
|
||||
|
||||
```bash
|
||||
./pm/bin/lgpm --modules-dir ./modules install --file result/*.lgx
|
||||
```
|
||||
|
||||
@@ -708,24 +723,39 @@ modules/calc_module/
|
||||
└── variant # Platform variant identifier
|
||||
```
|
||||
|
||||
### 5.3 Call methods
|
||||
### 6.3 Call methods
|
||||
|
||||
Start the daemon and call methods:
|
||||
|
||||
```bash
|
||||
# Start logoscore daemon with modules directory
|
||||
./logos/bin/logoscore -D -m ./modules &
|
||||
```
|
||||
|
||||
# Load the module
|
||||
```bash
|
||||
sleep 3
|
||||
```
|
||||
|
||||
```bash
|
||||
./logos/bin/logoscore load-module calc_module
|
||||
```
|
||||
|
||||
# Call methods
|
||||
```bash
|
||||
./logos/bin/logoscore call calc_module add 3 5
|
||||
./logos/bin/logoscore call calc_module factorial 5
|
||||
./logos/bin/logoscore call calc_module fibonacci 10
|
||||
./logos/bin/logoscore call calc_module libVersion
|
||||
```
|
||||
|
||||
# Stop the daemon when done
|
||||
```bash
|
||||
./logos/bin/logoscore call calc_module factorial 5
|
||||
```
|
||||
|
||||
```bash
|
||||
./logos/bin/logoscore call calc_module fibonacci 10
|
||||
```
|
||||
|
||||
```bash
|
||||
./logos/bin/logoscore call calc_module libVersion
|
||||
```
|
||||
|
||||
```bash
|
||||
./logos/bin/logoscore stop
|
||||
```
|
||||
|
||||
@@ -757,7 +787,7 @@ Method call successful. Result: ...
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Package for Distribution (Optional)
|
||||
## Package for Distribution (Optional)
|
||||
|
||||
The LGX package created in Step 5.2 is a **local** package — its libraries still reference `/nix/store` paths, so it only works on the machine that built it. To create a **portable** package that can be distributed to other machines:
|
||||
|
||||
@@ -785,8 +815,6 @@ nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm
|
||||
|
||||
> **Note:** Local builds of `logoscore` / `logos-basecamp` (via `nix build`) expect **local** `.lgx` packages. Portable builds (via `nix build '.#bin-bundle-dir'`, `.#bin-appimage`, or `.#bin-macos-app`) expect **portable** `.lgx` packages. See the [logos-basecamp README](https://github.com/logos-co/logos-basecamp/blob/master/README.md) for details.
|
||||
|
||||
---
|
||||
|
||||
## Common Wrapping Patterns
|
||||
|
||||
### Wrapping C functions with opaque pointers
|
||||
@@ -881,8 +909,6 @@ Q_INVOKABLE QString getData() {
|
||||
| `bool` / `int` | `bool` | `result != 0` | direct |
|
||||
| `void*` | (store in member) | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Wrapping a Library from a Flake Input
|
||||
|
||||
Instead of pre-building the library and placing it in `lib/`, you can have Nix fetch and build it from source. This is useful for libraries hosted on GitHub.
|
||||
@@ -977,8 +1003,6 @@ If the external library is written in Go with C bindings (`cgo`), set `go_build:
|
||||
|
||||
Setting `go_build: true` enables the Go toolchain and sets `CGO_ENABLED=1`.
|
||||
|
||||
---
|
||||
|
||||
## Real-World Example: logos-libp2p-module
|
||||
|
||||
The [logos-libp2p-module](https://github.com/logos-co/logos-libp2p-module) is a production module that wraps the `nim-libp2p` library (compiled to a C shared library). Key files:
|
||||
@@ -990,8 +1014,6 @@ The [logos-libp2p-module](https://github.com/logos-co/logos-libp2p-module) is a
|
||||
|
||||
It follows the exact same pattern as this tutorial, just at a larger scale.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `initLogos` marked 'override', but does not override
|
||||
|
||||
Reference in New Issue
Block a user