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)",
|
||||
};
|
||||
}
|
||||
111
src/tui/completion.cpp
Normal file
111
src/tui/completion.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
|
||||
void Tui::CompleteCommand(size_t start) {
|
||||
std::string current = input.substr(start);
|
||||
std::vector<std::string> matches;
|
||||
for (const auto &kv : commands)
|
||||
if (kv.first.rfind(current, 0) == 0) matches.push_back(kv.first);
|
||||
|
||||
if (matches.empty()) return;
|
||||
|
||||
auto replace_with = [&](const std::string &replacement) {
|
||||
input.replace(start, std::string::npos, replacement);
|
||||
cursor_pos = (int)input.size();
|
||||
};
|
||||
|
||||
if (matches.size() == 1) { replace_with(matches[0]); return; }
|
||||
|
||||
std::string lcp = LongestCommonPrefix(matches);
|
||||
if (lcp.size() > current.size()) { replace_with(lcp); return; }
|
||||
|
||||
std::string line = " ";
|
||||
for (const auto &m : matches) line += " " + m;
|
||||
Print(line);
|
||||
}
|
||||
|
||||
void Tui::CompletePath(size_t start) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::string current = input.substr(start);
|
||||
|
||||
auto pos = current.rfind('/');
|
||||
std::string disp, prefix;
|
||||
if (pos == std::string::npos) { disp = ""; prefix = current; }
|
||||
else { disp = current.substr(0, pos + 1); prefix = current.substr(pos + 1); }
|
||||
|
||||
std::string resolved = disp.empty() ? "." : disp;
|
||||
if (!resolved.empty() && resolved[0] == '~') {
|
||||
if (const char *home = std::getenv("HOME"))
|
||||
resolved = std::string(home) + resolved.substr(1);
|
||||
}
|
||||
|
||||
std::vector<std::string> names;
|
||||
std::vector<bool> is_dir;
|
||||
try {
|
||||
for (const auto &e : fs::directory_iterator(resolved)) {
|
||||
std::string n = e.path().filename().string();
|
||||
if (n.rfind(prefix, 0) == 0) {
|
||||
names.push_back(n);
|
||||
is_dir.push_back(e.is_directory());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception &) {
|
||||
return;
|
||||
}
|
||||
if (names.empty()) return;
|
||||
|
||||
auto replace_with = [&](const std::string &replacement) {
|
||||
input.replace(start, std::string::npos, replacement);
|
||||
cursor_pos = (int)input.size();
|
||||
};
|
||||
|
||||
if (names.size() == 1) {
|
||||
replace_with(disp + names[0] + (is_dir[0] ? "/" : ""));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string lcp = LongestCommonPrefix(names);
|
||||
if (lcp.size() > prefix.size()) { replace_with(disp + lcp); return; }
|
||||
|
||||
std::string line = " ";
|
||||
for (size_t i = 0; i < names.size(); ++i)
|
||||
line += " " + names[i] + (is_dir[i] ? "/" : "");
|
||||
Print(line);
|
||||
}
|
||||
|
||||
void Tui::CompleteInline() {
|
||||
bool ends_with_ws = !input.empty()
|
||||
&& std::isspace((unsigned char)input.back());
|
||||
|
||||
size_t arg_start;
|
||||
if (input.empty() || ends_with_ws) {
|
||||
arg_start = input.size();
|
||||
} else {
|
||||
size_t i = input.size();
|
||||
while (i > 0 && !std::isspace((unsigned char)input[i - 1])) --i;
|
||||
arg_start = i;
|
||||
}
|
||||
|
||||
auto preceding = Tokenize(input.substr(0, arg_start));
|
||||
int arg_index = (int)preceding.size();
|
||||
|
||||
if (arg_index == 0) { CompleteCommand(); return; }
|
||||
|
||||
if (preceding.empty()) return;
|
||||
auto cmd_it = commands.find(preceding[0]);
|
||||
if (cmd_it == commands.end()) return;
|
||||
const auto &spec = cmd_it->second;
|
||||
|
||||
int param_idx = arg_index - 1;
|
||||
if (param_idx >= (int)spec.params.size()) return;
|
||||
switch (spec.params[param_idx].completion) {
|
||||
case Completion::Path: CompletePath(arg_start); break;
|
||||
case Completion::Command: CompleteCommand(arg_start); break;
|
||||
case Completion::None: break;
|
||||
}
|
||||
}
|
||||
146
src/tui/screen_connect.cpp
Normal file
146
src/tui/screen_connect.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
void Tui::RefreshFilteredPartList(const std::vector<std::string> &modules,
|
||||
int m_idx,
|
||||
const std::string &filter,
|
||||
std::vector<std::string> &out,
|
||||
int &sel_idx) {
|
||||
out.clear();
|
||||
if (!sys || modules.empty()) { sel_idx = 0; return; }
|
||||
try {
|
||||
Module *mod = sys->modules()->get(modules[m_idx]);
|
||||
std::string needle = ToLower(filter);
|
||||
for (auto &pkv : *mod) {
|
||||
if (needle.empty()
|
||||
|| ToLower(pkv.first).find(needle) != std::string::npos) {
|
||||
out.push_back(pkv.first);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception &) {}
|
||||
std::sort(out.begin(), out.end(), NaturalLess);
|
||||
if (sel_idx >= (int)out.size()) sel_idx = std::max(0, (int)out.size() - 1);
|
||||
}
|
||||
|
||||
Component Tui::BuildConnectScreen() {
|
||||
InputOption pf_opt;
|
||||
pf_opt.multiline = false;
|
||||
pf_opt.transform = [](InputState s) {
|
||||
auto el = s.element;
|
||||
if (s.is_placeholder) el |= dim;
|
||||
return el;
|
||||
};
|
||||
|
||||
auto m1_menu = Menu(&connect_modules, &connect_m1_idx);
|
||||
auto p1_filter = Input(&connect_p1_filter, "filter…", pf_opt);
|
||||
auto p1_menu = Menu(&connect_p1_list, &connect_p1_idx);
|
||||
auto m2_menu = Menu(&connect_modules, &connect_m2_idx);
|
||||
auto p2_filter = Input(&connect_p2_filter, "filter…", pf_opt);
|
||||
auto p2_menu = Menu(&connect_p2_list, &connect_p2_idx);
|
||||
|
||||
auto do_connect = [this] {
|
||||
if (!sys || connect_modules.empty()) { screen_idx = 0; return; }
|
||||
if (connect_p1_list.empty() || connect_p2_list.empty()) {
|
||||
Print("connect: select a part on each side first.");
|
||||
screen_idx = 0;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Module *m1 = sys->modules()->get(connect_modules[connect_m1_idx]);
|
||||
Module *m2 = sys->modules()->get(connect_modules[connect_m2_idx]);
|
||||
Part *p1 = m1->get(connect_p1_list[connect_p1_idx]);
|
||||
Part *p2 = m2->get(connect_p2_list[connect_p2_idx]);
|
||||
|
||||
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.");
|
||||
screen_idx = 0;
|
||||
return;
|
||||
}
|
||||
std::string err = CheckIdentityCompatible(p1, p2);
|
||||
if (!err.empty()) {
|
||||
Print("connect refused: " + err);
|
||||
screen_idx = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto pin_map = t->apply(p1, p2);
|
||||
|
||||
std::string conn_name = m1->name + "/" + p1->name
|
||||
+ " <-> " + m2->name + "/" + p2->name;
|
||||
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());
|
||||
}
|
||||
screen_idx = 0;
|
||||
};
|
||||
auto connect_button = Button(" Connect ", do_connect);
|
||||
|
||||
auto components = Container::Vertical({
|
||||
m1_menu, p1_filter, p1_menu,
|
||||
m2_menu, p2_filter, p2_menu,
|
||||
connect_button,
|
||||
}, &connect_focus_idx);
|
||||
|
||||
return Renderer(components,
|
||||
[this, m1_menu, p1_filter, p1_menu,
|
||||
m2_menu, p2_filter, p2_menu, connect_button] {
|
||||
RefreshFilteredPartList(connect_modules, connect_m1_idx,
|
||||
connect_p1_filter, connect_p1_list, connect_p1_idx);
|
||||
RefreshFilteredPartList(connect_modules, connect_m2_idx,
|
||||
connect_p2_filter, connect_p2_list, connect_p2_idx);
|
||||
|
||||
auto col = [&](const std::string &title,
|
||||
Component mm, Component pf, Component pm) {
|
||||
return vbox({
|
||||
text(title) | bold,
|
||||
text("module") | dim,
|
||||
mm->Render() | yframe | size(HEIGHT, LESS_THAN, 8),
|
||||
separator(),
|
||||
hbox({text(" filter: "), pf->Render() | flex}) | border,
|
||||
text("part") | dim,
|
||||
pm->Render() | yframe | flex,
|
||||
}) | flex;
|
||||
};
|
||||
|
||||
return vbox({
|
||||
hbox({
|
||||
col("endpoint 1", m1_menu, p1_filter, p1_menu),
|
||||
separator(),
|
||||
col("endpoint 2", m2_menu, p2_filter, p2_menu),
|
||||
}) | flex,
|
||||
separator(),
|
||||
hbox({filler(), connect_button->Render(), filler()}),
|
||||
text(" Tab: cycle focus | Enter on [Connect]: confirm | Esc: leave ") | dim,
|
||||
}) | border;
|
||||
});
|
||||
}
|
||||
207
src/tui/screen_explore.cpp
Normal file
207
src/tui/screen_explore.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Component Tui::BuildExploreScreen() {
|
||||
InputOption pf_opt;
|
||||
pf_opt.multiline = false;
|
||||
pf_opt.transform = [](InputState s) {
|
||||
auto el = s.element;
|
||||
if (s.is_placeholder) el |= dim;
|
||||
return el;
|
||||
};
|
||||
|
||||
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);
|
||||
auto detail_filter = Input(&explore_detail_filter, "filter…", pf_opt);
|
||||
auto detail_menu = Menu(&explore_detail, &explore_detail_idx);
|
||||
|
||||
auto components = Container::Vertical(
|
||||
{module_menu, type_menu, child_filter, children_menu, detail_filter, detail_menu},
|
||||
&explore_focus_idx);
|
||||
|
||||
return Renderer(components,
|
||||
[this, module_menu, type_menu, child_filter, children_menu, detail_filter, detail_menu] {
|
||||
try {
|
||||
// Clamp the module index defensively (covers fresh systems / restore).
|
||||
if (explore_module_idx < 0
|
||||
|| explore_module_idx >= (int)explore_modules.size()) {
|
||||
explore_module_idx = 0;
|
||||
}
|
||||
|
||||
// Refresh the children list for the selected module + type, applying child filter.
|
||||
explore_children.clear();
|
||||
Module *cur_mod = nullptr;
|
||||
std::string c_needle = ToLower(explore_child_filter);
|
||||
if (sys && !explore_modules.empty()) {
|
||||
try {
|
||||
cur_mod = sys->modules()->get(explore_modules[explore_module_idx]);
|
||||
auto keep = [&](const std::string &name) {
|
||||
return c_needle.empty()
|
||||
|| ToLower(name).find(c_needle) != std::string::npos;
|
||||
};
|
||||
if (explore_type_idx == 0) {
|
||||
for (auto &pkv : *cur_mod)
|
||||
if (keep(pkv.first)) explore_children.push_back(pkv.first);
|
||||
} else if (explore_type_idx == 1) {
|
||||
for (auto &skv : *cur_mod->signals)
|
||||
if (keep(skv.first)) explore_children.push_back(skv.first);
|
||||
} else {
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
if ((c->m1 == cur_mod || c->m2 == cur_mod) && keep(ckv.first))
|
||||
explore_children.push_back(ckv.first);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception &) {}
|
||||
}
|
||||
std::sort(explore_children.begin(), explore_children.end(), NaturalLess);
|
||||
|
||||
// Never let the Menu see an empty list — FTXUI Menu's clamp(v, 0, size()-1)
|
||||
// is brittle when size() == 0; a placeholder also gives the user feedback.
|
||||
if (explore_children.empty()) explore_children.push_back("(no matches)");
|
||||
|
||||
if (explore_child_idx < 0
|
||||
|| explore_child_idx >= (int)explore_children.size()) {
|
||||
explore_child_idx = 0;
|
||||
}
|
||||
|
||||
std::string d_needle = ToLower(explore_detail_filter);
|
||||
auto keep_detail = [&](const std::string &line) {
|
||||
return d_needle.empty()
|
||||
|| ToLower(line).find(d_needle) != std::string::npos;
|
||||
};
|
||||
|
||||
// Build the detail pane (lines stored in explore_detail so the Menu
|
||||
// bound to it can scroll with arrow keys when focused).
|
||||
explore_header = "(no system)";
|
||||
explore_detail.clear();
|
||||
if (cur_mod && !explore_children.empty()) {
|
||||
const std::string &cname = explore_children[explore_child_idx];
|
||||
try {
|
||||
if (explore_type_idx == 0) {
|
||||
Part *p = cur_mod->get(cname);
|
||||
std::string type_str = p->connector_type.empty()
|
||||
? "(no type)" : p->connector_type;
|
||||
explore_header = cur_mod->name + "/" + p->name
|
||||
+ " — " + std::to_string(p->size())
|
||||
+ " pins • type: " + type_str;
|
||||
std::vector<std::pair<std::string, std::string>> rows;
|
||||
for (auto &pin_kv : *p) {
|
||||
Signal *s = pin_kv.second->signal();
|
||||
rows.emplace_back(pin_kv.first, s ? s->name : std::string("—"));
|
||||
}
|
||||
std::sort(rows.begin(), rows.end(),
|
||||
[](const auto &a, const auto &b) { return NaturalLess(a.first, b.first); });
|
||||
size_t maxw = 0;
|
||||
for (const auto &r : rows) maxw = std::max(maxw, r.first.size());
|
||||
for (const auto &r : rows) {
|
||||
std::string line = " " + r.first
|
||||
+ std::string(maxw - r.first.size() + 2, ' ')
|
||||
+ r.second;
|
||||
if (keep_detail(line))
|
||||
explore_detail.push_back(line);
|
||||
}
|
||||
} else if (explore_type_idx == 1) {
|
||||
Signal *s = cur_mod->signals->get(cname);
|
||||
explore_header = cur_mod->name + "/" + s->name
|
||||
+ " — " + std::to_string(s->size())
|
||||
+ " pins";
|
||||
std::vector<std::string> rows;
|
||||
for (auto &pin_kv : *s) {
|
||||
Pin *pin = pin_kv.second;
|
||||
std::string mname = (pin->prnt && pin->prnt->prnt)
|
||||
? pin->prnt->prnt->name : std::string("?");
|
||||
std::string pname = pin->prnt ? pin->prnt->name : std::string("?");
|
||||
rows.push_back(mname + "/" + pname + "/" + pin->name);
|
||||
}
|
||||
std::sort(rows.begin(), rows.end(), NaturalLess);
|
||||
for (const auto &r : rows)
|
||||
if (keep_detail(r))
|
||||
explore_detail.push_back(" " + r);
|
||||
} else {
|
||||
Connection *c = sys->connections()->get(cname);
|
||||
std::string tname = c->transform_name.empty()
|
||||
? "(unknown)" : c->transform_name;
|
||||
explore_header = c->name
|
||||
+ " — " + std::to_string(c->pin_map.size())
|
||||
+ " wires • transform: " + tname;
|
||||
std::vector<std::string> rows;
|
||||
for (auto &wp : c->pin_map) {
|
||||
Pin *a = wp.first;
|
||||
Pin *b = wp.second;
|
||||
auto label = [](Pin *pin) {
|
||||
if (!pin) return std::string("?");
|
||||
std::string m = (pin->prnt && pin->prnt->prnt)
|
||||
? pin->prnt->prnt->name : "?";
|
||||
std::string p = pin->prnt ? pin->prnt->name : "?";
|
||||
return m + "/" + p + "/" + pin->name;
|
||||
};
|
||||
rows.push_back(label(a) + " ↔ " + label(b));
|
||||
}
|
||||
std::sort(rows.begin(), rows.end(), NaturalLess);
|
||||
for (const auto &r : rows)
|
||||
if (keep_detail(r))
|
||||
explore_detail.push_back(" " + r);
|
||||
}
|
||||
} catch (const std::exception &) {}
|
||||
}
|
||||
|
||||
if (explore_detail.empty()) explore_detail.push_back("(empty)");
|
||||
if (explore_detail_idx < 0
|
||||
|| explore_detail_idx >= (int)explore_detail.size()) {
|
||||
explore_detail_idx = 0;
|
||||
}
|
||||
|
||||
auto col1 = vbox({
|
||||
text("module") | bold,
|
||||
module_menu->Render() | yframe | flex,
|
||||
}) | size(WIDTH, EQUAL, 24);
|
||||
|
||||
auto col2 = vbox({
|
||||
text("type") | bold,
|
||||
type_menu->Render() | flex,
|
||||
}) | size(WIDTH, EQUAL, 12);
|
||||
|
||||
auto col3 = vbox({
|
||||
text(explore_types[explore_type_idx]) | bold,
|
||||
hbox({text(" filter: "), child_filter->Render() | flex}) | border,
|
||||
children_menu->Render() | yframe | flex,
|
||||
}) | size(WIDTH, EQUAL, 36);
|
||||
|
||||
auto col4 = vbox({
|
||||
text(explore_header) | bold,
|
||||
hbox({text(" filter: "), detail_filter->Render() | flex}) | border,
|
||||
detail_menu->Render() | vscroll_indicator | yframe | flex,
|
||||
}) | flex;
|
||||
|
||||
return vbox({
|
||||
hbox({col1, separator(), col2, separator(), col3, separator(), col4}) | flex,
|
||||
text(" Tab: cycle focus (incl. detail to scroll) | Esc: leave explore ") | dim,
|
||||
}) | border;
|
||||
} catch (const std::exception &e) {
|
||||
return vbox({
|
||||
text("explore: render error") | bold,
|
||||
text(std::string(" ") + e.what()) | dim,
|
||||
text(" Esc: leave explore ") | dim,
|
||||
}) | border;
|
||||
}
|
||||
});
|
||||
}
|
||||
42
src/tui/screen_main.cpp
Normal file
42
src/tui/screen_main.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "tui/tui.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Component Tui::BuildMainScreen(ScreenInteractive &screen) {
|
||||
InputOption opt;
|
||||
opt.multiline = false;
|
||||
opt.cursor_position = &cursor_pos;
|
||||
opt.on_enter = [this] { Submit(); };
|
||||
opt.transform = [](InputState s) {
|
||||
auto el = s.element;
|
||||
if (s.is_placeholder) el |= dim;
|
||||
return el;
|
||||
};
|
||||
auto input_component = Input(&input, "type a command…", opt);
|
||||
|
||||
return Renderer(input_component, [this, &screen, input_component] {
|
||||
if (quit) screen.Exit();
|
||||
|
||||
Elements lines;
|
||||
for (const auto &l : output) lines.push_back(text(l));
|
||||
auto view = vbox(std::move(lines))
|
||||
| focusPositionRelative(0, 1)
|
||||
| yframe
|
||||
| flex;
|
||||
|
||||
std::string label = pending.empty()
|
||||
? "> "
|
||||
: pending.front().question + "? ";
|
||||
|
||||
return vbox({
|
||||
view,
|
||||
separator(),
|
||||
hbox({text(label), input_component->Render()}),
|
||||
}) | border;
|
||||
});
|
||||
}
|
||||
83
src/tui/screen_search.cpp
Normal file
83
src/tui/screen_search.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Component Tui::BuildSearchScreen() {
|
||||
InputOption query_opt;
|
||||
query_opt.multiline = false;
|
||||
query_opt.transform = [](InputState s) {
|
||||
auto el = s.element;
|
||||
if (s.is_placeholder) el |= dim;
|
||||
return el;
|
||||
};
|
||||
|
||||
auto query_input = Input(&search_query, "filter…", query_opt);
|
||||
auto module_menu = Menu(&search_modules, &search_module_idx);
|
||||
auto type_menu = Menu(&search_types, &search_type_idx);
|
||||
|
||||
auto components = Container::Vertical(
|
||||
{query_input, module_menu, type_menu}, &search_focus_idx);
|
||||
|
||||
return Renderer(components,
|
||||
[this, query_input, module_menu, type_menu] {
|
||||
std::vector<std::pair<std::string, size_t>> hits;
|
||||
if (!search_modules.empty() && sys) {
|
||||
const std::string &mname = search_modules[search_module_idx];
|
||||
try {
|
||||
Module *mod = sys->modules()->get(mname);
|
||||
std::string needle = ToLower(search_query);
|
||||
if (search_type_idx == 0) { // parts
|
||||
for (auto &pkv : *mod)
|
||||
if (needle.empty()
|
||||
|| ToLower(pkv.first).find(needle) != std::string::npos)
|
||||
hits.emplace_back(pkv.first, pkv.second->size());
|
||||
} else { // signals
|
||||
for (auto &skv : *mod->signals)
|
||||
if (needle.empty()
|
||||
|| ToLower(skv.first).find(needle) != std::string::npos)
|
||||
hits.emplace_back(skv.first, skv.second->size());
|
||||
}
|
||||
} catch (const std::exception &) {}
|
||||
}
|
||||
std::sort(hits.begin(), hits.end(),
|
||||
[](const auto &a, const auto &b) { return NaturalLess(a.first, b.first); });
|
||||
|
||||
Elements result_lines;
|
||||
for (const auto &h : hits)
|
||||
result_lines.push_back(
|
||||
text(" " + h.first + " (" + std::to_string(h.second) + " pins)"));
|
||||
|
||||
auto left = vbox({
|
||||
text("module") | bold,
|
||||
module_menu->Render() | yframe | flex,
|
||||
separator(),
|
||||
text("type") | bold,
|
||||
type_menu->Render(),
|
||||
}) | size(WIDTH, EQUAL, 28);
|
||||
|
||||
auto right = vbox({
|
||||
hbox({text(" search: "), query_input->Render() | flex}) | border,
|
||||
text(std::to_string(hits.size()) + " match(es)") | dim,
|
||||
vbox(std::move(result_lines)) | yframe | flex,
|
||||
}) | flex;
|
||||
|
||||
return vbox({
|
||||
hbox({left, separator(), right}) | flex,
|
||||
text(" Tab: cycle focus | Esc: leave search ") | dim,
|
||||
}) | border;
|
||||
});
|
||||
}
|
||||
129
src/tui/screen_settype.cpp
Normal file
129
src/tui/screen_settype.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform_vpx.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Component Tui::BuildSettypeScreen() {
|
||||
InputOption pf_opt;
|
||||
pf_opt.multiline = false;
|
||||
pf_opt.transform = [](InputState s) {
|
||||
auto el = s.element;
|
||||
if (s.is_placeholder) el |= dim;
|
||||
return el;
|
||||
};
|
||||
|
||||
auto module_menu = Menu(&settype_modules, &settype_m_idx);
|
||||
auto part_filter = Input(&settype_p_filter, "filter…", pf_opt);
|
||||
auto part_menu = Menu(&settype_p_list, &settype_p_idx);
|
||||
|
||||
InputOption type_opt;
|
||||
type_opt.multiline = false;
|
||||
type_opt.transform = pf_opt.transform;
|
||||
auto type_input = Input(&settype_type, "connector type (e.g. vpx-bp)", type_opt);
|
||||
|
||||
auto do_settype = [this] {
|
||||
if (!sys || settype_modules.empty()) {
|
||||
settype_status = "no system / no modules";
|
||||
return;
|
||||
}
|
||||
if (settype_p_list.empty()) {
|
||||
settype_status = "select a part first";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Module *mod = sys->modules()->get(settype_modules[settype_m_idx]);
|
||||
Part *prt = mod->get(settype_p_list[settype_p_idx]);
|
||||
std::string err = ValidatePartForKind(prt, settype_type);
|
||||
if (!err.empty()) {
|
||||
settype_status = "refused: " + err;
|
||||
return;
|
||||
}
|
||||
prt->connector_type = settype_type;
|
||||
std::string msg = mod->name + "/" + prt->name + " = "
|
||||
+ (settype_type.empty() ? "(none)" : settype_type);
|
||||
settype_status = "applied: " + msg;
|
||||
Print("set-type " + msg);
|
||||
} catch (const std::exception &e) {
|
||||
settype_status = std::string("failed: ") + e.what();
|
||||
}
|
||||
};
|
||||
auto button = Button(" Apply ", do_settype);
|
||||
|
||||
auto components = Container::Vertical({
|
||||
module_menu, part_filter, part_menu, type_input, button,
|
||||
}, &settype_focus_idx);
|
||||
|
||||
return Renderer(components,
|
||||
[this, module_menu, part_filter, part_menu, type_input, button] {
|
||||
RefreshFilteredPartList(settype_modules, settype_m_idx,
|
||||
settype_p_filter, settype_p_list, settype_p_idx);
|
||||
|
||||
std::string current = "(none)";
|
||||
if (sys && !settype_modules.empty() && !settype_p_list.empty()) {
|
||||
try {
|
||||
Module *mod = sys->modules()->get(settype_modules[settype_m_idx]);
|
||||
Part *prt = mod->get(settype_p_list[settype_p_idx]);
|
||||
if (!prt->connector_type.empty()) current = prt->connector_type;
|
||||
} catch (const std::exception &) {}
|
||||
}
|
||||
|
||||
std::vector<std::string> known_types;
|
||||
if (sys) {
|
||||
for (auto &mkv : *sys->modules())
|
||||
for (auto &pkv : *mkv.second)
|
||||
if (!pkv.second->connector_type.empty())
|
||||
known_types.push_back(pkv.second->connector_type);
|
||||
std::sort(known_types.begin(), known_types.end(), NaturalLess);
|
||||
known_types.erase(std::unique(known_types.begin(), known_types.end()),
|
||||
known_types.end());
|
||||
}
|
||||
std::string known_line = "in use:";
|
||||
if (known_types.empty()) known_line += " (none yet)";
|
||||
else for (const auto &k : known_types) known_line += " " + k;
|
||||
|
||||
auto left = vbox({
|
||||
text("module") | bold,
|
||||
module_menu->Render() | yframe | flex,
|
||||
}) | size(WIDTH, EQUAL, 28);
|
||||
|
||||
auto middle = vbox({
|
||||
hbox({text(" filter: "), part_filter->Render() | flex}) | border,
|
||||
text("part") | dim,
|
||||
part_menu->Render() | yframe | flex,
|
||||
}) | flex;
|
||||
|
||||
auto right = vbox({
|
||||
text("current type: ") | bold,
|
||||
text(" " + current),
|
||||
separator(),
|
||||
text("new type:") | bold,
|
||||
hbox({text(" "), type_input->Render() | flex}) | border,
|
||||
text(known_line) | dim,
|
||||
filler(),
|
||||
hbox({filler(), button->Render(), filler()}),
|
||||
}) | size(WIDTH, EQUAL, 40);
|
||||
|
||||
Element status = settype_status.empty()
|
||||
? text("") | dim
|
||||
: text(" " + settype_status) | bold;
|
||||
|
||||
return vbox({
|
||||
hbox({left, separator(), middle, separator(), right}) | flex,
|
||||
separator(),
|
||||
status,
|
||||
text(" Tab: cycle focus | Enter on [Apply]: apply (stay) | Esc: leave ") | dim,
|
||||
}) | border;
|
||||
});
|
||||
}
|
||||
236
src/tui/shell.cpp
Normal file
236
src/tui/shell.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <system_error>
|
||||
|
||||
void Tui::Print(const std::string &line) {
|
||||
output.push_back(line);
|
||||
}
|
||||
|
||||
void Tui::HistoryUp() {
|
||||
if (history.empty()) return;
|
||||
if (history_idx == -1) history_idx = (int)history.size() - 1;
|
||||
else if (history_idx > 0) history_idx--;
|
||||
input = history[history_idx];
|
||||
cursor_pos = (int)input.size();
|
||||
}
|
||||
|
||||
void Tui::HistoryDown() {
|
||||
if (history_idx == -1) return;
|
||||
history_idx++;
|
||||
if (history_idx >= (int)history.size()) {
|
||||
history_idx = -1;
|
||||
input.clear();
|
||||
} else {
|
||||
input = history[history_idx];
|
||||
}
|
||||
cursor_pos = (int)input.size();
|
||||
}
|
||||
|
||||
void Tui::CancelPending() {
|
||||
if (pending.empty()) return;
|
||||
pending.clear();
|
||||
input.clear();
|
||||
cursor_pos = 0;
|
||||
history_idx = -1;
|
||||
Print("(cancelled)");
|
||||
}
|
||||
|
||||
std::vector<std::string> Tui::Tokenize(const std::string &s) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
bool in_q = false;
|
||||
for (char c : s) {
|
||||
if (c == '"') { in_q = !in_q; continue; }
|
||||
if (!in_q && std::isspace((unsigned char)c)) {
|
||||
if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); }
|
||||
} else {
|
||||
cur.push_back(c);
|
||||
}
|
||||
}
|
||||
if (!cur.empty()) out.push_back(std::move(cur));
|
||||
return out;
|
||||
}
|
||||
|
||||
void Tui::Submit() {
|
||||
if (!pending.empty()) {
|
||||
if (input.empty()) { Print("(empty — Esc to cancel)"); return; }
|
||||
Prompt p = std::move(pending.front());
|
||||
pending.pop_front();
|
||||
Print(" " + p.question + ": " + input);
|
||||
std::string answer = std::move(input);
|
||||
input.clear();
|
||||
cursor_pos = 0;
|
||||
history_idx = -1;
|
||||
p.on_answer(answer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.empty()) return;
|
||||
history_idx = -1;
|
||||
Print("> " + input);
|
||||
std::string raw = std::move(input);
|
||||
input.clear();
|
||||
cursor_pos = 0;
|
||||
Dispatch(raw);
|
||||
}
|
||||
|
||||
void Tui::Dispatch(const std::string &raw) {
|
||||
auto tokens = Tokenize(raw);
|
||||
if (tokens.empty()) return;
|
||||
|
||||
auto it = commands.find(tokens[0]);
|
||||
if (it == commands.end()) {
|
||||
Print("unknown command: " + tokens[0]);
|
||||
if (!in_source) { history.push_back(raw); AppendHistory(raw); }
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string name = it->first;
|
||||
const CommandSpec &spec = it->second;
|
||||
|
||||
if (tokens.size() - 1 > spec.params.size()) {
|
||||
Print("too many arguments for '" + name + "'");
|
||||
if (!in_source) { history.push_back(raw); AppendHistory(raw); }
|
||||
return;
|
||||
}
|
||||
|
||||
auto args = std::make_shared<std::vector<std::string>>(
|
||||
tokens.begin() + 1, tokens.end());
|
||||
|
||||
if (args->size() == spec.params.size() || !spec.prompt_for_missing) {
|
||||
Finalize(name, spec, *args);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = args->size(); i < spec.params.size(); ++i) {
|
||||
bool last = (i + 1 == spec.params.size());
|
||||
const auto ¶m = spec.params[i];
|
||||
pending.push_back({
|
||||
param.name,
|
||||
[this, name, &spec, args, last](const std::string &s) {
|
||||
args->push_back(s);
|
||||
if (last) Finalize(name, spec, *args);
|
||||
},
|
||||
param.completion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Tui::Finalize(const std::string &name,
|
||||
const CommandSpec &spec,
|
||||
const std::vector<std::string> &args) {
|
||||
std::string canonical = name;
|
||||
for (const auto &a : args) {
|
||||
if (a.find_first_of(" \t\"") != std::string::npos)
|
||||
canonical += " \"" + a + "\"";
|
||||
else
|
||||
canonical += " " + a;
|
||||
}
|
||||
if (!in_source) {
|
||||
history.push_back(canonical);
|
||||
AppendHistory(canonical);
|
||||
}
|
||||
spec.action(args);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path HistoryPath() {
|
||||
namespace fs = std::filesystem;
|
||||
#ifdef _WIN32
|
||||
if (const char *p = std::getenv("LOCALAPPDATA"); p && *p)
|
||||
return fs::path(p) / "essim" / "history";
|
||||
if (const char *p = std::getenv("APPDATA"); p && *p)
|
||||
return fs::path(p) / "essim" / "history";
|
||||
if (const char *p = std::getenv("USERPROFILE"); p && *p)
|
||||
return fs::path(p) / "AppData" / "Local" / "essim" / "history";
|
||||
#else
|
||||
if (const char *p = std::getenv("XDG_DATA_HOME"); p && *p)
|
||||
return fs::path(p) / "essim" / "history";
|
||||
if (const char *p = std::getenv("HOME"); p && *p)
|
||||
return fs::path(p) / ".local" / "share" / "essim" / "history";
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Tui::LoadHistory() {
|
||||
auto p = HistoryPath();
|
||||
if (p.empty()) return;
|
||||
std::ifstream f(p);
|
||||
std::string line;
|
||||
while (std::getline(f, line))
|
||||
if (!line.empty()) history.push_back(line);
|
||||
}
|
||||
|
||||
void Tui::Source(const std::string &filename) {
|
||||
std::string expanded = filename;
|
||||
if (!expanded.empty() && expanded[0] == '~') {
|
||||
if (const char *home = std::getenv("HOME"))
|
||||
expanded = std::string(home) + expanded.substr(1);
|
||||
}
|
||||
std::ifstream f(expanded);
|
||||
if (!f) { Print("source failed: cannot open " + filename); return; }
|
||||
|
||||
bool prev = in_source;
|
||||
in_source = true;
|
||||
|
||||
int executed = 0;
|
||||
int lineno = 0;
|
||||
bool aborted = false;
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
++lineno;
|
||||
size_t start = line.find_first_not_of(" \t");
|
||||
if (start == std::string::npos) continue;
|
||||
if (line[start] == '#') continue;
|
||||
std::string trimmed = line.substr(start);
|
||||
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
|
||||
trimmed.pop_back();
|
||||
if (trimmed.empty()) continue;
|
||||
|
||||
input = trimmed;
|
||||
cursor_pos = (int)input.size();
|
||||
Submit();
|
||||
++executed;
|
||||
|
||||
if (screen_idx != 0) {
|
||||
Print("source: line " + std::to_string(lineno)
|
||||
+ " is interactive (would open a screen) — aborting.");
|
||||
screen_idx = 0;
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
in_source = prev;
|
||||
|
||||
if (!aborted)
|
||||
Print("source: " + filename + " (" + std::to_string(executed) + " line(s))");
|
||||
}
|
||||
|
||||
void Tui::AppendHistory(const std::string &cmd) {
|
||||
auto p = HistoryPath();
|
||||
if (p.empty()) return;
|
||||
std::string trimmed = cmd;
|
||||
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
|
||||
trimmed.pop_back();
|
||||
if (trimmed.empty()) return;
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(p.parent_path(), ec);
|
||||
if (ec) return;
|
||||
std::ofstream f(p, std::ios::app);
|
||||
if (f) f << trimmed << '\n';
|
||||
}
|
||||
509
src/tui/tui.cpp
509
src/tui/tui.cpp
@@ -1,29 +1,26 @@
|
||||
#include "tui/tui.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/event.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <system_error>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Tui::Tui()
|
||||
: cursor_pos(0), history_idx(-1), quit(false),
|
||||
: cursor_pos(0), history_idx(-1), quit(false), in_source(false),
|
||||
screen_idx(0),
|
||||
search_types{"parts", "signals"},
|
||||
search_module_idx(0), search_type_idx(0), search_focus_idx(0)
|
||||
search_module_idx(0), search_type_idx(0), search_focus_idx(0),
|
||||
connect_m1_idx(0), connect_m2_idx(0),
|
||||
connect_p1_idx(0), connect_p2_idx(0),
|
||||
connect_focus_idx(0),
|
||||
explore_module_idx(0),
|
||||
explore_types{"parts", "signals", "connections"},
|
||||
explore_type_idx(0), explore_child_idx(0),
|
||||
explore_detail_idx(0), explore_focus_idx(0),
|
||||
settype_m_idx(0), settype_p_idx(0), settype_focus_idx(0)
|
||||
{
|
||||
LoadHistory();
|
||||
RegisterCommands();
|
||||
@@ -32,450 +29,68 @@ Tui::Tui()
|
||||
|
||||
Tui::~Tui() = default;
|
||||
|
||||
void Tui::Print(const std::string &line) {
|
||||
output.push_back(line);
|
||||
}
|
||||
|
||||
void Tui::HistoryUp() {
|
||||
if (history.empty()) return;
|
||||
if (history_idx == -1) history_idx = (int)history.size() - 1;
|
||||
else if (history_idx > 0) history_idx--;
|
||||
input = history[history_idx];
|
||||
cursor_pos = (int)input.size();
|
||||
}
|
||||
|
||||
void Tui::HistoryDown() {
|
||||
if (history_idx == -1) return;
|
||||
history_idx++;
|
||||
if (history_idx >= (int)history.size()) {
|
||||
history_idx = -1;
|
||||
input.clear();
|
||||
} else {
|
||||
input = history[history_idx];
|
||||
}
|
||||
cursor_pos = (int)input.size();
|
||||
}
|
||||
|
||||
void Tui::CancelPending() {
|
||||
if (pending.empty()) return;
|
||||
pending.clear();
|
||||
input.clear();
|
||||
cursor_pos = 0;
|
||||
history_idx = -1;
|
||||
Print("(cancelled)");
|
||||
}
|
||||
|
||||
static std::string ToLower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
return s;
|
||||
}
|
||||
|
||||
std::vector<std::string> Tui::Tokenize(const std::string &s) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
bool in_q = false;
|
||||
for (char c : s) {
|
||||
if (c == '"') { in_q = !in_q; continue; }
|
||||
if (!in_q && std::isspace((unsigned char)c)) {
|
||||
if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); }
|
||||
} else {
|
||||
cur.push_back(c);
|
||||
}
|
||||
}
|
||||
if (!cur.empty()) out.push_back(std::move(cur));
|
||||
return out;
|
||||
}
|
||||
|
||||
void Tui::RegisterCommands() {
|
||||
commands["help"] = { {}, [this](auto &) {
|
||||
Print("Commands (give params inline or fill them in via prompt):");
|
||||
Print(" new create a new (empty) system");
|
||||
Print(" load <module> <file> <mentor|altium|ods> load a module into the system");
|
||||
Print(" search interactive search (parts/signals, live filter)");
|
||||
Print(" clear clear the visualization area");
|
||||
Print(" help show this message");
|
||||
Print(" quit / exit leave essim");
|
||||
Print(" Esc cancel a multi-step prompt");
|
||||
Print(" Tab complete command name / file path");
|
||||
}};
|
||||
commands["clear"] = { {}, [this](auto &) { output.clear(); }};
|
||||
commands["quit"] = { {}, [this](auto &) { quit = true; }};
|
||||
commands["exit"] = { {}, [this](auto &) { quit = true; }};
|
||||
commands["new"] = { {}, [this](auto &) {
|
||||
sys = std::make_unique<System>();
|
||||
Print("system created.");
|
||||
}};
|
||||
commands["load"] = {
|
||||
{{"module name", false},
|
||||
{"filename", true},
|
||||
{"import type [mentor|altium|ods]", false}},
|
||||
[this](const std::vector<std::string> &args) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
std::string ls = args[2];
|
||||
std::transform(ls.begin(), ls.end(), ls.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
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());
|
||||
}
|
||||
}
|
||||
};
|
||||
commands["search"] = { {}, [this](auto &) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
search_modules.clear();
|
||||
for (auto &m : *sys->modules()) search_modules.push_back(m.first);
|
||||
if (search_modules.empty()) { Print("no modules loaded."); return; }
|
||||
search_module_idx = 0;
|
||||
search_type_idx = 0;
|
||||
search_query.clear();
|
||||
search_focus_idx = 0; // start with the query input focused
|
||||
screen_idx = 1;
|
||||
}};
|
||||
}
|
||||
|
||||
void Tui::Submit() {
|
||||
if (!pending.empty()) {
|
||||
if (input.empty()) { Print("(empty — Esc to cancel)"); return; }
|
||||
Prompt p = std::move(pending.front());
|
||||
pending.pop_front();
|
||||
Print(" " + p.question + ": " + input);
|
||||
std::string answer = std::move(input);
|
||||
input.clear();
|
||||
cursor_pos = 0;
|
||||
history_idx = -1;
|
||||
p.on_answer(answer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.empty()) return;
|
||||
history_idx = -1;
|
||||
Print("> " + input);
|
||||
std::string raw = std::move(input);
|
||||
input.clear();
|
||||
cursor_pos = 0;
|
||||
Dispatch(raw);
|
||||
}
|
||||
|
||||
void Tui::Dispatch(const std::string &raw) {
|
||||
auto tokens = Tokenize(raw);
|
||||
if (tokens.empty()) return;
|
||||
|
||||
auto it = commands.find(tokens[0]);
|
||||
if (it == commands.end()) {
|
||||
Print("unknown command: " + tokens[0]);
|
||||
history.push_back(raw);
|
||||
AppendHistory(raw);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string name = it->first;
|
||||
const CommandSpec &spec = it->second;
|
||||
|
||||
if (tokens.size() - 1 > spec.params.size()) {
|
||||
Print("too many arguments for '" + name + "'");
|
||||
history.push_back(raw);
|
||||
AppendHistory(raw);
|
||||
return;
|
||||
}
|
||||
|
||||
auto args = std::make_shared<std::vector<std::string>>(
|
||||
tokens.begin() + 1, tokens.end());
|
||||
|
||||
if (args->size() == spec.params.size()) {
|
||||
Finalize(name, spec, *args);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = args->size(); i < spec.params.size(); ++i) {
|
||||
bool last = (i + 1 == spec.params.size());
|
||||
const auto ¶m = spec.params[i];
|
||||
pending.push_back({
|
||||
param.name,
|
||||
[this, name, &spec, args, last](const std::string &s) {
|
||||
args->push_back(s);
|
||||
if (last) Finalize(name, spec, *args);
|
||||
},
|
||||
param.path_completion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Tui::Finalize(const std::string &name,
|
||||
const CommandSpec &spec,
|
||||
const std::vector<std::string> &args) {
|
||||
std::string canonical = name;
|
||||
for (const auto &a : args) {
|
||||
if (a.find_first_of(" \t\"") != std::string::npos)
|
||||
canonical += " \"" + a + "\"";
|
||||
else
|
||||
canonical += " " + a;
|
||||
}
|
||||
history.push_back(canonical);
|
||||
AppendHistory(canonical);
|
||||
spec.action(args);
|
||||
}
|
||||
|
||||
static std::filesystem::path HistoryPath() {
|
||||
namespace fs = std::filesystem;
|
||||
#ifdef _WIN32
|
||||
if (const char *p = std::getenv("LOCALAPPDATA"); p && *p)
|
||||
return fs::path(p) / "essim" / "history";
|
||||
if (const char *p = std::getenv("APPDATA"); p && *p)
|
||||
return fs::path(p) / "essim" / "history";
|
||||
if (const char *p = std::getenv("USERPROFILE"); p && *p)
|
||||
return fs::path(p) / "AppData" / "Local" / "essim" / "history";
|
||||
#else
|
||||
if (const char *p = std::getenv("XDG_DATA_HOME"); p && *p)
|
||||
return fs::path(p) / "essim" / "history";
|
||||
if (const char *p = std::getenv("HOME"); p && *p)
|
||||
return fs::path(p) / ".local" / "share" / "essim" / "history";
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
void Tui::LoadHistory() {
|
||||
auto p = HistoryPath();
|
||||
if (p.empty()) return;
|
||||
std::ifstream f(p);
|
||||
std::string line;
|
||||
while (std::getline(f, line))
|
||||
if (!line.empty()) history.push_back(line);
|
||||
}
|
||||
|
||||
void Tui::AppendHistory(const std::string &cmd) {
|
||||
auto p = HistoryPath();
|
||||
if (p.empty()) return;
|
||||
std::string trimmed = cmd;
|
||||
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
|
||||
trimmed.pop_back();
|
||||
if (trimmed.empty()) return;
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(p.parent_path(), ec);
|
||||
if (ec) return;
|
||||
std::ofstream f(p, std::ios::app);
|
||||
if (f) f << trimmed << '\n';
|
||||
}
|
||||
|
||||
static std::string LongestCommonPrefix(const std::vector<std::string> &v) {
|
||||
if (v.empty()) return "";
|
||||
std::string lcp = v[0];
|
||||
for (size_t i = 1; i < v.size(); ++i) {
|
||||
size_t k = 0;
|
||||
while (k < lcp.size() && k < v[i].size() && lcp[k] == v[i][k]) ++k;
|
||||
lcp.resize(k);
|
||||
}
|
||||
return lcp;
|
||||
}
|
||||
|
||||
void Tui::CompleteCommand() {
|
||||
std::vector<std::string> matches;
|
||||
for (const auto &kv : commands)
|
||||
if (kv.first.rfind(input, 0) == 0) matches.push_back(kv.first);
|
||||
|
||||
if (matches.empty()) return;
|
||||
if (matches.size() == 1) { input = matches[0]; cursor_pos = (int)input.size(); return; }
|
||||
|
||||
std::string lcp = LongestCommonPrefix(matches);
|
||||
if (lcp.size() > input.size()) { input = lcp; cursor_pos = (int)input.size(); return; }
|
||||
|
||||
std::string line = " ";
|
||||
for (const auto &m : matches) line += " " + m;
|
||||
Print(line);
|
||||
}
|
||||
|
||||
void Tui::CompletePath() {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
auto pos = input.rfind('/');
|
||||
std::string disp, prefix;
|
||||
if (pos == std::string::npos) { disp = ""; prefix = input; }
|
||||
else { disp = input.substr(0, pos + 1); prefix = input.substr(pos + 1); }
|
||||
|
||||
std::string resolved = disp.empty() ? "." : disp;
|
||||
if (!resolved.empty() && resolved[0] == '~') {
|
||||
if (const char *home = std::getenv("HOME"))
|
||||
resolved = std::string(home) + resolved.substr(1);
|
||||
}
|
||||
|
||||
std::vector<std::string> names;
|
||||
std::vector<bool> is_dir;
|
||||
try {
|
||||
for (const auto &e : fs::directory_iterator(resolved)) {
|
||||
std::string n = e.path().filename().string();
|
||||
if (n.rfind(prefix, 0) == 0) {
|
||||
names.push_back(n);
|
||||
is_dir.push_back(e.is_directory());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception &) {
|
||||
return;
|
||||
}
|
||||
if (names.empty()) return;
|
||||
|
||||
if (names.size() == 1) {
|
||||
input = disp + names[0] + (is_dir[0] ? "/" : "");
|
||||
cursor_pos = (int)input.size();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string lcp = LongestCommonPrefix(names);
|
||||
if (lcp.size() > prefix.size()) { input = disp + lcp; cursor_pos = (int)input.size(); return; }
|
||||
|
||||
std::string line = " ";
|
||||
for (size_t i = 0; i < names.size(); ++i)
|
||||
line += " " + names[i] + (is_dir[i] ? "/" : "");
|
||||
Print(line);
|
||||
}
|
||||
|
||||
void Tui::Run() {
|
||||
auto screen = ScreenInteractive::Fullscreen();
|
||||
|
||||
// ---- Main TUI ----
|
||||
InputOption opt;
|
||||
opt.multiline = false;
|
||||
opt.cursor_position = &cursor_pos;
|
||||
opt.on_enter = [this] { Submit(); };
|
||||
opt.transform = [](InputState s) {
|
||||
auto el = s.element;
|
||||
if (s.is_placeholder) el |= dim;
|
||||
return el;
|
||||
};
|
||||
auto input_component = Input(&input, "type a command…", opt);
|
||||
auto main_screen = BuildMainScreen(screen);
|
||||
auto search_screen = BuildSearchScreen();
|
||||
auto connect_screen = BuildConnectScreen();
|
||||
auto settype_screen = BuildSettypeScreen();
|
||||
auto explore_screen = BuildExploreScreen();
|
||||
|
||||
auto main_renderer = Renderer(input_component, [this, &screen, input_component] {
|
||||
if (quit) screen.Exit();
|
||||
|
||||
Elements lines;
|
||||
for (const auto &l : output) lines.push_back(text(l));
|
||||
auto view = vbox(std::move(lines))
|
||||
| focusPositionRelative(0, 1)
|
||||
| yframe
|
||||
| flex;
|
||||
|
||||
std::string label = pending.empty()
|
||||
? "> "
|
||||
: pending.front().question + "? ";
|
||||
|
||||
return vbox({
|
||||
view,
|
||||
separator(),
|
||||
hbox({text(label), input_component->Render()}),
|
||||
}) | border;
|
||||
});
|
||||
|
||||
// ---- Search screen ----
|
||||
InputOption query_opt;
|
||||
query_opt.multiline = false;
|
||||
query_opt.transform = opt.transform;
|
||||
auto query_input = Input(&search_query, "filter…", query_opt);
|
||||
auto module_menu = Menu(&search_modules, &search_module_idx);
|
||||
auto type_menu = Menu(&search_types, &search_type_idx);
|
||||
|
||||
auto search_components = Container::Vertical(
|
||||
{query_input, module_menu, type_menu}, &search_focus_idx);
|
||||
|
||||
auto search_renderer = Renderer(search_components,
|
||||
[this, query_input, module_menu, type_menu] {
|
||||
// Compute filtered list.
|
||||
Elements result_lines;
|
||||
int total = 0;
|
||||
if (!search_modules.empty() && sys) {
|
||||
const std::string &mname = search_modules[search_module_idx];
|
||||
try {
|
||||
Module *mod = sys->modules()->get(mname);
|
||||
std::string needle = ToLower(search_query);
|
||||
if (search_type_idx == 0) { // parts
|
||||
for (auto &pkv : *mod) {
|
||||
if (needle.empty()
|
||||
|| ToLower(pkv.first).find(needle) != std::string::npos) {
|
||||
result_lines.push_back(
|
||||
text(" " + pkv.first
|
||||
+ " (" + std::to_string(pkv.second->size()) + " pins)"));
|
||||
++total;
|
||||
}
|
||||
}
|
||||
} else { // signals
|
||||
for (auto &skv : *mod->signals) {
|
||||
if (needle.empty()
|
||||
|| ToLower(skv.first).find(needle) != std::string::npos) {
|
||||
result_lines.push_back(
|
||||
text(" " + skv.first
|
||||
+ " (" + std::to_string(skv.second->size()) + " pins)"));
|
||||
++total;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception &) {}
|
||||
}
|
||||
|
||||
auto left = vbox({
|
||||
text("module") | bold,
|
||||
module_menu->Render() | yframe | flex,
|
||||
separator(),
|
||||
text("type") | bold,
|
||||
type_menu->Render(),
|
||||
}) | size(WIDTH, EQUAL, 28);
|
||||
|
||||
auto right = vbox({
|
||||
hbox({text(" search: "), query_input->Render() | flex}) | border,
|
||||
text(std::to_string(total) + " match(es)") | dim,
|
||||
vbox(std::move(result_lines)) | yframe | flex,
|
||||
}) | flex;
|
||||
|
||||
return vbox({
|
||||
hbox({left, separator(), right}) | flex,
|
||||
text(" Tab: cycle focus | Esc: leave search ") | dim,
|
||||
}) | border;
|
||||
});
|
||||
|
||||
// ---- Screen tab + global key handling ----
|
||||
auto tab = Container::Tab({main_renderer, search_renderer}, &screen_idx);
|
||||
auto tab = Container::Tab(
|
||||
{main_screen, search_screen, connect_screen, settype_screen, explore_screen},
|
||||
&screen_idx);
|
||||
|
||||
auto root = CatchEvent(tab, [this](Event e) {
|
||||
if (screen_idx == 1) {
|
||||
// Search mode
|
||||
switch (screen_idx) {
|
||||
case 4: // explore
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
// Cycle focus query → modules → type → query (Menu eats Tab otherwise).
|
||||
if (e == Event::Tab) {
|
||||
search_focus_idx = (search_focus_idx + 1) % 3;
|
||||
return true;
|
||||
}
|
||||
if (e == Event::TabReverse) {
|
||||
search_focus_idx = (search_focus_idx + 2) % 3;
|
||||
return true;
|
||||
}
|
||||
return false; // let menus / input handle the rest
|
||||
}
|
||||
if (e == Event::Tab) { explore_focus_idx = (explore_focus_idx + 1) % 6; return true; }
|
||||
if (e == Event::TabReverse) { explore_focus_idx = (explore_focus_idx + 5) % 6; return true; }
|
||||
return false;
|
||||
|
||||
// Main mode
|
||||
if (e == Event::Escape && !pending.empty()) { CancelPending(); return true; }
|
||||
if (e == Event::ArrowUp || e == Event::ArrowDown) {
|
||||
if (pending.empty()) {
|
||||
if (e == Event::ArrowUp) HistoryUp();
|
||||
else HistoryDown();
|
||||
case 3: // set-type
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
if (e == Event::Tab) { settype_focus_idx = (settype_focus_idx + 1) % 5; return true; }
|
||||
if (e == Event::TabReverse) { settype_focus_idx = (settype_focus_idx + 4) % 5; return true; }
|
||||
return false;
|
||||
|
||||
case 2: // connect
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
if (e == Event::Tab) { connect_focus_idx = (connect_focus_idx + 1) % 7; return true; }
|
||||
if (e == Event::TabReverse) { connect_focus_idx = (connect_focus_idx + 6) % 7; return true; }
|
||||
return false;
|
||||
|
||||
case 1: // search
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
if (e == Event::Tab) { search_focus_idx = (search_focus_idx + 1) % 3; return true; }
|
||||
if (e == Event::TabReverse) { search_focus_idx = (search_focus_idx + 2) % 3; return true; }
|
||||
return false;
|
||||
|
||||
default: // main
|
||||
if (e == Event::Escape && !pending.empty()) { CancelPending(); return true; }
|
||||
if (e == Event::ArrowUp || e == Event::ArrowDown) {
|
||||
if (pending.empty()) {
|
||||
if (e == Event::ArrowUp) HistoryUp();
|
||||
else HistoryDown();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (e == Event::Tab) {
|
||||
if (pending.empty()) {
|
||||
if (input.find(' ') == std::string::npos) CompleteCommand();
|
||||
} else if (pending.front().path_completion) {
|
||||
CompletePath();
|
||||
if (e == Event::Tab) {
|
||||
if (pending.empty()) {
|
||||
CompleteInline();
|
||||
} else {
|
||||
switch (pending.front().completion) {
|
||||
case Completion::Path: CompletePath(); break;
|
||||
case Completion::Command: CompleteCommand(); break;
|
||||
case Completion::None: break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
screen.Loop(root);
|
||||
|
||||
@@ -8,36 +8,50 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
|
||||
class System;
|
||||
|
||||
class Tui {
|
||||
enum class Completion { None, Path, Command };
|
||||
|
||||
struct Prompt {
|
||||
std::string question;
|
||||
std::function<void(const std::string &)> on_answer;
|
||||
bool path_completion = false;
|
||||
Completion completion = Completion::None;
|
||||
};
|
||||
|
||||
struct CommandSpec {
|
||||
struct Param {
|
||||
std::string name;
|
||||
bool path_completion = false;
|
||||
Completion completion = Completion::None;
|
||||
};
|
||||
std::vector<Param> params;
|
||||
std::function<void(const std::vector<std::string> &)> action;
|
||||
bool prompt_for_missing = true;
|
||||
std::string description;
|
||||
bool scriptable = true;
|
||||
};
|
||||
|
||||
// ---- Shell state ----
|
||||
std::vector<std::string> history;
|
||||
std::vector<std::string> recorded; // commands since the last 'new', for script-save
|
||||
std::vector<std::string> output;
|
||||
std::string input;
|
||||
int cursor_pos;
|
||||
int history_idx;
|
||||
bool quit;
|
||||
bool in_source;
|
||||
|
||||
std::unique_ptr<System> sys;
|
||||
std::deque<Prompt> pending;
|
||||
std::map<std::string, CommandSpec> commands;
|
||||
|
||||
// ---- Screen orchestration ----
|
||||
int screen_idx;
|
||||
|
||||
// ---- Search screen state ----
|
||||
std::vector<std::string> search_modules;
|
||||
std::vector<std::string> search_types;
|
||||
int search_module_idx;
|
||||
@@ -45,31 +59,84 @@ class Tui {
|
||||
int search_focus_idx;
|
||||
std::string search_query;
|
||||
|
||||
// ---- Connect screen state ----
|
||||
std::vector<std::string> connect_modules;
|
||||
int connect_m1_idx;
|
||||
int connect_m2_idx;
|
||||
std::string connect_p1_filter;
|
||||
std::string connect_p2_filter;
|
||||
std::vector<std::string> connect_p1_list;
|
||||
std::vector<std::string> connect_p2_list;
|
||||
int connect_p1_idx;
|
||||
int connect_p2_idx;
|
||||
int connect_focus_idx;
|
||||
|
||||
// ---- Explore screen state ----
|
||||
std::vector<std::string> explore_modules;
|
||||
int explore_module_idx;
|
||||
std::vector<std::string> explore_types;
|
||||
int explore_type_idx;
|
||||
std::vector<std::string> explore_children;
|
||||
int explore_child_idx;
|
||||
std::string explore_child_filter;
|
||||
std::string explore_detail_filter;
|
||||
std::vector<std::string> explore_detail;
|
||||
int explore_detail_idx;
|
||||
std::string explore_header;
|
||||
int explore_focus_idx;
|
||||
|
||||
// ---- Set-type screen state ----
|
||||
std::vector<std::string> settype_modules;
|
||||
int settype_m_idx;
|
||||
std::string settype_p_filter;
|
||||
std::vector<std::string> settype_p_list;
|
||||
int settype_p_idx;
|
||||
std::string settype_type;
|
||||
std::string settype_status;
|
||||
int settype_focus_idx;
|
||||
|
||||
public:
|
||||
Tui();
|
||||
~Tui();
|
||||
void Run();
|
||||
|
||||
private:
|
||||
// Lifecycle (commands.cpp)
|
||||
void RegisterCommands();
|
||||
|
||||
// Shell (shell.cpp)
|
||||
void Print(const std::string &line);
|
||||
void Submit();
|
||||
void Dispatch(const std::string &raw);
|
||||
void Finalize(const std::string &name,
|
||||
const CommandSpec &spec,
|
||||
const std::vector<std::string> &args);
|
||||
|
||||
void HistoryUp();
|
||||
void HistoryDown();
|
||||
void CancelPending();
|
||||
void Print(const std::string &line);
|
||||
|
||||
void CompleteCommand();
|
||||
void CompletePath();
|
||||
|
||||
void LoadHistory();
|
||||
void AppendHistory(const std::string &cmd);
|
||||
|
||||
void Source(const std::string &filename);
|
||||
static std::vector<std::string> Tokenize(const std::string &s);
|
||||
|
||||
// Completion (completion.cpp)
|
||||
void CompleteCommand(size_t start = 0);
|
||||
void CompletePath(size_t start = 0);
|
||||
void CompleteInline();
|
||||
|
||||
// Filtered part list rebuild (used by connect & set-type screens)
|
||||
void RefreshFilteredPartList(const std::vector<std::string> &modules,
|
||||
int m_idx,
|
||||
const std::string &filter,
|
||||
std::vector<std::string> &out,
|
||||
int &sel_idx);
|
||||
|
||||
// Screen builders (screen_*.cpp)
|
||||
ftxui::Component BuildMainScreen(ftxui::ScreenInteractive &screen);
|
||||
ftxui::Component BuildSearchScreen();
|
||||
ftxui::Component BuildConnectScreen();
|
||||
ftxui::Component BuildSettypeScreen();
|
||||
ftxui::Component BuildExploreScreen();
|
||||
};
|
||||
|
||||
#endif // _TUI_HPP_
|
||||
|
||||
54
src/tui/tui_helpers.cpp
Normal file
54
src/tui/tui_helpers.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#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); });
|
||||
return s;
|
||||
}
|
||||
|
||||
bool NaturalLess(const std::string &a, const std::string &b) {
|
||||
size_t i = 0, j = 0;
|
||||
while (i < a.size() && j < b.size()) {
|
||||
unsigned char ca = (unsigned char)a[i];
|
||||
unsigned char cb = (unsigned char)b[j];
|
||||
if (std::isdigit(ca) && std::isdigit(cb)) {
|
||||
size_t za = 0, zb = 0;
|
||||
while (i + za < a.size() && a[i + za] == '0') ++za;
|
||||
while (j + zb < b.size() && b[j + zb] == '0') ++zb;
|
||||
size_t ea = i + za;
|
||||
while (ea < a.size() && std::isdigit((unsigned char)a[ea])) ++ea;
|
||||
size_t eb = j + zb;
|
||||
while (eb < b.size() && std::isdigit((unsigned char)b[eb])) ++eb;
|
||||
size_t la = ea - (i + za);
|
||||
size_t lb = eb - (j + zb);
|
||||
if (la != lb) return la < lb;
|
||||
int cmp = a.compare(i + za, la, b, j + zb, lb);
|
||||
if (cmp != 0) return cmp < 0;
|
||||
if (za != zb) return za > zb;
|
||||
i = ea;
|
||||
j = eb;
|
||||
} else {
|
||||
char la = (char)std::tolower(ca);
|
||||
char lb = (char)std::tolower(cb);
|
||||
if (la != lb) return la < lb;
|
||||
++i; ++j;
|
||||
}
|
||||
}
|
||||
if (i < a.size()) return false;
|
||||
if (j < b.size()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string LongestCommonPrefix(const std::vector<std::string> &v) {
|
||||
if (v.empty()) return "";
|
||||
std::string lcp = v[0];
|
||||
for (size_t i = 1; i < v.size(); ++i) {
|
||||
size_t k = 0;
|
||||
while (k < lcp.size() && k < v[i].size() && lcp[k] == v[i][k]) ++k;
|
||||
lcp.resize(k);
|
||||
}
|
||||
return lcp;
|
||||
}
|
||||
17
src/tui/tui_helpers.hpp
Normal file
17
src/tui/tui_helpers.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef _TUI_HELPERS_HPP_
|
||||
#define _TUI_HELPERS_HPP_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Free helpers shared across the TUI translation units.
|
||||
|
||||
std::string ToLower(std::string s);
|
||||
|
||||
// Case-insensitive natural-order comparison: digit runs compared as integers,
|
||||
// letters compared after std::tolower.
|
||||
bool NaturalLess(const std::string &a, const std::string &b);
|
||||
|
||||
std::string LongestCommonPrefix(const std::vector<std::string> &v);
|
||||
|
||||
#endif // _TUI_HELPERS_HPP_
|
||||
Reference in New Issue
Block a user