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:
@@ -2,12 +2,26 @@
|
||||
#include "system/pins.hpp"
|
||||
#include "system/parts.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <regex>
|
||||
|
||||
using namespace std;
|
||||
|
||||
// Mentor netlists tag a no-connect pin with a signal name starting with
|
||||
// 'unconnected' (e.g. 'unconnected', 'unconnected (by TERM)'). We keep the
|
||||
// pin on the part but leave it unattached — it surfaces as `(NC)` in
|
||||
// `explore` and is excluded from every net.
|
||||
static bool is_nc_signal_name(const string &s) {
|
||||
static const string nc = "unconnected";
|
||||
if (s.size() < nc.size()) return false;
|
||||
for (size_t i = 0; i < nc.size(); ++i)
|
||||
if (std::tolower(static_cast<unsigned char>(s[i])) != nc[i])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enum representing the parsing state.
|
||||
*
|
||||
@@ -104,11 +118,15 @@ void ImportMentor::parse(Signals *signals)
|
||||
if (is_name_match)
|
||||
{
|
||||
auto pin = new Pin(names[0]); // Create a new pin with the first name.
|
||||
Signal *s = nullptr;
|
||||
prt->add(pin); // Add the pin to the current part.
|
||||
s = signals->merge(names[2]); // Merge the signal with module signals.
|
||||
s->add(pin); // Add the pin to the signal pins list.
|
||||
pin->connect(s); // Connect the pin to a signal.
|
||||
if (names.size() > 2 && !is_nc_signal_name(names[2])) {
|
||||
Signal *s = signals->merge(names[2]);
|
||||
s->add(pin);
|
||||
pin->connect(s);
|
||||
} else {
|
||||
// NC pin — kept on the part with no signal, tagged.
|
||||
pin->nc_origin = NcOrigin::ImportedUnconnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -46,7 +46,10 @@ bool save_system(const System *sys, const std::string &filename, std::string &er
|
||||
for (auto &nkv : *p) {
|
||||
Pin *pin = nkv.second;
|
||||
Signal *s = pin->signal();
|
||||
f << "N\t" << pin->name << "\t" << (s ? s->name : "") << "\n";
|
||||
f << "N\t" << pin->name << "\t" << (s ? s->name : "");
|
||||
const char *otag = nc_origin_tag(pin->nc_origin);
|
||||
if (*otag) f << "\t" << otag;
|
||||
f << "\n";
|
||||
}
|
||||
}
|
||||
// Signal types: only persist non-default (Other) overrides.
|
||||
@@ -127,6 +130,9 @@ System *restore_system(const std::string &filename, std::string &error)
|
||||
Signal *s = cur_mod->signals->merge(fs[2]);
|
||||
s->add(pin);
|
||||
pin->connect(s);
|
||||
} else if (fs.size() >= 4) {
|
||||
NcOrigin o;
|
||||
if (nc_origin_from_tag(fs[3], o)) pin->nc_origin = o;
|
||||
}
|
||||
} else if (tag == "S") {
|
||||
if (!cur_mod) return fail("S outside module");
|
||||
|
||||
@@ -6,6 +6,21 @@ Pin::Pin(std::string name)
|
||||
: SystemElement(name), sig(nullptr), prnt(nullptr),
|
||||
expected_signal_type(SignalType::Other) {};
|
||||
|
||||
const char *nc_origin_tag(NcOrigin o) {
|
||||
switch (o) {
|
||||
case NcOrigin::ImportedUnconnected: return "U";
|
||||
case NcOrigin::DroppedSingleton: return "D";
|
||||
case NcOrigin::None: return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool nc_origin_from_tag(const std::string &tag, NcOrigin &out) {
|
||||
if (tag == "U") { out = NcOrigin::ImportedUnconnected; return true; }
|
||||
if (tag == "D") { out = NcOrigin::DroppedSingleton; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Pin::connected()
|
||||
{
|
||||
return sig != nullptr;
|
||||
|
||||
@@ -10,6 +10,19 @@ class Part;
|
||||
#pragma once
|
||||
class Signal;
|
||||
|
||||
// Why a pin has no Signal attached. Set at import/post-load; preserved by
|
||||
// save/restore. Pins materialised by FillIdentityNCs keep `None` — they have
|
||||
// no local signal but are bridged via a Connection::pin_map and verify is
|
||||
// expected to filter them out via that bridge, not via this tag.
|
||||
enum class NcOrigin {
|
||||
None, ///< pin->sig != nullptr
|
||||
ImportedUnconnected, ///< Mentor 'unconnected*' encountered at parse
|
||||
DroppedSingleton, ///< drop_singleton_signals at end of load
|
||||
};
|
||||
|
||||
const char *nc_origin_tag(NcOrigin o); ///< "", "U", "D"
|
||||
bool nc_origin_from_tag(const std::string &tag, NcOrigin &out);
|
||||
|
||||
class Pin : public SystemElement
|
||||
{
|
||||
Signal *sig;
|
||||
@@ -17,6 +30,7 @@ public:
|
||||
Pin(std::string name);
|
||||
Part *prnt; ///< Pointer to the parent part.
|
||||
SignalType expected_signal_type; ///< Set from connector_type at set-type.
|
||||
NcOrigin nc_origin = NcOrigin::None;
|
||||
bool connected();
|
||||
Signal *signal() const { return sig; }
|
||||
void connect(Signal *signal);
|
||||
|
||||
@@ -8,5 +8,6 @@ enum class SignalType { Power, GndShield, Other };
|
||||
const char *signal_type_name(SignalType t);
|
||||
bool signal_type_from_name(const std::string &s, SignalType &out);
|
||||
SignalType infer_signal_type(const std::string &signal_name);
|
||||
SignalType next_signal_type(SignalType t); // Power → GndShield → Other → Power
|
||||
|
||||
#endif // _SIGNAL_TYPE_HPP_
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
const char *signal_type_name(SignalType t) {
|
||||
switch (t) {
|
||||
@@ -15,6 +15,15 @@ const char *signal_type_name(SignalType t) {
|
||||
return "other";
|
||||
}
|
||||
|
||||
SignalType next_signal_type(SignalType t) {
|
||||
switch (t) {
|
||||
case SignalType::Power: return SignalType::GndShield;
|
||||
case SignalType::GndShield: return SignalType::Other;
|
||||
case SignalType::Other: return SignalType::Power;
|
||||
}
|
||||
return SignalType::Other;
|
||||
}
|
||||
|
||||
bool signal_type_from_name(const std::string &s, SignalType &out) {
|
||||
std::string l = s;
|
||||
std::transform(l.begin(), l.end(), l.begin(),
|
||||
@@ -83,6 +92,23 @@ Signals::~Signals() {
|
||||
}
|
||||
}
|
||||
|
||||
int drop_singleton_signals(Signals *signals) {
|
||||
if (!signals) return 0;
|
||||
std::vector<Signal *> doomed;
|
||||
for (auto &kv : *signals)
|
||||
if (kv.second->size() == 1) doomed.push_back(kv.second);
|
||||
for (Signal *s : doomed) {
|
||||
// Detach the lone pin so it surfaces as `(NC)` in views.
|
||||
for (auto &pkv : *s) {
|
||||
pkv.second->connect(nullptr);
|
||||
pkv.second->nc_origin = NcOrigin::DroppedSingleton;
|
||||
}
|
||||
signals->remove(s->name);
|
||||
delete s;
|
||||
}
|
||||
return (int)doomed.size();
|
||||
}
|
||||
|
||||
void Signals::add(Signal *signal)
|
||||
{
|
||||
SystemElementContainer<Signal>::add(signal);
|
||||
|
||||
@@ -26,4 +26,9 @@ public:
|
||||
~Signals();
|
||||
};
|
||||
|
||||
// Drop every signal whose pin set is of size 1 — by definition unconnected.
|
||||
// The lone pin is detached (sig=nullptr) and the Signal object is removed
|
||||
// from the container and deleted. Returns the number of signals dropped.
|
||||
int drop_singleton_signals(Signals *signals);
|
||||
|
||||
#endif // _SIGNALS_HPP_
|
||||
@@ -161,6 +161,16 @@ public:
|
||||
* @param name Name of the element.
|
||||
* @return Pointer to the merged or newly created element.
|
||||
*/
|
||||
/**
|
||||
* @brief Removes the element with the given name from the container.
|
||||
* The element itself is NOT deleted — caller owns it.
|
||||
* @return True if an element was removed, false if the name was absent.
|
||||
*/
|
||||
bool remove(string name)
|
||||
{
|
||||
return content.erase(name) > 0;
|
||||
}
|
||||
|
||||
T *merge(string name)
|
||||
{
|
||||
if (exists(name))
|
||||
|
||||
@@ -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