Skip to content

feat(ctld-tools): interactive TUI + embedded reference (CTLD-TOOLS-TUI)#52

Merged
davidp57 merged 2 commits into
developfrom
feature/ctld-tools-tui
Jul 21, 2026
Merged

feat(ctld-tools): interactive TUI + embedded reference (CTLD-TOOLS-TUI)#52
davidp57 merged 2 commits into
developfrom
feature/ctld-tools-tui

Conversation

@davidp57

@davidp57 davidp57 commented Jul 21, 2026

Copy link
Copy Markdown
Member

Lot CTLD-TOOLS-TUI

Interactive textual TUI (ctld-tools tui) as the Mission Maker's single entry point, plus an
embedded reference so the tool runs with only the .exe (no CTLD src/).

Highlights

  • Embedded reference (ctld_tools/data/reference.json, generated by gen-reference, golden-tested): crates (AA injected), troop groups, arrays, scalar settings + defaults, and a settings schema. from_embedded() is the default; --src is a dev override. lupa is build-time only now — the .exe no longer bundles it.
  • Pure EditModel (operations + live validation + undo/redo + delete/update) and a pure picker filter, unit-tested without widgets; Pilot smoke test for the UI↔model wiring.
  • TUI: config by section with live validation; Add / Remove / Patch → type chooser → guided form with filter-as-you-type pickers; edit (e) and delete a tree entry (with confirmation); undo/redo; unsaved-changes guard on quit; Save/Generate use fixed canonical names; Inject via a .miz file browser; auto-loads user-config.yaml.
  • Settings help: filterable picker over the ~108 scalar settings, default shown (bold) + pre-filled; bool/enum picked from a list (enum values from the new src/CTLD_config_schema.yaml); unknown settings flagged (warning).
  • i18n (EN + FR) following the OS locale, forced by --lang / CTLD_LANG; covers the TUI and validation findings.

Runtime (src/)

  • New ctld.patchTroopGroup helper (mirrors patchCrate) → patch a troop group by name.
  • Crate weight uniqueness validated on patch too; gen-user maps a patch's weight_kg to the runtime weight key.

Packaging / docs

  • release.yml builds the .exe with textual + embedded bundle + locales, excluding lupa (verified locally: builds, lists tui, TUI starts, lupa excluded).
  • Docs mission-maker/ctld-tools.md (EN + FR), ADR 0009 note, CHANGELOG updated.

Tests

  • 101 Python tests (ruff/format/mypy clean). Lua patchTroopGroup has a busted spec and is also exercised end-to-end by gen-user via lupa. Lua lint/busted run in CI.

Closes the CTLD-TOOLS-TUI lot (PRD + 12-point amendment in .backlog/CTLD-TOOLS-TUI/).

Summary by Sourcery

Introduce an interactive TUI for ctld-tools backed by an embedded CTLD reference bundle, extend validation and runtime helpers to support troop patching and setting metadata, add i18n, and adjust packaging so the Mission Maker exe ships without lupa while tests and docs cover the new workflow.

New Features:

  • Add a textual-based interactive TUI (ctld-tools tui) as the primary Mission Maker interface for authoring and validating CTLD user-configs.
  • Embed the CTLD reference catalogue and scalar settings in a committed JSON bundle so ctld-tools runs without a CTLD src/ folder.
  • Support patching troop groups by name via a new ctld.patchTroopGroup runtime helper and corresponding YAML operations.
  • Provide i18n support (EN/FR) for the TUI and validation messages, with language selection via CLI flag or environment variable.

Enhancements:

  • Refactor ctld-tools into a pure edit model with undo/redo and entry deletion, driving both validation and generation independent of the UI.
  • Extend validation to enforce crate weight uniqueness on patches, warn on unknown scalar settings, and use structured, translatable error keys.
  • Add schema-driven metadata for scalar settings (e.g. enum choices) to power guided setting editors and value pickers in the TUI.
  • Introduce filter-as-you-type pickers and a unified save/generate/inject flow with fixed file names and unsaved-change guards in the TUI.

Build:

  • Adjust Poetry dependency groups so textual is a runtime dependency and lupa is build-time only, and configure PyInstaller to bundle the TUI, embedded data, and locales while excluding lupa.
  • Update CI and release workflows to install the correct dependency groups, generate CTLD defaults, build the exe with the embedded reference, and smoke-check the TUI command.

CI:

  • Ensure Lua and Python CI jobs run the new tests, including Lua specs for ctld.patchTroopGroup and textual-based tests with asyncio warnings filtered.

Deployment:

  • Update the release workflow to package and attach a ctld-tools.exe that includes the TUI and embedded reference but omits lupa and its native Lua binary.

Documentation:

  • Update mission-maker docs (EN/FR) to describe the interactive TUI workflow, embedded reference behaviour, and revised CLI usage.
  • Document the embedded reference bundle, i18n layer, and settings schema in the ctld-tools README, ADR 0009, roadmap, and backlog tickets.
  • Add changelog entries summarising the new TUI, embedded reference, runtime helper, and packaging changes for ctld-tools.

Tests:

  • Add unit tests for the edit model, TUI widgets and forms, embedded reference generation, i18n behaviour, and runtime no-lupa constraint.
  • Extend gen-user and validation tests to cover crate weight mapping on patch, troop patch compilation and end-to-end behaviour, and new error cases.
  • Introduce textual Pilot smoke tests to exercise the TUI flows (add/edit/delete, undo/redo, save/generate/inject) against the edit model.

A textual TUI (`ctld-tools tui`) is now the Mission Maker's single entry point for
authoring a user-config, and the reference catalogue is embedded so the tool works
with only the .exe (no CTLD src/).

Core:
- Embedded reference bundle (ctld_tools/data/reference.json), generated from src/ by a
  new `gen-reference` build step and committed with a golden parity test. It carries
  crates (AA injected), troop groups, array names, scalar settings (name -> default)
  and a settings schema. `Reference.from_embedded()` is the default for validate /
  gen-user / tui; `--src` stays a dev override. lupa moves to build-time only (dev
  group, lazy imports) — the .exe no longer bundles it.
- Pure `EditModel` (state + operations + live validation + undo/redo + delete/update)
  and a pure picker filter, unit-tested without widgets; a Pilot smoke test proves the
  UI<->model wiring.
- TUI: config shown by section with live validation; Add / Remove / Patch buttons ->
  type chooser -> guided form with filter-as-you-type pickers; edit (e) and delete a
  tree entry; undo/redo (Ctrl+Z/Y); unsaved-changes guard on quit; Save / Generate use
  fixed canonical names, Inject opens a .miz file browser. Auto-loads user-config.yaml.
- Settings help: filterable picker over the ~108 scalar settings with the default shown
  (bold) and pre-filled; bool/enum settings picked from a list (enum values from the
  new src/CTLD_config_schema.yaml); unknown settings flagged (warning).
- i18n (EN + FR) following the OS locale, forced by --lang / CTLD_LANG; covers the TUI
  and the validation findings. Tiny stdlib layer modelled on VMCT.

Runtime:
- New `ctld.patchTroopGroup(name, patch)` helper in CTLD_userSetup.lua (mirrors
  patchCrate), so a troop group can be patched by name from the user-config.
- Crate weight uniqueness is validated on patch too (not only add); gen-user maps a
  patch's weight_kg to the runtime weight key.

Packaging: release.yml builds the .exe with textual + the embedded bundle + locales,
excluding lupa. Docs (mission-maker EN + FR), ADR 0009 note, CHANGELOG updated.
@sourcery-ai

sourcery-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a full interactive textual TUI for ctld-tools, switches the tool to use an embedded reference catalogue by default (with build-time generation via lupa), and introduces i18n plus validation/runtime enhancements like troop patching and better settings/crate handling, wired through a new EditModel and CLI changes.

File-Level Changes

Change Details Files
Introduce an embedded CTLD reference bundle and adjust reference resolution and validation to use it, including scalar settings and an authoring schema.
  • Add genreference module to build a JSON bundle from CTLD_config.lua via lupa and commit it as ctld_tools/data/reference.json, with golden tests for parity.
  • Extend Reference to load from embedded JSON or live src/, expose crate/troop names, scalar settings, enum choices, and structured errors (RefError).
  • Update validate logic to consume the richer Reference API, enforce crate weight uniqueness on patch as well as add, and add scalar settings validation with i18n-aware Finding objects.
tools/ctld-tools/ctld_tools/genreference.py
tools/ctld-tools/ctld_tools/reference.py
tools/ctld-tools/tests/test_genreference.py
tools/ctld-tools/tests/test_reference.py
tools/ctld-tools/ctld_tools/validate.py
tools/ctld-tools/tests/test_validate.py
src/CTLD_config_schema.yaml
Add a pure EditModel for user-config state and operations, plus a textual TUI application with filterable pickers, forms, undo/redo, delete, save/generate/inject, and unsaved-changes guarding.
  • Implement EditModel to hold config, support add/remove/patch for crates and troops, settings and arrays edits, delete/update by address, undo/redo stack, live validation and generation gating.
  • Implement textual-based CtldToolsApp with tree view of config, RichLog for findings, action buttons wired to typed modal forms, file browser for .miz inject, canonical filenames, auto-loading of existing user-config.yaml, and quit guard when dirty.
  • Create TUI submodules: filter.py for pure option filtering, widgets.py for FilterablePicker, forms.py for all modal forms (add/edit crate/troop, set setting with bool/enum selects, append array, type chooser, confirm, file browser).
  • Add smoke tests for the TUI app, widgets, forms, and EditModel to verify UI↔model wiring, undo/redo, save/generate behaviour, and inject browser semantics.
tools/ctld-tools/ctld_tools/editmodel.py
tools/ctld-tools/ctld_tools/tui/app.py
tools/ctld-tools/ctld_tools/tui/forms.py
tools/ctld-tools/ctld_tools/tui/widgets.py
tools/ctld-tools/ctld_tools/tui/filter.py
tools/ctld-tools/ctld_tools/tui/__init__.py
tools/ctld-tools/tests/test_app.py
tools/ctld-tools/tests/test_editmodel.py
tools/ctld-tools/tests/test_widgets.py
tools/ctld-tools/tests/test_forms.py
tools/ctld-tools/tests/test_filter.py
Introduce an i18n layer for ctld-tools and wire it through CLI help, validation messages, and the TUI, with EN/FR catalogs and language selection.
  • Add ctld_tools.i18n module implementing locale detection, catalog loading from JSON, t() helper, and language context manager.
  • Create EN and FR JSON locale catalogs for app/TUI strings and validation keys; adapt Finding to store keys+params and derive translated messages via t().
  • Update CLI entrypoint to accept --lang, set language early before Typer help generation, and use translated help strings; ensure TUI app uses t() for all labels/bindings.
  • Add tests for the i18n behaviour and a runtime-no-lupa test to ensure embedded-reference path does not import lupa.
tools/ctld-tools/ctld_tools/i18n.py
tools/ctld-tools/ctld_tools/data/locales/en.json
tools/ctld-tools/ctld_tools/data/locales/fr.json
tools/ctld-tools/tests/test_i18n.py
tools/ctld-tools/tests/test_runtime_no_lupa.py
tools/ctld-tools/ctld_tools/cli.py
tools/ctld-tools/ctld_tools/validate.py
tools/ctld-tools/tests/test_validate.py
tools/ctld-tools/tui/app.py
Extend runtime Lua API with ctld.patchTroopGroup and ensure gen-user maps troop and crate patches correctly, with end-to-end tests via lupa.
  • Add ctld.patchTroopGroup helper in CTLD_userSetup.lua and merged CTLD.lua, implementing shallow+one-level-deep merge of patch tables into loadableGroups by name and logging a warning when not found.
  • Update genuser operations rendering to support troops.patch and to map crates.patch weight_kg to runtime weight field, mirroring add crate behaviour.
  • Enhance genuser tests to cover patchTroopGroup codegen and mapping, plus new end-to-end tests that run generated CTLD_userConfig.lua against real CTLD helpers via lupa to assert crate and troop mutations.
  • Add Lua busted specs for ctld.patchTroopGroup behaviour (field patching, nested table merge, warning on unknown group).
src/CTLD_userSetup.lua
CTLD.lua
tools/ctld-tools/ctld_tools/genuser.py
tools/ctld-tools/tests/test_genuser.py
tests/ci/unit/usersetup_spec.lua
Adjust CLI commands, packaging, and docs to expose the new TUI, default to embedded reference, and make lupa build-time-only while shipping textual in the exe.
  • Modify validate and gen-user CLI commands to use Reference.from_embedded() by default, make --src optional and clearly documented as a dev override, and relax gen-user requirements (no mandatory --src).
  • Add gen-reference and tui commands to the CLI, wiring tui to CtldToolsApp.run and gen-reference to write_reference, and handle --lang both at root and per-command.
  • Update pyproject to move lupa into dev group, add textual and pytest-asyncio, configure pytest asyncio and warnings, and adjust README to document build/runtime separation and commands.
  • Update CI and release workflows to install poetry groups appropriately (keep dev for gen-config, drop build where needed), build ctld-tools.exe including textual, embedded reference and locales but excluding lupa, and add smoke checks to ensure tui is present and works without lupa.
  • Revise mission-maker docs (EN and FR), roadmap, ADR, backlog tickets, CHANGELOG to describe the TUI, embedded reference, settings help, i18n, runtime helper, and lot closure.
tools/ctld-tools/ctld_tools/cli.py
.github/workflows/release.yml
.github/workflows/ci.yml
tools/ctld-tools/pyproject.toml
tools/ctld-tools/README.md
docs/mission-maker/ctld-tools.md
docs/mission-maker/ctld-tools.fr.md
CHANGELOG.md
dev/adr/0009-external-yaml-authoring-ctld-tools.md
dev/roadmap.md
.backlog/CTLD-TOOLS-TUI/PRD.md
.backlog/CTLD-TOOLS-TUI/tickets/01-embedded-reference.md
.backlog/CTLD-TOOLS-TUI/tickets/02-edit-model.md
.backlog/CTLD-TOOLS-TUI/tickets/03-filter-picker.md
.backlog/CTLD-TOOLS-TUI/tickets/04-textual-tui.md
.backlog/CTLD-TOOLS-TUI/tickets/05-cli-and-packaging.md
.backlog/CTLD-TOOLS-TUI/tickets/06-docs-changelog-index.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Set the index line to merged (PR #52) and flag the TUI roadmap item delivered.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tools/ctld-tools/tests/test_app.py" line_range="139" />
<code_context>
+    assert app.model.config["settings"]["numberOfTroops"] == 7
+
+
+async def test_bool_setting_offers_a_select():
+    app = CtldToolsApp()
+    async with app.run_test(size=(120, 40)) as pilot:
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding coverage for enum settings in the TUI (Select-based picker).

Enum settings in the TUI are handled via `Reference.enum_choices` and `SetSettingForm` (select widget with a marked default), but current tests only exercise numeric and boolean settings. Please add a smoke test for an enum-valued setting (e.g. `JTAC_lock`) that verifies: a `Select` is rendered, the default option is marked, and changing the selection updates `app.model.config['settings']` appropriately.

Suggested implementation:

```python
    assert app.model.config["settings"]["numberOfTroops"] == 7


async def test_enum_setting_offers_a_select_with_default_and_updates_config():
    """Smoke test for enum-valued settings in the TUI (e.g. JTAC_lock)."""
    app = CtldToolsApp()
    async with app.run_test(size=(120, 40)) as pilot:
        # Open settings view
        await pilot.click("#settings")
        await pilot.pause()

        # Locate the enum-valued setting (JTAC_lock) in the settings list
        settings_list = app.query_one("#settings-list", OptionList)
        for index, option in enumerate(settings_list.options):
            if str(option.prompt) == "JTAC_lock":
                settings_list.highlighted = index
                break
        else:
            # Fail explicitly if the JTAC_lock setting is not present
            raise AssertionError("JTAC_lock setting not found in settings list")

        # Open the edit form for JTAC_lock
        settings_list.action_select()
        await pilot.pause()

        # Enum settings should be rendered via a Select-backed SetSettingForm
        select = app.query_one("#value", Select)
        assert select is not None

        # Verify a default option is marked
        choices = list(select.choices)
        assert choices, "Enum Select should expose at least one choice"
        assert any(getattr(choice, "default", False) for choice in choices), "One choice should be marked as default"

        # Capture current config value for JTAC_lock
        original_value = app.model.config["settings"]["JTAC_lock"]

        # Change the selection to a different enum value
        # (advance once; if only one choice exists the assertion above will already have failed)
        select.action_next()
        await pilot.pause()

        # Submit the updated setting
        await pilot.click("#submit")
        await pilot.pause()

    # After the form closes, the config should reflect the new enum value
    updated_value = app.model.config["settings"]["JTAC_lock"]
    assert updated_value != original_value



        await pilot.click("#add")  # action button

```

1. Ensure the settings view is opened via `#settings` and the settings list is addressable as `#settings-list`; if your actual IDs differ, adjust the selectors in the test accordingly.
2. The test assumes the JTAC_lock setting is present in the settings list and that its label/string representation is `"JTAC_lock"`. If the prompt text or key differs, update the `str(option.prompt) == "JTAC_lock"` check.
3. The test queries the enum widget as `Select` with id `#value`, consistent with the numeric test using `Input#value`. If your `SetSettingForm` uses a different id or widget type, adapt the `app.query_one("#value", Select)` selector.
4. If `app.model.config["settings"]["JTAC_lock"]` is stored under a different key or structure, adjust the config lookups at the bottom of the test.
</issue_to_address>

### Comment 2
<location path=".backlog/CTLD-TOOLS-TUI/PRD.md" line_range="145" />
<code_context>
+
+## Amendment (2026-07-21, post-first-run review)
+
+After trying the TUI, four enhancements were agreed and folded into the same lot/PR:
+
+1. **i18n (EN + FR)** following the OS language, forced by `--lang` / `CTLD_LANG` — modelled on VMCT
</code_context>
<issue_to_address>
**issue (typo):** The phrase "four enhancements" conflicts with the twelve numbered items that follow.

The introduction says "four enhancements" but the list is numbered 1–12. Please adjust the wording to match the actual count (or use a neutral phrase like "several enhancements"), or restructure the list so the number and text are consistent.

```suggestion
After trying the TUI, several enhancements were agreed and folded into the same lot/PR:
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

assert app.model.config["settings"]["numberOfTroops"] == 7


async def test_bool_setting_offers_a_select():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding coverage for enum settings in the TUI (Select-based picker).

Enum settings in the TUI are handled via Reference.enum_choices and SetSettingForm (select widget with a marked default), but current tests only exercise numeric and boolean settings. Please add a smoke test for an enum-valued setting (e.g. JTAC_lock) that verifies: a Select is rendered, the default option is marked, and changing the selection updates app.model.config['settings'] appropriately.

Suggested implementation:

    assert app.model.config["settings"]["numberOfTroops"] == 7


async def test_enum_setting_offers_a_select_with_default_and_updates_config():
    """Smoke test for enum-valued settings in the TUI (e.g. JTAC_lock)."""
    app = CtldToolsApp()
    async with app.run_test(size=(120, 40)) as pilot:
        # Open settings view
        await pilot.click("#settings")
        await pilot.pause()

        # Locate the enum-valued setting (JTAC_lock) in the settings list
        settings_list = app.query_one("#settings-list", OptionList)
        for index, option in enumerate(settings_list.options):
            if str(option.prompt) == "JTAC_lock":
                settings_list.highlighted = index
                break
        else:
            # Fail explicitly if the JTAC_lock setting is not present
            raise AssertionError("JTAC_lock setting not found in settings list")

        # Open the edit form for JTAC_lock
        settings_list.action_select()
        await pilot.pause()

        # Enum settings should be rendered via a Select-backed SetSettingForm
        select = app.query_one("#value", Select)
        assert select is not None

        # Verify a default option is marked
        choices = list(select.choices)
        assert choices, "Enum Select should expose at least one choice"
        assert any(getattr(choice, "default", False) for choice in choices), "One choice should be marked as default"

        # Capture current config value for JTAC_lock
        original_value = app.model.config["settings"]["JTAC_lock"]

        # Change the selection to a different enum value
        # (advance once; if only one choice exists the assertion above will already have failed)
        select.action_next()
        await pilot.pause()

        # Submit the updated setting
        await pilot.click("#submit")
        await pilot.pause()

    # After the form closes, the config should reflect the new enum value
    updated_value = app.model.config["settings"]["JTAC_lock"]
    assert updated_value != original_value



        await pilot.click("#add")  # action button
  1. Ensure the settings view is opened via #settings and the settings list is addressable as #settings-list; if your actual IDs differ, adjust the selectors in the test accordingly.
  2. The test assumes the JTAC_lock setting is present in the settings list and that its label/string representation is "JTAC_lock". If the prompt text or key differs, update the str(option.prompt) == "JTAC_lock" check.
  3. The test queries the enum widget as Select with id #value, consistent with the numeric test using Input#value. If your SetSettingForm uses a different id or widget type, adapt the app.query_one("#value", Select) selector.
  4. If app.model.config["settings"]["JTAC_lock"] is stored under a different key or structure, adjust the config lookups at the bottom of the test.


## Amendment (2026-07-21, post-first-run review)

After trying the TUI, four enhancements were agreed and folded into the same lot/PR:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (typo): The phrase "four enhancements" conflicts with the twelve numbered items that follow.

The introduction says "four enhancements" but the list is numbered 1–12. Please adjust the wording to match the actual count (or use a neutral phrase like "several enhancements"), or restructure the list so the number and text are consistent.

Suggested change
After trying the TUI, four enhancements were agreed and folded into the same lot/PR:
After trying the TUI, several enhancements were agreed and folded into the same lot/PR:

@davidp57
davidp57 merged commit ee63095 into develop Jul 21, 2026
7 checks passed
@davidp57
davidp57 deleted the feature/ctld-tools-tui branch July 21, 2026 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant