diff --git a/CLAUDE.md b/DESIGN.md similarity index 81% rename from CLAUDE.md rename to DESIGN.md index 1f8e88b..7ccd563 100644 --- a/CLAUDE.md +++ b/DESIGN.md @@ -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` 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` 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 ` 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 ` describes a single command, including each `Param`'s name. The argument has `Completion::Command`, so `help ` 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 ''" 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) + `""` (bold) + `" — "` (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 ` — 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_.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`). diff --git a/src/tui/commands.cpp b/src/tui/commands.cpp index 856b298..77a12f6 100644 --- a/src/tui/commands.cpp +++ b/src/tui/commands.cpp @@ -25,15 +25,23 @@ void Tui::RegisterCommands() { {{"command name (optional)", Completion::Command}}, [this](const std::vector &args) { if (args.empty()) { - Print("Commands — type `help ` 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 ` 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 &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 (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 / through connections", + /*prompt_for_missing=*/ false, + "show all signals reachable from / 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"] = { diff --git a/src/tui/screen_connect.cpp b/src/tui/screen_connect.cpp index 6a30218..7b61e3e 100644 --- a/src/tui/screen_connect.cpp +++ b/src/tui/screen_connect.cpp @@ -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; }); diff --git a/src/tui/screen_explore.cpp b/src/tui/screen_explore.cpp index 759a9ad..c89cd6c 100644 --- a/src/tui/screen_explore.cpp +++ b/src/tui/screen_explore.cpp @@ -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, diff --git a/src/tui/screen_main.cpp b/src/tui/screen_main.cpp index 2007954..9de2b67 100644 --- a/src/tui/screen_main.cpp +++ b/src/tui/screen_main.cpp @@ -1,5 +1,9 @@ #include "tui/tui.hpp" +#include "system/connect.hpp" +#include "system/modules.hpp" +#include "system/system.hpp" + #include #include #include @@ -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}), diff --git a/src/tui/screen_net.cpp b/src/tui/screen_net.cpp new file mode 100644 index 0000000..4000164 --- /dev/null +++ b/src/tui/screen_net.cpp @@ -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 +#include +#include + +#include +#include + +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; + }); +} diff --git a/src/tui/screen_search.cpp b/src/tui/screen_search.cpp index 216f60c..86e7793 100644 --- a/src/tui/screen_search.cpp +++ b/src/tui/screen_search.cpp @@ -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; diff --git a/src/tui/screen_settype.cpp b/src/tui/screen_settype.cpp index 785612e..5bbada4 100644 --- a/src/tui/screen_settype.cpp +++ b/src/tui/screen_settype.cpp @@ -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, diff --git a/src/tui/tui.cpp b/src/tui/tui.cpp index b6f5a90..9255a28 100644 --- a/src/tui/tui.cpp +++ b/src/tui/tui.cpp @@ -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; } diff --git a/src/tui/tui.hpp b/src/tui/tui.hpp index 43bfaae..cb79ee8 100644 --- a/src/tui/tui.hpp +++ b/src/tui/tui.hpp @@ -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 net_modules; + int net_module_idx; + std::string net_sig_filter; + std::vector net_sigs; ///< rebuilt every frame from filter + int net_sig_idx; + int net_focus_idx; + // ---- Set-type screen state ---- std::vector 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_ diff --git a/src/tui/tui_helpers.hpp b/src/tui/tui_helpers.hpp index 78f525c..72cacd8 100644 --- a/src/tui/tui_helpers.hpp +++ b/src/tui/tui_helpers.hpp @@ -1,11 +1,19 @@ #ifndef _TUI_HELPERS_HPP_ #define _TUI_HELPERS_HPP_ +#include + #include #include // 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,