#include "tui/tui.hpp" #include "tui/tui_helpers.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 { // 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 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. OpenFileDialog( "Export — connections", "export.connections", "connections.csv", {{"CSV", ".csv"}, {"ODS", ".ods"}}, [this](const std::string &path) { Dispatch("export connections " + path); }); return; } if (args.size() != 2) { Print("usage: export (or no args for the dialog)"); return; } 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); return; } ShowError("export: unknown kind '" + kind + "'\n" "Known kinds: connections"); }, /*prompt_for_missing=*/ false, "export structured data to CSV / ODS (kinds: connections; " "bare form opens the file-picker dialog)", /*scriptable=*/ true, /*interactive=*/ true, }; }