diff --git a/src/imports/ods_writer.cpp b/src/imports/ods_writer.cpp index 47d7df0..e82eb99 100644 --- a/src/imports/ods_writer.cpp +++ b/src/imports/ods_writer.cpp @@ -104,14 +104,18 @@ std::string build_content_xml(const std::vector &sheets) { table.append_attribute("table:name") = s.name().c_str(); int rows = s.rows(); int cols = s.cols(); + int hdr = s.header_row(); for (int r = 0; r < rows; ++r) { auto row = table.append_child("table:table-row"); - const char *style = (r == 0) ? "hdr" - : (r % 2 == 1) ? "odd" - : "even"; + // Row class: meta (above header) → no style; header → `hdr`; + // data (below header) → zebra-stripe alternating odd/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) { 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"; auto p = cell.append_child("text:p"); 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 &sheets) { for (const auto &s : sheets) { int rows = s.rows(); int cols = s.cols(); - if (rows < 1 || cols < 1) continue; - std::string addr = quote_sheet(s.name()) + ".A1:" + int hdr = s.header_row(); + 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); auto dr = ranges.append_child("table:database-range"); std::string name = "filter_" + std::to_string(idx++); @@ -225,21 +231,22 @@ std::string build_settings_xml(const std::vector &sheets) { for (const auto &s : sheets) { auto t = tables.append_child("config:config-item-map-entry"); 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, "CursorPositionY", "int", "0"); - // 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, "CursorPositionY", "int", vsplit); add_item(t, "HorizontalSplitMode", "short", "0"); add_item(t, "VerticalSplitMode", "short", "2"); 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, "PositionLeft", "int", "0"); add_item(t, "PositionRight", "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, "ZoomValue", "int", "100"); add_item(t, "ShowGrid", "boolean", "true"); diff --git a/src/imports/ods_writer.hpp b/src/imports/ods_writer.hpp index 05623cf..7d2251f 100644 --- a/src/imports/ods_writer.hpp +++ b/src/imports/ods_writer.hpp @@ -29,6 +29,14 @@ public: 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_; } int rows() const { return (int)cells_.size(); } int cols() const; @@ -36,6 +44,7 @@ public: private: std::string name_; + int header_row_ = 0; // Row-major sparse storage: cells_[r][c] = value. Rows/cols are grown // lazily on set(). std::vector> cells_; diff --git a/src/tui/commands_export.cpp b/src/tui/commands_export.cpp index 7c4a3f7..1c50c33 100644 --- a/src/tui/commands_export.cpp +++ b/src/tui/commands_export.cpp @@ -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; } } diff --git a/src/tui/screen_dashboard.cpp b/src/tui/screen_dashboard.cpp index a6217b8..a7eb69f 100644 --- a/src/tui/screen_dashboard.cpp +++ b/src/tui/screen_dashboard.cpp @@ -299,7 +299,7 @@ Component Tui::BuildDashboardScreen() { {"e", "explore"}, {"a", "analyze (verify + groups)"}, {"h", "help screen"}, - {"x", "export (CSV)"}, + {"x", "export"}, {"PgUp", "scroll up"}, {"PgDn", "scroll down"}, {"Home", "scroll top"},