ODS import, persistence, scripting, connector types + VPX transforms.

- ODS importer (libzip + pugixml): each sheet → Part, rows → Pin/Signal.
- save / restore commands: tab-delimited snapshot of modules, parts,
  signals, connections + pin_map. `restore` replaces the System.
- source / script-save: replay a file of commands; record canonical
  commands since last `new` for replay later. Interactive screens
  refused during source. `explore` marked non-scriptable.
- TUI screen_explore: 4 columns (modules, type, children, detail) with
  filters on children and detail; detail is a Menu so arrows scroll
  long pin lists.
- Connector types & transforms: each Part carries a `connector_type`
  string. `set-type` validates the part's pin layout against the type
  (cols set check). `connect` strict pair: rejects when lookup falls
  back to identity unless types are both empty AND pin sets match.
- VPX 3U transforms: 3 registered pairs (vpx-3u-bkp-pN ↔ vpx-3u-payload-pN,
  N=0/1/2) with row-pattern correspondence tables ported from the user's
  Python reference.
- Code split for maintainability: src/tui/{shell,completion,commands,
  screen_main,screen_search,screen_connect,screen_settype,screen_explore,
  tui_helpers}.cpp.
- Bug fixes: Module::add(Part*) override sets part->prnt (was always
  null, breaking save's W lines). Defensive guards in explore against
  empty Menu lists. Renderer wrapped in try/catch so domain throws
  surface as on-screen errors instead of SIGABRT.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 19:58:51 +02:00
parent 3395469810
commit f3920964f0
31 changed files with 2369 additions and 467 deletions

View File

@@ -12,6 +12,7 @@ cmake --build build -j
- CMake **3.14+** required (uses `FetchContent_MakeAvailable`). - CMake **3.14+** required (uses `FetchContent_MakeAvailable`).
- FTXUI is fetched at configure time from GitHub (`v6.1.9`, shallow clone). First configure pays ~20 s for the clone; subsequent ones are cached in `build/_deps/`. - FTXUI is fetched at configure time from GitHub (`v6.1.9`, shallow clone). First configure pays ~20 s for the clone; subsequent ones are cached in `build/_deps/`.
- **System dependencies** (resolved via `find_package`): `libzip` (target `libzip::zip`) and `pugixml` (target `pugixml::pugixml`). Used by the ODS importer. Available on Arch via `pacman -S libzip pugixml`.
- Sources are collected with `file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")`. **After adding a new `.cpp`, re-run `cmake -S . -B build`** — CMake does not re-glob automatically and link will fail with "undefined reference". - Sources are collected with `file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")`. **After adding a new `.cpp`, re-run `cmake -S . -B build`** — CMake does not re-glob automatically and link will fail with "undefined reference".
## Layout ## Layout
@@ -31,8 +32,17 @@ src/
imports/ -- adapters that populate the domain imports/ -- adapters that populate the domain
import_base.hpp ImportBase interface import_base.hpp ImportBase interface
import_mentor.{hpp,cpp} Mentor Graphics netlist parser (done) import_mentor.{hpp,cpp} Mentor Graphics netlist parser (done)
tui/ -- FTXUI shell tui/ -- FTXUI shell, split by responsibility
tui.{hpp,cpp} Tui: visualisation area on top, input + history at the bottom tui.{hpp,cpp} Class Tui (state + Run() orchestrator + screen-mode event dispatcher)
tui_helpers.{hpp,cpp} Free helpers: ToLower, NaturalLess, LongestCommonPrefix
shell.cpp Print, Submit, Dispatch, Finalize, Tokenize, history persistence
completion.cpp CompleteCommand, CompletePath, CompleteInline
commands.cpp RegisterCommands (all built-in commands declared here)
screen_main.cpp BuildMainScreen (visualisation area + bottom input)
screen_search.cpp BuildSearchScreen
screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper
screen_settype.cpp BuildSettypeScreen
screen_explore.cpp BuildExploreScreen (browse modules → parts/signals/connections → details; not scriptable)
doc/classes.puml -- PlantUML class diagram doc/classes.puml -- PlantUML class diagram
``` ```
@@ -54,12 +64,36 @@ doc/classes.puml -- PlantUML class diagram
- `CatchEvent` is wrapped **outside** the `Renderer` (not between `Renderer` and `Input`). Pattern: `Renderer(input_component, lambda)` then `CatchEvent(renderer, handler)`. Wrapping `CatchEvent(input, …)` inside a `Container::Vertical({…})` and then `Renderer(container, …)` was found to break the `Input`'s content rendering on at least one terminal — typed characters didn't show even though `on_enter` fired correctly. - `CatchEvent` is wrapped **outside** the `Renderer` (not between `Renderer` and `Input`). Pattern: `Renderer(input_component, lambda)` then `CatchEvent(renderer, handler)`. Wrapping `CatchEvent(input, …)` inside a `Container::Vertical({…})` and then `Renderer(container, …)` was found to break the `Input`'s content rendering on at least one terminal — typed characters didn't show even though `on_enter` fired correctly.
- `InputOption::transform` is overridden to avoid hard-coded colors (default sets `White on Black` when focused, which is unreadable on light terminal themes). The custom transform only applies `dim` to the placeholder; everything else inherits terminal default fg/bg, so the UI adapts to any theme. - `InputOption::transform` is overridden to avoid hard-coded colors (default sets `White on Black` when focused, which is unreadable on light terminal themes). The custom transform only applies `dim` to the placeholder; everything else inherits terminal default fg/bg, so the UI adapts to any theme.
- `InputOption::multiline` **must** be set to `false` for the command prompt. Default is `true`, and FTXUI's `HandleReturn()` then both inserts `\n` into the content **and** fires `on_enter` — so `Submit()` would receive `"help\n"` instead of `"help"` and command lookups would all fall through to "unknown command". - `InputOption::multiline` **must** be set to `false` for the command prompt. Default is `true`, and FTXUI's `HandleReturn()` then both inserts `\n` into the content **and** fires `on_enter` — so `Submit()` would receive `"help\n"` instead of `"help"` and command lookups would all fall through to "unknown command".
- Commands live in a `std::map<std::string, CommandSpec>` registry built in `RegisterCommands()`. Each `CommandSpec` declares a list of `Param{name, path_completion}` and an `action(args)` lambda. `Dispatch(raw)` tokenises the input (whitespace split with `"…"` quoting), takes inline args first, and pushes a `Prompt` onto the queue for each missing param. The last prompt's callback calls `Finalize()`. - Commands live in a `std::map<std::string, CommandSpec>` registry built in `RegisterCommands()`. Each `CommandSpec` carries `params` (list of `Param{name, completion}` where `completion` is the `Tui::Completion` enum: `None | Path | Command`), an `action(args)` lambda, a `prompt_for_missing` bool (default `true`), a one-line `description` string, and a `scriptable` bool (default `true`). Setting `scriptable = false` (e.g. `explore`) excludes the command from the script-save buffer; if a non-scriptable command is invoked from a sourced file the `Source` loop still aborts via the `screen_idx != 0` check, since these commands are typically screen-openers. `Dispatch(raw)` tokenises the input (whitespace split with `"…"` quoting), takes inline args first, and (only when `prompt_for_missing`) pushes a `Prompt` onto the queue for each missing param. The last prompt's callback calls `Finalize()`. Set `prompt_for_missing = false` for commands that accept either zero args or a full arg list (e.g. `search`: bare → interactive, full → inline) — the action gets called immediately with whatever it received and decides what to do.
- `Tab` completes by `Param::completion`: `Path` triggers `CompletePath()` (filesystem listing with `~/` expansion), `Command` triggers `CompleteCommand()` (matches against the registry), `None` does nothing. Both work inline (`CompleteInline()` figures out the arg position) and inside a multi-step prompt.
- `help` (no args) iterates the registry and prints `name — description` for every command. `help <command>` describes a single command, including each `Param`'s name. The argument has `Completion::Command`, so `help <Tab>` lists registered commands.
- `Finalize()` rebuilds the **canonical inline form** (`name arg1 "arg with spaces" arg3`) and writes that to history — so a `load` command answered via interactive prompts still shows up in `history` (and on disk) as a single inline line, ready to be replayed via ↑. - `Finalize()` rebuilds the **canonical inline form** (`name arg1 "arg with spaces" arg3`) and writes that to history — so a `load` command answered via interactive prompts still shows up in `history` (and on disk) as a single inline line, ready to be replayed via ↑.
- Multi-step prompts work via a `std::deque<Prompt>` queue. `Submit()` pops them one by one before falling back to dispatch. Adding a new command = one entry in `RegisterCommands()`; the prompt-flow and inline-flow are both handled automatically. - Multi-step prompts work via a `std::deque<Prompt>` queue. `Submit()` pops them one by one before falling back to dispatch. Adding a new command = one entry in `RegisterCommands()`; the prompt-flow and inline-flow are both handled automatically.
- Tab completion: at the top-level prompt (no `pending`), completes built-in command names. Inside a prompt with `path_completion = true` (e.g. the `filename` step of `load`), completes file paths via `std::filesystem::directory_iterator` (handles `~/`, dirs get a trailing `/`). Logic: 1 match → replace; multiple with progress on the longest common prefix → extend; multiple stuck at LCP → list candidates in the visualisation area. - Tab completion: at the top-level prompt (no `pending`), completes built-in command names. Inside a prompt with `path_completion = true` (e.g. the `filename` step of `load`), completes file paths via `std::filesystem::directory_iterator` (handles `~/`, dirs get a trailing `/`). Logic: 1 match → replace; multiple with progress on the longest common prefix → extend; multiple stuck at LCP → list candidates in the visualisation area.
Built-in commands: `new`, `load`, `search`, `clear`, `help`, `quit`/`exit`. `Esc` cancels an in-progress multi-step prompt. Built-in commands: `new`, `load`, `save`, `restore`, `source`, `script-save`, `connect`, `set-type`, `search`, `explore`, `clear`, `help`, `quit`/`exit`. `Esc` cancels an in-progress multi-step prompt.
`script-save <file>` writes a replay-ready script of every command issued since the last `new` (the `recorded` buffer is cleared inside the `new` action). The buffer is appended to inside `Finalize()` after the action runs, with a denylist of commands that aren't useful in a replay: `clear`, `help`, `quit`, `exit`, `source`, `script-save`. Note the `source` exclusion: when a script is sourced, the *individual lines inside* the script are recorded (because each goes through `Finalize`), so the saved script reproduces the same end state without the indirection.
`source <file>` reads a script line by line and feeds each line through `Submit()`. While the script is running, `in_source = true` is set on the `Tui` and:
- `Dispatch` / `Finalize` skip writing to memory + on-disk history.
- After each `Submit`, if `screen_idx != 0` (a screen was opened by an "interactive" command like bare `connect`/`search`/`set-type`), the script is aborted with an error message and `screen_idx` is reset to 0 — interactive screen-opening commands are explicitly disallowed in scripts.
Pending prompts (from incomplete inline commands) are NOT considered interactive and are filled by subsequent script lines, the way you'd expect. Lines starting with `#` and blank lines are skipped; leading/trailing whitespace is trimmed; `~/` is expanded.
`save` / `restore` (`src/system/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `N` (pin → signal name; empty = NC), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). `restore` replaces `Tui::sys` entirely (`unique_ptr::reset`). Names are stored as-is — must not contain TAB or newline (true for the EE netlists we ingest). Format is versioned by the `# essim system snapshot v1` header for future compatibility.
**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-type` command — inline `set-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/system/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the same-name pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear.
`screen_idx` mapping: 0 = main TUI, 1 = search, 2 = connect, 3 = set-type. Adding a new screen mode is the same recipe each time: state members, `Container::Vertical` of focusable components, a `Renderer` lambda that recomputes derived state per frame (e.g. filtered part lists), an entry in `Container::Tab`, and Tab/Esc handling in the outer `CatchEvent`.
`connect` is dual-mode (`prompt_for_missing = false`):
- Inline: `connect m1 p1 m2 p2`. Each part argument is resolved as exact name first, then case-insensitive substring; ambiguous → lists candidates and aborts.
- Bare: opens a dedicated full-screen layout (`screen_idx = 2`) with two columns (`endpoint 1` / `endpoint 2`). Each column has a module `Menu`, a part filter `Input`, and a filtered part `Menu`. A `Button(" Connect ")` at the bottom commits. `Tab` cycles focus across the 7 components, `Esc` leaves.
The connect screen rebuilds the filtered part lists inside the `Renderer` lambda each frame (cheap; lets the part menus react live to module changes and filter typing). Selection indices are clamped if a list shrinks below the previous index.
The Connection record stores `Module*`/`Part*` for both endpoints (added to `Connection` for this — minimal struct fields, no behaviour change).
`search` switches to a second full-screen layout (handled by `Container::Tab({main, search}, &screen_idx)`). Layout: `search` switches to a second full-screen layout (handled by `Container::Tab({main, search}, &screen_idx)`). Layout:
- Left column: `Menu` for the module list and `Menu` for the type (`parts` / `signals`). - Left column: `Menu` for the module list and `Menu` for the type (`parts` / `signals`).
@@ -67,7 +101,7 @@ Built-in commands: `new`, `load`, `search`, `clear`, `help`, `quit`/`exit`. `Esc
- `Tab` cycles focus between the query input and the menus. **Implemented manually** in the outer `CatchEvent`: `Menu::OnEvent` consumes `Event::Tab` to cycle its own entries and returns `true`, which prevents `Container::Vertical` from ever seeing the event (Container only cycles between children when the active child returns `false`). So we short-circuit Tab/TabReverse upstream and mutate `search_focus_idx` directly. - `Tab` cycles focus between the query input and the menus. **Implemented manually** in the outer `CatchEvent`: `Menu::OnEvent` consumes `Event::Tab` to cycle its own entries and returns `true`, which prevents `Container::Vertical` from ever seeing the event (Container only cycles between children when the active child returns `false`). So we short-circuit Tab/TabReverse upstream and mutate `search_focus_idx` directly.
- `Esc` exits the search mode (flips `screen_idx` back to 0). The search state (selected module/type, query) is preserved across re-entries until `search` is run again. - `Esc` exits the search mode (flips `screen_idx` back to 0). The search state (selected module/type, query) is preserved across re-entries until `search` is run again.
Adding a new screen mode = add a child to `Container::Tab` and a `screen_idx` value; key handling already lives in the outer `CatchEvent`. Adding a new screen mode = drop a `screen_<name>.cpp` defining `Component Tui::BuildXxxScreen()`, add corresponding state members in `tui.hpp`, register a `BuildXxxScreen` declaration in the screen-builders block, then in `tui.cpp::Run()` add a child to `Container::Tab` and a case in the screen-mode `switch` of the outer `CatchEvent` (Tab cycling + Esc-leave). Commands lived in `commands.cpp` set `screen_idx` to enter the new mode.
Command history is persisted on disk and loaded on startup. Path resolution is platform-aware: Command history is persisted on disk and loaded on startup. Path resolution is platform-aware:
- Linux/macOS: `$XDG_DATA_HOME/essim/history`, falling back to `~/.local/share/essim/history`. - Linux/macOS: `$XDG_DATA_HOME/essim/history`, falling back to `~/.local/share/essim/history`.
@@ -77,7 +111,8 @@ Each successful submission appends a single line to the file (so a crash doesn't
## Gotchas ## Gotchas
- `System::Load` for `IMPORT_ALTIUM` / `IMPORT_ODS`: the corresponding constructor lines are commented out, so `imp` stays uninitialised → UB on `imp->parse(...)`. Only `IMPORT_MENTOR` is safe today. Wrap calls in `try/catch` (the TUI does). - `System::Load` for `IMPORT_ALTIUM`: the corresponding constructor line is still commented out, so `imp` stays uninitialised → UB on `imp->parse(...)`. `IMPORT_MENTOR` and `IMPORT_ODS` are wired. Wrap calls in `try/catch` (the TUI does).
- ODS importer: each spreadsheet sheet becomes a `Part` (sheet name = part name). Rows are pin/signal pairs; the **first non-empty row of each sheet is dropped as a header** (no validation of header content). Empty cells skip the row; `"NC"` keeps the pin in the part but doesn't connect it to a signal. Pins or parts whose name collides (rare in well-formed sheets) are silently dropped.
- `System::Load` throws `std::runtime_error("Unknown import type")` for any value outside the three enum cases. - `System::Load` throws `std::runtime_error("Unknown import type")` for any value outside the three enum cases.
- `Modules`/`Parts`/etc. have no const-correct iteration on the `*` accessor; iterators on `SystemElementContainer<T>` are available but `begin()`/`end()` are non-const-safe in some places. - `Modules`/`Parts`/etc. have no const-correct iteration on the `*` accessor; iterators on `SystemElementContainer<T>` are available but `begin()`/`end()` are non-const-safe in some places.

View File

@@ -23,6 +23,9 @@ FetchContent_Declare(ftxui
) )
FetchContent_MakeAvailable(ftxui) FetchContent_MakeAvailable(ftxui)
find_package(libzip REQUIRED)
find_package(pugixml REQUIRED)
file(GLOB_RECURSE ALL_SOURCES "src/*.cpp") file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")
add_executable(essim ${ALL_SOURCES}) add_executable(essim ${ALL_SOURCES})
@@ -34,4 +37,6 @@ target_link_libraries(essim
ftxui::screen ftxui::screen
ftxui::dom ftxui::dom
ftxui::component ftxui::component
libzip::zip
pugixml::pugixml
) )

121
src/imports/import_ods.cpp Normal file
View File

@@ -0,0 +1,121 @@
#include "import_ods.hpp"
#include "system/parts.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include <pugixml.hpp>
#include <zip.h>
#include <stdexcept>
#include <string>
#include <vector>
ImportOds::ImportOds(std::string filename)
: ImportBase(filename), filename_(std::move(filename)) {}
static std::string read_content_xml(const std::string &filename)
{
int err = 0;
zip_t *zip = zip_open(filename.c_str(), ZIP_RDONLY, &err);
if (!zip) throw std::runtime_error("ods: cannot open " + filename);
zip_stat_t st;
if (zip_stat(zip, "content.xml", 0, &st) != 0) {
zip_close(zip);
throw std::runtime_error("ods: missing content.xml in " + filename);
}
zip_file_t *f = zip_fopen(zip, "content.xml", 0);
if (!f) {
zip_close(zip);
throw std::runtime_error("ods: cannot read content.xml");
}
std::string out;
out.resize(static_cast<size_t>(st.size));
zip_int64_t n = zip_fread(f, out.data(), st.size);
zip_fclose(f);
zip_close(zip);
if (n < 0) throw std::runtime_error("ods: read failed");
out.resize(static_cast<size_t>(n));
return out;
}
static std::string cell_text(pugi::xml_node cell)
{
std::string out;
for (auto p : cell.children("text:p")) {
if (!out.empty()) out += " ";
for (auto n : p.children()) {
if (n.type() == pugi::node_pcdata) out += n.value();
}
}
return out;
}
static std::vector<std::string> expand_row(pugi::xml_node row)
{
std::vector<std::string> cells;
for (auto cell : row.children()) {
std::string n = cell.name();
if (n != "table:table-cell" && n != "table:covered-table-cell") continue;
int rep = cell.attribute("table:number-columns-repeated").as_int(1);
std::string v = cell_text(cell);
for (int i = 0; i < rep; ++i) cells.push_back(v);
}
while (!cells.empty() && cells.back().empty()) cells.pop_back();
return cells;
}
void ImportOds::parse(Signals *signals)
{
std::string xml = read_content_xml(filename_);
pugi::xml_document doc;
auto res = doc.load_string(xml.c_str());
if (!res) {
throw std::runtime_error(std::string("ods: xml parse: ") + res.description());
}
auto spreadsheet = doc.child("office:document-content")
.child("office:body")
.child("office:spreadsheet");
if (!spreadsheet) {
throw std::runtime_error("ods: no spreadsheet found");
}
for (auto t : spreadsheet.children("table:table")) {
std::string sheet = t.attribute("table:name").value();
if (sheet.empty()) continue;
Part *prt = new Part(sheet);
bool first_data = true;
for (auto row : t.children("table:table-row")) {
auto cells = expand_row(row);
if (cells.size() < 2) continue;
// Skip the first non-empty row (header).
if (first_data) { first_data = false; continue; }
const std::string &pin_name = cells[0];
const std::string &sig_name = cells[1];
if (pin_name.empty() || sig_name.empty()) continue;
Pin *pin = new Pin(pin_name);
try { prt->add(pin); }
catch (const std::exception &) { delete pin; continue; }
// "NC" = Not Connected: keep the pin in the part, no signal hookup.
if (sig_name == "NC") continue;
Signal *s = signals->merge(sig_name);
s->add(pin);
pin->connect(s);
}
try { prts->add(prt); }
catch (const std::exception &) { delete prt; }
}
}

View File

@@ -0,0 +1,15 @@
#ifndef _IMPORT_ODS_HPP_
#define _IMPORT_ODS_HPP_
#include "import_base.hpp"
class ImportOds : public ImportBase
{
std::string filename_;
public:
ImportOds(std::string filename);
void parse(Signals *signals) override;
};
#endif // _IMPORT_ODS_HPP_

View File

@@ -1,6 +1,10 @@
#include "connect.hpp" #include "connect.hpp"
Connection::Connection(std::string name) : SystemElement(name) {}; Connection::Connection(std::string name)
: SystemElement(name), m1(nullptr), p1(nullptr), m2(nullptr), p2(nullptr) {}
Connection::Connection(std::string name, Module *m1, Part *p1, Module *m2, Part *p2)
: SystemElement(name), m1(m1), p1(p1), m2(m2), p2(p2) {}
Connections::Connections(void): SystemElementContainer<Connection>("connections") {} Connections::Connections(void): SystemElementContainer<Connection>("connections") {}

View File

@@ -3,10 +3,25 @@
#include "syselmts.hpp" #include "syselmts.hpp"
#include <utility>
#include <vector>
class Module;
class Part;
class Pin;
class Connection : public SystemElement class Connection : public SystemElement
{ {
public: public:
Module *m1;
Part *p1;
Module *m2;
Part *p2;
std::string transform_name;
std::vector<std::pair<Pin *, Pin *>> pin_map;
Connection(std::string name); Connection(std::string name);
Connection(std::string name, Module *m1, Part *p1, Module *m2, Part *p2);
}; };
class Connections : public SystemElementContainer<Connection> class Connections : public SystemElementContainer<Connection>

View File

@@ -13,6 +13,11 @@ Module::~Module() {
delete signals; delete signals;
} }
void Module::add(Part *part) {
SystemElementContainer<Part>::add(part);
part->prnt = this;
}
Modules::Modules(void): SystemElementContainer<Module>("modules") {} Modules::Modules(void): SystemElementContainer<Module>("modules") {}
Modules::Modules(std::vector<Module *> modules): SystemElementContainer<Module>("modules", modules) {} Modules::Modules(std::vector<Module *> modules): SystemElementContainer<Module>("modules", modules) {}

View File

@@ -16,6 +16,8 @@ public:
Modules *prnt; Modules *prnt;
Module(std::string name); Module(std::string name);
~Module(); ~Module();
using SystemElementContainer<Part>::add;
void add(Part *part) override;
}; };
class Modules : public SystemElementContainer<Module> class Modules : public SystemElementContainer<Module>
@@ -27,4 +29,5 @@ public:
~Modules(); ~Modules();
}; };
#endif // _MODULES_HPP_ #endif // _MODULES_HPP_

View File

@@ -1,6 +1,6 @@
#include "parts.hpp" #include "parts.hpp"
Part::Part(std::string name) : SystemElementContainer<Pin>(name), prnt(nullptr) {}; Part::Part(std::string name) : SystemElementContainer<Pin>(name), prnt(nullptr), connector_type() {};
void Part::add(Pin *pin) void Part::add(Pin *pin)
{ {

View File

@@ -13,6 +13,7 @@ public:
Part(std::string name); Part(std::string name);
~Part(); ~Part();
Module *prnt; ///< Pointer to the parent module. Module *prnt; ///< Pointer to the parent module.
std::string connector_type; ///< Tag used by the transform registry; empty = untyped.
void add(Pin *pin) override; void add(Pin *pin) override;
}; };

153
src/system/persist.cpp Normal file
View File

@@ -0,0 +1,153 @@
#include "persist.hpp"
#include "connect.hpp"
#include "modules.hpp"
#include "parts.hpp"
#include "pins.hpp"
#include "signals.hpp"
#include "system.hpp"
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
// Tab-delimited tokeniser. Empty trailing fields are preserved.
std::vector<std::string> split_tab(const std::string &line) {
std::vector<std::string> out;
std::string cur;
for (char c : line) {
if (c == '\t') { out.push_back(std::move(cur)); cur.clear(); }
else cur.push_back(c);
}
out.push_back(std::move(cur));
return out;
}
} // namespace
bool save_system(const System *sys, const std::string &filename, std::string &error)
{
if (!sys) { error = "no system to save"; return false; }
std::ofstream f(filename);
if (!f) { error = "cannot open " + filename + " for writing"; return false; }
f << "# essim system snapshot v1\n";
for (auto &mkv : *sys->modules()) {
Module *mod = mkv.second;
f << "M\t" << mod->name << "\n";
for (auto &pkv : *mod) {
Part *p = pkv.second;
f << "P\t" << p->name << "\t" << p->connector_type << "\n";
for (auto &nkv : *p) {
Pin *pin = nkv.second;
Signal *s = pin->signal();
f << "N\t" << pin->name << "\t" << (s ? s->name : "") << "\n";
}
}
}
for (auto &ckv : *sys->connections()) {
Connection *c = ckv.second;
f << "C\t" << c->name
<< "\t" << (c->m1 ? c->m1->name : "")
<< "\t" << (c->p1 ? c->p1->name : "")
<< "\t" << (c->m2 ? c->m2->name : "")
<< "\t" << (c->p2 ? c->p2->name : "")
<< "\t" << c->transform_name << "\n";
for (auto &wp : c->pin_map) {
Pin *a = wp.first;
Pin *b = wp.second;
f << "W"
<< "\t" << (a && a->prnt && a->prnt->prnt ? a->prnt->prnt->name : "")
<< "\t" << (a && a->prnt ? a->prnt->name : "")
<< "\t" << (a ? a->name : "")
<< "\t" << (b && b->prnt && b->prnt->prnt ? b->prnt->prnt->name : "")
<< "\t" << (b && b->prnt ? b->prnt->name : "")
<< "\t" << (b ? b->name : "")
<< "\n";
}
}
return f.good();
}
System *restore_system(const std::string &filename, std::string &error)
{
std::ifstream f(filename);
if (!f) { error = "cannot open " + filename; return nullptr; }
System *sys = new System();
Module *cur_mod = nullptr;
Part *cur_part = nullptr;
Connection *cur_conn = nullptr;
std::string line;
int lineno = 0;
auto fail = [&](const std::string &msg) {
error = "line " + std::to_string(lineno) + ": " + msg;
delete sys;
return nullptr;
};
while (std::getline(f, line)) {
++lineno;
if (line.empty() || line[0] == '#') continue;
auto fs = split_tab(line);
if (fs.empty()) continue;
const std::string &tag = fs[0];
try {
if (tag == "M") {
if (fs.size() < 2) return fail("M needs <name>");
cur_mod = sys->modules()->merge(fs[1]);
cur_part = nullptr;
} else if (tag == "P") {
if (!cur_mod) return fail("P outside module");
if (fs.size() < 2) return fail("P needs <name>");
cur_part = new Part(fs[1]);
if (fs.size() >= 3) cur_part->connector_type = fs[2];
cur_mod->add(cur_part);
} else if (tag == "N") {
if (!cur_part || !cur_mod) return fail("N outside part");
if (fs.size() < 3) return fail("N needs <pin> <signal>");
Pin *pin = new Pin(fs[1]);
cur_part->add(pin);
if (!fs[2].empty()) {
Signal *s = cur_mod->signals->merge(fs[2]);
s->add(pin);
pin->connect(s);
}
} else if (tag == "C") {
if (fs.size() < 7) return fail("C needs <name> <m1> <p1> <m2> <p2> <transform>");
Module *m1 = sys->modules()->get(fs[2]);
Module *m2 = sys->modules()->get(fs[4]);
Part *p1 = m1->get(fs[3]);
Part *p2 = m2->get(fs[5]);
cur_conn = new Connection(fs[1], m1, p1, m2, p2);
cur_conn->transform_name = fs[6];
sys->connections()->add(cur_conn);
} else if (tag == "W") {
if (!cur_conn) return fail("W outside connection");
if (fs.size() < 7) return fail("W needs <m1> <p1> <pin1> <m2> <p2> <pin2>");
Module *m1 = sys->modules()->get(fs[1]);
Module *m2 = sys->modules()->get(fs[4]);
Part *p1 = m1->get(fs[2]);
Part *p2 = m2->get(fs[5]);
Pin *pin1 = p1->get(fs[3]);
Pin *pin2 = p2->get(fs[6]);
cur_conn->pin_map.emplace_back(pin1, pin2);
} else {
return fail("unknown tag '" + tag + "'");
}
} catch (const std::exception &e) {
return fail(std::string(tag) + ": " + e.what());
}
}
return sys;
}

16
src/system/persist.hpp Normal file
View File

@@ -0,0 +1,16 @@
#ifndef _PERSIST_HPP_
#define _PERSIST_HPP_
#include <string>
class System;
// Writes the system to `filename` using a tab-delimited line format.
// Returns true on success; on failure, sets `error` and returns false.
bool save_system(const System *sys, const std::string &filename, std::string &error);
// Loads a system snapshot from `filename`. On success, returns a freshly allocated
// System (caller takes ownership). On failure, returns nullptr and sets `error`.
System *restore_system(const std::string &filename, std::string &error);
#endif // _PERSIST_HPP_

View File

@@ -16,6 +16,7 @@ public:
Pin(std::string name); Pin(std::string name);
Part *prnt; ///< Pointer to the parent part. Part *prnt; ///< Pointer to the parent part.
bool connected(); bool connected();
Signal *signal() const { return sig; }
void connect(Signal *signal); void connect(Signal *signal);
}; };

View File

@@ -4,6 +4,7 @@
#include "connect.hpp" #include "connect.hpp"
#include "modules.hpp" #include "modules.hpp"
#include "imports/import_mentor.hpp" #include "imports/import_mentor.hpp"
#include "imports/import_ods.hpp"
System::System() : mods(nullptr), conns(nullptr) System::System() : mods(nullptr), conns(nullptr)
{ {
@@ -35,7 +36,7 @@ void System::Load(std::string module_name, std::string file_name, ImportType typ
// imp = new ImportAltium(file_name); // imp = new ImportAltium(file_name);
} else if (type == ImportType::IMPORT_ODS) } else if (type == ImportType::IMPORT_ODS)
{ {
// imp = new ImportOds(file_name); imp = new ImportOds(file_name);
} }
else else
{ {

View File

@@ -23,6 +23,7 @@ public:
System(); System();
void Load(std::string module_name, std::string filename, ImportType type); void Load(std::string module_name, std::string filename, ImportType type);
Modules *modules() const { return mods; } Modules *modules() const { return mods; }
Connections *connections() const { return conns; }
~System(); ~System();
}; };

View File

@@ -0,0 +1,90 @@
#include "transform.hpp"
#include "parts.hpp"
#include "pins.hpp"
#include "transform_vpx.hpp"
#include <set>
#include <vector>
#include <exception>
#include <utility>
Transform::Transform(std::string name) : name(std::move(name)) {}
std::string CheckIdentityCompatible(const Part *a, const Part *b)
{
if (!a || !b) return "missing part";
std::set<std::string> a_pins, b_pins;
for (auto &kv : *a) a_pins.insert(kv.first);
for (auto &kv : *b) b_pins.insert(kv.first);
if (a_pins == b_pins) return "";
std::vector<std::string> only_a, only_b;
for (const auto &n : a_pins) if (!b_pins.count(n)) only_a.push_back(n);
for (const auto &n : b_pins) if (!a_pins.count(n)) only_b.push_back(n);
std::string msg = "identity wiring requires same pin names on both sides";
if (!only_a.empty())
msg += "; only on '" + a->name + "': "
+ std::to_string(only_a.size()) + " (e.g. " + only_a.front() + ")";
if (!only_b.empty())
msg += "; only on '" + b->name + "': "
+ std::to_string(only_b.size()) + " (e.g. " + only_b.front() + ")";
return msg;
}
IdentityTransform::IdentityTransform() : Transform("identity") {}
std::vector<std::pair<Pin *, Pin *>> IdentityTransform::apply(Part *a, Part *b) const
{
std::vector<std::pair<Pin *, Pin *>> out;
for (auto &kv : *a) {
try {
Pin *pb = b->get(kv.first);
out.emplace_back(kv.second, pb);
} catch (const std::exception &) {
// No same-name pin on the other side — skip.
}
}
return out;
}
TransformRegistry::TransformRegistry() : identity_(new IdentityTransform()) {
RegisterVpxTransforms(*this);
}
TransformRegistry::~TransformRegistry()
{
for (auto &kv : entries) delete kv.second;
delete identity_;
}
TransformRegistry &TransformRegistry::get()
{
static TransformRegistry instance;
return instance;
}
void TransformRegistry::add(const std::string &kA, const std::string &kB, Transform *t)
{
auto key = std::make_pair(kA, kB);
auto it = entries.find(key);
if (it != entries.end()) {
delete it->second;
it->second = t;
} else {
entries.emplace(key, t);
}
}
Transform *TransformRegistry::lookup(const std::string &kA, const std::string &kB) const
{
auto it = entries.find({kA, kB});
if (it != entries.end()) return it->second;
auto it2 = entries.find({kB, kA});
if (it2 != entries.end()) return it2->second;
return identity_;
}
Transform *TransformRegistry::identity() const { return identity_; }

View File

@@ -0,0 +1,56 @@
#ifndef _TRANSFORM_HPP_
#define _TRANSFORM_HPP_
#include <map>
#include <string>
#include <utility>
#include <vector>
class Part;
class Pin;
// A Transform describes how a connector pair maps pins between two Parts.
// Returning the list of (pin on side A, pin on side B) wired by this connection
// is enough to record the wiring on the Connection.
// Returns "" if a and b have identical pin name sets (so the identity
// fallback would wire every pin), otherwise a description of the mismatch.
std::string CheckIdentityCompatible(const Part *a, const Part *b);
class Transform
{
public:
std::string name;
explicit Transform(std::string name);
virtual ~Transform() = default;
virtual std::vector<std::pair<Pin *, Pin *>> apply(Part *a, Part *b) const = 0;
};
// Identity: each pin on A is wired to the same-named pin on B, when present.
// Used as the fallback when no specific transform is registered for the pair.
class IdentityTransform : public Transform
{
public:
IdentityTransform();
std::vector<std::pair<Pin *, Pin *>> apply(Part *a, Part *b) const override;
};
class TransformRegistry
{
std::map<std::pair<std::string, std::string>, Transform *> entries;
Transform *identity_;
TransformRegistry();
~TransformRegistry();
public:
static TransformRegistry &get();
void add(const std::string &kindA, const std::string &kindB, Transform *t);
// Returns the transform registered for (kindA, kindB). If neither (kindA, kindB)
// nor (kindB, kindA) is registered, returns the identity transform.
Transform *lookup(const std::string &kindA, const std::string &kindB) const;
Transform *identity() const;
TransformRegistry(const TransformRegistry &) = delete;
TransformRegistry &operator=(const TransformRegistry &) = delete;
};
#endif // _TRANSFORM_HPP_

View File

@@ -0,0 +1,204 @@
#include "transform_vpx.hpp"
#include "parts.hpp"
#include "pins.hpp"
#include <cctype>
#include <exception>
#include <set>
#include <string>
#include <utility>
VpxTransform::VpxTransform(std::string name,
std::string bkp_kind, std::string payload_kind,
std::vector<ColTable> bkp_to_payload,
std::vector<ColTable> payload_to_bkp)
: Transform(std::move(name)),
bkp_kind_(std::move(bkp_kind)),
payload_kind_(std::move(payload_kind)),
bkp_to_payload_(std::move(bkp_to_payload)),
payload_to_bkp_(std::move(payload_to_bkp)) {}
namespace {
struct ParsedPin {
char col; // single-letter column
int row; // numeric row (1-based)
std::string digits; // original digit string (preserves leading zeros)
bool ok = false;
};
ParsedPin parse_pin(const std::string &name) {
ParsedPin out;
size_t i = 0;
while (i < name.size() && std::isalpha((unsigned char)name[i])) ++i;
if (i != 1) return out; // single letter only (VPX 3U convention)
if (i == name.size()) return out;
for (size_t k = i; k < name.size(); ++k)
if (!std::isdigit((unsigned char)name[k])) return out;
out.col = (char)std::toupper((unsigned char)name[0]);
out.digits = name.substr(i);
try { out.row = std::stoi(out.digits); }
catch (const std::exception &) { return out; }
if (out.row <= 0) return out;
out.ok = true;
return out;
}
// Try to find a pin in `to` whose name matches the new column + the source
// row, allowing for either the source's exact digit-string or the canonical
// integer form (so "C01" finds either "C01" or "C1").
Pin *find_target_pin(Part *to, char col, int row, const std::string &digits) {
std::string s1 = std::string(1, col) + digits;
try { return to->get(s1); } catch (const std::exception &) {}
std::string s2 = std::string(1, col) + std::to_string(row);
if (s2 != s1) {
try { return to->get(s2); } catch (const std::exception &) {}
}
return nullptr;
}
} // namespace
std::vector<std::pair<Pin *, Pin *>> VpxTransform::apply(Part *a, Part *b) const
{
std::vector<std::pair<Pin *, Pin *>> out;
if (!a || !b) return out;
bool a_is_bkp;
if (a->connector_type == bkp_kind_ && b->connector_type == payload_kind_)
a_is_bkp = true;
else if (a->connector_type == payload_kind_ && b->connector_type == bkp_kind_)
a_is_bkp = false;
else
return out; // not our pair
const std::vector<ColTable> &table = a_is_bkp ? bkp_to_payload_ : payload_to_bkp_;
if (table.empty()) return out;
for (auto &kv : *a) {
Pin *a_pin = kv.second;
ParsedPin p = parse_pin(kv.first);
if (!p.ok) continue;
size_t pat_idx = (size_t)((p.row - 1) % (int)table.size());
const ColTable &t = table[pat_idx];
auto it = t.find(p.col);
if (it == t.end()) continue;
char new_col = it->second;
Pin *b_pin = find_target_pin(b, new_col, p.row, p.digits);
if (!b_pin) continue;
out.emplace_back(a_pin, b_pin);
}
return out;
}
// ---- Built-in tables (translated verbatim from the user's Python reference) ----
namespace {
using Tbl = VpxTransform::ColTable;
using Pat = std::vector<Tbl>;
// Connector 0: 8 row-patterns each side.
const Pat bkp_to_payload_0 = {
{{'A','A'},{'B','C'},{'C','C'},{'D','C'},{'E','D'},{'F','E'},{'G','E'},{'H','F'},{'I','G'}},
{{'A','A'},{'B','C'},{'C','C'},{'D','C'},{'E','D'},{'F','E'},{'G','E'},{'H','F'},{'I','G'}},
{{'A','A'},{'B','C'},{'C','C'},{'D','C'},{'E','D'},{'F','E'},{'G','E'},{'H','F'},{'I','G'}},
{{'A','C'},{'B','A'},{'C','B'},{'D','C'},{'E','D'},{'F','E'},{'G','F'},{'H','G'},{'I','E'}},
{{'A','C'},{'B','A'},{'C','B'},{'D','C'},{'E','D'},{'F','E'},{'G','F'},{'H','G'},{'I','E'}},
{{'A','C'},{'B','A'},{'C','B'},{'D','C'},{'E','D'},{'F','E'},{'G','F'},{'H','G'},{'I','E'}},
{{'A','A'},{'B','B'},{'C','C'},{'D','C'},{'E','D'},{'F','E'},{'G','F'},{'H','F'},{'I','G'}},
{{'A','A'},{'B','A'},{'C','B'},{'D','C'},{'E','D'},{'F','D'},{'G','E'},{'H','F'},{'I','G'}},
};
const Pat payload_to_bkp_0 = {
{{'A','A'},{'B','B'},{'C','C'},{'D','E'},{'E','G'},{'F','H'},{'G','I'}},
{{'A','A'},{'B','B'},{'C','C'},{'D','E'},{'E','G'},{'F','H'},{'G','I'}},
{{'A','A'},{'B','B'},{'C','C'},{'D','E'},{'E','G'},{'F','H'},{'G','I'}},
{{'A','B'},{'B','C'},{'C','D'},{'D','E'},{'E','F'},{'F','G'},{'G','H'}},
{{'A','B'},{'B','C'},{'C','D'},{'D','E'},{'E','F'},{'F','G'},{'G','H'}},
{{'A','B'},{'B','C'},{'C','D'},{'D','E'},{'E','F'},{'F','G'},{'G','H'}},
{{'A','A'},{'B','B'},{'C','C'},{'D','E'},{'E','F'},{'F','G'},{'G','I'}},
{{'A','B'},{'B','C'},{'C','D'},{'D','E'},{'E','G'},{'F','H'},{'G','I'}},
};
// Connectors 1 and 2: 2 row-patterns each side.
const Pat bkp_to_payload_aux = {
{{'A','A'},{'B','B'},{'C','C'},{'D','C'},{'E','D'},{'F','E'},{'G','F'},{'H','F'},{'I','G'}},
{{'A','A'},{'B','A'},{'C','B'},{'D','C'},{'E','D'},{'F','D'},{'G','E'},{'H','F'},{'I','G'}},
};
const Pat payload_to_bkp_aux = {
{{'A','A'},{'B','B'},{'C','C'},{'D','E'},{'E','F'},{'F','G'},{'G','I'}},
{{'A','A'},{'B','C'},{'C','D'},{'D','E'},{'E','G'},{'F','H'},{'G','I'}},
};
} // namespace
namespace {
// Returns the set of acceptable column letters for a known VPX kind, or
// an empty set when `kind` is unknown (no built-in layout to enforce).
std::set<char> expected_cols(const std::string &kind) {
if (kind == "vpx-3u-bkp-p0"
|| kind == "vpx-3u-bkp-p1"
|| kind == "vpx-3u-bkp-p2") {
return {'A','B','C','D','E','F','G','H','I'};
}
if (kind == "vpx-3u-payload-p0"
|| kind == "vpx-3u-payload-p1"
|| kind == "vpx-3u-payload-p2") {
return {'A','B','C','D','E','F','G'};
}
return {};
}
} // namespace
std::string ValidatePartForKind(const Part *p, const std::string &kind)
{
if (!p) return "no part";
auto cols = expected_cols(kind);
if (cols.empty()) return ""; // unknown kind → caller handles freely
int bad_count = 0;
std::string sample;
std::set<char> seen;
for (auto &kv : *const_cast<Part*>(p)) {
ParsedPin pp = parse_pin(kv.first);
if (!pp.ok || !cols.count(pp.col)) {
++bad_count;
if (sample.empty()) sample = kv.first;
continue;
}
seen.insert(pp.col);
}
if (bad_count > 0) {
return std::to_string(bad_count) + " pin(s) don't fit the '" + kind
+ "' layout (e.g. '" + sample + "'); expected single-letter columns "
+ "{" + std::string(cols.begin(), cols.end()) + "}.";
}
if (seen != cols) {
std::string missing;
for (char c : cols) if (!seen.count(c)) missing += c;
return "missing pin column(s) for '" + kind + "': " + missing;
}
return "";
}
void RegisterVpxTransforms(TransformRegistry &reg)
{
reg.add("vpx-3u-bkp-p0", "vpx-3u-payload-p0",
new VpxTransform("vpx-3u-p0",
"vpx-3u-bkp-p0", "vpx-3u-payload-p0",
bkp_to_payload_0, payload_to_bkp_0));
reg.add("vpx-3u-bkp-p1", "vpx-3u-payload-p1",
new VpxTransform("vpx-3u-p1",
"vpx-3u-bkp-p1", "vpx-3u-payload-p1",
bkp_to_payload_aux, payload_to_bkp_aux));
reg.add("vpx-3u-bkp-p2", "vpx-3u-payload-p2",
new VpxTransform("vpx-3u-p2",
"vpx-3u-bkp-p2", "vpx-3u-payload-p2",
bkp_to_payload_aux, payload_to_bkp_aux));
}

View File

@@ -0,0 +1,52 @@
#ifndef _TRANSFORM_VPX_HPP_
#define _TRANSFORM_VPX_HPP_
#include "transform.hpp"
#include <map>
#include <string>
#include <vector>
// 3U-VPX connector pin-name transform.
//
// Each VPX 3U slot has 3 connectors per side: backplane (P0/P1/P2) and
// payload (J0/J1/J2). The pin-name correspondence between the two sides
// is *not* identity (differential-pair routing makes 9 backplane columns
// map onto 7 payload columns, and the mapping cycles on a row-pattern
// basis that depends on which connector index we're on).
//
// One VpxTransform handles a single connector index (0, 1 or 2). Three
// instances are registered at startup, under the type pairs
// (vpx-3u-bkp-N, vpx-3u-payload-N)
// for N in {0, 1, 2}. Tag each Part with the correct connector_type via
// `set-type`, then `connect` will pick the right transform automatically.
class VpxTransform : public Transform
{
public:
using ColTable = std::map<char, char>;
VpxTransform(std::string name,
std::string bkp_kind, std::string payload_kind,
std::vector<ColTable> bkp_to_payload,
std::vector<ColTable> payload_to_bkp);
std::vector<std::pair<Pin *, Pin *>> apply(Part *a, Part *b) const override;
private:
std::string bkp_kind_;
std::string payload_kind_;
std::vector<ColTable> bkp_to_payload_; // pattern row → col→col map (cycling by row index)
std::vector<ColTable> payload_to_bkp_;
};
class TransformRegistry;
void RegisterVpxTransforms(TransformRegistry &reg);
class Part;
// Returns "" if the part's pin layout is consistent with `kind`, otherwise an
// error message describing the mismatch. Returns "" for unknown `kind`s
// (no built-in layout to check against → caller-defined types).
std::string ValidatePartForKind(const Part *p, const std::string &kind);
#endif // _TRANSFORM_VPX_HPP_

417
src/tui/commands.cpp Normal file
View File

@@ -0,0 +1,417 @@
#include "tui/tui.hpp"
#include "tui/tui_helpers.hpp"
#include "system/connect.hpp"
#include "system/modules.hpp"
#include "system/parts.hpp"
#include "system/persist.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include "system/transform.hpp"
#include "system/transform_vpx.hpp"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <exception>
#include <fstream>
#include <utility>
void Tui::RegisterCommands() {
commands["help"] = {
{{"command name (optional)", Completion::Command}},
[this](const std::vector<std::string> &args) {
if (args.empty()) {
Print("Commands — type `help <name>` for details.");
size_t maxw = 0;
for (const auto &kv : commands) maxw = std::max(maxw, kv.first.size());
for (const auto &kv : commands) {
Print(" " + kv.first
+ std::string(maxw - kv.first.size() + 2, ' ')
+ kv.second.description);
}
Print("Keys: Esc cancels a multi-step prompt; Tab completes commands or paths.");
return;
}
const std::string &name = args[0];
auto it = commands.find(name);
if (it == commands.end()) { Print("unknown command: " + name); return; }
const auto &spec = it->second;
Print(name + "" + spec.description);
if (spec.params.empty()) {
Print(" no arguments.");
} else {
for (size_t i = 0; i < spec.params.size(); ++i) {
Print(" arg " + std::to_string(i + 1) + ": " + spec.params[i].name);
}
}
if (!spec.prompt_for_missing) {
Print(" run with no args for the interactive form.");
} else if (!spec.params.empty()) {
Print(" missing args trigger a prompt for each one.");
}
},
/*prompt_for_missing=*/ false,
"show command help (optionally for a specific command)",
};
commands["clear"] = { {}, [this](auto &) { output.clear(); }, true,
"clear the visualization area" };
commands["quit"] = { {}, [this](auto &) { quit = true; }, true,
"leave essim" };
commands["exit"] = { {}, [this](auto &) { quit = true; }, true,
"leave essim (alias of quit)" };
commands["new"] = { {}, [this](auto &) {
sys = std::make_unique<System>();
recorded.clear();
Print("system created.");
}, true, "create a new (empty) system; resets the script-save buffer" };
commands["load"] = {
{{"module name", Completion::None},
{"filename", Completion::Path},
{"import type [mentor|altium|ods]", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
std::string ls = ToLower(args[2]);
ImportType t;
if (ls == "mentor") t = ImportType::IMPORT_MENTOR;
else if (ls == "altium") t = ImportType::IMPORT_ALTIUM;
else if (ls == "ods") t = ImportType::IMPORT_ODS;
else { Print("unknown import type: " + args[2]); return; }
try {
sys->Load(args[0], args[1], t);
Module *mod = sys->modules()->get(args[0]);
Print("loaded '" + args[0] + "' from " + args[1]);
Print(" parts: " + std::to_string(mod->size()));
Print(" signals: " + std::to_string(mod->signals->size()));
} catch (const std::exception &e) {
Print(std::string("load failed: ") + e.what());
}
},
/*prompt_for_missing=*/ true,
"load a module from a netlist / pinout file (mentor, altium, ods)",
};
commands["source"] = {
{{"filename", Completion::Path}},
[this](const std::vector<std::string> &args) {
Source(args[0]);
},
/*prompt_for_missing=*/ true,
"execute a file of commands line by line (interactive cmds rejected)",
};
commands["script-save"] = {
{{"filename", Completion::Path}},
[this](const std::vector<std::string> &args) {
std::string expanded = args[0];
if (!expanded.empty() && expanded[0] == '~') {
if (const char *home = std::getenv("HOME"))
expanded = std::string(home) + expanded.substr(1);
}
std::ofstream f(expanded);
if (!f) { Print("script-save: cannot open " + args[0]); return; }
for (const auto &cmd : recorded) f << cmd << '\n';
Print("script-save: " + std::to_string(recorded.size())
+ " line(s) → " + args[0]);
},
/*prompt_for_missing=*/ true,
"write commands run since last 'new' as a replay-ready script",
};
commands["save"] = {
{{"filename", Completion::Path}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
std::string err;
if (save_system(sys.get(), args[0], err)) {
Print("saved to " + args[0]);
} else {
Print("save failed: " + err);
}
},
/*prompt_for_missing=*/ true,
"write the current system snapshot to a file",
};
commands["restore"] = {
{{"filename", Completion::Path}},
[this](const std::vector<std::string> &args) {
std::string err;
System *fresh = restore_system(args[0], err);
if (!fresh) { Print("restore failed: " + err); return; }
sys.reset(fresh);
int mods = (int)sys->modules()->size();
int conns = (int)sys->connections()->size();
Print("restored from " + args[0]
+ " (" + std::to_string(mods) + " module(s), "
+ std::to_string(conns) + " connection(s))");
},
/*prompt_for_missing=*/ true,
"replace the current system with a saved snapshot",
};
commands["set-type"] = {
{{"module", Completion::None},
{"part (name or pattern)", Completion::None},
{"connector type (free string, e.g. vpx-bp, vpx-payload)", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
if (args.empty()) {
settype_modules.clear();
for (auto &m : *sys->modules()) settype_modules.push_back(m.first);
std::sort(settype_modules.begin(), settype_modules.end(), NaturalLess);
if (settype_modules.empty()) { Print("no modules loaded."); return; }
settype_m_idx = 0;
settype_p_filter.clear();
settype_p_idx = 0;
settype_type.clear();
settype_status.clear();
settype_focus_idx = 0;
screen_idx = 3;
return;
}
if (args.size() != 3) {
Print("usage: set-type <module> <part> <kind> (or no args for interactive)");
return;
}
Module *mod;
try { mod = sys->modules()->get(args[0]); }
catch (const std::exception &) {
Print("unknown module: " + args[0]); return;
}
Part *prt = nullptr;
try { prt = mod->get(args[1]); }
catch (const std::exception &) {
std::string needle = ToLower(args[1]);
std::vector<Part *> matches;
for (auto &p : *mod)
if (ToLower(p.first).find(needle) != std::string::npos)
matches.push_back(p.second);
if (matches.size() == 1) prt = matches[0];
else {
Print(std::to_string(matches.size())
+ " match(es) for part '" + args[1] + "' in " + mod->name);
return;
}
}
std::string err = ValidatePartForKind(prt, args[2]);
if (!err.empty()) {
Print("set-type refused: " + err);
return;
}
prt->connector_type = args[2];
Print(mod->name + "/" + prt->name + ": connector_type = "
+ (args[2].empty() ? "(none)" : args[2]));
},
/*prompt_for_missing=*/ false,
"tag a part's connector type for transform lookup",
};
commands["connect"] = {
{{"module1", Completion::None},
{"part1 (name or pattern)", Completion::None},
{"module2", Completion::None},
{"part2 (name or pattern)", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
if (args.empty()) {
connect_modules.clear();
for (auto &m : *sys->modules()) connect_modules.push_back(m.first);
std::sort(connect_modules.begin(), connect_modules.end(), NaturalLess);
if (connect_modules.empty()) { Print("no modules loaded."); return; }
connect_m1_idx = 0;
connect_m2_idx = std::min(1, (int)connect_modules.size() - 1);
connect_p1_filter.clear();
connect_p2_filter.clear();
connect_p1_idx = 0;
connect_p2_idx = 0;
connect_focus_idx = 0;
screen_idx = 2;
return;
}
if (args.size() != 4) {
Print("usage: connect <m1> <p1> <m2> <p2> (or no args for interactive)");
return;
}
auto resolve_module = [this](const std::string &name)
-> std::pair<Module*, std::vector<std::string>> {
try { return {sys->modules()->get(name), {}}; }
catch (const std::exception &) {}
std::string needle = ToLower(name);
std::vector<Module*> matches;
std::vector<std::string> names;
for (auto &m : *sys->modules()) {
if (ToLower(m.first).find(needle) != std::string::npos) {
matches.push_back(m.second);
names.push_back(m.first);
}
}
if (matches.size() == 1) return {matches[0], {}};
return {nullptr, names};
};
auto resolve_part = [](Module *mod, const std::string &name)
-> std::pair<Part*, std::vector<std::string>> {
try { return {mod->get(name), {}}; }
catch (const std::exception &) {}
std::string needle = ToLower(name);
std::vector<Part*> matches;
std::vector<std::string> names;
for (auto &p : *mod) {
if (ToLower(p.first).find(needle) != std::string::npos) {
matches.push_back(p.second);
names.push_back(p.first);
}
}
if (matches.size() == 1) return {matches[0], {}};
return {nullptr, names};
};
auto report_ambiguous = [this](const std::string &what,
const std::string &needle,
const std::vector<std::string> &names) {
if (names.empty()) {
Print(what + " not found: " + needle);
} else {
Print(what + " ambiguous for '" + needle + "': "
+ std::to_string(names.size()) + " match(es)");
int shown = 0;
for (const auto &n : names) {
if (shown++ >= 8) { Print(""); break; }
Print(" " + n);
}
}
};
auto [m1, m1_alts] = resolve_module(args[0]);
if (!m1) { report_ambiguous("module", args[0], m1_alts); return; }
auto [p1, p1_alts] = resolve_part(m1, args[1]);
if (!p1) { report_ambiguous("part in " + m1->name, args[1], p1_alts); return; }
auto [m2, m2_alts] = resolve_module(args[2]);
if (!m2) { report_ambiguous("module", args[2], m2_alts); return; }
auto [p2, p2_alts] = resolve_part(m2, args[3]);
if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; }
auto &reg = TransformRegistry::get();
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
if (t == reg.identity()) {
if (!both_empty) {
Print("connect refused: no transform for types '"
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type)
+ "' ↔ '"
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type)
+ "'. Set matching types via 'set-type' first.");
return;
}
std::string err = CheckIdentityCompatible(p1, p2);
if (!err.empty()) {
Print("connect refused: " + err);
return;
}
}
auto pin_map = t->apply(p1, p2);
std::string conn_name = m1->name + "/" + p1->name
+ " <-> " + m2->name + "/" + p2->name;
try {
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
c->transform_name = t->name;
c->pin_map = std::move(pin_map);
sys->connections()->add(c);
Print("connected: " + conn_name
+ " via " + t->name
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
} catch (const std::exception &e) {
Print(std::string("connect failed: ") + e.what());
}
},
/*prompt_for_missing=*/ false,
"connect a part across two modules (interactive screen if no args)",
};
commands["explore"] = { {}, [this](auto &) {
if (!sys) { Print("no system: run 'new' first."); return; }
explore_modules.clear();
for (auto &m : *sys->modules()) explore_modules.push_back(m.first);
std::sort(explore_modules.begin(), explore_modules.end(), NaturalLess);
if (explore_modules.empty()) { Print("no modules loaded."); return; }
explore_module_idx = 0;
explore_type_idx = 0;
explore_child_idx = 0;
explore_detail_idx = 0;
explore_child_filter.clear();
explore_detail_filter.clear();
explore_focus_idx = 0;
screen_idx = 4;
}, true, "browse modules → parts/signals/connections → details (interactive)",
/*scriptable=*/ false };
commands["search"] = {
{{"module", Completion::None},
{"kind [parts|signals]", Completion::None},
{"pattern", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
if (args.empty()) {
search_modules.clear();
for (auto &m : *sys->modules()) search_modules.push_back(m.first);
std::sort(search_modules.begin(), search_modules.end(), NaturalLess);
if (search_modules.empty()) { Print("no modules loaded."); return; }
search_module_idx = 0;
search_type_idx = 0;
search_query.clear();
search_focus_idx = 0;
screen_idx = 1;
return;
}
if (args.size() != 3) {
Print("usage: search <module> <parts|signals> <pattern>");
Print(" search (interactive)");
return;
}
Module *mod;
try { mod = sys->modules()->get(args[0]); }
catch (const std::exception &) {
Print("unknown module: " + args[0]); return;
}
std::string kind = ToLower(args[1]);
std::string needle = ToLower(args[2]);
std::vector<std::pair<std::string, size_t>> hits;
if (kind == "parts" || kind == "part") {
for (auto &pkv : *mod)
if (ToLower(pkv.first).find(needle) != std::string::npos)
hits.emplace_back(pkv.first, pkv.second->size());
} else if (kind == "signals" || kind == "signal") {
for (auto &skv : *mod->signals)
if (ToLower(skv.first).find(needle) != std::string::npos)
hits.emplace_back(skv.first, skv.second->size());
} else {
Print("kind must be 'parts' or 'signals' (got: " + args[1] + ")");
return;
}
std::sort(hits.begin(), hits.end(),
[](const auto &a, const auto &b) { return NaturalLess(a.first, b.first); });
for (const auto &h : hits) {
Print(" " + args[0] + "/" + h.first
+ " (" + std::to_string(h.second) + " pins)");
}
Print(std::to_string(hits.size()) + " match(es).");
},
/*prompt_for_missing=*/ false,
"list parts/signals matching a pattern (interactive screen if no args)",
};
}

111
src/tui/completion.cpp Normal file
View File

@@ -0,0 +1,111 @@
#include "tui/tui.hpp"
#include "tui/tui_helpers.hpp"
#include <cctype>
#include <cstdlib>
#include <filesystem>
void Tui::CompleteCommand(size_t start) {
std::string current = input.substr(start);
std::vector<std::string> matches;
for (const auto &kv : commands)
if (kv.first.rfind(current, 0) == 0) matches.push_back(kv.first);
if (matches.empty()) return;
auto replace_with = [&](const std::string &replacement) {
input.replace(start, std::string::npos, replacement);
cursor_pos = (int)input.size();
};
if (matches.size() == 1) { replace_with(matches[0]); return; }
std::string lcp = LongestCommonPrefix(matches);
if (lcp.size() > current.size()) { replace_with(lcp); return; }
std::string line = " ";
for (const auto &m : matches) line += " " + m;
Print(line);
}
void Tui::CompletePath(size_t start) {
namespace fs = std::filesystem;
std::string current = input.substr(start);
auto pos = current.rfind('/');
std::string disp, prefix;
if (pos == std::string::npos) { disp = ""; prefix = current; }
else { disp = current.substr(0, pos + 1); prefix = current.substr(pos + 1); }
std::string resolved = disp.empty() ? "." : disp;
if (!resolved.empty() && resolved[0] == '~') {
if (const char *home = std::getenv("HOME"))
resolved = std::string(home) + resolved.substr(1);
}
std::vector<std::string> names;
std::vector<bool> is_dir;
try {
for (const auto &e : fs::directory_iterator(resolved)) {
std::string n = e.path().filename().string();
if (n.rfind(prefix, 0) == 0) {
names.push_back(n);
is_dir.push_back(e.is_directory());
}
}
} catch (const std::exception &) {
return;
}
if (names.empty()) return;
auto replace_with = [&](const std::string &replacement) {
input.replace(start, std::string::npos, replacement);
cursor_pos = (int)input.size();
};
if (names.size() == 1) {
replace_with(disp + names[0] + (is_dir[0] ? "/" : ""));
return;
}
std::string lcp = LongestCommonPrefix(names);
if (lcp.size() > prefix.size()) { replace_with(disp + lcp); return; }
std::string line = " ";
for (size_t i = 0; i < names.size(); ++i)
line += " " + names[i] + (is_dir[i] ? "/" : "");
Print(line);
}
void Tui::CompleteInline() {
bool ends_with_ws = !input.empty()
&& std::isspace((unsigned char)input.back());
size_t arg_start;
if (input.empty() || ends_with_ws) {
arg_start = input.size();
} else {
size_t i = input.size();
while (i > 0 && !std::isspace((unsigned char)input[i - 1])) --i;
arg_start = i;
}
auto preceding = Tokenize(input.substr(0, arg_start));
int arg_index = (int)preceding.size();
if (arg_index == 0) { CompleteCommand(); return; }
if (preceding.empty()) return;
auto cmd_it = commands.find(preceding[0]);
if (cmd_it == commands.end()) return;
const auto &spec = cmd_it->second;
int param_idx = arg_index - 1;
if (param_idx >= (int)spec.params.size()) return;
switch (spec.params[param_idx].completion) {
case Completion::Path: CompletePath(arg_start); break;
case Completion::Command: CompleteCommand(arg_start); break;
case Completion::None: break;
}
}

146
src/tui/screen_connect.cpp Normal file
View File

@@ -0,0 +1,146 @@
#include "tui/tui.hpp"
#include "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 <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp>
#include <ftxui/dom/elements.hpp>
#include <algorithm>
#include <exception>
#include <utility>
using namespace ftxui;
void Tui::RefreshFilteredPartList(const std::vector<std::string> &modules,
int m_idx,
const std::string &filter,
std::vector<std::string> &out,
int &sel_idx) {
out.clear();
if (!sys || modules.empty()) { sel_idx = 0; return; }
try {
Module *mod = sys->modules()->get(modules[m_idx]);
std::string needle = ToLower(filter);
for (auto &pkv : *mod) {
if (needle.empty()
|| ToLower(pkv.first).find(needle) != std::string::npos) {
out.push_back(pkv.first);
}
}
} catch (const std::exception &) {}
std::sort(out.begin(), out.end(), NaturalLess);
if (sel_idx >= (int)out.size()) sel_idx = std::max(0, (int)out.size() - 1);
}
Component Tui::BuildConnectScreen() {
InputOption pf_opt;
pf_opt.multiline = false;
pf_opt.transform = [](InputState s) {
auto el = s.element;
if (s.is_placeholder) el |= dim;
return el;
};
auto m1_menu = Menu(&connect_modules, &connect_m1_idx);
auto p1_filter = Input(&connect_p1_filter, "filter…", pf_opt);
auto p1_menu = Menu(&connect_p1_list, &connect_p1_idx);
auto m2_menu = Menu(&connect_modules, &connect_m2_idx);
auto p2_filter = Input(&connect_p2_filter, "filter…", pf_opt);
auto p2_menu = Menu(&connect_p2_list, &connect_p2_idx);
auto do_connect = [this] {
if (!sys || connect_modules.empty()) { screen_idx = 0; return; }
if (connect_p1_list.empty() || connect_p2_list.empty()) {
Print("connect: select a part on each side first.");
screen_idx = 0;
return;
}
try {
Module *m1 = sys->modules()->get(connect_modules[connect_m1_idx]);
Module *m2 = sys->modules()->get(connect_modules[connect_m2_idx]);
Part *p1 = m1->get(connect_p1_list[connect_p1_idx]);
Part *p2 = m2->get(connect_p2_list[connect_p2_idx]);
auto &reg = TransformRegistry::get();
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
if (t == reg.identity()) {
if (!both_empty) {
Print("connect refused: no transform for types '"
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type)
+ "' ↔ '"
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type)
+ "'. Set matching types via 'set-type' first.");
screen_idx = 0;
return;
}
std::string err = CheckIdentityCompatible(p1, p2);
if (!err.empty()) {
Print("connect refused: " + err);
screen_idx = 0;
return;
}
}
auto pin_map = t->apply(p1, p2);
std::string conn_name = m1->name + "/" + p1->name
+ " <-> " + m2->name + "/" + p2->name;
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
c->transform_name = t->name;
c->pin_map = std::move(pin_map);
sys->connections()->add(c);
Print("connected: " + conn_name
+ " via " + t->name
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
} catch (const std::exception &e) {
Print(std::string("connect failed: ") + e.what());
}
screen_idx = 0;
};
auto connect_button = Button(" Connect ", do_connect);
auto components = Container::Vertical({
m1_menu, p1_filter, p1_menu,
m2_menu, p2_filter, p2_menu,
connect_button,
}, &connect_focus_idx);
return Renderer(components,
[this, m1_menu, p1_filter, p1_menu,
m2_menu, p2_filter, p2_menu, connect_button] {
RefreshFilteredPartList(connect_modules, connect_m1_idx,
connect_p1_filter, connect_p1_list, connect_p1_idx);
RefreshFilteredPartList(connect_modules, connect_m2_idx,
connect_p2_filter, connect_p2_list, connect_p2_idx);
auto col = [&](const std::string &title,
Component mm, Component pf, Component pm) {
return vbox({
text(title) | bold,
text("module") | dim,
mm->Render() | yframe | size(HEIGHT, LESS_THAN, 8),
separator(),
hbox({text(" filter: "), pf->Render() | flex}) | border,
text("part") | dim,
pm->Render() | yframe | flex,
}) | flex;
};
return vbox({
hbox({
col("endpoint 1", m1_menu, p1_filter, p1_menu),
separator(),
col("endpoint 2", m2_menu, p2_filter, p2_menu),
}) | flex,
separator(),
hbox({filler(), connect_button->Render(), filler()}),
text(" Tab: cycle focus | Enter on [Connect]: confirm | Esc: leave ") | dim,
}) | border;
});
}

207
src/tui/screen_explore.cpp Normal file
View File

@@ -0,0 +1,207 @@
#include "tui/tui.hpp"
#include "tui/tui_helpers.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 <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp>
#include <ftxui/dom/elements.hpp>
#include <algorithm>
#include <exception>
using namespace ftxui;
Component Tui::BuildExploreScreen() {
InputOption pf_opt;
pf_opt.multiline = false;
pf_opt.transform = [](InputState s) {
auto el = s.element;
if (s.is_placeholder) el |= dim;
return el;
};
auto module_menu = Menu(&explore_modules, &explore_module_idx);
auto type_menu = Menu(&explore_types, &explore_type_idx);
auto child_filter = Input(&explore_child_filter, "filter…", pf_opt);
auto children_menu = Menu(&explore_children, &explore_child_idx);
auto detail_filter = Input(&explore_detail_filter, "filter…", pf_opt);
auto detail_menu = Menu(&explore_detail, &explore_detail_idx);
auto components = Container::Vertical(
{module_menu, type_menu, child_filter, children_menu, detail_filter, detail_menu},
&explore_focus_idx);
return Renderer(components,
[this, module_menu, type_menu, child_filter, children_menu, detail_filter, detail_menu] {
try {
// Clamp the module index defensively (covers fresh systems / restore).
if (explore_module_idx < 0
|| explore_module_idx >= (int)explore_modules.size()) {
explore_module_idx = 0;
}
// Refresh the children list for the selected module + type, applying child filter.
explore_children.clear();
Module *cur_mod = nullptr;
std::string c_needle = ToLower(explore_child_filter);
if (sys && !explore_modules.empty()) {
try {
cur_mod = sys->modules()->get(explore_modules[explore_module_idx]);
auto keep = [&](const std::string &name) {
return c_needle.empty()
|| ToLower(name).find(c_needle) != std::string::npos;
};
if (explore_type_idx == 0) {
for (auto &pkv : *cur_mod)
if (keep(pkv.first)) explore_children.push_back(pkv.first);
} else if (explore_type_idx == 1) {
for (auto &skv : *cur_mod->signals)
if (keep(skv.first)) explore_children.push_back(skv.first);
} else {
for (auto &ckv : *sys->connections()) {
Connection *c = ckv.second;
if ((c->m1 == cur_mod || c->m2 == cur_mod) && keep(ckv.first))
explore_children.push_back(ckv.first);
}
}
} catch (const std::exception &) {}
}
std::sort(explore_children.begin(), explore_children.end(), NaturalLess);
// Never let the Menu see an empty list — FTXUI Menu's clamp(v, 0, size()-1)
// is brittle when size() == 0; a placeholder also gives the user feedback.
if (explore_children.empty()) explore_children.push_back("(no matches)");
if (explore_child_idx < 0
|| explore_child_idx >= (int)explore_children.size()) {
explore_child_idx = 0;
}
std::string d_needle = ToLower(explore_detail_filter);
auto keep_detail = [&](const std::string &line) {
return d_needle.empty()
|| ToLower(line).find(d_needle) != std::string::npos;
};
// Build the detail pane (lines stored in explore_detail so the Menu
// bound to it can scroll with arrow keys when focused).
explore_header = "(no system)";
explore_detail.clear();
if (cur_mod && !explore_children.empty()) {
const std::string &cname = explore_children[explore_child_idx];
try {
if (explore_type_idx == 0) {
Part *p = cur_mod->get(cname);
std::string type_str = p->connector_type.empty()
? "(no type)" : p->connector_type;
explore_header = cur_mod->name + "/" + p->name
+ "" + std::to_string(p->size())
+ " pins • type: " + type_str;
std::vector<std::pair<std::string, std::string>> rows;
for (auto &pin_kv : *p) {
Signal *s = pin_kv.second->signal();
rows.emplace_back(pin_kv.first, s ? s->name : std::string(""));
}
std::sort(rows.begin(), rows.end(),
[](const auto &a, const auto &b) { return NaturalLess(a.first, b.first); });
size_t maxw = 0;
for (const auto &r : rows) maxw = std::max(maxw, r.first.size());
for (const auto &r : rows) {
std::string line = " " + r.first
+ std::string(maxw - r.first.size() + 2, ' ')
+ r.second;
if (keep_detail(line))
explore_detail.push_back(line);
}
} else if (explore_type_idx == 1) {
Signal *s = cur_mod->signals->get(cname);
explore_header = cur_mod->name + "/" + s->name
+ "" + std::to_string(s->size())
+ " pins";
std::vector<std::string> rows;
for (auto &pin_kv : *s) {
Pin *pin = pin_kv.second;
std::string mname = (pin->prnt && pin->prnt->prnt)
? pin->prnt->prnt->name : std::string("?");
std::string pname = pin->prnt ? pin->prnt->name : std::string("?");
rows.push_back(mname + "/" + pname + "/" + pin->name);
}
std::sort(rows.begin(), rows.end(), NaturalLess);
for (const auto &r : rows)
if (keep_detail(r))
explore_detail.push_back(" " + r);
} else {
Connection *c = sys->connections()->get(cname);
std::string tname = c->transform_name.empty()
? "(unknown)" : c->transform_name;
explore_header = c->name
+ "" + std::to_string(c->pin_map.size())
+ " wires • transform: " + tname;
std::vector<std::string> rows;
for (auto &wp : c->pin_map) {
Pin *a = wp.first;
Pin *b = wp.second;
auto label = [](Pin *pin) {
if (!pin) return std::string("?");
std::string m = (pin->prnt && pin->prnt->prnt)
? pin->prnt->prnt->name : "?";
std::string p = pin->prnt ? pin->prnt->name : "?";
return m + "/" + p + "/" + pin->name;
};
rows.push_back(label(a) + "" + label(b));
}
std::sort(rows.begin(), rows.end(), NaturalLess);
for (const auto &r : rows)
if (keep_detail(r))
explore_detail.push_back(" " + r);
}
} catch (const std::exception &) {}
}
if (explore_detail.empty()) explore_detail.push_back("(empty)");
if (explore_detail_idx < 0
|| explore_detail_idx >= (int)explore_detail.size()) {
explore_detail_idx = 0;
}
auto col1 = vbox({
text("module") | bold,
module_menu->Render() | yframe | flex,
}) | size(WIDTH, EQUAL, 24);
auto col2 = vbox({
text("type") | bold,
type_menu->Render() | flex,
}) | size(WIDTH, EQUAL, 12);
auto col3 = vbox({
text(explore_types[explore_type_idx]) | bold,
hbox({text(" filter: "), child_filter->Render() | flex}) | border,
children_menu->Render() | yframe | flex,
}) | size(WIDTH, EQUAL, 36);
auto col4 = vbox({
text(explore_header) | bold,
hbox({text(" filter: "), detail_filter->Render() | flex}) | border,
detail_menu->Render() | vscroll_indicator | yframe | flex,
}) | flex;
return vbox({
hbox({col1, separator(), col2, separator(), col3, separator(), col4}) | flex,
text(" Tab: cycle focus (incl. detail to scroll) | Esc: leave explore ") | dim,
}) | border;
} catch (const std::exception &e) {
return vbox({
text("explore: render error") | bold,
text(std::string(" ") + e.what()) | dim,
text(" Esc: leave explore ") | dim,
}) | border;
}
});
}

42
src/tui/screen_main.cpp Normal file
View File

@@ -0,0 +1,42 @@
#include "tui/tui.hpp"
#include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/elements.hpp>
using namespace ftxui;
Component Tui::BuildMainScreen(ScreenInteractive &screen) {
InputOption opt;
opt.multiline = false;
opt.cursor_position = &cursor_pos;
opt.on_enter = [this] { Submit(); };
opt.transform = [](InputState s) {
auto el = s.element;
if (s.is_placeholder) el |= dim;
return el;
};
auto input_component = Input(&input, "type a command…", opt);
return Renderer(input_component, [this, &screen, input_component] {
if (quit) screen.Exit();
Elements lines;
for (const auto &l : output) lines.push_back(text(l));
auto view = vbox(std::move(lines))
| focusPositionRelative(0, 1)
| yframe
| flex;
std::string label = pending.empty()
? "> "
: pending.front().question + "? ";
return vbox({
view,
separator(),
hbox({text(label), input_component->Render()}),
}) | border;
});
}

83
src/tui/screen_search.cpp Normal file
View File

@@ -0,0 +1,83 @@
#include "tui/tui.hpp"
#include "tui/tui_helpers.hpp"
#include "system/modules.hpp"
#include "system/parts.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp>
#include <ftxui/dom/elements.hpp>
#include <algorithm>
#include <exception>
#include <utility>
using namespace ftxui;
Component Tui::BuildSearchScreen() {
InputOption query_opt;
query_opt.multiline = false;
query_opt.transform = [](InputState s) {
auto el = s.element;
if (s.is_placeholder) el |= dim;
return el;
};
auto query_input = Input(&search_query, "filter…", query_opt);
auto module_menu = Menu(&search_modules, &search_module_idx);
auto type_menu = Menu(&search_types, &search_type_idx);
auto components = Container::Vertical(
{query_input, module_menu, type_menu}, &search_focus_idx);
return Renderer(components,
[this, query_input, module_menu, type_menu] {
std::vector<std::pair<std::string, size_t>> hits;
if (!search_modules.empty() && sys) {
const std::string &mname = search_modules[search_module_idx];
try {
Module *mod = sys->modules()->get(mname);
std::string needle = ToLower(search_query);
if (search_type_idx == 0) { // parts
for (auto &pkv : *mod)
if (needle.empty()
|| ToLower(pkv.first).find(needle) != std::string::npos)
hits.emplace_back(pkv.first, pkv.second->size());
} else { // signals
for (auto &skv : *mod->signals)
if (needle.empty()
|| ToLower(skv.first).find(needle) != std::string::npos)
hits.emplace_back(skv.first, skv.second->size());
}
} catch (const std::exception &) {}
}
std::sort(hits.begin(), hits.end(),
[](const auto &a, const auto &b) { return NaturalLess(a.first, b.first); });
Elements result_lines;
for (const auto &h : hits)
result_lines.push_back(
text(" " + h.first + " (" + std::to_string(h.second) + " pins)"));
auto left = vbox({
text("module") | bold,
module_menu->Render() | yframe | flex,
separator(),
text("type") | bold,
type_menu->Render(),
}) | size(WIDTH, EQUAL, 28);
auto right = vbox({
hbox({text(" search: "), query_input->Render() | flex}) | border,
text(std::to_string(hits.size()) + " match(es)") | dim,
vbox(std::move(result_lines)) | yframe | flex,
}) | flex;
return vbox({
hbox({left, separator(), right}) | flex,
text(" Tab: cycle focus | Esc: leave search ") | dim,
}) | border;
});
}

129
src/tui/screen_settype.cpp Normal file
View File

@@ -0,0 +1,129 @@
#include "tui/tui.hpp"
#include "tui/tui_helpers.hpp"
#include "system/modules.hpp"
#include "system/parts.hpp"
#include "system/system.hpp"
#include "system/transform_vpx.hpp"
#include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp>
#include <ftxui/dom/elements.hpp>
#include <algorithm>
#include <exception>
using namespace ftxui;
Component Tui::BuildSettypeScreen() {
InputOption pf_opt;
pf_opt.multiline = false;
pf_opt.transform = [](InputState s) {
auto el = s.element;
if (s.is_placeholder) el |= dim;
return el;
};
auto module_menu = Menu(&settype_modules, &settype_m_idx);
auto part_filter = Input(&settype_p_filter, "filter…", pf_opt);
auto part_menu = Menu(&settype_p_list, &settype_p_idx);
InputOption type_opt;
type_opt.multiline = false;
type_opt.transform = pf_opt.transform;
auto type_input = Input(&settype_type, "connector type (e.g. vpx-bp)", type_opt);
auto do_settype = [this] {
if (!sys || settype_modules.empty()) {
settype_status = "no system / no modules";
return;
}
if (settype_p_list.empty()) {
settype_status = "select a part first";
return;
}
try {
Module *mod = sys->modules()->get(settype_modules[settype_m_idx]);
Part *prt = mod->get(settype_p_list[settype_p_idx]);
std::string err = ValidatePartForKind(prt, settype_type);
if (!err.empty()) {
settype_status = "refused: " + err;
return;
}
prt->connector_type = settype_type;
std::string msg = mod->name + "/" + prt->name + " = "
+ (settype_type.empty() ? "(none)" : settype_type);
settype_status = "applied: " + msg;
Print("set-type " + msg);
} catch (const std::exception &e) {
settype_status = std::string("failed: ") + e.what();
}
};
auto button = Button(" Apply ", do_settype);
auto components = Container::Vertical({
module_menu, part_filter, part_menu, type_input, button,
}, &settype_focus_idx);
return Renderer(components,
[this, module_menu, part_filter, part_menu, type_input, button] {
RefreshFilteredPartList(settype_modules, settype_m_idx,
settype_p_filter, settype_p_list, settype_p_idx);
std::string current = "(none)";
if (sys && !settype_modules.empty() && !settype_p_list.empty()) {
try {
Module *mod = sys->modules()->get(settype_modules[settype_m_idx]);
Part *prt = mod->get(settype_p_list[settype_p_idx]);
if (!prt->connector_type.empty()) current = prt->connector_type;
} catch (const std::exception &) {}
}
std::vector<std::string> known_types;
if (sys) {
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second)
if (!pkv.second->connector_type.empty())
known_types.push_back(pkv.second->connector_type);
std::sort(known_types.begin(), known_types.end(), NaturalLess);
known_types.erase(std::unique(known_types.begin(), known_types.end()),
known_types.end());
}
std::string known_line = "in use:";
if (known_types.empty()) known_line += " (none yet)";
else for (const auto &k : known_types) known_line += " " + k;
auto left = vbox({
text("module") | bold,
module_menu->Render() | yframe | flex,
}) | size(WIDTH, EQUAL, 28);
auto middle = vbox({
hbox({text(" filter: "), part_filter->Render() | flex}) | border,
text("part") | dim,
part_menu->Render() | yframe | flex,
}) | flex;
auto right = vbox({
text("current type: ") | bold,
text(" " + current),
separator(),
text("new type:") | bold,
hbox({text(" "), type_input->Render() | flex}) | border,
text(known_line) | dim,
filler(),
hbox({filler(), button->Render(), filler()}),
}) | size(WIDTH, EQUAL, 40);
Element status = settype_status.empty()
? text("") | dim
: text(" " + settype_status) | bold;
return vbox({
hbox({left, separator(), middle, separator(), right}) | flex,
separator(),
status,
text(" Tab: cycle focus | Enter on [Apply]: apply (stay) | Esc: leave ") | dim,
}) | border;
});
}

236
src/tui/shell.cpp Normal file
View File

@@ -0,0 +1,236 @@
#include "tui/tui.hpp"
#include "tui/tui_helpers.hpp"
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <set>
#include <system_error>
void Tui::Print(const std::string &line) {
output.push_back(line);
}
void Tui::HistoryUp() {
if (history.empty()) return;
if (history_idx == -1) history_idx = (int)history.size() - 1;
else if (history_idx > 0) history_idx--;
input = history[history_idx];
cursor_pos = (int)input.size();
}
void Tui::HistoryDown() {
if (history_idx == -1) return;
history_idx++;
if (history_idx >= (int)history.size()) {
history_idx = -1;
input.clear();
} else {
input = history[history_idx];
}
cursor_pos = (int)input.size();
}
void Tui::CancelPending() {
if (pending.empty()) return;
pending.clear();
input.clear();
cursor_pos = 0;
history_idx = -1;
Print("(cancelled)");
}
std::vector<std::string> Tui::Tokenize(const std::string &s) {
std::vector<std::string> out;
std::string cur;
bool in_q = false;
for (char c : s) {
if (c == '"') { in_q = !in_q; continue; }
if (!in_q && std::isspace((unsigned char)c)) {
if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); }
} else {
cur.push_back(c);
}
}
if (!cur.empty()) out.push_back(std::move(cur));
return out;
}
void Tui::Submit() {
if (!pending.empty()) {
if (input.empty()) { Print("(empty — Esc to cancel)"); return; }
Prompt p = std::move(pending.front());
pending.pop_front();
Print(" " + p.question + ": " + input);
std::string answer = std::move(input);
input.clear();
cursor_pos = 0;
history_idx = -1;
p.on_answer(answer);
return;
}
if (input.empty()) return;
history_idx = -1;
Print("> " + input);
std::string raw = std::move(input);
input.clear();
cursor_pos = 0;
Dispatch(raw);
}
void Tui::Dispatch(const std::string &raw) {
auto tokens = Tokenize(raw);
if (tokens.empty()) return;
auto it = commands.find(tokens[0]);
if (it == commands.end()) {
Print("unknown command: " + tokens[0]);
if (!in_source) { history.push_back(raw); AppendHistory(raw); }
return;
}
const std::string name = it->first;
const CommandSpec &spec = it->second;
if (tokens.size() - 1 > spec.params.size()) {
Print("too many arguments for '" + name + "'");
if (!in_source) { history.push_back(raw); AppendHistory(raw); }
return;
}
auto args = std::make_shared<std::vector<std::string>>(
tokens.begin() + 1, tokens.end());
if (args->size() == spec.params.size() || !spec.prompt_for_missing) {
Finalize(name, spec, *args);
return;
}
for (size_t i = args->size(); i < spec.params.size(); ++i) {
bool last = (i + 1 == spec.params.size());
const auto &param = spec.params[i];
pending.push_back({
param.name,
[this, name, &spec, args, last](const std::string &s) {
args->push_back(s);
if (last) Finalize(name, spec, *args);
},
param.completion,
});
}
}
void Tui::Finalize(const std::string &name,
const CommandSpec &spec,
const std::vector<std::string> &args) {
std::string canonical = name;
for (const auto &a : args) {
if (a.find_first_of(" \t\"") != std::string::npos)
canonical += " \"" + a + "\"";
else
canonical += " " + a;
}
if (!in_source) {
history.push_back(canonical);
AppendHistory(canonical);
}
spec.action(args);
static const std::set<std::string> no_record = {
"clear", "help", "quit", "exit", "source", "script-save",
};
if (spec.scriptable && !no_record.count(name)) recorded.push_back(canonical);
}
namespace {
std::filesystem::path HistoryPath() {
namespace fs = std::filesystem;
#ifdef _WIN32
if (const char *p = std::getenv("LOCALAPPDATA"); p && *p)
return fs::path(p) / "essim" / "history";
if (const char *p = std::getenv("APPDATA"); p && *p)
return fs::path(p) / "essim" / "history";
if (const char *p = std::getenv("USERPROFILE"); p && *p)
return fs::path(p) / "AppData" / "Local" / "essim" / "history";
#else
if (const char *p = std::getenv("XDG_DATA_HOME"); p && *p)
return fs::path(p) / "essim" / "history";
if (const char *p = std::getenv("HOME"); p && *p)
return fs::path(p) / ".local" / "share" / "essim" / "history";
#endif
return {};
}
} // namespace
void Tui::LoadHistory() {
auto p = HistoryPath();
if (p.empty()) return;
std::ifstream f(p);
std::string line;
while (std::getline(f, line))
if (!line.empty()) history.push_back(line);
}
void Tui::Source(const std::string &filename) {
std::string expanded = filename;
if (!expanded.empty() && expanded[0] == '~') {
if (const char *home = std::getenv("HOME"))
expanded = std::string(home) + expanded.substr(1);
}
std::ifstream f(expanded);
if (!f) { Print("source failed: cannot open " + filename); return; }
bool prev = in_source;
in_source = true;
int executed = 0;
int lineno = 0;
bool aborted = false;
std::string line;
while (std::getline(f, line)) {
++lineno;
size_t start = line.find_first_not_of(" \t");
if (start == std::string::npos) continue;
if (line[start] == '#') continue;
std::string trimmed = line.substr(start);
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
trimmed.pop_back();
if (trimmed.empty()) continue;
input = trimmed;
cursor_pos = (int)input.size();
Submit();
++executed;
if (screen_idx != 0) {
Print("source: line " + std::to_string(lineno)
+ " is interactive (would open a screen) — aborting.");
screen_idx = 0;
aborted = true;
break;
}
}
in_source = prev;
if (!aborted)
Print("source: " + filename + " (" + std::to_string(executed) + " line(s))");
}
void Tui::AppendHistory(const std::string &cmd) {
auto p = HistoryPath();
if (p.empty()) return;
std::string trimmed = cmd;
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
trimmed.pop_back();
if (trimmed.empty()) return;
std::error_code ec;
std::filesystem::create_directories(p.parent_path(), ec);
if (ec) return;
std::ofstream f(p, std::ios::app);
if (f) f << trimmed << '\n';
}

View File

@@ -1,29 +1,26 @@
#include "tui/tui.hpp" #include "tui/tui.hpp"
#include "system/modules.hpp"
#include "system/parts.hpp"
#include "system/signals.hpp"
#include "system/system.hpp" #include "system/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp>
#include <ftxui/component/screen_interactive.hpp> #include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/elements.hpp>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <exception>
#include <filesystem>
#include <fstream>
#include <system_error>
using namespace ftxui; using namespace ftxui;
Tui::Tui() Tui::Tui()
: cursor_pos(0), history_idx(-1), quit(false), : cursor_pos(0), history_idx(-1), quit(false), in_source(false),
screen_idx(0), screen_idx(0),
search_types{"parts", "signals"}, search_types{"parts", "signals"},
search_module_idx(0), search_type_idx(0), search_focus_idx(0) search_module_idx(0), search_type_idx(0), search_focus_idx(0),
connect_m1_idx(0), connect_m2_idx(0),
connect_p1_idx(0), connect_p2_idx(0),
connect_focus_idx(0),
explore_module_idx(0),
explore_types{"parts", "signals", "connections"},
explore_type_idx(0), explore_child_idx(0),
explore_detail_idx(0), explore_focus_idx(0),
settype_m_idx(0), settype_p_idx(0), settype_focus_idx(0)
{ {
LoadHistory(); LoadHistory();
RegisterCommands(); RegisterCommands();
@@ -32,450 +29,68 @@ Tui::Tui()
Tui::~Tui() = default; Tui::~Tui() = default;
void Tui::Print(const std::string &line) {
output.push_back(line);
}
void Tui::HistoryUp() {
if (history.empty()) return;
if (history_idx == -1) history_idx = (int)history.size() - 1;
else if (history_idx > 0) history_idx--;
input = history[history_idx];
cursor_pos = (int)input.size();
}
void Tui::HistoryDown() {
if (history_idx == -1) return;
history_idx++;
if (history_idx >= (int)history.size()) {
history_idx = -1;
input.clear();
} else {
input = history[history_idx];
}
cursor_pos = (int)input.size();
}
void Tui::CancelPending() {
if (pending.empty()) return;
pending.clear();
input.clear();
cursor_pos = 0;
history_idx = -1;
Print("(cancelled)");
}
static std::string ToLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
return s;
}
std::vector<std::string> Tui::Tokenize(const std::string &s) {
std::vector<std::string> out;
std::string cur;
bool in_q = false;
for (char c : s) {
if (c == '"') { in_q = !in_q; continue; }
if (!in_q && std::isspace((unsigned char)c)) {
if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); }
} else {
cur.push_back(c);
}
}
if (!cur.empty()) out.push_back(std::move(cur));
return out;
}
void Tui::RegisterCommands() {
commands["help"] = { {}, [this](auto &) {
Print("Commands (give params inline or fill them in via prompt):");
Print(" new create a new (empty) system");
Print(" load <module> <file> <mentor|altium|ods> load a module into the system");
Print(" search interactive search (parts/signals, live filter)");
Print(" clear clear the visualization area");
Print(" help show this message");
Print(" quit / exit leave essim");
Print(" Esc cancel a multi-step prompt");
Print(" Tab complete command name / file path");
}};
commands["clear"] = { {}, [this](auto &) { output.clear(); }};
commands["quit"] = { {}, [this](auto &) { quit = true; }};
commands["exit"] = { {}, [this](auto &) { quit = true; }};
commands["new"] = { {}, [this](auto &) {
sys = std::make_unique<System>();
Print("system created.");
}};
commands["load"] = {
{{"module name", false},
{"filename", true},
{"import type [mentor|altium|ods]", false}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
std::string ls = args[2];
std::transform(ls.begin(), ls.end(), ls.begin(),
[](unsigned char c) { return std::tolower(c); });
ImportType t;
if (ls == "mentor") t = ImportType::IMPORT_MENTOR;
else if (ls == "altium") t = ImportType::IMPORT_ALTIUM;
else if (ls == "ods") t = ImportType::IMPORT_ODS;
else { Print("unknown import type: " + args[2]); return; }
try {
sys->Load(args[0], args[1], t);
Module *mod = sys->modules()->get(args[0]);
Print("loaded '" + args[0] + "' from " + args[1]);
Print(" parts: " + std::to_string(mod->size()));
Print(" signals: " + std::to_string(mod->signals->size()));
} catch (const std::exception &e) {
Print(std::string("load failed: ") + e.what());
}
}
};
commands["search"] = { {}, [this](auto &) {
if (!sys) { Print("no system: run 'new' first."); return; }
search_modules.clear();
for (auto &m : *sys->modules()) search_modules.push_back(m.first);
if (search_modules.empty()) { Print("no modules loaded."); return; }
search_module_idx = 0;
search_type_idx = 0;
search_query.clear();
search_focus_idx = 0; // start with the query input focused
screen_idx = 1;
}};
}
void Tui::Submit() {
if (!pending.empty()) {
if (input.empty()) { Print("(empty — Esc to cancel)"); return; }
Prompt p = std::move(pending.front());
pending.pop_front();
Print(" " + p.question + ": " + input);
std::string answer = std::move(input);
input.clear();
cursor_pos = 0;
history_idx = -1;
p.on_answer(answer);
return;
}
if (input.empty()) return;
history_idx = -1;
Print("> " + input);
std::string raw = std::move(input);
input.clear();
cursor_pos = 0;
Dispatch(raw);
}
void Tui::Dispatch(const std::string &raw) {
auto tokens = Tokenize(raw);
if (tokens.empty()) return;
auto it = commands.find(tokens[0]);
if (it == commands.end()) {
Print("unknown command: " + tokens[0]);
history.push_back(raw);
AppendHistory(raw);
return;
}
const std::string name = it->first;
const CommandSpec &spec = it->second;
if (tokens.size() - 1 > spec.params.size()) {
Print("too many arguments for '" + name + "'");
history.push_back(raw);
AppendHistory(raw);
return;
}
auto args = std::make_shared<std::vector<std::string>>(
tokens.begin() + 1, tokens.end());
if (args->size() == spec.params.size()) {
Finalize(name, spec, *args);
return;
}
for (size_t i = args->size(); i < spec.params.size(); ++i) {
bool last = (i + 1 == spec.params.size());
const auto &param = spec.params[i];
pending.push_back({
param.name,
[this, name, &spec, args, last](const std::string &s) {
args->push_back(s);
if (last) Finalize(name, spec, *args);
},
param.path_completion,
});
}
}
void Tui::Finalize(const std::string &name,
const CommandSpec &spec,
const std::vector<std::string> &args) {
std::string canonical = name;
for (const auto &a : args) {
if (a.find_first_of(" \t\"") != std::string::npos)
canonical += " \"" + a + "\"";
else
canonical += " " + a;
}
history.push_back(canonical);
AppendHistory(canonical);
spec.action(args);
}
static std::filesystem::path HistoryPath() {
namespace fs = std::filesystem;
#ifdef _WIN32
if (const char *p = std::getenv("LOCALAPPDATA"); p && *p)
return fs::path(p) / "essim" / "history";
if (const char *p = std::getenv("APPDATA"); p && *p)
return fs::path(p) / "essim" / "history";
if (const char *p = std::getenv("USERPROFILE"); p && *p)
return fs::path(p) / "AppData" / "Local" / "essim" / "history";
#else
if (const char *p = std::getenv("XDG_DATA_HOME"); p && *p)
return fs::path(p) / "essim" / "history";
if (const char *p = std::getenv("HOME"); p && *p)
return fs::path(p) / ".local" / "share" / "essim" / "history";
#endif
return {};
}
void Tui::LoadHistory() {
auto p = HistoryPath();
if (p.empty()) return;
std::ifstream f(p);
std::string line;
while (std::getline(f, line))
if (!line.empty()) history.push_back(line);
}
void Tui::AppendHistory(const std::string &cmd) {
auto p = HistoryPath();
if (p.empty()) return;
std::string trimmed = cmd;
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
trimmed.pop_back();
if (trimmed.empty()) return;
std::error_code ec;
std::filesystem::create_directories(p.parent_path(), ec);
if (ec) return;
std::ofstream f(p, std::ios::app);
if (f) f << trimmed << '\n';
}
static std::string LongestCommonPrefix(const std::vector<std::string> &v) {
if (v.empty()) return "";
std::string lcp = v[0];
for (size_t i = 1; i < v.size(); ++i) {
size_t k = 0;
while (k < lcp.size() && k < v[i].size() && lcp[k] == v[i][k]) ++k;
lcp.resize(k);
}
return lcp;
}
void Tui::CompleteCommand() {
std::vector<std::string> matches;
for (const auto &kv : commands)
if (kv.first.rfind(input, 0) == 0) matches.push_back(kv.first);
if (matches.empty()) return;
if (matches.size() == 1) { input = matches[0]; cursor_pos = (int)input.size(); return; }
std::string lcp = LongestCommonPrefix(matches);
if (lcp.size() > input.size()) { input = lcp; cursor_pos = (int)input.size(); return; }
std::string line = " ";
for (const auto &m : matches) line += " " + m;
Print(line);
}
void Tui::CompletePath() {
namespace fs = std::filesystem;
auto pos = input.rfind('/');
std::string disp, prefix;
if (pos == std::string::npos) { disp = ""; prefix = input; }
else { disp = input.substr(0, pos + 1); prefix = input.substr(pos + 1); }
std::string resolved = disp.empty() ? "." : disp;
if (!resolved.empty() && resolved[0] == '~') {
if (const char *home = std::getenv("HOME"))
resolved = std::string(home) + resolved.substr(1);
}
std::vector<std::string> names;
std::vector<bool> is_dir;
try {
for (const auto &e : fs::directory_iterator(resolved)) {
std::string n = e.path().filename().string();
if (n.rfind(prefix, 0) == 0) {
names.push_back(n);
is_dir.push_back(e.is_directory());
}
}
} catch (const std::exception &) {
return;
}
if (names.empty()) return;
if (names.size() == 1) {
input = disp + names[0] + (is_dir[0] ? "/" : "");
cursor_pos = (int)input.size();
return;
}
std::string lcp = LongestCommonPrefix(names);
if (lcp.size() > prefix.size()) { input = disp + lcp; cursor_pos = (int)input.size(); return; }
std::string line = " ";
for (size_t i = 0; i < names.size(); ++i)
line += " " + names[i] + (is_dir[i] ? "/" : "");
Print(line);
}
void Tui::Run() { void Tui::Run() {
auto screen = ScreenInteractive::Fullscreen(); auto screen = ScreenInteractive::Fullscreen();
// ---- Main TUI ---- auto main_screen = BuildMainScreen(screen);
InputOption opt; auto search_screen = BuildSearchScreen();
opt.multiline = false; auto connect_screen = BuildConnectScreen();
opt.cursor_position = &cursor_pos; auto settype_screen = BuildSettypeScreen();
opt.on_enter = [this] { Submit(); }; auto explore_screen = BuildExploreScreen();
opt.transform = [](InputState s) {
auto el = s.element;
if (s.is_placeholder) el |= dim;
return el;
};
auto input_component = Input(&input, "type a command…", opt);
auto main_renderer = Renderer(input_component, [this, &screen, input_component] { auto tab = Container::Tab(
if (quit) screen.Exit(); {main_screen, search_screen, connect_screen, settype_screen, explore_screen},
&screen_idx);
Elements lines;
for (const auto &l : output) lines.push_back(text(l));
auto view = vbox(std::move(lines))
| focusPositionRelative(0, 1)
| yframe
| flex;
std::string label = pending.empty()
? "> "
: pending.front().question + "? ";
return vbox({
view,
separator(),
hbox({text(label), input_component->Render()}),
}) | border;
});
// ---- Search screen ----
InputOption query_opt;
query_opt.multiline = false;
query_opt.transform = opt.transform;
auto query_input = Input(&search_query, "filter…", query_opt);
auto module_menu = Menu(&search_modules, &search_module_idx);
auto type_menu = Menu(&search_types, &search_type_idx);
auto search_components = Container::Vertical(
{query_input, module_menu, type_menu}, &search_focus_idx);
auto search_renderer = Renderer(search_components,
[this, query_input, module_menu, type_menu] {
// Compute filtered list.
Elements result_lines;
int total = 0;
if (!search_modules.empty() && sys) {
const std::string &mname = search_modules[search_module_idx];
try {
Module *mod = sys->modules()->get(mname);
std::string needle = ToLower(search_query);
if (search_type_idx == 0) { // parts
for (auto &pkv : *mod) {
if (needle.empty()
|| ToLower(pkv.first).find(needle) != std::string::npos) {
result_lines.push_back(
text(" " + pkv.first
+ " (" + std::to_string(pkv.second->size()) + " pins)"));
++total;
}
}
} else { // signals
for (auto &skv : *mod->signals) {
if (needle.empty()
|| ToLower(skv.first).find(needle) != std::string::npos) {
result_lines.push_back(
text(" " + skv.first
+ " (" + std::to_string(skv.second->size()) + " pins)"));
++total;
}
}
}
} catch (const std::exception &) {}
}
auto left = vbox({
text("module") | bold,
module_menu->Render() | yframe | flex,
separator(),
text("type") | bold,
type_menu->Render(),
}) | size(WIDTH, EQUAL, 28);
auto right = vbox({
hbox({text(" search: "), query_input->Render() | flex}) | border,
text(std::to_string(total) + " match(es)") | dim,
vbox(std::move(result_lines)) | yframe | flex,
}) | flex;
return vbox({
hbox({left, separator(), right}) | flex,
text(" Tab: cycle focus | Esc: leave search ") | dim,
}) | border;
});
// ---- Screen tab + global key handling ----
auto tab = Container::Tab({main_renderer, search_renderer}, &screen_idx);
auto root = CatchEvent(tab, [this](Event e) { auto root = CatchEvent(tab, [this](Event e) {
if (screen_idx == 1) { switch (screen_idx) {
// Search mode case 4: // explore
if (e == Event::Escape) { screen_idx = 0; return true; } if (e == Event::Escape) { screen_idx = 0; return true; }
// Cycle focus query → modules → type → query (Menu eats Tab otherwise). if (e == Event::Tab) { explore_focus_idx = (explore_focus_idx + 1) % 6; return true; }
if (e == Event::Tab) { if (e == Event::TabReverse) { explore_focus_idx = (explore_focus_idx + 5) % 6; return true; }
search_focus_idx = (search_focus_idx + 1) % 3; return false;
return true;
}
if (e == Event::TabReverse) {
search_focus_idx = (search_focus_idx + 2) % 3;
return true;
}
return false; // let menus / input handle the rest
}
// Main mode case 3: // set-type
if (e == Event::Escape && !pending.empty()) { CancelPending(); return true; } if (e == Event::Escape) { screen_idx = 0; return true; }
if (e == Event::ArrowUp || e == Event::ArrowDown) { if (e == Event::Tab) { settype_focus_idx = (settype_focus_idx + 1) % 5; return true; }
if (pending.empty()) { if (e == Event::TabReverse) { settype_focus_idx = (settype_focus_idx + 4) % 5; return true; }
if (e == Event::ArrowUp) HistoryUp(); return false;
else HistoryDown();
case 2: // connect
if (e == Event::Escape) { screen_idx = 0; return true; }
if (e == Event::Tab) { connect_focus_idx = (connect_focus_idx + 1) % 7; return true; }
if (e == Event::TabReverse) { connect_focus_idx = (connect_focus_idx + 6) % 7; return true; }
return false;
case 1: // search
if (e == Event::Escape) { screen_idx = 0; return true; }
if (e == Event::Tab) { search_focus_idx = (search_focus_idx + 1) % 3; return true; }
if (e == Event::TabReverse) { search_focus_idx = (search_focus_idx + 2) % 3; return true; }
return false;
default: // main
if (e == Event::Escape && !pending.empty()) { CancelPending(); return true; }
if (e == Event::ArrowUp || e == Event::ArrowDown) {
if (pending.empty()) {
if (e == Event::ArrowUp) HistoryUp();
else HistoryDown();
}
return true;
} }
return true; if (e == Event::Tab) {
} if (pending.empty()) {
if (e == Event::Tab) { CompleteInline();
if (pending.empty()) { } else {
if (input.find(' ') == std::string::npos) CompleteCommand(); switch (pending.front().completion) {
} else if (pending.front().path_completion) { case Completion::Path: CompletePath(); break;
CompletePath(); case Completion::Command: CompleteCommand(); break;
case Completion::None: break;
}
}
return true;
} }
return true; return false;
} }
return false;
}); });
screen.Loop(root); screen.Loop(root);

View File

@@ -8,36 +8,50 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>
class System; class System;
class Tui { class Tui {
enum class Completion { None, Path, Command };
struct Prompt { struct Prompt {
std::string question; std::string question;
std::function<void(const std::string &)> on_answer; std::function<void(const std::string &)> on_answer;
bool path_completion = false; Completion completion = Completion::None;
}; };
struct CommandSpec { struct CommandSpec {
struct Param { struct Param {
std::string name; std::string name;
bool path_completion = false; Completion completion = Completion::None;
}; };
std::vector<Param> params; std::vector<Param> params;
std::function<void(const std::vector<std::string> &)> action; std::function<void(const std::vector<std::string> &)> action;
bool prompt_for_missing = true;
std::string description;
bool scriptable = true;
}; };
// ---- Shell state ----
std::vector<std::string> history; std::vector<std::string> history;
std::vector<std::string> recorded; // commands since the last 'new', for script-save
std::vector<std::string> output; std::vector<std::string> output;
std::string input; std::string input;
int cursor_pos; int cursor_pos;
int history_idx; int history_idx;
bool quit; bool quit;
bool in_source;
std::unique_ptr<System> sys; std::unique_ptr<System> sys;
std::deque<Prompt> pending; std::deque<Prompt> pending;
std::map<std::string, CommandSpec> commands; std::map<std::string, CommandSpec> commands;
// ---- Screen orchestration ----
int screen_idx; int screen_idx;
// ---- Search screen state ----
std::vector<std::string> search_modules; std::vector<std::string> search_modules;
std::vector<std::string> search_types; std::vector<std::string> search_types;
int search_module_idx; int search_module_idx;
@@ -45,31 +59,84 @@ class Tui {
int search_focus_idx; int search_focus_idx;
std::string search_query; std::string search_query;
// ---- Connect screen state ----
std::vector<std::string> connect_modules;
int connect_m1_idx;
int connect_m2_idx;
std::string connect_p1_filter;
std::string connect_p2_filter;
std::vector<std::string> connect_p1_list;
std::vector<std::string> connect_p2_list;
int connect_p1_idx;
int connect_p2_idx;
int connect_focus_idx;
// ---- Explore screen state ----
std::vector<std::string> explore_modules;
int explore_module_idx;
std::vector<std::string> explore_types;
int explore_type_idx;
std::vector<std::string> explore_children;
int explore_child_idx;
std::string explore_child_filter;
std::string explore_detail_filter;
std::vector<std::string> explore_detail;
int explore_detail_idx;
std::string explore_header;
int explore_focus_idx;
// ---- Set-type screen state ----
std::vector<std::string> settype_modules;
int settype_m_idx;
std::string settype_p_filter;
std::vector<std::string> settype_p_list;
int settype_p_idx;
std::string settype_type;
std::string settype_status;
int settype_focus_idx;
public: public:
Tui(); Tui();
~Tui(); ~Tui();
void Run(); void Run();
private: private:
// Lifecycle (commands.cpp)
void RegisterCommands(); void RegisterCommands();
// Shell (shell.cpp)
void Print(const std::string &line);
void Submit(); void Submit();
void Dispatch(const std::string &raw); void Dispatch(const std::string &raw);
void Finalize(const std::string &name, void Finalize(const std::string &name,
const CommandSpec &spec, const CommandSpec &spec,
const std::vector<std::string> &args); const std::vector<std::string> &args);
void HistoryUp(); void HistoryUp();
void HistoryDown(); void HistoryDown();
void CancelPending(); void CancelPending();
void Print(const std::string &line);
void CompleteCommand();
void CompletePath();
void LoadHistory(); void LoadHistory();
void AppendHistory(const std::string &cmd); void AppendHistory(const std::string &cmd);
void Source(const std::string &filename);
static std::vector<std::string> Tokenize(const std::string &s); static std::vector<std::string> Tokenize(const std::string &s);
// Completion (completion.cpp)
void CompleteCommand(size_t start = 0);
void CompletePath(size_t start = 0);
void CompleteInline();
// Filtered part list rebuild (used by connect & set-type screens)
void RefreshFilteredPartList(const std::vector<std::string> &modules,
int m_idx,
const std::string &filter,
std::vector<std::string> &out,
int &sel_idx);
// Screen builders (screen_*.cpp)
ftxui::Component BuildMainScreen(ftxui::ScreenInteractive &screen);
ftxui::Component BuildSearchScreen();
ftxui::Component BuildConnectScreen();
ftxui::Component BuildSettypeScreen();
ftxui::Component BuildExploreScreen();
}; };
#endif // _TUI_HPP_ #endif // _TUI_HPP_

54
src/tui/tui_helpers.cpp Normal file
View File

@@ -0,0 +1,54 @@
#include "tui/tui_helpers.hpp"
#include <algorithm>
#include <cctype>
std::string ToLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
return s;
}
bool NaturalLess(const std::string &a, const std::string &b) {
size_t i = 0, j = 0;
while (i < a.size() && j < b.size()) {
unsigned char ca = (unsigned char)a[i];
unsigned char cb = (unsigned char)b[j];
if (std::isdigit(ca) && std::isdigit(cb)) {
size_t za = 0, zb = 0;
while (i + za < a.size() && a[i + za] == '0') ++za;
while (j + zb < b.size() && b[j + zb] == '0') ++zb;
size_t ea = i + za;
while (ea < a.size() && std::isdigit((unsigned char)a[ea])) ++ea;
size_t eb = j + zb;
while (eb < b.size() && std::isdigit((unsigned char)b[eb])) ++eb;
size_t la = ea - (i + za);
size_t lb = eb - (j + zb);
if (la != lb) return la < lb;
int cmp = a.compare(i + za, la, b, j + zb, lb);
if (cmp != 0) return cmp < 0;
if (za != zb) return za > zb;
i = ea;
j = eb;
} else {
char la = (char)std::tolower(ca);
char lb = (char)std::tolower(cb);
if (la != lb) return la < lb;
++i; ++j;
}
}
if (i < a.size()) return false;
if (j < b.size()) return true;
return false;
}
std::string LongestCommonPrefix(const std::vector<std::string> &v) {
if (v.empty()) return "";
std::string lcp = v[0];
for (size_t i = 1; i < v.size(); ++i) {
size_t k = 0;
while (k < lcp.size() && k < v[i].size() && lcp[k] == v[i][k]) ++k;
lcp.resize(k);
}
return lcp;
}

17
src/tui/tui_helpers.hpp Normal file
View File

@@ -0,0 +1,17 @@
#ifndef _TUI_HELPERS_HPP_
#define _TUI_HELPERS_HPP_
#include <string>
#include <vector>
// Free helpers shared across the TUI translation units.
std::string ToLower(std::string s);
// Case-insensitive natural-order comparison: digit runs compared as integers,
// letters compared after std::tolower.
bool NaturalLess(const std::string &a, const std::string &b);
std::string LongestCommonPrefix(const std::vector<std::string> &v);
#endif // _TUI_HELPERS_HPP_