ODS import, persistence, scripting, connector types + VPX transforms.
- ODS importer (libzip + pugixml): each sheet → Part, rows → Pin/Signal.
- save / restore commands: tab-delimited snapshot of modules, parts,
signals, connections + pin_map. `restore` replaces the System.
- source / script-save: replay a file of commands; record canonical
commands since last `new` for replay later. Interactive screens
refused during source. `explore` marked non-scriptable.
- TUI screen_explore: 4 columns (modules, type, children, detail) with
filters on children and detail; detail is a Menu so arrows scroll
long pin lists.
- Connector types & transforms: each Part carries a `connector_type`
string. `set-type` validates the part's pin layout against the type
(cols set check). `connect` strict pair: rejects when lookup falls
back to identity unless types are both empty AND pin sets match.
- VPX 3U transforms: 3 registered pairs (vpx-3u-bkp-pN ↔ vpx-3u-payload-pN,
N=0/1/2) with row-pattern correspondence tables ported from the user's
Python reference.
- Code split for maintainability: src/tui/{shell,completion,commands,
screen_main,screen_search,screen_connect,screen_settype,screen_explore,
tui_helpers}.cpp.
- Bug fixes: Module::add(Part*) override sets part->prnt (was always
null, breaking save's W lines). Defensive guards in explore against
empty Menu lists. Renderer wrapped in try/catch so domain throws
surface as on-screen errors instead of SIGABRT.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
417
src/tui/commands.cpp
Normal file
417
src/tui/commands.cpp
Normal file
@@ -0,0 +1,417 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/persist.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 <utility>
|
||||
|
||||
void Tui::RegisterCommands() {
|
||||
commands["help"] = {
|
||||
{{"command name (optional)", Completion::Command}},
|
||||
[this](const std::vector<std::string> &args) {
|
||||
if (args.empty()) {
|
||||
Print("Commands — type `help <name>` for details.");
|
||||
size_t maxw = 0;
|
||||
for (const auto &kv : commands) maxw = std::max(maxw, kv.first.size());
|
||||
for (const auto &kv : commands) {
|
||||
Print(" " + kv.first
|
||||
+ std::string(maxw - kv.first.size() + 2, ' ')
|
||||
+ kv.second.description);
|
||||
}
|
||||
Print("Keys: Esc cancels a multi-step prompt; Tab completes commands or paths.");
|
||||
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;
|
||||
Print(name + " — " + 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.prompt_for_missing) {
|
||||
Print(" run with no args for the interactive 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();
|
||||
Print("system created.");
|
||||
}, true, "create a new (empty) system; resets the script-save buffer" };
|
||||
|
||||
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]);
|
||||
Print("loaded '" + args[0] + "' from " + args[1]);
|
||||
Print(" parts: " + std::to_string(mod->size()));
|
||||
Print(" signals: " + std::to_string(mod->signals->size()));
|
||||
} 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["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];
|
||||
Print(mod->name + "/" + prt->name + ": connector_type = "
|
||||
+ (args[2].empty() ? "(none)" : args[2]));
|
||||
},
|
||||
/*prompt_for_missing=*/ false,
|
||||
"tag a part's connector type for transform lookup",
|
||||
};
|
||||
|
||||
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 ® = 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 err = CheckIdentityCompatible(p1, p2);
|
||||
if (!err.empty()) {
|
||||
Print("connect refused: " + err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
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)",
|
||||
};
|
||||
|
||||
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 };
|
||||
|
||||
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)",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user