From 3010bb25ebd8a85a6531cd6b877865e24fa33c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 19:23:41 +0200 Subject: [PATCH 01/11] core/ui: extract export into src/app (frontend-agnostic), thin TUI command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First step of separating business logic from the TUI. The export command built the CSV/ODS file inside its lambda, mixed with Print/ShowError/dialog calls. Move all of it — CSV + ODS building, sheet-name sanitising, file writing — into src/app/export.{hpp,cpp} (namespace app, no FTXUI/console dependency): export_connections(const System*, path, format) -> ExportResult. The TUI command is now a thin wrapper (resolve args/dialog, call the core, render). The core is unit-tested without any UI (test_export); 342 assertions pass. Co-Authored-By: Claude Opus 4.8 --- src/app/export.cpp | 190 +++++++++++++++++++++++++++++++ src/app/export.hpp | 34 ++++++ src/tui/commands_export.cpp | 219 +++++------------------------------- tests/test_export.cpp | 76 +++++++++++++ 4 files changed, 327 insertions(+), 192 deletions(-) create mode 100644 src/app/export.cpp create mode 100644 src/app/export.hpp create mode 100644 tests/test_export.cpp diff --git a/src/app/export.cpp b/src/app/export.cpp new file mode 100644 index 0000000..c828b6d --- /dev/null +++ b/src/app/export.cpp @@ -0,0 +1,190 @@ +#include "app/export.hpp" + +#include "imports/ods_writer.hpp" +#include "system/connect.hpp" +#include "system/modules.hpp" +#include "system/parts.hpp" +#include "system/pins.hpp" +#include "system/signal_type.hpp" +#include "system/signals.hpp" +#include "system/system.hpp" + +#include +#include +#include + +namespace app { + +namespace { + +std::string to_lower(std::string s) +{ + for (char &c : s) c = (char)std::tolower((unsigned char)c); + return s; +} + +// Minimal CSV quoter — wraps in "…" and doubles internal quotes when the field +// contains a comma, quote, or newline. +std::string csv_quote(const std::string &s) +{ + if (s.find_first_of(",\"\n") == std::string::npos) + return s; + std::string out = "\""; + for (char c : s) { if (c == '"') out += '"'; out += c; } + out += '"'; + return out; +} + +// Flatten one pin into the string slots an export row uses. +void pin_side(Pin *p, std::string &mod, std::string &part, std::string &pin, + std::string &sig, std::string &type, std::string &suspect) +{ + if (!p) { mod = part = pin = sig = type = suspect = ""; return; } + mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : ""; + part = p->prnt ? p->prnt->name : ""; + pin = p->name; + Signal *s = p->signal(); + if (!s) { + sig = ""; type = "(NC)"; suspect = ""; + } else { + sig = s->name; + type = signal_type_name(s->type); + suspect = (infer_signal_type(s->name) == SignalType::Power + && s->type == SignalType::Other) ? "yes" : "no"; + } +} + +// Excel rejects /\?*:[] in sheet names; ODS forbids < > & in raw table names. +// Sanitise to underscores and clip to Excel's 31-char hard limit. +std::string sanitise_sheet_name(std::string name) +{ + for (char &ch : name) + if (ch == '/' || ch == '\\' || ch == '?' || ch == '*' || ch == ':' + || ch == '[' || ch == ']' || ch == '<' || ch == '>' || ch == '&') + ch = '_'; + if (name.size() > 31) name = name.substr(0, 31); + return name; +} + +ExportResult write_ods(const System *sys, const std::string &path) +{ + ExportResult r; + OdsWriter w; + for (auto &ckv : *sys->connections()) { + Connection *c = ckv.second; + OdsSheet *s = w.add_sheet(sanitise_sheet_name(c->name)); + + std::string lmod = c->m1 ? c->m1->name : ""; + std::string lprt = c->p1 ? c->p1->name : ""; + std::string rmod = c->m2 ? c->m2->name : ""; + std::string rprt = c->p2 ? c->p2->name : ""; + + // Meta header: label/value rows, a blank row, then the column headers. + auto meta = [&](int row, const std::string &k, const std::string &v) { + s->set(row, 0, k); + s->set(row, 1, v); + }; + meta(0, "Connection", c->name); + meta(1, "Transform", c->transform_name); + meta(2, "Left", lmod + " / " + lprt); + meta(3, "Right", rmod + " / " + rprt); + // Row 4 left blank by design. + + const int HDR = 5; + s->set_header_row(HDR); + const char *hdr[] = { + "left_pin", "left_signal", "left_type", "left_suspect", + "right_pin", "right_signal", "right_type", "right_suspect", + "type_mismatch"}; + for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]); + + int row = HDR + 1; + for (auto &wp : c->pin_map) { + std::string lm, lp, ln, ls, lt, lsus; + std::string rm, rp, rn, rs, rt, rsus; + pin_side(wp.first, lm, lp, ln, ls, lt, lsus); + pin_side(wp.second, rm, rp, rn, rs, rt, rsus); + std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no"; + s->set(row, 0, ln); s->set(row, 1, ls); + s->set(row, 2, lt); s->set(row, 3, lsus); + s->set(row, 4, rn); s->set(row, 5, rs); + s->set(row, 6, rt); s->set(row, 7, rsus); + s->set(row, 8, tm); + ++row; ++r.rows; + } + } + + std::string err; + if (!w.save(path, err)) { + r.error = err; + return r; + } + r.ok = true; + r.sheets = (int)sys->connections()->size(); + return r; +} + +ExportResult write_csv(const System *sys, const std::string &path) +{ + ExportResult r; + std::ofstream f(path); + if (!f) { + r.error = "cannot open '" + path + "' for writing"; + return r; + } + + // One rectangular table: each row repeats the per-connection constants so + // the file stays parser-friendly (pandas / awk / spreadsheet). + f << "connection,transform," + "left_module,left_part," + "left_pin,left_signal,left_type,left_suspect," + "right_module,right_part," + "right_pin,right_signal,right_type,right_suspect," + "type_mismatch\n"; + + for (auto &ckv : *sys->connections()) { + Connection *c = ckv.second; + for (auto &wp : c->pin_map) { + std::string lm, lp, ln, ls, lt, lsus; + std::string rm, rp, rn, rs, rt, rsus; + pin_side(wp.first, lm, lp, ln, ls, lt, lsus); + pin_side(wp.second, rm, rp, rn, rs, rt, rsus); + std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no"; + f << csv_quote(c->name) << ',' + << csv_quote(c->transform_name) << ',' + << csv_quote(lm) << ',' << csv_quote(lp) << ',' + << csv_quote(ln) << ',' << csv_quote(ls) << ',' + << csv_quote(lt) << ',' << csv_quote(lsus) << ',' + << csv_quote(rm) << ',' << csv_quote(rp) << ',' + << csv_quote(rn) << ',' << csv_quote(rs) << ',' + << csv_quote(rt) << ',' << csv_quote(rsus) << ',' + << tm << '\n'; + ++r.rows; + } + } + r.ok = f.good(); + if (!r.ok) r.error = "write error on '" + path + "'"; + return r; +} + +} // namespace + +bool export_format_from_path(const std::string &path, ExportFormat &out) +{ + size_t dot = path.rfind('.'); + std::string ext = (dot == std::string::npos) ? "" : to_lower(path.substr(dot)); + if (ext == ".csv") { out = ExportFormat::Csv; return true; } + if (ext == ".ods") { out = ExportFormat::Ods; return true; } + return false; +} + +ExportResult export_connections(const System *sys, const std::string &path, + ExportFormat format) +{ + ExportResult r; + if (!sys) { r.error = "no system"; return r; } + return (format == ExportFormat::Ods) ? write_ods(sys, path) + : write_csv(sys, path); +} + +} // namespace app diff --git a/src/app/export.hpp b/src/app/export.hpp new file mode 100644 index 0000000..c756c2b --- /dev/null +++ b/src/app/export.hpp @@ -0,0 +1,34 @@ +#ifndef _APP_EXPORT_HPP_ +#define _APP_EXPORT_HPP_ + +#include + +class System; + +// Application layer: UI-independent operations that any frontend (TUI, GUI, …) +// can call. No console, no dialogs, no FTXUI — just System in, result out. +namespace app { + +enum class ExportFormat { Csv, Ods }; + +// Outcome of an export. The only side effect is writing the target file; the +// caller renders `error` / the stats however it likes. +struct ExportResult { + bool ok = false; + std::string error; ///< human-readable, set when !ok + int sheets = 0; ///< ODS: number of sheets (one per connection); 0 for CSV + int rows = 0; ///< wires written +}; + +// Map a filename extension (.csv / .ods, case-insensitive) to a format. +// Returns false if the extension is neither. +bool export_format_from_path(const std::string &path, ExportFormat &out); + +// Export the system's connections to `path` in `format`. Builds the file and +// returns stats or an error. Pure core — safe to call from any frontend. +ExportResult export_connections(const System *sys, const std::string &path, + ExportFormat format); + +} // namespace app + +#endif // _APP_EXPORT_HPP_ diff --git a/src/tui/commands_export.cpp b/src/tui/commands_export.cpp index 1c50c33..75cf1b2 100644 --- a/src/tui/commands_export.cpp +++ b/src/tui/commands_export.cpp @@ -1,64 +1,25 @@ #include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" -#include "imports/ods_writer.hpp" +#include "app/export.hpp" #include "system/connect.hpp" -#include "system/modules.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/signal_type.hpp" -#include "system/signals.hpp" #include "system/system.hpp" -#include #include #include -namespace { - -// Minimal CSV quoter — wraps in `"…"` and doubles internal quotes when -// the field contains a comma, quote, or newline. Local to this file. -std::string csv_quote(const std::string &s) { - bool needs = s.find_first_of(",\"\n") != std::string::npos; - if (!needs) return s; - std::string out = "\""; - for (char c : s) { if (c == '"') out += '"'; out += c; } - out += '"'; - return out; -} - -// Flatten one pin into the 6 string slots the export row uses. -void pin_side(Pin *p, std::string &mod, std::string &part, - std::string &pin, std::string &sig, - std::string &type, std::string &suspect) { - if (!p) { mod = part = pin = sig = type = suspect = ""; return; } - mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : ""; - part = p->prnt ? p->prnt->name : ""; - pin = p->name; - Signal *s = p->signal(); - if (!s) { - sig = ""; type = "(NC)"; suspect = ""; - } else { - sig = s->name; - type = signal_type_name(s->type); - suspect = (infer_signal_type(s->name) == SignalType::Power - && s->type == SignalType::Other) ? "yes" : "no"; - } -} - -} // namespace - +// Thin UI wrapper around app::export_connections — this file only resolves +// arguments / the file dialog and renders the result. All the actual export +// (CSV / ODS building, file writing) lives in src/app/export.cpp. void Tui::RegisterExportCommands() { commands["export"] = { {{"kind [connections]", Completion::None}, {"filename (.csv)", Completion::Path}}, [this](const std::vector &args) { if (!sys) { Print("no system: run 'new' first."); return; } + if (args.empty()) { - // Bare → reuse the generic file dialog. Filters give a - // one-keystroke CSV/ODS toggle; picking either rewrites - // the filename's extension, and the action below - // dispatches on that extension. + // Bare → the generic file dialog. The CSV/ODS filter rewrites + // the extension; the action below dispatches on it. OpenFileDialog( "Export — connections", "export.connections", @@ -76,155 +37,29 @@ void Tui::RegisterExportCommands() { const std::string &kind = args[0]; const std::string &path = args[1]; - if (kind == "connections") { - // Accepted extensions: `.csv` (flat file) and `.ods` - // (one sheet per connection). Anything else is an error. - std::string ext; - { - size_t dot = path.rfind('.'); - if (dot != std::string::npos) ext = ToLower(path.substr(dot)); - } - bool ods = (ext == ".ods"); - bool csv = (ext == ".csv"); - if (!ods && !csv) { - ShowError("export: unknown extension '" - + (ext.empty() ? std::string("(none)") : ext) - + "'\nAccepted: .csv, .ods"); - return; - } - - if (ods) { - OdsWriter w; - int total = 0; - for (auto &ckv : *sys->connections()) { - Connection *c = ckv.second; - // Sheet names: Excel rejects /\?*:[] characters, - // ODS forbids < > & in raw cell/table names. - // Sanitise to underscores; clip to 31 chars - // (Excel's hard limit). - std::string sname = c->name; - for (char &ch : sname) - if (ch == '/' || ch == '\\' || ch == '?' || ch == '*' - || ch == ':' || ch == '[' || ch == ']' - || ch == '<' || ch == '>' || ch == '&') ch = '_'; - if (sname.size() > 31) sname = sname.substr(0, 31); - OdsSheet *s = w.add_sheet(sname); - - // Pull the constants for this connection once. - // `transform`, the left module/part, and the - // right module/part don't vary across the wires - // of a single connection — putting them in - // every row was repetitive. - std::string lmod, lprt; - std::string rmod, rprt; - if (c->m1) lmod = c->m1->name; - if (c->p1) lprt = c->p1->name; - if (c->m2) rmod = c->m2->name; - if (c->p2) rprt = c->p2->name; - - // Meta header above the table: 5 rows of label / - // value, then a blank, then the column headers - // on row 6 (index 5). - auto meta = [&](int r, const std::string &k, - const std::string &v) { - s->set(r, 0, k); - s->set(r, 1, v); - }; - meta(0, "Connection", c->name); - meta(1, "Transform", c->transform_name); - meta(2, "Left", lmod + " / " + lprt); - meta(3, "Right", rmod + " / " + rprt); - // Row 4 left blank by design. - - const int HDR = 5; - s->set_header_row(HDR); - const char *hdr[] = { - "left_pin", "left_signal", "left_type", "left_suspect", - "right_pin", "right_signal", "right_type", "right_suspect", - "type_mismatch"}; - for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]); - - int row = HDR + 1; - for (auto &wp : c->pin_map) { - std::string lm, lp, ln, ls, lt, lsus; - std::string rm, rp, rn, rs, rt, rsus; - pin_side(wp.first, lm, lp, ln, ls, lt, lsus); - pin_side(wp.second, rm, rp, rn, rs, rt, rsus); - // `type_mismatch = yes` when both sides have a - // real signal AND their types disagree (e.g. - // Power ↔ Gnd, or Power ↔ Other). - std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) - ? "yes" : "no"; - s->set(row, 0, ln); s->set(row, 1, ls); - s->set(row, 2, lt); s->set(row, 3, lsus); - s->set(row, 4, rn); s->set(row, 5, rs); - s->set(row, 6, rt); s->set(row, 7, rsus); - s->set(row, 8, tm); - ++row; ++total; - } - } - std::string err; - if (!w.save(path, err)) { - ShowError("export (.ods) failed:\n" + err); - return; - } - Print("export connections (.ods): " - + std::to_string(sys->connections()->size()) - + " sheet(s), " + std::to_string(total) - + " wire(s) → " + path); - return; - } - - // Classic flat CSV: a single rectangular table — one - // header line, N data rows, every row carries the per- - // connection constants too. The 9 right-most column - // names match the ODS sheet headers exactly; the 5 - // leading ones (connection, transform, left_module, - // left_part, right_module, right_part) correspond to - // the ODS meta block (Connection, Transform, Left, - // Right). Repeating the constants per row keeps the - // file parser-friendly (pandas / awk / spreadsheet). - std::ofstream f(path); - if (!f) { - ShowError("export: cannot open '" + path + "' for writing"); - return; - } - f << "connection,transform," - "left_module,left_part," - "left_pin,left_signal,left_type,left_suspect," - "right_module,right_part," - "right_pin,right_signal,right_type,right_suspect," - "type_mismatch\n"; - - int rows = 0; - for (auto &ckv : *sys->connections()) { - Connection *c = ckv.second; - for (auto &wp : c->pin_map) { - std::string lm, lp, ln, ls, lt, lsus; - std::string rm, rp, rn, rs, rt, rsus; - pin_side(wp.first, lm, lp, ln, ls, lt, lsus); - pin_side(wp.second, rm, rp, rn, rs, rt, rsus); - std::string tm = "no"; - if (lt != "(NC)" && rt != "(NC)" && lt != rt) tm = "yes"; - f << csv_quote(c->name) << ',' - << csv_quote(c->transform_name) << ',' - << csv_quote(lm) << ',' << csv_quote(lp) << ',' - << csv_quote(ln) << ',' << csv_quote(ls) << ',' - << csv_quote(lt) << ',' << csv_quote(lsus) << ',' - << csv_quote(rm) << ',' << csv_quote(rp) << ',' - << csv_quote(rn) << ',' << csv_quote(rs) << ',' - << csv_quote(rt) << ',' << csv_quote(rsus) << ',' - << tm << '\n'; - ++rows; - } - } - Print("export connections (.csv): " + std::to_string(rows) - + " wire(s) → " + path); + if (kind != "connections") { + ShowError("export: unknown kind '" + kind + "'\n" + "Known kinds: connections"); return; } - ShowError("export: unknown kind '" + kind + "'\n" - "Known kinds: connections"); + app::ExportFormat fmt; + if (!app::export_format_from_path(path, fmt)) { + ShowError("export: unknown extension — accepted: .csv, .ods"); + return; + } + + app::ExportResult r = app::export_connections(sys.get(), path, fmt); + if (!r.ok) { + ShowError("export failed:\n" + r.error); + return; + } + + const bool ods = (fmt == app::ExportFormat::Ods); + std::string msg = ods ? "export connections (.ods): " + : "export connections (.csv): "; + if (ods) msg += std::to_string(r.sheets) + " sheet(s), "; + Print(msg + std::to_string(r.rows) + " wire(s) → " + path); }, /*prompt_for_missing=*/ false, "export structured data to CSV / ODS (kinds: connections; " diff --git a/tests/test_export.cpp b/tests/test_export.cpp new file mode 100644 index 0000000..549fccc --- /dev/null +++ b/tests/test_export.cpp @@ -0,0 +1,76 @@ +#include + +#include "app/export.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 +#include +#include +#include + +namespace { + +std::string slurp(const std::string &path) +{ + std::ifstream f(path); + std::stringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +} // namespace + +TEST_CASE("export_format_from_path maps extensions") { + app::ExportFormat f; + CHECK(app::export_format_from_path("a.csv", f)); + CHECK(f == app::ExportFormat::Csv); + CHECK(app::export_format_from_path("a.ODS", f)); + CHECK(f == app::ExportFormat::Ods); + CHECK_FALSE(app::export_format_from_path("a.txt", f)); + CHECK_FALSE(app::export_format_from_path("noext", f)); +} + +TEST_CASE("export_connections writes a flat CSV (no UI needed)") { + // Two cards, one wired pin pair via a connection. + System sys; + Module *a = sys.modules()->merge("A"); + Module *b = sys.modules()->merge("B"); + Part *ja = new Part("J1"); a->add(ja); + Part *jb = new Part("P1"); b->add(jb); + Pin *pa = new Pin("1"); ja->add(pa); + Pin *pb = new Pin("1"); jb->add(pb); + Signal *sa = a->signals->merge("NETA"); sa->add(pa); pa->connect(sa); + Signal *sb = b->signals->merge("NETB"); sb->add(pb); pb->connect(sb); + + Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb); + c->transform_name = "identity"; + c->pin_map.emplace_back(pa, pb); + sys.connections()->add(c); + + const char *path = "test_export_out.csv"; + app::ExportResult r = app::export_connections(&sys, path, app::ExportFormat::Csv); + CHECK(r.ok); + CHECK(r.rows == 1); + + std::string out = slurp(path); + CHECK(out.find("connection,transform,") == 0); // header present + CHECK(out.find("A.J1<->B.P1") != std::string::npos); // connection name + CHECK(out.find("identity") != std::string::npos); // transform + CHECK(out.find("NETA") != std::string::npos); // left signal + CHECK(out.find("NETB") != std::string::npos); // right signal + + std::remove(path); +} + +TEST_CASE("export_connections reports a bad path instead of crashing") { + System sys; + app::ExportResult r = app::export_connections( + &sys, "/nonexistent-dir-xyz/out.csv", app::ExportFormat::Csv); + CHECK_FALSE(r.ok); + CHECK_FALSE(r.error.empty()); +} From 63ca17d04896b03a5331a94b1106f54174831d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 19:33:06 +0200 Subject: [PATCH 02/11] build: split core/ from frontends/; prepare for multiple GUI/TUI targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganise the tree into business vs frontend as separate directories: src/core/{domain,imports,app} (was system/, imports/, app/) src/frontends/tui/ (was tui/ + main.cpp) tests/tui/ (the FTXUI-coupled helper test) All cross-dir #include paths rewritten; same-dir includes untouched. CMake: essim_core is the frontend-agnostic business library — links libzip, pugixml and bsdl, NO GUI toolkit. Each frontend is a self-contained src/frontends// (own CMakeLists, toolkit, main.cpp) that links essim_core, selected with -DESSIM_FRONTEND= (default tui; 'none' = core + tests only, no toolkit fetched). FTXUI moved into the tui frontend. Tests are split: essim_tests links essim_core (no FTXUI), essim_tui_tests links essim_tui. Verified: default tui build green (ctest 2/2); ESSIM_FRONTEND=none builds the core + tests with FTXUI never fetched and no `essim` binary. Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 95 +++++++++++-------- src/{ => core}/app/export.cpp | 18 ++-- src/{ => core}/app/export.hpp | 0 src/{system => core/domain}/analysis.cpp | 0 src/{system => core/domain}/analysis.hpp | 0 src/{system => core/domain}/bsdl_check.cpp | 0 src/{system => core/domain}/bsdl_check.hpp | 0 src/{system => core/domain}/bsdl_model.cpp | 0 src/{system => core/domain}/bsdl_model.hpp | 0 .../domain}/component_kind.cpp | 2 +- .../domain}/component_kind.hpp | 0 src/{system => core/domain}/connect.cpp | 0 src/{system => core/domain}/connect.hpp | 0 src/{system => core/domain}/modules.cpp | 0 src/{system => core/domain}/modules.hpp | 0 src/{system => core/domain}/nets.cpp | 14 +-- src/{system => core/domain}/nets.hpp | 0 src/{system => core/domain}/parts.cpp | 0 src/{system => core/domain}/parts.hpp | 0 src/{system => core/domain}/persist.cpp | 0 src/{system => core/domain}/persist.hpp | 0 src/{system => core/domain}/pin_model.cpp | 0 src/{system => core/domain}/pin_model.hpp | 0 src/{system => core/domain}/pin_name.cpp | 2 +- src/{system => core/domain}/pin_name.hpp | 0 src/{system => core/domain}/pin_role.cpp | 0 src/{system => core/domain}/pin_role.hpp | 0 src/{system => core/domain}/pin_spec.hpp | 0 src/{system => core/domain}/pins.cpp | 0 src/{system => core/domain}/pins.hpp | 0 src/{system => core/domain}/signal_type.hpp | 0 src/{system => core/domain}/signals.cpp | 0 src/{system => core/domain}/signals.hpp | 0 src/{system => core/domain}/syselmts.hpp | 0 src/{system => core/domain}/system.cpp | 6 +- src/{system => core/domain}/system.hpp | 2 +- src/{system => core/domain}/transform.cpp | 0 src/{system => core/domain}/transform.hpp | 0 src/{system => core/domain}/transform_vpx.cpp | 0 src/{system => core/domain}/transform_vpx.hpp | 0 src/{ => core}/imports/import_altium.cpp | 6 +- src/{ => core}/imports/import_altium.hpp | 0 src/{ => core}/imports/import_base.hpp | 4 +- src/{ => core}/imports/import_mentor.cpp | 4 +- src/{ => core}/imports/import_mentor.hpp | 0 src/{ => core}/imports/import_ods.cpp | 6 +- src/{ => core}/imports/import_ods.hpp | 0 src/{ => core}/imports/ods_writer.cpp | 0 src/{ => core}/imports/ods_writer.hpp | 0 src/frontends/tui/CMakeLists.txt | 35 +++++++ src/{ => frontends}/tui/commands.cpp | 34 +++---- src/{ => frontends}/tui/commands_export.cpp | 8 +- src/{ => frontends}/tui/completion.cpp | 4 +- src/{ => frontends/tui}/main.cpp | 2 +- src/{ => frontends}/tui/screen_analyze.cpp | 22 ++--- src/{ => frontends}/tui/screen_confirm.cpp | 2 +- src/{ => frontends}/tui/screen_connect.cpp | 14 +-- src/{ => frontends}/tui/screen_dashboard.cpp | 22 ++--- src/{ => frontends}/tui/screen_error.cpp | 2 +- src/{ => frontends}/tui/screen_explore.cpp | 18 ++-- src/{ => frontends}/tui/screen_filedialog.cpp | 4 +- src/{ => frontends}/tui/screen_help.cpp | 4 +- src/{ => frontends}/tui/screen_main.cpp | 4 +- src/{ => frontends}/tui/screen_palette.cpp | 10 +- src/{ => frontends}/tui/screen_settype.cpp | 18 ++-- .../tui/screen_sigtype_modal.cpp | 8 +- src/{ => frontends}/tui/shell.cpp | 10 +- src/{ => frontends}/tui/tui.cpp | 4 +- src/{ => frontends}/tui/tui.hpp | 0 src/{ => frontends}/tui/tui_helpers.cpp | 2 +- src/{ => frontends}/tui/tui_helpers.hpp | 0 tests/test_analysis.cpp | 12 +-- tests/test_bsdl_apply.cpp | 18 ++-- tests/test_bsdl_check.cpp | 16 ++-- tests/test_component_kind.cpp | 4 +- tests/test_export.cpp | 14 +-- tests/test_nc_origin.cpp | 12 +-- tests/test_persist.cpp | 14 +-- tests/test_pin_model.cpp | 8 +- tests/test_pin_name.cpp | 8 +- tests/test_signal_type.cpp | 2 +- tests/test_vpx_transform.cpp | 14 +-- tests/{ => tui}/test_helpers.cpp | 2 +- 83 files changed, 282 insertions(+), 228 deletions(-) rename src/{ => core}/app/export.cpp (95%) rename src/{ => core}/app/export.hpp (100%) rename src/{system => core/domain}/analysis.cpp (100%) rename src/{system => core/domain}/analysis.hpp (100%) rename src/{system => core/domain}/bsdl_check.cpp (100%) rename src/{system => core/domain}/bsdl_check.hpp (100%) rename src/{system => core/domain}/bsdl_model.cpp (100%) rename src/{system => core/domain}/bsdl_model.hpp (100%) rename src/{system => core/domain}/component_kind.cpp (98%) rename src/{system => core/domain}/component_kind.hpp (100%) rename src/{system => core/domain}/connect.cpp (100%) rename src/{system => core/domain}/connect.hpp (100%) rename src/{system => core/domain}/modules.cpp (100%) rename src/{system => core/domain}/modules.hpp (100%) rename src/{system => core/domain}/nets.cpp (93%) rename src/{system => core/domain}/nets.hpp (100%) rename src/{system => core/domain}/parts.cpp (100%) rename src/{system => core/domain}/parts.hpp (100%) rename src/{system => core/domain}/persist.cpp (100%) rename src/{system => core/domain}/persist.hpp (100%) rename src/{system => core/domain}/pin_model.cpp (100%) rename src/{system => core/domain}/pin_model.hpp (100%) rename src/{system => core/domain}/pin_name.cpp (94%) rename src/{system => core/domain}/pin_name.hpp (100%) rename src/{system => core/domain}/pin_role.cpp (100%) rename src/{system => core/domain}/pin_role.hpp (100%) rename src/{system => core/domain}/pin_spec.hpp (100%) rename src/{system => core/domain}/pins.cpp (100%) rename src/{system => core/domain}/pins.hpp (100%) rename src/{system => core/domain}/signal_type.hpp (100%) rename src/{system => core/domain}/signals.cpp (100%) rename src/{system => core/domain}/signals.hpp (100%) rename src/{system => core/domain}/syselmts.hpp (100%) rename src/{system => core/domain}/system.cpp (88%) rename src/{system => core/domain}/system.hpp (93%) rename src/{system => core/domain}/transform.cpp (100%) rename src/{system => core/domain}/transform.hpp (100%) rename src/{system => core/domain}/transform_vpx.cpp (100%) rename src/{system => core/domain}/transform_vpx.hpp (100%) rename src/{ => core}/imports/import_altium.cpp (96%) rename src/{ => core}/imports/import_altium.hpp (100%) rename src/{ => core}/imports/import_base.hpp (95%) rename src/{ => core}/imports/import_mentor.cpp (98%) rename src/{ => core}/imports/import_mentor.hpp (100%) rename src/{ => core}/imports/import_ods.cpp (97%) rename src/{ => core}/imports/import_ods.hpp (100%) rename src/{ => core}/imports/ods_writer.cpp (100%) rename src/{ => core}/imports/ods_writer.hpp (100%) create mode 100644 src/frontends/tui/CMakeLists.txt rename src/{ => frontends}/tui/commands.cpp (98%) rename src/{ => frontends}/tui/commands_export.cpp (95%) rename src/{ => frontends}/tui/completion.cpp (97%) rename src/{ => frontends/tui}/main.cpp (99%) rename src/{ => frontends}/tui/screen_analyze.cpp (97%) rename src/{ => frontends}/tui/screen_confirm.cpp (98%) rename src/{ => frontends}/tui/screen_connect.cpp (96%) rename src/{ => frontends}/tui/screen_dashboard.cpp (97%) rename src/{ => frontends}/tui/screen_error.cpp (97%) rename src/{ => frontends}/tui/screen_explore.cpp (98%) rename src/{ => frontends}/tui/screen_filedialog.cpp (99%) rename src/{ => frontends}/tui/screen_help.cpp (99%) rename src/{ => frontends}/tui/screen_main.cpp (97%) rename src/{ => frontends}/tui/screen_palette.cpp (97%) rename src/{ => frontends}/tui/screen_settype.cpp (94%) rename src/{ => frontends}/tui/screen_sigtype_modal.cpp (95%) rename src/{ => frontends}/tui/shell.cpp (98%) rename src/{ => frontends}/tui/tui.cpp (99%) rename src/{ => frontends}/tui/tui.hpp (100%) rename src/{ => frontends}/tui/tui_helpers.cpp (98%) rename src/{ => frontends}/tui/tui_helpers.hpp (100%) rename tests/{ => tui}/test_helpers.cpp (98%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 04cc668..bbdc32b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,49 +11,54 @@ project(essim include(FetchContent) -set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "") -set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "") -set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "") -set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "") - -FetchContent_Declare(ftxui - GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git - GIT_TAG v6.1.9 - GIT_SHALLOW TRUE -) -FetchContent_MakeAvailable(ftxui) - -find_package(libzip REQUIRED) -find_package(pugixml REQUIRED) - -# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked from essim -# (EUPL-1.2, which the LGPL permits). Path overridable via -DBSDL_DIR=...; -# its CLI and tests are not needed inside essim's build. +# ----------------------------------------------------------------- core deps +# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2, +# which the LGPL permits). Override its path with -DBSDL_DIR=... set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree") set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE) set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE) add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl) -# Library target = everything except main.cpp; reused by `essim` and `essim_tests`. -file(GLOB_RECURSE LIB_SOURCES "src/*.cpp") -list(REMOVE_ITEM LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp") +find_package(libzip REQUIRED) +find_package(pugixml REQUIRED) -add_library(essim_lib STATIC ${LIB_SOURCES}) -target_include_directories(essim_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_link_libraries(essim_lib +# =============================================================== essim_core +# All business logic — domain model, importers, application operations +# (src/core/{domain,imports,app}). Frontend-agnostic: it links NO GUI/TUI +# toolkit, so every frontend and the test suite share the exact same core. +file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp") +add_library(essim_core STATIC ${CORE_SOURCES}) +target_include_directories(essim_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_link_libraries(essim_core PUBLIC - ftxui::screen - ftxui::dom - ftxui::component libzip::zip pugixml::pugixml bsdl::bsdl ) -add_executable(essim src/main.cpp) -target_link_libraries(essim PRIVATE essim_lib) +# =============================================================== frontend(s) +# Pick the GUI/TUI frontend to build the `essim` binary against. Each frontend +# is a self-contained src/frontends// (own CMakeLists, GUI toolkit, and +# main.cpp) that links essim_core. "none" builds the core + tests only — no GUI +# toolkit is fetched. To add a frontend (e.g. a Qt GUI), create +# src/frontends/gui/ and configure with -DESSIM_FRONTEND=gui. +set(ESSIM_FRONTEND "tui" CACHE STRING + "Frontend to build: a directory name under src/frontends/, or 'none'") +set_property(CACHE ESSIM_FRONTEND PROPERTY STRINGS tui none) -# Tests +if(ESSIM_FRONTEND STREQUAL "none") + message(STATUS "essim: ESSIM_FRONTEND=none — core + tests only (no frontend, no GUI toolkit)") +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt") + message(STATUS "essim: building frontend '${ESSIM_FRONTEND}'") + add_subdirectory(src/frontends/${ESSIM_FRONTEND}) +else() + message(FATAL_ERROR + "Unknown ESSIM_FRONTEND '${ESSIM_FRONTEND}' — expected " + "src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt, or 'none'.") +endif() + +# =============================================================== tests (core) +# The suite exercises essim_core only — no frontend, no GUI toolkit. include(CTest) if(BUILD_TESTING) set(CMAKE_POLICY_VERSION_MINIMUM 3.5) @@ -65,20 +70,35 @@ if(BUILD_TESTING) FetchContent_MakeAvailable(doctest) unset(CMAKE_POLICY_VERSION_MINIMUM) + # Core tests — exercise essim_core only (tests/*.cpp, non-recursive, so the + # per-frontend tests under tests// are not pulled in here). file(GLOB TEST_SOURCES "tests/*.cpp") if(TEST_SOURCES) add_executable(essim_tests ${TEST_SOURCES}) - target_link_libraries(essim_tests PRIVATE essim_lib doctest::doctest) + target_link_libraries(essim_tests PRIVATE essim_core doctest::doctest) add_test(NAME essim_tests COMMAND essim_tests) endif() + + # Per-frontend tests — tests//*.cpp, built and linked against that + # frontend's library only when the frontend itself is built. + if(TARGET essim_tui) + file(GLOB TUI_TEST_SOURCES "tests/tui/*.cpp") + if(TUI_TEST_SOURCES) + add_executable(essim_tui_tests + "${CMAKE_CURRENT_SOURCE_DIR}/tests/doctest_main.cpp" ${TUI_TEST_SOURCES}) + target_link_libraries(essim_tui_tests PRIVATE essim_tui doctest::doctest) + add_test(NAME essim_tui_tests COMMAND essim_tui_tests) + endif() + endif() endif() -# Documentation: Doxygen → XML → custom Python script → doc/api/ (Markdown rendered by gitea). -# Optional — `doc` target is only created if Doxygen and Python 3 are present. +# =============================================================== documentation +# Doxygen → XML → gen_api_md.py → doc/api/, plus `essim --commands-md`. Needs the +# `essim` binary, so it's only wired when a frontend that provides one is built. find_package(Doxygen COMPONENTS doxygen) find_package(Python3 COMPONENTS Interpreter) -if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND) +if(TARGET essim AND DOXYGEN_FOUND AND Python3_Interpreter_FOUND) set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/doc") file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}") configure_file( @@ -103,11 +123,10 @@ if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND) WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)" VERBATIM) -elseif(NOT DOXYGEN_FOUND AND NOT Python3_Interpreter_FOUND) - message(STATUS "doc: Doxygen and Python 3 not found — `doc` target disabled.") +elseif(NOT TARGET essim) + message(STATUS "doc: no `essim` binary (ESSIM_FRONTEND=none) — `doc` target disabled.") elseif(NOT DOXYGEN_FOUND) - message(STATUS "doc: Doxygen not found — `doc` target disabled " - "(install via `pacman -S doxygen`).") + message(STATUS "doc: Doxygen not found — `doc` target disabled.") else() message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.") endif() diff --git a/src/app/export.cpp b/src/core/app/export.cpp similarity index 95% rename from src/app/export.cpp rename to src/core/app/export.cpp index c828b6d..257b073 100644 --- a/src/app/export.cpp +++ b/src/core/app/export.cpp @@ -1,13 +1,13 @@ -#include "app/export.hpp" +#include "core/app/export.hpp" -#include "imports/ods_writer.hpp" -#include "system/connect.hpp" -#include "system/modules.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/signal_type.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/imports/ods_writer.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signal_type.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/app/export.hpp b/src/core/app/export.hpp similarity index 100% rename from src/app/export.hpp rename to src/core/app/export.hpp diff --git a/src/system/analysis.cpp b/src/core/domain/analysis.cpp similarity index 100% rename from src/system/analysis.cpp rename to src/core/domain/analysis.cpp diff --git a/src/system/analysis.hpp b/src/core/domain/analysis.hpp similarity index 100% rename from src/system/analysis.hpp rename to src/core/domain/analysis.hpp diff --git a/src/system/bsdl_check.cpp b/src/core/domain/bsdl_check.cpp similarity index 100% rename from src/system/bsdl_check.cpp rename to src/core/domain/bsdl_check.cpp diff --git a/src/system/bsdl_check.hpp b/src/core/domain/bsdl_check.hpp similarity index 100% rename from src/system/bsdl_check.hpp rename to src/core/domain/bsdl_check.hpp diff --git a/src/system/bsdl_model.cpp b/src/core/domain/bsdl_model.cpp similarity index 100% rename from src/system/bsdl_model.cpp rename to src/core/domain/bsdl_model.cpp diff --git a/src/system/bsdl_model.hpp b/src/core/domain/bsdl_model.hpp similarity index 100% rename from src/system/bsdl_model.hpp rename to src/core/domain/bsdl_model.hpp diff --git a/src/system/component_kind.cpp b/src/core/domain/component_kind.cpp similarity index 98% rename from src/system/component_kind.cpp rename to src/core/domain/component_kind.cpp index e35da3c..b4a28e3 100644 --- a/src/system/component_kind.cpp +++ b/src/core/domain/component_kind.cpp @@ -1,4 +1,4 @@ -#include "system/component_kind.hpp" +#include "core/domain/component_kind.hpp" #include #include diff --git a/src/system/component_kind.hpp b/src/core/domain/component_kind.hpp similarity index 100% rename from src/system/component_kind.hpp rename to src/core/domain/component_kind.hpp diff --git a/src/system/connect.cpp b/src/core/domain/connect.cpp similarity index 100% rename from src/system/connect.cpp rename to src/core/domain/connect.cpp diff --git a/src/system/connect.hpp b/src/core/domain/connect.hpp similarity index 100% rename from src/system/connect.hpp rename to src/core/domain/connect.hpp diff --git a/src/system/modules.cpp b/src/core/domain/modules.cpp similarity index 100% rename from src/system/modules.cpp rename to src/core/domain/modules.cpp diff --git a/src/system/modules.hpp b/src/core/domain/modules.hpp similarity index 100% rename from src/system/modules.hpp rename to src/core/domain/modules.hpp diff --git a/src/system/nets.cpp b/src/core/domain/nets.cpp similarity index 93% rename from src/system/nets.cpp rename to src/core/domain/nets.cpp index f43d1e9..56a0eb2 100644 --- a/src/system/nets.cpp +++ b/src/core/domain/nets.cpp @@ -1,11 +1,11 @@ -#include "system/nets.hpp" +#include "core/domain/nets.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 "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/system/nets.hpp b/src/core/domain/nets.hpp similarity index 100% rename from src/system/nets.hpp rename to src/core/domain/nets.hpp diff --git a/src/system/parts.cpp b/src/core/domain/parts.cpp similarity index 100% rename from src/system/parts.cpp rename to src/core/domain/parts.cpp diff --git a/src/system/parts.hpp b/src/core/domain/parts.hpp similarity index 100% rename from src/system/parts.hpp rename to src/core/domain/parts.hpp diff --git a/src/system/persist.cpp b/src/core/domain/persist.cpp similarity index 100% rename from src/system/persist.cpp rename to src/core/domain/persist.cpp diff --git a/src/system/persist.hpp b/src/core/domain/persist.hpp similarity index 100% rename from src/system/persist.hpp rename to src/core/domain/persist.hpp diff --git a/src/system/pin_model.cpp b/src/core/domain/pin_model.cpp similarity index 100% rename from src/system/pin_model.cpp rename to src/core/domain/pin_model.cpp diff --git a/src/system/pin_model.hpp b/src/core/domain/pin_model.hpp similarity index 100% rename from src/system/pin_model.hpp rename to src/core/domain/pin_model.hpp diff --git a/src/system/pin_name.cpp b/src/core/domain/pin_name.cpp similarity index 94% rename from src/system/pin_name.cpp rename to src/core/domain/pin_name.cpp index d50da1b..6d83e59 100644 --- a/src/system/pin_name.cpp +++ b/src/core/domain/pin_name.cpp @@ -1,4 +1,4 @@ -#include "system/pin_name.hpp" +#include "core/domain/pin_name.hpp" #include #include diff --git a/src/system/pin_name.hpp b/src/core/domain/pin_name.hpp similarity index 100% rename from src/system/pin_name.hpp rename to src/core/domain/pin_name.hpp diff --git a/src/system/pin_role.cpp b/src/core/domain/pin_role.cpp similarity index 100% rename from src/system/pin_role.cpp rename to src/core/domain/pin_role.cpp diff --git a/src/system/pin_role.hpp b/src/core/domain/pin_role.hpp similarity index 100% rename from src/system/pin_role.hpp rename to src/core/domain/pin_role.hpp diff --git a/src/system/pin_spec.hpp b/src/core/domain/pin_spec.hpp similarity index 100% rename from src/system/pin_spec.hpp rename to src/core/domain/pin_spec.hpp diff --git a/src/system/pins.cpp b/src/core/domain/pins.cpp similarity index 100% rename from src/system/pins.cpp rename to src/core/domain/pins.cpp diff --git a/src/system/pins.hpp b/src/core/domain/pins.hpp similarity index 100% rename from src/system/pins.hpp rename to src/core/domain/pins.hpp diff --git a/src/system/signal_type.hpp b/src/core/domain/signal_type.hpp similarity index 100% rename from src/system/signal_type.hpp rename to src/core/domain/signal_type.hpp diff --git a/src/system/signals.cpp b/src/core/domain/signals.cpp similarity index 100% rename from src/system/signals.cpp rename to src/core/domain/signals.cpp diff --git a/src/system/signals.hpp b/src/core/domain/signals.hpp similarity index 100% rename from src/system/signals.hpp rename to src/core/domain/signals.hpp diff --git a/src/system/syselmts.hpp b/src/core/domain/syselmts.hpp similarity index 100% rename from src/system/syselmts.hpp rename to src/core/domain/syselmts.hpp diff --git a/src/system/system.cpp b/src/core/domain/system.cpp similarity index 88% rename from src/system/system.cpp rename to src/core/domain/system.cpp index 923f01f..6f29f97 100644 --- a/src/system/system.cpp +++ b/src/core/domain/system.cpp @@ -3,9 +3,9 @@ #include "connect.hpp" #include "modules.hpp" -#include "imports/import_altium.hpp" -#include "imports/import_mentor.hpp" -#include "imports/import_ods.hpp" +#include "core/imports/import_altium.hpp" +#include "core/imports/import_mentor.hpp" +#include "core/imports/import_ods.hpp" System::System() : mods(nullptr), conns(nullptr) { diff --git a/src/system/system.hpp b/src/core/domain/system.hpp similarity index 93% rename from src/system/system.hpp rename to src/core/domain/system.hpp index 6d8f955..e0c3864 100644 --- a/src/system/system.hpp +++ b/src/core/domain/system.hpp @@ -1,7 +1,7 @@ #ifndef _SYSTEM_HPP_ #define _SYSTEM_HPP_ -#include "imports/import_base.hpp" +#include "core/imports/import_base.hpp" #pragma once class Modules; ///< Forward declaration of the Modules class. diff --git a/src/system/transform.cpp b/src/core/domain/transform.cpp similarity index 100% rename from src/system/transform.cpp rename to src/core/domain/transform.cpp diff --git a/src/system/transform.hpp b/src/core/domain/transform.hpp similarity index 100% rename from src/system/transform.hpp rename to src/core/domain/transform.hpp diff --git a/src/system/transform_vpx.cpp b/src/core/domain/transform_vpx.cpp similarity index 100% rename from src/system/transform_vpx.cpp rename to src/core/domain/transform_vpx.cpp diff --git a/src/system/transform_vpx.hpp b/src/core/domain/transform_vpx.hpp similarity index 100% rename from src/system/transform_vpx.hpp rename to src/core/domain/transform_vpx.hpp diff --git a/src/imports/import_altium.cpp b/src/core/imports/import_altium.cpp similarity index 96% rename from src/imports/import_altium.cpp rename to src/core/imports/import_altium.cpp index f8f7521..41046e8 100644 --- a/src/imports/import_altium.cpp +++ b/src/core/imports/import_altium.cpp @@ -1,8 +1,8 @@ #include "import_altium.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" #include #include diff --git a/src/imports/import_altium.hpp b/src/core/imports/import_altium.hpp similarity index 100% rename from src/imports/import_altium.hpp rename to src/core/imports/import_altium.hpp diff --git a/src/imports/import_base.hpp b/src/core/imports/import_base.hpp similarity index 95% rename from src/imports/import_base.hpp rename to src/core/imports/import_base.hpp index 44b6241..b63a947 100644 --- a/src/imports/import_base.hpp +++ b/src/core/imports/import_base.hpp @@ -4,8 +4,8 @@ #include #include -#include "system/parts.hpp" -#include "system/signals.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/signals.hpp" /** * @brief Base class for importing data from a file. diff --git a/src/imports/import_mentor.cpp b/src/core/imports/import_mentor.cpp similarity index 98% rename from src/imports/import_mentor.cpp rename to src/core/imports/import_mentor.cpp index 90cf460..5795c4c 100644 --- a/src/imports/import_mentor.cpp +++ b/src/core/imports/import_mentor.cpp @@ -1,6 +1,6 @@ #include "import_mentor.hpp" -#include "system/pins.hpp" -#include "system/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/parts.hpp" #include #include diff --git a/src/imports/import_mentor.hpp b/src/core/imports/import_mentor.hpp similarity index 100% rename from src/imports/import_mentor.hpp rename to src/core/imports/import_mentor.hpp diff --git a/src/imports/import_ods.cpp b/src/core/imports/import_ods.cpp similarity index 97% rename from src/imports/import_ods.cpp rename to src/core/imports/import_ods.cpp index 1270db8..2bcb65a 100644 --- a/src/imports/import_ods.cpp +++ b/src/core/imports/import_ods.cpp @@ -1,8 +1,8 @@ #include "import_ods.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" #include #include diff --git a/src/imports/import_ods.hpp b/src/core/imports/import_ods.hpp similarity index 100% rename from src/imports/import_ods.hpp rename to src/core/imports/import_ods.hpp diff --git a/src/imports/ods_writer.cpp b/src/core/imports/ods_writer.cpp similarity index 100% rename from src/imports/ods_writer.cpp rename to src/core/imports/ods_writer.cpp diff --git a/src/imports/ods_writer.hpp b/src/core/imports/ods_writer.hpp similarity index 100% rename from src/imports/ods_writer.hpp rename to src/core/imports/ods_writer.hpp diff --git a/src/frontends/tui/CMakeLists.txt b/src/frontends/tui/CMakeLists.txt new file mode 100644 index 0000000..ea58b21 --- /dev/null +++ b/src/frontends/tui/CMakeLists.txt @@ -0,0 +1,35 @@ +# TUI frontend (FTXUI). Builds the `essim` executable against essim_core. +# +# A frontend is self-contained here: it pulls its own GUI toolkit, compiles its +# sources into a library that links essim_core, and produces the `essim` binary +# from its own entry point (main.cpp). To add another frontend, create a sibling +# src/frontends// with the same shape and select it with +# -DESSIM_FRONTEND=. + +set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "") +set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "") +set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "") +set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "") + +FetchContent_Declare(ftxui + GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git + GIT_TAG v6.1.9 + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(ftxui) + +# Frontend library = every .cpp here except the entry point. +file(GLOB TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +list(REMOVE_ITEM TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") + +add_library(essim_tui STATIC ${TUI_SOURCES}) +target_link_libraries(essim_tui + PUBLIC + essim_core + ftxui::screen + ftxui::dom + ftxui::component +) + +add_executable(essim "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") +target_link_libraries(essim PRIVATE essim_tui) diff --git a/src/tui/commands.cpp b/src/frontends/tui/commands.cpp similarity index 98% rename from src/tui/commands.cpp rename to src/frontends/tui/commands.cpp index 036458d..0906856 100644 --- a/src/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -1,21 +1,21 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" -#include "system/analysis.hpp" -#include "system/connect.hpp" -#include "system/modules.hpp" -#include "system/nets.hpp" -#include "system/parts.hpp" -#include "system/persist.hpp" -#include "system/pin_role.hpp" -#include "system/pin_model.hpp" -#include "system/bsdl_model.hpp" -#include "system/bsdl_check.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" -#include "system/transform.hpp" -#include "system/transform_vpx.hpp" +#include "core/domain/analysis.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/nets.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/persist.hpp" +#include "core/domain/pin_role.hpp" +#include "core/domain/pin_model.hpp" +#include "core/domain/bsdl_model.hpp" +#include "core/domain/bsdl_check.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" +#include "core/domain/transform.hpp" +#include "core/domain/transform_vpx.hpp" #include #include diff --git a/src/tui/commands_export.cpp b/src/frontends/tui/commands_export.cpp similarity index 95% rename from src/tui/commands_export.cpp rename to src/frontends/tui/commands_export.cpp index 75cf1b2..063f6ad 100644 --- a/src/tui/commands_export.cpp +++ b/src/frontends/tui/commands_export.cpp @@ -1,8 +1,8 @@ -#include "tui/tui.hpp" +#include "frontends/tui/tui.hpp" -#include "app/export.hpp" -#include "system/connect.hpp" -#include "system/system.hpp" +#include "core/app/export.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/tui/completion.cpp b/src/frontends/tui/completion.cpp similarity index 97% rename from src/tui/completion.cpp rename to src/frontends/tui/completion.cpp index e2bd547..721bc00 100644 --- a/src/tui/completion.cpp +++ b/src/frontends/tui/completion.cpp @@ -1,5 +1,5 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" #include #include diff --git a/src/main.cpp b/src/frontends/tui/main.cpp similarity index 99% rename from src/main.cpp rename to src/frontends/tui/main.cpp index f288cdd..d3011f2 100644 --- a/src/main.cpp +++ b/src/frontends/tui/main.cpp @@ -1,4 +1,4 @@ -#include "tui/tui.hpp" +#include "frontends/tui/tui.hpp" #include #include diff --git a/src/tui/screen_analyze.cpp b/src/frontends/tui/screen_analyze.cpp similarity index 97% rename from src/tui/screen_analyze.cpp rename to src/frontends/tui/screen_analyze.cpp index 9a8518b..2f5f425 100644 --- a/src/tui/screen_analyze.cpp +++ b/src/frontends/tui/screen_analyze.cpp @@ -1,15 +1,15 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" -#include "system/analysis.hpp" -#include "system/bsdl_check.hpp" -#include "system/connect.hpp" -#include "system/modules.hpp" -#include "system/nets.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/analysis.hpp" +#include "core/domain/bsdl_check.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/nets.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/tui/screen_confirm.cpp b/src/frontends/tui/screen_confirm.cpp similarity index 98% rename from src/tui/screen_confirm.cpp rename to src/frontends/tui/screen_confirm.cpp index 5a3931a..111bcfe 100644 --- a/src/tui/screen_confirm.cpp +++ b/src/frontends/tui/screen_confirm.cpp @@ -1,4 +1,4 @@ -#include "tui/tui.hpp" +#include "frontends/tui/tui.hpp" #include #include diff --git a/src/tui/screen_connect.cpp b/src/frontends/tui/screen_connect.cpp similarity index 96% rename from src/tui/screen_connect.cpp rename to src/frontends/tui/screen_connect.cpp index d88865f..bb01b4a 100644 --- a/src/tui/screen_connect.cpp +++ b/src/frontends/tui/screen_connect.cpp @@ -1,11 +1,11 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/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 "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/system.hpp" +#include "core/domain/transform.hpp" #include #include diff --git a/src/tui/screen_dashboard.cpp b/src/frontends/tui/screen_dashboard.cpp similarity index 97% rename from src/tui/screen_dashboard.cpp rename to src/frontends/tui/screen_dashboard.cpp index 124d99a..27f4dfd 100644 --- a/src/tui/screen_dashboard.cpp +++ b/src/frontends/tui/screen_dashboard.cpp @@ -1,15 +1,15 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" -#include "system/analysis.hpp" -#include "system/bsdl_check.hpp" -#include "system/connect.hpp" -#include "system/modules.hpp" -#include "system/nets.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/analysis.hpp" +#include "core/domain/bsdl_check.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/nets.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/tui/screen_error.cpp b/src/frontends/tui/screen_error.cpp similarity index 97% rename from src/tui/screen_error.cpp rename to src/frontends/tui/screen_error.cpp index 2774d49..52f0750 100644 --- a/src/tui/screen_error.cpp +++ b/src/frontends/tui/screen_error.cpp @@ -1,4 +1,4 @@ -#include "tui/tui.hpp" +#include "frontends/tui/tui.hpp" #include #include diff --git a/src/tui/screen_explore.cpp b/src/frontends/tui/screen_explore.cpp similarity index 98% rename from src/tui/screen_explore.cpp rename to src/frontends/tui/screen_explore.cpp index 1552ac5..0ca4afd 100644 --- a/src/tui/screen_explore.cpp +++ b/src/frontends/tui/screen_explore.cpp @@ -1,13 +1,13 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" -#include "system/connect.hpp" -#include "system/modules.hpp" -#include "system/nets.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/nets.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/tui/screen_filedialog.cpp b/src/frontends/tui/screen_filedialog.cpp similarity index 99% rename from src/tui/screen_filedialog.cpp rename to src/frontends/tui/screen_filedialog.cpp index 57d91f7..34a8886 100644 --- a/src/tui/screen_filedialog.cpp +++ b/src/frontends/tui/screen_filedialog.cpp @@ -1,5 +1,5 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" #include #include diff --git a/src/tui/screen_help.cpp b/src/frontends/tui/screen_help.cpp similarity index 99% rename from src/tui/screen_help.cpp rename to src/frontends/tui/screen_help.cpp index 4042e24..4578c96 100644 --- a/src/tui/screen_help.cpp +++ b/src/frontends/tui/screen_help.cpp @@ -1,5 +1,5 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" #include #include diff --git a/src/tui/screen_main.cpp b/src/frontends/tui/screen_main.cpp similarity index 97% rename from src/tui/screen_main.cpp rename to src/frontends/tui/screen_main.cpp index b7db6d8..bada2a9 100644 --- a/src/tui/screen_main.cpp +++ b/src/frontends/tui/screen_main.cpp @@ -1,5 +1,5 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" #include #include diff --git a/src/tui/screen_palette.cpp b/src/frontends/tui/screen_palette.cpp similarity index 97% rename from src/tui/screen_palette.cpp rename to src/frontends/tui/screen_palette.cpp index 2869ba2..d322fd1 100644 --- a/src/tui/screen_palette.cpp +++ b/src/frontends/tui/screen_palette.cpp @@ -1,9 +1,9 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" -#include "system/modules.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/tui/screen_settype.cpp b/src/frontends/tui/screen_settype.cpp similarity index 94% rename from src/tui/screen_settype.cpp rename to src/frontends/tui/screen_settype.cpp index deb897e..1f45125 100644 --- a/src/tui/screen_settype.cpp +++ b/src/frontends/tui/screen_settype.cpp @@ -1,13 +1,13 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" -#include "system/modules.hpp" -#include "system/parts.hpp" -#include "system/pin_model.hpp" -#include "system/pin_role.hpp" -#include "system/pins.hpp" -#include "system/system.hpp" -#include "system/transform_vpx.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pin_model.hpp" +#include "core/domain/pin_role.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/system.hpp" +#include "core/domain/transform_vpx.hpp" #include #include diff --git a/src/tui/screen_sigtype_modal.cpp b/src/frontends/tui/screen_sigtype_modal.cpp similarity index 95% rename from src/tui/screen_sigtype_modal.cpp rename to src/frontends/tui/screen_sigtype_modal.cpp index 3319532..b00f216 100644 --- a/src/tui/screen_sigtype_modal.cpp +++ b/src/frontends/tui/screen_sigtype_modal.cpp @@ -1,8 +1,8 @@ -#include "tui/tui.hpp" +#include "frontends/tui/tui.hpp" -#include "system/modules.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/tui/shell.cpp b/src/frontends/tui/shell.cpp similarity index 98% rename from src/tui/shell.cpp rename to src/frontends/tui/shell.cpp index 2f70afa..1ca4bc5 100644 --- a/src/tui/shell.cpp +++ b/src/frontends/tui/shell.cpp @@ -1,9 +1,9 @@ -#include "tui/tui.hpp" -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui.hpp" +#include "frontends/tui/tui_helpers.hpp" -#include "system/modules.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/tui/tui.cpp b/src/frontends/tui/tui.cpp similarity index 99% rename from src/tui/tui.cpp rename to src/frontends/tui/tui.cpp index df209fc..408bd46 100644 --- a/src/tui/tui.cpp +++ b/src/frontends/tui/tui.cpp @@ -1,6 +1,6 @@ -#include "tui/tui.hpp" +#include "frontends/tui/tui.hpp" -#include "system/system.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/src/tui/tui.hpp b/src/frontends/tui/tui.hpp similarity index 100% rename from src/tui/tui.hpp rename to src/frontends/tui/tui.hpp diff --git a/src/tui/tui_helpers.cpp b/src/frontends/tui/tui_helpers.cpp similarity index 98% rename from src/tui/tui_helpers.cpp rename to src/frontends/tui/tui_helpers.cpp index 6fcd475..eb85d87 100644 --- a/src/tui/tui_helpers.cpp +++ b/src/frontends/tui/tui_helpers.cpp @@ -1,4 +1,4 @@ -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui_helpers.hpp" #include diff --git a/src/tui/tui_helpers.hpp b/src/frontends/tui/tui_helpers.hpp similarity index 100% rename from src/tui/tui_helpers.hpp rename to src/frontends/tui/tui_helpers.hpp diff --git a/tests/test_analysis.cpp b/tests/test_analysis.cpp index e4b10f5..6a0cbae 100644 --- a/tests/test_analysis.cpp +++ b/tests/test_analysis.cpp @@ -1,11 +1,11 @@ #include -#include "system/analysis.hpp" -#include "system/modules.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/analysis.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/tests/test_bsdl_apply.cpp b/tests/test_bsdl_apply.cpp index 34a9de9..39e6465 100644 --- a/tests/test_bsdl_apply.cpp +++ b/tests/test_bsdl_apply.cpp @@ -1,14 +1,14 @@ #include -#include "system/analysis.hpp" -#include "system/bsdl_check.hpp" -#include "system/bsdl_model.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/pin_spec.hpp" -#include "system/system.hpp" -#include "system/modules.hpp" -#include "system/persist.hpp" +#include "core/domain/analysis.hpp" +#include "core/domain/bsdl_check.hpp" +#include "core/domain/bsdl_model.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/pin_spec.hpp" +#include "core/domain/system.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/persist.hpp" #include #include diff --git a/tests/test_bsdl_check.cpp b/tests/test_bsdl_check.cpp index 51e949b..ec8a344 100644 --- a/tests/test_bsdl_check.cpp +++ b/tests/test_bsdl_check.cpp @@ -1,13 +1,13 @@ #include -#include "system/analysis.hpp" -#include "system/bsdl_check.hpp" -#include "system/modules.hpp" -#include "system/parts.hpp" -#include "system/pin_spec.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/analysis.hpp" +#include "core/domain/bsdl_check.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pin_spec.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/tests/test_component_kind.cpp b/tests/test_component_kind.cpp index 70779aa..e293565 100644 --- a/tests/test_component_kind.cpp +++ b/tests/test_component_kind.cpp @@ -1,7 +1,7 @@ #include -#include "system/component_kind.hpp" -#include "system/parts.hpp" +#include "core/domain/component_kind.hpp" +#include "core/domain/parts.hpp" #include diff --git a/tests/test_export.cpp b/tests/test_export.cpp index 549fccc..d126c0b 100644 --- a/tests/test_export.cpp +++ b/tests/test_export.cpp @@ -1,12 +1,12 @@ #include -#include "app/export.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 "core/app/export.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/tests/test_nc_origin.cpp b/tests/test_nc_origin.cpp index cb769c0..1aee1dc 100644 --- a/tests/test_nc_origin.cpp +++ b/tests/test_nc_origin.cpp @@ -1,11 +1,11 @@ #include -#include "system/modules.hpp" -#include "system/parts.hpp" -#include "system/persist.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/persist.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/tests/test_persist.cpp b/tests/test_persist.cpp index 21aaf89..d7d87b5 100644 --- a/tests/test_persist.cpp +++ b/tests/test_persist.cpp @@ -1,12 +1,12 @@ #include -#include "system/connect.hpp" -#include "system/modules.hpp" -#include "system/parts.hpp" -#include "system/persist.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/persist.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" #include #include diff --git a/tests/test_pin_model.cpp b/tests/test_pin_model.cpp index 23569b3..76f7af4 100644 --- a/tests/test_pin_model.cpp +++ b/tests/test_pin_model.cpp @@ -1,9 +1,9 @@ #include -#include "system/parts.hpp" -#include "system/pin_model.hpp" -#include "system/pin_spec.hpp" -#include "system/pins.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pin_model.hpp" +#include "core/domain/pin_spec.hpp" +#include "core/domain/pins.hpp" #include #include diff --git a/tests/test_pin_name.cpp b/tests/test_pin_name.cpp index 9be5099..1f1b1f2 100644 --- a/tests/test_pin_name.cpp +++ b/tests/test_pin_name.cpp @@ -1,9 +1,9 @@ #include -#include "system/parts.hpp" -#include "system/pin_name.hpp" -#include "system/pins.hpp" -#include "system/transform.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pin_name.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/transform.hpp" #include diff --git a/tests/test_signal_type.cpp b/tests/test_signal_type.cpp index 59c12a8..0d62e50 100644 --- a/tests/test_signal_type.cpp +++ b/tests/test_signal_type.cpp @@ -1,6 +1,6 @@ #include -#include "system/signal_type.hpp" +#include "core/domain/signal_type.hpp" TEST_CASE("signal_type_name round-trips with from_name") { SignalType t; diff --git a/tests/test_vpx_transform.cpp b/tests/test_vpx_transform.cpp index 9d1adf5..b646f01 100644 --- a/tests/test_vpx_transform.cpp +++ b/tests/test_vpx_transform.cpp @@ -1,12 +1,12 @@ #include -#include "system/modules.hpp" -#include "system/parts.hpp" -#include "system/pins.hpp" -#include "system/signals.hpp" -#include "system/system.hpp" -#include "system/transform.hpp" -#include "system/transform_vpx.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" +#include "core/domain/transform.hpp" +#include "core/domain/transform_vpx.hpp" #include #include diff --git a/tests/test_helpers.cpp b/tests/tui/test_helpers.cpp similarity index 98% rename from tests/test_helpers.cpp rename to tests/tui/test_helpers.cpp index 18098ac..b14b723 100644 --- a/tests/test_helpers.cpp +++ b/tests/tui/test_helpers.cpp @@ -1,6 +1,6 @@ #include -#include "tui/tui_helpers.hpp" +#include "frontends/tui/tui_helpers.hpp" #include #include From cccc5f131d38766bb30ba939f1f220cdc641f950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 19:39:21 +0200 Subject: [PATCH 03/11] docs: rewrite DESIGN + README for the core/frontends structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESIGN.md: new layered Build section (essim_core + ESSIM_FRONTEND, FTXUI per frontend, split tests), a rewritten Layout tree (src/core/{domain,imports,app}, src/frontends/tui, tests/{,tui}), and a new "Architecture — core vs frontends" section stating the rule (core never depends on a frontend) with the export operation as the worked example. README: layered-build note, FTXUI-is-the-tui- frontend's dependency, core-vs-frontend test split, nested project-layout tree. Also keep the binary at ./build/essim via RUNTIME_OUTPUT_DIRECTORY now that the exe is produced from the frontend subdir. Co-Authored-By: Claude Opus 4.8 --- DESIGN.md | 171 ++++++++++++++++++------------- README.md | 35 +++++-- src/frontends/tui/CMakeLists.txt | 4 + 3 files changed, 130 insertions(+), 80 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 290ed26..db33e52 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -10,72 +10,99 @@ cmake --build build -j ./build/essim ``` -- CMake **3.14+** required (uses `FetchContent_MakeAvailable`). -- FTXUI is fetched at configure time from GitHub (`v6.1.9`, shallow clone). First configure pays ~20 s for the clone; subsequent ones are cached in `build/_deps/`. -- **System dependencies** (resolved via `find_package`): `libzip` (target `libzip::zip`) and `pugixml` (target `pugixml::pugixml`). Used by the ODS importer. Arch: `pacman -S libzip pugixml`; Debian/Ubuntu: `libzip-dev libpugixml-dev`. -- **`libbsdl`** (standalone BSDL parser, LGPL-2.1) is the sibling repo at `../libbsdl`, pulled in via `add_subdirectory` (path overridable with `-DBSDL_DIR=...`) and linked dynamically (`bsdl::bsdl`; an LGPL `.so` is fine from EUPL essim). Powers the BSDL ingest behind `attach-bsdl`. -- Sources are collected with `file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")`. **After adding a new `.cpp`, re-run `cmake -S . -B build`** — CMake does not re-glob automatically and link will fail with "undefined reference". +- CMake **3.14+** (uses `FetchContent_MakeAvailable`). +- **Layered build** (see *Architecture* below). `essim_core` is the + frontend-agnostic business library; a frontend under `src/frontends//` + links it and produces the `essim` binary. Choose it with + `-DESSIM_FRONTEND=` (default `tui`). **`-DESSIM_FRONTEND=none` builds the + core + tests only — no GUI toolkit is fetched.** +- **Core system dependencies** (via `find_package`): `libzip` (`libzip::zip`) + and `pugixml` (`pugixml::pugixml`) for the ODS importer. Arch: + `pacman -S libzip pugixml`; Debian/Ubuntu: `libzip-dev libpugixml-dev`. +- **`libbsdl`** (standalone BSDL parser, LGPL-2.1) — sibling repo at + `../libbsdl`, `add_subdirectory` (override `-DBSDL_DIR=...`), linked + dynamically **into the core** (`bsdl::bsdl`). +- **FTXUI** is fetched by the **tui frontend only** + (`src/frontends/tui/CMakeLists.txt`), never by the core. +- Sources are globbed per layer: `src/core/*.cpp` → `essim_core`, + `src/frontends//*.cpp` → that frontend's lib + the `essim` binary. + **After adding a `.cpp`, re-run `cmake -S . -B build`** — CMake doesn't re-glob. +- **Tests** are split: `essim_tests` links `essim_core` (no FTXUI) from + `tests/*.cpp`; per-frontend tests like `essim_tui_tests` link `essim_tui` from + `tests//*.cpp`. - **Headless / batch**: `essim --batch --source FILE` runs a script and prints its console output to stdout, then exits without the TUI (good for CI / capturing `verify`). Also `--restore FILE` and `--commands-md [FILE]`. `BootDispatch` runs `--restore`/`--source` synchronously before the event loop (`Source` takes its headless drain branch when no screen is attached), so the console buffer is complete by the time `--batch` dumps it (`Tui::DumpOutput`). ## Layout ``` src/ - main.cpp -- launches Tui - system/ -- domain model - syselmts.hpp SystemElement + SystemElementContainer (templated, get/merge/iterate) - modules.{hpp,cpp} Module, Modules - parts.{hpp,cpp} Part (carries `kind` + `connector_type`), Parts - pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins - signals.{hpp,cpp} Signal, Signals - signal_type.hpp SignalType + helpers - pin_spec.hpp PinSpec (function/direction/pad/source) + SignalType mapping - component_kind.{hpp,cpp} ComponentKind enum + infer_component_kind(name) - pin_name.{hpp,cpp} canonical_pin_name(s) — zero-pad digit suffix to 3 - connect.{hpp,cpp} Connection, Connections - transform.{hpp,cpp} Transform / IdentityTransform / TransformRegistry + - CheckIdentityCompatible + FillIdentityNCs - pin_role.{hpp,cpp} pin_role(kind, name) → PinSpec, pin_layout(kind), - FillPartFromLayout(part, kind) - pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel - bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl - bsdl_check.{hpp,cpp} check_pin_specs / check_jtag_chain → vector - nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent - analysis.{hpp,cpp} analyze_system → AnalysisReport (diff pairs, buses, anomalies) - persist.{hpp,cpp} save / restore (tab-delimited) - system.{hpp,cpp} System: owns Modules + Connections, exposes Load() - imports/ -- adapters that populate or emit the domain - import_base.hpp ImportBase interface - import_mentor.{hpp,cpp} Mentor Graphics netlist parser - import_altium.{hpp,cpp} Altium netlist parser (`[ ]` parts, `( )` signals) - import_ods.{hpp,cpp} ODS spreadsheet pinout (libzip + pugixml) - ods_writer.{hpp,cpp} Minimal .ods writer (multi-sheet, string cells) - tui/ -- FTXUI shell, split by responsibility - tui.{hpp,cpp} Class Tui (state + Run() orchestrator + screen-mode event dispatcher) - tui_helpers.{hpp,cpp} Free helpers: ToLower, NaturalLess, LongestCommonPrefix - shell.cpp Print, Submit, Dispatch, Finalize, Tokenize, history persistence - completion.cpp CompleteCommand, CompletePath, CompleteInline - commands.cpp RegisterCommands (orchestrator + lifecycle / shell / topology commands) - commands_export.cpp RegisterExportCommands (export → CSV / ODS, file-dialog hook) - screen_main.cpp BuildMainScreen (visualisation area + bottom input) - screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper - screen_settype.cpp BuildSettypeScreen - screen_explore.cpp BuildExploreScreen (browse modules → parts/signals/connections → details; not scriptable) - screen_dashboard.cpp BuildDashboardScreen (read-only system overview) - screen_analyze.cpp BuildAnalyzeScreen (anomalies / groups / power decisions) - screen_palette.cpp BuildPaletteModal (global Ctrl-P fuzzy launcher) - screen_filedialog.cpp BuildFileDialog + OpenFileDialog (reusable file picker) - screen_error.cpp BuildErrorModal + ShowError (centred error popup) - screen_help.cpp BuildHelpScreen (topic-driven feature reference) - screen_sigtype_modal.cpp BuildSignalTypeModal (popup attached to explore via Modal()) -doc/classes.puml -- PlantUML class diagram + core/ -- business logic; NO GUI toolkit (builds libessim_core) + domain/ -- the model + read-only analyses + syselmts.hpp SystemElement + SystemElementContainer (get/merge/iterate) + modules.{hpp,cpp} Module, Modules + parts.{hpp,cpp} Part (kind, connector_type, bsdl_path; PinSpec per pin) + pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins + signals.{hpp,cpp} Signal, Signals + signal_type.hpp SignalType + helpers + pin_spec.hpp PinSpec (function/direction/pad/source), mappings, spec_source_rank + component_kind.{hpp,cpp} ComponentKind + infer_component_kind(name) + pin_name.{hpp,cpp} canonical_pin_name (zero-pad digit suffix to 3) + connect.{hpp,cpp} Connection, Connections + transform*.{hpp,cpp} Transform / IdentityTransform / TransformRegistry, VPX transform + pin_role.{hpp,cpp} pin_role(kind,name) -> PinSpec, pin_layout, FillPartFromLayout + pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel + bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl + bsdl_check.{hpp,cpp} check_pin_specs / _jtag_chain / _source_conflicts / _bsdl_completeness + nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent + analysis.{hpp,cpp} analyze_system -> AnalysisReport (diff pairs, buses, anomalies) + persist.{hpp,cpp} save / restore (tab-delimited; `B` tag = bsdl_path) + system.{hpp,cpp} System: owns Modules + Connections, Load() + imports/ -- netlist / pinout adapters + import_base.hpp / import_{mentor,altium,ods}.{hpp,cpp} / ods_writer.{hpp,cpp} + app/ -- application operations (UI-independent use cases) + export.{hpp,cpp} export_connections(System*, path, format) -> ExportResult + frontends/ -- one directory per GUI/TUI engine; each links essim_core + tui/ -- FTXUI shell (builds libessim_tui + the `essim` binary) + CMakeLists.txt fetches FTXUI; builds essim_tui + essim + main.cpp entry point (CLI flags -> Tui) + tui.{hpp,cpp} class Tui (state + Run() + screen-mode event dispatch) + tui_helpers.{hpp,cpp} ToLower, NaturalLess, RenderHelpPanel + shell.cpp Print, Submit, Dispatch, Source / ProcessNextSourceLine + completion.cpp CompleteCommand / CompletePath / CompleteInline + commands.cpp RegisterCommands (thin: resolve args -> call core -> render) + commands_export.cpp thin wrapper over app::export_connections + screen_*.cpp dashboard, connect, settype, explore, analyze, help, main + (console), palette, file dialog, error/confirm, sigtype modal +tests/ -- core tests (link essim_core) + tui/ -- frontend tests (link essim_tui) +doc/ , test/ -- docs; sample netlists + system.essim bring-up script ``` -`include/` and `lib/` are kept empty by design — FTXUI used to live there as precompiled `.a` + headers, now it comes through FetchContent. +## Architecture — core vs frontends + +The hard rule: **`src/core/` never depends on a frontend** — no `#include +"frontends/…"`, no GUI toolkit. Frontends depend on the core, never the reverse +(`essim_core` links libzip / pugixml / bsdl only). + +- **Domain** (`core/domain/`) — the model and the read-only analyses + (`analyze_system`, the `check_*` passes, `compute_all_nets`). +- **Application** (`core/app/`) — use-case operations a frontend invokes, e.g. + `export_connections(System*, path, format) -> ExportResult`. An operation + builds its artefact and returns data/stats; it **never** prints or opens a + dialog. (Anti-pattern being removed: the export command used to build the file + inside its lambda. The TUI command is now a thin wrapper — resolve args/dialog + → call the core op → render the result.) +- **Frontends** (`frontends//`) — thin: map UI events to core calls and + render results. Add one by creating `src/frontends//CMakeLists.txt` (build + `essim_` linking `essim_core`, produce the `essim` binary) and configuring + `-DESSIM_FRONTEND=`. + +Because the core links no toolkit, the suite links `essim_core` directly and +`-DESSIM_FRONTEND=none` builds + tests the whole core with FTXUI never fetched. ## Domain conventions -- Everything in `system/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container. +- Everything in `core/domain/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container. - `SystemElementContainer::merge(name)` is the get-or-create primitive — call it instead of `add` when you don't know whether the element already exists. `add` throws on duplicate names or empty names. - `using namespace std;` is present in `syselmts.hpp` — pre-existing, don't add more `using namespace` in headers. - Include guards `_NAME_HPP_` *and* `#pragma once` are both used. Match the existing style. @@ -110,11 +137,11 @@ Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source Pending prompts (from incomplete inline commands) are NOT considered interactive and are filled by subsequent script lines, the way you'd expect. Lines starting with `#` and blank lines are skipped; leading/trailing whitespace is trimmed; `~/` is expanded. -`save` / `restore` (`src/system/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `B` (part's attached BSDL `.bsd` path — re-parsed and re-applied on restore; the path is persisted, **not** the derived pin specs), `N` (pin → signal name; empty = NC; optional 4th field carries `nc_origin_tag()`: `U` = ImportedUnconnected, `D` = DroppedSingleton — omitted when the pin has a signal or when origin is `None`), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). The 4th N field is backward-compatible: pre-existing snapshots without it restore with `nc_origin = None`. +`save` / `restore` (`src/core/domain/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `B` (part's attached BSDL `.bsd` path — re-parsed and re-applied on restore; the path is persisted, **not** the derived pin specs), `N` (pin → signal name; empty = NC; optional 4th field carries `nc_origin_tag()`: `U` = ImportedUnconnected, `D` = DroppedSingleton — omitted when the pin has a signal or when origin is `None`), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). The 4th N field is backward-compatible: pre-existing snapshots without it restore with `nc_origin = None`. **Signals** carry a `type` (`SignalType::Power | GndShield | Other`). The `Signal` constructor **defaults to `Other`** — auto-inference no longer happens at construction. Types are set in three ways, in priority order: -1. **`infer_signal_types(System*)`** (`src/system/analysis.{hpp,cpp}`) runs at the end of every `load` (after `drop_singleton_signals`). It assigns: +1. **`infer_signal_types(System*)`** (`src/core/domain/analysis.{hpp,cpp}`) runs at the end of every `load` (after `drop_singleton_signals`). It assigns: - `GndShield` when the **name alone** is unambiguous (`GND`, `SHIELD`, `CHASSIS`, `EARTH`, …) — false-positive rate is essentially zero on these. - `Power` requires (a) the name heuristic (`infer_signal_type` says Power), (b) a **hard fan-out floor**: signals with fewer than `POWER_FANOUT_HARD_FLOOR = 3` pins are *always* refused, regardless of name or voltage pattern (a real rail physically cannot land on just 1-2 pads), and (c) at least one positive structural signal — fan-out ≥ `POWER_FANOUT_CONFIRM_MIN = 4` **or** a voltage pattern in the name (`3V3`, `5V`, `12V`, …; detector: a `V` adjacent to a digit). This catches `VSEL_*`, `PWR_OK`, `_VDD_SENSE` etc. which look like Power by name but aren't real rails. Both thresholds are exposed in `analysis.hpp` so the analyze screen can render the same reasoning without duplicating constants. - `Other` otherwise. The "name-said-Power-but-refuted-by-structure" count is reported by `load`. @@ -123,17 +150,17 @@ Pending prompts (from incomplete inline commands) are NOT considered interactive The explore screen shows the type in the signal detail header. -**Pin spec (expected attributes)**: every Pin carries a `PinSpec spec` (`src/system/pin_spec.hpp`) — the *expected* half of verification, set from a model: `function` (Power/Ground/Signal/Clock/NoConnect/Jtag*), `direction` (In/Out/Bidir/Passive/Power), `pad` (physical package terminal, e.g. a BSDL ball), and `source` (which model wrote it). `set-connector-type` populates it via `pin_role(connector_type, pin_name) → PinSpec`. `Pin::expected_signal_type()` is now a **derived accessor** — `to_signal_type(spec.function)` (Power→Power, Ground→GndShield, else Other) — not a stored field; the *observed* half stays `Pin::signal()` + the net + inference, and `verify` diffs the two. The VPX 3U connector lookup (`vpx_3u_role`) is still a stub returning Other, so connector-typed pins resolve to function Unknown until that table is filled. **`direction`/`function`/`pad` are populated from BSDL** via `attach-bsdl` (see below) and consumed by the model-driven checks (`check_pin_specs`: contention / undriven / NC-wired) and the JTAG chain check (`check_jtag_chain`), both run by `verify`. +**Pin spec (expected attributes)**: every Pin carries a `PinSpec spec` (`src/core/domain/pin_spec.hpp`) — the *expected* half of verification, set from a model: `function` (Power/Ground/Signal/Clock/NoConnect/Jtag*), `direction` (In/Out/Bidir/Passive/Power), `pad` (physical package terminal, e.g. a BSDL ball), and `source` (which model wrote it). `set-connector-type` populates it via `pin_role(connector_type, pin_name) → PinSpec`. `Pin::expected_signal_type()` is now a **derived accessor** — `to_signal_type(spec.function)` (Power→Power, Ground→GndShield, else Other) — not a stored field; the *observed* half stays `Pin::signal()` + the net + inference, and `verify` diffs the two. The VPX 3U connector lookup (`vpx_3u_role`) is still a stub returning Other, so connector-typed pins resolve to function Unknown until that table is filled. **`direction`/`function`/`pad` are populated from BSDL** via `attach-bsdl` (see below) and consumed by the model-driven checks (`check_pin_specs`: contention / undriven / NC-wired) and the JTAG chain check (`check_jtag_chain`), both run by `verify`. **Connector pin layout (preparation)**: `pin_layout(connector_type)` returns the canonical full pin-name list for a known connector kind, and `FillPartFromLayout(part, kind)` materialises NC pins for any layout position absent from the imported netlist. `set-connector-type` calls it after setting `connector_type` (no-op today since `pin_layout` is a stub returning `{}` for everything — populate alongside `vpx_3u_role`). End-to-end chain in place: `set-connector-type → FillPartFromLayout → pin_role`. -**BSDL models (`attach-bsdl`)**: `attach-bsdl ` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/system/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports. +**BSDL models (`attach-bsdl`)**: `attach-bsdl ` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/core/domain/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports. -**Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/system/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`). Sources are ranked (`spec_source_rank` in `pin_spec.hpp`: UserOverride > Bsdl > ConnectorModel > Inferred > Imported) and apply_model refuses to overwrite a spec owned by a higher-rank source — so one source never clobbers a more authoritative one, which is also the basis for `check_source_conflicts`. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`. +**Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/core/domain/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`). Sources are ranked (`spec_source_rank` in `pin_spec.hpp`: UserOverride > Bsdl > ConnectorModel > Inferred > Imported) and apply_model refuses to overwrite a spec owned by a higher-rank source — so one source never clobbers a more authoritative one, which is also the basis for `check_source_conflicts`. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`. **`verify` (seven passes)**: (1) typed pins — local mismatch between each pin's `expected_signal_type()` (derived from its `PinSpec`) and the actual signal type; (2) bridged nets — Power↔GndShield inconsistencies; (3) orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)` (filters out pins bridged via any `Connection::pin_map` — typically `FillIdentityNCs`-materialised); (4) **model-driven pin checks** (`check_pin_specs`): `DriveContention` (≥2 push-pull `Out` on a net), `UndrivenNet` (a **fully-modelled** net with input(s) but no driver — nets with any Unknown-direction pin are skipped, so un-modelled drivers don't cause false positives), `NcWired` (a no-connect pin on a multi-pin net); (5) **JTAG chain** (`check_jtag_chain`): collects TAP pins by `spec.function`, maps each to its net, emits `JtagTapIncomplete` / `JtagBusUnbridged` (TMS or TCK not common to all TAP devices) / `JtagChainBreak` (dangling TDO/TDI, chain fan-out, or not a single head→tail daisy chain); (6) **source conflicts** (`check_source_conflicts`): a pin the BSDL declares power/ground (a must-connect rail) that the netlist leaves unconnected — a rail floated in the schematic (`SourceConflict`; the reverse, a BSDL no-connect that *is* wired, is the `NcWired` check); (7) **BSDL completeness** (`check_bsdl_completeness`): device power/ground ports (from the attached `.bsd`, re-parsed) with no matching pin on the netlist part — a rail the schematic symbol is missing (`BsdlPinMissing`, one aggregated finding per part). The BFS-reached `(module, signal)` set for any signal is shown live in `explore`'s detail pane when a signal entry is selected. -**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/system/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped): +**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/core/domain/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped): - **Diff pairs**: signal names ending `_P` / `_N` (case-insensitive) grouped by stem. Both halves present → candidate matched pair. - **Diff buses**: ≥ 2 matched diff pairs whose pair-stems share a common outer-stem after stripping a trailing integer (`MDI0` / `MDI1` / `MDI2` → outer `MDI` + indices). The strict `_` rule from plain buses does NOT apply to this trailing-index split: `_P`/`_N` was already stripped, so we know remaining digits are an index. Two index variants accepted: contiguous (`MDI0`) and underscore-separated (`PCIE_TX_0`). Emitted as `SignalGroup{kind=DiffBus, lo, hi}` with label `OUTER[lo..hi]_P/N`. Members include all 2·N constituent signals. A "bus" of size 1 falls back to `DiffPair` (single index does not a bus make). @@ -145,13 +172,13 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l **Component classification**: every `Part` carries a `ComponentKind kind` (`Passive | Semiconductor | IntegratedCircuit | Connector | TestPoint | Switch | Crystal | Mechanical | Other`) inferred at construction by `infer_component_kind(name)` from the leading reference-designator letter(s) (longest-match: `LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV` first, then single-letter R/C/L/F/D/Q/U/J/P/Y/X/S). Recomputed on `restore` (no persistence tag). Not yet exposed in TUI commands — branchpoints will be `set-connector-type` guard, `explore` filter, and `explore` header. -`SignalType` lives in its own header `src/system/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle). +`SignalType` lives in its own header `src/core/domain/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle). **Pins** are either NC (`signal() == nullptr`) or connected to exactly one signal. The ODS importer creates a Pin for every row that has a non-empty pin name, even when the signal column is empty or `"NC"` — the pin stays in the Part as NC. `restore` replaces `Tui::sys` entirely (`unique_ptr::reset`). Names are stored as-is — must not contain TAB or newline (true for the EE netlists we ingest). Format is versioned by the `# essim system snapshot v1` header for future compatibility. **NC origin tag**: each `Pin` carries `NcOrigin nc_origin` (`None | ImportedUnconnected | DroppedSingleton`, default `None`). Set in three places: (a) Mentor importer when the signal field starts with `unconnected` → `ImportedUnconnected`; (b) `drop_singleton_signals(Signals*)` called at the end of `load` → `DroppedSingleton` on each detached pin (signals with exactly one pin are NC by definition — see commits motivating this); (c) `duplicate` propagates the tag. Pins materialised by `FillIdentityNCs` keep `None` — they have no local signal but are bridged via `pin_map` and shouldn't be counted as orphans. The tag is persisted (see `N` record), reported as a total in `verify`, and tested in `tests/test_nc_origin.cpp`. -**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-connector-type` command — inline `set-connector-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/system/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the canonical-equivalent pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear. +**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-connector-type` command — inline `set-connector-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/core/domain/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the canonical-equivalent pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear. **Identity wiring uses canonical names**: `IdentityTransform::apply` builds `unordered_map` for side B and looks up each side-A pin by its canonical form. So `A1` (one card) auto-pairs with `A001` (the other) thanks to `canonical_pin_name` (`pre + zero-padded(3) digit suffix`; mixed/non-numeric returns the original). Same canonicalisation in `CheckIdentityCompatible`. **`pin_role` doesn't need canonicalisation** because `parse_pin` extracts `(col, row)` via `stoi` which already strips leading zeros. @@ -188,7 +215,7 @@ Today the only caller is `export` (`{"CSV", ".csv"}, {"ODS", ".ods"}` filters, k **Per-key path persistence** (`SaveLastUsed(key, dir, filename)` / `LoadLastUsed(key, &dir, &filename)` in `shell.cpp`): each key writes a tiny two-line file (`dir\nfilename\n`) under `UserDataDir() / .last`. `UserDataDir()` is the cross-platform `XDG_DATA_HOME` / `LOCALAPPDATA` etc. helper also used by the command history file. Free functions, not Tui members, so any module (the file dialog today; could be the script-save buffer or the save command tomorrow) can use them with the same minimal API. -**ODS writer** (`src/imports/ods_writer.{hpp,cpp}`): minimal OpenDocument Spreadsheet writer. Backed by libzip + pugixml (both already pulled in by the ODS importer). Class hierarchy is two structs: +**ODS writer** (`src/core/imports/ods_writer.{hpp,cpp}`): minimal OpenDocument Spreadsheet writer. Backed by libzip + pugixml (both already pulled in by the ODS importer). Class hierarchy is two structs: - `OdsSheet` — sparse row-major grid of string cells (`set(row, col, value)`). - `OdsWriter` — owns the sheets, emits a valid `.ods` archive with `mimetype` (stored uncompressed, magic header), `META-INF/manifest.xml`, and `content.xml`. @@ -248,9 +275,9 @@ Everything in this section is a precise description of how signals, pins, parts, Default: `Signal::type = SignalType::Other` (constructor does no inference). -Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called at the end of every `load` and after `duplicate`. The decision per signal: +Type is set by `infer_signal_types(System*)` (`src/core/domain/analysis.cpp`), called at the end of every `load` and after `duplicate`. The decision per signal: -1. Compute `named = infer_signal_type(name)` (`src/system/signals.cpp`): +1. Compute `named = infer_signal_type(name)` (`src/core/domain/signals.cpp`): - `GndShield` if name matches **case-insensitively**: `GND`, `GROUND`, `EARTH`, `SHIELD`, `CHASSIS`, or starts with `GND_` / `GROUND_` / `EARTH_` / `SHIELD_` / `CHASSIS_`. - `Power` if the name contains any of `PWR`, `POWER`, `VCC`, `VDD`, `VEE`, `VSS`, `VBAT`, or starts with `VS_`, `VS3_`, `+`, `-` followed by a digit. - Else `Other`. @@ -264,15 +291,15 @@ Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called ### NC pin origin -`Pin::nc_origin` (`src/system/pins.hpp`). Default `NcOrigin::None`. Set by: +`Pin::nc_origin` (`src/core/domain/pins.hpp`). Default `NcOrigin::None`. Set by: -- **`NcOrigin::ImportedUnconnected`** — Mentor importer (`src/imports/import_mentor.cpp`): the signal field of an `Explicit Pin:` row starts case-insensitively with `unconnected` (e.g. `'unconnected'`, `'unconnected (by TERM)'`). The pin is kept on the part with no signal. -- **`NcOrigin::DroppedSingleton`** — `drop_singleton_signals(Signals*)` (`src/system/signals.cpp`) called at the end of `load`: any signal whose pin set has size exactly 1 is unconnected by definition. The pin is detached (`sig = nullptr`) and tagged; the `Signal` object is deleted. +- **`NcOrigin::ImportedUnconnected`** — Mentor importer (`src/core/imports/import_mentor.cpp`): the signal field of an `Explicit Pin:` row starts case-insensitively with `unconnected` (e.g. `'unconnected'`, `'unconnected (by TERM)'`). The pin is kept on the part with no signal. +- **`NcOrigin::DroppedSingleton`** — `drop_singleton_signals(Signals*)` (`src/core/domain/signals.cpp`) called at the end of `load`: any signal whose pin set has size exactly 1 is unconnected by definition. The pin is detached (`sig = nullptr`) and tagged; the `Signal` object is deleted. - **`NcOrigin::None` (no tag)** — pins materialised by `FillIdentityNCs` at `connect` time. These are unconnected locally but bridged to a real signal on the peer module via `Connection::pin_map`; they are explicitly excluded from the "orphan" count in `verify` and the analyze screen. ### Signal groups -`analyze_system(System*)` (`src/system/analysis.cpp`) emits `SignalGroup`s **per module** (signals are module-scoped). Internal names (starting with `$` — Mentor's `$Nxxxx` convention) are skipped wholesale at every step. +`analyze_system(System*)` (`src/core/domain/analysis.cpp`) emits `SignalGroup`s **per module** (signals are module-scoped). Internal names (starting with `$` — Mentor's `$Nxxxx` convention) are skipped wholesale at every step. **DiffPair** (`GroupKind::DiffPair`): - Signal name ends `_P` or `_N` (case-insensitive). The character before the suffix must be `_`. @@ -303,7 +330,7 @@ The `verify` command (not the analyze screen, yet) also emits the **model-driven ### Component kind -`Part::kind` is inferred at construction (`src/system/component_kind.cpp`) from the leading reference-designator letter(s) of the part name. **Longest-match wins**: +`Part::kind` is inferred at construction (`src/core/domain/component_kind.cpp`) from the leading reference-designator letter(s) of the part name. **Longest-match wins**: - Two-letter prefixes (checked first, case-insensitive): `LED → Semiconductor`, `TP → TestPoint`, `SW → Switch`, `FB → Passive`, `MK / MP / MH → Mechanical`, `HS → Mechanical`, `RA / RN / RP / RV → Passive`. - Single-letter fallback: `R / C / L / F → Passive`, `D / Q → Semiconductor`, `U → IntegratedCircuit`, `J / P → Connector`, `Y / X → Crystal`, `S → Switch`. @@ -315,7 +342,7 @@ Recomputed on `restore` (no persistence tag). Currently not used by any decision `connect` looks up a registered transform for `(p1->connector_type, p2->connector_type)` via `TransformRegistry::lookup`, tried in both directions. Fall-through is `IdentityTransform`: -- Compares pin sets by **canonical name** (`canonical_pin_name(s)`, `src/system/pin_name.cpp`): split into prefix + digit suffix; if the suffix is pure digits, zero-pad to width 3 (`A1` → `A001`, `A001` → `A001`, `A1B` → `A1B`, `VCC` → `VCC`). +- Compares pin sets by **canonical name** (`canonical_pin_name(s)`, `src/core/domain/pin_name.cpp`): split into prefix + digit suffix; if the suffix is pure digits, zero-pad to width 3 (`A1` → `A001`, `A001` → `A001`, `A1B` → `A1B`, `VCC` → `VCC`). - `CheckIdentityCompatible(a, b)` accepts the **subset case** (one side's canonical set is a subset of the other's — typical because Altium drops NC, Mentor doesn't). Bidirectional mismatch (both sides have orphans) is refused. - After acceptance, `FillIdentityNCs(p1, p2)` **materialises** the missing canonical positions on the smaller side as new NC pins (`new Pin(other_side_name)`, no signal attached, `nc_origin = None`). Idempotent. diff --git a/README.md b/README.md index e9efe95..9de2b37 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,13 @@ cmake --build build -j ./build/essim ``` +The build is **layered**: `essim_core` is the frontend-agnostic business +library (domain + importers + operations); the `essim` binary comes from a +**frontend** under `src/frontends//` that links it. Select one with +`-DESSIM_FRONTEND=` (default `tui`); `-DESSIM_FRONTEND=none` builds the +core + tests only, with no GUI toolkit fetched. Architecture in +[`DESIGN.md`](DESIGN.md). + Inside the shell, type `help` for the live command list — or read the auto-generated reference at [`doc/user/commands.md`](doc/user/commands.md). A worked bring-up script is at [`test/system.essim`](test/system.essim); @@ -41,10 +48,14 @@ Step-by-step walkthroughs for both the batch and TUI workflows are in `../libbsdl`, pulled in via `add_subdirectory` and linked dynamically. Override its location with `-DBSDL_DIR=/path/to/libbsdl`. Powers the `attach-bsdl` command and the pin/JTAG checks. -- Fetched automatically at configure time via `FetchContent` (nothing to - install): **FTXUI** v6.1.9 and **doctest** v2.4.11. +- Fetched automatically via `FetchContent` (nothing to install): **FTXUI** + v6.1.9 — only when building the **tui** frontend — and **doctest** v2.4.11 + for the tests. - Optional, only for the `doc` target: **doxygen** and **python3**. +libzip, pugixml and libbsdl are the **core** dependencies; FTXUI belongs to the +tui frontend, so a `-DESSIM_FRONTEND=none` build needs none of it. + ## Tests ```sh @@ -53,6 +64,9 @@ Step-by-step walkthroughs for both the batch and TUI workflows are in ctest --test-dir build ``` +`ctest` runs `essim_tests` (core — links `essim_core`, no GUI toolkit) and +`essim_tui_tests` (the FTXUI frontend's tests, under `tests/tui/`). + Skip building tests entirely: ```sh @@ -76,12 +90,17 @@ cmake --build build --target doc # needs doxygen + python3 ## Project layout ``` -src/system/ domain model (Module/Part/Pin/Signal, Connection, Transform, …) -src/imports/ Mentor / Altium / ODS netlist importers -src/tui/ FTXUI shell (commands, screens, completion, history) -tests/ doctest suite -doc/ api/ + user/ Markdown trees, Doxyfile.in, gen_api_md.py -test/ sample netlists + system.essim bring-up script +src/ + core/ business logic, NO GUI toolkit (→ libessim_core) + domain/ model (Module/Part/Pin/Signal, Connection, Transform…) + analyses + imports/ Mentor / Altium / ODS netlist importers + ODS writer + app/ use-case operations (export → CSV/ODS, …) + frontends/ one dir per GUI/TUI engine, each links essim_core + tui/ FTXUI shell + main.cpp (→ libessim_tui + the `essim` binary) +tests/ core tests (link essim_core) + tui/ frontend tests (link essim_tui) +doc/ api/ + user/ Markdown, Doxyfile.in, gen_api_md.py +test/ sample netlists + system.essim bring-up script ``` Full layout & rationale in [`DESIGN.md`](DESIGN.md). diff --git a/src/frontends/tui/CMakeLists.txt b/src/frontends/tui/CMakeLists.txt index ea58b21..684e03f 100644 --- a/src/frontends/tui/CMakeLists.txt +++ b/src/frontends/tui/CMakeLists.txt @@ -33,3 +33,7 @@ target_link_libraries(essim_tui add_executable(essim "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") target_link_libraries(essim PRIVATE essim_tui) + +# Keep the binary at the top of the build tree (./build/essim), regardless of +# which frontend subdir produced it. +set_target_properties(essim PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") From e3350b8d95062c9672a52371960aed403575ad34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 19:51:53 +0200 Subject: [PATCH 04/11] Extract verify into core (app::verify); thin the TUI command. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the 7-pass verify orchestration out of the TUI command lambda and into core/app/verify.{hpp,cpp}: app::verify(System*) returns a structured VerifyReport (role mismatches, net inconsistencies, orphan counts, the four model-driven anomaly vectors) with no Print/dialog/FTXUI. The nets are computed once and fed to the net-based checks. The verify command is now a thin renderer over the report, byte-identical output. Prune the now-dead nets.hpp / bsdl_check.hpp / includes from commands.cpp. Add tests/test_verify.cpp: builds small systems by hand and asserts the report (empty system, Power/GndShield bridged-net inconsistency, orphan counts by import origin) — pure core, no UI. This is the structuring extraction: the same VerifyReport can now back the analyze screen's Issues pane and the dashboard health rows, removing the triple duplication of passes 1-3. Co-Authored-By: Claude Opus 4.8 --- src/core/app/verify.cpp | 98 +++++++++++++++++++++++++ src/core/app/verify.hpp | 61 ++++++++++++++++ src/frontends/tui/commands.cpp | 128 +++++++++------------------------ tests/test_verify.cpp | 80 +++++++++++++++++++++ 4 files changed, 272 insertions(+), 95 deletions(-) create mode 100644 src/core/app/verify.cpp create mode 100644 src/core/app/verify.hpp create mode 100644 tests/test_verify.cpp diff --git a/src/core/app/verify.cpp b/src/core/app/verify.cpp new file mode 100644 index 0000000..29ae7d4 --- /dev/null +++ b/src/core/app/verify.cpp @@ -0,0 +1,98 @@ +#include "core/app/verify.hpp" + +#include "core/domain/bsdl_check.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/nets.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" + +#include +#include +#include + +namespace app { + +VerifyReport verify(System *sys) +{ + VerifyReport r; + if (!sys) + return r; + + // Pass 1 — typed pins: expected (model) vs actual (net) signal type. + for (auto &mkv : *sys->modules()) { + Module *mod = mkv.second; + for (auto &pkv : *mod) { + Part *prt = pkv.second; + if (prt->connector_type.empty()) + continue; + for (auto &nkv : *prt) { + Pin *pin = nkv.second; + ++r.typed_pins; + SignalType expected = pin->expected_signal_type(); + if (expected == SignalType::Other) + continue; + Signal *s = pin->signal(); + SignalType actual = s ? s->type : SignalType::Other; + if (actual == expected) + continue; + RoleMismatch m; + m.module = mod->name; + m.part = prt->name; + m.pin = pin->name; + m.signal = s ? s->name : std::string("(NC)"); + m.expected = expected; + m.actual = actual; + r.role_mismatches.push_back(std::move(m)); + } + } + } + + // Pass 2 — bridged nets: flag Power/GndShield mixing. Compute the nets once + // here and reuse them for the model checks below. + std::vector nets = compute_all_nets(sys); + r.total_nets = (int)nets.size(); + for (const Net &n : nets) { + if (n.members.size() < 2) + continue; + ++r.bridged_nets; + SignalType dom; + if (net_type_consistent(n, dom)) + continue; + NetInconsistency ni; + for (const auto &mp : n.members) + ni.members.push_back({mp.first->name, mp.second->name, mp.second->type}); + r.net_inconsistencies.push_back(std::move(ni)); + } + + // Pass 3 — orphans: pins with no signal and not bridged via a connection. + std::unordered_set bridged_pins; + for (auto &ckv : *sys->connections()) + for (auto &wp : ckv.second->pin_map) { + if (wp.first) bridged_pins.insert(wp.first); + if (wp.second) bridged_pins.insert(wp.second); + } + for (auto &mkv : *sys->modules()) + for (auto &pkv : *mkv.second) + for (auto &nkv : *pkv.second) { + Pin *pin = nkv.second; + if (pin->signal() || bridged_pins.count(pin)) + continue; + if (pin->nc_origin == NcOrigin::ImportedUnconnected) + ++r.orphan_imported; + else if (pin->nc_origin == NcOrigin::DroppedSingleton) + ++r.orphan_dropped; + } + + // Passes 4-7 — model-driven checks (reuse the nets from pass 2). + r.pin_anomalies = check_pin_specs(sys, &nets); + r.jtag_anomalies = check_jtag_chain(sys, &nets); + r.conflict_anomalies = check_source_conflicts(sys); + r.completeness_anomalies = check_bsdl_completeness(sys); + + return r; +} + +} // namespace app diff --git a/src/core/app/verify.hpp b/src/core/app/verify.hpp new file mode 100644 index 0000000..007dcdf --- /dev/null +++ b/src/core/app/verify.hpp @@ -0,0 +1,61 @@ +#ifndef _APP_VERIFY_HPP_ +#define _APP_VERIFY_HPP_ + +#include "core/domain/analysis.hpp" // Anomaly +#include "core/domain/signal_type.hpp" // SignalType + +#include +#include + +class System; + +namespace app { + +// One typed-pin role mismatch: the connector/BSDL expectation disagrees with +// the actual net type. +struct RoleMismatch { + std::string module, part, pin; + std::string signal; ///< signal name, or "(NC)" + SignalType expected = SignalType::Other; + SignalType actual = SignalType::Other; +}; + +// One bridged net mixing Power and GndShield, with its members for display. +struct NetInconsistency { + struct Member { std::string module, signal; SignalType type; }; + std::vector members; +}; + +// The full result of `verify`: structured data only — no strings beyond the +// names, no formatting. Frontends (the verify command, the analyze screen, the +// dashboard) render it however they like. +struct VerifyReport { + int typed_pins = 0; ///< pins with a non-Other expectation considered + std::vector role_mismatches; + + int total_nets = 0; + int bridged_nets = 0; + std::vector net_inconsistencies; + + int orphan_imported = 0; + int orphan_dropped = 0; + + std::vector pin_anomalies; ///< check_pin_specs + std::vector jtag_anomalies; ///< check_jtag_chain + std::vector conflict_anomalies; ///< check_source_conflicts + std::vector completeness_anomalies; ///< check_bsdl_completeness + + int orphan_total() const { return orphan_imported + orphan_dropped; } + int model_total() const { + return (int)(pin_anomalies.size() + jtag_anomalies.size() + + conflict_anomalies.size() + completeness_anomalies.size()); + } +}; + +// Run every verify pass over the system and return the findings. Pure core — +// computes the nets once and feeds them to the net-based checks. +VerifyReport verify(System *sys); + +} // namespace app + +#endif // _APP_VERIFY_HPP_ diff --git a/src/frontends/tui/commands.cpp b/src/frontends/tui/commands.cpp index 0906856..18e2996 100644 --- a/src/frontends/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -4,16 +4,16 @@ #include "core/domain/analysis.hpp" #include "core/domain/connect.hpp" #include "core/domain/modules.hpp" -#include "core/domain/nets.hpp" #include "core/domain/parts.hpp" #include "core/domain/persist.hpp" #include "core/domain/pin_role.hpp" #include "core/domain/pin_model.hpp" #include "core/domain/bsdl_model.hpp" -#include "core/domain/bsdl_check.hpp" #include "core/domain/pins.hpp" #include "core/domain/signals.hpp" #include "core/domain/system.hpp" + +#include "core/app/verify.hpp" #include "core/domain/transform.hpp" #include "core/domain/transform_vpx.hpp" @@ -22,7 +22,6 @@ #include #include #include -#include #include void Tui::RegisterCommands() { @@ -228,104 +227,43 @@ void Tui::RegisterCommands() { commands["verify"] = { {}, [this](auto &) { if (!sys) { Print("no system: run 'new' first."); return; } - int checked = 0; - int mismatches = 0; - for (auto &mkv : *sys->modules()) { - Module *mod = mkv.second; - for (auto &pkv : *mod) { - Part *prt = pkv.second; - if (prt->connector_type.empty()) continue; - for (auto &nkv : *prt) { - Pin *pin = nkv.second; - ++checked; - SignalType expected = pin->expected_signal_type(); - if (expected == SignalType::Other) continue; - Signal *s = pin->signal(); - SignalType actual = s ? s->type : SignalType::Other; - if (actual == expected) continue; - ++mismatches; - std::string sig_label = s ? s->name : std::string("(NC)"); - Print(" " + mod->name + "/" + prt->name + "/" + pin->name - + ": expected " + signal_type_name(expected) - + ", got " + signal_type_name(actual) - + " (signal: " + sig_label + ")"); - } - } - } - Print("verify: " + std::to_string(mismatches) + " local mismatch(es) over " - + std::to_string(checked) + " typed pin(s)."); + app::VerifyReport r = app::verify(sys.get()); - auto nets = compute_all_nets(sys.get()); - int bridged = 0, inconsistent = 0; - for (const auto &n : nets) { - if (n.members.size() < 2) continue; - ++bridged; - SignalType dom; - if (net_type_consistent(n, dom)) continue; - ++inconsistent; + for (const auto &m : r.role_mismatches) + Print(" " + m.module + "/" + m.part + "/" + m.pin + + ": expected " + signal_type_name(m.expected) + + ", got " + signal_type_name(m.actual) + + " (signal: " + m.signal + ")"); + Print("verify: " + std::to_string(r.role_mismatches.size()) + + " local mismatch(es) over " + std::to_string(r.typed_pins) + + " typed pin(s)."); + + for (const auto &ni : r.net_inconsistencies) { std::string line = " net mixes Power and GndShield:"; - for (const auto &mp : n.members) { - line += " " + mp.first->name + "/" + mp.second->name - + "(" + signal_type_name(mp.second->type) + ")"; - } + for (const auto &mem : ni.members) + line += " " + mem.module + "/" + mem.signal + + "(" + signal_type_name(mem.type) + ")"; Print(line); } - Print("verify: " + std::to_string(inconsistent) + " inconsistent net(s) over " - + std::to_string(bridged) + " bridged net(s) (" - + std::to_string(nets.size()) + " total)."); + Print("verify: " + std::to_string(r.net_inconsistencies.size()) + + " inconsistent net(s) over " + std::to_string(r.bridged_nets) + + " bridged net(s) (" + std::to_string(r.total_nets) + " total)."); - // Orphan pin report. A pin is "orphan" if it came out of import (or - // post-import drop) with no signal, and is still not bridged to a - // real signal via any Connection::pin_map. Use `nc-export` for the - // per-pin list. - std::unordered_set bridged_pins; - for (auto &ckv : *sys->connections()) - for (auto &wp : ckv.second->pin_map) { - if (wp.first) bridged_pins.insert(wp.first); - if (wp.second) bridged_pins.insert(wp.second); - } - int orph_imported = 0, orph_dropped = 0; - for (auto &mkv : *sys->modules()) - for (auto &pkv : *mkv.second) - for (auto &nkv : *pkv.second) { - Pin *pin = nkv.second; - if (pin->signal() || bridged_pins.count(pin)) continue; - if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported; - else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped; - } - Print("verify: " + std::to_string(orph_imported + orph_dropped) - + " orphan pin(s) at import (" - + std::to_string(orph_imported) + " imported NC, " - + std::to_string(orph_dropped) + " dropped singleton)."); + Print("verify: " + std::to_string(r.orphan_total()) + + " orphan pin(s) at import (" + std::to_string(r.orphan_imported) + + " imported NC, " + std::to_string(r.orphan_dropped) + + " dropped singleton)."); - // Model-driven pin checks (drive contention / undriven net / NC-wired) - // from the PinSpec direction/function populated by connector/BSDL models. - auto pin_anoms = check_pin_specs(sys.get(), &nets); - for (const auto &a : pin_anoms) - Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); - Print("verify: " + std::to_string(pin_anoms.size()) - + " model-driven pin anomaly(ies)."); - - // JTAG boundary-scan chain integrity (TAP pins → nets). - auto jtag_anoms = check_jtag_chain(sys.get(), &nets); - for (const auto &a : jtag_anoms) - Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); - Print("verify: " + std::to_string(jtag_anoms.size()) - + " JTAG chain anomaly(ies)."); - - // Model-vs-netlist conflicts (e.g. a BSDL power pin left unconnected). - auto conflict_anoms = check_source_conflicts(sys.get()); - for (const auto &a : conflict_anoms) - Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); - Print("verify: " + std::to_string(conflict_anoms.size()) - + " source-conflict(s)."); - - // BSDL completeness: device power/ground pins missing from the netlist. - auto missing_anoms = check_bsdl_completeness(sys.get()); - for (const auto &a : missing_anoms) - Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); - Print("verify: " + std::to_string(missing_anoms.size()) - + " BSDL completeness issue(s)."); + // Each model-driven group: per-finding lines + a one-line summary. + auto render = [this](const std::vector &v, const char *tail) { + for (const auto &a : v) + Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); + Print("verify: " + std::to_string(v.size()) + tail); + }; + render(r.pin_anomalies, " model-driven pin anomaly(ies)."); + render(r.jtag_anomalies, " JTAG chain anomaly(ies)."); + render(r.conflict_anomalies, " source-conflict(s)."); + render(r.completeness_anomalies, " BSDL completeness issue(s)."); }, true, "check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" }; diff --git a/tests/test_verify.cpp b/tests/test_verify.cpp new file mode 100644 index 0000000..713574a --- /dev/null +++ b/tests/test_verify.cpp @@ -0,0 +1,80 @@ +#include + +#include "core/app/verify.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/signals.hpp" +#include "core/domain/system.hpp" + +// app::verify is pure core: it takes a System* and returns a VerifyReport of +// structured findings, with no Print/dialog/FTXUI. These tests build small +// systems by hand and assert the report — no UI involved. + +TEST_CASE("verify on a null or empty system reports nothing") { + app::VerifyReport none = app::verify(nullptr); + CHECK(none.typed_pins == 0); + CHECK(none.total_nets == 0); + CHECK(none.role_mismatches.empty()); + + System sys; + app::VerifyReport r = app::verify(&sys); + CHECK(r.typed_pins == 0); + CHECK(r.total_nets == 0); + CHECK(r.bridged_nets == 0); + CHECK(r.net_inconsistencies.empty()); + CHECK(r.orphan_total() == 0); + CHECK(r.model_total() == 0); +} + +TEST_CASE("verify flags a bridged net that mixes Power and GndShield") { + // Two cards, one wired pin pair: A.NETA (Power) <-> B.NETB (GndShield). + System sys; + Module *a = sys.modules()->merge("A"); + Module *b = sys.modules()->merge("B"); + Part *ja = new Part("J1"); a->add(ja); + Part *jb = new Part("P1"); b->add(jb); + Pin *pa = new Pin("1"); ja->add(pa); + Pin *pb = new Pin("1"); jb->add(pb); + Signal *sa = a->signals->merge("NETA"); sa->type = SignalType::Power; + Signal *sb = b->signals->merge("NETB"); sb->type = SignalType::GndShield; + sa->add(pa); pa->connect(sa); + sb->add(pb); pb->connect(sb); + + Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb); + c->transform_name = "identity"; + c->pin_map.emplace_back(pa, pb); + sys.connections()->add(c); + + app::VerifyReport r = app::verify(&sys); + + CHECK(r.total_nets == 1); + CHECK(r.bridged_nets == 1); + REQUIRE(r.net_inconsistencies.size() == 1); + CHECK(r.net_inconsistencies[0].members.size() == 2); + // Both endpoints are present with their declared types. + bool seen_power = false, seen_gnd = false; + for (const auto &m : r.net_inconsistencies[0].members) { + if (m.type == SignalType::Power) seen_power = true; + if (m.type == SignalType::GndShield) seen_gnd = true; + } + CHECK(seen_power); + CHECK(seen_gnd); +} + +TEST_CASE("verify counts orphan pins by their import origin") { + System sys; + Module *m = sys.modules()->merge("M"); + Part *p = new Part("J1"); m->add(p); + Pin *imp = new Pin("1"); imp->nc_origin = NcOrigin::ImportedUnconnected; p->add(imp); + Pin *drp = new Pin("2"); drp->nc_origin = NcOrigin::DroppedSingleton; p->add(drp); + Pin *wired = new Pin("3"); p->add(wired); + Signal *s = m->signals->merge("NET"); s->add(wired); wired->connect(s); + + app::VerifyReport r = app::verify(&sys); + + CHECK(r.orphan_imported == 1); + CHECK(r.orphan_dropped == 1); + CHECK(r.orphan_total() == 2); +} From 25939998abb8f221cd8b925c97a0b55c7a113d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 20:04:31 +0200 Subject: [PATCH 05/11] De-dup verify passes: drive analyze screen + dashboard from app::verify. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The analyze Issues pane and the dashboard Health rows each recomputed the same verify passes inline (pin-role mismatches, Power/Gnd net-mix, NC orphan rollup, model-driven checks) — the third and second copies of what the verify command also did. Route both screens through app::verify(System*) instead, so the passes live in exactly one place. Enrich VerifyReport with a per-pin OrphanPin detail list (module/part/pin + dropped flag) so the dashboard can still nest its dropped-singleton breakdown under the NC health line without re-walking modules/parts/pins. Output is unchanged in both screens (same label formats, same numbers). Prune the now-dead includes (nets/bsdl_check/connect/parts/pins as applicable, ) from both screens. Extend tests/test_verify.cpp to cover the new orphans detail. Co-Authored-By: Claude Opus 4.8 --- src/core/app/verify.cpp | 11 ++- src/core/app/verify.hpp | 8 ++ src/frontends/tui/screen_analyze.cpp | 109 ++++++++----------------- src/frontends/tui/screen_dashboard.cpp | 73 ++++------------- tests/test_verify.cpp | 15 ++++ 5 files changed, 82 insertions(+), 134 deletions(-) diff --git a/src/core/app/verify.cpp b/src/core/app/verify.cpp index 29ae7d4..dfc0044 100644 --- a/src/core/app/verify.cpp +++ b/src/core/app/verify.cpp @@ -80,10 +80,17 @@ VerifyReport verify(System *sys) Pin *pin = nkv.second; if (pin->signal() || bridged_pins.count(pin)) continue; - if (pin->nc_origin == NcOrigin::ImportedUnconnected) + bool dropped; + if (pin->nc_origin == NcOrigin::ImportedUnconnected) { ++r.orphan_imported; - else if (pin->nc_origin == NcOrigin::DroppedSingleton) + dropped = false; + } else if (pin->nc_origin == NcOrigin::DroppedSingleton) { ++r.orphan_dropped; + dropped = true; + } else { + continue; + } + r.orphans.push_back({mkv.first, pkv.first, nkv.first, dropped}); } // Passes 4-7 — model-driven checks (reuse the nets from pass 2). diff --git a/src/core/app/verify.hpp b/src/core/app/verify.hpp index 007dcdf..38c905d 100644 --- a/src/core/app/verify.hpp +++ b/src/core/app/verify.hpp @@ -26,6 +26,13 @@ struct NetInconsistency { std::vector members; }; +// One orphan pin: no signal and not bridged via a connection. `dropped` is true +// for a dropped singleton (essim detached it), false for an import-time NC. +struct OrphanPin { + std::string module, part, pin; + bool dropped = false; +}; + // The full result of `verify`: structured data only — no strings beyond the // names, no formatting. Frontends (the verify command, the analyze screen, the // dashboard) render it however they like. @@ -39,6 +46,7 @@ struct VerifyReport { int orphan_imported = 0; int orphan_dropped = 0; + std::vector orphans; ///< per-pin detail (both origins) std::vector pin_anomalies; ///< check_pin_specs std::vector jtag_anomalies; ///< check_jtag_chain diff --git a/src/frontends/tui/screen_analyze.cpp b/src/frontends/tui/screen_analyze.cpp index 2f5f425..19c755c 100644 --- a/src/frontends/tui/screen_analyze.cpp +++ b/src/frontends/tui/screen_analyze.cpp @@ -1,13 +1,9 @@ #include "frontends/tui/tui.hpp" #include "frontends/tui/tui_helpers.hpp" +#include "core/app/verify.hpp" #include "core/domain/analysis.hpp" -#include "core/domain/bsdl_check.hpp" -#include "core/domain/connect.hpp" #include "core/domain/modules.hpp" -#include "core/domain/nets.hpp" -#include "core/domain/parts.hpp" -#include "core/domain/pins.hpp" #include "core/domain/signals.hpp" #include "core/domain/system.hpp" @@ -17,7 +13,6 @@ #include #include -#include using namespace ftxui; @@ -57,41 +52,23 @@ Component Tui::BuildAnalyzeScreen() { // connection), then structural anomalies from the analysis pass. analyze_issues.clear(); - int n_role_mismatches = 0, n_typed_pins = 0; - for (auto &mkv : *sys->modules()) - for (auto &pkv : *mkv.second) { - Part *prt = pkv.second; - if (prt->connector_type.empty()) continue; - for (auto &nkv : *prt) { - Pin *pin = nkv.second; - ++n_typed_pins; - SignalType expected = pin->expected_signal_type(); - if (expected == SignalType::Other) continue; - Signal *s = pin->signal(); - SignalType actual = s ? s->type : SignalType::Other; - if (actual == expected) continue; - ++n_role_mismatches; - std::string sig_label = s ? s->name : std::string("(NC)"); - analyze_issues.push_back( - "[pin-role] " + mkv.first + "/" + prt->name + "/" - + pin->name + ": expected " + signal_type_name(expected) - + ", got " + signal_type_name(actual) - + " (signal: " + sig_label + ")"); - } - } + // verify + structural anomalies. The verify passes (pin-role, net-mix, + // orphans, model checks) come from the shared core op; the structural + // anomalies (diff-pair/bus) come from analyze_system above. + app::VerifyReport vr = app::verify(sys.get()); - auto nets = compute_all_nets(sys.get()); - int n_bridged = 0, n_inconsistent = 0; - for (const auto &n : nets) { - if (n.members.size() < 2) continue; - ++n_bridged; - SignalType dom; - if (net_type_consistent(n, dom)) continue; - ++n_inconsistent; + for (const auto &m : vr.role_mismatches) + analyze_issues.push_back( + "[pin-role] " + m.module + "/" + m.part + "/" + m.pin + + ": expected " + signal_type_name(m.expected) + + ", got " + signal_type_name(m.actual) + + " (signal: " + m.signal + ")"); + + for (const auto &ni : vr.net_inconsistencies) { std::string line = "[net-mix] mixes Power and Gnd:"; - for (const auto &mp : n.members) - line += " " + mp.first->name + "/" + mp.second->name - + "(" + signal_type_name(mp.second->type) + ")"; + for (const auto &mem : ni.members) + line += " " + mem.module + "/" + mem.signal + + "(" + signal_type_name(mem.type) + ")"; analyze_issues.push_back(std::move(line)); } @@ -100,28 +77,25 @@ Component Tui::BuildAnalyzeScreen() { + anomaly_kind_name(a.kind) + "] " + a.message); - // Model-driven checks (same as `verify`), reusing the nets above. - std::vector model_anoms; - { - auto a1 = check_pin_specs(sys.get(), &nets); - auto a2 = check_jtag_chain(sys.get(), &nets); - auto a3 = check_source_conflicts(sys.get()); - auto a4 = check_bsdl_completeness(sys.get()); - model_anoms.insert(model_anoms.end(), a1.begin(), a1.end()); - model_anoms.insert(model_anoms.end(), a2.begin(), a2.end()); - model_anoms.insert(model_anoms.end(), a3.begin(), a3.end()); - model_anoms.insert(model_anoms.end(), a4.begin(), a4.end()); - } - for (const auto &a : model_anoms) - analyze_issues.push_back(std::string("[") - + anomaly_kind_name(a.kind) + "] " - + a.message); - int n_model = (int)model_anoms.size(); + // Model-driven checks (pin / JTAG / source-conflict / completeness). + auto push_anoms = [this](const std::vector &v) { + for (const auto &a : v) + analyze_issues.push_back(std::string("[") + + anomaly_kind_name(a.kind) + "] " + + a.message); + }; + push_anoms(vr.pin_anomalies); + push_anoms(vr.jtag_anomalies); + push_anoms(vr.conflict_anomalies); + push_anoms(vr.completeness_anomalies); + int n_model = vr.model_total(); if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)"); if (analyze_issue_idx >= (int)analyze_issues.size()) analyze_issue_idx = (int)analyze_issues.size() - 1; + int n_role_mismatches = (int)vr.role_mismatches.size(); + int n_inconsistent = (int)vr.net_inconsistencies.size(); std::string issues_header = "Issues (" + std::to_string(n_role_mismatches + n_inconsistent + (int)rep.anomalies.size() + n_model) @@ -215,26 +189,11 @@ Component Tui::BuildAnalyzeScreen() { std::string(tag) + r.mod + "/" + r.sig + " — " + reason); } - // NC orphan rollup — same filter as the verify pass. - std::unordered_set bridged_pins; - for (auto &ckv : *sys->connections()) - for (auto &wp : ckv.second->pin_map) { - if (wp.first) bridged_pins.insert(wp.first); - if (wp.second) bridged_pins.insert(wp.second); - } - int orph_imported = 0, orph_dropped = 0; - for (auto &mkv : *sys->modules()) - for (auto &pkv : *mkv.second) - for (auto &nkv : *pkv.second) { - Pin *pin = nkv.second; - if (pin->signal() || bridged_pins.count(pin)) continue; - if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported; - else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped; - } + // NC orphan rollup — from the shared verify report. analyze_types.push_back( - "[NC] orphan pin(s): " + std::to_string(orph_imported + orph_dropped) - + " (" + std::to_string(orph_imported) + " imported, " - + std::to_string(orph_dropped) + " dropped)"); + "[NC] orphan pin(s): " + std::to_string(vr.orphan_total()) + + " (" + std::to_string(vr.orphan_imported) + " imported, " + + std::to_string(vr.orphan_dropped) + " dropped)"); if (analyze_type_idx >= (int)analyze_types.size()) analyze_type_idx = (int)analyze_types.size() - 1; diff --git a/src/frontends/tui/screen_dashboard.cpp b/src/frontends/tui/screen_dashboard.cpp index 27f4dfd..0103747 100644 --- a/src/frontends/tui/screen_dashboard.cpp +++ b/src/frontends/tui/screen_dashboard.cpp @@ -1,13 +1,11 @@ #include "frontends/tui/tui.hpp" #include "frontends/tui/tui_helpers.hpp" +#include "core/app/verify.hpp" #include "core/domain/analysis.hpp" -#include "core/domain/bsdl_check.hpp" #include "core/domain/connect.hpp" #include "core/domain/modules.hpp" -#include "core/domain/nets.hpp" #include "core/domain/parts.hpp" -#include "core/domain/pins.hpp" #include "core/domain/signals.hpp" #include "core/domain/system.hpp" @@ -16,7 +14,6 @@ #include #include -#include #include using namespace ftxui; @@ -77,58 +74,23 @@ Component Tui::BuildDashboardScreen() { } int n_conn = (int)sys->connections()->size(); - // ---- verify-style health (recomputed; cheap on realistic sizes) ---- - int n_role_mismatches = 0, n_typed_pins = 0; - for (auto &mkv : *sys->modules()) - for (auto &pkv : *mkv.second) { - Part *prt = pkv.second; - if (prt->connector_type.empty()) continue; - for (auto &nkv : *prt) { - Pin *pin = nkv.second; - ++n_typed_pins; - SignalType expected = pin->expected_signal_type(); - if (expected == SignalType::Other) continue; - Signal *s = pin->signal(); - SignalType actual = s ? s->type : SignalType::Other; - if (actual != expected) ++n_role_mismatches; - } - } + // ---- verify-style health (shared core op; cheap on realistic sizes) ---- + app::VerifyReport vr = app::verify(sys.get()); + int n_role_mismatches = (int)vr.role_mismatches.size(); + int n_typed_pins = vr.typed_pins; + int n_inconsistent = (int)vr.net_inconsistencies.size(); + int n_bridged = vr.bridged_nets; + int orph_imported = vr.orphan_imported; + int orph_dropped = vr.orphan_dropped; - auto nets = compute_all_nets(sys.get()); - int n_bridged = 0, n_inconsistent = 0; - for (const auto &n : nets) { - if (n.members.size() < 2) continue; - ++n_bridged; - SignalType dom; - if (!net_type_consistent(n, dom)) ++n_inconsistent; - } - - // ---- NC orphan summary (matches verify pass 3) ---- - std::unordered_set bridged_pins; - for (auto &ckv : *sys->connections()) - for (auto &wp : ckv.second->pin_map) { - if (wp.first) bridged_pins.insert(wp.first); - if (wp.second) bridged_pins.insert(wp.second); - } - int orph_imported = 0, orph_dropped = 0; // Per-module list of dropped-singleton pins, for the detail rows below // the NC health line. The signal name is gone (the Signal object was // deleted by `drop_singleton_signals`), but the pin's full path is // enough to locate it in `explore`. std::map> dropped_by_module; - for (auto &mkv : *sys->modules()) - for (auto &pkv : *mkv.second) - for (auto &nkv : *pkv.second) { - Pin *pin = nkv.second; - if (pin->signal() || bridged_pins.count(pin)) continue; - if (pin->nc_origin == NcOrigin::ImportedUnconnected) { - ++orph_imported; - } else if (pin->nc_origin == NcOrigin::DroppedSingleton) { - ++orph_dropped; - dropped_by_module[mkv.first].push_back( - pkv.first + "/" + nkv.first); - } - } + for (const auto &o : vr.orphans) + if (o.dropped) + dropped_by_module[o.module].push_back(o.part + "/" + o.pin); auto health_line = [](bool ok, const std::string &s) { return hbox({ @@ -144,7 +106,7 @@ Component Tui::BuildDashboardScreen() { + " typed pin(s)")); health_rows.push_back(health_line(n_inconsistent == 0, "nets: " + std::to_string(n_inconsistent) + " inconsistent over " - + std::to_string(n_bridged) + " bridged (" + std::to_string(nets.size()) + + std::to_string(n_bridged) + " bridged (" + std::to_string(vr.total_nets) + " total)")); int orph_total = orph_imported + orph_dropped; health_rows.push_back(health_line(orph_total == 0, @@ -172,12 +134,9 @@ Component Tui::BuildDashboardScreen() { } } - // Model-driven checks (BSDL pin specs, JTAG chain, source conflicts), - // reusing the nets computed above. - int n_model = (int)(check_pin_specs(sys.get(), &nets).size() - + check_jtag_chain(sys.get(), &nets).size() - + check_source_conflicts(sys.get()).size() - + check_bsdl_completeness(sys.get()).size()); + // Model-driven checks (BSDL pin specs, JTAG chain, source conflicts, + // completeness) — from the shared verify report. + int n_model = vr.model_total(); health_rows.push_back(health_line(n_model == 0, "model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)")); diff --git a/tests/test_verify.cpp b/tests/test_verify.cpp index 713574a..9edc309 100644 --- a/tests/test_verify.cpp +++ b/tests/test_verify.cpp @@ -77,4 +77,19 @@ TEST_CASE("verify counts orphan pins by their import origin") { CHECK(r.orphan_imported == 1); CHECK(r.orphan_dropped == 1); CHECK(r.orphan_total() == 2); + + // Per-pin detail carries the path and origin (the dashboard lists the + // dropped ones under the NC health row). + REQUIRE(r.orphans.size() == 2); + int n_dropped = 0; + bool dropped_path_ok = false; + for (const auto &o : r.orphans) { + if (o.dropped) { + ++n_dropped; + if (o.module == "M" && o.part == "J1" && o.pin == "2") + dropped_path_ok = true; + } + } + CHECK(n_dropped == 1); + CHECK(dropped_path_ok); } From a040cc1957c1a93df53f7b1da2c25a0350e104e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 20:12:11 +0200 Subject: [PATCH 06/11] Extract connect into core (app::connect_parts); thin the command + screen. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the wiring orchestration — transform lookup, identity-compatibility check, identity NC fill, transform apply, Connection creation/add — out of the `connect` command and the interactive connect screen into core/app/connect.{hpp,cpp}: app::connect_parts(System*, m1,p1, m2,p2) returns a ConnectResult (ok/refused/error, connection_name, transform_name, wires, identity_info, nc_added) with no Print/dialog/FTXUI. Name/pattern resolution and ambiguity reporting stay in the command — that is arg-parsing, not the op. Both frontends now call the one core op, removing the duplicated logic. This also unifies a divergence: the interactive screen previously called CheckIdentityCompatible without the info pointer and so never filled identity NC pins (unlike the command); routing it through app::connect_parts makes the screen fill NCs and surface the same warning, matching the scriptable path. Command output is unchanged. Prune the now-dead transform.hpp / domain connect.hpp includes from the frontends (commands.cpp keeps transform_vpx.hpp only for ValidatePartForKind). Add tests/test_connect.cpp (core, no UI): identity-compatible pair wires, unknown type pairing is refused with nothing created, subset side gets NC pins filled and the warning reported. Co-Authored-By: Claude Opus 4.8 --- src/core/app/connect.cpp | 67 +++++++++++++++++++++++ src/core/app/connect.hpp | 42 +++++++++++++++ src/frontends/tui/commands.cpp | 60 +++++++-------------- src/frontends/tui/screen_connect.cpp | 48 ++++++----------- tests/test_connect.cpp | 79 ++++++++++++++++++++++++++++ 5 files changed, 223 insertions(+), 73 deletions(-) create mode 100644 src/core/app/connect.cpp create mode 100644 src/core/app/connect.hpp create mode 100644 tests/test_connect.cpp diff --git a/src/core/app/connect.cpp b/src/core/app/connect.cpp new file mode 100644 index 0000000..b3484f1 --- /dev/null +++ b/src/core/app/connect.cpp @@ -0,0 +1,67 @@ +#include "core/app/connect.hpp" + +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/system.hpp" +#include "core/domain/transform.hpp" + +#include +#include + +namespace app { + +ConnectResult connect_parts(System *sys, Module *m1, Part *p1, + Module *m2, Part *p2) +{ + ConnectResult r; + + 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) { + r.refused = true; + r.error = "no transform for types '" + + (p1->connector_type.empty() ? std::string("(none)") + : p1->connector_type) + + "' ↔ '" + + (p2->connector_type.empty() ? std::string("(none)") + : p2->connector_type) + + "'. Set matching types via 'set-connector-type' first."; + return r; + } + std::string info; + std::string err = CheckIdentityCompatible(p1, p2, &info); + if (!err.empty()) { + r.refused = true; + r.error = err; + return r; + } + if (!info.empty()) { + r.identity_info = info; + r.nc_added = FillIdentityNCs(p1, p2); + } + } + + auto pin_map = t->apply(p1, p2); + + r.connection_name = m1->name + "/" + p1->name + + " <-> " + m2->name + "/" + p2->name; + r.transform_name = t->name; + try { + Connection *c = new Connection(r.connection_name, m1, p1, m2, p2); + c->transform_name = t->name; + c->pin_map = std::move(pin_map); + sys->connections()->add(c); + r.wires = (int)c->pin_map.size(); + r.ok = true; + } catch (const std::exception &e) { + r.error = e.what(); + } + + return r; +} + +} // namespace app diff --git a/src/core/app/connect.hpp b/src/core/app/connect.hpp new file mode 100644 index 0000000..4522486 --- /dev/null +++ b/src/core/app/connect.hpp @@ -0,0 +1,42 @@ +#ifndef _APP_CONNECT_HPP_ +#define _APP_CONNECT_HPP_ + +#include + +class System; +class Module; +class Part; + +// Application layer: UI-independent operations that any frontend (TUI, GUI, …) +// can call. No console, no dialogs, no FTXUI — just System in, result out. +namespace app { + +// Outcome of connecting two parts. The side effects (filling identity NC pins, +// creating the Connection and adding it to the system) all happen in core; the +// caller only renders the fields. +struct ConnectResult { + bool ok = false; ///< a Connection was created and added + bool refused = false; ///< a business rule rejected it (vs. an exception) + std::string error; ///< why refused/failed; empty when ok + + std::string connection_name; + std::string transform_name; + int wires = 0; ///< pin_map size of the created connection + + // Identity-transform path only: the compatibility info line and how many NC + // pins were materialised so both sides match. Empty / 0 otherwise. + std::string identity_info; + int nc_added = 0; +}; + +// Wire part `p1` (in module `m1`) to part `p2` (in module `m2`): look up the +// transform for their connector types, refuse on an unknown pairing or an +// identity-incompatible layout, fill identity NC pins when needed, apply the +// transform and create the Connection. Pure core — no resolution of names or +// patterns (the frontend turns user input into the Module*/Part* it passes). +ConnectResult connect_parts(System *sys, Module *m1, Part *p1, + Module *m2, Part *p2); + +} // namespace app + +#endif // _APP_CONNECT_HPP_ diff --git a/src/frontends/tui/commands.cpp b/src/frontends/tui/commands.cpp index 18e2996..d80c7c7 100644 --- a/src/frontends/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -13,9 +13,9 @@ #include "core/domain/signals.hpp" #include "core/domain/system.hpp" +#include "core/app/connect.hpp" #include "core/app/verify.hpp" -#include "core/domain/transform.hpp" -#include "core/domain/transform_vpx.hpp" +#include "core/domain/transform_vpx.hpp" // ValidatePartForKind #include #include @@ -560,47 +560,23 @@ void Tui::RegisterCommands() { auto [p2, p2_alts] = resolve_part(m2, args[3]); if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; } - auto ® = TransformRegistry::get(); - Transform *t = reg.lookup(p1->connector_type, p2->connector_type); - bool both_empty = p1->connector_type.empty() && p2->connector_type.empty(); - if (t == reg.identity()) { - if (!both_empty) { - Print("connect refused: no transform for types '" - + (p1->connector_type.empty() ? "(none)" : p1->connector_type) - + "' ↔ '" - + (p2->connector_type.empty() ? "(none)" : p2->connector_type) - + "'. Set matching types via 'set-connector-type' first."); - return; - } - std::string info; - std::string err = CheckIdentityCompatible(p1, p2, &info); - if (!err.empty()) { - Print("connect refused: " + err); - return; - } - if (!info.empty()) { - int added = FillIdentityNCs(p1, p2); - Print("connect: " + info); - if (added > 0) - Print("connect: added " + std::to_string(added) - + " NC pin(s) so both sides match"); - } - } - auto pin_map = t->apply(p1, p2); - - std::string conn_name = m1->name + "/" + p1->name - + " <-> " + m2->name + "/" + p2->name; - try { - Connection *c = new Connection(conn_name, m1, p1, m2, p2); - c->transform_name = t->name; - c->pin_map = std::move(pin_map); - sys->connections()->add(c); - Print("connected: " + conn_name - + " via " + t->name - + " (" + std::to_string(c->pin_map.size()) + " wires)"); - } catch (const std::exception &e) { - Print(std::string("connect failed: ") + e.what()); + // Resolution above is arg-parsing (user text → objects); the wiring + // itself — transform lookup, identity NC fill, Connection creation — + // is app::connect_parts. + app::ConnectResult cr = app::connect_parts(sys.get(), m1, p1, m2, p2); + if (cr.refused) { Print("connect refused: " + cr.error); return; } + if (!cr.identity_info.empty()) { + Print("connect: " + cr.identity_info); + if (cr.nc_added > 0) + Print("connect: added " + std::to_string(cr.nc_added) + + " NC pin(s) so both sides match"); } + if (cr.ok) + Print("connected: " + cr.connection_name + + " via " + cr.transform_name + + " (" + std::to_string(cr.wires) + " wires)"); + else + Print(std::string("connect failed: ") + cr.error); }, /*prompt_for_missing=*/ false, "connect a part across two modules (interactive screen if no args)", diff --git a/src/frontends/tui/screen_connect.cpp b/src/frontends/tui/screen_connect.cpp index bb01b4a..80dbaa7 100644 --- a/src/frontends/tui/screen_connect.cpp +++ b/src/frontends/tui/screen_connect.cpp @@ -1,11 +1,10 @@ #include "frontends/tui/tui.hpp" #include "frontends/tui/tui_helpers.hpp" -#include "core/domain/connect.hpp" +#include "core/app/connect.hpp" #include "core/domain/modules.hpp" #include "core/domain/parts.hpp" #include "core/domain/system.hpp" -#include "core/domain/transform.hpp" #include #include @@ -67,37 +66,24 @@ Component Tui::BuildConnectScreen() { 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-connector-type' first."); - screen_idx = 0; - return; - } - std::string err = CheckIdentityCompatible(p1, p2); - if (!err.empty()) { - Print("connect refused: " + err); - screen_idx = 0; - return; + // Same wiring op as the `connect` command — see app::connect_parts. + app::ConnectResult cr = app::connect_parts(sys.get(), m1, p1, m2, p2); + if (cr.refused) { + Print("connect refused: " + cr.error); + } else { + if (!cr.identity_info.empty()) { + Print("connect: " + cr.identity_info); + if (cr.nc_added > 0) + Print("connect: added " + std::to_string(cr.nc_added) + + " NC pin(s) so both sides match"); } + if (cr.ok) + Print("connected: " + cr.connection_name + + " via " + cr.transform_name + + " (" + std::to_string(cr.wires) + " wires)"); + else + Print(std::string("connect failed: ") + cr.error); } - 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()); } diff --git a/tests/test_connect.cpp b/tests/test_connect.cpp new file mode 100644 index 0000000..30041d6 --- /dev/null +++ b/tests/test_connect.cpp @@ -0,0 +1,79 @@ +#include + +#include "core/app/connect.hpp" +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" +#include "core/domain/system.hpp" + +// app::connect_parts is pure core: given two already-resolved parts it looks up +// the transform, fills identity NC pins, creates the Connection and returns a +// ConnectResult. No Print/dialog/FTXUI. These tests drive it directly. + +namespace { + +// A part with the given pin names, attached to a fresh module. +Part *make_part(System &sys, const std::string &mod, const std::string &part, + std::initializer_list pins, + const std::string &type = "") +{ + Module *m = sys.modules()->merge(mod); + Part *p = new Part(part); + p->connector_type = type; + m->add(p); + for (const char *pn : pins) p->add(new Pin(pn)); + return p; +} + +} // namespace + +TEST_CASE("connect_parts wires an identity-compatible pair") { + System sys; + Module *a = sys.modules()->merge("A"); + Module *b = sys.modules()->merge("B"); + Part *p1 = make_part(sys, "A", "J1", {"1", "2"}); + Part *p2 = make_part(sys, "B", "P1", {"1", "2"}); + + app::ConnectResult r = app::connect_parts(&sys, a, p1, b, p2); + + CHECK(r.ok); + CHECK_FALSE(r.refused); + CHECK(r.transform_name == "identity"); + CHECK(r.wires == 2); + CHECK(r.identity_info.empty()); // identical sets → no NC fill, no warning + CHECK(r.nc_added == 0); + CHECK(r.connection_name == "A/J1 <-> B/P1"); + CHECK(sys.connections()->size() == 1); +} + +TEST_CASE("connect_parts refuses an unknown connector-type pairing") { + System sys; + Module *a = sys.modules()->merge("A"); + Module *b = sys.modules()->merge("B"); + Part *p1 = make_part(sys, "A", "J1", {"1"}, "foo"); + Part *p2 = make_part(sys, "B", "P1", {"1"}, "bar"); + + app::ConnectResult r = app::connect_parts(&sys, a, p1, b, p2); + + CHECK_FALSE(r.ok); + CHECK(r.refused); + CHECK(r.error.find("no transform") != std::string::npos); + CHECK(sys.connections()->size() == 0); // nothing created +} + +TEST_CASE("connect_parts fills NC pins on the subset side and reports it") { + System sys; + Module *a = sys.modules()->merge("A"); + Module *b = sys.modules()->merge("B"); + Part *p1 = make_part(sys, "A", "J1", {"1", "2", "3"}); // larger side + Part *p2 = make_part(sys, "B", "P1", {"1", "2"}); // missing "3" + + app::ConnectResult r = app::connect_parts(&sys, a, p1, b, p2); + + CHECK(r.ok); + CHECK_FALSE(r.identity_info.empty()); // subset path surfaces a warning + CHECK(r.nc_added == 1); // pin "3" materialised on B + CHECK(r.wires == 3); // all three now wired + CHECK(p2->size() == 3); // the NC pin really got added +} From b36af3167a5cc9f5185ddfe427b9832dd29558da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 20:18:35 +0200 Subject: [PATCH 07/11] Extract load into core (app::load_module); thin the command. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the import orchestration — System::Load + drop_singleton_signals + infer_signal_types + the post-import counts — out of the `load` command into core/app/load.{hpp,cpp}: app::load_module(System*, name, path, ImportType) returns a LoadResult (ok/error, parts, signals, dropped, power/gnd/kept_other) with no Print/dialog/FTXUI. The "mentor|altium|ods" string→enum mapping moves to app::import_type_from_name (mirrors export_format_from_path). The command only parses the type and renders the counts; output is byte-identical. Add tests/test_load.cpp (core, no UI): the name mapping; a minimal Mentor netlist that imports two parts and drops one singleton signal; and a pin test of the pre-existing missing-file behaviour (ImportBase doesn't check is_open(), so a missing file yields an empty module rather than an error — preserved by the extraction and pinned so any future hardening is a deliberate change). Co-Authored-By: Claude Opus 4.8 --- src/core/app/load.cpp | 46 ++++++++++++++++++++++++ src/core/app/load.hpp | 36 +++++++++++++++++++ src/frontends/tui/commands.cpp | 37 +++++++++----------- tests/test_load.cpp | 64 ++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 src/core/app/load.cpp create mode 100644 src/core/app/load.hpp create mode 100644 tests/test_load.cpp diff --git a/src/core/app/load.cpp b/src/core/app/load.cpp new file mode 100644 index 0000000..3a4f94e --- /dev/null +++ b/src/core/app/load.cpp @@ -0,0 +1,46 @@ +#include "core/app/load.hpp" + +#include "core/domain/analysis.hpp" // infer_signal_types, SignalTypeInferenceStats +#include "core/domain/modules.hpp" +#include "core/domain/signals.hpp" // drop_singleton_signals +#include "core/domain/system.hpp" + +#include +#include + +namespace app { + +bool import_type_from_name(const std::string &name, ImportType &out) +{ + std::string ls; + ls.reserve(name.size()); + for (char c : name) ls += (char)std::tolower((unsigned char)c); + if (ls == "mentor") { out = ImportType::IMPORT_MENTOR; return true; } + if (ls == "altium") { out = ImportType::IMPORT_ALTIUM; return true; } + if (ls == "ods") { out = ImportType::IMPORT_ODS; return true; } + return false; +} + +LoadResult load_module(System *sys, const std::string &module_name, + const std::string &path, ImportType type) +{ + LoadResult r; + if (!sys) { r.error = "no system"; return r; } + try { + sys->Load(module_name, path, type); + Module *mod = sys->modules()->get(module_name); + r.dropped = drop_singleton_signals(mod->signals); + SignalTypeInferenceStats inf = infer_signal_types(sys); + r.parts = (int)mod->size(); + r.signals = (int)mod->signals->size(); + r.power = inf.power; + r.gnd = inf.gnd; + r.kept_other = inf.kept_other; + r.ok = true; + } catch (const std::exception &e) { + r.error = e.what(); + } + return r; +} + +} // namespace app diff --git a/src/core/app/load.hpp b/src/core/app/load.hpp new file mode 100644 index 0000000..93308c3 --- /dev/null +++ b/src/core/app/load.hpp @@ -0,0 +1,36 @@ +#ifndef _APP_LOAD_HPP_ +#define _APP_LOAD_HPP_ + +#include "core/domain/system.hpp" // ImportType + +#include + +// Application layer: UI-independent operations that any frontend (TUI, GUI, …) +// can call. No console, no dialogs, no FTXUI — just System in, result out. +namespace app { + +// Map an import-type name (mentor / altium / ods, case-insensitive) to an +// ImportType. Returns false if the name is none of those. +bool import_type_from_name(const std::string &name, ImportType &out); + +// Outcome of loading a module: the post-import counts the caller renders. +struct LoadResult { + bool ok = false; + std::string error; ///< human-readable, set when !ok + int parts = 0; + int signals = 0; + int dropped = 0; ///< singleton/NC signals removed after import + int power = 0; ///< signals inferred Power (name + structure) + int gnd = 0; ///< signals inferred GndShield (name) + int kept_other = 0; ///< name said Power but evidence too weak → kept Other +}; + +// Import a module from a netlist/pinout file into `sys`, drop singleton signals, +// then infer signal types. Returns the counts or an error. Pure core — safe to +// call from any frontend. +LoadResult load_module(System *sys, const std::string &module_name, + const std::string &path, ImportType type); + +} // namespace app + +#endif // _APP_LOAD_HPP_ diff --git a/src/frontends/tui/commands.cpp b/src/frontends/tui/commands.cpp index d80c7c7..4dcd472 100644 --- a/src/frontends/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -14,6 +14,7 @@ #include "core/domain/system.hpp" #include "core/app/connect.hpp" +#include "core/app/load.hpp" #include "core/app/verify.hpp" #include "core/domain/transform_vpx.hpp" // ValidatePartForKind @@ -137,29 +138,23 @@ void Tui::RegisterCommands() { {"import type [mentor|altium|ods]", Completion::None}}, [this](const std::vector &args) { if (!sys) { Print("no system: run 'new' first."); return; } - std::string ls = ToLower(args[2]); ImportType t; - if (ls == "mentor") t = ImportType::IMPORT_MENTOR; - else if (ls == "altium") t = ImportType::IMPORT_ALTIUM; - else if (ls == "ods") t = ImportType::IMPORT_ODS; - else { Print("unknown import type: " + args[2]); return; } - try { - sys->Load(args[0], args[1], t); - Module *mod = sys->modules()->get(args[0]); - int dropped = drop_singleton_signals(mod->signals); - auto inf = infer_signal_types(sys.get()); - Print("loaded '" + args[0] + "' from " + args[1]); - Print(" parts: " + std::to_string(mod->size())); - Print(" signals: " + std::to_string(mod->signals->size()) - + (dropped ? " (dropped " + std::to_string(dropped) - + " singleton/NC signal(s))" : "")); - Print(" types: " + std::to_string(inf.power) + " power, " - + std::to_string(inf.gnd) + " gnd, " - + std::to_string(inf.kept_other) - + " suspect Power (name only — kept as Other)"); - } catch (const std::exception &e) { - Print(std::string("load failed: ") + e.what()); + if (!app::import_type_from_name(args[2], t)) { + Print("unknown import type: " + args[2]); return; } + // Import + drop-singletons + infer-types is one core op; the command + // only parses the type and renders the counts. + app::LoadResult r = app::load_module(sys.get(), args[0], args[1], t); + if (!r.ok) { Print(std::string("load failed: ") + r.error); return; } + Print("loaded '" + args[0] + "' from " + args[1]); + Print(" parts: " + std::to_string(r.parts)); + Print(" signals: " + std::to_string(r.signals) + + (r.dropped ? " (dropped " + std::to_string(r.dropped) + + " singleton/NC signal(s))" : "")); + Print(" types: " + std::to_string(r.power) + " power, " + + std::to_string(r.gnd) + " gnd, " + + std::to_string(r.kept_other) + + " suspect Power (name only — kept as Other)"); }, /*prompt_for_missing=*/ true, "load a module from a netlist / pinout file (mentor, altium, ods)", diff --git a/tests/test_load.cpp b/tests/test_load.cpp new file mode 100644 index 0000000..db70413 --- /dev/null +++ b/tests/test_load.cpp @@ -0,0 +1,64 @@ +#include + +#include "core/app/load.hpp" +#include "core/domain/system.hpp" + +#include +#include +#include + +// app::load_module is pure core: import a module, drop singleton signals, infer +// signal types, return counts or an error — no Print/dialog/FTXUI. The parse +// helper import_type_from_name is likewise UI-free. + +TEST_CASE("import_type_from_name maps names case-insensitively") { + ImportType t; + CHECK(app::import_type_from_name("mentor", t)); + CHECK(t == ImportType::IMPORT_MENTOR); + CHECK(app::import_type_from_name("ALTIUM", t)); + CHECK(t == ImportType::IMPORT_ALTIUM); + CHECK(app::import_type_from_name("Ods", t)); + CHECK(t == ImportType::IMPORT_ODS); + CHECK_FALSE(app::import_type_from_name("kicad", t)); + CHECK_FALSE(app::import_type_from_name("", t)); +} + +TEST_CASE("load_module imports, drops singletons and reports counts") { + // Minimal Mentor netlist: two parts; NETA/NETB span both parts (2 pins + // each, kept), LONELY sits on one pin only (dropped as a singleton). + const char *path = "test_load_in.net"; + { + std::ofstream f(path); + f << "COMP: 'C1' 'J1'\n" + " Explicit Pin: '1' 'x' 'NETA'\n" + " Explicit Pin: '2' 'x' 'NETB'\n" + " Explicit Pin: '3' 'x' 'LONELY'\n" + "COMP: 'C2' 'J2'\n" + " Explicit Pin: '1' 'x' 'NETA'\n" + " Explicit Pin: '2' 'x' 'NETB'\n"; + } + + System sys; + app::LoadResult r = app::load_module(&sys, "M", path, ImportType::IMPORT_MENTOR); + + CHECK(r.ok); + CHECK(r.error.empty()); + CHECK(r.parts == 2); + CHECK(r.signals == 2); // NETA, NETB — LONELY dropped + CHECK(r.dropped == 1); // LONELY + + std::remove(path); +} + +TEST_CASE("load_module on a missing file currently succeeds empty (no throw)") { + // Pre-existing behaviour, preserved by the extraction: ImportBase opens the + // stream without checking is_open(), so a missing file yields an empty + // module rather than an error. Pinned here so any future hardening of this + // path is a deliberate, visible change. + System sys; + app::LoadResult r = app::load_module( + &sys, "M", "/nonexistent-dir-xyz/nope.net", ImportType::IMPORT_MENTOR); + CHECK(r.ok); + CHECK(r.parts == 0); + CHECK(r.signals == 0); +} From 4ef110ab70a7f5ab77d60bf93925edde41d75485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 20:24:11 +0200 Subject: [PATCH 08/11] Harden ImportBase: open read-only and fail fast on an unreadable file. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ImportBase opened the input with a default std::fstream (in|out), which had two consequences: a missing file silently produced an empty module (no error), and a present-but-read-only file failed to open and also loaded as empty. Open the stream read-only (std::ios::in) instead, and expose is_open(). System::Load now builds the importer first, checks is_open(), and throws "cannot open file: " before creating the module — so a failed load surfaces as `load failed: …` and leaves no empty module behind. A read-only but present file now loads correctly. Flip the test that pinned the old silent-empty behaviour to assert the clean failure (error + no module created). Co-Authored-By: Claude Opus 4.8 --- src/core/domain/system.cpp | 22 +++++++++++++--------- src/core/imports/import_base.hpp | 13 ++++++++++++- tests/test_load.cpp | 15 +++++++-------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/core/domain/system.cpp b/src/core/domain/system.cpp index 6f29f97..bb95e16 100644 --- a/src/core/domain/system.cpp +++ b/src/core/domain/system.cpp @@ -21,14 +21,8 @@ System::~System() void System::Load(std::string module_name, std::string file_name, ImportType type) { + // Build the importer first, based on the import type. ImportBase *imp; - Module *mod = nullptr; - Parts *prts = nullptr; - - // Creation or retrieval of the module. - mod = mods->merge(module_name); - - // Parsing of the file based on the import type. if (type == ImportType::IMPORT_MENTOR) { imp = new ImportMentor(file_name); @@ -43,7 +37,17 @@ void System::Load(std::string module_name, std::string file_name, ImportType typ { throw std::runtime_error("Unknown import type"); } + + // Fail fast on a missing/unreadable file, before touching the module table, + // so a failed load never leaves behind an empty module. + if (!imp->is_open()) + { + delete imp; + throw std::runtime_error("cannot open file: " + file_name); + } + + // Creation or retrieval of the module, then parse into it. + Module *mod = mods->merge(module_name); imp->parse(mod->signals); - prts = imp->parts(); - mod->add(prts); + mod->add(imp->parts()); } \ No newline at end of file diff --git a/src/core/imports/import_base.hpp b/src/core/imports/import_base.hpp index b63a947..04a1c28 100644 --- a/src/core/imports/import_base.hpp +++ b/src/core/imports/import_base.hpp @@ -27,11 +27,22 @@ public: * * @param file_name Name of the file to be imported. */ - ImportBase(std::string file_name) : file_lines(std::fstream(file_name)) + ImportBase(std::string file_name) : file_lines(file_name, std::ios::in) { prts = new Parts(); }; + /** + * @brief Whether the input file was opened successfully. + * + * Opened read-only, so this is false only when the file is genuinely + * missing or unreadable (a read-only but present file still opens). + * System::Load checks it to fail fast instead of producing an empty module. + * + * @return true if the file stream is open. + */ + bool is_open() const { return file_lines.is_open(); } + /** * @brief Pure virtual method for parsing the file. * diff --git a/tests/test_load.cpp b/tests/test_load.cpp index db70413..38787dd 100644 --- a/tests/test_load.cpp +++ b/tests/test_load.cpp @@ -1,6 +1,7 @@ #include #include "core/app/load.hpp" +#include "core/domain/modules.hpp" #include "core/domain/system.hpp" #include @@ -50,15 +51,13 @@ TEST_CASE("load_module imports, drops singletons and reports counts") { std::remove(path); } -TEST_CASE("load_module on a missing file currently succeeds empty (no throw)") { - // Pre-existing behaviour, preserved by the extraction: ImportBase opens the - // stream without checking is_open(), so a missing file yields an empty - // module rather than an error. Pinned here so any future hardening of this - // path is a deliberate, visible change. +TEST_CASE("load_module fails cleanly on a missing file") { + // ImportBase opens read-only and System::Load checks is_open(), so a missing + // file is a clean error — and no empty module is left in the system. System sys; app::LoadResult r = app::load_module( &sys, "M", "/nonexistent-dir-xyz/nope.net", ImportType::IMPORT_MENTOR); - CHECK(r.ok); - CHECK(r.parts == 0); - CHECK(r.signals == 0); + CHECK_FALSE(r.ok); + CHECK(r.error.find("cannot open") != std::string::npos); + CHECK(sys.modules()->size() == 0); } From 0517a82a5c4300023fb243375195ec5cac4eb8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 20:29:43 +0200 Subject: [PATCH 09/11] Fix importer leak: own Parts in ~ImportBase and delete the importer. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit System::Load never deleted the ImportBase it allocated, and ~ImportBase was defaulted so the Parts container it held leaked too. Give ~ImportBase a body that deletes prts (the container only — the Part objects have been transferred to the Module via add(), which owns them; the default ~Parts frees the map without touching the elements, so no double free), and delete the importer at the end of System::Load (and on the early unreadable-file error path). Drop ImportMentor's explicit destructor, which called ImportBase::~ImportBase() in its body — harmless when the base dtor was empty, but a double delete of prts now that it frees memory. The implicit destructor calls the base once. Verified with valgrind on a batch load (mentor + a missing-file error path): "All heap blocks were freed — no leaks are possible", 0 errors. Co-Authored-By: Claude Opus 4.8 --- src/core/domain/system.cpp | 5 ++++- src/core/imports/import_base.hpp | 8 ++++++-- src/core/imports/import_mentor.cpp | 10 ---------- src/core/imports/import_mentor.hpp | 1 - 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/core/domain/system.cpp b/src/core/domain/system.cpp index bb95e16..3487d33 100644 --- a/src/core/domain/system.cpp +++ b/src/core/domain/system.cpp @@ -46,8 +46,11 @@ void System::Load(std::string module_name, std::string file_name, ImportType typ throw std::runtime_error("cannot open file: " + file_name); } - // Creation or retrieval of the module, then parse into it. + // Creation or retrieval of the module, then parse into it. add() copies the + // Part pointers into the module, which takes ownership; deleting the + // importer then frees the (now drained) Parts container, not the parts. Module *mod = mods->merge(module_name); imp->parse(mod->signals); mod->add(imp->parts()); + delete imp; } \ No newline at end of file diff --git a/src/core/imports/import_base.hpp b/src/core/imports/import_base.hpp index 04a1c28..205feb9 100644 --- a/src/core/imports/import_base.hpp +++ b/src/core/imports/import_base.hpp @@ -64,9 +64,13 @@ public: /** * @brief Virtual destructor for ImportBase. * - * Ensures proper cleanup of derived classes. + * Frees the Parts container object. Only the container is deleted, not the + * Part objects it holds: by the time the importer is destroyed those have + * been transferred to a Module (SystemElementContainer::add copies the + * pointers), which owns and deletes them. The default ~Parts frees the map + * without touching the elements, so there is no double free. */ - virtual ~ImportBase() = default; + virtual ~ImportBase() { delete prts; } }; #endif // _IMPORT_BASE_HPP_ \ No newline at end of file diff --git a/src/core/imports/import_mentor.cpp b/src/core/imports/import_mentor.cpp index 5795c4c..2bce6e4 100644 --- a/src/core/imports/import_mentor.cpp +++ b/src/core/imports/import_mentor.cpp @@ -43,16 +43,6 @@ enum class State */ ImportMentor::ImportMentor(string filename) : ImportBase(filename) {} -/** - * @brief Destructor for ImportMentor. - * - * Ensures proper cleanup by calling the base class destructor. - */ -ImportMentor::~ImportMentor() -{ - ImportBase::~ImportBase(); -} - /** * @brief Parses the file to extract parts, pins, and signals. * diff --git a/src/core/imports/import_mentor.hpp b/src/core/imports/import_mentor.hpp index 0b0f143..00029f5 100644 --- a/src/core/imports/import_mentor.hpp +++ b/src/core/imports/import_mentor.hpp @@ -10,7 +10,6 @@ class ImportMentor : public ImportBase public: ImportMentor(std::string filename); void parse(Signals *signals) override; - ~ImportMentor(); }; #endif // _IMPORT_MENTOR_HPP_ \ No newline at end of file From af36f7c15017a6b562985bb3fcc43562ae64d5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 20:34:29 +0200 Subject: [PATCH 10/11] Add a Frontend interface; make the process entry frontend-agnostic. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main.cpp was entirely TUI-specific (constructed Tui, parsed argv, drove BootDispatch/DumpOutput/Run directly). Introduce a shared frontends layer so a second frontend can reuse the whole launch flow: - src/frontends/frontend.hpp — abstract Frontend interface (BootDispatch, DumpCommandsMd, DumpOutput, Run), header-only, no GUI toolkit, no core dep. - src/frontends/frontend_main.{hpp,cpp} — frontend_main(argc, argv, Frontend&): all the argv parsing (--source/--restore/--batch/--commands-md/--help) and the boot → batch/run flow, driving any frontend through the interface. - Tui now implements Frontend (the four methods already matched; just marked override). - The TUI main.cpp shrinks to: construct Tui, call frontend_main. A second frontend's main() is identical with its own Frontend type. Build: a small GUI-toolkit-free static lib essim_frontend (frontend_main.cpp) is added at the top level when a frontend is selected, and the essim exe links it. ESSIM_FRONTEND=none still builds core+tests only (no essim_frontend, no FTXUI). Binary stays ./build/essim. Behaviour unchanged across --batch/--commands-md/--help/exit codes; only the usage text is genericised ("the TUI" → "the interface", "console screen" → "console") now that the launcher is shared. Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 6 ++ src/frontends/frontend.hpp | 30 ++++++++++ src/frontends/frontend_main.cpp | 99 ++++++++++++++++++++++++++++++++ src/frontends/frontend_main.hpp | 13 +++++ src/frontends/tui/CMakeLists.txt | 2 +- src/frontends/tui/main.cpp | 99 ++------------------------------ src/frontends/tui/tui.hpp | 12 ++-- 7 files changed, 162 insertions(+), 99 deletions(-) create mode 100644 src/frontends/frontend.hpp create mode 100644 src/frontends/frontend_main.cpp create mode 100644 src/frontends/frontend_main.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bbdc32b..278da8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,12 @@ if(ESSIM_FRONTEND STREQUAL "none") message(STATUS "essim: ESSIM_FRONTEND=none — core + tests only (no frontend, no GUI toolkit)") elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt") message(STATUS "essim: building frontend '${ESSIM_FRONTEND}'") + # Shared, GUI-toolkit-free frontend support: the abstract Frontend interface + # (header-only) and the frontend-agnostic launcher frontend_main(). Every + # frontend's main() links this and forwards argv to it. + add_library(essim_frontend STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/frontend_main.cpp") + target_include_directories(essim_frontend PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) add_subdirectory(src/frontends/${ESSIM_FRONTEND}) else() message(FATAL_ERROR diff --git a/src/frontends/frontend.hpp b/src/frontends/frontend.hpp new file mode 100644 index 0000000..6c32f0f --- /dev/null +++ b/src/frontends/frontend.hpp @@ -0,0 +1,30 @@ +#ifndef _FRONTEND_HPP_ +#define _FRONTEND_HPP_ + +#include +#include + +// Abstract entry-point interface every frontend (TUI, GUI, …) implements, so +// one shared launcher (frontend_main) can drive any of them: parse argv, run +// boot commands, optionally dump output (batch / docs) and enter the event +// loop. Lives in the frontends layer — essim_core never depends on it. +class Frontend { +public: + virtual ~Frontend() = default; + + // Dispatch one command synchronously, exactly as if the user typed it + // (e.g. "restore foo.essim" or "source bring-up.essim"), before the event + // loop starts — used to seed the system at boot. + virtual void BootDispatch(const std::string &raw) = 0; + + // Write the command registry as Markdown (used for doc generation). + virtual void DumpCommandsMd(std::ostream &out) const = 0; + + // Write the accumulated console output (batch mode: no event loop). + virtual void DumpOutput(std::ostream &out) const = 0; + + // Enter the interactive event loop. + virtual void Run() = 0; +}; + +#endif // _FRONTEND_HPP_ diff --git a/src/frontends/frontend_main.cpp b/src/frontends/frontend_main.cpp new file mode 100644 index 0000000..dbd99d4 --- /dev/null +++ b/src/frontends/frontend_main.cpp @@ -0,0 +1,99 @@ +#include "frontends/frontend_main.hpp" + +#include "frontends/frontend.hpp" + +#include +#include +#include + +namespace { + +void print_usage(const char *prog) { + std::cerr << + "usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n" + " " << prog << " --commands-md [FILE]\n" + " " << prog << " --help\n" + " (no args) launch the interface on an empty system.\n" + " --source FILE after boot, run FILE as an essim script\n" + " (one command per line; same as the `source`\n" + " command). Output goes to the console.\n" + " --restore FILE after boot, restore the system snapshot in\n" + " FILE (same as the `restore` command).\n" + " Combine with --source to layer a script on\n" + " top of a restored snapshot.\n" + " --batch run --restore/--source, print the console\n" + " output to stdout, and exit without launching\n" + " the interface.\n" + " --commands-md [FILE] dump the command registry as Markdown.\n" + " With FILE: write there. Without: stdout.\n" + " (Used by `cmake --build build --target doc`.)\n" + " --help, -h show this help.\n"; +} + +} // namespace + +int frontend_main(int argc, char **argv, Frontend &fe) { + std::string boot_restore; + std::string boot_source; + bool batch = false; + + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "--commands-md") { + if (i + 1 < argc) { + std::ofstream f(argv[++i]); + if (!f) { + std::cerr << "essim: cannot open " << argv[i] << " for writing\n"; + return 1; + } + fe.DumpCommandsMd(f); + } else { + fe.DumpCommandsMd(std::cout); + } + return 0; + } + if (a == "--source") { + if (i + 1 >= argc) { + std::cerr << "essim: --source needs a filename\n"; + return 2; + } + boot_source = argv[++i]; + continue; + } + if (a == "--restore") { + if (i + 1 >= argc) { + std::cerr << "essim: --restore needs a filename\n"; + return 2; + } + boot_restore = argv[++i]; + continue; + } + if (a == "--batch") { + batch = true; + continue; + } + if (a == "--help" || a == "-h") { + print_usage(argv[0]); + return 0; + } + std::cerr << "essim: unknown option: " << a << "\n"; + print_usage(argv[0]); + return 2; + } + + // Order matters: a `--restore` brings up a snapshot, then `--source` + // can layer additional commands on top of it (useful e.g. for "load + // snapshot, then re-run a small script that adds a new card"). + if (!boot_restore.empty()) fe.BootDispatch("restore " + boot_restore); + if (!boot_source.empty()) fe.BootDispatch("source " + boot_source); + + // Batch mode: the boot dispatch already ran synchronously (no event loop + // yet), so the console output is complete. Print it and exit. + if (batch) { + fe.DumpOutput(std::cout); + return 0; + } + + fe.Run(); + return 0; +} diff --git a/src/frontends/frontend_main.hpp b/src/frontends/frontend_main.hpp new file mode 100644 index 0000000..24a80ee --- /dev/null +++ b/src/frontends/frontend_main.hpp @@ -0,0 +1,13 @@ +#ifndef _FRONTEND_MAIN_HPP_ +#define _FRONTEND_MAIN_HPP_ + +class Frontend; + +// Shared process entry point, frontend-agnostic. Parses argv +// (--source / --restore / --batch / --commands-md / --help), drives `fe` +// through the boot commands and then either dumps output (batch) or enters its +// event loop, and returns the process exit code. Each frontend's main() just +// constructs its concrete Frontend and forwards to this. +int frontend_main(int argc, char **argv, Frontend &fe); + +#endif // _FRONTEND_MAIN_HPP_ diff --git a/src/frontends/tui/CMakeLists.txt b/src/frontends/tui/CMakeLists.txt index 684e03f..9f31b6d 100644 --- a/src/frontends/tui/CMakeLists.txt +++ b/src/frontends/tui/CMakeLists.txt @@ -32,7 +32,7 @@ target_link_libraries(essim_tui ) add_executable(essim "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") -target_link_libraries(essim PRIVATE essim_tui) +target_link_libraries(essim PRIVATE essim_tui essim_frontend) # Keep the binary at the top of the build tree (./build/essim), regardless of # which frontend subdir produced it. diff --git a/src/frontends/tui/main.cpp b/src/frontends/tui/main.cpp index d3011f2..f23144d 100644 --- a/src/frontends/tui/main.cpp +++ b/src/frontends/tui/main.cpp @@ -1,98 +1,11 @@ +#include "frontends/frontend_main.hpp" #include "frontends/tui/tui.hpp" -#include -#include -#include - -namespace { - -void print_usage(const char *prog) { - std::cerr << - "usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n" - " " << prog << " --commands-md [FILE]\n" - " " << prog << " --help\n" - " (no args) launch the TUI on an empty system.\n" - " --source FILE after boot, run FILE as an essim script\n" - " (one command per line; same as the `source`\n" - " command). Output is in the console screen.\n" - " --restore FILE after boot, restore the system snapshot in\n" - " FILE (same as the `restore` command).\n" - " Combine with --source to layer a script on\n" - " top of a restored snapshot.\n" - " --batch run --restore/--source, print the console\n" - " output to stdout, and exit without the TUI.\n" - " --commands-md [FILE] dump the command registry as Markdown.\n" - " With FILE: write there. Without: stdout.\n" - " (Used by `cmake --build build --target doc`.)\n" - " --help, -h show this help.\n"; -} - -} // namespace - +// The TUI frontend's entry point: construct the concrete Frontend (Tui) and +// hand off to the shared, frontend-agnostic launcher. All argv parsing and the +// boot/batch/run flow live in frontend_main(); a second frontend's main() looks +// exactly like this with its own Frontend type. int main(int argc, char **argv) { - std::string boot_restore; - std::string boot_source; - bool batch = false; - - for (int i = 1; i < argc; ++i) { - std::string a = argv[i]; - if (a == "--commands-md") { - Tui tui; - if (i + 1 < argc) { - std::ofstream f(argv[++i]); - if (!f) { - std::cerr << "essim: cannot open " << argv[i] << " for writing\n"; - return 1; - } - tui.DumpCommandsMd(f); - } else { - tui.DumpCommandsMd(std::cout); - } - return 0; - } - if (a == "--source") { - if (i + 1 >= argc) { - std::cerr << "essim: --source needs a filename\n"; - return 2; - } - boot_source = argv[++i]; - continue; - } - if (a == "--restore") { - if (i + 1 >= argc) { - std::cerr << "essim: --restore needs a filename\n"; - return 2; - } - boot_restore = argv[++i]; - continue; - } - if (a == "--batch") { - batch = true; - continue; - } - if (a == "--help" || a == "-h") { - print_usage(argv[0]); - return 0; - } - std::cerr << "essim: unknown option: " << a << "\n"; - print_usage(argv[0]); - return 2; - } - Tui tui; - // Order matters: a `--restore` brings up a snapshot, then `--source` - // can layer additional commands on top of it (useful e.g. for "load - // snapshot, then re-run a small script that adds a new card"). - if (!boot_restore.empty()) tui.BootDispatch("restore " + boot_restore); - if (!boot_source.empty()) tui.BootDispatch("source " + boot_source); - - // Batch mode: the boot dispatch already ran synchronously (no screen yet), - // so the console output is complete. Print it and exit without the TUI. - if (batch) { - tui.DumpOutput(std::cout); - return 0; - } - - tui.Run(); - return 0; + return frontend_main(argc, argv, tui); } diff --git a/src/frontends/tui/tui.hpp b/src/frontends/tui/tui.hpp index 75e5ac0..8744b4a 100644 --- a/src/frontends/tui/tui.hpp +++ b/src/frontends/tui/tui.hpp @@ -13,9 +13,11 @@ #include #include +#include "frontends/frontend.hpp" + class System; -class Tui { +class Tui : public Frontend { enum class Completion { None, Path, Command }; struct Prompt { @@ -198,16 +200,16 @@ private: public: Tui(); ~Tui(); - void Run(); - void DumpCommandsMd(std::ostream &out) const; + void Run() override; + void DumpCommandsMd(std::ostream &out) const override; // Write the accumulated console output to `out`. Used by batch mode to // surface a script's output without starting the TUI. - void DumpOutput(std::ostream &out) const; + void DumpOutput(std::ostream &out) const override; // Boot-time hook: dispatch a single command exactly as if the user // typed it (e.g. `restore foo.essim` or `source bring-up.essim`). // Call before `Run()` to seed the system before the event loop starts. - void BootDispatch(const std::string &raw); + void BootDispatch(const std::string &raw) override; private: // Lifecycle (commands.cpp) From 3b6e626c8fc12727c7c6e3bfc825ceadc417f39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 20:35:24 +0200 Subject: [PATCH 11/11] =?UTF-8?q?docs:=20DESIGN=20=E2=80=94=20Frontend=20i?= =?UTF-8?q?nterface=20+=20frontend=5Fmain,=20app/=20ops,=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reflect the new shared frontends layer (frontend.hpp / frontend_main) in the Architecture section and layout tree, and list the verify/connect/load app ops alongside export. Co-Authored-By: Claude Opus 4.8 --- DESIGN.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index db33e52..afe33b1 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -61,10 +61,15 @@ src/ import_base.hpp / import_{mentor,altium,ods}.{hpp,cpp} / ods_writer.{hpp,cpp} app/ -- application operations (UI-independent use cases) export.{hpp,cpp} export_connections(System*, path, format) -> ExportResult + verify.{hpp,cpp} verify(System*) -> VerifyReport (the 7 verify passes) + connect.{hpp,cpp} connect_parts(System*, m1,p1, m2,p2) -> ConnectResult + load.{hpp,cpp} load_module(System*, name, path, ImportType) -> LoadResult frontends/ -- one directory per GUI/TUI engine; each links essim_core + frontend.hpp -- abstract Frontend interface (BootDispatch/Dump*/Run) + frontend_main.{hpp,cpp} -- frontend_main(argc,argv,Frontend&): argv + boot/batch/run tui/ -- FTXUI shell (builds libessim_tui + the `essim` binary) CMakeLists.txt fetches FTXUI; builds essim_tui + essim - main.cpp entry point (CLI flags -> Tui) + main.cpp entry point: construct Tui, call frontend_main tui.{hpp,cpp} class Tui (state + Run() + screen-mode event dispatch) tui_helpers.{hpp,cpp} ToLower, NaturalLess, RenderHelpPanel shell.cpp Print, Submit, Dispatch, Source / ProcessNextSourceLine @@ -93,8 +98,15 @@ The hard rule: **`src/core/` never depends on a frontend** — no `#include inside its lambda. The TUI command is now a thin wrapper — resolve args/dialog → call the core op → render the result.) - **Frontends** (`frontends//`) — thin: map UI events to core calls and - render results. Add one by creating `src/frontends//CMakeLists.txt` (build - `essim_` linking `essim_core`, produce the `essim` binary) and configuring + render results. Each implements the **`Frontend`** interface + (`frontends/frontend.hpp`: `BootDispatch`, `DumpCommandsMd`, `DumpOutput`, + `Run`). The process entry is shared and frontend-agnostic: + `frontend_main(argc, argv, Frontend&)` (`frontends/frontend_main.cpp`, built + into the toolkit-free `essim_frontend` lib) parses the CLI flags and drives the + boot → batch/run flow through the interface; a frontend's `main()` is just + *construct the concrete Frontend, call `frontend_main`*. Add one by creating + `src/frontends//CMakeLists.txt` (build `essim_` linking `essim_core`, + produce the `essim` binary linking `essim_frontend`) and configuring `-DESSIM_FRONTEND=`. Because the core links no toolkit, the suite links `essim_core` directly and