feat(ctld-tools): interactive TUI + embedded reference (CTLD-TOOLS-TUI)#52
Conversation
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.
Reviewer's GuideAdds 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
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Set the index line to merged (PR #52) and flag the TUI roadmap item delivered.
There was a problem hiding this comment.
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>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(): |
There was a problem hiding this comment.
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- Ensure the settings view is opened via
#settingsand the settings list is addressable as#settings-list; if your actual IDs differ, adjust the selectors in the test accordingly. - 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 thestr(option.prompt) == "JTAC_lock"check. - The test queries the enum widget as
Selectwith id#value, consistent with the numeric test usingInput#value. If yourSetSettingFormuses a different id or widget type, adapt theapp.query_one("#value", Select)selector. - 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: |
There was a problem hiding this comment.
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.
| 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: |
Lot CTLD-TOOLS-TUI
Interactive textual TUI (
ctld-tools tui) as the Mission Maker's single entry point, plus anembedded reference so the tool runs with only the
.exe(no CTLDsrc/).Highlights
ctld_tools/data/reference.json, generated bygen-reference, golden-tested): crates (AA injected), troop groups, arrays, scalar settings + defaults, and a settings schema.from_embedded()is the default;--srcis a dev override. lupa is build-time only now — the.exeno longer bundles it.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.e) and delete a tree entry (with confirmation); undo/redo; unsaved-changes guard on quit; Save/Generate use fixed canonical names; Inject via a.mizfile browser; auto-loadsuser-config.yaml.src/CTLD_config_schema.yaml); unknown settings flagged (warning).--lang/CTLD_LANG; covers the TUI and validation findings.Runtime (src/)
ctld.patchTroopGrouphelper (mirrorspatchCrate) → patch a troop group by name.gen-usermaps a patch'sweight_kgto the runtimeweightkey.Packaging / docs
release.ymlbuilds the.exewith textual + embedded bundle + locales, excluding lupa (verified locally: builds, liststui, TUI starts, lupa excluded).mission-maker/ctld-tools.md(EN + FR), ADR 0009 note, CHANGELOG updated.Tests
patchTroopGrouphas a busted spec and is also exercised end-to-end bygen-uservia lupa. Lua lint/busted run in CI.Closes the
CTLD-TOOLS-TUIlot (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:
ctld-tools tui) as the primary Mission Maker interface for authoring and validating CTLD user-configs.src/folder.ctld.patchTroopGroupruntime helper and corresponding YAML operations.Enhancements:
Build:
CI:
ctld.patchTroopGroupand textual-based tests with asyncio warnings filtered.Deployment:
Documentation:
Tests: