Compare commits
7 Commits
60c00eb914
...
separate-c
| Author | SHA1 | Date | |
|---|---|---|---|
| 63ca17d048 | |||
| 3010bb25eb | |||
| ac2edd90c4 | |||
| 53eb79c760 | |||
| 29cb353d75 | |||
| c70e767cf1 | |||
| 527a48145b |
@@ -11,49 +11,54 @@ project(essim
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
# ----------------------------------------------------------------- core deps
|
||||
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2,
|
||||
# which the LGPL permits). Override its path with -DBSDL_DIR=...
|
||||
set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree")
|
||||
set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE)
|
||||
set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl)
|
||||
|
||||
# Library target = everything except main.cpp; reused by `essim` and `essim_tests`.
|
||||
file(GLOB_RECURSE LIB_SOURCES "src/*.cpp")
|
||||
list(REMOVE_ITEM LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp")
|
||||
find_package(libzip REQUIRED)
|
||||
find_package(pugixml REQUIRED)
|
||||
|
||||
add_library(essim_lib STATIC ${LIB_SOURCES})
|
||||
target_include_directories(essim_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
target_link_libraries(essim_lib
|
||||
# =============================================================== essim_core
|
||||
# All business logic — domain model, importers, application operations
|
||||
# (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
|
||||
ftxui::screen
|
||||
ftxui::dom
|
||||
ftxui::component
|
||||
libzip::zip
|
||||
pugixml::pugixml
|
||||
bsdl::bsdl
|
||||
)
|
||||
|
||||
add_executable(essim src/main.cpp)
|
||||
target_link_libraries(essim PRIVATE essim_lib)
|
||||
# =============================================================== frontend(s)
|
||||
# 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)
|
||||
if(BUILD_TESTING)
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
||||
@@ -65,20 +70,35 @@ if(BUILD_TESTING)
|
||||
FetchContent_MakeAvailable(doctest)
|
||||
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")
|
||||
if(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)
|
||||
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()
|
||||
|
||||
# Documentation: Doxygen → XML → custom Python script → doc/api/ (Markdown rendered by gitea).
|
||||
# Optional — `doc` target is only created if Doxygen and Python 3 are present.
|
||||
# =============================================================== documentation
|
||||
# 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(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")
|
||||
file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}")
|
||||
configure_file(
|
||||
@@ -103,11 +123,10 @@ if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)"
|
||||
VERBATIM)
|
||||
elseif(NOT DOXYGEN_FOUND AND NOT Python3_Interpreter_FOUND)
|
||||
message(STATUS "doc: Doxygen and Python 3 not found — `doc` target disabled.")
|
||||
elseif(NOT TARGET essim)
|
||||
message(STATUS "doc: no `essim` binary (ESSIM_FRONTEND=none) — `doc` target disabled.")
|
||||
elseif(NOT DOXYGEN_FOUND)
|
||||
message(STATUS "doc: Doxygen not found — `doc` target disabled "
|
||||
"(install via `pacman -S doxygen`).")
|
||||
message(STATUS "doc: Doxygen not found — `doc` target disabled.")
|
||||
else()
|
||||
message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.")
|
||||
endif()
|
||||
|
||||
190
src/core/app/export.cpp
Normal file
190
src/core/app/export.cpp
Normal 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
34
src/core/app/export.hpp
Normal 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_
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "system/component_kind.hpp"
|
||||
#include "core/domain/component_kind.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "system/nets.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.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 <queue>
|
||||
#include <unordered_map>
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "system/pin_name.hpp"
|
||||
#include "core/domain/pin_name.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
#include "connect.hpp"
|
||||
#include "modules.hpp"
|
||||
#include "imports/import_altium.hpp"
|
||||
#include "imports/import_mentor.hpp"
|
||||
#include "imports/import_ods.hpp"
|
||||
#include "core/imports/import_altium.hpp"
|
||||
#include "core/imports/import_mentor.hpp"
|
||||
#include "core/imports/import_ods.hpp"
|
||||
|
||||
System::System() : mods(nullptr), conns(nullptr)
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef _SYSTEM_HPP_
|
||||
#define _SYSTEM_HPP_
|
||||
|
||||
#include "imports/import_base.hpp"
|
||||
#include "core/imports/import_base.hpp"
|
||||
|
||||
#pragma once
|
||||
class Modules; ///< Forward declaration of the Modules class.
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "import_altium.hpp"
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
@@ -4,8 +4,8 @@
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
|
||||
/**
|
||||
* @brief Base class for importing data from a file.
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "import_mentor.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <vector>
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "import_ods.hpp"
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
#include <zip.h>
|
||||
35
src/frontends/tui/CMakeLists.txt
Normal file
35
src/frontends/tui/CMakeLists.txt
Normal 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)
|
||||
@@ -1,21 +1,21 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/persist.hpp"
|
||||
#include "system/pin_role.hpp"
|
||||
#include "system/pin_model.hpp"
|
||||
#include "system/bsdl_model.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform.hpp"
|
||||
#include "system/transform_vpx.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
#include "core/domain/pin_role.hpp"
|
||||
#include "core/domain/pin_model.hpp"
|
||||
#include "core/domain/bsdl_model.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/transform.hpp"
|
||||
#include "core/domain/transform_vpx.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
70
src/frontends/tui/commands_export.cpp
Normal file
70
src/frontends/tui/commands_export.cpp
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -1,15 +1,15 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/event.hpp>
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/transform.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,15 +1,15 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
@@ -44,6 +44,8 @@ Component Tui::BuildDashboardScreen() {
|
||||
|
||||
Element early_help = RenderHelpPanel("dashboard", {
|
||||
{"c", "console"},
|
||||
{"o", "open/run a script"},
|
||||
{"r", "restore a snapshot"},
|
||||
{"a", "analyze"},
|
||||
{"h", "help screen"},
|
||||
{"q", "quit"},
|
||||
@@ -56,8 +58,8 @@ Component Tui::BuildDashboardScreen() {
|
||||
separator(),
|
||||
hbox({
|
||||
vbox({
|
||||
text(" no system loaded — run 'new' or 'restore <file>'") | dim,
|
||||
text(" (press 'c' for the console, or Ctrl-P for the palette)") | dim,
|
||||
text(" no system loaded") | dim,
|
||||
text(" (press 'o' open a script · 'r' restore a snapshot · 'c' console · Ctrl-P palette)") | dim,
|
||||
filler(),
|
||||
}) | flex,
|
||||
separator(),
|
||||
@@ -311,6 +313,7 @@ Component Tui::BuildDashboardScreen() {
|
||||
{"x", "export"},
|
||||
{"o", "open/run a script"},
|
||||
{"s", "save system"},
|
||||
{"r", "restore a snapshot"},
|
||||
{"PgUp", "scroll up"},
|
||||
{"PgDn", "scroll down"},
|
||||
{"Home", "scroll top"},
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/event.hpp>
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -54,11 +54,13 @@ void Tui::OpenFileDialog(std::string title,
|
||||
std::string persist_key,
|
||||
std::string default_filename,
|
||||
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.persist_key = std::move(persist_key);
|
||||
file_dialog.on_confirm = std::move(on_confirm);
|
||||
file_dialog.filters = std::move(filters);
|
||||
file_dialog.confirm_overwrite = confirm_overwrite;
|
||||
file_dialog.filter_labels.clear();
|
||||
for (const auto &f : file_dialog.filters)
|
||||
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
|
||||
// the action proceed. Esc / No cancels; Yes runs the action.
|
||||
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"
|
||||
"Overwrite?",
|
||||
invoke);
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -89,18 +89,8 @@ Component Tui::BuildMainScreen(ScreenInteractive &screen) {
|
||||
}) | flex,
|
||||
}) | border;
|
||||
|
||||
if (loading) {
|
||||
int total = (int)loading_lines.size();
|
||||
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});
|
||||
}
|
||||
// The "Computing…" overlay is rendered globally in Run(), so it shows
|
||||
// on whatever screen is active while a script loads.
|
||||
return base;
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pin_model.hpp"
|
||||
#include "system/pin_role.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform_vpx.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_model.hpp"
|
||||
#include "core/domain/pin_role.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/transform_vpx.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
@@ -296,8 +296,10 @@ void Tui::Source(const std::string &filename) {
|
||||
loading_executed = 0;
|
||||
loading_lineno = 0;
|
||||
loading_prev_in_source = in_source;
|
||||
source_origin_screen = screen_idx; // a sourced line that leaves this screen aborts
|
||||
in_source = true;
|
||||
loading = true;
|
||||
computing_open = true; // raise the global "Computing…" progress modal
|
||||
|
||||
if (!screen_ptr) {
|
||||
// Headless fallback (e.g. tests): drain synchronously.
|
||||
@@ -346,11 +348,12 @@ void Tui::ProcessNextSourceLine() {
|
||||
Submit();
|
||||
++loading_executed;
|
||||
|
||||
if (screen_idx != 0) {
|
||||
if (screen_idx != source_origin_screen) {
|
||||
Print("source: line " + std::to_string(loading_lineno)
|
||||
+ " is interactive (would open a screen) — aborting.");
|
||||
screen_idx = 0;
|
||||
screen_idx = source_origin_screen;
|
||||
loading.store(false);
|
||||
computing_open = false;
|
||||
tick_in_flight.store(false);
|
||||
in_source = loading_prev_in_source;
|
||||
return;
|
||||
@@ -363,6 +366,7 @@ void Tui::ProcessNextSourceLine() {
|
||||
Print("source: " + loading_filename
|
||||
+ " (" + std::to_string(loading_executed) + " line(s))");
|
||||
loading.store(false);
|
||||
computing_open = false;
|
||||
tick_in_flight.store(false);
|
||||
in_source = loading_prev_in_source;
|
||||
}
|
||||
@@ -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/event.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
@@ -58,10 +59,31 @@ void Tui::Run() {
|
||||
auto with_error = with_confirm
|
||||
| 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.
|
||||
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.
|
||||
if (e == Event::CtrlP) { OpenPalette(); return true; }
|
||||
@@ -106,7 +128,8 @@ void Tui::Run() {
|
||||
"dashboard.source", "", {},
|
||||
[this](const std::string &path) {
|
||||
Dispatch("source " + path);
|
||||
});
|
||||
},
|
||||
/*confirm_overwrite=*/false); // opening, not saving
|
||||
return true;
|
||||
}
|
||||
if (e == Event::Character("s")) { // save the system snapshot
|
||||
@@ -118,6 +141,15 @@ void Tui::Run() {
|
||||
});
|
||||
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;
|
||||
|
||||
case 3: // explore
|
||||
@@ -152,7 +184,6 @@ void Tui::Run() {
|
||||
return false;
|
||||
|
||||
default: // 0: main (console / log view)
|
||||
if (e == Event::Special("\x02tick")) { ProcessNextSourceLine(); return true; }
|
||||
if (e == Event::Escape) {
|
||||
if (!pending.empty()) { CancelPending(); return true; }
|
||||
screen_idx = 4; return true;
|
||||
@@ -103,6 +103,8 @@ class Tui {
|
||||
int loading_executed;
|
||||
int loading_lineno;
|
||||
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.
|
||||
|
||||
// ---- 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`
|
||||
int filter_idx = 0;
|
||||
std::function<void(const std::string &)> on_confirm;
|
||||
bool confirm_overwrite = true; ///< false in "open" mode — skip the overwrite prompt.
|
||||
};
|
||||
FileDialogState file_dialog;
|
||||
|
||||
@@ -272,11 +275,14 @@ private:
|
||||
// dir + filename are stored (one tiny file per key under the user
|
||||
// data directory). `on_confirm` runs when the user presses Enter on
|
||||
// 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,
|
||||
std::string persist_key,
|
||||
std::string default_filename,
|
||||
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();
|
||||
ftxui::Component BuildSignalTypeModal();
|
||||
ftxui::Component BuildPaletteModal();
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/analysis.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 <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/bsdl_model.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/pin_spec.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/persist.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/bsdl_model.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/pin_spec.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pin_spec.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_spec.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/component_kind.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "core/domain/component_kind.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
||||
76
tests/test_export.cpp
Normal file
76
tests/test_export.cpp
Normal 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());
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/persist.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/persist.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pin_model.hpp"
|
||||
#include "system/pin_spec.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_model.hpp"
|
||||
#include "core/domain/pin_spec.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pin_name.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/transform.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_name.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/transform.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#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") {
|
||||
SignalType t;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform.hpp"
|
||||
#include "system/transform_vpx.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 "core/domain/transform.hpp"
|
||||
#include "core/domain/transform_vpx.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
Reference in New Issue
Block a user