Interactive net screen, main + per-screen title bars, focus highlight, help split.

- New `net` full-screen layout (`screen_net.cpp`, `screen_idx = 5`): three
  columns (module menu / signal filter + menu / live BFS result). Bare
  `net` opens the screen; `net <m> <s>` keeps the inline path.
- Main screen grows a title bar: " essim — system digital twin "
  (bold + dim) on the left, live "N module(s), M connection(s)" on
  the right.
- Every interactive screen now renders the same breadcrumb at the top:
  " essim → <name>  — <short description> ", followed by a separator.
- `tui_helpers.hpp` exports `FocusLabel(elem, focused)`. Every
  interactive screen wraps its field labels with it so the active
  field's label flips to inverted video. Buttons (Connect, Apply)
  invert as a whole.
- `CommandSpec` gains a `bool interactive`. `help` (no args) splits
  the listing into "Interactive (open a full-screen mode)" and
  "Other". `help <name>` tags interactive entries with [interactive]
  and explains the bare-vs-inline duality.
- `DESIGN.md` (renamed from `CLAUDE.md`): refreshed Layout, TUI, and
  screen-recipe sections to cover the new field, the title idiom,
  FocusLabel, the `net` screen, and the event-paced `Computing…`
  modal during `source`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 07:52:22 +02:00
parent c3bb00cb4d
commit fe2dc13c89
11 changed files with 309 additions and 38 deletions

View File

@@ -25,15 +25,23 @@ void Tui::RegisterCommands() {
{{"command name (optional)", Completion::Command}},
[this](const std::vector<std::string> &args) {
if (args.empty()) {
Print("Commands — type `help <name>` for details.");
size_t maxw = 0;
for (const auto &kv : commands) maxw = std::max(maxw, kv.first.size());
for (const auto &kv : commands) {
Print(" " + kv.first
+ std::string(maxw - kv.first.size() + 2, ' ')
+ kv.second.description);
}
Print("Keys: Esc cancels a multi-step prompt; Tab completes commands or paths;");
auto print_group = [&](const std::string &title, bool want_interactive) {
bool printed_any = false;
for (const auto &kv : commands) {
if (kv.second.interactive != want_interactive) continue;
if (!printed_any) { Print(title); printed_any = true; }
Print(" " + kv.first
+ std::string(maxw - kv.first.size() + 2, ' ')
+ kv.second.description);
}
};
Print("Commands — type `help <name>` for details.");
print_group("Interactive (open a full-screen mode):", true);
print_group("Other:", false);
Print("Keys: Esc cancels a multi-step prompt or leaves an interactive screen;");
Print(" Tab completes commands/paths or cycles focus in interactive screens;");
Print(" PageUp/PageDown scroll output (10 lines), Home/End jump to top/bottom.");
return;
}
@@ -41,7 +49,8 @@ void Tui::RegisterCommands() {
auto it = commands.find(name);
if (it == commands.end()) { Print("unknown command: " + name); return; }
const auto &spec = it->second;
Print(name + " " + spec.description);
std::string tag = spec.interactive ? " [interactive]" : "";
Print(name + tag + "" + spec.description);
if (spec.params.empty()) {
Print(" no arguments.");
} else {
@@ -49,8 +58,11 @@ void Tui::RegisterCommands() {
Print(" arg " + std::to_string(i + 1) + ": " + spec.params[i].name);
}
}
if (!spec.prompt_for_missing) {
Print(" run with no args for the interactive form.");
if (spec.interactive) {
Print(" run with no args to open the interactive screen,");
Print(" or with all args for inline (scriptable) execution.");
} else if (!spec.prompt_for_missing) {
Print(" no per-arg prompt — provide all args inline (or use the bare form).");
} else if (!spec.params.empty()) {
Print(" missing args trigger a prompt for each one.");
}
@@ -245,6 +257,25 @@ void Tui::RegisterCommands() {
{"signal name", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
if (args.empty()) {
net_modules.clear();
for (auto &m : *sys->modules()) net_modules.push_back(m.first);
std::sort(net_modules.begin(), net_modules.end(), NaturalLess);
if (net_modules.empty()) { Print("no modules loaded."); return; }
net_module_idx = 0;
net_sig_filter.clear();
net_sig_idx = 0;
net_focus_idx = 0;
screen_idx = 5;
return;
}
if (args.size() != 2) {
Print("usage: net <module> <signal> (or no args for interactive)");
return;
}
Module *mod;
try { mod = sys->modules()->get(args[0]); }
catch (const std::exception &) {
@@ -267,8 +298,11 @@ void Tui::RegisterCommands() {
+ " (" + signal_type_name(mp.second->type) + ")");
}
},
/*prompt_for_missing=*/ true,
"show all signals reachable from <module>/<signal> through connections",
/*prompt_for_missing=*/ false,
"show all signals reachable from <module>/<signal> through connections "
"(interactive screen if no args)",
/*scriptable=*/ true,
/*interactive=*/ true,
};
commands["set-signal-type"] = {
@@ -364,6 +398,8 @@ void Tui::RegisterCommands() {
},
/*prompt_for_missing=*/ false,
"tag a part's connector type for transform lookup",
/*scriptable=*/ true,
/*interactive=*/ true,
};
commands["connect"] = {
@@ -498,6 +534,8 @@ void Tui::RegisterCommands() {
},
/*prompt_for_missing=*/ false,
"connect a part across two modules (interactive screen if no args)",
/*scriptable=*/ true,
/*interactive=*/ true,
};
commands["explore"] = { {}, [this](auto &) {
@@ -515,7 +553,8 @@ void Tui::RegisterCommands() {
explore_focus_idx = 0;
screen_idx = 4;
}, true, "browse modules → parts/signals/connections → details (interactive)",
/*scriptable=*/ false };
/*scriptable=*/ false,
/*interactive=*/ true };
commands["search"] = {
{{"module", Completion::None},
@@ -575,6 +614,8 @@ void Tui::RegisterCommands() {
},
/*prompt_for_missing=*/ false,
"list parts/signals matching a pattern (interactive screen if no args)",
/*scriptable=*/ true,
/*interactive=*/ true,
};
commands["duplicate"] = {