Merge search and net screens into explore; drop both commands.
`explore` was already a superset of `search` (4 columns: module → type → filtered children → detail, with parts/signals/connections — vs search's 2 columns of parts/signals only). It now also subsumes the former `net` screen: when a signal entry is selected, the detail pane shows the local pins followed by a `Net members (across connections)` section listing every `(module, signal, type)` reachable through the BFS over `Connection::pin_map`, with the count + dominant type and an INCONSISTENT flag in the signal-detail header. Removed: - `src/tui/screen_search.cpp`, `src/tui/screen_net.cpp`. - `commands["search"]`, `commands["net"]` (including its textual inline form). The `find_net` / `Net` API stays for explore's BFS panel and the analyze screen's net-mix check. - `[s]` and `[n]` letter shortcuts on the dashboard. - `net_*` and `search_*` state members + builders + constructor inits. screen_idx renumbering (the slots vacated by search + net are removed, not left dead): 0 = console (unchanged) 1 = connect 2 = set-connector-type 3 = explore (unchanged number, but now subsumes search + net) 4 = dashboard (boot) 5 = analyze Palette signal items now jump to `explore` prefilled on the signals tab with the child filter seeded to the exact signal name; the BFS section in the detail pane is what shows the cross-module net. Net-member rows in the detail pane are deliberately read-only for now (Enter is a no-op): the signal-type popup is scoped to the currently selected module, so opening it on a peer-module member would mis-fire. Cross-module Enter navigation can come later if needed. DESIGN.md and user docs updated accordingly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
30
DESIGN.md
30
DESIGN.md
@@ -50,15 +50,13 @@ src/
|
||||
completion.cpp CompleteCommand, CompletePath, CompleteInline
|
||||
commands.cpp RegisterCommands (all built-in commands declared here)
|
||||
screen_main.cpp BuildMainScreen (visualisation area + bottom input)
|
||||
screen_search.cpp BuildSearchScreen
|
||||
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))
|
||||
screen_dashboard.cpp BuildDashboardScreen (read-only system overview)
|
||||
screen_analyze.cpp BuildAnalyzeScreen (anomalies / groups / power decisions)
|
||||
screen_palette.cpp BuildPaletteModal (global Ctrl-P fuzzy launcher)
|
||||
screen_sigtype_modal.cpp BuildSignalTypeModal (popup attached to net + explore via Modal())
|
||||
screen_sigtype_modal.cpp BuildSignalTypeModal (popup attached to explore via Modal())
|
||||
doc/classes.puml -- PlantUML class diagram
|
||||
```
|
||||
|
||||
@@ -87,7 +85,7 @@ doc/classes.puml -- PlantUML class diagram
|
||||
- Multi-step prompts work via a `std::deque<Prompt>` queue. `Submit()` pops them one by one before falling back to dispatch. Adding a new command = one entry in `RegisterCommands()`; the prompt-flow and inline-flow are both handled automatically.
|
||||
- Tab completion: at the top-level prompt (no `pending`), completes built-in command names. Inside a prompt with `path_completion = true` (e.g. the `filename` step of `load`), completes file paths via `std::filesystem::directory_iterator` (handles `~/`, dirs get a trailing `/`). Logic: 1 match → replace; multiple with progress on the longest common prefix → extend; multiple stuck at LCP → list candidates in the visualisation area.
|
||||
|
||||
Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source`, `script-save`, `connect`, `set-connector-type`, `set-signal-type`, `search`, `explore`, `verify`, `net`, `clear`, `help`, `quit`/`exit`. `Esc` cancels an in-progress multi-step prompt.
|
||||
Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source`, `script-save`, `connect` (alias `plug`), `set-connector-type`, `set-signal-type`, `explore`, `verify`, `analyze`, `dashboard`, `clear`, `help`, `quit`/`exit`. `Esc` cancels an in-progress multi-step prompt.
|
||||
|
||||
`set <name> <value>` declares a session-scoped variable. Subsequent commands expand `$name` and `${name}` in their args (substitution happens in `Finalize` between canonical-form recording and `spec.action(args)` — so `history` and `script-save` keep the **unexpanded** form, while the action sees resolved values). Unknown variables are left literal. `vars` is reset by `new`. Validation: `[A-Za-z_][A-Za-z0-9_]*`.
|
||||
|
||||
@@ -97,7 +95,7 @@ Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source
|
||||
|
||||
`source <file>` reads a script line by line and feeds each line through `Submit()`. While the script is running, `in_source = true` is set on the `Tui` and:
|
||||
- `Dispatch` / `Finalize` skip writing to memory + on-disk history.
|
||||
- After each `Submit`, if `screen_idx != 0` (a screen was opened by an "interactive" command like bare `connect`/`search`/`set-connector-type`), the script is aborted with an error message and `screen_idx` is reset to 0 — interactive screen-opening commands are explicitly disallowed in scripts.
|
||||
- After each `Submit`, if `screen_idx != 0` (a screen was opened by an "interactive" command like bare `connect` / `explore` / `set-connector-type`), the script is aborted with an error message and `screen_idx` is reset to 0 — interactive screen-opening commands are explicitly disallowed in scripts.
|
||||
|
||||
Pending prompts (from incomplete inline commands) are NOT considered interactive and are filled by subsequent script lines, the way you'd expect. Lines starting with `#` and blank lines are skipped; leading/trailing whitespace is trimmed; `~/` is expanded.
|
||||
|
||||
@@ -118,7 +116,7 @@ The explore screen shows the type in the signal detail header.
|
||||
|
||||
**Connector pin layout (preparation)**: `pin_layout(connector_type)` returns the canonical full pin-name list for a known connector kind, and `FillPartFromLayout(part, kind)` materialises NC pins for any layout position absent from the imported netlist. `set-connector-type` calls it after setting `connector_type` (no-op today since `pin_layout` is a stub returning `{}` for everything — populate alongside `vpx_3u_role`). End-to-end chain in place: `set-connector-type → FillPartFromLayout → pin_role`.
|
||||
|
||||
**`verify` (three passes)**: (1) walks all typed pins and reports local mismatches between `expected_signal_type` and the actual signal type; (2) walks all bridged nets reporting Power↔GndShield inconsistencies; (3) prints a single-line orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)`. The orphan pass filters out pins that appear in any `Connection::pin_map` — those are bridged to a real signal on the peer module (typically `FillIdentityNCs`-materialised) and not real NCs at system level. `net <module> <signal>` prints the BFS-reached `(module, signal)` set with types and an `[INCONSISTENT]` flag.
|
||||
**`verify` (three passes)**: (1) walks all typed pins and reports local mismatches between `expected_signal_type` and the actual signal type; (2) walks all bridged nets reporting Power↔GndShield inconsistencies; (3) prints a single-line orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)`. The orphan pass filters out pins that appear in any `Connection::pin_map` — those are bridged to a real signal on the peer module (typically `FillIdentityNCs`-materialised) and not real NCs at system level. The BFS-reached `(module, signal)` set for any signal is shown live in `explore`'s detail pane when a signal entry is selected.
|
||||
|
||||
**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/system/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped):
|
||||
|
||||
@@ -130,7 +128,7 @@ The explore screen shows the type in the signal detail header.
|
||||
|
||||
Exposed as the `analyze` shell command which prints groups (sorted by module + label) followed by anomalies. Designed to be consumed by the upcoming dashboard so the summary is visible at a glance. Tests: `tests/test_analysis.cpp`.
|
||||
|
||||
**Component classification**: every `Part` carries a `ComponentKind kind` (`Passive | Semiconductor | IntegratedCircuit | Connector | TestPoint | Switch | Crystal | Mechanical | Other`) inferred at construction by `infer_component_kind(name)` from the leading reference-designator letter(s) (longest-match: `LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV` first, then single-letter R/C/L/F/D/Q/U/J/P/Y/X/S). Recomputed on `restore` (no persistence tag). Not yet exposed in TUI commands — branchpoints will be `search` filter, `set-connector-type` guard, and `explore` header.
|
||||
**Component classification**: every `Part` carries a `ComponentKind kind` (`Passive | Semiconductor | IntegratedCircuit | Connector | TestPoint | Switch | Crystal | Mechanical | Other`) inferred at construction by `infer_component_kind(name)` from the leading reference-designator letter(s) (longest-match: `LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV` first, then single-letter R/C/L/F/D/Q/U/J/P/Y/X/S). Recomputed on `restore` (no persistence tag). Not yet exposed in TUI commands — branchpoints will be `set-connector-type` guard, `explore` filter, and `explore` header.
|
||||
|
||||
`SignalType` lives in its own header `src/system/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle).
|
||||
|
||||
@@ -144,7 +142,7 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l
|
||||
|
||||
**Subset wiring + NC backfill**: `CheckIdentityCompatible(a, b, info=&s)` accepts the case where one side's canonical pin set is a subset of the other's — typical when one importer drops NC pins (Altium) and the other doesn't (Mentor). It populates `info` with a non-fatal "N pin(s) only on '<part>'" message. Bidirectional mismatch (both sides have orphans) is still refused. After acceptance, `connect` calls `FillIdentityNCs(p1, p2)` which materialises the orphan canonical positions on the missing side as NC pins (`new Pin(other_side_name)`) — so `Connection::pin_map.size()` matches the larger side's count. Idempotent.
|
||||
|
||||
`screen_idx` mapping: **6 = dashboard (home, set in the constructor)**, 0 = console (textual shell + log view), 1 = search, 2 = connect, 3 = set-connector-type, 4 = explore, 5 = net, 7 = analyze. The dashboard is the boot screen; the console is the secondary screen reachable via the `[c]` shortcut and used to display textual output from `verify`/`analyze`/etc. plus collect arguments for multi-step commands. The label was renamed from "log" to "console" because the screen is also where commands are typed — "log" only described half of what it does.
|
||||
`screen_idx` mapping: **4 = dashboard (home, set in the constructor)**, 0 = console (textual shell + log view), 1 = connect, 2 = set-connector-type, 3 = explore, 5 = analyze. The dashboard is the boot screen; the console is the secondary screen reachable via the `[c]` shortcut and used to display textual output from `verify`/`analyze`/etc. plus collect arguments for multi-step commands. The label was renamed from "log" to "console" because the screen is also where commands are typed — "log" only described half of what it does. **The previous `search` and `net` screens (formerly `screen_idx = 1` and `5`) have been removed** — `explore` is a superset of both. Search by pattern: use `explore`'s child filter. Net tracing: select a signal in `explore` and the detail pane lists both the local pins and the BFS-reached `(module, signal)` members across connections. The palette (`Ctrl-P`) also covers the "jump to this signal / module" use case.
|
||||
|
||||
**Dashboard letter conflicts**: with the screen renames, `[c]` now opens the **console** rather than `connect`. The connect command is surfaced as **`[p]lug`** on the dashboard (a UI rename only — the canonical command stays `connect` for script + save/restore stability, with `plug` registered as an alias so the palette finds it under either name).
|
||||
|
||||
@@ -162,11 +160,11 @@ Everything is recomputed every frame so manual overrides via the signal-type pop
|
||||
|
||||
**Command palette** (`screen_palette.cpp`): a global modal launcher attached to the whole tab tree via `tab | Modal(BuildPaletteModal(), &palette_open)` in `Run()`. Trigger: `Event::CtrlP` (FTXUI Input does not consume Ctrl-P, so the outer `CatchEvent` reliably picks it up first). Behaviour: a single Input bound to `palette_query` plus a result list rebuilt on every frame. Indexes three kinds of entries: commands (from the `commands` map), modules and per-module signals (qualified as `module/signal`). Fuzzy match is subsequence-based, case-insensitive: lower score wins, computed as `first_match_position * 100 + sum_of_gaps`. Kinds are biased by a constant offset (commands +0, modules +1000, signals +2000) so command matches come first when scores tie. Output capped at 20 rows to keep render cheap on big systems. Activation (`Enter`): commands → `Dispatch(name)` (which dispatches like the shell, including opening interactive screens), module → prefill `explore_*` state and jump to `screen_idx = 4`, signal → prefill `net_modules` + seed `net_sig_filter` to the exact signal name and jump to `screen_idx = 5`. `Esc` closes the palette. While the palette is open, the outer `CatchEvent` cedes events to it so Tab/Esc/etc. don't leak into the underlying screen.
|
||||
|
||||
**Dashboard** (`screen_dashboard.cpp`, `dashboard` command, `screen_idx = 6`): read-only system overview. Single Renderer, no Input child. Recomputes everything per frame (cheap on realistic sizes): counters (modules/parts/signals/connections), three health rows (verify pin-role mismatches, bridged-net inconsistencies, NC orphans — green check / yellow warning prefix), an analysis summary line (diff pairs / buses / anomaly count, coloured if non-zero), and a per-module table (parts / signals / `connector_type`-tagged parts). Letter shortcuts handled in the outer `CatchEvent`: `s/c/t/e/n` `Dispatch` the bare interactive command (which populates state + flips `screen_idx`), `v` and `a` `Dispatch` the textual command then jump back to the shell (`screen_idx = 0`) so the output is readable. `Esc` returns to the shell. The dashboard is `interactive = true`, `scriptable = false`; running `dashboard` inside `source` aborts the script.
|
||||
**Dashboard** (`screen_dashboard.cpp`, `dashboard` command, `screen_idx = 4`): read-only system overview. Single Renderer, no Input child. Recomputes everything per frame (cheap on realistic sizes): counters (modules/parts/signals/connections), three health rows (verify pin-role mismatches, bridged-net inconsistencies, NC orphans — green check / yellow warning prefix), an analysis summary line (diff pairs / buses / anomaly count, coloured if non-zero), and a per-module table (parts / signals / `connector_type`-tagged parts). Letter shortcuts handled in the outer `CatchEvent`: `c`=console, `p`=plug (connect), `t`=set-connector-type, `e`=explore, `a`=analyze, `q`=quit. `Esc` is swallowed on the dashboard (home). The dashboard is `interactive = true`, `scriptable = false`; running `dashboard` inside `source` aborts the script.
|
||||
|
||||
**Screen titles** (shared idiom): every interactive screen renders a top bar in the form `" essim "` (bold) + `"→ "` (dim) + `"<screen-name>"` (bold) + `" — <short description>"` (dim), followed by a `separator()`. The main screen has its own variant that adds a live `N module(s), M connection(s)` counter on the right. Aim is to make the breadcrumb between essim and the current mode visible at all times.
|
||||
|
||||
**Focus highlighting**: each interactive screen reuses `FocusLabel(elem, focused)` from `tui_helpers.hpp` (inline helper, `focused ? e | inverted : e`) on the label of the currently-focused field so the user sees at a glance where the next keystroke lands. Indices match the `Container::Vertical` order — e.g. `connect` has 7 (m1, p1-filter, p1-menu, m2, p2-filter, p2-menu, Button), `net` has 3 (filter, module, signal). Buttons (`Connect`, `Apply`) get the highlight on the button itself, not a separate label.
|
||||
**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), `explore` has 6 (module, type, child-filter, child, detail-filter, detail). Buttons (`Connect`, `Apply`) get the highlight on the button itself, not a separate label.
|
||||
|
||||
**Context help panel**: every screen renders a right-column help panel via `RenderHelpPanel(title, entries)` (`tui_helpers.{hpp,cpp}`). Fixed width 30 cols, key column 9 chars, then a flex description. The entries list is per-screen so it acts as a context cheat-sheet: dashboard advertises its letter shortcuts + `Ctrl-P` + `q`; the shell advertises history / scroll / `Ctrl-P` / common commands; the interactive screens advertise `Tab` / `↑↓` / `Enter` / `Esc` semantics. Helps replace the previous one-line dim footers, which were truncated at small widths.
|
||||
|
||||
@@ -182,17 +180,9 @@ 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:
|
||||
- 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.
|
||||
**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.
|
||||
|
||||
**Signal-type popup (shared)**: Enter on a signal entry in the `net` or `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` is dual-mode (`prompt_for_missing = false`, `interactive = true`):
|
||||
- Inline: `net <module> <signal>` — prints the BFS-reached `(module, signal)` set in the visualisation area (with types and an `[INCONSISTENT]` flag).
|
||||
- Bare: opens `screen_idx = 5`. Three columns: module `Menu` (left), filter `Input` + filtered signal `Menu` of the selected module (middle), and a read-only panel (right) that recomputes the net on every frame and lists `(module, signal, type)` for each member plus a header summarising count + dominant type + inconsistency flag. The signal list is sorted with `NaturalLess`; `net_sig_idx` is clamped if the filter shrinks it. `Tab` cycles 3 fields (filter → module → signal); `Esc` leaves.
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
@@ -124,10 +124,10 @@ how to run today.
|
||||
|
||||
Every classification is advisory. To force a different type:
|
||||
|
||||
- **Signal type**: from the `net` or `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).
|
||||
- **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).
|
||||
- **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.
|
||||
|
||||
@@ -65,8 +65,8 @@ share the same conventions:
|
||||
a sourced script. A sourced script must use the inline form of these
|
||||
commands instead.
|
||||
|
||||
Today's interactive screens: `connect`, `search`, `set-connector-type`,
|
||||
`explore`, `net`. See [`commands.md`](commands.md) for each.
|
||||
Today's interactive screens: `connect`, `set-connector-type`,
|
||||
`explore`, `analyze`. See [`commands.md`](commands.md) for each.
|
||||
|
||||
## Saving, restoring, replaying
|
||||
|
||||
|
||||
@@ -293,7 +293,7 @@ void Tui::RegisterCommands() {
|
||||
"check pin roles locally and signal-type consistency across bridged nets" };
|
||||
|
||||
commands["dashboard"] = { {}, [this](auto &) {
|
||||
screen_idx = 6;
|
||||
screen_idx = 4;
|
||||
}, true,
|
||||
"open the dashboard (system overview)",
|
||||
/*scriptable=*/ false,
|
||||
@@ -349,58 +349,6 @@ void Tui::RegisterCommands() {
|
||||
}, true,
|
||||
"detect signal groups (diff pairs, buses) and structural anomalies" };
|
||||
|
||||
commands["net"] = {
|
||||
{{"module", Completion::None},
|
||||
{"signal name", Completion::None}},
|
||||
[this](const std::vector<std::string> &args) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
|
||||
if (args.empty()) {
|
||||
net_modules.clear();
|
||||
for (auto &m : *sys->modules()) net_modules.push_back(m.first);
|
||||
std::sort(net_modules.begin(), net_modules.end(), NaturalLess);
|
||||
if (net_modules.empty()) { Print("no modules loaded."); return; }
|
||||
net_module_idx = 0;
|
||||
net_sig_filter.clear();
|
||||
net_sig_idx = 0;
|
||||
net_focus_idx = 0;
|
||||
screen_idx = 5;
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() != 2) {
|
||||
Print("usage: net <module> <signal> (or no args for interactive)");
|
||||
return;
|
||||
}
|
||||
|
||||
Module *mod;
|
||||
try { mod = sys->modules()->get(args[0]); }
|
||||
catch (const std::exception &) {
|
||||
Print("unknown module: " + args[0]); return;
|
||||
}
|
||||
Signal *sig;
|
||||
try { sig = mod->signals->get(args[1]); }
|
||||
catch (const std::exception &) {
|
||||
Print("unknown signal: " + mod->name + "/" + args[1]); return;
|
||||
}
|
||||
Net n = find_net(sys.get(), mod, sig);
|
||||
SignalType dom;
|
||||
bool ok = net_type_consistent(n, dom);
|
||||
Print("net containing " + mod->name + "/" + sig->name
|
||||
+ " — " + std::to_string(n.members.size()) + " signal(s)"
|
||||
+ (ok ? "" : " [INCONSISTENT]")
|
||||
+ ", dominant: " + signal_type_name(dom));
|
||||
for (const auto &mp : n.members) {
|
||||
Print(" " + mp.first->name + "/" + mp.second->name
|
||||
+ " (" + signal_type_name(mp.second->type) + ")");
|
||||
}
|
||||
},
|
||||
/*prompt_for_missing=*/ false,
|
||||
"show all signals reachable from <module>/<signal> through connections "
|
||||
"(interactive screen if no args)",
|
||||
/*scriptable=*/ true,
|
||||
/*interactive=*/ true,
|
||||
};
|
||||
|
||||
commands["set-signal-type"] = {
|
||||
{{"module", Completion::None},
|
||||
@@ -449,7 +397,7 @@ void Tui::RegisterCommands() {
|
||||
settype_type.clear();
|
||||
settype_status.clear();
|
||||
settype_focus_idx = 0;
|
||||
screen_idx = 3;
|
||||
screen_idx = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -518,7 +466,7 @@ void Tui::RegisterCommands() {
|
||||
connect_p1_idx = 0;
|
||||
connect_p2_idx = 0;
|
||||
connect_focus_idx = 0;
|
||||
screen_idx = 2;
|
||||
screen_idx = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -651,72 +599,11 @@ void Tui::RegisterCommands() {
|
||||
explore_child_filter.clear();
|
||||
explore_detail_filter.clear();
|
||||
explore_focus_idx = 0;
|
||||
screen_idx = 4;
|
||||
screen_idx = 3;
|
||||
}, true, "browse modules → parts/signals/connections → details (interactive)",
|
||||
/*scriptable=*/ false,
|
||||
/*interactive=*/ true };
|
||||
|
||||
commands["search"] = {
|
||||
{{"module", Completion::None},
|
||||
{"kind [parts|signals]", Completion::None},
|
||||
{"pattern", Completion::None}},
|
||||
[this](const std::vector<std::string> &args) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
|
||||
if (args.empty()) {
|
||||
search_modules.clear();
|
||||
for (auto &m : *sys->modules()) search_modules.push_back(m.first);
|
||||
std::sort(search_modules.begin(), search_modules.end(), NaturalLess);
|
||||
if (search_modules.empty()) { Print("no modules loaded."); return; }
|
||||
search_module_idx = 0;
|
||||
search_type_idx = 0;
|
||||
search_query.clear();
|
||||
search_focus_idx = 0;
|
||||
screen_idx = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() != 3) {
|
||||
Print("usage: search <module> <parts|signals> <pattern>");
|
||||
Print(" search (interactive)");
|
||||
return;
|
||||
}
|
||||
|
||||
Module *mod;
|
||||
try { mod = sys->modules()->get(args[0]); }
|
||||
catch (const std::exception &) {
|
||||
Print("unknown module: " + args[0]); return;
|
||||
}
|
||||
|
||||
std::string kind = ToLower(args[1]);
|
||||
std::string needle = ToLower(args[2]);
|
||||
|
||||
std::vector<std::pair<std::string, size_t>> hits;
|
||||
if (kind == "parts" || kind == "part") {
|
||||
for (auto &pkv : *mod)
|
||||
if (ToLower(pkv.first).find(needle) != std::string::npos)
|
||||
hits.emplace_back(pkv.first, pkv.second->size());
|
||||
} else if (kind == "signals" || kind == "signal") {
|
||||
for (auto &skv : *mod->signals)
|
||||
if (ToLower(skv.first).find(needle) != std::string::npos)
|
||||
hits.emplace_back(skv.first, skv.second->size());
|
||||
} else {
|
||||
Print("kind must be 'parts' or 'signals' (got: " + args[1] + ")");
|
||||
return;
|
||||
}
|
||||
std::sort(hits.begin(), hits.end(),
|
||||
[](const auto &a, const auto &b) { return NaturalLess(a.first, b.first); });
|
||||
for (const auto &h : hits) {
|
||||
Print(" " + args[0] + "/" + h.first
|
||||
+ " (" + std::to_string(h.second) + " pins)");
|
||||
}
|
||||
Print(std::to_string(hits.size()) + " match(es).");
|
||||
},
|
||||
/*prompt_for_missing=*/ false,
|
||||
"list parts/signals matching a pattern (interactive screen if no args)",
|
||||
/*scriptable=*/ true,
|
||||
/*interactive=*/ true,
|
||||
};
|
||||
|
||||
commands["duplicate"] = {
|
||||
{{"source module", Completion::None},
|
||||
|
||||
@@ -294,11 +294,9 @@ Component Tui::BuildDashboardScreen() {
|
||||
|
||||
Element help = RenderHelpPanel("dashboard", {
|
||||
{"c", "console"},
|
||||
{"s", "search"},
|
||||
{"p", "plug"},
|
||||
{"t", "set-connector-type"},
|
||||
{"e", "explore"},
|
||||
{"n", "net"},
|
||||
{"a", "analyze (verify + groups)"},
|
||||
{"PgUp", "scroll up"},
|
||||
{"PgDn", "scroll down"},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
@@ -14,6 +15,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <set>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
@@ -147,9 +149,31 @@ Component Tui::BuildExploreScreen() {
|
||||
}
|
||||
} else if (explore_type_idx == 1) {
|
||||
Signal *s = cur_mod->signals->get(cname);
|
||||
// BFS over Connection::pin_map to expose every (module,
|
||||
// signal) reachable through the system's connections.
|
||||
Net n = find_net(sys.get(), cur_mod, s);
|
||||
SignalType dom = SignalType::Other;
|
||||
bool consistent = net_type_consistent(n, dom);
|
||||
std::string net_hdr = "1 member (local)";
|
||||
if (n.members.size() >= 2) {
|
||||
net_hdr = std::to_string(n.members.size())
|
||||
+ " net members across "
|
||||
+ std::to_string([&]{
|
||||
std::set<Module*> mods;
|
||||
for (const auto &mp : n.members) mods.insert(mp.first);
|
||||
return mods.size();
|
||||
}())
|
||||
+ " module(s)"
|
||||
+ (consistent ? "" : " — INCONSISTENT");
|
||||
}
|
||||
explore_header = cur_mod->name + "/" + s->name
|
||||
+ " — " + std::to_string(s->size())
|
||||
+ " pins • type: " + signal_type_name(s->type);
|
||||
+ " pins • type: " + signal_type_name(s->type)
|
||||
+ " • " + net_hdr;
|
||||
|
||||
// Local pins first.
|
||||
explore_detail.push_back(" Local pins:");
|
||||
explore_detail_sig.push_back({});
|
||||
std::vector<std::string> rows;
|
||||
for (auto &pin_kv : *s) {
|
||||
Pin *pin = pin_kv.second;
|
||||
@@ -160,8 +184,36 @@ Component Tui::BuildExploreScreen() {
|
||||
}
|
||||
std::sort(rows.begin(), rows.end(), NaturalLess);
|
||||
for (const auto &r : rows)
|
||||
if (keep_detail(r))
|
||||
explore_detail.push_back(" " + r);
|
||||
if (keep_detail(r)) {
|
||||
explore_detail.push_back(" " + r);
|
||||
explore_detail_sig.push_back({});
|
||||
}
|
||||
|
||||
// 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).
|
||||
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;
|
||||
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));
|
||||
}
|
||||
std::sort(net_rows.begin(), net_rows.end(), NaturalLess);
|
||||
for (const auto &r : net_rows)
|
||||
if (keep_detail(r)) {
|
||||
explore_detail.push_back(" " + r);
|
||||
explore_detail_sig.push_back({});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Connection *c = sys->connections()->get(cname);
|
||||
std::string tname = c->transform_name.empty()
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Component Tui::BuildNetScreen() {
|
||||
InputOption filter_opt;
|
||||
filter_opt.multiline = false;
|
||||
filter_opt.transform = [](InputState s) {
|
||||
auto el = s.element;
|
||||
if (s.is_placeholder) el |= dim;
|
||||
return el;
|
||||
};
|
||||
auto filter_input = Input(&net_sig_filter, "filter signals…", filter_opt);
|
||||
auto module_menu = Menu(&net_modules, &net_module_idx);
|
||||
|
||||
MenuOption sig_opt = MenuOption::Vertical();
|
||||
sig_opt.entries = &net_sigs;
|
||||
sig_opt.selected = &net_sig_idx;
|
||||
sig_opt.on_enter = [this]() {
|
||||
if (net_modules.empty() || net_sigs.empty()) return;
|
||||
OpenSignalTypeDialog(net_modules[net_module_idx], net_sigs[net_sig_idx]);
|
||||
};
|
||||
auto signal_menu = Menu(sig_opt);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
Element help = RenderHelpPanel("net", {
|
||||
{"Tab", "cycle focus"},
|
||||
{"↑/↓", "navigate menu"},
|
||||
{"Enter", "set signal type"},
|
||||
{"Ctrl-P", "palette"},
|
||||
{"Esc", "dashboard"},
|
||||
});
|
||||
|
||||
return vbox({
|
||||
title,
|
||||
separator(),
|
||||
hbox({left, separator(), middle, separator(), right | flex,
|
||||
separator(), help}) | flex,
|
||||
}) | border;
|
||||
});
|
||||
}
|
||||
@@ -77,27 +77,30 @@ void Tui::ActivatePaletteEntry() {
|
||||
explore_detail_filter.clear();
|
||||
explore_detail_idx = 0;
|
||||
explore_focus_idx = 0;
|
||||
screen_idx = 4;
|
||||
screen_idx = 3;
|
||||
return;
|
||||
}
|
||||
if (kind == 's') {
|
||||
// payload = "module\tsignal" → jump to net screen prefilled.
|
||||
// payload = "module\tsignal" → jump to explore prefilled on the
|
||||
// signals tab, with the child filter seeded to the exact name so
|
||||
// the BFS net-members section appears in the detail pane.
|
||||
size_t tab = payload.find('\t');
|
||||
if (tab == std::string::npos || !sys) return;
|
||||
std::string mname = payload.substr(0, tab);
|
||||
std::string sname = payload.substr(tab + 1);
|
||||
net_modules.clear();
|
||||
for (auto &mk : *sys->modules()) net_modules.push_back(mk.first);
|
||||
std::sort(net_modules.begin(), net_modules.end(), NaturalLess);
|
||||
auto it = std::find(net_modules.begin(), net_modules.end(), mname);
|
||||
if (it == net_modules.end()) return;
|
||||
net_module_idx = (int)(it - net_modules.begin());
|
||||
// The net screen recomputes net_sigs every frame from the filter;
|
||||
// pre-seeding the filter to the exact name highlights the target.
|
||||
net_sig_filter = sname;
|
||||
net_sig_idx = 0;
|
||||
net_focus_idx = 2; // start focused on the signal menu
|
||||
screen_idx = 5;
|
||||
explore_modules.clear();
|
||||
for (auto &mk : *sys->modules()) explore_modules.push_back(mk.first);
|
||||
std::sort(explore_modules.begin(), explore_modules.end(), NaturalLess);
|
||||
auto it = std::find(explore_modules.begin(), explore_modules.end(), mname);
|
||||
if (it == explore_modules.end()) return;
|
||||
explore_module_idx = (int)(it - explore_modules.begin());
|
||||
explore_type_idx = 1; // signals tab
|
||||
explore_child_filter = sname;
|
||||
explore_child_idx = 0;
|
||||
explore_detail_filter.clear();
|
||||
explore_detail_idx = 0;
|
||||
explore_focus_idx = 3; // start on the children menu
|
||||
screen_idx = 3;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Component Tui::BuildSearchScreen() {
|
||||
InputOption query_opt;
|
||||
query_opt.multiline = false;
|
||||
query_opt.transform = [](InputState s) {
|
||||
auto el = s.element;
|
||||
if (s.is_placeholder) el |= dim;
|
||||
return el;
|
||||
};
|
||||
|
||||
auto query_input = Input(&search_query, "filter…", query_opt);
|
||||
auto module_menu = Menu(&search_modules, &search_module_idx);
|
||||
auto type_menu = Menu(&search_types, &search_type_idx);
|
||||
|
||||
auto components = Container::Vertical(
|
||||
{query_input, module_menu, type_menu}, &search_focus_idx);
|
||||
|
||||
return Renderer(components,
|
||||
[this, query_input, module_menu, type_menu] {
|
||||
std::vector<std::pair<std::string, size_t>> hits;
|
||||
if (!search_modules.empty() && sys) {
|
||||
const std::string &mname = search_modules[search_module_idx];
|
||||
try {
|
||||
Module *mod = sys->modules()->get(mname);
|
||||
std::string needle = ToLower(search_query);
|
||||
if (search_type_idx == 0) { // parts
|
||||
for (auto &pkv : *mod)
|
||||
if (needle.empty()
|
||||
|| ToLower(pkv.first).find(needle) != std::string::npos)
|
||||
hits.emplace_back(pkv.first, pkv.second->size());
|
||||
} else { // signals
|
||||
for (auto &skv : *mod->signals)
|
||||
if (needle.empty()
|
||||
|| ToLower(skv.first).find(needle) != std::string::npos)
|
||||
hits.emplace_back(skv.first, skv.second->size());
|
||||
}
|
||||
} catch (const std::exception &) {}
|
||||
}
|
||||
std::sort(hits.begin(), hits.end(),
|
||||
[](const auto &a, const auto &b) { return NaturalLess(a.first, b.first); });
|
||||
|
||||
Elements result_lines;
|
||||
for (const auto &h : hits)
|
||||
result_lines.push_back(
|
||||
text(" " + h.first + " (" + std::to_string(h.second) + " pins)"));
|
||||
|
||||
auto left = vbox({
|
||||
FocusLabel(text(" module "), search_focus_idx == 1) | bold,
|
||||
module_menu->Render() | yframe | flex,
|
||||
separator(),
|
||||
FocusLabel(text(" type "), search_focus_idx == 2) | bold,
|
||||
type_menu->Render(),
|
||||
}) | size(WIDTH, EQUAL, 28);
|
||||
|
||||
auto right = vbox({
|
||||
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,
|
||||
});
|
||||
|
||||
Element help = RenderHelpPanel("search", {
|
||||
{"Tab", "cycle focus"},
|
||||
{"↑/↓", "navigate menu"},
|
||||
{"Ctrl-P", "palette"},
|
||||
{"Esc", "dashboard"},
|
||||
});
|
||||
|
||||
return vbox({
|
||||
title,
|
||||
separator(),
|
||||
hbox({left, separator(), right | flex, separator(), help}) | flex,
|
||||
}) | border;
|
||||
});
|
||||
}
|
||||
@@ -13,10 +13,7 @@ Tui::Tui()
|
||||
loading(false), tick_in_flight(false),
|
||||
loading_idx(0), loading_executed(0), loading_lineno(0),
|
||||
loading_prev_in_source(false), screen_ptr(nullptr),
|
||||
screen_idx(6), // boot to the dashboard; shell (screen 0) is now a sub-screen
|
||||
|
||||
search_types{"parts", "signals"},
|
||||
search_module_idx(0), search_type_idx(0), search_focus_idx(0),
|
||||
screen_idx(4), // boot to the dashboard; console (screen 0) is now a sub-screen
|
||||
connect_m1_idx(0), connect_m2_idx(0),
|
||||
connect_p1_idx(0), connect_p2_idx(0),
|
||||
connect_focus_idx(0),
|
||||
@@ -24,7 +21,6 @@ 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();
|
||||
@@ -39,19 +35,16 @@ void Tui::Run() {
|
||||
screen_ptr = &screen;
|
||||
|
||||
auto main_screen = BuildMainScreen(screen);
|
||||
auto search_screen = BuildSearchScreen();
|
||||
auto connect_screen = BuildConnectScreen();
|
||||
auto settype_screen = BuildSettypeScreen();
|
||||
auto explore_screen = BuildExploreScreen() | Modal(BuildSignalTypeModal(),
|
||||
&sigtype_dialog_open);
|
||||
auto net_screen = BuildNetScreen() | Modal(BuildSignalTypeModal(),
|
||||
&sigtype_dialog_open);
|
||||
auto dashboard_screen = BuildDashboardScreen();
|
||||
auto analyze_screen = BuildAnalyzeScreen();
|
||||
|
||||
auto tab = Container::Tab(
|
||||
{main_screen, search_screen, connect_screen, settype_screen, explore_screen,
|
||||
net_screen, dashboard_screen, analyze_screen},
|
||||
{main_screen, connect_screen, settype_screen, explore_screen,
|
||||
dashboard_screen, analyze_screen},
|
||||
&screen_idx);
|
||||
|
||||
// Palette is a global Modal — overlays the tab on every screen.
|
||||
@@ -64,11 +57,11 @@ void Tui::Run() {
|
||||
// Ctrl-P opens the palette from any screen.
|
||||
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.
|
||||
switch (screen_idx) {
|
||||
case 7: // analyze
|
||||
if (e == Event::Escape) { screen_idx = 6; return true; }
|
||||
// Tab and ←/→ both switch the active tab. ↑/↓ stay with the
|
||||
// detail Menu so it can scroll.
|
||||
case 5: // analyze
|
||||
if (e == Event::Escape) { screen_idx = 4; return true; }
|
||||
if (e == Event::Tab || e == Event::ArrowRight) {
|
||||
analyze_focus_idx = (analyze_focus_idx + 1) % 3;
|
||||
return true;
|
||||
@@ -79,12 +72,8 @@ void Tui::Run() {
|
||||
}
|
||||
return false;
|
||||
|
||||
case 6: // dashboard (home)
|
||||
// Home has no parent — Esc is swallowed. Use 'q' to quit.
|
||||
case 4: // dashboard (home)
|
||||
if (e == Event::Escape) { return true; }
|
||||
// Scroll the dashboard when content overflows the viewport. The
|
||||
// upper bound is clamped inside the Renderer (we don't know the
|
||||
// line count from here).
|
||||
if (e == Event::PageDown) { dashboard_scroll_offset += 10; return true; }
|
||||
if (e == Event::PageUp) {
|
||||
dashboard_scroll_offset = std::max(0, dashboard_scroll_offset - 10);
|
||||
@@ -93,57 +82,36 @@ void Tui::Run() {
|
||||
if (e == Event::Home) { dashboard_scroll_offset = 0; return true; }
|
||||
if (e == Event::End) { dashboard_scroll_offset = 100000; return true; }
|
||||
if (e == Event::Character("q")) { Dispatch("quit"); return true; }
|
||||
// [c]onsole = the textual shell screen (former [l]og). [p]lug
|
||||
// = the `connect` command (UI rename only; the underlying
|
||||
// command stays `connect` for script + save/restore stability,
|
||||
// with `plug` registered as an alias so the palette finds it).
|
||||
if (e == Event::Character("c")) { screen_idx = 0; return true; }
|
||||
if (e == Event::Character("p")) { Dispatch("connect"); return true; }
|
||||
if (e == Event::Character("s")) { Dispatch("search"); 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("n")) { Dispatch("net"); return true; }
|
||||
// [a]nalyze is the unified verify + analyze screen (issues +
|
||||
// groups + types). The textual `verify` and `analyze` commands
|
||||
// still exist for scripts.
|
||||
if (e == Event::Character("a")) { screen_idx = 7; return true; }
|
||||
if (e == Event::Character("a")) { screen_idx = 5; return true; }
|
||||
return false;
|
||||
|
||||
case 5: // net
|
||||
if (e == Event::Escape) { screen_idx = 6; 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 = 6; return true; }
|
||||
case 3: // explore
|
||||
if (e == Event::Escape) { screen_idx = 4; return true; }
|
||||
if (e == Event::Tab) { explore_focus_idx = (explore_focus_idx + 1) % 6; return true; }
|
||||
if (e == Event::TabReverse) { explore_focus_idx = (explore_focus_idx + 5) % 6; return true; }
|
||||
return false;
|
||||
|
||||
case 3: // set-connector-type
|
||||
if (e == Event::Escape) { screen_idx = 6; return true; }
|
||||
case 2: // set-connector-type
|
||||
if (e == Event::Escape) { 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;
|
||||
|
||||
case 2: // connect
|
||||
if (e == Event::Escape) { screen_idx = 6; return true; }
|
||||
case 1: // connect
|
||||
if (e == Event::Escape) { screen_idx = 4; return true; }
|
||||
if (e == Event::Tab) { connect_focus_idx = (connect_focus_idx + 1) % 7; return true; }
|
||||
if (e == Event::TabReverse) { connect_focus_idx = (connect_focus_idx + 6) % 7; return true; }
|
||||
return false;
|
||||
|
||||
case 1: // search
|
||||
if (e == Event::Escape) { screen_idx = 6; return true; }
|
||||
if (e == Event::Tab) { search_focus_idx = (search_focus_idx + 1) % 3; return true; }
|
||||
if (e == Event::TabReverse) { search_focus_idx = (search_focus_idx + 2) % 3; return true; }
|
||||
return false;
|
||||
|
||||
default: // main (shell / log view)
|
||||
default: // 0: main (console / log view)
|
||||
if (e == Event::Special("\x02tick")) { ProcessNextSourceLine(); return true; }
|
||||
if (e == Event::Escape) {
|
||||
if (!pending.empty()) { CancelPending(); return true; }
|
||||
screen_idx = 6; return true;
|
||||
screen_idx = 4; return true;
|
||||
}
|
||||
if (e == Event::PageUp) { scroll_offset += 10; return true; }
|
||||
if (e == Event::PageDown) { scroll_offset = std::max(0, scroll_offset - 10); return true; }
|
||||
|
||||
@@ -56,14 +56,6 @@ class Tui {
|
||||
// ---- Screen orchestration ----
|
||||
int screen_idx;
|
||||
|
||||
// ---- Search screen state ----
|
||||
std::vector<std::string> search_modules;
|
||||
std::vector<std::string> search_types;
|
||||
int search_module_idx;
|
||||
int search_type_idx;
|
||||
int search_focus_idx;
|
||||
std::string search_query;
|
||||
|
||||
// ---- Connect screen state ----
|
||||
std::vector<std::string> connect_modules;
|
||||
int connect_m1_idx;
|
||||
@@ -102,14 +94,6 @@ class Tui {
|
||||
bool loading_prev_in_source;
|
||||
ftxui::ScreenInteractive *screen_ptr; ///< set in Run() so Source() can post events.
|
||||
|
||||
// ---- Net screen state ----
|
||||
std::vector<std::string> net_modules;
|
||||
int net_module_idx;
|
||||
std::string net_sig_filter;
|
||||
std::vector<std::string> net_sigs; ///< rebuilt every frame from filter
|
||||
int net_sig_idx;
|
||||
int net_focus_idx;
|
||||
|
||||
// ---- Dashboard scroll state (0 = top; grows as the user scrolls down) ----
|
||||
int dashboard_scroll_offset = 0;
|
||||
|
||||
@@ -197,11 +181,9 @@ private:
|
||||
|
||||
// Screen builders (screen_*.cpp)
|
||||
ftxui::Component BuildMainScreen(ftxui::ScreenInteractive &screen);
|
||||
ftxui::Component BuildSearchScreen();
|
||||
ftxui::Component BuildConnectScreen();
|
||||
ftxui::Component BuildSettypeScreen();
|
||||
ftxui::Component BuildExploreScreen();
|
||||
ftxui::Component BuildNetScreen();
|
||||
ftxui::Component BuildDashboardScreen();
|
||||
ftxui::Component BuildAnalyzeScreen();
|
||||
ftxui::Component BuildSignalTypeModal();
|
||||
|
||||
Reference in New Issue
Block a user