Help screen, explore→set-connector-type Enter, settype UI polish.

- 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 <name>` 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 10:31:09 +02:00
parent 792e4745d3
commit 300e871aed
10 changed files with 395 additions and 66 deletions

View File

@@ -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 <m> <s> <t>` 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.

View File

@@ -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 <module> <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 <module> <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 <module> <part> <connector-kind>`
(also via the dashboard `[t]` shortcut). This drives the pin role
expectations, which feed the `pin-role` check.

View File

@@ -27,24 +27,9 @@ void Tui::RegisterCommands() {
{{"command name (optional)", Completion::Command}},
[this](const std::vector<std::string> &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 <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.");
// 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 <name>` → 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;
}

View File

@@ -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"},

View File

@@ -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<std::string> 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<std::string> 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<std::string> net_rows;
std::vector<std::pair<std::string, std::string>> 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 {

236
src/tui/screen_help.cpp Normal file
View File

@@ -0,0 +1,236 @@
#include "tui/tui.hpp"
#include "tui/tui_helpers.hpp"
#include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp>
#include <ftxui/dom/elements.hpp>
#include <utility>
#include <vector>
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<std::pair<std::string, std::string>> &topics() {
static const std::vector<std::pair<std::string, std::string>> 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 <file>` 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 <name> <value>` declares a session-scoped variable; "
"subsequent commands expand `$name` and `${name}` in their args. "
"`script-save <file>` 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 <file>` 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 <file>` 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;
});
}

View File

@@ -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 <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp>
#include <ftxui/component/screen_interactive.hpp>
@@ -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();

View File

@@ -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,

View File

@@ -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;

View File

@@ -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<std::string> 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<std::string> 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<std::string> 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).