From 300e871aedd1c589ded343b0e63fe68176aaf216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Fri, 15 May 2026 10:31:09 +0200 Subject: [PATCH] =?UTF-8?q?Help=20screen,=20explore=E2=86=92set-connector-?= =?UTF-8?q?type=20Enter,=20settype=20UI=20polish.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New `screen_help.cpp` (`screen_idx = 6`). Left column: menu of 13 topics (Overview, Dashboard, Console, Palette, Explore, Connect/plug, set-connector-type, Signal types, NC pins, Analyze, Scripting, Save/restore, Quitting). Centre column: paragraphs of the focused topic, word-wrapped via `paragraph()` and scrollable. Right column: standard help panel. - `help` bare → opens the screen; `help ` keeps the existing textual command-help behaviour for scripts. - Dashboard `[h]` shortcut opens the screen, and the dashboard help panel (both the loaded and the no-system branch) lists it. - Console: title gets the standard breadcrumb (`essim → console — type commands, read textual output`). Module/connection counters moved off (they live on the dashboard now). - Explore Enter on a part jumps to `set-connector-type` with the exact-match index pre-computed in the filtered list (avoids the substring-match collision where `J20` would land on the wrong row when J200/J21 also matched). - set-connector-type screen: bind `focused_entry` to `selected` on both menus so the cursor `>` tracks the selected row when state is pre-seeded from outside. Right column drops its strict `size(WIDTH, EQUAL, 40)` in favour of `flex`, and the `new type` input uses `xflex` so it actually stretches across the column. - Esc on `set-connector-type` honours `screen_back_idx` — when entered via Enter on a part in `explore`, Esc returns to explore; otherwise it returns to the dashboard like every other screen. Standalone command entries explicitly reset the back-link. - Net-member rows in the explore detail pane carry a `module\tsignal` payload so Enter opens the popup scoped to the peer module rather than mis-firing on the locally selected one. Same scheme for local-pin rows. Co-Authored-By: Claude Opus 4.7 --- DESIGN.md | 11 +- doc/user/analysis.md | 12 +- src/tui/commands.cpp | 26 ++-- src/tui/screen_dashboard.cpp | 3 +- src/tui/screen_explore.cpp | 94 ++++++++++---- src/tui/screen_help.cpp | 236 +++++++++++++++++++++++++++++++++++ src/tui/screen_main.cpp | 20 ++- src/tui/screen_settype.cpp | 25 +++- src/tui/tui.cpp | 24 +++- src/tui/tui.hpp | 10 ++ 10 files changed, 395 insertions(+), 66 deletions(-) create mode 100644 src/tui/screen_help.cpp diff --git a/DESIGN.md b/DESIGN.md index 8c74273..da18171 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -182,7 +182,16 @@ The Connection record stores `Module*`/`Part*` for both endpoints (added to `Con **Signal-type popup (shared)**: Enter on a signal entry in the `explore` screen opens a modal (`Tui::sigtype_dialog_open`) that lets the user pick `power | gnd | other` for the currently-selected signal. Built in `screen_sigtype_modal.cpp::BuildSignalTypeModal()`, attached to both screens via the `Modal(...)` decorator in `Run()`. Inside the modal, Enter applies + closes + records `set-signal-type ` in the script-save buffer (`recorded`); Esc closes without applying. Two safeguards: (a) re-selecting the type the pin already has is a no-op and records nothing; (b) the recorder collapses consecutive edits of the same `(module, signal)` — if the previous line in `recorded` already targets the same pair, it is replaced rather than appended. The outer `CatchEvent` in `Run()` cedes Tab/Esc to the modal whenever `sigtype_dialog_open` is true, so the underlying screen doesn't yank focus back. In `explore` the popup also fires from the detail pane when browsing `parts`: each `pin → signal` row carries its signal name in the parallel `explore_detail_sig` vector, and Enter on a non-`(NC)` row opens the popup for that signal. -**Net tracing in `explore`**: when the user selects a signal entry in `explore` (type tab = `signals`), the detail pane shows two sections: the local pins of that signal (module/part/pin labels), then — if `find_net(sys, module, signal)` returns ≥ 2 members — a `Net members (across connections)` section listing every `(module, signal, type)` reachable through `Connection::pin_map`. The signal-detail header includes `K net members across N module(s)` (or `INCONSISTENT` if `net_type_consistent` returns false). Members are sorted naturally and pushed into `explore_detail_sig` so pressing Enter on a member opens the signal-type popup for it. This is what replaced the former dedicated `net` screen. +**Net tracing in `explore`**: when the user selects a signal entry in `explore` (type tab = `signals`), the detail pane shows two sections: the local pins of that signal (module/part/pin labels), then — if `find_net(sys, module, signal)` returns ≥ 2 members — a `Net members (across connections)` section listing every `(module, signal, type)` reachable through `Connection::pin_map`. The signal-detail header includes `K net members across N module(s)` (or `INCONSISTENT` if `net_type_consistent` returns false). This is what replaced the former dedicated `net` screen. + +**Enter-to-edit matrix in `explore`** — `explore_detail_sig` stores a `module\tsignal` payload per detail row (empty = read-only). The detail-menu `on_enter` parses the `\t` and calls `OpenSignalTypeDialog(mod, sig)`, so Enter works correctly for peer-module rows. Today: + +| Focus | parts | signals | connections | +|--------------------|-----------------------|----------------------------------|-------------| +| `children_menu` | jump to set-connector-type screen pre-filled on this part (focus on type input) | open signal-type popup for the selected signal | — | +| `detail_menu` | open signal-type popup for the pin's signal (skipped on `(NC)`) | open signal-type popup for the row's `(module, signal)` — works for both local pins (= current signal) and cross-module net members | — | + +Module / type / filter focuses do not have an Enter binding. **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. diff --git a/doc/user/analysis.md b/doc/user/analysis.md index 0696667..4e799ea 100644 --- a/doc/user/analysis.md +++ b/doc/user/analysis.md @@ -126,8 +126,16 @@ Every classification is advisory. To force a different type: - **Signal type**: from the `explore` screen, press Enter on a signal entry → a popup lets you pick `power` / `gnd` / `other`. - Or type `set-signal-type ` in the console - (or from the palette). + The same popup also opens when you press Enter on a pin row in + the parts detail (changes the pin's signal type) or on a net + member row in the signal detail (works across modules). Or type + `set-signal-type ` in the console (or + from the palette). +- **Connector type**: from `explore` with `type = parts`, press + Enter on a part to jump to the dedicated `set-connector-type` + screen with that part pre-selected and the cursor on the type + input. The `set-connector-type` command also keeps working from + the console / palette. - **Connector type**: `set-connector-type ` (also via the dashboard `[t]` shortcut). This drives the pin role expectations, which feed the `pin-role` check. diff --git a/src/tui/commands.cpp b/src/tui/commands.cpp index 213ca69..ab2bd6e 100644 --- a/src/tui/commands.cpp +++ b/src/tui/commands.cpp @@ -27,24 +27,9 @@ void Tui::RegisterCommands() { {{"command name (optional)", Completion::Command}}, [this](const std::vector &args) { if (args.empty()) { - size_t maxw = 0; - for (const auto &kv : commands) maxw = std::max(maxw, kv.first.size()); - 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."); + // Bare → open the feature-reference screen. + screen_back_idx = -1; + screen_idx = 6; return; } const std::string &name = args[0]; @@ -70,7 +55,9 @@ void Tui::RegisterCommands() { } }, /*prompt_for_missing=*/ false, - "show command help (optionally for a specific command)", + "bare → open the help screen; `help ` → textual command help", + /*scriptable=*/ false, + /*interactive=*/ true, }; commands["clear"] = { {}, [this](auto &) { output.clear(); }, true, "clear the visualization area" }; @@ -397,6 +384,7 @@ void Tui::RegisterCommands() { settype_type.clear(); settype_status.clear(); settype_focus_idx = 0; + screen_back_idx = -1; // standalone entry — Esc → dashboard screen_idx = 2; return; } diff --git a/src/tui/screen_dashboard.cpp b/src/tui/screen_dashboard.cpp index bd2482a..e6e4e9c 100644 --- a/src/tui/screen_dashboard.cpp +++ b/src/tui/screen_dashboard.cpp @@ -44,6 +44,7 @@ Component Tui::BuildDashboardScreen() { Element early_help = RenderHelpPanel("dashboard", { {"c", "console"}, {"a", "analyze"}, + {"h", "help screen"}, {"q", "quit"}, {"Ctrl-P", "palette"}, }); @@ -295,9 +296,9 @@ Component Tui::BuildDashboardScreen() { Element help = RenderHelpPanel("dashboard", { {"c", "console"}, {"p", "plug"}, - {"t", "set-connector-type"}, {"e", "explore"}, {"a", "analyze (verify + groups)"}, + {"h", "help screen"}, {"PgUp", "scroll up"}, {"PgDn", "scroll down"}, {"Home", "scroll top"}, diff --git a/src/tui/screen_explore.cpp b/src/tui/screen_explore.cpp index 67c12ad..caf0e0a 100644 --- a/src/tui/screen_explore.cpp +++ b/src/tui/screen_explore.cpp @@ -36,9 +36,49 @@ Component Tui::BuildExploreScreen() { child_opt.entries = &explore_children; child_opt.selected = &explore_child_idx; child_opt.on_enter = [this]() { - if (explore_type_idx != 1 || explore_children.empty()) return; - OpenSignalTypeDialog(explore_modules[explore_module_idx], - explore_children[explore_child_idx]); + if (!sys || explore_children.empty()) return; + const std::string &mod_name = explore_modules[explore_module_idx]; + const std::string &child = explore_children[explore_child_idx]; + if (explore_type_idx == 1) { + // Signal → signal-type popup. + OpenSignalTypeDialog(mod_name, child); + return; + } + if (explore_type_idx == 0) { + // Part → jump to the set-connector-type screen pre-filled on + // this part, with focus on the type input so the user can type + // the kind directly. + settype_modules.clear(); + for (auto &mk : *sys->modules()) settype_modules.push_back(mk.first); + std::sort(settype_modules.begin(), settype_modules.end(), NaturalLess); + auto it = std::find(settype_modules.begin(), + settype_modules.end(), mod_name); + if (it == settype_modules.end()) return; + settype_m_idx = (int)(it - settype_modules.begin()); + settype_p_filter = child; // narrows the part menu to this row + + // Predict the filtered + sorted list to find the *exact-match* + // index — substring match can put the target at a non-zero + // index (e.g. filtering "J2" surfaces J2, J20, J21 ; the right + // pane needs J2 selected, not whichever sorts first). + Module *mod = sys->modules()->get(mod_name); + std::vector matches; + std::string needle = ToLower(child); + for (auto &pkv : *mod) + if (ToLower(pkv.first).find(needle) != std::string::npos) + matches.push_back(pkv.first); + std::sort(matches.begin(), matches.end(), NaturalLess); + auto pit = std::find(matches.begin(), matches.end(), child); + settype_p_idx = (pit == matches.end()) + ? 0 : (int)(pit - matches.begin()); + + settype_type.clear(); + settype_status.clear(); + settype_focus_idx = 3; // straight to the type input + screen_back_idx = 3; // Esc on settype → back to explore + screen_idx = 2; + } + // type_idx == 2 (connections): no edit yet — left as a no-op. }; auto children_menu = Menu(child_opt); @@ -47,12 +87,17 @@ Component Tui::BuildExploreScreen() { MenuOption detail_opt = MenuOption::Vertical(); detail_opt.entries = &explore_detail; detail_opt.selected = &explore_detail_idx; + // Each `explore_detail_sig` slot is either empty (no action) or a + // `module\tsignal` pair. The cross-module form is what lets Enter on a + // net member row open the popup for that peer module's signal. detail_opt.on_enter = [this]() { if (explore_detail_idx < 0 || explore_detail_idx >= (int)explore_detail_sig.size()) return; - const std::string &sig = explore_detail_sig[explore_detail_idx]; - if (sig.empty()) return; - OpenSignalTypeDialog(explore_modules[explore_module_idx], sig); + const std::string &payload = explore_detail_sig[explore_detail_idx]; + if (payload.empty()) return; + size_t tab = payload.find('\t'); + if (tab == std::string::npos) return; + OpenSignalTypeDialog(payload.substr(0, tab), payload.substr(tab + 1)); }; auto detail_menu = Menu(detail_opt); @@ -142,9 +187,12 @@ Component Tui::BuildExploreScreen() { + r.second; if (keep_detail(line)) { explore_detail.push_back(line); - // "(NC)" → no underlying Signal to retype. + // "(NC)" → no underlying Signal to retype; for a + // real signal we store `module\tsignal` so Enter + // opens the popup scoped correctly. explore_detail_sig.push_back( - r.second == "(NC)" ? std::string{} : r.second); + r.second == "(NC)" ? std::string{} + : (cur_mod->name + "\t" + r.second)); } } } else if (explore_type_idx == 1) { @@ -171,9 +219,13 @@ Component Tui::BuildExploreScreen() { + " pins • type: " + signal_type_name(s->type) + " • " + net_hdr; - // Local pins first. + // Local pins first. Enter on any of these reopens the + // signal-type popup for the current signal (= same as + // Enter on the signal in the children menu — redundant + // but natural). explore_detail.push_back(" Local pins:"); explore_detail_sig.push_back({}); + std::string self_payload = cur_mod->name + "\t" + s->name; std::vector rows; for (auto &pin_kv : *s) { Pin *pin = pin_kv.second; @@ -186,32 +238,32 @@ Component Tui::BuildExploreScreen() { for (const auto &r : rows) if (keep_detail(r)) { explore_detail.push_back(" " + r); - explore_detail_sig.push_back({}); + explore_detail_sig.push_back(self_payload); } // Cross-module net members (only when truly bridged). - // Net-member rows are read-only: Enter is a no-op (we - // intentionally push an empty `explore_detail_sig` slot - // — the popup takes a (module, signal) pair scoped to - // the *currently selected* module, which would mis-fire - // for a member living on a peer module). + // Each row carries its own `module\tsignal` payload so + // Enter opens the popup for that peer-module signal — + // not the locally-selected module. if (n.members.size() >= 2) { explore_detail.push_back(""); explore_detail_sig.push_back({}); explore_detail.push_back(" Net members (across connections):"); explore_detail_sig.push_back({}); - std::vector net_rows; + std::vector> net_rows; for (const auto &mp : n.members) { std::string label = mp.first->name + "/" + mp.second->name + " (" + signal_type_name(mp.second->type) + ")"; - net_rows.push_back(std::move(label)); + net_rows.emplace_back(std::move(label), + mp.first->name + "\t" + mp.second->name); } - std::sort(net_rows.begin(), net_rows.end(), NaturalLess); + std::sort(net_rows.begin(), net_rows.end(), + [](const auto &a, const auto &b) { return NaturalLess(a.first, b.first); }); for (const auto &r : net_rows) - if (keep_detail(r)) { - explore_detail.push_back(" " + r); - explore_detail_sig.push_back({}); + if (keep_detail(r.first)) { + explore_detail.push_back(" " + r.first); + explore_detail_sig.push_back(r.second); } } } else { diff --git a/src/tui/screen_help.cpp b/src/tui/screen_help.cpp new file mode 100644 index 0000000..521fc67 --- /dev/null +++ b/src/tui/screen_help.cpp @@ -0,0 +1,236 @@ +#include "tui/tui.hpp" +#include "tui/tui_helpers.hpp" + +#include +#include +#include + +#include +#include + +using namespace ftxui; + +namespace { + +// Topic table: (menu label, body). Body paragraphs are separated by an +// empty line ("\n\n") and rendered with `paragraph()` so they wrap to +// the available column width. +const std::vector> &topics() { + static const std::vector> data = { + {"Overview", + "essim is a digital twin for the inter-card connections inside " + "a system. Load each card's netlist or pinout, tag its connectors, " + "wire the connectors together, then ask essim to verify pin roles " + "and trace nets across modules.\n\n" + "The dashboard is the home screen. From there you reach every " + "other view by a single keystroke; the global palette (Ctrl-P) " + "fuzzy-finds commands, modules and signals from anywhere."}, + + {"Dashboard", + "Read-only overview of the loaded system. Sections:\n\n" + "• Overview — modules, parts, signals, connections counters.\n\n" + "• Health — verify pin-role mismatches, bridged-net inconsistencies, " + "NC orphans. Green = clean, yellow = something to inspect.\n\n" + "• Analysis — diff pairs, diff buses, plain buses, anomaly count.\n\n" + "• Modules — per-module breakdown: parts × signals, the connector " + "types assigned (grouped), Power/Gnd inference summary.\n\n" + "Letter shortcuts open the other screens. PgUp/PgDn scroll the " + "dashboard if its content overflows."}, + + {"Console", + "The textual shell. Reach it from the dashboard with [c]. Type any " + "registered command; Enter submits, ↑/↓ walks history (persistent on " + "disk), Tab completes command names and file paths.\n\n" + "Multi-step commands (load, save, restore, source, set, " + "set-signal-type, …) prompt for each argument in turn when called " + "with no arguments. The same commands accept the full arg list " + "inline (scriptable form).\n\n" + "PageUp / PageDown / Home / End scroll the output buffer. Esc " + "cancels a pending prompt; a second Esc returns to the dashboard."}, + + {"Palette (Ctrl-P)", + "Press Ctrl-P from any screen to open a fuzzy search modal. " + "Type characters and the result list ranks commands, module names " + "and per-module signal names (qualified `module/signal`) by " + "subsequence match.\n\n" + "Enter activates the highlighted entry: a command runs (with its " + "wizard if it needs arguments), a module jumps to explore prefilled " + "on that module, a signal jumps to explore on the signals tab with " + "the child filter pre-seeded.\n\n" + "Esc closes the palette without acting."}, + + {"Explore", + "Browse the system module-first. Four columns: module → type " + "(parts / signals / connections) → filtered children → detail.\n\n" + "Pressing Enter modifies the focused element when it makes sense:\n\n" + "• on a part (children, type=parts) → jumps to set-connector-type " + "pre-filled on that part, focus on the type input;\n\n" + "• on a signal (children, type=signals) → opens the signal-type " + "popup (power / gnd / other);\n\n" + "• on a pin row (detail, type=parts) → opens the signal-type popup " + "for that pin's signal;\n\n" + "• on a net-member row (detail, type=signals) → opens the popup " + "for that peer-module signal — Enter works across modules.\n\n" + "When a signal is selected the detail pane shows the local pins " + "plus the BFS net members across all connections, with the count " + "and an INCONSISTENT flag if the bridged net mixes Power and Gnd."}, + + {"Connect / plug", + "Wire two parts that sit on different modules. Reachable from the " + "dashboard via [p] (plug) or as the `connect` command. The screen " + "has two endpoint columns (module + part filter + part menu) and a " + "Connect button.\n\n" + "The transform is picked automatically from the registered " + "(connector_type_A, connector_type_B) pair via TransformRegistry. " + "If neither side carries a registered type, an identity transform " + "matches pins by canonical name (e.g. A1 ↔ A001) and materialises " + "missing NC pads on the shorter side."}, + + {"set-connector-type", + "Tag a part with a connector kind such as `vpx-3u-bkp-p0`. Driven " + "by the transform registry: tagging two parts on compatible kinds " + "makes the next `connect` pick the right transform. Tagging also " + "populates each pin's expected signal type via the pin_role lookup, " + "which feeds the `pin-role` mismatch check in the analyze screen.\n\n" + "Reach the screen from explore by pressing Enter on a part, or run " + "the command directly (inline or interactive)."}, + + {"Signal types", + "Every signal carries a type: Power, GndShield, or Other (default).\n\n" + "The classification runs at the end of every `load` (and after " + "`duplicate`). Rules:\n\n" + "• Gnd: name matches GND / GROUND / EARTH / SHIELD / CHASSIS or " + "starts with any of those followed by `_`. Name alone is enough.\n\n" + "• Power: requires (a) the name suggests Power (contains PWR / VCC " + "/ VDD / VEE / VSS / VBAT, starts with VS_ or +Nv / -Nv), AND (b) " + "fan-out ≥ 3 (hard floor — never Power below this), AND (c) at " + "least one of: fan-out ≥ 4, or a voltage pattern in the name " + "(3V3, 5V, 12V…).\n\n" + "• Other: everything else.\n\n" + "`set-signal-type` overrides the inference. The analyze screen's " + "Types tab lists every classification with the reason attached."}, + + {"NC pins", + "Pins that have no signal attached. Three origins:\n\n" + "• Imported NC — the netlist explicitly marks the pin as " + "unconnected (Mentor uses signal name `unconnected`; Altium simply " + "omits the pin from every signal block).\n\n" + "• Dropped singleton — at end of `load`, every signal whose pin " + "set has size 1 is detached (a one-pin net carries no signal). " + "The lone pin survives as NC.\n\n" + "• Filled at connect — when wiring two parts of unequal length, " + "the shorter side gets new NC pads on the missing canonical " + "positions. These are bridged via the connection's pin_map and " + "are NOT counted as orphans."}, + + {"Analyze", + "Unified verify + structural analysis. Three tabs, switch with " + "Tab or ←/→:\n\n" + "• Issues — pin-role mismatches (typed connector pin disagreeing " + "with the signal landing on it), net-mix (bridged net carrying " + "both Power and Gnd), and structural anomalies (orphan _P, gap in " + "a bus, missing lane in a diff bus).\n\n" + "• Groups — every detected diff pair, diff bus, plain bus with " + "count + member list.\n\n" + "• Types — every Power / Gnd classification with the reason " + "(name + fan-out + voltage). Lists `[Suspect Power]` rows for " + "names that look like Power but failed the structural check."}, + + {"Scripting", + "`source ` runs a text script line by line. Comments start " + "with `#`, blank lines are skipped. While running, a centred " + "modal shows progress; the file is paced one effective line per " + "~30 ms so FTXUI redraws between commands.\n\n" + "`set ` declares a session-scoped variable; " + "subsequent commands expand `$name` and `${name}` in their args. " + "`script-save ` writes every command issued since the " + "last `new` to a replay file (excluding clear / help / quit / " + "exit / source / script-save themselves, plus bare interactive " + "commands).\n\n" + "`new` resets the recorded buffer and the variable table."}, + + {"Save / restore", + "`save ` writes a tab-delimited snapshot of the whole " + "system (modules, parts with connector types, pins with signal " + "links and NC origin tags, signal type overrides, connections " + "with their transform and pin maps). `restore ` replaces " + "the current system with the snapshot.\n\n" + "The snapshot format is versioned by a `# essim system snapshot " + "v1` header. The S-record line tags a signal type only when it " + "differs from the default (Other), keeping snapshots small."}, + + {"Quitting", + "From the dashboard: press `q`, or run the `quit` (or `exit`) " + "command from the console / palette. Quit works from any " + "screen — the command calls into the FTXUI screen directly so " + "the loop returns immediately."}, + }; + return data; +} + +} // namespace + +Component Tui::BuildHelpScreen() { + help_topic_names.clear(); + for (const auto &t : topics()) help_topic_names.push_back(t.first); + + MenuOption topic_opt = MenuOption::Vertical(); + topic_opt.entries = &help_topic_names; + topic_opt.selected = &help_topic_idx; + topic_opt.focused_entry = &help_topic_idx; + auto topic_menu = Menu(topic_opt); + + return Renderer(topic_menu, [this, topic_menu] { + auto title = hbox({ + text(" essim ") | bold, + text("→ ") | dim, + text("help") | bold, + text(" — main feature reference") | dim, + }); + + int idx = help_topic_idx; + if (idx < 0) idx = 0; + if (idx >= (int)topics().size()) + idx = (int)topics().size() - 1; + const auto &topic = topics()[idx]; + + // Render the body: split on blank lines so each chunk becomes a + // paragraph (FTXUI's `paragraph()` wraps to the available width). + Elements body; + body.push_back(text(" " + topic.first + " ") | bold); + body.push_back(separator()); + const std::string &b = topic.second; + size_t pos = 0; + while (pos < b.size()) { + size_t end = b.find("\n\n", pos); + std::string chunk = (end == std::string::npos) + ? b.substr(pos) + : b.substr(pos, end - pos); + body.push_back(paragraph(chunk)); + body.push_back(text("")); // blank line between paragraphs + if (end == std::string::npos) break; + pos = end + 2; + } + + auto left = vbox({ + text(" topic ") | bold, + separator(), + topic_menu->Render() | yframe | flex, + }) | size(WIDTH, EQUAL, 28); + + auto center = vbox(std::move(body)) + | vscroll_indicator | yframe | flex; + + Element help = RenderHelpPanel("help", { + {"↑/↓", "pick topic"}, + {"Ctrl-P", "palette"}, + {"Esc", "dashboard"}, + }); + + return vbox({ + title, + separator(), + hbox({left, separator(), center, separator(), help}) | flex, + }) | border; + }); +} diff --git a/src/tui/screen_main.cpp b/src/tui/screen_main.cpp index 4eac288..3526c08 100644 --- a/src/tui/screen_main.cpp +++ b/src/tui/screen_main.cpp @@ -1,10 +1,6 @@ #include "tui/tui.hpp" #include "tui/tui_helpers.hpp" -#include "system/connect.hpp" -#include "system/modules.hpp" -#include "system/system.hpp" - #include #include #include @@ -27,17 +23,15 @@ 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"; + // Same title pattern as every other screen — the console is no + // longer the home screen, so it gets the standard breadcrumb. The + // module/connection counters live on the dashboard now. auto title = hbox({ text(" essim ") | bold, - text("— system digital twin") | dim, - filler(), - text(subtitle) | dim, - text(" "), - }) | bgcolor(Color::Default); + text("→ ") | dim, + text("console") | bold, + text(" — type commands, read textual output") | dim, + }); // Clamp scroll offset to a meaningful range and pick the line to focus. int n = (int)output.size(); diff --git a/src/tui/screen_settype.cpp b/src/tui/screen_settype.cpp index 450ab96..c7eae03 100644 --- a/src/tui/screen_settype.cpp +++ b/src/tui/screen_settype.cpp @@ -26,9 +26,24 @@ Component Tui::BuildSettypeScreen() { return el; }; - auto module_menu = Menu(&settype_modules, &settype_m_idx); + // Bind `focused_entry` to the same int as `selected` on every Menu so + // the cursor (`>`) always tracks the selected row. Without this, when + // we pre-seed `settype_m_idx` / `settype_p_idx` from outside (e.g. + // jumping from `explore`), the highlight lands on the target while the + // cursor stays on whatever the user last hovered → visually confusing. + MenuOption mopt = MenuOption::Vertical(); + mopt.entries = &settype_modules; + mopt.selected = &settype_m_idx; + mopt.focused_entry = &settype_m_idx; + auto module_menu = Menu(mopt); + auto part_filter = Input(&settype_p_filter, "filter…", pf_opt); - auto part_menu = Menu(&settype_p_list, &settype_p_idx); + + MenuOption popt = MenuOption::Vertical(); + popt.entries = &settype_p_list; + popt.selected = &settype_p_idx; + popt.focused_entry = &settype_p_idx; + auto part_menu = Menu(popt); InputOption type_opt; type_opt.multiline = false; @@ -114,13 +129,13 @@ Component Tui::BuildSettypeScreen() { text(" " + current), separator(), FocusLabel(text(" new type: "), settype_focus_idx == 3) | bold, - hbox({text(" "), type_input->Render() | flex}) | border, + hbox({text(" "), type_input->Render() | xflex}) | border, text(known_line) | dim, filler(), hbox({filler(), FocusLabel(button->Render(), settype_focus_idx == 4), filler()}), - }) | size(WIDTH, EQUAL, 40); + }) | flex; Element status = settype_status.empty() ? text("") | dim @@ -144,7 +159,7 @@ Component Tui::BuildSettypeScreen() { return vbox({ title, separator(), - hbox({left, separator(), middle, separator(), right | flex, + hbox({left, separator(), middle, separator(), right, separator(), help}) | flex, separator(), status, diff --git a/src/tui/tui.cpp b/src/tui/tui.cpp index bbc6c9c..a981de2 100644 --- a/src/tui/tui.cpp +++ b/src/tui/tui.cpp @@ -41,10 +41,11 @@ void Tui::Run() { &sigtype_dialog_open); auto dashboard_screen = BuildDashboardScreen(); auto analyze_screen = BuildAnalyzeScreen(); + auto help_screen = BuildHelpScreen(); auto tab = Container::Tab( {main_screen, connect_screen, settype_screen, explore_screen, - dashboard_screen, analyze_screen}, + dashboard_screen, analyze_screen, help_screen}, &screen_idx); // Palette is a global Modal — overlays the tab on every screen. @@ -58,8 +59,12 @@ void Tui::Run() { if (e == Event::CtrlP) { OpenPalette(); return true; } // screen_idx mapping: 0 = console, 1 = connect, 2 = set-connector-type, - // 3 = explore, 4 = dashboard (home), 5 = analyze. + // 3 = explore, 4 = dashboard (home), 5 = analyze, 6 = help. switch (screen_idx) { + case 6: // help + if (e == Event::Escape) { screen_idx = 4; return true; } + return false; + case 5: // analyze if (e == Event::Escape) { screen_idx = 4; return true; } if (e == Event::Tab || e == Event::ArrowRight) { @@ -84,9 +89,9 @@ void Tui::Run() { if (e == Event::Character("q")) { Dispatch("quit"); return true; } if (e == Event::Character("c")) { screen_idx = 0; return true; } if (e == Event::Character("p")) { Dispatch("connect"); return true; } - if (e == Event::Character("t")) { Dispatch("set-connector-type"); return true; } if (e == Event::Character("e")) { Dispatch("explore"); return true; } if (e == Event::Character("a")) { screen_idx = 5; return true; } + if (e == Event::Character("h")) { screen_idx = 6; return true; } return false; case 3: // explore @@ -96,7 +101,18 @@ void Tui::Run() { return false; case 2: // set-connector-type - if (e == Event::Escape) { screen_idx = 4; return true; } + if (e == Event::Escape) { + // Honour an inter-screen back-link (e.g. came via Enter on + // a part in `explore`). Otherwise fall through to the + // dashboard like every other Esc. + if (screen_back_idx >= 0) { + screen_idx = screen_back_idx; + screen_back_idx = -1; + } else { + screen_idx = 4; + } + return true; + } if (e == Event::Tab) { settype_focus_idx = (settype_focus_idx + 1) % 5; return true; } if (e == Event::TabReverse) { settype_focus_idx = (settype_focus_idx + 4) % 5; return true; } return false; diff --git a/src/tui/tui.hpp b/src/tui/tui.hpp index c3019f8..60c754e 100644 --- a/src/tui/tui.hpp +++ b/src/tui/tui.hpp @@ -55,6 +55,11 @@ class Tui { // ---- Screen orchestration ---- int screen_idx; + // Where Esc should send the user *next* if they came via an inter-screen + // jump (e.g. Enter on a part in `explore` → set-connector-type). −1 means + // "no back-link, Esc goes to the dashboard like usual". Always reset + // after consumption to avoid stale links across unrelated navigation. + int screen_back_idx = -1; // ---- Connect screen state ---- std::vector connect_modules; @@ -97,6 +102,10 @@ class Tui { // ---- Dashboard scroll state (0 = top; grows as the user scrolls down) ---- int dashboard_scroll_offset = 0; + // ---- Help screen state ---- + int help_topic_idx = 0; + std::vector help_topic_names; ///< populated by BuildHelpScreen + // ---- Analyze screen state (unified verify + analyze) ---- int analyze_focus_idx = 0; ///< 0=issues 1=groups 2=types std::vector analyze_issues; @@ -186,6 +195,7 @@ private: ftxui::Component BuildExploreScreen(); ftxui::Component BuildDashboardScreen(); ftxui::Component BuildAnalyzeScreen(); + ftxui::Component BuildHelpScreen(); ftxui::Component BuildSignalTypeModal(); ftxui::Component BuildPaletteModal(); // Open palette (resets query/index, builds initial list).