Export command (CSV + ODS), file dialog, error modal, path persistence.
New user-facing features:
- `export connections <file>` writes a tabular dump of every wire pair:
connection, transform, left/right module/part/pin/signal/type/suspect,
mixed-types flag. Dispatch on extension: `.csv` (flat file) or `.ods`
(one sheet per connection). Any other extension shows an error and
writes nothing.
- Bare `export` (or dashboard `[x]`, or palette `export`) opens an
interactive file-picker dialog with a CSV/ODS toggle at the top.
Picking a filter rewrites the filename's extension. Last-used
directory and filename are remembered per-call-site.
- Two new CLI flags on the binary: `--source FILE` to run a script at
boot, `--restore FILE` to restore a snapshot at boot. Combinable.
Reusable infrastructure:
- `OdsWriter` (`src/imports/ods_writer.{hpp,cpp}`): minimal .ods writer
using libzip + pugixml (already in the build for the importer).
Multi-sheet workbook of string cells. ~180 lines, no new dep.
- Generic file-picker dialog (`screen_filedialog.cpp`): one Modal
reused for any "pick a path" interaction via
`OpenFileDialog(title, persist_key, default_filename, filters, cb)`.
Validates the picked extension against the filter whitelist;
unknown ones stay in the dialog with a status message. Persists
(dir, filename) per `persist_key`.
- Generic error modal (`screen_error.cpp`, `ShowError(msg)`): centred
red-titled popup, dismissable with Esc/Enter. Used by the export
failures (open-for-write, ODS save, unknown extension/kind);
ready for adoption elsewhere.
- Per-key path persistence (`SaveLastUsed`/`LoadLastUsed` in
`shell.cpp`): two-line file per key under the user-data dir.
- `UserDataDir()` extracted from the history path helper so the new
per-key persistence shares the same XDG/AppData logic.
- New help-screen topic "Export"; user-facing `doc/user/analysis.md`
gains an "Exporting" section; `DESIGN.md` gains a generics
section covering the dialog / error modal / persistence / ODS
writer; `DumpCommandsMd` now respects the `hidden` flag (the
`connect` alias no longer appears in the auto-gen reference).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,21 @@
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
|
||||
void Tui::BootDispatch(const std::string &raw) {
|
||||
// Called before Run() — no FTXUI screen yet, so any `Source()` invoked
|
||||
// from here takes the headless synchronous branch (drains the script
|
||||
// without the event-paced loop). Output goes to `output` and is
|
||||
// visible as soon as the screen starts.
|
||||
//
|
||||
// We pin `screen_idx = 0` (console) for the duration of the dispatch
|
||||
// so the source loop's "is interactive (would open a screen)" guard
|
||||
// doesn't trip on the constructor's boot value of 4 (dashboard).
|
||||
// After execution, restore to 4 so the user lands on the dashboard.
|
||||
screen_idx = 0;
|
||||
Dispatch(raw);
|
||||
screen_idx = 4;
|
||||
}
|
||||
|
||||
void Tui::OpenSignalTypeDialog(const std::string &mod_name,
|
||||
const std::string &sig_name) {
|
||||
if (!sys) return;
|
||||
@@ -228,26 +243,73 @@ std::string Tui::ExpandVars(const std::string &s) const {
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path HistoryPath() {
|
||||
// User-data dir, platform-aware. Returns the base directory under which
|
||||
// every persistent file lives (history, export-last, …). Empty if none
|
||||
// of the expected env vars are set.
|
||||
std::filesystem::path UserDataDir() {
|
||||
namespace fs = std::filesystem;
|
||||
#ifdef _WIN32
|
||||
if (const char *p = std::getenv("LOCALAPPDATA"); p && *p)
|
||||
return fs::path(p) / "essim" / "history";
|
||||
return fs::path(p) / "essim";
|
||||
if (const char *p = std::getenv("APPDATA"); p && *p)
|
||||
return fs::path(p) / "essim" / "history";
|
||||
return fs::path(p) / "essim";
|
||||
if (const char *p = std::getenv("USERPROFILE"); p && *p)
|
||||
return fs::path(p) / "AppData" / "Local" / "essim" / "history";
|
||||
return fs::path(p) / "AppData" / "Local" / "essim";
|
||||
#else
|
||||
if (const char *p = std::getenv("XDG_DATA_HOME"); p && *p)
|
||||
return fs::path(p) / "essim" / "history";
|
||||
return fs::path(p) / "essim";
|
||||
if (const char *p = std::getenv("HOME"); p && *p)
|
||||
return fs::path(p) / ".local" / "share" / "essim" / "history";
|
||||
return fs::path(p) / ".local" / "share" / "essim";
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path HistoryPath() {
|
||||
auto d = UserDataDir();
|
||||
return d.empty() ? std::filesystem::path{} : (d / "history");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Free helpers for "last used (dir, filename) per purpose". Stored as a
|
||||
// tiny two-line file per key under the user-data dir. Used by the file
|
||||
// dialog so each call site (export, save, restore, …) gets its own
|
||||
// memory without further plumbing.
|
||||
namespace {
|
||||
|
||||
std::filesystem::path LastUsedPath(const std::string &key) {
|
||||
auto d = UserDataDir();
|
||||
return d.empty() ? std::filesystem::path{} : (d / (key + ".last"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SaveLastUsed(const std::string &key,
|
||||
const std::string &dir,
|
||||
const std::string &filename) {
|
||||
auto p = LastUsedPath(key);
|
||||
if (p.empty()) return;
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(p.parent_path(), ec);
|
||||
std::ofstream f(p);
|
||||
if (!f) return;
|
||||
f << dir << '\n' << filename << '\n';
|
||||
}
|
||||
|
||||
bool LoadLastUsed(const std::string &key,
|
||||
std::string &dir, std::string &filename) {
|
||||
auto p = LastUsedPath(key);
|
||||
if (p.empty()) return false;
|
||||
std::ifstream f(p);
|
||||
if (!f) return false;
|
||||
std::string d, n;
|
||||
if (std::getline(f, d) && std::getline(f, n) && !d.empty() && !n.empty()) {
|
||||
dir = d; filename = n;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Tui::LoadHistory() {
|
||||
auto p = HistoryPath();
|
||||
if (p.empty()) return;
|
||||
@@ -365,6 +427,7 @@ void Tui::DumpCommandsMd(std::ostream &out) const {
|
||||
bool printed_title = false;
|
||||
for (const auto &kv : commands) {
|
||||
const CommandSpec &spec = kv.second;
|
||||
if (spec.hidden) continue; // aliases & internal entries
|
||||
if (spec.interactive != want_interactive) continue;
|
||||
if (!printed_title) {
|
||||
out << "\n## " << title << "\n\n";
|
||||
|
||||
Reference in New Issue
Block a user