#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 #include #include #include #include #include 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"}, {"h", "help screen"}, {"q", "quit"}, {"Ctrl-P", "palette"}, }); if (!sys) { return vbox({ title, separator(), hbox({ vbox({ text(" no system loaded — run 'new' or 'restore '") | 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 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> 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 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> 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_suspect = 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_suspect; } if (n_pwr_ok + n_pwr_suspect + n_gnd > 0) { std::string label = "power: " + std::to_string(n_pwr_ok) + " confirmed, " + std::to_string(n_pwr_suspect) + " suspect gnd: " + std::to_string(n_gnd); auto el = text(" " + label); if (n_pwr_suspect > 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"}, {"p", "plug"}, {"e", "explore"}, {"a", "analyze (verify + groups)"}, {"h", "help screen"}, {"x", "export (CSV)"}, {"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; }); }