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

@@ -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},

View File

@@ -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"},

View File

@@ -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()

View File

@@ -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;
});
}

View File

@@ -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;
}
}

View File

@@ -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;
});
}

View File

@@ -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; }

View File

@@ -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();