Files
essim/src/tui/commands.cpp
François 5e89b33088 Signal analysis pass (analyze), NC tests, DESIGN.md catch-up.
- New `src/system/analysis.{hpp,cpp}` — stateless post-processing pass
  `analyze_system(System*) → AnalysisReport`. Per-module detection of
  signal groups and anomalies; pure read, re-runnable.
  - Groups: diff pairs (`*_P` / `*_N`, case-insensitive), buses
    (`NAME[N]` or strict `NAME_N` — the `_` before digits is required
    so names like `GETH_01_VDD12` are not misread as a bus).
  - Anomalies: `DiffPairOrphan` (asymmetric: only `_P` without `_N` is
    reported — `_N` alone is overloaded with active-low semantics and
    floods the output with false positives), `BusGap` (missing index
    inside a detected `[lo..hi]`).
  - Noise filters: signals starting with `$` (Mentor internals) are
    skipped wholesale.
- New `analyze` shell command — prints groups sorted by module +
  label, then anomalies. Sized for the upcoming dashboard.
- `tests/test_analysis.cpp` — 8 cases covering both detectors, false-
  positive guards (no-underscore digits, `$`-prefixed internals), and
  per-module scoping.
- `tests/test_nc_origin.cpp` — completes the prior NC-tagging commit
  with round-trip + drop_singleton_signals coverage.
- DESIGN.md updated: layout entry for `analysis.{hpp,cpp}` and new
  section explaining the pass; NC-origin paragraph aligned with the
  actual tag semantics and the verify three-pass summary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:42:58 +02:00

755 lines
32 KiB
C++

#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/persist.hpp"
#include "system/pin_role.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include "system/transform.hpp"
#include "system/transform_vpx.hpp"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <exception>
#include <fstream>
#include <unordered_set>
#include <utility>
void Tui::RegisterCommands() {
commands["help"] = {
{{"command name (optional)", Completion::Command}},
[this](const std::vector<std::string> &args) {
if (args.empty()) {
size_t maxw = 0;
for (const auto &kv : commands) maxw = std::max(maxw, kv.first.size());
auto print_group = [&](const std::string &title, bool want_interactive) {
bool printed_any = false;
for (const auto &kv : commands) {
if (kv.second.interactive != want_interactive) continue;
if (!printed_any) { Print(title); printed_any = true; }
Print(" " + kv.first
+ std::string(maxw - kv.first.size() + 2, ' ')
+ kv.second.description);
}
};
Print("Commands — type `help <name>` for details.");
print_group("Interactive (open a full-screen mode):", true);
print_group("Other:", false);
Print("Keys: Esc cancels a multi-step prompt or leaves an interactive screen;");
Print(" Tab completes commands/paths or cycles focus in interactive screens;");
Print(" PageUp/PageDown scroll output (10 lines), Home/End jump to top/bottom.");
return;
}
const std::string &name = args[0];
auto it = commands.find(name);
if (it == commands.end()) { Print("unknown command: " + name); return; }
const auto &spec = it->second;
std::string tag = spec.interactive ? " [interactive]" : "";
Print(name + tag + "" + spec.description);
if (spec.params.empty()) {
Print(" no arguments.");
} else {
for (size_t i = 0; i < spec.params.size(); ++i) {
Print(" arg " + std::to_string(i + 1) + ": " + spec.params[i].name);
}
}
if (spec.interactive) {
Print(" run with no args to open the interactive screen,");
Print(" or with all args for inline (scriptable) execution.");
} else if (!spec.prompt_for_missing) {
Print(" no per-arg prompt — provide all args inline (or use the bare form).");
} else if (!spec.params.empty()) {
Print(" missing args trigger a prompt for each one.");
}
},
/*prompt_for_missing=*/ false,
"show command help (optionally for a specific command)",
};
commands["clear"] = { {}, [this](auto &) { output.clear(); }, true,
"clear the visualization area" };
commands["quit"] = { {}, [this](auto &) { quit = true; }, true,
"leave essim" };
commands["exit"] = { {}, [this](auto &) { quit = true; }, true,
"leave essim (alias of quit)" };
commands["new"] = { {}, [this](auto &) {
sys = std::make_unique<System>();
recorded.clear();
vars.clear();
Print("system created.");
}, true, "create a new (empty) system; resets the script-save buffer and $vars" };
commands["set"] = {
{{"name", Completion::None},
{"value", Completion::None}},
[this](const std::vector<std::string> &args) {
if (args.empty()) {
if (vars.empty()) { Print("(no variables defined)"); return; }
for (const auto &kv : vars)
Print(" $" + kv.first + " = " + kv.second);
return;
}
if (args.size() != 2) {
Print("usage: set <name> <value> (or no args to list)");
return;
}
const std::string &name = args[0];
if (name.empty()) { Print("set: empty name"); return; }
for (size_t i = 0; i < name.size(); ++i) {
char c = name[i];
bool ok = std::isalnum((unsigned char)c) || c == '_';
bool first_ok = i == 0 ? !std::isdigit((unsigned char)c) : true;
if (!ok || !first_ok) {
Print("set: invalid name '" + name
+ "' (must match [A-Za-z_][A-Za-z0-9_]*)");
return;
}
}
vars[name] = args[1];
},
/*prompt_for_missing=*/ false,
"define a $variable for substitution in subsequent commands "
"(no args = list defined vars)",
};
commands["load"] = {
{{"module name", Completion::None},
{"filename", Completion::Path},
{"import type [mentor|altium|ods]", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
std::string ls = ToLower(args[2]);
ImportType t;
if (ls == "mentor") t = ImportType::IMPORT_MENTOR;
else if (ls == "altium") t = ImportType::IMPORT_ALTIUM;
else if (ls == "ods") t = ImportType::IMPORT_ODS;
else { Print("unknown import type: " + args[2]); return; }
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())
+ (dropped ? " (dropped " + std::to_string(dropped)
+ " singleton/NC signal(s))" : ""));
} catch (const std::exception &e) {
Print(std::string("load failed: ") + e.what());
}
},
/*prompt_for_missing=*/ true,
"load a module from a netlist / pinout file (mentor, altium, ods)",
};
commands["source"] = {
{{"filename", Completion::Path}},
[this](const std::vector<std::string> &args) {
Source(args[0]);
},
/*prompt_for_missing=*/ true,
"execute a file of commands line by line (interactive cmds rejected)",
};
commands["script-save"] = {
{{"filename", Completion::Path}},
[this](const std::vector<std::string> &args) {
std::string expanded = args[0];
if (!expanded.empty() && expanded[0] == '~') {
if (const char *home = std::getenv("HOME"))
expanded = std::string(home) + expanded.substr(1);
}
std::ofstream f(expanded);
if (!f) { Print("script-save: cannot open " + args[0]); return; }
for (const auto &cmd : recorded) f << cmd << '\n';
Print("script-save: " + std::to_string(recorded.size())
+ " line(s) → " + args[0]);
},
/*prompt_for_missing=*/ true,
"write commands run since last 'new' as a replay-ready script",
};
commands["save"] = {
{{"filename", Completion::Path}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
std::string err;
if (save_system(sys.get(), args[0], err)) {
Print("saved to " + args[0]);
} else {
Print("save failed: " + err);
}
},
/*prompt_for_missing=*/ true,
"write the current system snapshot to a file",
};
commands["restore"] = {
{{"filename", Completion::Path}},
[this](const std::vector<std::string> &args) {
std::string err;
System *fresh = restore_system(args[0], err);
if (!fresh) { Print("restore failed: " + err); return; }
sys.reset(fresh);
int mods = (int)sys->modules()->size();
int conns = (int)sys->connections()->size();
Print("restored from " + args[0]
+ " (" + std::to_string(mods) + " module(s), "
+ std::to_string(conns) + " connection(s))");
},
/*prompt_for_missing=*/ true,
"replace the current system with a saved snapshot",
};
commands["verify"] = { {}, [this](auto &) {
if (!sys) { Print("no system: run 'new' first."); return; }
int checked = 0;
int mismatches = 0;
for (auto &mkv : *sys->modules()) {
Module *mod = mkv.second;
for (auto &pkv : *mod) {
Part *prt = pkv.second;
if (prt->connector_type.empty()) continue;
for (auto &nkv : *prt) {
Pin *pin = nkv.second;
++checked;
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) continue;
++mismatches;
std::string sig_label = s ? s->name : std::string("(NC)");
Print(" " + mod->name + "/" + prt->name + "/" + pin->name
+ ": expected " + signal_type_name(expected)
+ ", got " + signal_type_name(actual)
+ " (signal: " + sig_label + ")");
}
}
}
Print("verify: " + std::to_string(mismatches) + " local mismatch(es) over "
+ std::to_string(checked) + " typed pin(s).");
auto nets = compute_all_nets(sys.get());
int bridged = 0, inconsistent = 0;
for (const auto &n : nets) {
if (n.members.size() < 2) continue;
++bridged;
SignalType dom;
if (net_type_consistent(n, dom)) continue;
++inconsistent;
std::string line = " net mixes Power and GndShield:";
for (const auto &mp : n.members) {
line += " " + mp.first->name + "/" + mp.second->name
+ "(" + signal_type_name(mp.second->type) + ")";
}
Print(line);
}
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" };
commands["analyze"] = { {}, [this](auto &) {
if (!sys) { Print("no system: run 'new' first."); return; }
AnalysisReport rep = analyze_system(sys.get());
int n_diff = 0, n_bus = 0;
for (const auto &g : rep.groups) {
if (g.kind == GroupKind::DiffPair) ++n_diff;
else if (g.kind == GroupKind::Bus) ++n_bus;
}
int n_dp_orph = 0, n_bus_gap = 0;
for (const auto &a : rep.anomalies) {
if (a.kind == AnomalyKind::DiffPairOrphan) ++n_dp_orph;
else if (a.kind == AnomalyKind::BusGap) ++n_bus_gap;
}
Print("analyze: " + std::to_string(n_diff) + " diff pair(s), "
+ std::to_string(n_bus) + " bus(es).");
// Sort groups by module then label so output is stable.
auto by_label = [](const SignalGroup &a, const SignalGroup &b) {
std::string ma = a.module ? a.module->name : std::string{};
std::string mb = b.module ? b.module->name : std::string{};
if (ma != mb) return ma < mb;
return a.label < b.label;
};
auto groups = rep.groups; // copy: report stays untouched
std::sort(groups.begin(), groups.end(), by_label);
for (const auto &g : groups) {
std::string mname = g.module ? g.module->name : std::string("?");
std::string line = " " + mname + "/" + g.label
+ " [" + group_kind_name(g.kind) + "]"
+ "" + std::to_string(g.members.size())
+ " signal(s)";
Print(line);
}
if (rep.anomalies.empty()) {
Print("analyze: no anomaly.");
} else {
Print("analyze: " + std::to_string(rep.anomalies.size())
+ " anomaly(ies) ("
+ std::to_string(n_dp_orph) + " diff-pair orphan, "
+ std::to_string(n_bus_gap) + " bus gap):");
for (const auto &a : rep.anomalies)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] "
+ a.message);
}
}, 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},
{"signal name", Completion::None},
{"type [power|gnd|other]", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); 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;
}
SignalType t;
if (!signal_type_from_name(args[2], t)) {
Print("type must be one of: power, gnd, other (got: " + args[2] + ")");
return;
}
sig->type = t;
Print(mod->name + "/" + sig->name + ": signal type = "
+ signal_type_name(t));
},
/*prompt_for_missing=*/ true,
"override the auto-detected signal type (power | gnd | other)",
};
commands["set-type"] = {
{{"module", Completion::None},
{"part (name or pattern)", Completion::None},
{"connector type (free string, e.g. vpx-bp, vpx-payload)", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
if (args.empty()) {
settype_modules.clear();
for (auto &m : *sys->modules()) settype_modules.push_back(m.first);
std::sort(settype_modules.begin(), settype_modules.end(), NaturalLess);
if (settype_modules.empty()) { Print("no modules loaded."); return; }
settype_m_idx = 0;
settype_p_filter.clear();
settype_p_idx = 0;
settype_type.clear();
settype_status.clear();
settype_focus_idx = 0;
screen_idx = 3;
return;
}
if (args.size() != 3) {
Print("usage: set-type <module> <part> <kind> (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;
}
Part *prt = nullptr;
try { prt = mod->get(args[1]); }
catch (const std::exception &) {
std::string needle = ToLower(args[1]);
std::vector<Part *> matches;
for (auto &p : *mod)
if (ToLower(p.first).find(needle) != std::string::npos)
matches.push_back(p.second);
if (matches.size() == 1) prt = matches[0];
else {
Print(std::to_string(matches.size())
+ " match(es) for part '" + args[1] + "' in " + mod->name);
return;
}
}
std::string err = ValidatePartForKind(prt, args[2]);
if (!err.empty()) {
Print("set-type refused: " + err);
return;
}
prt->connector_type = args[2];
int filled = FillPartFromLayout(prt, args[2]);
for (auto &kv : *prt)
kv.second->expected_signal_type = pin_role(args[2], kv.first);
Print(mod->name + "/" + prt->name + ": connector_type = "
+ (args[2].empty() ? "(none)" : args[2]));
if (filled > 0)
Print("set-type: materialised " + std::to_string(filled)
+ " NC pin(s) from connector layout");
},
/*prompt_for_missing=*/ false,
"tag a part's connector type for transform lookup",
/*scriptable=*/ true,
/*interactive=*/ true,
};
commands["connect"] = {
{{"module1", Completion::None},
{"part1 (name or pattern)", Completion::None},
{"module2", Completion::None},
{"part2 (name or pattern)", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
if (args.empty()) {
connect_modules.clear();
for (auto &m : *sys->modules()) connect_modules.push_back(m.first);
std::sort(connect_modules.begin(), connect_modules.end(), NaturalLess);
if (connect_modules.empty()) { Print("no modules loaded."); return; }
connect_m1_idx = 0;
connect_m2_idx = std::min(1, (int)connect_modules.size() - 1);
connect_p1_filter.clear();
connect_p2_filter.clear();
connect_p1_idx = 0;
connect_p2_idx = 0;
connect_focus_idx = 0;
screen_idx = 2;
return;
}
if (args.size() != 4) {
Print("usage: connect <m1> <p1> <m2> <p2> (or no args for interactive)");
return;
}
auto resolve_module = [this](const std::string &name)
-> std::pair<Module*, std::vector<std::string>> {
try { return {sys->modules()->get(name), {}}; }
catch (const std::exception &) {}
std::string needle = ToLower(name);
std::vector<Module*> matches;
std::vector<std::string> names;
for (auto &m : *sys->modules()) {
if (ToLower(m.first).find(needle) != std::string::npos) {
matches.push_back(m.second);
names.push_back(m.first);
}
}
if (matches.size() == 1) return {matches[0], {}};
return {nullptr, names};
};
auto resolve_part = [](Module *mod, const std::string &name)
-> std::pair<Part*, std::vector<std::string>> {
try { return {mod->get(name), {}}; }
catch (const std::exception &) {}
std::string needle = ToLower(name);
std::vector<Part*> matches;
std::vector<std::string> names;
for (auto &p : *mod) {
if (ToLower(p.first).find(needle) != std::string::npos) {
matches.push_back(p.second);
names.push_back(p.first);
}
}
if (matches.size() == 1) return {matches[0], {}};
return {nullptr, names};
};
auto report_ambiguous = [this](const std::string &what,
const std::string &needle,
const std::vector<std::string> &names) {
if (names.empty()) {
Print(what + " not found: " + needle);
} else {
Print(what + " ambiguous for '" + needle + "': "
+ std::to_string(names.size()) + " match(es)");
int shown = 0;
for (const auto &n : names) {
if (shown++ >= 8) { Print(""); break; }
Print(" " + n);
}
}
};
auto [m1, m1_alts] = resolve_module(args[0]);
if (!m1) { report_ambiguous("module", args[0], m1_alts); return; }
auto [p1, p1_alts] = resolve_part(m1, args[1]);
if (!p1) { report_ambiguous("part in " + m1->name, args[1], p1_alts); return; }
auto [m2, m2_alts] = resolve_module(args[2]);
if (!m2) { report_ambiguous("module", args[2], m2_alts); return; }
auto [p2, p2_alts] = resolve_part(m2, args[3]);
if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; }
auto &reg = TransformRegistry::get();
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
if (t == reg.identity()) {
if (!both_empty) {
Print("connect refused: no transform for types '"
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type)
+ "' ↔ '"
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type)
+ "'. Set matching types via 'set-type' first.");
return;
}
std::string info;
std::string err = CheckIdentityCompatible(p1, p2, &info);
if (!err.empty()) {
Print("connect refused: " + err);
return;
}
if (!info.empty()) {
int added = FillIdentityNCs(p1, p2);
Print("connect: " + info);
if (added > 0)
Print("connect: materialised " + std::to_string(added)
+ " NC pin(s) so both sides match");
}
}
auto pin_map = t->apply(p1, p2);
std::string conn_name = m1->name + "/" + p1->name
+ " <-> " + m2->name + "/" + p2->name;
try {
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
c->transform_name = t->name;
c->pin_map = std::move(pin_map);
sys->connections()->add(c);
Print("connected: " + conn_name
+ " via " + t->name
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
} catch (const std::exception &e) {
Print(std::string("connect failed: ") + e.what());
}
},
/*prompt_for_missing=*/ false,
"connect a part across two modules (interactive screen if no args)",
/*scriptable=*/ true,
/*interactive=*/ true,
};
commands["explore"] = { {}, [this](auto &) {
if (!sys) { Print("no system: run 'new' first."); return; }
explore_modules.clear();
for (auto &m : *sys->modules()) explore_modules.push_back(m.first);
std::sort(explore_modules.begin(), explore_modules.end(), NaturalLess);
if (explore_modules.empty()) { Print("no modules loaded."); return; }
explore_module_idx = 0;
explore_type_idx = 0;
explore_child_idx = 0;
explore_detail_idx = 0;
explore_child_filter.clear();
explore_detail_filter.clear();
explore_focus_idx = 0;
screen_idx = 4;
}, 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},
{"new module name", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
Module *src;
try { src = sys->modules()->get(args[0]); }
catch (const std::exception &) {
Print("unknown module: " + args[0]); return;
}
if (sys->modules()->exists(args[1])) {
Print("duplicate refused: module '" + args[1] + "' already exists.");
return;
}
Module *dst = new Module(args[1]);
// 1. Copy signals (preserve type overrides).
for (auto &skv : *src->signals) {
Signal *ss = skv.second;
Signal *ds = new Signal(ss->name);
ds->type = ss->type;
dst->signals->add(ds);
}
// 2. Copy parts, pins, and re-wire pin→signal.
for (auto &pkv : *src) {
Part *sp = pkv.second;
Part *dp = new Part(sp->name);
dp->connector_type = sp->connector_type;
for (auto &nkv : *sp) {
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);
ds->add(dn);
dn->connect(ds);
}
}
dst->add(dp);
}
sys->modules()->add(dst);
Print("duplicate: '" + args[0] + "' → '" + args[1] + "'"
+ " (" + std::to_string(dst->size()) + " part(s), "
+ std::to_string(dst->signals->size()) + " signal(s))");
},
/*prompt_for_missing=*/ true,
"clone a module under a new name (parts, pins, signals; no connections)",
};
}