// Preloaded example tutorials. Auto-generated from uploaded specs. window.EXAMPLES = { "wrapping-c-library": { "filename": "tutorial-wrapping-c-library.test.yaml", "yaml": "name: \"Tutorial: Wrapping a C Library as a Logos Module\"\noutput: tutorial-wrapping-c-library.md\nproject_name: logos-calc-module\nrelease: \"\"\n\nintro: |\n This tutorial walks you through wrapping a C shared library (`.so` on Linux, `.dylib` on macOS) as a Logos module. By the end, you will have a module that compiles, loads, and responds to method calls via `logoscore`.\n\nwhat_you_build: \"A `calc_module` that wraps a tiny C calculator library (`libcalc`), exposing arithmetic functions to the Logos platform.\"\n\nwhat_you_learn:\n - How a Logos module wraps a C library\n - The role of each file in the module project\n - How to build, inspect, and test your module\n - How `logoscore` discovers, loads, and calls your module\n\nprerequisites:\n - |\n **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n\n ```bash\n mkdir -p ~/.config/nix\n echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n ```\n\n Verify: `nix flake --help >/dev/null 2>&1 && echo \"Flakes enabled\"`\n - \"**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.\"\n - \"Basic familiarity with C and C++.\"\n\nsections:\n # ── Step 1: Scaffold ──────────────────────────────────────────────────────\n - title: \"Scaffold the Module Project\"\n step: true\n text: |\n Before writing any C code, scaffold the Logos module project using the official template. This gives you the correct `flake.nix`, `metadata.json`, directory structure, and build configuration out of the box.\n steps:\n - title: \"Create the project using the module builder template\"\n text: |\n For a module that wraps an external C library:\n\n `mkdir logos-calc-module && cd logos-calc-module`\n run: \"nix flake init -t github:logos-co/logos-module-builder{release}#with-external-lib\"\n code_block: |\n nix flake init -t github:logos-co/logos-module-builder{release}#with-external-lib\n\n # Or for a plain module (no external library):\n # nix flake init -t github:logos-co/logos-module-builder{release}\n post_text: |\n 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.\n\n > **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.\n\n > **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.\n\n # ── Step 2: Write the C library ────────────────────────────────────────────\n - title: \"Write the C Library\"\n step: true\n text: |\n Create the C library that your module will wrap. Place the header and implementation in the `lib/` directory.\n steps:\n - title: \"Create the lib directory\"\n run: \"mkdir -p lib\"\n\n - title: \"Write the C header\"\n text: \"Create `lib/libcalc.h`:\"\n file:\n path: lib/libcalc.h\n language: c\n content: |\n #ifndef LIBCALC_H\n #define LIBCALC_H\n\n #ifdef __cplusplus\n extern \"C\" {\n #endif\n\n /** Add two integers. */\n int calc_add(int a, int b);\n\n /** Multiply two integers. */\n int calc_multiply(int a, int b);\n\n /** Compute factorial of n (n must be >= 0). Returns -1 on error. */\n int calc_factorial(int n);\n\n /** Compute the nth Fibonacci number (n must be >= 0). Returns -1 on error. */\n int calc_fibonacci(int n);\n\n /** Return the library version string. Caller must NOT free. */\n const char* calc_version(void);\n\n #ifdef __cplusplus\n }\n #endif\n\n #endif /* LIBCALC_H */\n post_text: |\n The `extern \"C\"` block is essential — it prevents C++ name mangling so the Logos module can find the symbols.\n\n - title: \"Write the C implementation\"\n text: \"Create `lib/libcalc.c`:\"\n file:\n path: lib/libcalc.c\n language: c\n content: |\n #include \"libcalc.h\"\n\n int calc_add(int a, int b)\n {\n return a + b;\n }\n\n int calc_multiply(int a, int b)\n {\n return a * b;\n }\n\n int calc_factorial(int n)\n {\n if (n < 0) return -1;\n if (n <= 1) return 1;\n int result = 1;\n for (int i = 2; i <= n; i++) {\n result *= i;\n }\n return result;\n }\n\n int calc_fibonacci(int n)\n {\n if (n < 0) return -1;\n if (n == 0) return 0;\n if (n == 1) return 1;\n int a = 0, b = 1;\n for (int i = 2; i <= n; i++) {\n int tmp = a + b;\n a = b;\n b = tmp;\n }\n return b;\n }\n\n const char* calc_version(void)\n {\n return \"1.0.0\";\n }\n\n - title: \"Build the shared library\"\n run: \"cd lib && gcc {shared_flags} -o libcalc.{ext} libcalc.c && cd ..\"\n code_block: |\n cd lib\n\n # Linux\n gcc -shared -fPIC -o libcalc.so libcalc.c\n\n # macOS\n # gcc -shared -fPIC -o libcalc.dylib libcalc.c\n\n cd ..\n post_text: \"Verify the symbols are exported:\"\n extra_run:\n run: \"nm -gU lib/libcalc.{ext} | grep calc\"\n code_block: |\n # Linux\n nm -D lib/libcalc.so | grep calc\n\n # macOS\n # nm -gU lib/libcalc.dylib | grep calc\n post_text: |\n You should see each symbol marked with `T` (text/code section). Addresses will vary:\n\n ```\n 0000000000001139 T calc_add\n 0000000000001179 T calc_factorial\n 00000000000011f5 T calc_fibonacci\n 0000000000001159 T calc_multiply\n 0000000000001299 T calc_version\n ```\n\n > **Wrapping a third-party library?** If you're wrapping an existing library (e.g., from a system package or a GitHub repo), you don't need to write the C code — just place the pre-built `.so`/`.dylib` and its header file in `lib/`.\n\n # ── Step 3: Configure the Logos Module ──────────────────────────────────────\n - title: \"Configure the Logos Module\"\n step: true\n text: |\n 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**.\n\n After editing, your project should look like this:\n\n | File | What to change |\n | ---------------------- | ----------------------------------------------------------------- |\n | `metadata.json` | Module name, description, library name, include dirs |\n | `CMakeLists.txt` | Project name, module name, source filenames, library name |\n | `flake.nix` | Description (and dependency inputs if needed) |\n | `src/*.h`, `src/*.cpp` | Rename files, replace class/method names, add your wrapping logic |\n\n ```\n logos-calc-module/\n ├── flake.nix # Nix build configuration (~10 lines)\n ├── metadata.json # Module metadata, build settings, and runtime config\n ├── CMakeLists.txt # CMake build file\n ├── lib/\n │ ├── libcalc.h # C library header\n │ └── libcalc.c # C library source (compiled by CMake)\n └── src/\n ├── calc_module_interface.h # Interface declaration\n ├── calc_module_plugin.h # Plugin header\n └── calc_module_plugin.cpp # Plugin implementation (wrapping logic)\n ```\n steps:\n - title: \"`metadata.json` — Module Configuration\"\n text: |\n > **Edit:** Change `name`, `description`, `main`, `nix.external_libraries[].name`, and `nix.cmake.extra_include_dirs` to match your module and library.\n\n This is the single source of truth for your module. It is embedded into the plugin binary by Qt's `Q_PLUGIN_METADATA` macro (for runtime metadata), read by `logos-module-builder` to configure the Nix build, used by CMake to resolve external dependencies and link libraries (via the `nix` section), and used by `nix-bundle-lgx` to generate the LGX manifest.\n file:\n path: metadata.json\n language: json\n content: |\n {\n \"name\": \"calc_module\",\n \"version\": \"1.0.0\",\n \"type\": \"core\",\n \"category\": \"general\",\n \"description\": \"Calculator module wrapping libcalc C library\",\n \"main\": \"calc_module_plugin\",\n \"dependencies\": [],\n\n \"nix\": {\n \"packages\": {\n \"build\": [],\n \"runtime\": []\n },\n \"external_libraries\": [\n {\n \"name\": \"calc\",\n \"vendor_path\": \"lib\"\n }\n ],\n \"cmake\": {\n \"find_packages\": [],\n \"extra_sources\": [],\n \"extra_include_dirs\": [\"lib\"],\n \"extra_link_libraries\": []\n }\n }\n }\n post_text: |\n **Key fields explained:**\n\n | Field | What it does |\n | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n | `name` | Module name — must be a valid C identifier (used in filenames, method calls) |\n | `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 |\n | `nix.cmake.extra_include_dirs` | Added to the CMake include path so your C++ code can `#include \"libcalc.h\"` |\n\n - title: \"`CMakeLists.txt` — Build File\"\n text: |\n > **Edit:** Change `project()` name, `NAME`, `SOURCES` filenames, and `EXTERNAL_LIBS` to match your module and library.\n file:\n path: CMakeLists.txt\n language: cmake\n content: |\n cmake_minimum_required(VERSION 3.14)\n project(CalcModulePlugin LANGUAGES CXX)\n\n # Include the Logos Module CMake helper (provided by logos-module-builder)\n if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})\n include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)\n elseif(EXISTS \"${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake\")\n include(cmake/LogosModule.cmake)\n else()\n message(FATAL_ERROR \"LogosModule.cmake not found\")\n endif()\n\n # Define the module with its external library dependency\n logos_module(\n NAME calc_module\n SOURCES\n src/calc_module_interface.h\n src/calc_module_plugin.h\n src/calc_module_plugin.cpp\n EXTERNAL_LIBS\n calc\n )\n post_text: |\n The template generates this with default names (e.g., `external_lib`). You **must** update:\n\n - **`project()`** — rename to match your module (e.g., `CalcModulePlugin`)\n - **`NAME`** — your module name (must match `name` in `metadata.json`, e.g., `calc_module`)\n - **`SOURCES`** — your renamed source files\n - **`EXTERNAL_LIBS`** — names of external libraries to link (must match `nix.external_libraries[].name` in `metadata.json`)\n\n The `if/elseif/else` block above it is boilerplate — don't change it.\n\n > **Common mistake:** If `NAME` doesn't match `name` in `metadata.json`, the build will succeed but the install phase will fail because it looks for `_plugin.dylib` based on `metadata.json`.\n\n **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.\n\n - title: \"`flake.nix` — Nix Build Config\"\n text: |\n Change `description`. Add flake inputs here if your module depends on other modules or fetches a library from source.\n file:\n path: flake.nix\n language: nix\n content: |\n {\n description = \"Calculator module - wraps libcalc C library for Logos\";\n\n inputs = {\n logos-module-builder.url = \"github:logos-co/logos-module-builder{release}\";\n };\n\n outputs = inputs@{ logos-module-builder, ... }:\n logos-module-builder.lib.mkLogosModule {\n src = ./.;\n configFile = ./metadata.json;\n flakeInputs = inputs;\n };\n }\n post_text: |\n 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.\n\n > **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\"`.\n\n - title: \"`src/calc_module_interface.h` — Interface Declaration\"\n text: |\n 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`.\n file:\n path: src/calc_module_interface.h\n language: cpp\n content: |\n #ifndef CALC_MODULE_INTERFACE_H\n #define CALC_MODULE_INTERFACE_H\n\n #include \n #include \n #include \"interface.h\"\n\n class CalcModuleInterface : public PluginInterface\n {\n public:\n virtual ~CalcModuleInterface() = default;\n\n Q_INVOKABLE virtual int add(int a, int b) = 0;\n Q_INVOKABLE virtual int multiply(int a, int b) = 0;\n Q_INVOKABLE virtual int factorial(int n) = 0;\n Q_INVOKABLE virtual int fibonacci(int n) = 0;\n Q_INVOKABLE virtual QString libVersion() = 0;\n };\n\n #define CalcModuleInterface_iid \"org.logos.CalcModuleInterface\"\n Q_DECLARE_INTERFACE(CalcModuleInterface, CalcModuleInterface_iid)\n\n #endif // CALC_MODULE_INTERFACE_H\n post_text: |\n **Rules for the interface:**\n\n - Every method you want callable by other modules must be `Q_INVOKABLE` and `virtual`\n - Supported parameter/return types: `int`, `bool`, `QString`, `QByteArray`, `QVariant`, `QJsonArray`, `QStringList`, `LogosResult`\n - The interface ID string (e.g., `\"org.logos.CalcModuleInterface\"`) must be unique across all modules\n\n - title: \"`src/calc_module_plugin.h` — Plugin Header\"\n text: |\n This is the actual plugin class. It inherits from both `QObject` (for Qt's meta-object system) and your interface.\n file:\n path: src/calc_module_plugin.h\n language: cpp\n content: |\n #ifndef CALC_MODULE_PLUGIN_H\n #define CALC_MODULE_PLUGIN_H\n\n #include \n #include \n #include \"calc_module_interface.h\"\n\n // Include the C library header\n #include \"lib/libcalc.h\"\n\n class LogosAPI;\n\n class CalcModulePlugin : public QObject, public CalcModuleInterface\n {\n Q_OBJECT\n Q_PLUGIN_METADATA(IID CalcModuleInterface_iid FILE \"metadata.json\")\n Q_INTERFACES(CalcModuleInterface PluginInterface)\n\n public:\n explicit CalcModulePlugin(QObject* parent = nullptr);\n ~CalcModulePlugin() override;\n\n // PluginInterface\n QString name() const override { return \"calc_module\"; }\n QString version() const override { return \"1.0.0\"; }\n\n Q_INVOKABLE void initLogos(LogosAPI* api);\n\n // CalcModuleInterface\n Q_INVOKABLE int add(int a, int b) override;\n Q_INVOKABLE int multiply(int a, int b) override;\n Q_INVOKABLE int factorial(int n) override;\n Q_INVOKABLE int fibonacci(int n) override;\n Q_INVOKABLE QString libVersion() override;\n Q_INVOKABLE void libVersionNotify();\n\n signals:\n void eventResponse(const QString& eventName, const QVariantList& args);\n\n };\n\n #endif // CALC_MODULE_PLUGIN_H\n post_text: |\n **Critical details:**\n\n - `Q_PLUGIN_METADATA(IID ... FILE \"metadata.json\")` — embeds the metadata into the binary\n - `Q_INTERFACES(CalcModuleInterface PluginInterface)` — registers both interfaces with Qt's plugin system\n - `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`\n - `eventResponse` signal is required for event forwarding between modules. Emit it to push data to subscribers (e.g., QML UIs listening via `logos.onModuleEvent()`)\n - `name()` must return the same string as the `name` field in `metadata.json`\n - **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.\n\n - title: \"`src/calc_module_plugin.cpp` — Plugin Implementation\"\n text: |\n This is where the wrapping happens. Each method calls the corresponding C function.\n file:\n path: src/calc_module_plugin.cpp\n language: cpp\n content: |\n #include \"calc_module_plugin.h\"\n #include \"logos_api.h\"\n #include \n\n CalcModulePlugin::CalcModulePlugin(QObject* parent)\n : QObject(parent)\n {\n qDebug() << \"CalcModulePlugin: created\";\n }\n\n CalcModulePlugin::~CalcModulePlugin()\n {\n qDebug() << \"CalcModulePlugin: destroyed\";\n }\n\n void CalcModulePlugin::initLogos(LogosAPI* api)\n {\n logosAPI = api;\n qDebug() << \"CalcModulePlugin: LogosAPI initialized\";\n }\n\n int CalcModulePlugin::add(int a, int b)\n {\n int result = calc_add(a, b);\n qDebug() << \"CalcModulePlugin::add\" << a << \"+\" << b << \"=\" << result;\n return result;\n }\n\n int CalcModulePlugin::multiply(int a, int b)\n {\n int result = calc_multiply(a, b);\n qDebug() << \"CalcModulePlugin::multiply\" << a << \"*\" << b << \"=\" << result;\n return result;\n }\n\n int CalcModulePlugin::factorial(int n)\n {\n int result = calc_factorial(n);\n qDebug() << \"CalcModulePlugin::factorial\" << n << \"! =\" << result;\n return result;\n }\n\n int CalcModulePlugin::fibonacci(int n)\n {\n int result = calc_fibonacci(n);\n qDebug() << \"CalcModulePlugin::fibonacci fib(\" << n << \") =\" << result;\n return result;\n }\n\n QString CalcModulePlugin::libVersion()\n {\n const char* ver = calc_version();\n QString result = QString::fromUtf8(ver);\n qDebug() << \"CalcModulePlugin::libVersion\" << result;\n return result;\n }\n\n void CalcModulePlugin::libVersionNotify()\n {\n const char* ver = calc_version();\n QString result = QString::fromUtf8(ver);\n qDebug() << \"CalcModulePlugin::libVersionNotify\" << result;\n emit eventResponse(\"versionReady\", {result});\n }\n post_text: |\n **The wrapping pattern** is always the same:\n\n 1. Call the C function with the arguments\n 2. Convert the C result to a Qt type if needed (e.g., `const char*` → `QString`)\n 3. Return the Qt type\n\n # ── Step 4: Build the Module ────────────────────────────────────────────────\n - title: \"Build the Module\"\n step: true\n steps:\n - title: \"Initialize the Git repo\"\n text: |\n Nix flakes require a git repository.\n\n Before staging files, create a `.gitignore` to exclude build artifacts:\n file:\n path: .gitignore\n language: text\n content: |\n # Nix build output\n result\n result-*\n\n # CMake build directory\n build/\n\n - text: \"Then initialise the repo:\"\n run: \"git init\"\n - run: \"git add -A\"\n - run: \"nix flake update\"\n - run: \"git add flake.lock\"\n\n - title: \"Build the plugin library\"\n text: |\n Build just the plugin library (`.so` / `.dylib`):\n run: \"nix build '.#lib'\"\n post_text: |\n > **Quoting matters:** Use `'.#lib'` (with quotes) rather than bare `nix build .#lib`. Some shells (especially zsh) may interpret the `#` as a comment character.\n\n 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.\n\n - title: \"Build the full package\"\n text: \"Build everything (library + generated SDK headers):\"\n run: \"nix build\"\n\n - title: \"Inspect the output\"\n run: \"ls -la result/lib/\"\n post_text: |\n You should see two files (extensions depend on your platform):\n\n ```\n # Linux\n calc_module_plugin.so # Your Logos module plugin\n libcalc.so # The C library (copied alongside)\n\n # macOS\n calc_module_plugin.dylib\n libcalc.dylib\n ```\n\n Both library files are placed together so the plugin can find the C library at runtime via RPATH.\n\n - check_file: \"result/lib/calc_module_plugin.{ext}\"\n\n # ── Step 5: Inspect the Module ──────────────────────────────────────────────\n - title: \"Inspect the Module\"\n step: true\n text: |\n Use the `lm` CLI tool (from `logos-module`) to inspect the compiled module binary.\n steps:\n - title: \"Build the `lm` tool\"\n text: |\n The `lm` CLI inspects compiled module binaries. Build it from the `logos-module` repo:\n run: \"nix build 'github:logos-co/logos-module{release}#lm' --out-link ./lm\"\n\n - title: \"View metadata\"\n run: \"./lm/bin/lm metadata result/lib/calc_module_plugin.{ext}\"\n code_block: |\n # Linux\n ./lm/bin/lm metadata result/lib/calc_module_plugin.so\n\n # macOS\n ./lm/bin/lm metadata result/lib/calc_module_plugin.dylib\n expect_contains:\n - \"Name: calc_module\"\n - \"Version: 1.0.0\"\n - \"Type: core\"\n post_text: |\n Output:\n\n ```\n Plugin Metadata:\n ================\n Name: calc_module\n Version: 1.0.0\n Description: Calculator module wrapping libcalc C library\n Author:\n Type: core\n Dependencies: (none)\n ```\n\n - title: \"List methods\"\n run: \"./lm/bin/lm methods result/lib/calc_module_plugin.{ext}\"\n code_block: |\n # Linux\n ./lm/bin/lm methods result/lib/calc_module_plugin.so\n\n # macOS\n ./lm/bin/lm methods result/lib/calc_module_plugin.dylib\n expect_contains:\n - \"int add(int a, int b)\"\n - \"int multiply(int a, int b)\"\n - \"int factorial(int n)\"\n - \"int fibonacci(int n)\"\n - \"QString libVersion()\"\n post_text: |\n Output:\n\n ```\n Plugin Methods:\n ===============\n\n void eventResponse(QString eventName, QVariantList args)\n Signature: eventResponse(QString,QVariantList)\n Invokable: no\n\n void initLogos(LogosAPI* api)\n Signature: initLogos(LogosAPI*)\n Invokable: yes\n\n int add(int a, int b)\n Signature: add(int,int)\n Invokable: yes\n\n int multiply(int a, int b)\n Signature: multiply(int,int)\n Invokable: yes\n\n int factorial(int n)\n Signature: factorial(int)\n Invokable: yes\n\n int fibonacci(int n)\n Signature: fibonacci(int)\n Invokable: yes\n\n QString libVersion()\n Signature: libVersion()\n Invokable: yes\n ```\n\n All five wrapping methods are visible and invokable. The `initLogos` method is automatically called by the Logos host when loading the module.\n\n - title: \"JSON output\"\n text: \"For scripting and CI, use `--json`:\"\n run: \"./lm/bin/lm methods result/lib/calc_module_plugin.{ext} --json\"\n code_block: |\n # Linux\n ./lm/bin/lm methods result/lib/calc_module_plugin.so --json\n\n # macOS\n ./lm/bin/lm methods result/lib/calc_module_plugin.dylib --json\n expect_contains:\n - '\"name\": \"add\"'\n post_text: |\n ```json\n [\n {\n \"isInvokable\": true,\n \"name\": \"add\",\n \"parameters\": [\n { \"name\": \"a\", \"type\": \"int\" },\n { \"name\": \"b\", \"type\": \"int\" }\n ],\n \"returnType\": \"int\",\n \"signature\": \"add(int,int)\"\n },\n ...\n ]\n ```\n\n # ── Step 6: Test with logoscore ─────────────────────────────────────────────\n - title: \"Test with `logoscore`\"\n step: true\n steps:\n - title: \"Build logoscore\"\n run: \"nix build 'github:logos-co/logos-logoscore-cli{release}' --out-link ./logos\"\n\n - title: \"Set up the modules directory\"\n text: |\n `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:\n run: \"nix build '.#lgx'\"\n - run: \"nix build 'github:logos-co/logos-package-manager{release}#cli' --out-link ./pm\"\n - run: \"mkdir -p modules\"\n - run: \"./pm/bin/lgpm --modules-dir ./modules install --file result/*.lgx\"\n post_text: |\n This extracts the plugin, external libraries, and manifest into the correct directory structure:\n\n ```\n modules/calc_module/\n ├── calc_module_plugin.dylib # (or .so on Linux)\n ├── libcalc.dylib # (or .so on Linux)\n ├── manifest.json # Auto-generated by lgx\n └── variant # Platform variant identifier\n ```\n\n - title: \"Call methods\"\n text: \"Start the daemon and call methods:\"\n run: \"./logos/bin/logoscore -D -m ./modules &\"\n\n - run: \"sleep 3\"\n\n - run: \"./logos/bin/logoscore load-module calc_module\"\n\n - run: \"./logos/bin/logoscore call calc_module add 3 5\"\n expect_contains:\n - '\"result\":8'\n\n - run: \"./logos/bin/logoscore call calc_module factorial 5\"\n expect_contains:\n - '\"result\":120'\n\n - run: \"./logos/bin/logoscore call calc_module fibonacci 10\"\n expect_contains:\n - '\"result\":55'\n\n - run: \"./logos/bin/logoscore call calc_module libVersion\"\n expect_contains:\n - '\"result\":\"1.0.0\"'\n\n - run: \"./logos/bin/logoscore stop\"\n post_text: |\n > For inline (legacy) mode and other logoscore options, see the [Developer Guide -- Running with logoscore](logos-developer-guide.md#51-running-with-logoscore).\n\n **What happens under the hood:**\n\n 1. `logoscore` scans `./modules/` for subdirectories containing `manifest.json`\n 2. It finds `calc_module` and extracts metadata from the plugin binary\n 3. It spawns a `logos_host` process that loads `calc_module_plugin.so`\n 4. `logos_host` calls `initLogos()` on the plugin, providing a `LogosAPI*` for inter-module communication\n 5. The call command is parsed: module name `calc_module`, method `add`, args `[3, 5]`\n 6. `logoscore` sends the call to `logos_host` via Qt Remote Objects (IPC)\n 7. `logos_host` invokes `CalcModulePlugin::add(3, 5)` which calls `calc_add(3, 5)` from libcalc\n 8. The result is returned via IPC to `logoscore`\n\n You'll see debug output like:\n\n ```\n Debug: Found plugin: \"./modules/calc_module/calc_module_plugin.so\"\n Debug: Plugin Metadata:\n Debug: - Name: \"calc_module\"\n Debug: - Version: \"1.0.0\"\n Debug: - Description: \"Calculator module wrapping libcalc C library\"\n Debug: Loading plugin: \"calc_module\" in separate process\n Debug: Executing call: \"calc_module\" . \"add\" with 2 params\n Method call successful. Result: ...\n ```\n\n # ── Package for Distribution (prose only) ──────────────────────────────────\n - title: \"Package for Distribution (Optional)\"\n text: |\n 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:\n\n ```bash\n nix build '.#lgx-portable'\n ```\n\n Portable LGX packages are fully self-contained with no `/nix/store` references at runtime. These are the packages used by the Logos App Package Manager UI and published to [logos-modules](https://github.com/logos-co/logos-modules) releases.\n\n To create both dev and portable variants (the dev variant works with local `nix build` of basecamp; the portable variant works with standalone basecamp builds), use `--out-link` to avoid overwriting the `result` symlink:\n\n ```bash\n nix build '.#lgx' --out-link result-lgx\n nix build '.#lgx-portable' --out-link result-lgx-portable\n ```\n\n > 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).\n\n To install a portable package on another machine:\n\n ```bash\n nix build 'github:logos-co/logos-package-manager{release}#cli' --out-link ./pm\n ./pm/bin/lgpm --modules-dir ./modules install --file result-lgx-portable/*.lgx\n ```\n\n > **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.\n\n # ── Common Wrapping Patterns (prose only) ──────────────────────────────────\n - title: \"Common Wrapping Patterns\"\n text: |\n ### Wrapping C functions with opaque pointers\n\n Many C libraries use opaque pointers (handles) for state management:\n\n ```c\n // C API\n typedef struct db_ctx db_ctx_t;\n db_ctx_t* db_open(const char* path);\n int db_get(db_ctx_t* ctx, const char* key, char* buf, int buf_len);\n void db_close(db_ctx_t* ctx);\n ```\n\n Store the handle in your plugin class:\n\n ```cpp\n class DbModulePlugin : public QObject, public DbModuleInterface\n {\n // ...\n private:\n db_ctx_t* m_ctx = nullptr;\n\n public:\n Q_INVOKABLE bool open(const QString& path) {\n m_ctx = db_open(path.toUtf8().constData());\n return m_ctx != nullptr;\n }\n\n Q_INVOKABLE QString get(const QString& key) {\n if (!m_ctx) return QString();\n char buf[4096];\n int len = db_get(m_ctx, key.toUtf8().constData(), buf, sizeof(buf));\n if (len < 0) return QString();\n return QString::fromUtf8(buf, len);\n }\n\n ~DbModulePlugin() {\n if (m_ctx) db_close(m_ctx);\n }\n };\n ```\n\n ### Wrapping C callbacks\n\n C libraries often use callbacks for async operations:\n\n ```c\n typedef void (*event_cb)(int code, const char* msg, void* user_data);\n void lib_set_callback(void* ctx, event_cb cb, void* user_data);\n ```\n\n Use a static method as the callback, passing `this` as `user_data`:\n\n ```cpp\n class MyPlugin : public QObject, public MyInterface\n {\n // ...\n static void c_callback(int code, const char* msg, void* user_data) {\n auto* self = static_cast(user_data);\n // Forward to Qt signal (thread-safe)\n emit self->eventResponse(\"lib_event\",\n QVariantList() << code << QString::fromUtf8(msg));\n }\n\n Q_INVOKABLE void startListening() {\n lib_set_callback(m_ctx, c_callback, this);\n }\n };\n ```\n\n ### Wrapping C libraries that allocate strings\n\n If the C library returns allocated strings that must be freed:\n\n ```cpp\n Q_INVOKABLE QString getData() {\n char* c_str = lib_get_data(m_ctx); // Library allocates\n QString result = QString::fromUtf8(c_str);\n lib_free_string(c_str); // Library deallocates\n return result;\n }\n ```\n\n ### String conversion reference\n\n | C type | Qt type | C → Qt | Qt → C |\n | ---------------------- | ----------------- | -------------------------- | -------------------------- |\n | `const char*` | `QString` | `QString::fromUtf8(c_str)` | `str.toUtf8().constData()` |\n | `const char*` (binary) | `QByteArray` | `QByteArray(data, len)` | `ba.data()`, `ba.size()` |\n | `int` | `int` | direct | direct |\n | `bool` / `int` | `bool` | `result != 0` | direct |\n | `void*` | (store in member) | — | — |\n\n # ── Advanced: Wrapping a Library from a Flake Input (prose only) ──────────\n - title: \"Advanced: Wrapping a Library from a Flake Input\"\n text: |\n 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.\n\n ### flake.nix with external library input\n\n ```nix\n {\n description = \"Module wrapping libfoo from GitHub\";\n\n inputs = {\n logos-module-builder.url = \"github:logos-co/logos-module-builder\";\n\n # Fetch the library source (non-flake)\n libfoo-src = {\n url = \"github:example/libfoo\";\n flake = false;\n };\n };\n\n outputs = inputs@{ logos-module-builder, libfoo-src, ... }:\n logos-module-builder.lib.mkLogosModule {\n src = ./.;\n configFile = ./metadata.json;\n flakeInputs = inputs;\n\n # Pass the fetched source to the builder\n externalLibInputs = {\n foo = libfoo-src;\n };\n };\n }\n ```\n\n ### metadata.json for flake input\n\n ```json\n {\n \"name\": \"foo_module\",\n \"version\": \"1.0.0\",\n \"type\": \"core\",\n \"description\": \"Module wrapping libfoo\",\n \"main\": \"foo_module_plugin\",\n \"dependencies\": [],\n\n \"nix\": {\n \"packages\": { \"build\": [], \"runtime\": [] },\n \"external_libraries\": [\n {\n \"name\": \"foo\",\n \"flake_input\": \"github:example/libfoo\",\n \"build_command\": \"make shared\",\n \"output_pattern\": \"build/libfoo.*\"\n }\n ],\n \"cmake\": {\n \"find_packages\": [],\n \"extra_sources\": [],\n \"extra_include_dirs\": [\"lib\"],\n \"extra_link_libraries\": []\n }\n }\n }\n ```\n\n **Key difference:** The `externalLibInputs` key in flake.nix (`foo`) must match the `name` field in `nix.external_libraries` (`foo`). The builder will:\n\n 1. Clone the source from the flake input\n 2. Run `build_command` (`make shared`)\n 3. Search for output files matching `output_pattern`\n 4. Copy the resulting `.so`/`.dylib` and headers to `lib/`\n 5. Proceed with the normal module build\n\n ### For Go libraries\n\n If the external library is written in Go with C bindings (`cgo`), set `go_build: true` in the `nix.external_libraries` entry within `metadata.json`:\n\n ```json\n {\n \"nix\": {\n \"external_libraries\": [\n {\n \"name\": \"mygolib\",\n \"flake_input\": \"github:example/mygolib\",\n \"go_build\": true,\n \"output_pattern\": \"libmygolib.*\"\n }\n ]\n }\n }\n ```\n\n Setting `go_build: true` enables the Go toolchain and sets `CGO_ENABLED=1`.\n\n # ── Real-World Example (prose only) ──────────────────────────────────────\n - title: \"Real-World Example: logos-libp2p-module\"\n text: |\n 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:\n\n - `**flake.nix**` — Uses `externalLibInputs` to fetch the nim-libp2p C bindings from a GitHub flake\n - `**metadata.json**` — Declares `nim_libp2p` as an external library with `go_build: false` in the `nix` section\n - `**src/plugin.cpp**` — Wraps ~40 C functions (`libp2p_new`, `libp2p_start`, `libp2p_connect`, `libp2p_dial`, `libp2p_gossipsub_subscribe`, etc.) as `Q_INVOKABLE` methods\n - `**tests/**` — Qt test suite that exercises every wrapped function\n\n It follows the exact same pattern as this tutorial, just at a larger scale.\n\n # ── Troubleshooting (prose only) ────────────────────────────────────────────\n - title: \"Troubleshooting\"\n text: |\n ### `initLogos` marked 'override', but does not override\n\n ```\n error: 'void MyPlugin::initLogos(LogosAPI*)' marked 'override', but does not override\n ```\n\n **Fix:** Remove the `override` keyword from `initLogos`. The base `PluginInterface` class does not declare it as virtual. The Logos host calls it reflectively via `QMetaObject::invokeMethod`. Declare it as:\n\n ```cpp\n Q_INVOKABLE void initLogos(LogosAPI* api); // No override!\n ```\n\n ### Library not found at runtime\n\n ```\n Cannot load library calc_module_plugin.so: libcalc.so: cannot open shared object file\n ```\n\n **Fix:** Ensure `libcalc.so` / `libcalc.dylib` is in the same directory as the plugin. The build system sets RPATH to `$ORIGIN` (Linux) / `@loader_path` (macOS) so the plugin looks for libraries in its own directory.\n\n ### `initLogos` stores API pointer in wrong variable\n\n If inter-module calls or API features silently fail, check that `initLogos` assigns to the **global** `logosAPI` variable (defined in the Logos SDK / liblogos), not to a class member like `m_logosAPI`:\n\n ```cpp\n // CORRECT — uses the global variable from liblogos\n void MyPlugin::initLogos(LogosAPI* api)\n {\n logosAPI = api;\n }\n\n // WRONG — stores in a local member, API calls won't work\n void MyPlugin::initLogos(LogosAPI* api)\n {\n m_logosAPI = api;\n }\n ```\n\n ### Plugin not discovered by logoscore\n\n **Check:**\n\n 1. The module is in a **subdirectory** of the modules dir (e.g., `modules/calc_module/`)\n 2. The subdirectory contains a `manifest.json` with a valid `main` object\n 3. The platform key in `main` matches your OS/arch (e.g., `linux-aarch64`, `darwin-arm64`)\n\n ### `nix build .#lib` does nothing or fails silently\n\n Some shells (notably zsh) treat `#` as a comment character. Always quote the flake reference:\n\n ```bash\n # Correct\n nix build '.#lib'\n\n # May fail in zsh\n nix build .#lib\n ```\n\n ### First build is slow\n\n The first `nix build` downloads Qt 6, the Logos C++ SDK, the code generator, and other dependencies. This is a one-time cost — subsequent builds use the Nix cache and are fast (usually under 30 seconds).\n\n ### Symbol not found errors\n\n If you get \"undefined symbol\" errors for your C library functions:\n\n 1. Verify the `.so`/`.dylib` is in `lib/` before building\n 2. Verify the header has `extern \"C\"` guards\n 3. Check the symbols are exported: `nm -D lib/libcalc.so | grep calc`\n" }, "qml-ui-app": { "filename": "tutorial-qml-ui-app.test.yaml", "yaml": "name: \"Tutorial Part 2: Building a QML UI for Your Logos Module\"\noutput: tutorial-qml-ui-app.md\nproject_name: logos-calc-ui\nrequires:\n - tutorial-wrapping-c-library.test.yaml\nrelease: \"\"\n\nintro: |\n 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`.\n\nwhat_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.\"\n\nwhat_you_learn:\n - How QML UI plugins work in the Logos platform\n - \"The `logos.callModule()` bridge that connects QML to core modules\"\n - The project structure and metadata for a QML plugin\n - \"How to package and install your UI into `logos-basecamp`\"\n\nprerequisites:\n - \"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/`)\"\n - Nix with flakes enabled (same as Part 1)\n - \"Basic familiarity with QML (Qt's declarative UI language)\"\n\nsections:\n # ── How QML UI Plugins Work (prose only) ────────────────────────────────────\n - title: \"How QML UI Plugins Work\"\n text: |\n Before writing code, let's understand the architecture:\n\n ```\n +-------------------+ logos.callModule() +-------------------+\n | calc_ui | --------------------------> | calc_module |\n | Main.qml (QML) | IPC (Qt Remote Objects) | C++ plugin |\n +-------------------+ +-------------------+\n ^ ^\n └──────────────── loaded by ───────────────────────┘\n logos-basecamp / logos-standalone-app\n ```\n\n Key points:\n\n - **No compilation.** A QML plugin is just `.qml` files and a `metadata.json`.\n - **Sandboxed.** No network access, no filesystem access outside the module directory.\n - **The `logos` bridge** is injected by the host. Call core modules with `logos.callModule(\"module\", \"method\", [args])`.\n - **Entry point** is defined by the required `\"view\"` field in `metadata.json` (for this tutorial it is `Main.qml`).\n\n # ── Step 1: Scaffold ───────────────────────────────────────────────────────\n - title: \"Scaffold\"\n step: true\n text: |\n Create a new directory and initialise it from the QML module template:\n\n `mkdir logos-calc-ui && cd logos-calc-ui`\n steps:\n - run: \"nix flake init -t github:logos-co/logos-module-builder{release}#ui-qml\"\n post_text: |\n > **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.\n\n - run: \"git init\"\n - run: \"git add -A\"\n post_text: |\n This gives you:\n\n ```\n logos-calc-ui/\n ├── flake.nix # Nix build + nix run support\n ├── metadata.json # Plugin metadata\n └── Main.qml # Your UI (starter template)\n ```\n\n # ── Step 2: Update metadata.json ───────────────────────────────────────────\n - title: \"Update `metadata.json`\"\n step: true\n text: |\n 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:\n steps:\n - file:\n path: metadata.json\n language: json\n content: |\n {\n \"name\": \"calc_ui\",\n \"version\": \"1.0.0\",\n \"description\": \"Calculator UI - QML frontend for the calc_module\",\n \"type\": \"ui_qml\",\n \"view\": \"Main.qml\",\n \"dependencies\": [\"calc_module\"],\n \"category\": \"tools\",\n \"icon\": \"icons/calc.png\",\n\n \"nix\": {\n \"packages\": {\n \"build\": [],\n \"runtime\": []\n },\n \"external_libraries\": [],\n \"cmake\": {\n \"find_packages\": [],\n \"extra_sources\": [],\n \"extra_include_dirs\": [],\n \"extra_link_libraries\": []\n }\n }\n }\n post_text: |\n Create the icon directory and add a placeholder icon. The icon is displayed in the `logos-basecamp` sidebar when the module is loaded:\n\n - run: \"mkdir -p icons && echo 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAmElEQVR4nO3QMREAIBDAsFeEN3ziCWRkoEP2XmedfX82OkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAO0BN/SiO/PatoIAAAAASUVORK5CYII=' | base64 -d > icons/calc.png\"\n code_block: |\n mkdir -p icons\n # Copy any PNG here — or generate a 64×64 placeholder:\n echo \"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAmElEQVR4nO3QMREAIBDAsFeEN3ziCWRkoEP2XmedfX82OkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAK0BOkBrgA7QGqADtAboAO0BN/SiO/PatoIAAAAASUVORK5CYII=\" | base64 -d > icons/calc.png\n post_text: |\n 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.\n\n > **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.\n\n # ── Step 3: Write Main.qml ─────────────────────────────────────────────────\n - title: \"Write `Main.qml`\"\n step: true\n text: |\n Replace the starter file with the calculator UI. This demonstrates two communication patterns:\n\n 1. **Direct calls** — `logos.callModule()` sends a request and returns the result immediately\n 2. **Event-based** — `logos.callModule()` fires-and-forgets, the module emits an event, and QML receives it via `logos.onModuleEvent()`\n steps:\n - file:\n path: Main.qml\n language: qml\n content: |\n import QtQuick\n import QtQuick.Controls\n import QtQuick.Layouts\n\n Item {\n id: root\n\n property string result: \"\"\n property string errorText: \"\"\n property string versionFromEvent: \"\"\n\n // ── Event subscription ────────────────────────────────────\n // Subscribe to \"versionReady\" events pushed from calc_module.\n Component.onCompleted: {\n if (typeof logos !== \"undefined\" && logos.onModuleEvent)\n logos.onModuleEvent(\"calc_module\", \"versionReady\")\n }\n\n Connections {\n target: typeof logos !== \"undefined\" ? logos : null\n function onModuleEventReceived(moduleName, eventName, data) {\n if (eventName === \"versionReady\")\n root.versionFromEvent = data[0]\n }\n }\n\n ColumnLayout {\n anchors.fill: parent\n anchors.margins: 24\n spacing: 16\n\n // ── Title ──────────────────────────────────────────────\n Text {\n text: \"Logos Calculator\"\n font.pixelSize: 20\n font.weight: Font.DemiBold\n color: \"#ffffff\"\n Layout.alignment: Qt.AlignHCenter\n }\n\n // ── Pattern 1: Direct call (request -> response) ──────\n Text {\n text: \"Direct calls (logos.callModule -> returns result)\"\n color: \"#8b949e\"\n font.pixelSize: 12\n }\n\n RowLayout {\n spacing: 12\n Layout.fillWidth: true\n\n TextField {\n id: inputA\n placeholderText: \"a\"\n Layout.preferredWidth: 80\n validator: IntValidator {}\n }\n\n TextField {\n id: inputB\n placeholderText: \"b\"\n Layout.preferredWidth: 80\n validator: IntValidator {}\n }\n\n Button {\n text: \"Add\"\n onClicked: callTwoOp(\"add\", inputA.text, inputB.text)\n }\n\n Button {\n text: \"Multiply\"\n onClicked: callTwoOp(\"multiply\", inputA.text, inputB.text)\n }\n }\n\n RowLayout {\n spacing: 12\n Layout.fillWidth: true\n\n TextField {\n id: inputN\n placeholderText: \"n\"\n Layout.preferredWidth: 80\n validator: IntValidator { bottom: 0 }\n }\n\n Button {\n text: \"Factorial\"\n onClicked: callOneOp(\"factorial\", inputN.text)\n }\n\n Button {\n text: \"Fibonacci\"\n onClicked: callOneOp(\"fibonacci\", inputN.text)\n }\n\n Button {\n text: \"libcalc version\"\n onClicked: callModule(\"libVersion\", [])\n }\n }\n\n // Direct call result\n Rectangle {\n Layout.fillWidth: true\n height: 56\n color: root.errorText.length > 0 ? \"#3d1a1a\" : \"#1a2d1a\"\n radius: 8\n\n Text {\n anchors.centerIn: parent\n text: root.errorText.length > 0 ? root.errorText\n : (root.result.length > 0 ? root.result : \"Enter values and press a button\")\n color: root.errorText.length > 0 ? \"#f85149\" : \"#56d364\"\n font.pixelSize: 15\n }\n }\n\n // ── Pattern 2: Event-based (fire-and-forget -> event) ─\n Text {\n text: \"Event-based (fire-and-forget call -> result via event)\"\n color: \"#8b949e\"\n font.pixelSize: 12\n }\n\n RowLayout {\n spacing: 12\n Layout.fillWidth: true\n\n Button {\n text: \"libcalc version (event)\"\n onClicked: {\n if (typeof logos !== \"undefined\" && logos.callModule)\n logos.callModule(\"calc_module\", \"libVersionNotify\", [])\n }\n }\n }\n\n // Event result\n Rectangle {\n Layout.fillWidth: true\n height: 56\n color: \"#1a1a2d\"\n radius: 8\n\n Text {\n anchors.centerIn: parent\n text: root.versionFromEvent.length > 0\n ? (\"Version (via event): \" + root.versionFromEvent)\n : \"Press the event button — result arrives via event\"\n color: \"#7ab8ff\"\n font.pixelSize: 15\n }\n }\n\n Item { Layout.fillHeight: true }\n }\n\n // ── Direct call helpers ───────────────────────────────────\n\n function callModule(method, args) {\n root.errorText = \"\"\n root.result = \"\"\n\n if (typeof logos === \"undefined\" || !logos.callModule) {\n root.errorText = \"Logos bridge not available\"\n return\n }\n\n root.result = String(logos.callModule(\"calc_module\", method, args))\n }\n\n function callTwoOp(method, a, b) {\n if (a === \"\" || b === \"\") { root.errorText = \"Enter values for a and b\"; return }\n callModule(method, [parseInt(a), parseInt(b)])\n }\n\n function callOneOp(method, n) {\n if (n === \"\") { root.errorText = \"Enter a value for n\"; return }\n callModule(method, [parseInt(n)])\n }\n }\n post_text: |\n The UI demonstrates two communication patterns:\n\n - **Green section (direct calls):** `logos.callModule(\"calc_module\", \"libVersion\", [])` sends a request to `calc_module` and returns the result synchronously. Simple request/response.\n\n - **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`.\n\n The `logos` object is injected by the host at runtime.\n\n # ── Step 4: Update flake.nix ───────────────────────────────────────────────\n - title: \"Update `flake.nix`\"\n step: true\n text: |\n The template already has everything wired up. Update the description and add `calc_module` as a dependency input:\n steps:\n - file:\n path: flake.nix\n language: nix\n content: |\n {\n description = \"Calculator QML UI Plugin for Logos - frontend for calc_module\";\n\n inputs = {\n logos-module-builder.url = \"github:logos-co/logos-module-builder{release}\";\n\n # Option A: point to a remote repo (for CI or when calc_module is published)\n calc_module.url = \"github:logos-co/logos-tutorial?dir=logos-calc-module\";\n\n # Option B: point to your local checkout (for local development)\n # calc_module.url = \"path:../logos-calc-module\";\n };\n\n outputs = inputs@{ logos-module-builder, ... }:\n logos-module-builder.lib.mkLogosQmlModule {\n src = ./.;\n configFile = ./metadata.json;\n flakeInputs = inputs;\n };\n }\n post_text: |\n The input attribute name (`calc_module`) must match the dependency name in `metadata.json`.\n\n The `calc_module.url` can be either:\n\n - **`github:`** — fetches from a remote GitHub repo. Use this for CI or when `calc_module` has been published.\n - **`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`).\n\n > **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.\n\n `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.\n\n > **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).\n\n # ── Step 5: Test with nix run ──────────────────────────────────────────────\n - title: \"Test with `nix run`\"\n step: true\n steps:\n - title: \"UI only (layout preview)\"\n run: \"git add -A\"\n - run: \"nix flake update\"\n - run: \"git add flake.lock\"\n - ui_test:\n launch: \"nix run .\"\n setup:\n - \"nix build 'github:logos-co/logos-qt-mcp{release}' -o result-mcp\"\n qt_mcp: \"result-mcp\"\n tests:\n - name: \"App window opens with title\"\n action: wait_for\n texts: [\"Logos Calculator\"]\n timeout: 15000\n - name: \"Add button visible\"\n action: wait_for\n texts: [\"Add\"]\n timeout: 5000\n - name: \"Multiply button visible\"\n action: wait_for\n texts: [\"Multiply\"]\n timeout: 5000\n - name: \"Factorial button visible\"\n action: wait_for\n texts: [\"Factorial\"]\n timeout: 5000\n - name: \"Fibonacci button visible\"\n action: wait_for\n texts: [\"Fibonacci\"]\n timeout: 5000\n post_text: |\n 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.\n\n # ── Step 5b: Full functionality (with modules) ───────────────────────────\n # Requires ../logos-calc-module from Part 1. Use --phase modules to enable.\n - title: \"Full functionality (with modules)\"\n step: true\n text: |\n 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.\n steps:\n - title: \"Ensure `calc_module` is built\"\n text: |\n Go back to your `logos-calc-module` directory and verify the shared library exists:\n run: \"ls ../logos-calc-module/lib/libcalc.{ext}\"\n code_block: |\n ls ../logos-calc-module/lib/libcalc.so # Linux\n ls ../logos-calc-module/lib/libcalc.dylib # macOS\n post_text: |\n 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)):\n extra_run:\n run: \"cd ../logos-calc-module/lib && gcc {shared_flags} -o libcalc.{ext} libcalc.c && cd ../../logos-calc-ui\"\n code_block: |\n cd ../logos-calc-module/lib\n gcc -shared -fPIC -o libcalc.so libcalc.c # Linux\n # gcc -shared -fPIC -o libcalc.dylib libcalc.c # macOS\n cd ../../logos-calc-ui\n post_text: |\n Also make sure the module itself builds successfully:\n\n - run: \"cd ../logos-calc-module && git add -A && nix build && cd ../logos-calc-ui\"\n code_block: |\n cd ../logos-calc-module\n git add -A\n nix build\n cd ../logos-calc-ui\n post_text: |\n 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.\n\n - title: \"Option A: Use `--override-input` (quick, no flake.nix edits)\"\n text: |\n If your `flake.nix` points to a `github:` URL, you can override it at build time to use your local checkout:\n ui_test:\n launch: \"nix run . --override-input calc_module path:../logos-calc-module\"\n setup:\n - \"nix build 'github:logos-co/logos-qt-mcp{release}' -o result-mcp\"\n qt_mcp: \"result-mcp\"\n tests:\n - name: \"App title visible\"\n action: wait_for\n texts: [\"Logos Calculator\"]\n timeout: 15000\n - name: \"All operation buttons visible\"\n action: wait_for\n texts: [\"Add\", \"Multiply\", \"Factorial\", \"Fibonacci\"]\n timeout: 5000\n post_text: |\n 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.\n\n - title: \"Option B: Set `path:` in `flake.nix` (persistent local development)\"\n text: |\n 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:\n\n ```nix\n # From remote:\n calc_module.url = \"github:logos-co/logos-tutorial?dir=logos-calc-module\";\n # To local:\n calc_module.url = \"path:../logos-calc-module\";\n ```\n\n Then run normally without overrides:\n\n ```bash\n nix flake update # re-lock with the local path\n git add flake.lock\n nix run .\n ```\n\n 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.\n\n - title: \"Option C: Pin to the remote repo\"\n text: |\n 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:\n\n ```bash\n nix run .\n ```\n\n > **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.\n\n Whichever option you choose, clicking **Add**, **Multiply**, **Factorial**, or **Fibonacci** now calls the real module.\n\n # ── Step 6: Using the Logos Design System ──────────────────────────────────\n - title: \"Using the Logos Design System\"\n step: true\n text: |\n `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.\n\n ```qml\n import Logos.Theme\n import Logos.Controls\n import Logos.Icons // optional: shared icon assets (LogosIcons.search, .install, .refresh, …)\n ```\n\n ### Why use it\n\n 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.\n\n ### What's available\n\n Run the storybook to browse every component interactively with live property editors:\n\n ```bash\n cd repos/logos-design-system\n nix run # or: ws run logos-design-system\n ```\n\n The sidebar splits components into two sections:\n\n - **Controls** — *designed per Figma, production-ready*. Use these directly. Examples: `LogosButton`, `LogosBadge`, `LogosCheckbox`, `LogosComboBox`, `LogosIconButton`, `LogosPaginator`, `LogosSearchBar`, `LogosTabBar` / `LogosTabButton`, `LogosTable` / `LogosTableColumn`, `LogosText`, `LogosTextField`, `LogosToolTip`.\n - **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`.\n\n Each storybook page exposes a `designed: true/false` flag if you want to see at a glance which it is.\n\n ### Replace raw Qt controls with Logos equivalents\n\n ```qml\n // Instead of Button:\n LogosButton {\n text: qsTr(\"Add\")\n onClicked: callTwoOp(\"add\", inputA.text, inputB.text)\n }\n\n // Instead of TextField:\n LogosTextField {\n id: inputA\n placeholderText: qsTr(\"a\")\n }\n\n // Use theme colors instead of hardcoded hex values:\n Rectangle {\n color: Theme.palette.backgroundSecondary\n Text { color: Theme.palette.text }\n }\n ```\n\n ### Theme tokens — avoid hardcoding magic numbers\n\n ```qml\n // Palette — Theme.palette.*\n // background, backgroundSecondary, backgroundMuted, surface,\n // text, textSecondary, textMuted, textTertiary,\n // border, borderSubtle, primary, success, warning, error, info, hover, pressed, …\n\n // Spacing — Theme.spacing.*\n // tiny, small, medium, large, xlarge, xxlarge,\n // radiusSmall, radiusMedium, radiusLarge\n\n // Typography — Theme.typography.*\n // pageTitleText (36), titleText (30), panelTitleText (24),\n // subtitleText (16), primaryText (14), secondaryText (12),\n // weightRegular (400), weightMedium (500), weightBold (700),\n // publicSans (font family)\n\n // Icons — Logos.Icons.LogosIcons.*\n // arrowLeft, arrowRight, refresh, install, trash, more, search, …\n ```\n\n 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.\n\n ### Feedback and contributions\n\n 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.\n\n # ── Step 7: Load in logos-basecamp ─────────────────────────────────────────\n - title: \"Load in `logos-basecamp`\"\n step: true\n steps:\n - title: \"Bundle as LGX packages\"\n text: |\n Create `.lgx` packages for both dev and portable variants. Use `--out-link` to avoid overwriting the `result` symlink:\n 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\"\n code_block: |\n # Package calc_module (from Part 1)\n cd ../logos-calc-module\n nix build '.#lgx' --out-link result-lgx\n nix build '.#lgx-portable' --out-link result-lgx-portable\n\n # Package the QML UI plugin\n cd ../logos-calc-ui\n nix build '.#lgx' --out-link result-lgx\n nix build '.#lgx-portable' --out-link result-lgx-portable\n extra_run:\n run: \"nix build '.#lgx' --out-link result-lgx && nix build '.#lgx-portable' --out-link result-lgx-portable\"\n post_text: |\n > 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).\n\n - title: \"Build and run logos-basecamp\"\n text: |\n Build logos-basecamp, launch it once to preinstall its bundled modules, then install your modules.\n\n > **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.\n run: \"nix build 'github:logos-co/logos-basecamp{release}' -o basecamp-result\"\n post_text: |\n ```bash\n # Launch once to preinstall bundled modules, then close it\n ./basecamp-result/bin/logos-basecamp\n ```\n\n 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:\n\n ```bash\n # macOS (typical path, may vary):\n ls ~/Library/Application\\ Support/Logos/\n\n # Linux (typical path, may vary):\n ls ~/.local/share/Logos/\n ```\n\n The dev build directory is named `LogosBasecampDev` (portable builds use `LogosBasecamp`).\n\n - title: \"Install modules with lgpm\"\n text: |\n Install your modules using `lgpm`. First, set `BASECAMP_DIR` to your platform's path:\n\n ```bash\n # macOS:\n BASECAMP_DIR=\"$HOME/Library/Application Support/Logos/LogosBasecampDev\"\n\n # Linux:\n BASECAMP_DIR=\"$HOME/.local/share/Logos/LogosBasecampDev\"\n ```\n run: \"nix build 'github:logos-co/logos-package-manager{release}#cli' --out-link ./pm\"\n post_text: |\n ```bash\n # Install core module\n ./pm/bin/lgpm --modules-dir \"$BASECAMP_DIR/modules\" \\\n install --file ../logos-calc-module/result-lgx/*.lgx\n\n # Install UI plugin\n ./pm/bin/lgpm --ui-plugins-dir \"$BASECAMP_DIR/plugins\" \\\n install --file result-lgx/*.lgx\n\n # Launch basecamp -- your modules appear alongside the built-in ones\n ./basecamp-result/bin/logos-basecamp\n ```\n\n - title: \"Portable basecamp build (optional)\"\n text: |\n The dev build above depends on nix store paths at runtime. For a self-contained portable build that works without nix:\n run: \"nix build 'github:logos-co/logos-basecamp{release}#bin-bundle-dir' -o basecamp-portable\"\n post_text: |\n ```bash\n # Launch once to preinstall bundled modules\n ./basecamp-portable/bin/logos-basecamp\n ```\n\n The portable build uses a different data directory (`LogosBasecamp` instead of `LogosBasecampDev`). Set `BASECAMP_DIR` to your platform's path:\n\n ```bash\n # macOS:\n BASECAMP_DIR=\"$HOME/Library/Application Support/Logos/LogosBasecamp\"\n\n # Linux:\n BASECAMP_DIR=\"$HOME/.local/share/Logos/LogosBasecamp\"\n ```\n\n Install your modules using the **portable** `.lgx` variants:\n\n ```bash\n # Install core module (use portable variant)\n ./pm/bin/lgpm --modules-dir \"$BASECAMP_DIR/modules\" \\\n install --file ../logos-calc-module/result-lgx-portable/*.lgx\n\n # Install UI plugin (use portable variant)\n ./pm/bin/lgpm --ui-plugins-dir \"$BASECAMP_DIR/plugins\" \\\n install --file result-lgx-portable/*.lgx\n\n # Launch\n ./basecamp-portable/bin/logos-basecamp\n ```\n\n > **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.\n\n - title: \"Install via logos-basecamp UI\"\n text: |\n Instead of using `lgpm` on the command line, you can install modules through the basecamp UI:\n\n 1. Launch `logos-basecamp`\n 2. Go to **Package Manager**\n 3. Click **Install from file**\n 4. Select `../logos-calc-module/result-lgx/*.lgx` — installs `calc_module`\n 5. Repeat for `result-lgx/*.lgx` — installs `calc_ui`\n\n The \"Calculator UI\" tab appears in the sidebar. Clicking it loads your `Main.qml`.\n\n - title: \"Live reloading with `logos-standalone-app`\"\n text: |\n 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):\n\n ```bash\n DEV_QML_PATH=$PWD nix run .\n ```\n\n 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.\n\n **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:\n\n - **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.\n - **C++ / `.rep` / `metadata.json` / CMake changes** rebuild as normal.\n - The flake-evaluation overhead on each `nix run` is fixed and unavoidable while invoking through nix.\n\n For the absolute fastest loop (no nix involvement after the first build), do the build once and run the resulting binary directly:\n\n ```bash\n # Build once — populates result/ in the nix store\n nix build .\n\n # Subsequent runs: invoke the bundled standalone wrapper directly,\n # skipping nix entirely. DEV_QML_PATH still redirects QML loading.\n DEV_QML_PATH=$PWD ./result/bin/run-logos-standalone-ui\n ```\n\n (Adjust the binary name to whatever `ls result/bin/` shows on your build.)\n\n > **Naming:** Only `DEV_QML_PATH` is honored. See `repos/logos-standalone-app/README.md`.\n\n > 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.\n\n - title: \"Testing without any runtime\"\n text: |\n You can open `Main.qml` in any QML viewer (e.g., `qml` from Qt) to test the layout.\n\n #### Install\n\n You'll need to have QML and any included modules (`QtQuick` and submodules `Controls`, and `Layout`).\n\n Eg, to simply install on linux (apt package manager):\n\n ```bash\n sudo apt install qml-qt6 qml6-module-qtquick qml6-module-qtquick-controls qml6-module-qtquick-layouts\n ```\n\n #### Viewing the QML\n\n 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.\n\n ```bash\n # If you have Qt and included modules installed\n # macOS:\n qml Main.qml\n\n # Linux:\n qml6 Main.qml\n ```\n\n # ── Step 8: UI Integration Tests ─────────────────────────────────────────\n - title: \"UI Integration Tests\"\n step: true\n text: |\n 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.\n\n 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.\n steps:\n - title: \"Create a test file\"\n text: |\n Create `tests/ui-tests.mjs`:\n file:\n path: tests/ui-tests.mjs\n language: javascript\n content: |\n import { resolve } from \"node:path\";\n\n // CI sets LOGOS_QT_MCP automatically; for interactive use: nix build .#test-framework -o result-mcp\n const root =\n process.env.LOGOS_QT_MCP ||\n new URL(\"../result-mcp\", import.meta.url).pathname;\n const { test, run } = await import(\n resolve(root, \"test-framework/framework.mjs\")\n );\n\n test(\"calc_ui: loads and shows title\", async (app) => {\n await app.waitFor(\n async () => {\n await app.expectTexts([\"Logos Calculator\"]);\n },\n { timeout: 15000, interval: 500, description: \"calc_ui to load\" },\n );\n });\n\n test(\"calc_ui: add button visible\", async (app) => {\n await app.expectTexts([\"Add\"]);\n });\n\n test(\"calc_ui: click add shows validation\", async (app) => {\n await app.click(\"Add\");\n await app.waitFor(\n async () => {\n await app.expectTexts([\"Enter values for a and b\"]);\n },\n { timeout: 5000, interval: 500, description: \"validation message to appear\" },\n );\n });\n\n run();\n\n - title: \"Run the tests\"\n run: \"git add tests/\"\n - run: \"nix build .#integration-test -L --override-input calc_module path:../logos-calc-module\"\n code_block: |\n nix build .#integration-test -L\n post_text: |\n 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/`.\n\n You can have multiple test files (e.g., `tests/smoke.mjs`, `tests/interactions.mjs`) — they are all discovered and run automatically.\n\n To run tests interactively (against an already-running app):\n\n ```bash\n nix build .#test-framework -o result-mcp\n nix run . # start the app with inspector on :3768\n node tests/ui-tests.mjs # in another terminal\n ```\n\n # ── Known Limitations (prose only) ─────────────────────────────────────────\n - title: \"Known Limitations\"\n text: |\n ### QML-to-C++ type coercion\n\n 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.\n\n This means you can define methods with their natural parameter types (`int`, `bool`, `double`, etc.) and calls from QML will work without manual conversion:\n\n ```cpp\n // This works — the runtime coerces arguments automatically\n Q_INVOKABLE int add(int a, int b) { return a + b; }\n ```\n\n > **Note:** Type coercion uses `QVariant::convert()`, which rounds (not truncates) when converting `double` to `int` — e.g., `3.7` becomes `4`.\n\n ### QML changes not appearing after rebuild\n\n 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:\n\n ```bash\n QML_DISABLE_DISK_CACHE=1 ./basecamp-result/bin/logos-basecamp\n ```\n\n ### UI module not loading or basecamp behaving unexpectedly\n\n 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:\n\n ```bash\n # Remove basecamp's data directory\n # macOS:\n rm -rf ~/Library/Application\\ Support/Logos/LogosBasecampDev\n\n # Linux:\n rm -rf ~/.local/share/Logos/LogosBasecampDev\n\n # Relaunch — basecamp will re-preinstall its bundled modules\n ./basecamp-result/bin/logos-basecamp\n ```\n\n Then reinstall your custom modules.\n\n # ── Recap (prose only) ─────────────────────────────────────────────────────\n - title: \"Recap\"\n text: |\n | | Core Module (Part 1) | QML UI Plugin (Part 2) |\n | ------------------- | ----------------------------------------------- | ----------------------------- |\n | Language | C++ | QML / JavaScript |\n | Files | `.cpp`, `.h`, `CMakeLists.txt`, `metadata.json` | `Main.qml`, `metadata.json` |\n | Compilation | Yes (CMake → `.so`) | No (file copy) |\n | `metadata.type` | `\"core\"` | `\"ui_qml\"` |\n | Test command | `logoscore -m ./result/lib -l calc_module` | `nix run .` |\n | Calls other modules | Via `LogosAPI*` (C++) | Via `logos.callModule()` (JS) |\n\n # ── What's Next (prose only) ───────────────────────────────────────────────\n - title: \"What's Next\"\n text: |\n - **Add more methods** to `calc_module` and call them from QML\n - **Use Logos Design System** styled components for consistent look and feel\n - **Build a C++ UI module** for cases where QML sandboxing is too restrictive — see [Developer Guide](logos-developer-guide.md), Section 7.2\n" } };