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:
317
src/tui/screen_dashboard.cpp
Normal file
317
src/tui/screen_dashboard.cpp
Normal file
@@ -0,0 +1,317 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
namespace {
|
||||
|
||||
// Two-column key/value row, fixed-width key so columns line up.
|
||||
Element kv(const std::string &k, const std::string &v) {
|
||||
return hbox({
|
||||
text(" " + k) | dim | size(WIDTH, EQUAL, 16),
|
||||
text(v),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Component Tui::BuildDashboardScreen() {
|
||||
return Renderer([this] {
|
||||
auto title = hbox({
|
||||
text(" essim ") | bold,
|
||||
text("→ ") | dim,
|
||||
text("dashboard") | bold,
|
||||
text(" — system overview at a glance") | dim,
|
||||
});
|
||||
|
||||
Element early_help = RenderHelpPanel("dashboard", {
|
||||
{"c", "console"},
|
||||
{"a", "analyze"},
|
||||
{"q", "quit"},
|
||||
{"Ctrl-P", "palette"},
|
||||
});
|
||||
|
||||
if (!sys) {
|
||||
return vbox({
|
||||
title,
|
||||
separator(),
|
||||
hbox({
|
||||
vbox({
|
||||
text(" no system loaded — run 'new' or 'restore <file>'") | dim,
|
||||
text(" (press 'c' for the console, or Ctrl-P for the palette)") | dim,
|
||||
filler(),
|
||||
}) | flex,
|
||||
separator(),
|
||||
early_help,
|
||||
}) | flex,
|
||||
}) | border;
|
||||
}
|
||||
|
||||
// ---- counters ----
|
||||
int n_modules = (int)sys->modules()->size();
|
||||
int n_parts = 0, n_signals = 0;
|
||||
for (auto &mkv : *sys->modules()) {
|
||||
n_parts += (int)mkv.second->size();
|
||||
n_signals += (int)mkv.second->signals->size();
|
||||
}
|
||||
int n_conn = (int)sys->connections()->size();
|
||||
|
||||
// ---- verify-style health (recomputed; cheap on realistic sizes) ----
|
||||
int n_role_mismatches = 0, n_typed_pins = 0;
|
||||
for (auto &mkv : *sys->modules())
|
||||
for (auto &pkv : *mkv.second) {
|
||||
Part *prt = pkv.second;
|
||||
if (prt->connector_type.empty()) continue;
|
||||
for (auto &nkv : *prt) {
|
||||
Pin *pin = nkv.second;
|
||||
++n_typed_pins;
|
||||
SignalType expected = pin->expected_signal_type;
|
||||
if (expected == SignalType::Other) continue;
|
||||
Signal *s = pin->signal();
|
||||
SignalType actual = s ? s->type : SignalType::Other;
|
||||
if (actual != expected) ++n_role_mismatches;
|
||||
}
|
||||
}
|
||||
|
||||
auto nets = compute_all_nets(sys.get());
|
||||
int n_bridged = 0, n_inconsistent = 0;
|
||||
for (const auto &n : nets) {
|
||||
if (n.members.size() < 2) continue;
|
||||
++n_bridged;
|
||||
SignalType dom;
|
||||
if (!net_type_consistent(n, dom)) ++n_inconsistent;
|
||||
}
|
||||
|
||||
// ---- NC orphan summary (matches verify pass 3) ----
|
||||
std::unordered_set<Pin *> bridged_pins;
|
||||
for (auto &ckv : *sys->connections())
|
||||
for (auto &wp : ckv.second->pin_map) {
|
||||
if (wp.first) bridged_pins.insert(wp.first);
|
||||
if (wp.second) bridged_pins.insert(wp.second);
|
||||
}
|
||||
int orph_imported = 0, orph_dropped = 0;
|
||||
// Per-module list of dropped-singleton pins, for the detail rows below
|
||||
// the NC health line. The signal name is gone (the Signal object was
|
||||
// deleted by `drop_singleton_signals`), but the pin's full path is
|
||||
// enough to locate it in `explore`.
|
||||
std::map<std::string, std::vector<std::string>> dropped_by_module;
|
||||
for (auto &mkv : *sys->modules())
|
||||
for (auto &pkv : *mkv.second)
|
||||
for (auto &nkv : *pkv.second) {
|
||||
Pin *pin = nkv.second;
|
||||
if (pin->signal() || bridged_pins.count(pin)) continue;
|
||||
if (pin->nc_origin == NcOrigin::ImportedUnconnected) {
|
||||
++orph_imported;
|
||||
} else if (pin->nc_origin == NcOrigin::DroppedSingleton) {
|
||||
++orph_dropped;
|
||||
dropped_by_module[mkv.first].push_back(
|
||||
pkv.first + "/" + nkv.first);
|
||||
}
|
||||
}
|
||||
|
||||
auto health_line = [](bool ok, const std::string &s) {
|
||||
return hbox({
|
||||
text(ok ? " ✓ " : " ⚠ ") | (ok ? color(Color::Green) : color(Color::Yellow)),
|
||||
text(s),
|
||||
});
|
||||
};
|
||||
|
||||
Elements health_rows;
|
||||
health_rows.push_back(health_line(n_role_mismatches == 0,
|
||||
"verify: " + std::to_string(n_role_mismatches)
|
||||
+ " pin-role mismatch(es) over " + std::to_string(n_typed_pins)
|
||||
+ " typed pin(s)"));
|
||||
health_rows.push_back(health_line(n_inconsistent == 0,
|
||||
"nets: " + std::to_string(n_inconsistent) + " inconsistent over "
|
||||
+ std::to_string(n_bridged) + " bridged (" + std::to_string(nets.size())
|
||||
+ " total)"));
|
||||
int orph_total = orph_imported + orph_dropped;
|
||||
health_rows.push_back(health_line(orph_total == 0,
|
||||
"NC: " + std::to_string(orph_total) + " orphan pin(s) ("
|
||||
+ std::to_string(orph_imported) + " imported, "
|
||||
+ std::to_string(orph_dropped) + " dropped)"));
|
||||
|
||||
// ---- analysis summary ----
|
||||
AnalysisReport rep = analyze_system(sys.get());
|
||||
int n_diff = 0, n_diff_bus = 0, n_bus = 0;
|
||||
for (const auto &g : rep.groups) {
|
||||
if (g.kind == GroupKind::DiffPair) ++n_diff;
|
||||
else if (g.kind == GroupKind::DiffBus) ++n_diff_bus;
|
||||
else if (g.kind == GroupKind::Bus) ++n_bus;
|
||||
}
|
||||
|
||||
// ---- per-module table ----
|
||||
std::vector<std::string> mod_names;
|
||||
for (auto &mkv : *sys->modules()) mod_names.push_back(mkv.first);
|
||||
std::sort(mod_names.begin(), mod_names.end(), NaturalLess);
|
||||
size_t maxw = 1;
|
||||
for (const auto &n : mod_names) maxw = std::max(maxw, n.size());
|
||||
Elements mod_rows;
|
||||
for (const auto &name : mod_names) {
|
||||
Module *m = sys->modules()->get(name);
|
||||
int total = (int)m->size();
|
||||
|
||||
// Group parts by connector_type so the table answers "which type
|
||||
// is on which part?" rather than just "how many are typed?".
|
||||
std::map<std::string, std::vector<std::string>> by_type;
|
||||
for (auto &pkv : *m)
|
||||
if (!pkv.second->connector_type.empty())
|
||||
by_type[pkv.second->connector_type].push_back(pkv.first);
|
||||
|
||||
mod_rows.push_back(hbox({
|
||||
text(" " + name + std::string(maxw - name.size(), ' '))
|
||||
| size(WIDTH, EQUAL, (int)maxw + 4),
|
||||
text(std::to_string(total) + " part(s)") | size(WIDTH, EQUAL, 14),
|
||||
text(std::to_string(m->signals->size()) + " signal(s)"),
|
||||
}));
|
||||
|
||||
// Power-signal breakdown for this module — same classification as
|
||||
// the analyze screen so the dashboard summary stays consistent.
|
||||
int n_pwr_ok = 0, n_pwr_refuted = 0, n_gnd = 0;
|
||||
for (auto &skv : *m->signals) {
|
||||
Signal *s = skv.second;
|
||||
SignalType named = infer_signal_type(s->name);
|
||||
if (named == SignalType::GndShield && s->type == SignalType::GndShield) ++n_gnd;
|
||||
else if (named == SignalType::Power && s->type == SignalType::Power) ++n_pwr_ok;
|
||||
else if (named == SignalType::Power && s->type == SignalType::Other) ++n_pwr_refuted;
|
||||
}
|
||||
if (n_pwr_ok + n_pwr_refuted + n_gnd > 0) {
|
||||
std::string label = "power: " + std::to_string(n_pwr_ok)
|
||||
+ " confirmed, " + std::to_string(n_pwr_refuted)
|
||||
+ " refuted gnd: " + std::to_string(n_gnd);
|
||||
auto el = text(" " + label);
|
||||
if (n_pwr_refuted > 0) el = el | color(Color::Yellow);
|
||||
mod_rows.push_back(el);
|
||||
}
|
||||
|
||||
if (by_type.empty()) {
|
||||
mod_rows.push_back(hbox({
|
||||
text(" "),
|
||||
text("(no connector types assigned)") | dim,
|
||||
}));
|
||||
} else {
|
||||
for (auto &tkv : by_type) {
|
||||
std::sort(tkv.second.begin(), tkv.second.end(), NaturalLess);
|
||||
std::string parts_csv;
|
||||
for (size_t i = 0; i < tkv.second.size(); ++i) {
|
||||
if (i) parts_csv += ", ";
|
||||
parts_csv += tkv.second[i];
|
||||
}
|
||||
mod_rows.push_back(hbox({
|
||||
text(" "),
|
||||
text(tkv.first) | bold,
|
||||
text(": "),
|
||||
text(parts_csv),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten the dashboard into a list of lines so we can scroll it as a
|
||||
// whole when the content overflows. The pattern mirrors the shell's
|
||||
// scrollback: pick one focused line and wrap in `yframe`.
|
||||
Elements lines;
|
||||
lines.push_back(text(" Overview") | bold);
|
||||
lines.push_back(hbox({
|
||||
vbox({
|
||||
kv("Modules", std::to_string(n_modules)),
|
||||
kv("Parts", std::to_string(n_parts)),
|
||||
}) | flex,
|
||||
vbox({
|
||||
kv("Signals", std::to_string(n_signals)),
|
||||
kv("Connections", std::to_string(n_conn)),
|
||||
}) | flex,
|
||||
}));
|
||||
lines.push_back(separator());
|
||||
lines.push_back(text(" Health") | bold);
|
||||
for (auto &h : health_rows) lines.push_back(std::move(h));
|
||||
// Detail rows for the dropped-singleton NCs. Imported NCs are not
|
||||
// expanded — they were already explicit in the netlist. Dropped NCs
|
||||
// come from a heuristic, so listing them gives the user a chance to
|
||||
// spot a false positive.
|
||||
if (orph_dropped > 0) {
|
||||
lines.push_back(hbox({
|
||||
text(" dropped detail:") | dim,
|
||||
}));
|
||||
for (auto &dkv : dropped_by_module) {
|
||||
std::sort(dkv.second.begin(), dkv.second.end(), NaturalLess);
|
||||
std::string csv;
|
||||
for (size_t i = 0; i < dkv.second.size(); ++i) {
|
||||
if (i) csv += ", ";
|
||||
csv += dkv.second[i];
|
||||
}
|
||||
lines.push_back(hbox({
|
||||
text(" " + dkv.first + ": ") | bold,
|
||||
text(csv),
|
||||
}));
|
||||
}
|
||||
}
|
||||
lines.push_back(separator());
|
||||
lines.push_back(text(" Analysis") | bold);
|
||||
lines.push_back(hbox({text(" • ") | dim,
|
||||
text(std::to_string(n_diff) + " diff pair(s)")}));
|
||||
lines.push_back(hbox({text(" • ") | dim,
|
||||
text(std::to_string(n_diff_bus) + " diff bus(es)")}));
|
||||
lines.push_back(hbox({text(" • ") | dim,
|
||||
text(std::to_string(n_bus) + " bus(es)")}));
|
||||
lines.push_back(hbox({text(" • ") | dim,
|
||||
rep.anomalies.empty()
|
||||
? text(std::to_string(rep.anomalies.size()) + " anomaly(ies)")
|
||||
: text(std::to_string(rep.anomalies.size()) + " anomaly(ies)")
|
||||
| color(Color::Yellow)}));
|
||||
lines.push_back(separator());
|
||||
lines.push_back(text(" Modules") | bold);
|
||||
for (auto &r : mod_rows) lines.push_back(std::move(r));
|
||||
|
||||
// Clamp scroll, mark a focused line so `yframe` positions the view.
|
||||
int line_count = (int)lines.size();
|
||||
if (dashboard_scroll_offset < 0) dashboard_scroll_offset = 0;
|
||||
if (dashboard_scroll_offset > line_count - 1)
|
||||
dashboard_scroll_offset = std::max(0, line_count - 1);
|
||||
lines[dashboard_scroll_offset] = lines[dashboard_scroll_offset] | focus;
|
||||
|
||||
Element main_col = vbox(std::move(lines))
|
||||
| vscroll_indicator
|
||||
| yframe
|
||||
| flex;
|
||||
|
||||
Element help = RenderHelpPanel("dashboard", {
|
||||
{"c", "console"},
|
||||
{"s", "search"},
|
||||
{"p", "plug"},
|
||||
{"t", "set-type"},
|
||||
{"e", "explore"},
|
||||
{"n", "net"},
|
||||
{"a", "analyze (verify + groups)"},
|
||||
{"PgUp", "scroll up"},
|
||||
{"PgDn", "scroll down"},
|
||||
{"Home", "scroll top"},
|
||||
{"End", "scroll bottom"},
|
||||
{"Ctrl-P", "palette"},
|
||||
{"q", "quit"},
|
||||
});
|
||||
|
||||
return vbox({
|
||||
title,
|
||||
separator(),
|
||||
hbox({main_col, separator(), help}) | flex,
|
||||
}) | border;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user