Dashboard + palette + analyze screen; consolidated categorization rules.
UI restructuring:
- Dashboard (`screen_dashboard.cpp`, `screen_idx = 6`) is the new home
screen at boot. Reads Overview / Health / Analysis / Modules from
the current System every frame; per-module rows list parts grouped
by `connector_type` and a Power/Gnd inference summary (yellow when
any name-Power signal is refuted). Scrollable via PgUp/PgDn/Home/End.
Letter shortcuts: `c`=console, `s`=search, `p`=plug (alias of
connect), `t`=set-type, `e`=explore, `n`=net, `a`=analyze, `q`=quit.
- Global Ctrl-P palette (`screen_palette.cpp`) — fuzzy-finds over
registered commands + module / signal names. Activation runs the
bare command or jumps to the matching screen with state seeded.
- Unified analyze screen (`screen_analyze.cpp`, `screen_idx = 7`):
tabbed layout (`Issues / Groups / Types`), Tab or ←→ to switch
tabs, ↑/↓ to navigate the focused list. Replaces the previous
shell-bouncing `[v]erify` shortcut — `verify` content is now in
the Issues tab. Types tab attaches the decision rationale to each
signal row (fan-out / voltage / hard floor).
- Context help panel: `RenderHelpPanel(title, entries)` in
`tui_helpers.{hpp,cpp}` rendered on the right of every screen.
- Console (former "log") rename: screen 0 is `[c]onsole` in the UI
and "console" in its help-panel title. The underlying screen and
the shell prompt are unchanged.
- Esc from any non-home screen returns to the dashboard. The
dashboard itself swallows Esc; quit via `q` / the `quit` command.
`quit` now calls `screen_ptr->Exit()` directly so it works from
any screen including via the palette.
Signal type inference:
- `Signal::type` defaults to `Other` — auto-inference no longer
happens at construction.
- `infer_signal_types(System*)` is called at the end of every load.
Three rules: GndShield from name alone; Power requires name match
+ a hard fan-out floor (< 3 pins = always Other, regardless of
name or voltage) + at least one positive structural signal
(fan-out ≥ 4 OR voltage pattern in the name like `3V3`, `5V`).
- Thresholds exposed in `analysis.hpp` (`POWER_FANOUT_HARD_FLOOR`,
`POWER_FANOUT_CONFIRM_MIN`, `has_voltage_pattern`) so the analyze
screen can render the same rationale without duplicating logic.
- `set-signal-type` still wins; save/restore round-trips the type.
Analysis groups & anomalies:
- New `GroupKind::DiffBus` — ≥ 2 diff pairs sharing the same
outer-stem with consecutive integer indices are aggregated into a
single bus (`MDI[0..3]_P/N`). `MDI0` and `PCIE_TX_0` index forms
both accepted. Solo pairs under a bus-able stem fall back to
`DiffPair`.
- New `AnomalyKind::DiffBusGap` for missing lanes.
Documentation:
- `DESIGN.md`: dedicated "Categorization rules (normative)" section
consolidating signal type, NC origin, signal groups, anomalies,
component kind, and connector wiring rules with exact thresholds
and decision order.
- `doc/user/analysis.md` (new): user-facing version of the same
rules in plain language. Linked from `doc/user/index.md`.
Tests: +6 new cases (62 total). Adjusted `test_persist.cpp` to set
the signal type explicitly in the fixture (no more auto-inference).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,8 @@ 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(0),
|
||||
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),
|
||||
connect_m1_idx(0), connect_m2_idx(0),
|
||||
@@ -37,59 +38,113 @@ void Tui::Run() {
|
||||
auto screen = ScreenInteractive::Fullscreen();
|
||||
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 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},
|
||||
net_screen, dashboard_screen, analyze_screen},
|
||||
&screen_idx);
|
||||
|
||||
auto root = CatchEvent(tab, [this](Event e) {
|
||||
// The signal-type popup must own Escape / Tab while it's open so the
|
||||
// outer switch doesn't yank us back to the main screen.
|
||||
if (sigtype_dialog_open) return false;
|
||||
// Palette is a global Modal — overlays the tab on every screen.
|
||||
auto with_palette = tab | Modal(BuildPaletteModal(), &palette_open);
|
||||
|
||||
auto root = CatchEvent(with_palette, [this](Event e) {
|
||||
// Modals (palette + sigtype popup) own their events while open.
|
||||
if (palette_open || sigtype_dialog_open) return false;
|
||||
|
||||
// Ctrl-P opens the palette from any screen.
|
||||
if (e == Event::CtrlP) { OpenPalette(); return true; }
|
||||
|
||||
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.
|
||||
if (e == Event::Tab || e == Event::ArrowRight) {
|
||||
analyze_focus_idx = (analyze_focus_idx + 1) % 3;
|
||||
return true;
|
||||
}
|
||||
if (e == Event::TabReverse || e == Event::ArrowLeft) {
|
||||
analyze_focus_idx = (analyze_focus_idx + 2) % 3;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case 6: // dashboard (home)
|
||||
// Home has no parent — Esc is swallowed. Use 'q' to quit.
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
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-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; }
|
||||
return false;
|
||||
|
||||
case 5: // net
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
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 = 0; return true; }
|
||||
if (e == Event::Escape) { screen_idx = 6; 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-type
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
if (e == Event::Escape) { screen_idx = 6; 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 = 0; return true; }
|
||||
if (e == Event::Escape) { screen_idx = 6; 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 = 0; return true; }
|
||||
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
|
||||
default: // main (shell / log view)
|
||||
if (e == Event::Special("\x02tick")) { ProcessNextSourceLine(); return true; }
|
||||
if (e == Event::Escape && !pending.empty()) { CancelPending(); return true; }
|
||||
if (e == Event::Escape) {
|
||||
if (!pending.empty()) { CancelPending(); return true; }
|
||||
screen_idx = 6; 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; }
|
||||
if (e == Event::Home) { scroll_offset = (int)output.size(); return true; }
|
||||
|
||||
Reference in New Issue
Block a user