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:
@@ -53,6 +53,7 @@ src/
|
||||
screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper
|
||||
screen_settype.cpp BuildSettypeScreen
|
||||
screen_explore.cpp BuildExploreScreen (browse modules → parts/signals/connections → details; not scriptable)
|
||||
screen_net.cpp BuildNetScreen (BFS over connections from a starting (module, signal))
|
||||
doc/classes.puml -- PlantUML class diagram
|
||||
```
|
||||
|
||||
@@ -74,7 +75,7 @@ doc/classes.puml -- PlantUML class diagram
|
||||
- `CatchEvent` is wrapped **outside** the `Renderer` (not between `Renderer` and `Input`). Pattern: `Renderer(input_component, lambda)` then `CatchEvent(renderer, handler)`. Wrapping `CatchEvent(input, …)` inside a `Container::Vertical({…})` and then `Renderer(container, …)` was found to break the `Input`'s content rendering on at least one terminal — typed characters didn't show even though `on_enter` fired correctly.
|
||||
- `InputOption::transform` is overridden to avoid hard-coded colors (default sets `White on Black` when focused, which is unreadable on light terminal themes). The custom transform only applies `dim` to the placeholder; everything else inherits terminal default fg/bg, so the UI adapts to any theme.
|
||||
- `InputOption::multiline` **must** be set to `false` for the command prompt. Default is `true`, and FTXUI's `HandleReturn()` then both inserts `\n` into the content **and** fires `on_enter` — so `Submit()` would receive `"help\n"` instead of `"help"` and command lookups would all fall through to "unknown command".
|
||||
- Commands live in a `std::map<std::string, CommandSpec>` registry built in `RegisterCommands()`. Each `CommandSpec` carries `params` (list of `Param{name, completion}` where `completion` is the `Tui::Completion` enum: `None | Path | Command`), an `action(args)` lambda, a `prompt_for_missing` bool (default `true`), a one-line `description` string, and a `scriptable` bool (default `true`). Setting `scriptable = false` (e.g. `explore`) excludes the command from the script-save buffer; if a non-scriptable command is invoked from a sourced file the `Source` loop still aborts via the `screen_idx != 0` check, since these commands are typically screen-openers. `Dispatch(raw)` tokenises the input (whitespace split with `"…"` quoting), takes inline args first, and (only when `prompt_for_missing`) pushes a `Prompt` onto the queue for each missing param. The last prompt's callback calls `Finalize()`. Set `prompt_for_missing = false` for commands that accept either zero args or a full arg list (e.g. `search`: bare → interactive, full → inline) — the action gets called immediately with whatever it received and decides what to do.
|
||||
- Commands live in a `std::map<std::string, CommandSpec>` registry built in `RegisterCommands()`. Each `CommandSpec` carries `params` (list of `Param{name, completion}` where `completion` is the `Tui::Completion` enum: `None | Path | Command`), an `action(args)` lambda, a `prompt_for_missing` bool (default `true`), a one-line `description` string, a `scriptable` bool (default `true`), and an `interactive` bool (default `false`) that flags screen-opening commands. Setting `scriptable = false` (e.g. `explore`) excludes the command from the script-save buffer; if a non-scriptable command is invoked from a sourced file the `Source` loop still aborts via the `screen_idx != 0` check, since these commands are typically screen-openers. `interactive = true` is consumed only by `help`: the listing splits commands into two sections (`Interactive (open a full-screen mode)` and `Other`), and `help <name>` adds an `[interactive]` tag plus a note about the bare-vs-inline duality. `Dispatch(raw)` tokenises the input (whitespace split with `"…"` quoting), takes inline args first, and (only when `prompt_for_missing`) pushes a `Prompt` onto the queue for each missing param. The last prompt's callback calls `Finalize()`. Set `prompt_for_missing = false` for commands that accept either zero args or a full arg list (e.g. `search`: bare → interactive, full → inline) — the action gets called immediately with whatever it received and decides what to do.
|
||||
- `Tab` completes by `Param::completion`: `Path` triggers `CompletePath()` (filesystem listing with `~/` expansion), `Command` triggers `CompleteCommand()` (matches against the registry), `None` does nothing. Both work inline (`CompleteInline()` figures out the arg position) and inside a multi-step prompt.
|
||||
- `help` (no args) iterates the registry and prints `name — description` for every command. `help <command>` describes a single command, including each `Param`'s name. The argument has `Completion::Command`, so `help <Tab>` lists registered commands.
|
||||
- `Finalize()` rebuilds the **canonical inline form** (`name arg1 "arg with spaces" arg3`) and writes that to history — so a `load` command answered via interactive prompts still shows up in `history` (and on disk) as a single inline line, ready to be replayed via ↑.
|
||||
@@ -117,7 +118,13 @@ Pending prompts (from incomplete inline commands) are NOT considered interactive
|
||||
|
||||
**Subset wiring + NC backfill**: `CheckIdentityCompatible(a, b, info=&s)` accepts the case where one side's canonical pin set is a subset of the other's — typical when one importer drops NC pins (Altium) and the other doesn't (Mentor). It populates `info` with a non-fatal "N pin(s) only on '<part>'" message. Bidirectional mismatch (both sides have orphans) is still refused. After acceptance, `connect` calls `FillIdentityNCs(p1, p2)` which materialises the orphan canonical positions on the missing side as NC pins (`new Pin(other_side_name)`) — so `Connection::pin_map.size()` matches the larger side's count. Idempotent.
|
||||
|
||||
`screen_idx` mapping: 0 = main TUI, 1 = search, 2 = connect, 3 = set-type, 4 = explore. Adding a new screen mode is the same recipe each time: state members, `Container::Vertical` of focusable components, a `Renderer` lambda that recomputes derived state per frame (e.g. filtered part lists), an entry in `Container::Tab`, and Tab/Esc handling in the outer `CatchEvent`.
|
||||
`screen_idx` mapping: 0 = main TUI, 1 = search, 2 = connect, 3 = set-type, 4 = explore, 5 = net. Adding a new screen mode is the same recipe each time: state members, `Container::Vertical` of focusable components, a `Renderer` lambda that recomputes derived state per frame (e.g. filtered part lists), an entry in `Container::Tab`, and Tab/Esc handling in the outer `CatchEvent`.
|
||||
|
||||
**Screen titles** (shared idiom): every interactive screen renders a top bar in the form `" essim "` (bold) + `"→ "` (dim) + `"<screen-name>"` (bold) + `" — <short description>"` (dim), followed by a `separator()`. The main screen has its own variant that adds a live `N module(s), M connection(s)` counter on the right. Aim is to make the breadcrumb between essim and the current mode visible at all times.
|
||||
|
||||
**Focus highlighting**: each interactive screen reuses `FocusLabel(elem, focused)` from `tui_helpers.hpp` (inline helper, `focused ? e | inverted : e`) on the label of the currently-focused field so the user sees at a glance where the next keystroke lands. Indices match the `Container::Vertical` order — e.g. `connect` has 7 (m1, p1-filter, p1-menu, m2, p2-filter, p2-menu, Button), `net` has 3 (filter, module, signal). Buttons (`Connect`, `Apply`) get the highlight on the button itself, not a separate label.
|
||||
|
||||
**Main-screen title bar**: top of `BuildMainScreen` renders `" essim "` (bold) + `"— system digital twin"` (dim) on the left and a live `"N module(s), M connection(s)"` (or `"no system loaded"`) on the right, then a `separator()`. Built from `sys->modules()->size()` / `sys->connections()->size()` each frame — cheap. `screen_main.cpp` therefore needs to include `system/system.hpp`, `system/modules.hpp`, `system/connect.hpp` directly (forward-declared in `tui.hpp`).
|
||||
|
||||
**Main-screen scrollback**: the visualisation area lets you scroll through past output. State is `Tui::scroll_offset` (0 = follow tail). Keys (default branch of the outer `CatchEvent` in `Run()`): `PageUp` / `PageDown` step 10 lines, `Home` jumps to top, `End` returns to tail. `Print()` resets `scroll_offset = 0` so any new output snaps the view back to the tail (otherwise late errors would be hidden). Render: instead of `focusPositionRelative(0, 1)` always anchoring to the bottom, the screen places `| focus` on `output[size - 1 - scroll_offset]` and shows a `[scroll: -N / PgUp PgDn Home End]` indicator next to the prompt when offset > 0. `vscroll_indicator | yframe` for the FTXUI-side scroll bar.
|
||||
|
||||
@@ -129,12 +136,18 @@ The connect screen rebuilds the filtered part lists inside the `Renderer` lambda
|
||||
|
||||
The Connection record stores `Module*`/`Part*` for both endpoints (added to `Connection` for this — minimal struct fields, no behaviour change).
|
||||
|
||||
`search` switches to a second full-screen layout (handled by `Container::Tab({main, search}, &screen_idx)`). Layout:
|
||||
`search` switches to a second full-screen layout (handled by `Container::Tab({main, search, …}, &screen_idx)`). Layout:
|
||||
- Left column: `Menu` for the module list and `Menu` for the type (`parts` / `signals`).
|
||||
- Right column: `Input` for the live filter query, plus a results panel rebuilt every frame.
|
||||
- `Tab` cycles focus between the query input and the menus. **Implemented manually** in the outer `CatchEvent`: `Menu::OnEvent` consumes `Event::Tab` to cycle its own entries and returns `true`, which prevents `Container::Vertical` from ever seeing the event (Container only cycles between children when the active child returns `false`). So we short-circuit Tab/TabReverse upstream and mutate `search_focus_idx` directly.
|
||||
- `Esc` exits the search mode (flips `screen_idx` back to 0). The search state (selected module/type, query) is preserved across re-entries until `search` is run again.
|
||||
|
||||
`net` is dual-mode (`prompt_for_missing = false`, `interactive = true`):
|
||||
- Inline: `net <module> <signal>` — prints the BFS-reached `(module, signal)` set in the visualisation area (with types and an `[INCONSISTENT]` flag).
|
||||
- Bare: opens `screen_idx = 5`. Three columns: module `Menu` (left), filter `Input` + filtered signal `Menu` of the selected module (middle), and a read-only panel (right) that recomputes the net on every frame and lists `(module, signal, type)` for each member plus a header summarising count + dominant type + inconsistency flag. The signal list is sorted with `NaturalLess`; `net_sig_idx` is clamped if the filter shrinks it. `Tab` cycles 3 fields (filter → module → signal); `Esc` leaves.
|
||||
|
||||
**Long-running scripts (`source`)**: `Source(file)` is event-paced. It reads all lines, sets `loading = true`, then spawns a detached pacing thread that posts `Event::Special("\x02tick")` every ~30 ms — **but only one tick at a time**: the main thread acks each one (`tick_in_flight = false` at the end of `ProcessNextSourceLine`) before the ticker sleeps and posts the next. Without this, FTXUI batches all queued events into one render pass; a long line (e.g. a heavy Mentor parse) would let the ticker queue many ticks and the modal would freeze. Each tick triggers `ProcessNextSourceLine`, which processes one effective line (skipping comments/blanks) via `Submit()`. A centred `borderDouble` modal (`" Computing… "` + filename + `N / M lines`) is overlaid via `dbox` while `loading` is true.
|
||||
|
||||
Adding a new screen mode = drop a `screen_<name>.cpp` defining `Component Tui::BuildXxxScreen()`, add corresponding state members in `tui.hpp`, register a `BuildXxxScreen` declaration in the screen-builders block, then in `tui.cpp::Run()` add a child to `Container::Tab` and a case in the screen-mode `switch` of the outer `CatchEvent` (Tab cycling + Esc-leave). Commands lived in `commands.cpp` set `screen_idx` to enter the new mode.
|
||||
|
||||
Command history is persisted on disk and loaded on startup. Path resolution is platform-aware:
|
||||
@@ -154,3 +167,5 @@ Each successful submission appends a single line to the file (so a crash doesn't
|
||||
## Memory layout for sessions
|
||||
|
||||
Persistent notes live in `~/.claude/projects/-home-francois-Projets-essim/memory/`. Index: `MEMORY.md`. Add project/feedback/user/reference entries there when relevant — see top-level Claude Code memory rules.
|
||||
|
||||
This file is `DESIGN.md` (renamed from the original `CLAUDE.md`).
|
||||
@@ -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"] = {
|
||||
|
||||
@@ -120,26 +120,39 @@ Component Tui::BuildConnectScreen() {
|
||||
connect_p2_filter, connect_p2_list, connect_p2_idx);
|
||||
|
||||
auto col = [&](const std::string &title,
|
||||
Component mm, Component pf, Component pm) {
|
||||
Component mm, Component pf, Component pm,
|
||||
int m_focus, int f_focus, int p_focus) {
|
||||
return vbox({
|
||||
text(title) | bold,
|
||||
text("module") | dim,
|
||||
FocusLabel(text(" module "), connect_focus_idx == m_focus) | dim,
|
||||
mm->Render() | yframe | size(HEIGHT, LESS_THAN, 8),
|
||||
separator(),
|
||||
hbox({text(" filter: "), pf->Render() | flex}) | border,
|
||||
text("part") | dim,
|
||||
hbox({FocusLabel(text(" filter: "), connect_focus_idx == f_focus),
|
||||
pf->Render() | flex}) | border,
|
||||
FocusLabel(text(" part "), connect_focus_idx == p_focus) | dim,
|
||||
pm->Render() | yframe | flex,
|
||||
}) | flex;
|
||||
};
|
||||
|
||||
auto title = hbox({
|
||||
text(" essim ") | bold,
|
||||
text("→ ") | dim,
|
||||
text("connect") | bold,
|
||||
text(" — wire two parts across modules (TransformRegistry-driven)") | dim,
|
||||
});
|
||||
|
||||
return vbox({
|
||||
title,
|
||||
separator(),
|
||||
hbox({
|
||||
col("endpoint 1", m1_menu, p1_filter, p1_menu),
|
||||
col("endpoint 1", m1_menu, p1_filter, p1_menu, 0, 1, 2),
|
||||
separator(),
|
||||
col("endpoint 2", m2_menu, p2_filter, p2_menu),
|
||||
col("endpoint 2", m2_menu, p2_filter, p2_menu, 3, 4, 5),
|
||||
}) | flex,
|
||||
separator(),
|
||||
hbox({filler(), connect_button->Render(), filler()}),
|
||||
hbox({filler(),
|
||||
FocusLabel(connect_button->Render(), connect_focus_idx == 6),
|
||||
filler()}),
|
||||
text(" Tab: cycle focus | Enter on [Connect]: confirm | Esc: leave ") | dim,
|
||||
}) | border;
|
||||
});
|
||||
|
||||
@@ -171,33 +171,49 @@ Component Tui::BuildExploreScreen() {
|
||||
}
|
||||
|
||||
auto col1 = vbox({
|
||||
text("module") | bold,
|
||||
FocusLabel(text(" module "), explore_focus_idx == 0) | bold,
|
||||
module_menu->Render() | yframe | flex,
|
||||
}) | size(WIDTH, EQUAL, 24);
|
||||
|
||||
auto col2 = vbox({
|
||||
text("type") | bold,
|
||||
FocusLabel(text(" type "), explore_focus_idx == 1) | bold,
|
||||
type_menu->Render() | flex,
|
||||
}) | size(WIDTH, EQUAL, 12);
|
||||
|
||||
auto col3 = vbox({
|
||||
text(explore_types[explore_type_idx]) | bold,
|
||||
hbox({text(" filter: "), child_filter->Render() | flex}) | border,
|
||||
FocusLabel(text(" " + explore_types[explore_type_idx] + " "),
|
||||
explore_focus_idx == 3) | bold,
|
||||
hbox({FocusLabel(text(" filter: "), explore_focus_idx == 2),
|
||||
child_filter->Render() | flex}) | border,
|
||||
children_menu->Render() | yframe | flex,
|
||||
}) | size(WIDTH, EQUAL, 36);
|
||||
|
||||
auto col4 = vbox({
|
||||
text(explore_header) | bold,
|
||||
hbox({text(" filter: "), detail_filter->Render() | flex}) | border,
|
||||
FocusLabel(text(" " + explore_header + " "),
|
||||
explore_focus_idx == 5) | bold,
|
||||
hbox({FocusLabel(text(" filter: "), explore_focus_idx == 4),
|
||||
detail_filter->Render() | flex}) | border,
|
||||
detail_menu->Render() | vscroll_indicator | yframe | flex,
|
||||
}) | flex;
|
||||
|
||||
auto title = hbox({
|
||||
text(" essim ") | bold,
|
||||
text("→ ") | dim,
|
||||
text("explore") | bold,
|
||||
text(" — browse modules → parts/signals/connections → details") | dim,
|
||||
});
|
||||
|
||||
return vbox({
|
||||
title,
|
||||
separator(),
|
||||
hbox({col1, separator(), col2, separator(), col3, separator(), col4}) | flex,
|
||||
text(" Tab: cycle focus (incl. detail to scroll) | Esc: leave explore ") | dim,
|
||||
}) | border;
|
||||
} catch (const std::exception &e) {
|
||||
return vbox({
|
||||
hbox({text(" essim ") | bold, text("→ ") | dim,
|
||||
text("explore") | bold}),
|
||||
separator(),
|
||||
text("explore: render error") | bold,
|
||||
text(std::string(" ") + e.what()) | dim,
|
||||
text(" Esc: leave explore ") | dim,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "tui/tui.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
@@ -22,6 +26,18 @@ Component Tui::BuildMainScreen(ScreenInteractive &screen) {
|
||||
return Renderer(input_component, [this, &screen, input_component] {
|
||||
if (quit) screen.Exit();
|
||||
|
||||
std::string subtitle = sys
|
||||
? std::to_string(sys->modules()->size()) + " module(s), "
|
||||
+ std::to_string(sys->connections()->size()) + " connection(s)"
|
||||
: "no system loaded";
|
||||
auto title = hbox({
|
||||
text(" essim ") | bold,
|
||||
text("— system digital twin") | dim,
|
||||
filler(),
|
||||
text(subtitle) | dim,
|
||||
text(" "),
|
||||
}) | bgcolor(Color::Default);
|
||||
|
||||
// Clamp scroll offset to a meaningful range and pick the line to focus.
|
||||
int n = (int)output.size();
|
||||
if (scroll_offset < 0) scroll_offset = 0;
|
||||
@@ -50,6 +66,8 @@ Component Tui::BuildMainScreen(ScreenInteractive &screen) {
|
||||
: "";
|
||||
|
||||
auto base = vbox({
|
||||
title,
|
||||
separator(),
|
||||
view,
|
||||
separator(),
|
||||
hbox({text(label), input_component->Render(), filler(), text(status) | dim}),
|
||||
|
||||
119
src/tui/screen_net.cpp
Normal file
119
src/tui/screen_net.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Component Tui::BuildNetScreen() {
|
||||
InputOption filter_opt;
|
||||
filter_opt.multiline = false;
|
||||
filter_opt.transform = [](InputState s) {
|
||||
auto el = s.element;
|
||||
if (s.is_placeholder) el |= dim;
|
||||
return el;
|
||||
};
|
||||
auto filter_input = Input(&net_sig_filter, "filter signals…", filter_opt);
|
||||
auto module_menu = Menu(&net_modules, &net_module_idx);
|
||||
auto signal_menu = Menu(&net_sigs, &net_sig_idx);
|
||||
|
||||
auto components = Container::Vertical(
|
||||
{filter_input, module_menu, signal_menu}, &net_focus_idx);
|
||||
|
||||
return Renderer(components,
|
||||
[this, filter_input, module_menu, signal_menu] {
|
||||
// Rebuild filtered signal list for current module + filter.
|
||||
net_sigs.clear();
|
||||
Module *cur_mod = nullptr;
|
||||
if (sys && !net_modules.empty()) {
|
||||
try { cur_mod = sys->modules()->get(net_modules[net_module_idx]); }
|
||||
catch (const std::exception &) {}
|
||||
}
|
||||
if (cur_mod) {
|
||||
std::string needle = ToLower(net_sig_filter);
|
||||
for (auto &skv : *cur_mod->signals) {
|
||||
if (needle.empty()
|
||||
|| ToLower(skv.first).find(needle) != std::string::npos)
|
||||
net_sigs.push_back(skv.first);
|
||||
}
|
||||
std::sort(net_sigs.begin(), net_sigs.end(), NaturalLess);
|
||||
}
|
||||
if (net_sig_idx >= (int)net_sigs.size())
|
||||
net_sig_idx = std::max(0, (int)net_sigs.size() - 1);
|
||||
|
||||
// Compute the net for the current selection.
|
||||
Net net;
|
||||
SignalType dom = SignalType::Other;
|
||||
bool consistent = true;
|
||||
if (cur_mod && !net_sigs.empty()) {
|
||||
try {
|
||||
Signal *sig = cur_mod->signals->get(net_sigs[net_sig_idx]);
|
||||
net = find_net(sys.get(), cur_mod, sig);
|
||||
consistent = net_type_consistent(net, dom);
|
||||
} catch (const std::exception &) {}
|
||||
}
|
||||
|
||||
Elements member_lines;
|
||||
for (const auto &mp : net.members)
|
||||
member_lines.push_back(text(
|
||||
" " + mp.first->name + "/" + mp.second->name
|
||||
+ " (" + signal_type_name(mp.second->type) + ")"));
|
||||
|
||||
std::string header;
|
||||
if (cur_mod && !net_sigs.empty()) {
|
||||
header = cur_mod->name + "/" + net_sigs[net_sig_idx]
|
||||
+ " — " + std::to_string(net.members.size()) + " signal(s)"
|
||||
+ (consistent ? "" : " [INCONSISTENT]")
|
||||
+ " / dominant: " + signal_type_name(dom);
|
||||
} else {
|
||||
header = "(select a module + signal)";
|
||||
}
|
||||
|
||||
auto left = vbox({
|
||||
FocusLabel(text(" module "), net_focus_idx == 1) | bold,
|
||||
module_menu->Render() | yframe | flex,
|
||||
}) | size(WIDTH, EQUAL, 24);
|
||||
|
||||
auto filter_row = hbox({
|
||||
FocusLabel(text(" filter: "), net_focus_idx == 0),
|
||||
filter_input->Render() | flex,
|
||||
}) | border;
|
||||
|
||||
auto middle = vbox({
|
||||
filter_row,
|
||||
FocusLabel(text(" signal "), net_focus_idx == 2) | bold,
|
||||
text(std::to_string(net_sigs.size()) + " signal(s)") | dim,
|
||||
signal_menu->Render() | yframe | flex,
|
||||
}) | size(WIDTH, EQUAL, 32);
|
||||
|
||||
auto right = vbox({
|
||||
text(header) | bold,
|
||||
separator(),
|
||||
vbox(std::move(member_lines)) | yframe | flex,
|
||||
}) | flex;
|
||||
|
||||
auto title = hbox({
|
||||
text(" essim ") | bold,
|
||||
text("→ ") | dim,
|
||||
text("net") | bold,
|
||||
text(" — BFS of (module, signal) bridged through connections") | dim,
|
||||
});
|
||||
|
||||
return vbox({
|
||||
title,
|
||||
separator(),
|
||||
hbox({left, separator(), middle, separator(), right}) | flex,
|
||||
text(" Tab: cycle focus (filter ↔ module ↔ signal) | Esc: leave net ") | dim,
|
||||
}) | border;
|
||||
});
|
||||
}
|
||||
@@ -62,20 +62,30 @@ Component Tui::BuildSearchScreen() {
|
||||
text(" " + h.first + " (" + std::to_string(h.second) + " pins)"));
|
||||
|
||||
auto left = vbox({
|
||||
text("module") | bold,
|
||||
FocusLabel(text(" module "), search_focus_idx == 1) | bold,
|
||||
module_menu->Render() | yframe | flex,
|
||||
separator(),
|
||||
text("type") | bold,
|
||||
FocusLabel(text(" type "), search_focus_idx == 2) | bold,
|
||||
type_menu->Render(),
|
||||
}) | size(WIDTH, EQUAL, 28);
|
||||
|
||||
auto right = vbox({
|
||||
hbox({text(" search: "), query_input->Render() | flex}) | border,
|
||||
hbox({FocusLabel(text(" search: "), search_focus_idx == 0),
|
||||
query_input->Render() | flex}) | border,
|
||||
text(std::to_string(hits.size()) + " match(es)") | dim,
|
||||
vbox(std::move(result_lines)) | yframe | flex,
|
||||
}) | flex;
|
||||
|
||||
auto title = hbox({
|
||||
text(" essim ") | bold,
|
||||
text("→ ") | dim,
|
||||
text("search") | bold,
|
||||
text(" — filter parts and signals by pattern") | dim,
|
||||
});
|
||||
|
||||
return vbox({
|
||||
title,
|
||||
separator(),
|
||||
hbox({left, separator(), right}) | flex,
|
||||
text(" Tab: cycle focus | Esc: leave search ") | dim,
|
||||
}) | border;
|
||||
|
||||
@@ -98,13 +98,14 @@ Component Tui::BuildSettypeScreen() {
|
||||
else for (const auto &k : known_types) known_line += " " + k;
|
||||
|
||||
auto left = vbox({
|
||||
text("module") | bold,
|
||||
FocusLabel(text(" module "), settype_focus_idx == 0) | bold,
|
||||
module_menu->Render() | yframe | flex,
|
||||
}) | size(WIDTH, EQUAL, 28);
|
||||
|
||||
auto middle = vbox({
|
||||
hbox({text(" filter: "), part_filter->Render() | flex}) | border,
|
||||
text("part") | dim,
|
||||
hbox({FocusLabel(text(" filter: "), settype_focus_idx == 1),
|
||||
part_filter->Render() | flex}) | border,
|
||||
FocusLabel(text(" part "), settype_focus_idx == 2) | dim,
|
||||
part_menu->Render() | yframe | flex,
|
||||
}) | flex;
|
||||
|
||||
@@ -112,18 +113,29 @@ Component Tui::BuildSettypeScreen() {
|
||||
text("current type: ") | bold,
|
||||
text(" " + current),
|
||||
separator(),
|
||||
text("new type:") | bold,
|
||||
FocusLabel(text(" new type: "), settype_focus_idx == 3) | bold,
|
||||
hbox({text(" "), type_input->Render() | flex}) | border,
|
||||
text(known_line) | dim,
|
||||
filler(),
|
||||
hbox({filler(), button->Render(), filler()}),
|
||||
hbox({filler(),
|
||||
FocusLabel(button->Render(), settype_focus_idx == 4),
|
||||
filler()}),
|
||||
}) | size(WIDTH, EQUAL, 40);
|
||||
|
||||
Element status = settype_status.empty()
|
||||
? text("") | dim
|
||||
: text(" " + settype_status) | bold;
|
||||
|
||||
auto title = hbox({
|
||||
text(" essim ") | bold,
|
||||
text("→ ") | dim,
|
||||
text("set-type") | bold,
|
||||
text(" — tag a part with its connector kind (drives transforms + pin roles)") | dim,
|
||||
});
|
||||
|
||||
return vbox({
|
||||
title,
|
||||
separator(),
|
||||
hbox({left, separator(), middle, separator(), right}) | flex,
|
||||
separator(),
|
||||
status,
|
||||
|
||||
@@ -23,6 +23,7 @@ Tui::Tui()
|
||||
explore_types{"parts", "signals", "connections"},
|
||||
explore_type_idx(0), explore_child_idx(0),
|
||||
explore_detail_idx(0), explore_focus_idx(0),
|
||||
net_module_idx(0), net_sig_idx(0), net_focus_idx(0),
|
||||
settype_m_idx(0), settype_p_idx(0), settype_focus_idx(0)
|
||||
{
|
||||
LoadHistory();
|
||||
@@ -41,13 +42,21 @@ void Tui::Run() {
|
||||
auto connect_screen = BuildConnectScreen();
|
||||
auto settype_screen = BuildSettypeScreen();
|
||||
auto explore_screen = BuildExploreScreen();
|
||||
auto net_screen = BuildNetScreen();
|
||||
|
||||
auto tab = Container::Tab(
|
||||
{main_screen, search_screen, connect_screen, settype_screen, explore_screen},
|
||||
{main_screen, search_screen, connect_screen, settype_screen, explore_screen,
|
||||
net_screen},
|
||||
&screen_idx);
|
||||
|
||||
auto root = CatchEvent(tab, [this](Event e) {
|
||||
switch (screen_idx) {
|
||||
case 5: // net
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
if (e == Event::Tab) { net_focus_idx = (net_focus_idx + 1) % 3; return true; }
|
||||
if (e == Event::TabReverse) { net_focus_idx = (net_focus_idx + 2) % 3; return true; }
|
||||
return false;
|
||||
|
||||
case 4: // explore
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
if (e == Event::Tab) { explore_focus_idx = (explore_focus_idx + 1) % 6; return true; }
|
||||
|
||||
@@ -33,6 +33,7 @@ class Tui {
|
||||
bool prompt_for_missing = true;
|
||||
std::string description;
|
||||
bool scriptable = true;
|
||||
bool interactive = false; ///< opens a full-screen mode when called bare
|
||||
};
|
||||
|
||||
// ---- Shell state ----
|
||||
@@ -99,6 +100,14 @@ class Tui {
|
||||
bool loading_prev_in_source;
|
||||
ftxui::ScreenInteractive *screen_ptr; ///< set in Run() so Source() can post events.
|
||||
|
||||
// ---- Net screen state ----
|
||||
std::vector<std::string> net_modules;
|
||||
int net_module_idx;
|
||||
std::string net_sig_filter;
|
||||
std::vector<std::string> net_sigs; ///< rebuilt every frame from filter
|
||||
int net_sig_idx;
|
||||
int net_focus_idx;
|
||||
|
||||
// ---- Set-type screen state ----
|
||||
std::vector<std::string> settype_modules;
|
||||
int settype_m_idx;
|
||||
@@ -152,6 +161,7 @@ private:
|
||||
ftxui::Component BuildConnectScreen();
|
||||
ftxui::Component BuildSettypeScreen();
|
||||
ftxui::Component BuildExploreScreen();
|
||||
ftxui::Component BuildNetScreen();
|
||||
};
|
||||
|
||||
#endif // _TUI_HPP_
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
#ifndef _TUI_HELPERS_HPP_
|
||||
#define _TUI_HELPERS_HPP_
|
||||
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Free helpers shared across the TUI translation units.
|
||||
|
||||
// Highlight the label of a focused field in interactive screens. Used so the
|
||||
// user can see at a glance which field will receive their next keystroke.
|
||||
inline ftxui::Element FocusLabel(ftxui::Element e, bool focused) {
|
||||
return focused ? (e | ftxui::inverted) : e;
|
||||
}
|
||||
|
||||
std::string ToLower(std::string s);
|
||||
|
||||
// Case-insensitive natural-order comparison: digit runs compared as integers,
|
||||
|
||||
Reference in New Issue
Block a user