Signal-type popup, NC pin tagging, interactive viewer hygiene.
- Enter on a signal entry (net / explore) opens a modal popup to pick power / gnd / other. Recording is deduped: a sequence of toggles on the same signal collapses to a single `set-signal-type` line; no-op selections record nothing. - Bare interactive commands (the ones that open a full-screen mode) are no longer recorded by `script-save`. Their inline forms still are. Mutating actions inside a screen record their own canonical line. - Mentor importer treats signals whose name starts with `unconnected` as no-connect — the pin is kept on the part without a signal and tagged `ImportedUnconnected`. - `drop_singleton_signals` runs at the end of `load`: any signal with exactly one pin is detached (singletons are NC by definition); the pin is tagged `DroppedSingleton`. Count is reported inline. - `verify` gains a one-line orphan summary (imported NC / dropped singleton totals). Pins materialised by `FillIdentityNCs` are excluded via a `pin_map` filter — they are bridged to a real signal on the peer module and are not real NCs at system level. - NcOrigin tag is serialized in save snapshots as an optional 4th field on N records (backward-compatible). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
void Tui::RegisterCommands() {
|
||||
@@ -132,9 +133,12 @@ void Tui::RegisterCommands() {
|
||||
try {
|
||||
sys->Load(args[0], args[1], t);
|
||||
Module *mod = sys->modules()->get(args[0]);
|
||||
int dropped = drop_singleton_signals(mod->signals);
|
||||
Print("loaded '" + args[0] + "' from " + args[1]);
|
||||
Print(" parts: " + std::to_string(mod->size()));
|
||||
Print(" signals: " + std::to_string(mod->signals->size()));
|
||||
Print(" signals: " + std::to_string(mod->signals->size())
|
||||
+ (dropped ? " (dropped " + std::to_string(dropped)
|
||||
+ " singleton/NC signal(s))" : ""));
|
||||
} catch (const std::exception &e) {
|
||||
Print(std::string("load failed: ") + e.what());
|
||||
}
|
||||
@@ -249,6 +253,30 @@ void Tui::RegisterCommands() {
|
||||
Print("verify: " + std::to_string(inconsistent) + " inconsistent net(s) over "
|
||||
+ std::to_string(bridged) + " bridged net(s) ("
|
||||
+ std::to_string(nets.size()) + " total).");
|
||||
|
||||
// Orphan pin report. A pin is "orphan" if it came out of import (or
|
||||
// post-import drop) with no signal, and is still not bridged to a
|
||||
// real signal via any Connection::pin_map. Use `nc-export` for the
|
||||
// per-pin list.
|
||||
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;
|
||||
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;
|
||||
}
|
||||
Print("verify: " + std::to_string(orph_imported + orph_dropped)
|
||||
+ " orphan pin(s) at import ("
|
||||
+ std::to_string(orph_imported) + " imported NC, "
|
||||
+ std::to_string(orph_dropped) + " dropped singleton).");
|
||||
}, true,
|
||||
"check pin roles locally and signal-type consistency across bridged nets" };
|
||||
|
||||
@@ -653,6 +681,7 @@ void Tui::RegisterCommands() {
|
||||
Pin *sn = nkv.second;
|
||||
Pin *dn = new Pin(sn->name);
|
||||
dn->expected_signal_type = sn->expected_signal_type;
|
||||
dn->nc_origin = sn->nc_origin;
|
||||
dp->add(dn);
|
||||
if (sn->signal()) {
|
||||
Signal *ds = dst->signals->get(sn->signal()->name);
|
||||
|
||||
@@ -29,9 +29,30 @@ Component Tui::BuildExploreScreen() {
|
||||
auto module_menu = Menu(&explore_modules, &explore_module_idx);
|
||||
auto type_menu = Menu(&explore_types, &explore_type_idx);
|
||||
auto child_filter = Input(&explore_child_filter, "filter…", pf_opt);
|
||||
auto children_menu = Menu(&explore_children, &explore_child_idx);
|
||||
|
||||
MenuOption child_opt = MenuOption::Vertical();
|
||||
child_opt.entries = &explore_children;
|
||||
child_opt.selected = &explore_child_idx;
|
||||
child_opt.on_enter = [this]() {
|
||||
if (explore_type_idx != 1 || explore_children.empty()) return;
|
||||
OpenSignalTypeDialog(explore_modules[explore_module_idx],
|
||||
explore_children[explore_child_idx]);
|
||||
};
|
||||
auto children_menu = Menu(child_opt);
|
||||
|
||||
auto detail_filter = Input(&explore_detail_filter, "filter…", pf_opt);
|
||||
auto detail_menu = Menu(&explore_detail, &explore_detail_idx);
|
||||
|
||||
MenuOption detail_opt = MenuOption::Vertical();
|
||||
detail_opt.entries = &explore_detail;
|
||||
detail_opt.selected = &explore_detail_idx;
|
||||
detail_opt.on_enter = [this]() {
|
||||
if (explore_detail_idx < 0
|
||||
|| explore_detail_idx >= (int)explore_detail_sig.size()) return;
|
||||
const std::string &sig = explore_detail_sig[explore_detail_idx];
|
||||
if (sig.empty()) return;
|
||||
OpenSignalTypeDialog(explore_modules[explore_module_idx], sig);
|
||||
};
|
||||
auto detail_menu = Menu(detail_opt);
|
||||
|
||||
auto components = Container::Vertical(
|
||||
{module_menu, type_menu, child_filter, children_menu, detail_filter, detail_menu},
|
||||
@@ -93,6 +114,7 @@ Component Tui::BuildExploreScreen() {
|
||||
// bound to it can scroll with arrow keys when focused).
|
||||
explore_header = "(no system)";
|
||||
explore_detail.clear();
|
||||
explore_detail_sig.clear();
|
||||
if (cur_mod && !explore_children.empty()) {
|
||||
const std::string &cname = explore_children[explore_child_idx];
|
||||
try {
|
||||
@@ -116,8 +138,12 @@ Component Tui::BuildExploreScreen() {
|
||||
std::string line = " " + r.first
|
||||
+ std::string(maxw - r.first.size() + 2, ' ')
|
||||
+ r.second;
|
||||
if (keep_detail(line))
|
||||
if (keep_detail(line)) {
|
||||
explore_detail.push_back(line);
|
||||
// "(NC)" → no underlying Signal to retype.
|
||||
explore_detail_sig.push_back(
|
||||
r.second == "(NC)" ? std::string{} : r.second);
|
||||
}
|
||||
}
|
||||
} else if (explore_type_idx == 1) {
|
||||
Signal *s = cur_mod->signals->get(cname);
|
||||
@@ -165,6 +191,9 @@ Component Tui::BuildExploreScreen() {
|
||||
}
|
||||
|
||||
if (explore_detail.empty()) explore_detail.push_back("(empty)");
|
||||
// Pad the parallel sig vector so any index into explore_detail is safe.
|
||||
while (explore_detail_sig.size() < explore_detail.size())
|
||||
explore_detail_sig.push_back({});
|
||||
if (explore_detail_idx < 0
|
||||
|| explore_detail_idx >= (int)explore_detail.size()) {
|
||||
explore_detail_idx = 0;
|
||||
@@ -207,7 +236,7 @@ Component Tui::BuildExploreScreen() {
|
||||
title,
|
||||
separator(),
|
||||
hbox({col1, separator(), col2, separator(), col3, separator(), col4}) | flex,
|
||||
text(" Tab: cycle focus (incl. detail to scroll) | Esc: leave explore ") | dim,
|
||||
text(" Tab: cycle focus | Enter (on a signal): set signal type | Esc: leave ") | dim,
|
||||
}) | border;
|
||||
} catch (const std::exception &e) {
|
||||
return vbox({
|
||||
|
||||
@@ -25,7 +25,15 @@ Component Tui::BuildNetScreen() {
|
||||
};
|
||||
auto filter_input = Input(&net_sig_filter, "filter signals…", filter_opt);
|
||||
auto module_menu = Menu(&net_modules, &net_module_idx);
|
||||
auto signal_menu = Menu(&net_sigs, &net_sig_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);
|
||||
@@ -113,7 +121,7 @@ Component Tui::BuildNetScreen() {
|
||||
title,
|
||||
separator(),
|
||||
hbox({left, separator(), middle, separator(), right}) | flex,
|
||||
text(" Tab: cycle focus (filter ↔ module ↔ signal) | Esc: leave net ") | dim,
|
||||
text(" Tab: cycle focus | Enter (on signal): set signal type | Esc: leave ") | dim,
|
||||
}) | border;
|
||||
});
|
||||
}
|
||||
|
||||
35
src/tui/screen_sigtype_modal.cpp
Normal file
35
src/tui/screen_sigtype_modal.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "tui/tui.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/component/event.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Component Tui::BuildSignalTypeModal() {
|
||||
sigtype_dialog_entries = {"power", "gnd", "other"};
|
||||
|
||||
MenuOption opt = MenuOption::Vertical();
|
||||
opt.entries = &sigtype_dialog_entries;
|
||||
opt.selected = &sigtype_dialog_choice;
|
||||
opt.on_enter = [this]() { ApplySignalTypeChoice(); };
|
||||
auto menu = Menu(opt);
|
||||
|
||||
auto with_esc = CatchEvent(menu, [this](Event e) {
|
||||
if (e == Event::Escape) { sigtype_dialog_open = false; return true; }
|
||||
return false;
|
||||
});
|
||||
|
||||
return Renderer(with_esc, [this, menu] {
|
||||
return vbox({
|
||||
text(" signal type ") | bold,
|
||||
separator(),
|
||||
text(sigtype_dialog_mod + "/" + sigtype_dialog_sig) | dim,
|
||||
separator(),
|
||||
menu->Render(),
|
||||
separator(),
|
||||
text(" ↑/↓ select • Enter apply • Esc cancel ") | dim,
|
||||
}) | border | size(WIDTH, GREATER_THAN, 32);
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <ostream>
|
||||
@@ -11,6 +16,55 @@
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
|
||||
void Tui::OpenSignalTypeDialog(const std::string &mod_name,
|
||||
const std::string &sig_name) {
|
||||
if (!sys) return;
|
||||
Signal *sig = nullptr;
|
||||
try {
|
||||
Module *m = sys->modules()->get(mod_name);
|
||||
sig = m->signals->get(sig_name);
|
||||
} catch (const std::exception &) { return; }
|
||||
|
||||
sigtype_dialog_mod = mod_name;
|
||||
sigtype_dialog_sig = sig_name;
|
||||
switch (sig->type) {
|
||||
case SignalType::Power: sigtype_dialog_choice = 0; break;
|
||||
case SignalType::GndShield: sigtype_dialog_choice = 1; break;
|
||||
default: sigtype_dialog_choice = 2; break;
|
||||
}
|
||||
sigtype_dialog_open = true;
|
||||
}
|
||||
|
||||
void Tui::ApplySignalTypeChoice() {
|
||||
sigtype_dialog_open = false;
|
||||
if (!sys) return;
|
||||
SignalType t;
|
||||
switch (sigtype_dialog_choice) {
|
||||
case 0: t = SignalType::Power; break;
|
||||
case 1: t = SignalType::GndShield; break;
|
||||
default: t = SignalType::Other; break;
|
||||
}
|
||||
Signal *sig = nullptr;
|
||||
try {
|
||||
Module *m = sys->modules()->get(sigtype_dialog_mod);
|
||||
sig = m->signals->get(sigtype_dialog_sig);
|
||||
} catch (const std::exception &) { return; }
|
||||
if (sig->type == t) return; // no-op, no record
|
||||
sig->type = t;
|
||||
if (in_source) return;
|
||||
|
||||
// Dedup: if the immediately previous recorded line targets the same
|
||||
// signal, replace it so a sequence of toggles collapses to one line.
|
||||
std::string line = "set-signal-type " + sigtype_dialog_mod + " "
|
||||
+ sigtype_dialog_sig + " " + signal_type_name(t);
|
||||
std::string prefix = "set-signal-type " + sigtype_dialog_mod + " "
|
||||
+ sigtype_dialog_sig + " ";
|
||||
if (!recorded.empty() && recorded.back().rfind(prefix, 0) == 0)
|
||||
recorded.back() = std::move(line);
|
||||
else
|
||||
recorded.push_back(std::move(line));
|
||||
}
|
||||
|
||||
void Tui::Print(const std::string &line) {
|
||||
output.push_back(line);
|
||||
scroll_offset = 0; // any new line snaps the view back to the tail
|
||||
@@ -136,7 +190,12 @@ void Tui::Finalize(const std::string &name,
|
||||
static const std::set<std::string> no_record = {
|
||||
"clear", "help", "quit", "exit", "source", "script-save",
|
||||
};
|
||||
if (spec.scriptable && !no_record.count(name)) recorded.push_back(canonical);
|
||||
// A bare invocation of an `interactive` command opens a full-screen mode
|
||||
// rather than mutating state — skip it. Any mutating action taken inside
|
||||
// that screen records its own canonical line via the action callbacks.
|
||||
bool opens_screen = spec.interactive && args.empty();
|
||||
if (spec.scriptable && !opens_screen && !no_record.count(name))
|
||||
recorded.push_back(canonical);
|
||||
}
|
||||
|
||||
std::string Tui::ExpandVars(const std::string &s) const {
|
||||
|
||||
@@ -41,8 +41,10 @@ void Tui::Run() {
|
||||
auto search_screen = BuildSearchScreen();
|
||||
auto connect_screen = BuildConnectScreen();
|
||||
auto settype_screen = BuildSettypeScreen();
|
||||
auto explore_screen = BuildExploreScreen();
|
||||
auto net_screen = BuildNetScreen();
|
||||
auto explore_screen = BuildExploreScreen() | Modal(BuildSignalTypeModal(),
|
||||
&sigtype_dialog_open);
|
||||
auto net_screen = BuildNetScreen() | Modal(BuildSignalTypeModal(),
|
||||
&sigtype_dialog_open);
|
||||
|
||||
auto tab = Container::Tab(
|
||||
{main_screen, search_screen, connect_screen, settype_screen, explore_screen,
|
||||
@@ -50,6 +52,10 @@ void Tui::Run() {
|
||||
&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;
|
||||
|
||||
switch (screen_idx) {
|
||||
case 5: // net
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
|
||||
@@ -86,6 +86,7 @@ class Tui {
|
||||
std::string explore_child_filter;
|
||||
std::string explore_detail_filter;
|
||||
std::vector<std::string> explore_detail;
|
||||
std::vector<std::string> explore_detail_sig; ///< parallel: signal name per detail line (empty = no signal)
|
||||
int explore_detail_idx;
|
||||
std::string explore_header;
|
||||
int explore_focus_idx;
|
||||
@@ -109,6 +110,13 @@ class Tui {
|
||||
int net_sig_idx;
|
||||
int net_focus_idx;
|
||||
|
||||
// ---- Signal-type popup (shared between net + explore screens) ----
|
||||
bool sigtype_dialog_open = false;
|
||||
std::string sigtype_dialog_mod;
|
||||
std::string sigtype_dialog_sig;
|
||||
std::vector<std::string> sigtype_dialog_entries; ///< ["power","gnd","other"]
|
||||
int sigtype_dialog_choice = 0;
|
||||
|
||||
// ---- Set-type screen state ----
|
||||
std::vector<std::string> settype_modules;
|
||||
int settype_m_idx;
|
||||
@@ -150,6 +158,14 @@ private:
|
||||
void CompletePath(size_t start = 0);
|
||||
void CompleteInline();
|
||||
|
||||
// Open the signal-type popup for <mod>/<sig> (no-op if names don't resolve).
|
||||
void OpenSignalTypeDialog(const std::string &mod_name,
|
||||
const std::string &sig_name);
|
||||
// Apply the selected type to the targeted signal, record a
|
||||
// `set-signal-type` line (deduping consecutive edits of the same signal),
|
||||
// and close the popup.
|
||||
void ApplySignalTypeChoice();
|
||||
|
||||
// Filtered part list rebuild (used by connect & set-type screens)
|
||||
void RefreshFilteredPartList(const std::vector<std::string> &modules,
|
||||
int m_idx,
|
||||
@@ -164,6 +180,7 @@ private:
|
||||
ftxui::Component BuildSettypeScreen();
|
||||
ftxui::Component BuildExploreScreen();
|
||||
ftxui::Component BuildNetScreen();
|
||||
ftxui::Component BuildSignalTypeModal();
|
||||
};
|
||||
|
||||
#endif // _TUI_HPP_
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
|
||||
std::string ToLower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
Reference in New Issue
Block a user