Compare commits

...

7 Commits

Author SHA1 Message Date
63ca17d048 build: split core/ from frontends/; prepare for multiple GUI/TUI targets
Reorganise the tree into business vs frontend as separate directories:
  src/core/{domain,imports,app}   (was system/, imports/, app/)
  src/frontends/tui/              (was tui/ + main.cpp)
  tests/tui/                      (the FTXUI-coupled helper test)
All cross-dir #include paths rewritten; same-dir includes untouched.

CMake: essim_core is the frontend-agnostic business library — links libzip,
pugixml and bsdl, NO GUI toolkit. Each frontend is a self-contained
src/frontends/<name>/ (own CMakeLists, toolkit, main.cpp) that links
essim_core, selected with -DESSIM_FRONTEND=<name> (default tui; 'none' = core +
tests only, no toolkit fetched). FTXUI moved into the tui frontend. Tests are
split: essim_tests links essim_core (no FTXUI), essim_tui_tests links essim_tui.

Verified: default tui build green (ctest 2/2); ESSIM_FRONTEND=none builds the
core + tests with FTXUI never fetched and no `essim` binary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:33:06 +02:00
3010bb25eb core/ui: extract export into src/app (frontend-agnostic), thin TUI command
First step of separating business logic from the TUI. The export command built
the CSV/ODS file inside its lambda, mixed with Print/ShowError/dialog calls.
Move all of it — CSV + ODS building, sheet-name sanitising, file writing — into
src/app/export.{hpp,cpp} (namespace app, no FTXUI/console dependency):
export_connections(const System*, path, format) -> ExportResult. The TUI
command is now a thin wrapper (resolve args/dialog, call the core, render). The
core is unit-tested without any UI (test_export); 342 assertions pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:23:41 +02:00
ac2edd90c4 tui: show the Computing... progress as a real Modal (was invisible)
The Renderer-overlay approach to the global progress box didn't render. Use a
proper Modal like the palette / file dialog, driven by a plain bool
'computing_open' raised when a source starts and lowered when it ends or
aborts. The tick handler stays ahead of the modal guard, so the script keeps
running (and the screen behind it keeps updating) while the modal is shown;
computing_open is also added to the guard so stray keys are ignored mid-load.
The console screen's own Computing block was already removed, so no duplicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:11:27 +02:00
53eb79c760 source: abort guard compares to the originating screen, not 0
A script opened from the dashboard (screen_idx 4) aborted after its first
line: the guard treated any non-console screen as 'an interactive command
opened a screen'. Record the screen the source started from and abort only
when a sourced line navigates away from it (what a bare interactive command
does). Now 'o' from the dashboard runs the whole script in place — the
dashboard populates live behind the global Computing overlay — while a bare
connect/explore inside a script still aborts. Batch unaffected (BootDispatch
pins screen 0, so origin 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:06:18 +02:00
29cb353d75 tui: global script progress + dashboard source/restore work
Opening a script from the dashboard did nothing visually: the tick driving the
line-by-line loader was only handled in the console case, and the Computing…
overlay was console-only. Move both to the global layer — ticks now process on
any screen (the dashboard updates live as the script loads) and the Computing…
box overlays whatever screen is active. Add an 'r' dashboard shortcut to
restore a snapshot via the file picker (open mode, like 'o'). Dashboard help
hints (loaded + empty state) updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:02:13 +02:00
c70e767cf1 filedialog: add open mode (no overwrite prompt) and use it for 'o'
The picker is built for saving, so it asks 'file exists — overwrite?' on
confirm. That's wrong when opening a script (you want an existing file). Add a
confirm_overwrite flag (default true; FileDialogState + OpenFileDialog param);
ConfirmFileDialog only prompts when it's set. The dashboard 'o' shortcut now
opens the dialog with confirm_overwrite=false. save/export keep the guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:52:53 +02:00
527a48145b dashboard: show 'o' (open script) in the no-system help panel too
The empty-dashboard panel (early_help) is a separate hint list from the
loaded-system one, and didn't list the new o/s keys — so 'o' worked but wasn't
advertised when no system is loaded (exactly when you'd use it). Add 'o' to
early_help and mention it in the 'no system loaded' line. (s/x need a system,
so they stay out of the empty-state panel.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:49:42 +02:00
84 changed files with 691 additions and 466 deletions

View File

@@ -11,49 +11,54 @@ project(essim
include(FetchContent) include(FetchContent)
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "") # ----------------------------------------------------------------- core deps
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "") # libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2,
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "") # which the LGPL permits). Override its path with -DBSDL_DIR=...
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
FetchContent_Declare(ftxui
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
GIT_TAG v6.1.9
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(ftxui)
find_package(libzip REQUIRED)
find_package(pugixml REQUIRED)
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked from essim
# (EUPL-1.2, which the LGPL permits). Path overridable via -DBSDL_DIR=...;
# its CLI and tests are not needed inside essim's build.
set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree") set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree")
set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE) set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE)
set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl) add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl)
# Library target = everything except main.cpp; reused by `essim` and `essim_tests`. find_package(libzip REQUIRED)
file(GLOB_RECURSE LIB_SOURCES "src/*.cpp") find_package(pugixml REQUIRED)
list(REMOVE_ITEM LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp")
add_library(essim_lib STATIC ${LIB_SOURCES}) # =============================================================== essim_core
target_include_directories(essim_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) # All business logic — domain model, importers, application operations
target_link_libraries(essim_lib # (src/core/{domain,imports,app}). Frontend-agnostic: it links NO GUI/TUI
# toolkit, so every frontend and the test suite share the exact same core.
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
add_library(essim_core STATIC ${CORE_SOURCES})
target_include_directories(essim_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(essim_core
PUBLIC PUBLIC
ftxui::screen
ftxui::dom
ftxui::component
libzip::zip libzip::zip
pugixml::pugixml pugixml::pugixml
bsdl::bsdl bsdl::bsdl
) )
add_executable(essim src/main.cpp) # =============================================================== frontend(s)
target_link_libraries(essim PRIVATE essim_lib) # Pick the GUI/TUI frontend to build the `essim` binary against. Each frontend
# is a self-contained src/frontends/<name>/ (own CMakeLists, GUI toolkit, and
# main.cpp) that links essim_core. "none" builds the core + tests only — no GUI
# toolkit is fetched. To add a frontend (e.g. a Qt GUI), create
# src/frontends/gui/ and configure with -DESSIM_FRONTEND=gui.
set(ESSIM_FRONTEND "tui" CACHE STRING
"Frontend to build: a directory name under src/frontends/, or 'none'")
set_property(CACHE ESSIM_FRONTEND PROPERTY STRINGS tui none)
# Tests if(ESSIM_FRONTEND STREQUAL "none")
message(STATUS "essim: ESSIM_FRONTEND=none — core + tests only (no frontend, no GUI toolkit)")
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt")
message(STATUS "essim: building frontend '${ESSIM_FRONTEND}'")
add_subdirectory(src/frontends/${ESSIM_FRONTEND})
else()
message(FATAL_ERROR
"Unknown ESSIM_FRONTEND '${ESSIM_FRONTEND}' — expected "
"src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt, or 'none'.")
endif()
# =============================================================== tests (core)
# The suite exercises essim_core only — no frontend, no GUI toolkit.
include(CTest) include(CTest)
if(BUILD_TESTING) if(BUILD_TESTING)
set(CMAKE_POLICY_VERSION_MINIMUM 3.5) set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
@@ -65,20 +70,35 @@ if(BUILD_TESTING)
FetchContent_MakeAvailable(doctest) FetchContent_MakeAvailable(doctest)
unset(CMAKE_POLICY_VERSION_MINIMUM) unset(CMAKE_POLICY_VERSION_MINIMUM)
# Core tests — exercise essim_core only (tests/*.cpp, non-recursive, so the
# per-frontend tests under tests/<frontend>/ are not pulled in here).
file(GLOB TEST_SOURCES "tests/*.cpp") file(GLOB TEST_SOURCES "tests/*.cpp")
if(TEST_SOURCES) if(TEST_SOURCES)
add_executable(essim_tests ${TEST_SOURCES}) add_executable(essim_tests ${TEST_SOURCES})
target_link_libraries(essim_tests PRIVATE essim_lib doctest::doctest) target_link_libraries(essim_tests PRIVATE essim_core doctest::doctest)
add_test(NAME essim_tests COMMAND essim_tests) add_test(NAME essim_tests COMMAND essim_tests)
endif() endif()
# Per-frontend tests — tests/<frontend>/*.cpp, built and linked against that
# frontend's library only when the frontend itself is built.
if(TARGET essim_tui)
file(GLOB TUI_TEST_SOURCES "tests/tui/*.cpp")
if(TUI_TEST_SOURCES)
add_executable(essim_tui_tests
"${CMAKE_CURRENT_SOURCE_DIR}/tests/doctest_main.cpp" ${TUI_TEST_SOURCES})
target_link_libraries(essim_tui_tests PRIVATE essim_tui doctest::doctest)
add_test(NAME essim_tui_tests COMMAND essim_tui_tests)
endif()
endif()
endif() endif()
# Documentation: Doxygen → XML → custom Python script → doc/api/ (Markdown rendered by gitea). # =============================================================== documentation
# Optional — `doc` target is only created if Doxygen and Python 3 are present. # Doxygen → XML → gen_api_md.py → doc/api/, plus `essim --commands-md`. Needs the
# `essim` binary, so it's only wired when a frontend that provides one is built.
find_package(Doxygen COMPONENTS doxygen) find_package(Doxygen COMPONENTS doxygen)
find_package(Python3 COMPONENTS Interpreter) find_package(Python3 COMPONENTS Interpreter)
if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND) if(TARGET essim AND DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/doc") set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/doc")
file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}") file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}")
configure_file( configure_file(
@@ -103,11 +123,10 @@ if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)" COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)"
VERBATIM) VERBATIM)
elseif(NOT DOXYGEN_FOUND AND NOT Python3_Interpreter_FOUND) elseif(NOT TARGET essim)
message(STATUS "doc: Doxygen and Python 3 not found — `doc` target disabled.") message(STATUS "doc: no `essim` binary (ESSIM_FRONTEND=none) — `doc` target disabled.")
elseif(NOT DOXYGEN_FOUND) elseif(NOT DOXYGEN_FOUND)
message(STATUS "doc: Doxygen not found — `doc` target disabled " message(STATUS "doc: Doxygen not found — `doc` target disabled.")
"(install via `pacman -S doxygen`).")
else() else()
message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.") message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.")
endif() endif()

190
src/core/app/export.cpp Normal file
View File

@@ -0,0 +1,190 @@
#include "core/app/export.hpp"
#include "core/imports/ods_writer.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signal_type.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <cctype>
#include <fstream>
#include <string>
namespace app {
namespace {
std::string to_lower(std::string s)
{
for (char &c : s) c = (char)std::tolower((unsigned char)c);
return s;
}
// Minimal CSV quoter — wraps in "…" and doubles internal quotes when the field
// contains a comma, quote, or newline.
std::string csv_quote(const std::string &s)
{
if (s.find_first_of(",\"\n") == std::string::npos)
return s;
std::string out = "\"";
for (char c : s) { if (c == '"') out += '"'; out += c; }
out += '"';
return out;
}
// Flatten one pin into the string slots an 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";
}
}
// Excel rejects /\?*:[] in sheet names; ODS forbids < > & in raw table names.
// Sanitise to underscores and clip to Excel's 31-char hard limit.
std::string sanitise_sheet_name(std::string name)
{
for (char &ch : name)
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*' || ch == ':'
|| ch == '[' || ch == ']' || ch == '<' || ch == '>' || ch == '&')
ch = '_';
if (name.size() > 31) name = name.substr(0, 31);
return name;
}
ExportResult write_ods(const System *sys, const std::string &path)
{
ExportResult r;
OdsWriter w;
for (auto &ckv : *sys->connections()) {
Connection *c = ckv.second;
OdsSheet *s = w.add_sheet(sanitise_sheet_name(c->name));
std::string lmod = c->m1 ? c->m1->name : "";
std::string lprt = c->p1 ? c->p1->name : "";
std::string rmod = c->m2 ? c->m2->name : "";
std::string rprt = c->p2 ? c->p2->name : "";
// Meta header: label/value rows, a blank row, then the column headers.
auto meta = [&](int row, const std::string &k, const std::string &v) {
s->set(row, 0, k);
s->set(row, 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[] = {
"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 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; ++r.rows;
}
}
std::string err;
if (!w.save(path, err)) {
r.error = err;
return r;
}
r.ok = true;
r.sheets = (int)sys->connections()->size();
return r;
}
ExportResult write_csv(const System *sys, const std::string &path)
{
ExportResult r;
std::ofstream f(path);
if (!f) {
r.error = "cannot open '" + path + "' for writing";
return r;
}
// One rectangular table: each row repeats the per-connection constants so
// the file stays parser-friendly (pandas / awk / spreadsheet).
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,"
"type_mismatch\n";
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 tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no";
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';
++r.rows;
}
}
r.ok = f.good();
if (!r.ok) r.error = "write error on '" + path + "'";
return r;
}
} // namespace
bool export_format_from_path(const std::string &path, ExportFormat &out)
{
size_t dot = path.rfind('.');
std::string ext = (dot == std::string::npos) ? "" : to_lower(path.substr(dot));
if (ext == ".csv") { out = ExportFormat::Csv; return true; }
if (ext == ".ods") { out = ExportFormat::Ods; return true; }
return false;
}
ExportResult export_connections(const System *sys, const std::string &path,
ExportFormat format)
{
ExportResult r;
if (!sys) { r.error = "no system"; return r; }
return (format == ExportFormat::Ods) ? write_ods(sys, path)
: write_csv(sys, path);
}
} // namespace app

34
src/core/app/export.hpp Normal file
View File

@@ -0,0 +1,34 @@
#ifndef _APP_EXPORT_HPP_
#define _APP_EXPORT_HPP_
#include <string>
class System;
// Application layer: UI-independent operations that any frontend (TUI, GUI, …)
// can call. No console, no dialogs, no FTXUI — just System in, result out.
namespace app {
enum class ExportFormat { Csv, Ods };
// Outcome of an export. The only side effect is writing the target file; the
// caller renders `error` / the stats however it likes.
struct ExportResult {
bool ok = false;
std::string error; ///< human-readable, set when !ok
int sheets = 0; ///< ODS: number of sheets (one per connection); 0 for CSV
int rows = 0; ///< wires written
};
// Map a filename extension (.csv / .ods, case-insensitive) to a format.
// Returns false if the extension is neither.
bool export_format_from_path(const std::string &path, ExportFormat &out);
// Export the system's connections to `path` in `format`. Builds the file and
// returns stats or an error. Pure core — safe to call from any frontend.
ExportResult export_connections(const System *sys, const std::string &path,
ExportFormat format);
} // namespace app
#endif // _APP_EXPORT_HPP_

View File

@@ -1,4 +1,4 @@
#include "system/component_kind.hpp" #include "core/domain/component_kind.hpp"
#include <cctype> #include <cctype>
#include <string> #include <string>

View File

@@ -1,11 +1,11 @@
#include "system/nets.hpp" #include "core/domain/nets.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <queue> #include <queue>
#include <unordered_map> #include <unordered_map>

View File

@@ -1,4 +1,4 @@
#include "system/pin_name.hpp" #include "core/domain/pin_name.hpp"
#include <cstdio> #include <cstdio>
#include <stdexcept> #include <stdexcept>

View File

@@ -3,9 +3,9 @@
#include "connect.hpp" #include "connect.hpp"
#include "modules.hpp" #include "modules.hpp"
#include "imports/import_altium.hpp" #include "core/imports/import_altium.hpp"
#include "imports/import_mentor.hpp" #include "core/imports/import_mentor.hpp"
#include "imports/import_ods.hpp" #include "core/imports/import_ods.hpp"
System::System() : mods(nullptr), conns(nullptr) System::System() : mods(nullptr), conns(nullptr)
{ {

View File

@@ -1,7 +1,7 @@
#ifndef _SYSTEM_HPP_ #ifndef _SYSTEM_HPP_
#define _SYSTEM_HPP_ #define _SYSTEM_HPP_
#include "imports/import_base.hpp" #include "core/imports/import_base.hpp"
#pragma once #pragma once
class Modules; ///< Forward declaration of the Modules class. class Modules; ///< Forward declaration of the Modules class.

View File

@@ -1,8 +1,8 @@
#include "import_altium.hpp" #include "import_altium.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include <cctype> #include <cctype>
#include <string> #include <string>

View File

@@ -4,8 +4,8 @@
#include <string> #include <string>
#include <fstream> #include <fstream>
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
/** /**
* @brief Base class for importing data from a file. * @brief Base class for importing data from a file.

View File

@@ -1,6 +1,6 @@
#include "import_mentor.hpp" #include "import_mentor.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include <cctype> #include <cctype>
#include <vector> #include <vector>

View File

@@ -1,8 +1,8 @@
#include "import_ods.hpp" #include "import_ods.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include <pugixml.hpp> #include <pugixml.hpp>
#include <zip.h> #include <zip.h>

View File

@@ -0,0 +1,35 @@
# TUI frontend (FTXUI). Builds the `essim` executable against essim_core.
#
# A frontend is self-contained here: it pulls its own GUI toolkit, compiles its
# sources into a library that links essim_core, and produces the `essim` binary
# from its own entry point (main.cpp). To add another frontend, create a sibling
# src/frontends/<name>/ with the same shape and select it with
# -DESSIM_FRONTEND=<name>.
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "")
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "")
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "")
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
FetchContent_Declare(ftxui
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
GIT_TAG v6.1.9
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(ftxui)
# Frontend library = every .cpp here except the entry point.
file(GLOB TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")
list(REMOVE_ITEM TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
add_library(essim_tui STATIC ${TUI_SOURCES})
target_link_libraries(essim_tui
PUBLIC
essim_core
ftxui::screen
ftxui::dom
ftxui::component
)
add_executable(essim "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
target_link_libraries(essim PRIVATE essim_tui)

View File

@@ -1,21 +1,21 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/nets.hpp" #include "core/domain/nets.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/persist.hpp" #include "core/domain/persist.hpp"
#include "system/pin_role.hpp" #include "core/domain/pin_role.hpp"
#include "system/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "system/bsdl_model.hpp" #include "core/domain/bsdl_model.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform.hpp" #include "core/domain/transform.hpp"
#include "system/transform_vpx.hpp" #include "core/domain/transform_vpx.hpp"
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>

View File

@@ -0,0 +1,70 @@
#include "frontends/tui/tui.hpp"
#include "core/app/export.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/system.hpp"
#include <string>
#include <vector>
// Thin UI wrapper around app::export_connections — this file only resolves
// arguments / the file dialog and renders the result. All the actual export
// (CSV / ODS building, file writing) lives in src/app/export.cpp.
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 → the generic file dialog. The CSV/ODS filter rewrites
// the extension; the action below dispatches on it.
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") {
ShowError("export: unknown kind '" + kind + "'\n"
"Known kinds: connections");
return;
}
app::ExportFormat fmt;
if (!app::export_format_from_path(path, fmt)) {
ShowError("export: unknown extension — accepted: .csv, .ods");
return;
}
app::ExportResult r = app::export_connections(sys.get(), path, fmt);
if (!r.ok) {
ShowError("export failed:\n" + r.error);
return;
}
const bool ods = (fmt == app::ExportFormat::Ods);
std::string msg = ods ? "export connections (.ods): "
: "export connections (.csv): ";
if (ods) msg += std::to_string(r.sheets) + " sheet(s), ";
Print(msg + std::to_string(r.rows) + " wire(s) → " + path);
},
/*prompt_for_missing=*/ false,
"export structured data to CSV / ODS (kinds: connections; "
"bare form opens the file-picker dialog)",
/*scriptable=*/ true,
/*interactive=*/ true,
};
}

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <cctype> #include <cctype>
#include <cstdlib> #include <cstdlib>

View File

@@ -1,4 +1,4 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>

View File

@@ -1,15 +1,15 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/nets.hpp" #include "core/domain/nets.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,4 +1,4 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp> #include <ftxui/component/event.hpp>

View File

@@ -1,11 +1,11 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform.hpp" #include "core/domain/transform.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,15 +1,15 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/nets.hpp" #include "core/domain/nets.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/dom/elements.hpp> #include <ftxui/dom/elements.hpp>
@@ -44,6 +44,8 @@ Component Tui::BuildDashboardScreen() {
Element early_help = RenderHelpPanel("dashboard", { Element early_help = RenderHelpPanel("dashboard", {
{"c", "console"}, {"c", "console"},
{"o", "open/run a script"},
{"r", "restore a snapshot"},
{"a", "analyze"}, {"a", "analyze"},
{"h", "help screen"}, {"h", "help screen"},
{"q", "quit"}, {"q", "quit"},
@@ -56,8 +58,8 @@ Component Tui::BuildDashboardScreen() {
separator(), separator(),
hbox({ hbox({
vbox({ vbox({
text(" no system loaded — run 'new' or 'restore <file>'") | dim, text(" no system loaded") | dim,
text(" (press 'c' for the console, or Ctrl-P for the palette)") | dim, text(" (press 'o' open a script · 'r' restore a snapshot · 'c' console · Ctrl-P palette)") | dim,
filler(), filler(),
}) | flex, }) | flex,
separator(), separator(),
@@ -311,6 +313,7 @@ Component Tui::BuildDashboardScreen() {
{"x", "export"}, {"x", "export"},
{"o", "open/run a script"}, {"o", "open/run a script"},
{"s", "save system"}, {"s", "save system"},
{"r", "restore a snapshot"},
{"PgUp", "scroll up"}, {"PgUp", "scroll up"},
{"PgDn", "scroll down"}, {"PgDn", "scroll down"},
{"Home", "scroll top"}, {"Home", "scroll top"},

View File

@@ -1,4 +1,4 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp> #include <ftxui/component/event.hpp>

View File

@@ -1,13 +1,13 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/nets.hpp" #include "core/domain/nets.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>
@@ -54,11 +54,13 @@ void Tui::OpenFileDialog(std::string title,
std::string persist_key, std::string persist_key,
std::string default_filename, std::string default_filename,
std::vector<FilenameFilter> filters, std::vector<FilenameFilter> filters,
std::function<void(const std::string &)> on_confirm) { std::function<void(const std::string &)> on_confirm,
bool confirm_overwrite) {
file_dialog.title = std::move(title); file_dialog.title = std::move(title);
file_dialog.persist_key = std::move(persist_key); file_dialog.persist_key = std::move(persist_key);
file_dialog.on_confirm = std::move(on_confirm); file_dialog.on_confirm = std::move(on_confirm);
file_dialog.filters = std::move(filters); file_dialog.filters = std::move(filters);
file_dialog.confirm_overwrite = confirm_overwrite;
file_dialog.filter_labels.clear(); file_dialog.filter_labels.clear();
for (const auto &f : file_dialog.filters) for (const auto &f : file_dialog.filters)
file_dialog.filter_labels.push_back(f.label); file_dialog.filter_labels.push_back(f.label);
@@ -122,7 +124,7 @@ void Tui::ConfirmFileDialog() {
// Overwrite guard: if the file already exists, ask before letting // Overwrite guard: if the file already exists, ask before letting
// the action proceed. Esc / No cancels; Yes runs the action. // the action proceed. Esc / No cancels; Yes runs the action.
std::error_code ec; std::error_code ec;
if (fs::exists(full, ec) && !ec) { if (file_dialog.confirm_overwrite && fs::exists(full, ec) && !ec) {
ShowConfirm("File '" + full.string() + "' already exists.\n" ShowConfirm("File '" + full.string() + "' already exists.\n"
"Overwrite?", "Overwrite?",
invoke); invoke);

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>
@@ -89,18 +89,8 @@ Component Tui::BuildMainScreen(ScreenInteractive &screen) {
}) | flex, }) | flex,
}) | border; }) | border;
if (loading) { // The "Computing…" overlay is rendered globally in Run(), so it shows
int total = (int)loading_lines.size(); // on whatever screen is active while a script loads.
std::string progress = std::to_string(loading_executed) + " / "
+ std::to_string(total) + " lines";
auto modal = vbox({
text(" Computing… ") | bold | center,
separator(),
text(loading_filename) | center,
text(progress) | center,
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
return dbox({base, modal | center});
}
return base; return base;
}); });
} }

View File

@@ -1,9 +1,9 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,13 +1,13 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "system/pin_role.hpp" #include "core/domain/pin_role.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform_vpx.hpp" #include "core/domain/transform_vpx.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,8 +1,8 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,9 +1,9 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <cctype> #include <cctype>
#include <chrono> #include <chrono>
@@ -296,8 +296,10 @@ void Tui::Source(const std::string &filename) {
loading_executed = 0; loading_executed = 0;
loading_lineno = 0; loading_lineno = 0;
loading_prev_in_source = in_source; loading_prev_in_source = in_source;
source_origin_screen = screen_idx; // a sourced line that leaves this screen aborts
in_source = true; in_source = true;
loading = true; loading = true;
computing_open = true; // raise the global "Computing…" progress modal
if (!screen_ptr) { if (!screen_ptr) {
// Headless fallback (e.g. tests): drain synchronously. // Headless fallback (e.g. tests): drain synchronously.
@@ -346,11 +348,12 @@ void Tui::ProcessNextSourceLine() {
Submit(); Submit();
++loading_executed; ++loading_executed;
if (screen_idx != 0) { if (screen_idx != source_origin_screen) {
Print("source: line " + std::to_string(loading_lineno) Print("source: line " + std::to_string(loading_lineno)
+ " is interactive (would open a screen) — aborting."); + " is interactive (would open a screen) — aborting.");
screen_idx = 0; screen_idx = source_origin_screen;
loading.store(false); loading.store(false);
computing_open = false;
tick_in_flight.store(false); tick_in_flight.store(false);
in_source = loading_prev_in_source; in_source = loading_prev_in_source;
return; return;
@@ -363,6 +366,7 @@ void Tui::ProcessNextSourceLine() {
Print("source: " + loading_filename Print("source: " + loading_filename
+ " (" + std::to_string(loading_executed) + " line(s))"); + " (" + std::to_string(loading_executed) + " line(s))");
loading.store(false); loading.store(false);
computing_open = false;
tick_in_flight.store(false); tick_in_flight.store(false);
in_source = loading_prev_in_source; in_source = loading_prev_in_source;
} }

View File

@@ -1,10 +1,11 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp> #include <ftxui/component/event.hpp>
#include <ftxui/component/screen_interactive.hpp> #include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/elements.hpp>
using namespace ftxui; using namespace ftxui;
@@ -58,10 +59,31 @@ void Tui::Run() {
auto with_error = with_confirm auto with_error = with_confirm
| Modal(BuildErrorModal(), &error_open); | Modal(BuildErrorModal(), &error_open);
auto root = CatchEvent(with_error, [this](Event e) { // Global "Computing…" progress modal while a script loads — a proper Modal
// (like the palette / file dialog), so it shows on any screen, e.g. when a
// script is opened from the dashboard. The Renderer re-reads the live
// progress every frame.
auto computing_modal = Renderer([this] {
std::string progress = std::to_string(loading_executed) + " / "
+ std::to_string((int)loading_lines.size()) + " lines";
return vbox({
text(" Computing… ") | bold | center,
separator(),
text(loading_filename) | center,
text(progress) | center,
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
});
auto with_loading = with_error | Modal(computing_modal, &computing_open);
auto root = CatchEvent(with_loading, [this](Event e) {
// Source ticks drive the line-by-line loader; handle them on ANY screen
// (and before the modal guard) so `source` keeps running while the
// Computing modal is up.
if (e == Event::Special("\x02tick")) { ProcessNextSourceLine(); return true; }
// Modals own their events while open. Error modal sits on top. // Modals own their events while open. Error modal sits on top.
if (error_open || confirm_open || palette_open if (error_open || confirm_open || palette_open
|| sigtype_dialog_open || file_dialog.open) return false; || sigtype_dialog_open || file_dialog.open || computing_open) return false;
// Ctrl-P opens the palette from any screen. // Ctrl-P opens the palette from any screen.
if (e == Event::CtrlP) { OpenPalette(); return true; } if (e == Event::CtrlP) { OpenPalette(); return true; }
@@ -106,7 +128,8 @@ void Tui::Run() {
"dashboard.source", "", {}, "dashboard.source", "", {},
[this](const std::string &path) { [this](const std::string &path) {
Dispatch("source " + path); Dispatch("source " + path);
}); },
/*confirm_overwrite=*/false); // opening, not saving
return true; return true;
} }
if (e == Event::Character("s")) { // save the system snapshot if (e == Event::Character("s")) { // save the system snapshot
@@ -118,6 +141,15 @@ void Tui::Run() {
}); });
return true; return true;
} }
if (e == Event::Character("r")) { // restore a saved system
OpenFileDialog("Restore system — load a snapshot",
"dashboard.restore", "", {},
[this](const std::string &path) {
Dispatch("restore " + path);
},
/*confirm_overwrite=*/false); // opening, not saving
return true;
}
return false; return false;
case 3: // explore case 3: // explore
@@ -152,7 +184,6 @@ void Tui::Run() {
return false; return false;
default: // 0: main (console / log view) default: // 0: main (console / log view)
if (e == Event::Special("\x02tick")) { ProcessNextSourceLine(); return true; }
if (e == Event::Escape) { if (e == Event::Escape) {
if (!pending.empty()) { CancelPending(); return true; } if (!pending.empty()) { CancelPending(); return true; }
screen_idx = 4; return true; screen_idx = 4; return true;

View File

@@ -103,6 +103,8 @@ class Tui {
int loading_executed; int loading_executed;
int loading_lineno; int loading_lineno;
bool loading_prev_in_source; bool loading_prev_in_source;
int source_origin_screen = 0; ///< screen a `source` started from; a sourced line that navigates away (opens an interactive screen) aborts it.
bool computing_open = false; ///< drives the global "Computing…" progress modal while a script loads.
ftxui::ScreenInteractive *screen_ptr; ///< set in Run() so Source() can post events. ftxui::ScreenInteractive *screen_ptr; ///< set in Run() so Source() can post events.
// ---- Dashboard scroll state (0 = top; grows as the user scrolls down) ---- // ---- Dashboard scroll state (0 = top; grows as the user scrolls down) ----
@@ -149,6 +151,7 @@ private:
std::vector<std::string> filter_labels; ///< parallel to `filters` std::vector<std::string> filter_labels; ///< parallel to `filters`
int filter_idx = 0; int filter_idx = 0;
std::function<void(const std::string &)> on_confirm; std::function<void(const std::string &)> on_confirm;
bool confirm_overwrite = true; ///< false in "open" mode — skip the overwrite prompt.
}; };
FileDialogState file_dialog; FileDialogState file_dialog;
@@ -272,11 +275,14 @@ private:
// dir + filename are stored (one tiny file per key under the user // dir + filename are stored (one tiny file per key under the user
// data directory). `on_confirm` runs when the user presses Enter on // data directory). `on_confirm` runs when the user presses Enter on
// the action button — it receives the absolute path the user picked. // the action button — it receives the absolute path the user picked.
// `confirm_overwrite = false` puts the dialog in "open" mode: it skips the
// "file exists — overwrite?" prompt (you *want* an existing file to open).
void OpenFileDialog(std::string title, void OpenFileDialog(std::string title,
std::string persist_key, std::string persist_key,
std::string default_filename, std::string default_filename,
std::vector<FilenameFilter> filters, std::vector<FilenameFilter> filters,
std::function<void(const std::string &)> on_confirm); std::function<void(const std::string &)> on_confirm,
bool confirm_overwrite = true);
void ConfirmFileDialog(); void ConfirmFileDialog();
ftxui::Component BuildSignalTypeModal(); ftxui::Component BuildSignalTypeModal();
ftxui::Component BuildPaletteModal(); ftxui::Component BuildPaletteModal();

View File

@@ -1,4 +1,4 @@
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/dom/elements.hpp> #include <ftxui/dom/elements.hpp>

View File

@@ -1,235 +0,0 @@
#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,
// 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 = '_';
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[] = {
"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);
// `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;
}
}
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;
}
// 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,"
"type_mismatch\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 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;
}
}
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,
};
}

View File

@@ -1,11 +1,11 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <memory> #include <memory>
#include <string> #include <string>

View File

@@ -1,14 +1,14 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/bsdl_model.hpp" #include "core/domain/bsdl_model.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/pin_spec.hpp" #include "core/domain/pin_spec.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/persist.hpp" #include "core/domain/persist.hpp"
#include <cstdio> #include <cstdio>
#include <fstream> #include <fstream>

View File

@@ -1,13 +1,13 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_spec.hpp" #include "core/domain/pin_spec.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>

View File

@@ -1,7 +1,7 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/component_kind.hpp" #include "core/domain/component_kind.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include <memory> #include <memory>

76
tests/test_export.cpp Normal file
View File

@@ -0,0 +1,76 @@
#include <doctest/doctest.h>
#include "core/app/export.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
namespace {
std::string slurp(const std::string &path)
{
std::ifstream f(path);
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}
} // namespace
TEST_CASE("export_format_from_path maps extensions") {
app::ExportFormat f;
CHECK(app::export_format_from_path("a.csv", f));
CHECK(f == app::ExportFormat::Csv);
CHECK(app::export_format_from_path("a.ODS", f));
CHECK(f == app::ExportFormat::Ods);
CHECK_FALSE(app::export_format_from_path("a.txt", f));
CHECK_FALSE(app::export_format_from_path("noext", f));
}
TEST_CASE("export_connections writes a flat CSV (no UI needed)") {
// Two cards, one wired pin pair via a connection.
System sys;
Module *a = sys.modules()->merge("A");
Module *b = sys.modules()->merge("B");
Part *ja = new Part("J1"); a->add(ja);
Part *jb = new Part("P1"); b->add(jb);
Pin *pa = new Pin("1"); ja->add(pa);
Pin *pb = new Pin("1"); jb->add(pb);
Signal *sa = a->signals->merge("NETA"); sa->add(pa); pa->connect(sa);
Signal *sb = b->signals->merge("NETB"); sb->add(pb); pb->connect(sb);
Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb);
c->transform_name = "identity";
c->pin_map.emplace_back(pa, pb);
sys.connections()->add(c);
const char *path = "test_export_out.csv";
app::ExportResult r = app::export_connections(&sys, path, app::ExportFormat::Csv);
CHECK(r.ok);
CHECK(r.rows == 1);
std::string out = slurp(path);
CHECK(out.find("connection,transform,") == 0); // header present
CHECK(out.find("A.J1<->B.P1") != std::string::npos); // connection name
CHECK(out.find("identity") != std::string::npos); // transform
CHECK(out.find("NETA") != std::string::npos); // left signal
CHECK(out.find("NETB") != std::string::npos); // right signal
std::remove(path);
}
TEST_CASE("export_connections reports a bad path instead of crashing") {
System sys;
app::ExportResult r = app::export_connections(
&sys, "/nonexistent-dir-xyz/out.csv", app::ExportFormat::Csv);
CHECK_FALSE(r.ok);
CHECK_FALSE(r.error.empty());
}

View File

@@ -1,11 +1,11 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/persist.hpp" #include "core/domain/persist.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>

View File

@@ -1,12 +1,12 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/persist.hpp" #include "core/domain/persist.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>

View File

@@ -1,9 +1,9 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "system/pin_spec.hpp" #include "core/domain/pin_spec.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include <string> #include <string>
#include <vector> #include <vector>

View File

@@ -1,9 +1,9 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_name.hpp" #include "core/domain/pin_name.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/transform.hpp" #include "core/domain/transform.hpp"
#include <memory> #include <memory>

View File

@@ -1,6 +1,6 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/signal_type.hpp" #include "core/domain/signal_type.hpp"
TEST_CASE("signal_type_name round-trips with from_name") { TEST_CASE("signal_type_name round-trips with from_name") {
SignalType t; SignalType t;

View File

@@ -1,12 +1,12 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform.hpp" #include "core/domain/transform.hpp"
#include "system/transform_vpx.hpp" #include "core/domain/transform_vpx.hpp"
#include <map> #include <map>
#include <memory> #include <memory>

View File

@@ -1,6 +1,6 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>