Export: ODS meta block + header-row freeze/filter; flat CSV with aligned column names.

ODS sheets now carry a per-connection meta block (Connection / Transform
/ Left / Right) above the data; the header row anchors the freeze, the
auto-filter range, and the zebra striping. CSV stays a single flat
15-column table whose names match the ODS headers exactly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 18:21:50 +02:00
parent 3be5bc3f6e
commit fdf86a2e17
4 changed files with 103 additions and 49 deletions

View File

@@ -99,42 +99,67 @@ void Tui::RegisterExportCommands() {
for (auto &ckv : *sys->connections()) {
Connection *c = ckv.second;
// Sheet names: Excel rejects /\?*:[] characters,
// ODS forbids < > & in raw cell/table names
// (they'd need entity-escaping but a few viewers
// choke on them). Sanitise to underscores.
// 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 = '_';
// Sheet names also can't exceed 31 chars in
// Excel; clip aggressively to stay portable.
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[] = {
"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",
"mixed"};
for (int i = 0; i < 14; ++i) s->set(0, i, hdr[i]);
int row = 1;
"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 mixed = (lt != "(NC)" && rt != "(NC)" && lt != rt)
? "yes" : "no";
s->set(row, 0, c->transform_name);
s->set(row, 1, lm); s->set(row, 2, lp);
s->set(row, 3, ln); s->set(row, 4, ls);
s->set(row, 5, lt); s->set(row, 6, lsus);
s->set(row, 7, rm); s->set(row, 8, rp);
s->set(row, 9, rn); s->set(row, 10, rs);
s->set(row, 11, rt); s->set(row, 12, rsus);
s->set(row, 13, mixed);
// `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;
}
}
@@ -150,16 +175,26 @@ void Tui::RegisterExportCommands() {
return;
}
// CSV fallback.
// 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,"
"mixed\n";
"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()) {
@@ -169,14 +204,17 @@ void Tui::RegisterExportCommands() {
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 mixed = "no";
if (lt != "(NC)" && rt != "(NC)" && lt != rt) mixed = "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) << ','
<< mixed << '\n';
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;
}
}