Refactor: extract export commands; co-locate sigtype popup logic.
Two focused, behaviour-preserving moves: 1. `OpenSignalTypeDialog` + `ApplySignalTypeChoice` moved from `shell.cpp` to `screen_sigtype_modal.cpp` so the popup owns all of its logic instead of having its open/apply functions live in the shell file. 2. The `export` command extracted from `commands.cpp` to a new `commands_export.cpp` under a `Tui::RegisterExportCommands()` member. `RegisterCommands()` calls it at the end. File-local helpers (`csv_quote`, `pin_side`) move alongside in an anonymous namespace. Establishes the pattern for future per-group splits: declare a `Register<X>Commands()` member, define it in its own file, call it from the orchestrator. Other groups stay in `commands.cpp` for now — nothing else has grown large enough to warrant the split. Sizes: shell.cpp 497 → 448, commands.cpp 846 → 675 (+ 191 for the new commands_export.cpp). DESIGN.md updated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,8 @@ src/
|
|||||||
tui_helpers.{hpp,cpp} Free helpers: ToLower, NaturalLess, LongestCommonPrefix
|
tui_helpers.{hpp,cpp} Free helpers: ToLower, NaturalLess, LongestCommonPrefix
|
||||||
shell.cpp Print, Submit, Dispatch, Finalize, Tokenize, history persistence
|
shell.cpp Print, Submit, Dispatch, Finalize, Tokenize, history persistence
|
||||||
completion.cpp CompleteCommand, CompletePath, CompleteInline
|
completion.cpp CompleteCommand, CompletePath, CompleteInline
|
||||||
commands.cpp RegisterCommands (all built-in commands declared here)
|
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_main.cpp BuildMainScreen (visualisation area + bottom input)
|
||||||
screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper
|
screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper
|
||||||
screen_settype.cpp BuildSettypeScreen
|
screen_settype.cpp BuildSettypeScreen
|
||||||
@@ -162,6 +163,8 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l
|
|||||||
|
|
||||||
Everything is recomputed every frame so manual overrides via the signal-type popup are reflected immediately. Esc returns to the dashboard. The dashboard's previous `[v]erify` letter shortcut was removed — its content is fully covered by this screen. The textual `verify` / `analyze` commands still exist for scripts.
|
Everything is recomputed every frame so manual overrides via the signal-type popup are reflected immediately. Esc returns to the dashboard. The dashboard's previous `[v]erify` letter shortcut was removed — its content is fully covered by this screen. The textual `verify` / `analyze` commands still exist for scripts.
|
||||||
|
|
||||||
|
**Command group factorisation**: `RegisterCommands()` in `commands.cpp` owns most built-ins, but self-contained groups live in their own files (one `Register<X>Commands()` member each). Today only `RegisterExportCommands()` in `commands_export.cpp` follows the pattern. Adding a new group is mechanical: declare a new member in `tui.hpp`'s `private:` section, define it in `commands_<group>.cpp`, and call it from the orchestrator. Each group can have file-local helpers (e.g. `commands_export.cpp` has its own anonymous-namespace `csv_quote` and `pin_side`).
|
||||||
|
|
||||||
**Generic file-picker dialog** (`screen_filedialog.cpp`): one reusable modal for every "pick a path" interaction. State lives in `Tui::file_dialog` (a single `FileDialogState`); attached to the tab tree via `Modal(BuildFileDialog(), &file_dialog.open)` in `Run()`. API:
|
**Generic file-picker dialog** (`screen_filedialog.cpp`): one reusable modal for every "pick a path" interaction. State lives in `Tui::file_dialog` (a single `FileDialogState`); attached to the tab tree via `Modal(BuildFileDialog(), &file_dialog.open)` in `Run()`. API:
|
||||||
|
|
||||||
- `OpenFileDialog(title, persist_key, default_filename, filters, on_confirm)` — opens the modal, restoring the last-used `(dir, filename)` for `persist_key` if previously saved.
|
- `OpenFileDialog(title, persist_key, default_filename, filters, on_confirm)` — opens the modal, restoring the last-used `(dir, filename)` for `persist_key` if previously saved.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include "imports/ods_writer.hpp"
|
|
||||||
#include "system/analysis.hpp"
|
#include "system/analysis.hpp"
|
||||||
#include "system/connect.hpp"
|
#include "system/connect.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "system/modules.hpp"
|
||||||
@@ -206,180 +205,6 @@ void Tui::RegisterCommands() {
|
|||||||
"write the current system snapshot to a file",
|
"write the current system snapshot to a file",
|
||||||
};
|
};
|
||||||
|
|
||||||
commands["export"] = {
|
|
||||||
{{"kind [connections]", Completion::None},
|
|
||||||
{"filename (.csv)", Completion::Path}},
|
|
||||||
[this](const std::vector<std::string> &args) {
|
|
||||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
||||||
if (args.empty()) {
|
|
||||||
// Bare → reuse the generic file dialog. The kind defaults
|
|
||||||
// to "connections" (the only one today); when more kinds
|
|
||||||
// are added, prompt for it inside the dialog before the
|
|
||||||
// filename step. Filters give the user a one-keystroke
|
|
||||||
// toggle between CSV and ODS — 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 <kind> <file> (or no args for the dialog)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const std::string &kind = args[0];
|
|
||||||
const std::string &path = args[1];
|
|
||||||
|
|
||||||
// Minimal CSV quoter — wraps in `"…"` and doubles internal
|
|
||||||
// quotes when the field contains a comma, quote, or newline.
|
|
||||||
auto q = [](const std::string &s) -> std::string {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (kind == "connections") {
|
|
||||||
auto 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";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dispatch on extension. Accepted: `.csv` (flat file) and
|
|
||||||
// `.ods` (one sheet per connection). Anything else is an
|
|
||||||
// error — silently writing a CSV under a `.xyz` filename
|
|
||||||
// would be confusing.
|
|
||||||
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 can't contain '/' in some viewers
|
|
||||||
// (LibreOffice tolerates it, Excel rejects it).
|
|
||||||
// Sanitise just in case.
|
|
||||||
std::string sname = c->name;
|
|
||||||
for (char &ch : sname)
|
|
||||||
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|
|
||||||
|| ch == ':' || ch == '[' || ch == ']') ch = '_';
|
|
||||||
OdsSheet *s = w.add_sheet(sname);
|
|
||||||
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;
|
|
||||||
for (auto &wp : c->pin_map) {
|
|
||||||
std::string lm, lp, ln, ls, lt, lsus;
|
|
||||||
std::string rm, rp, rn, rs, rt, rsus;
|
|
||||||
side(wp.first, lm, lp, ln, ls, lt, lsus);
|
|
||||||
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);
|
|
||||||
++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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSV fallback.
|
|
||||||
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";
|
|
||||||
|
|
||||||
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;
|
|
||||||
side(wp.first, lm, lp, ln, ls, lt, lsus);
|
|
||||||
side(wp.second, rm, rp, rn, rs, rt, rsus);
|
|
||||||
std::string mixed = "no";
|
|
||||||
if (lt != "(NC)" && rt != "(NC)" && lt != rt) mixed = "yes";
|
|
||||||
f << q(c->name) << ',' << q(c->transform_name) << ','
|
|
||||||
<< q(lm) << ',' << q(lp) << ',' << q(ln) << ','
|
|
||||||
<< q(ls) << ',' << q(lt) << ',' << q(lsus) << ','
|
|
||||||
<< q(rm) << ',' << q(rp) << ',' << q(rn) << ','
|
|
||||||
<< q(rs) << ',' << q(rt) << ',' << q(rsus) << ','
|
|
||||||
<< mixed << '\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 (kinds: connections; "
|
|
||||||
"bare form opens the file-picker dialog)",
|
|
||||||
/*scriptable=*/ true,
|
|
||||||
/*interactive=*/ true,
|
|
||||||
};
|
|
||||||
|
|
||||||
commands["restore"] = {
|
commands["restore"] = {
|
||||||
{{"filename", Completion::Path}},
|
{{"filename", Completion::Path}},
|
||||||
@@ -843,4 +668,8 @@ void Tui::RegisterCommands() {
|
|||||||
/*prompt_for_missing=*/ true,
|
/*prompt_for_missing=*/ true,
|
||||||
"clone a module under a new name (parts, pins, signals; no connections)",
|
"clone a module under a new name (parts, pins, signals; no connections)",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Per-group registrators living in their own files. Keeps each
|
||||||
|
// self-contained concern out of this orchestrator.
|
||||||
|
RegisterExportCommands();
|
||||||
}
|
}
|
||||||
|
|||||||
191
src/tui/commands_export.cpp
Normal file
191
src/tui/commands_export.cpp
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
#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 <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<std::string> &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 <kind> <file> (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
|
||||||
|
// (LibreOffice tolerates them but stays portable).
|
||||||
|
std::string sname = c->name;
|
||||||
|
for (char &ch : sname)
|
||||||
|
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|
||||||
|
|| ch == ':' || ch == '[' || ch == ']') ch = '_';
|
||||||
|
OdsSheet *s = w.add_sheet(sname);
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
++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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSV fallback.
|
||||||
|
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";
|
||||||
|
|
||||||
|
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 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';
|
||||||
|
++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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,12 +1,71 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "tui/tui.hpp"
|
||||||
|
|
||||||
|
#include "system/modules.hpp"
|
||||||
|
#include "system/signals.hpp"
|
||||||
|
#include "system/system.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
#include <ftxui/component/event.hpp>
|
#include <ftxui/component/event.hpp>
|
||||||
#include <ftxui/dom/elements.hpp>
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
using namespace ftxui;
|
using namespace ftxui;
|
||||||
|
|
||||||
|
// Lifecycle of the signal-type popup. The Open* and Apply* methods used
|
||||||
|
// to live in shell.cpp; moved here so the modal owns all of its logic.
|
||||||
|
|
||||||
|
void Tui::OpenSignalTypeDialog(const std::string &mod_name,
|
||||||
|
const std::string &sig_name) {
|
||||||
|
if (!sys) return;
|
||||||
|
Signal *sig = nullptr;
|
||||||
|
try {
|
||||||
|
Module *m = sys->modules()->get(mod_name);
|
||||||
|
sig = m->signals->get(sig_name);
|
||||||
|
} catch (const std::exception &) { return; }
|
||||||
|
|
||||||
|
sigtype_dialog_mod = mod_name;
|
||||||
|
sigtype_dialog_sig = sig_name;
|
||||||
|
switch (sig->type) {
|
||||||
|
case SignalType::Power: sigtype_dialog_choice = 0; break;
|
||||||
|
case SignalType::GndShield: sigtype_dialog_choice = 1; break;
|
||||||
|
default: sigtype_dialog_choice = 2; break;
|
||||||
|
}
|
||||||
|
sigtype_dialog_open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tui::ApplySignalTypeChoice() {
|
||||||
|
sigtype_dialog_open = false;
|
||||||
|
if (!sys) return;
|
||||||
|
SignalType t;
|
||||||
|
switch (sigtype_dialog_choice) {
|
||||||
|
case 0: t = SignalType::Power; break;
|
||||||
|
case 1: t = SignalType::GndShield; break;
|
||||||
|
default: t = SignalType::Other; break;
|
||||||
|
}
|
||||||
|
Signal *sig = nullptr;
|
||||||
|
try {
|
||||||
|
Module *m = sys->modules()->get(sigtype_dialog_mod);
|
||||||
|
sig = m->signals->get(sigtype_dialog_sig);
|
||||||
|
} catch (const std::exception &) { return; }
|
||||||
|
if (sig->type == t) return; // no-op, no record
|
||||||
|
sig->type = t;
|
||||||
|
if (in_source) return;
|
||||||
|
|
||||||
|
// Dedup: if the immediately previous recorded line targets the same
|
||||||
|
// signal, replace it so a sequence of toggles collapses to one line.
|
||||||
|
std::string line = "set-signal-type " + sigtype_dialog_mod + " "
|
||||||
|
+ sigtype_dialog_sig + " " + signal_type_name(t);
|
||||||
|
std::string prefix = "set-signal-type " + sigtype_dialog_mod + " "
|
||||||
|
+ sigtype_dialog_sig + " ";
|
||||||
|
if (!recorded.empty() && recorded.back().rfind(prefix, 0) == 0)
|
||||||
|
recorded.back() = std::move(line);
|
||||||
|
else
|
||||||
|
recorded.push_back(std::move(line));
|
||||||
|
}
|
||||||
|
|
||||||
Component Tui::BuildSignalTypeModal() {
|
Component Tui::BuildSignalTypeModal() {
|
||||||
sigtype_dialog_entries = {"power", "gnd", "other"};
|
sigtype_dialog_entries = {"power", "gnd", "other"};
|
||||||
|
|
||||||
|
|||||||
@@ -31,55 +31,6 @@ void Tui::BootDispatch(const std::string &raw) {
|
|||||||
screen_idx = 4;
|
screen_idx = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tui::OpenSignalTypeDialog(const std::string &mod_name,
|
|
||||||
const std::string &sig_name) {
|
|
||||||
if (!sys) return;
|
|
||||||
Signal *sig = nullptr;
|
|
||||||
try {
|
|
||||||
Module *m = sys->modules()->get(mod_name);
|
|
||||||
sig = m->signals->get(sig_name);
|
|
||||||
} catch (const std::exception &) { return; }
|
|
||||||
|
|
||||||
sigtype_dialog_mod = mod_name;
|
|
||||||
sigtype_dialog_sig = sig_name;
|
|
||||||
switch (sig->type) {
|
|
||||||
case SignalType::Power: sigtype_dialog_choice = 0; break;
|
|
||||||
case SignalType::GndShield: sigtype_dialog_choice = 1; break;
|
|
||||||
default: sigtype_dialog_choice = 2; break;
|
|
||||||
}
|
|
||||||
sigtype_dialog_open = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Tui::ApplySignalTypeChoice() {
|
|
||||||
sigtype_dialog_open = false;
|
|
||||||
if (!sys) return;
|
|
||||||
SignalType t;
|
|
||||||
switch (sigtype_dialog_choice) {
|
|
||||||
case 0: t = SignalType::Power; break;
|
|
||||||
case 1: t = SignalType::GndShield; break;
|
|
||||||
default: t = SignalType::Other; break;
|
|
||||||
}
|
|
||||||
Signal *sig = nullptr;
|
|
||||||
try {
|
|
||||||
Module *m = sys->modules()->get(sigtype_dialog_mod);
|
|
||||||
sig = m->signals->get(sigtype_dialog_sig);
|
|
||||||
} catch (const std::exception &) { return; }
|
|
||||||
if (sig->type == t) return; // no-op, no record
|
|
||||||
sig->type = t;
|
|
||||||
if (in_source) return;
|
|
||||||
|
|
||||||
// Dedup: if the immediately previous recorded line targets the same
|
|
||||||
// signal, replace it so a sequence of toggles collapses to one line.
|
|
||||||
std::string line = "set-signal-type " + sigtype_dialog_mod + " "
|
|
||||||
+ sigtype_dialog_sig + " " + signal_type_name(t);
|
|
||||||
std::string prefix = "set-signal-type " + sigtype_dialog_mod + " "
|
|
||||||
+ sigtype_dialog_sig + " ";
|
|
||||||
if (!recorded.empty() && recorded.back().rfind(prefix, 0) == 0)
|
|
||||||
recorded.back() = std::move(line);
|
|
||||||
else
|
|
||||||
recorded.push_back(std::move(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Tui::Print(const std::string &line) {
|
void Tui::Print(const std::string &line) {
|
||||||
output.push_back(line);
|
output.push_back(line);
|
||||||
scroll_offset = 0; // any new line snaps the view back to the tail
|
scroll_offset = 0; // any new line snaps the view back to the tail
|
||||||
|
|||||||
@@ -201,6 +201,9 @@ public:
|
|||||||
private:
|
private:
|
||||||
// Lifecycle (commands.cpp)
|
// Lifecycle (commands.cpp)
|
||||||
void RegisterCommands();
|
void RegisterCommands();
|
||||||
|
// Per-file command-group registrators. Each adds entries to the
|
||||||
|
// `commands` map. Called from RegisterCommands().
|
||||||
|
void RegisterExportCommands(); // commands_export.cpp
|
||||||
|
|
||||||
// Shell (shell.cpp)
|
// Shell (shell.cpp)
|
||||||
void Print(const std::string &line);
|
void Print(const std::string &line);
|
||||||
|
|||||||
Reference in New Issue
Block a user