#include "frontends/tui/tui.hpp" #include "frontends/tui/tui_helpers.hpp" #include "core/domain/analysis.hpp" #include "core/domain/connect.hpp" #include "core/domain/modules.hpp" #include "core/domain/nets.hpp" #include "core/domain/parts.hpp" #include "core/domain/persist.hpp" #include "core/domain/pin_role.hpp" #include "core/domain/pin_model.hpp" #include "core/domain/bsdl_model.hpp" #include "core/domain/bsdl_check.hpp" #include "core/domain/pins.hpp" #include "core/domain/signals.hpp" #include "core/domain/system.hpp" #include "core/domain/transform.hpp" #include "core/domain/transform_vpx.hpp" #include #include #include #include #include #include #include void Tui::RegisterCommands() { commands["help"] = { {{"command name (optional)", Completion::Command}}, [this](const std::vector &args) { if (args.empty()) { // Bare → textual list of commands. The feature-reference // screen lives at `screen_idx = 6` and is reachable from // the dashboard with `[h]`. 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.hidden) continue; 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 ` for details."); print_group("Interactive (open a full-screen mode):", true); print_group("Other:", false); Print("Keys: Esc cancels a multi-step prompt or returns to the dashboard;"); Print(" Tab completes commands/paths or cycles focus in screens;"); Print(" PageUp/PageDown scroll output, 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, "list commands (or `help ` for one command's details)", }; commands["clear"] = { {}, [this](auto &) { output.clear(); }, true, "clear the console output" }; // quit / exit work from any screen: set the flag *and* call Exit() on the // captured ScreenInteractive so the FTXUI loop returns immediately. The // legacy main-screen Renderer also reads `quit` as a belt-and-braces // backup when the screen_ptr hasn't been set yet (early-init / tests). auto do_quit = [this](auto &) { quit = true; if (screen_ptr) screen_ptr->Exit(); }; commands["quit"] = { {}, do_quit, true, "leave essim" }; commands["exit"] = { {}, do_quit, true, "leave essim (alias of quit)" }; commands["new"] = { {}, [this](auto &) { sys = std::make_unique(); 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 &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 (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 &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); auto inf = infer_signal_types(sys.get()); 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))" : "")); Print(" types: " + std::to_string(inf.power) + " power, " + std::to_string(inf.gnd) + " gnd, " + std::to_string(inf.kept_other) + " suspect Power (name only — kept as Other)"); } 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 &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 &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 &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 &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 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)."); // Model-driven pin checks (drive contention / undriven net / NC-wired) // from the PinSpec direction/function populated by connector/BSDL models. auto pin_anoms = check_pin_specs(sys.get(), &nets); for (const auto &a : pin_anoms) Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print("verify: " + std::to_string(pin_anoms.size()) + " model-driven pin anomaly(ies)."); // JTAG boundary-scan chain integrity (TAP pins → nets). auto jtag_anoms = check_jtag_chain(sys.get(), &nets); for (const auto &a : jtag_anoms) Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print("verify: " + std::to_string(jtag_anoms.size()) + " JTAG chain anomaly(ies)."); // Model-vs-netlist conflicts (e.g. a BSDL power pin left unconnected). auto conflict_anoms = check_source_conflicts(sys.get()); for (const auto &a : conflict_anoms) Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print("verify: " + std::to_string(conflict_anoms.size()) + " source-conflict(s)."); // BSDL completeness: device power/ground pins missing from the netlist. auto missing_anoms = check_bsdl_completeness(sys.get()); for (const auto &a : missing_anoms) Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print("verify: " + std::to_string(missing_anoms.size()) + " BSDL completeness issue(s)."); }, true, "check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" }; commands["dashboard"] = { {}, [this](auto &) { screen_idx = 4; }, true, "open the dashboard (system overview)", /*scriptable=*/ false, /*interactive=*/ true }; 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["set-signal-type"] = { {{"module", Completion::None}, {"signal name", Completion::None}, {"type [power|gnd|other]", Completion::None}}, [this](const std::vector &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-connector-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 &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_back_idx = -1; // standalone entry — Esc → dashboard screen_idx = 2; return; } if (args.size() != 3) { Print("usage: set-connector-type (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 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-connector-type refused: " + err); return; } prt->connector_type = args[2]; ConnectorModel model(args[2]); ApplyReport rep = apply_model(prt, model); Print(mod->name + "/" + prt->name + ": connector_type = " + (args[2].empty() ? "(none)" : args[2])); if (rep.materialised > 0) Print("set-connector-type: added " + std::to_string(rep.materialised) + " NC pin(s) from the connector layout"); }, /*prompt_for_missing=*/ false, "tag a part's connector type (tells connect how to wire its pins)", /*scriptable=*/ true, /*interactive=*/ true, }; commands["attach-bsdl"] = { {{"module", Completion::None}, {"part (name or pattern)", Completion::None}, {"bsdl file (.bsd path)", Completion::None}}, [this](const std::vector &args) { if (!sys) { Print("no system: run 'new' first."); return; } if (args.size() != 3) { Print("usage: attach-bsdl "); 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 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; } } BsdlModel model = BsdlModel::from_file(args[2]); if (!model.valid()) { Print("attach-bsdl: cannot parse " + args[2] + (model.error().empty() ? "" : (": " + model.error()))); return; } BsdlApplyReport r = apply_bsdl(prt, model); prt->bsdl_path = args[2]; Print(mod->name + "/" + prt->name + ": attached BSDL '" + model.entity() + "' — " + std::to_string(r.bound) + "/" + std::to_string((int)model.ports().size()) + " ports bound" + (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : "")); }, /*prompt_for_missing=*/ false, "attach a BSDL (.bsd) model to a part (fills in each pin's role and direction)", /*scriptable=*/ true, /*interactive=*/ false, }; commands["connect"] = { {{"module1", Completion::None}, {"part1 (name or pattern)", Completion::None}, {"module2", Completion::None}, {"part2 (name or pattern)", Completion::None}}, [this](const std::vector &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 = 1; return; } if (args.size() != 4) { Print("usage: connect (or no args for interactive)"); return; } auto resolve_module = [this](const std::string &name) -> std::pair> { try { return {sys->modules()->get(name), {}}; } catch (const std::exception &) {} std::string needle = ToLower(name); std::vector matches; std::vector 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> { try { return {mod->get(name), {}}; } catch (const std::exception &) {} std::string needle = ToLower(name); std::vector matches; std::vector 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 &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 ® = 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-connector-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: added " + 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, }; // UI alias: the dashboard surfaces this command as `plug`. Keep the // canonical `connect` for script + save/restore stability. // `plug` is the user-facing name (dashboard shortcut [p]). `connect` // stays registered for script + save/restore backward compatibility but // is hidden from `help`. commands["plug"] = commands["connect"]; commands["connect"].hidden = 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 = 3; }, true, "browse modules → parts/signals/connections → details (interactive)", /*scriptable=*/ false, /*interactive=*/ true }; commands["duplicate"] = { {{"source module", Completion::None}, {"new module name", Completion::None}}, [this](const std::vector &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->spec = sn->spec; 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)", }; // Per-group registrators living in their own files. Keeps each // self-contained concern out of this orchestrator. RegisterExportCommands(); }