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:
2026-05-14 21:49:26 +02:00
parent 7145577df7
commit 792e4745d3
11 changed files with 109 additions and 463 deletions

View File

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