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:
@@ -104,14 +104,18 @@ std::string build_content_xml(const std::vector<OdsSheet> &sheets) {
|
|||||||
table.append_attribute("table:name") = s.name().c_str();
|
table.append_attribute("table:name") = s.name().c_str();
|
||||||
int rows = s.rows();
|
int rows = s.rows();
|
||||||
int cols = s.cols();
|
int cols = s.cols();
|
||||||
|
int hdr = s.header_row();
|
||||||
for (int r = 0; r < rows; ++r) {
|
for (int r = 0; r < rows; ++r) {
|
||||||
auto row = table.append_child("table:table-row");
|
auto row = table.append_child("table:table-row");
|
||||||
const char *style = (r == 0) ? "hdr"
|
// Row class: meta (above header) → no style; header → `hdr`;
|
||||||
: (r % 2 == 1) ? "odd"
|
// data (below header) → zebra-stripe alternating odd/even.
|
||||||
: "even";
|
const char *style = nullptr;
|
||||||
|
if (r == hdr) style = "hdr";
|
||||||
|
else if (r > hdr) style = ((r - hdr) % 2 == 1) ? "odd" : "even";
|
||||||
for (int c = 0; c < cols; ++c) {
|
for (int c = 0; c < cols; ++c) {
|
||||||
auto cell = row.append_child("table:table-cell");
|
auto cell = row.append_child("table:table-cell");
|
||||||
cell.append_attribute("table:style-name") = style;
|
if (style)
|
||||||
|
cell.append_attribute("table:style-name") = style;
|
||||||
cell.append_attribute("office:value-type") = "string";
|
cell.append_attribute("office:value-type") = "string";
|
||||||
auto p = cell.append_child("text:p");
|
auto p = cell.append_child("text:p");
|
||||||
p.append_child(pugi::node_pcdata).set_value(s.cell(r, c).c_str());
|
p.append_child(pugi::node_pcdata).set_value(s.cell(r, c).c_str());
|
||||||
@@ -141,8 +145,10 @@ std::string build_content_xml(const std::vector<OdsSheet> &sheets) {
|
|||||||
for (const auto &s : sheets) {
|
for (const auto &s : sheets) {
|
||||||
int rows = s.rows();
|
int rows = s.rows();
|
||||||
int cols = s.cols();
|
int cols = s.cols();
|
||||||
if (rows < 1 || cols < 1) continue;
|
int hdr = s.header_row();
|
||||||
std::string addr = quote_sheet(s.name()) + ".A1:"
|
if (rows <= hdr || cols < 1) continue;
|
||||||
|
std::string addr = quote_sheet(s.name())
|
||||||
|
+ ".A" + std::to_string(hdr + 1) + ":"
|
||||||
+ col_letter(cols - 1) + std::to_string(rows);
|
+ col_letter(cols - 1) + std::to_string(rows);
|
||||||
auto dr = ranges.append_child("table:database-range");
|
auto dr = ranges.append_child("table:database-range");
|
||||||
std::string name = "filter_" + std::to_string(idx++);
|
std::string name = "filter_" + std::to_string(idx++);
|
||||||
@@ -225,21 +231,22 @@ std::string build_settings_xml(const std::vector<OdsSheet> &sheets) {
|
|||||||
for (const auto &s : sheets) {
|
for (const auto &s : sheets) {
|
||||||
auto t = tables.append_child("config:config-item-map-entry");
|
auto t = tables.append_child("config:config-item-map-entry");
|
||||||
t.append_attribute("config:name") = s.name().c_str();
|
t.append_attribute("config:name") = s.name().c_str();
|
||||||
// Cursor at A1 (anchors the view state).
|
// Freeze everything up to and including the header row only —
|
||||||
|
// first column scrolls normally. ActiveSplitRange = 2
|
||||||
|
// (bottom-left sub-pane = the scrollable data).
|
||||||
|
int freeze_after = s.header_row() + 1; // 1-based row count
|
||||||
|
std::string vsplit = std::to_string(freeze_after);
|
||||||
add_item(t, "CursorPositionX", "int", "0");
|
add_item(t, "CursorPositionX", "int", "0");
|
||||||
add_item(t, "CursorPositionY", "int", "0");
|
add_item(t, "CursorPositionY", "int", vsplit);
|
||||||
// Freeze the first row only: horizontal split off, vertical
|
|
||||||
// split frozen one row in. ActiveSplitRange = 2 (bottom-left
|
|
||||||
// sub-pane = the scrollable data).
|
|
||||||
add_item(t, "HorizontalSplitMode", "short", "0");
|
add_item(t, "HorizontalSplitMode", "short", "0");
|
||||||
add_item(t, "VerticalSplitMode", "short", "2");
|
add_item(t, "VerticalSplitMode", "short", "2");
|
||||||
add_item(t, "HorizontalSplitPosition", "int", "0");
|
add_item(t, "HorizontalSplitPosition", "int", "0");
|
||||||
add_item(t, "VerticalSplitPosition", "int", "1");
|
add_item(t, "VerticalSplitPosition", "int", vsplit);
|
||||||
add_item(t, "ActiveSplitRange", "short", "2");
|
add_item(t, "ActiveSplitRange", "short", "2");
|
||||||
add_item(t, "PositionLeft", "int", "0");
|
add_item(t, "PositionLeft", "int", "0");
|
||||||
add_item(t, "PositionRight", "int", "0");
|
add_item(t, "PositionRight", "int", "0");
|
||||||
add_item(t, "PositionTop", "int", "0");
|
add_item(t, "PositionTop", "int", "0");
|
||||||
add_item(t, "PositionBottom", "int", "1");
|
add_item(t, "PositionBottom", "int", vsplit);
|
||||||
add_item(t, "ZoomType", "short", "0");
|
add_item(t, "ZoomType", "short", "0");
|
||||||
add_item(t, "ZoomValue", "int", "100");
|
add_item(t, "ZoomValue", "int", "100");
|
||||||
add_item(t, "ShowGrid", "boolean", "true");
|
add_item(t, "ShowGrid", "boolean", "true");
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ public:
|
|||||||
|
|
||||||
void set(int row, int col, std::string value);
|
void set(int row, int col, std::string value);
|
||||||
|
|
||||||
|
// Index of the row that holds the column headers — gets the bold/grey
|
||||||
|
// style, anchors the freeze, and is the first row of the auto-filter
|
||||||
|
// range. Rows above it are rendered un-styled (a place for free-form
|
||||||
|
// metadata such as the connection name). Default 0 = first row is
|
||||||
|
// the header (no meta block).
|
||||||
|
void set_header_row(int r) { header_row_ = r; }
|
||||||
|
int header_row() const { return header_row_; }
|
||||||
|
|
||||||
const std::string &name() const { return name_; }
|
const std::string &name() const { return name_; }
|
||||||
int rows() const { return (int)cells_.size(); }
|
int rows() const { return (int)cells_.size(); }
|
||||||
int cols() const;
|
int cols() const;
|
||||||
@@ -36,6 +44,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::string name_;
|
std::string name_;
|
||||||
|
int header_row_ = 0;
|
||||||
// Row-major sparse storage: cells_[r][c] = value. Rows/cols are grown
|
// Row-major sparse storage: cells_[r][c] = value. Rows/cols are grown
|
||||||
// lazily on set().
|
// lazily on set().
|
||||||
std::vector<std::vector<std::string>> cells_;
|
std::vector<std::vector<std::string>> cells_;
|
||||||
|
|||||||
@@ -99,42 +99,67 @@ void Tui::RegisterExportCommands() {
|
|||||||
for (auto &ckv : *sys->connections()) {
|
for (auto &ckv : *sys->connections()) {
|
||||||
Connection *c = ckv.second;
|
Connection *c = ckv.second;
|
||||||
// Sheet names: Excel rejects /\?*:[] characters,
|
// Sheet names: Excel rejects /\?*:[] characters,
|
||||||
// ODS forbids < > & in raw cell/table names
|
// ODS forbids < > & in raw cell/table names.
|
||||||
// (they'd need entity-escaping but a few viewers
|
// Sanitise to underscores; clip to 31 chars
|
||||||
// choke on them). Sanitise to underscores.
|
// (Excel's hard limit).
|
||||||
std::string sname = c->name;
|
std::string sname = c->name;
|
||||||
for (char &ch : sname)
|
for (char &ch : sname)
|
||||||
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|
||||||
|| ch == ':' || ch == '[' || ch == ']'
|
|| 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);
|
if (sname.size() > 31) sname = sname.substr(0, 31);
|
||||||
OdsSheet *s = w.add_sheet(sname);
|
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[] = {
|
const char *hdr[] = {
|
||||||
"transform",
|
"left_pin", "left_signal", "left_type", "left_suspect",
|
||||||
"left_module", "left_part", "left_pin",
|
"right_pin", "right_signal", "right_type", "right_suspect",
|
||||||
"left_signal", "left_type", "left_suspect",
|
"type_mismatch"};
|
||||||
"right_module", "right_part", "right_pin",
|
for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]);
|
||||||
"right_signal", "right_type", "right_suspect",
|
|
||||||
"mixed"};
|
int row = HDR + 1;
|
||||||
for (int i = 0; i < 14; ++i) s->set(0, i, hdr[i]);
|
|
||||||
int row = 1;
|
|
||||||
for (auto &wp : c->pin_map) {
|
for (auto &wp : c->pin_map) {
|
||||||
std::string lm, lp, ln, ls, lt, lsus;
|
std::string lm, lp, ln, ls, lt, lsus;
|
||||||
std::string rm, rp, rn, rs, rt, rsus;
|
std::string rm, rp, rn, rs, rt, rsus;
|
||||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||||
std::string mixed = (lt != "(NC)" && rt != "(NC)" && lt != rt)
|
// `type_mismatch = yes` when both sides have a
|
||||||
? "yes" : "no";
|
// real signal AND their types disagree (e.g.
|
||||||
s->set(row, 0, c->transform_name);
|
// Power ↔ Gnd, or Power ↔ Other).
|
||||||
s->set(row, 1, lm); s->set(row, 2, lp);
|
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt)
|
||||||
s->set(row, 3, ln); s->set(row, 4, ls);
|
? "yes" : "no";
|
||||||
s->set(row, 5, lt); s->set(row, 6, lsus);
|
s->set(row, 0, ln); s->set(row, 1, ls);
|
||||||
s->set(row, 7, rm); s->set(row, 8, rp);
|
s->set(row, 2, lt); s->set(row, 3, lsus);
|
||||||
s->set(row, 9, rn); s->set(row, 10, rs);
|
s->set(row, 4, rn); s->set(row, 5, rs);
|
||||||
s->set(row, 11, rt); s->set(row, 12, rsus);
|
s->set(row, 6, rt); s->set(row, 7, rsus);
|
||||||
s->set(row, 13, mixed);
|
s->set(row, 8, tm);
|
||||||
++row; ++total;
|
++row; ++total;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,16 +175,26 @@ void Tui::RegisterExportCommands() {
|
|||||||
return;
|
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);
|
std::ofstream f(path);
|
||||||
if (!f) {
|
if (!f) {
|
||||||
ShowError("export: cannot open '" + path + "' for writing");
|
ShowError("export: cannot open '" + path + "' for writing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
f << "connection,transform,"
|
f << "connection,transform,"
|
||||||
"left_module,left_part,left_pin,left_signal,left_type,left_suspect,"
|
"left_module,left_part,"
|
||||||
"right_module,right_part,right_pin,right_signal,right_type,right_suspect,"
|
"left_pin,left_signal,left_type,left_suspect,"
|
||||||
"mixed\n";
|
"right_module,right_part,"
|
||||||
|
"right_pin,right_signal,right_type,right_suspect,"
|
||||||
|
"type_mismatch\n";
|
||||||
|
|
||||||
int rows = 0;
|
int rows = 0;
|
||||||
for (auto &ckv : *sys->connections()) {
|
for (auto &ckv : *sys->connections()) {
|
||||||
@@ -169,14 +204,17 @@ void Tui::RegisterExportCommands() {
|
|||||||
std::string rm, rp, rn, rs, rt, rsus;
|
std::string rm, rp, rn, rs, rt, rsus;
|
||||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||||
std::string mixed = "no";
|
std::string tm = "no";
|
||||||
if (lt != "(NC)" && rt != "(NC)" && lt != rt) mixed = "yes";
|
if (lt != "(NC)" && rt != "(NC)" && lt != rt) tm = "yes";
|
||||||
f << csv_quote(c->name) << ',' << csv_quote(c->transform_name) << ','
|
f << csv_quote(c->name) << ','
|
||||||
<< csv_quote(lm) << ',' << csv_quote(lp) << ',' << csv_quote(ln) << ','
|
<< csv_quote(c->transform_name) << ','
|
||||||
<< csv_quote(ls) << ',' << csv_quote(lt) << ',' << csv_quote(lsus) << ','
|
<< csv_quote(lm) << ',' << csv_quote(lp) << ','
|
||||||
<< csv_quote(rm) << ',' << csv_quote(rp) << ',' << csv_quote(rn) << ','
|
<< csv_quote(ln) << ',' << csv_quote(ls) << ','
|
||||||
<< csv_quote(rs) << ',' << csv_quote(rt) << ',' << csv_quote(rsus) << ','
|
<< csv_quote(lt) << ',' << csv_quote(lsus) << ','
|
||||||
<< mixed << '\n';
|
<< csv_quote(rm) << ',' << csv_quote(rp) << ','
|
||||||
|
<< csv_quote(rn) << ',' << csv_quote(rs) << ','
|
||||||
|
<< csv_quote(rt) << ',' << csv_quote(rsus) << ','
|
||||||
|
<< tm << '\n';
|
||||||
++rows;
|
++rows;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ Component Tui::BuildDashboardScreen() {
|
|||||||
{"e", "explore"},
|
{"e", "explore"},
|
||||||
{"a", "analyze (verify + groups)"},
|
{"a", "analyze (verify + groups)"},
|
||||||
{"h", "help screen"},
|
{"h", "help screen"},
|
||||||
{"x", "export (CSV)"},
|
{"x", "export"},
|
||||||
{"PgUp", "scroll up"},
|
{"PgUp", "scroll up"},
|
||||||
{"PgDn", "scroll down"},
|
{"PgDn", "scroll down"},
|
||||||
{"Home", "scroll top"},
|
{"Home", "scroll top"},
|
||||||
|
|||||||
Reference in New Issue
Block a user