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:
47
CLAUDE.md
47
CLAUDE.md
@@ -12,6 +12,7 @@ cmake --build build -j
|
||||
|
||||
- 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/`.
|
||||
- **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".
|
||||
|
||||
## Layout
|
||||
@@ -31,8 +32,17 @@ src/
|
||||
imports/ -- adapters that populate the domain
|
||||
import_base.hpp ImportBase interface
|
||||
import_mentor.{hpp,cpp} Mentor Graphics netlist parser (done)
|
||||
tui/ -- FTXUI shell
|
||||
tui.{hpp,cpp} Tui: visualisation area on top, input + history at the bottom
|
||||
tui/ -- FTXUI shell, split by responsibility
|
||||
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
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
- `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".
|
||||
- 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 ↑.
|
||||
- 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.
|
||||
|
||||
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:
|
||||
- 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.
|
||||
- `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:
|
||||
- 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
|
||||
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ FetchContent_Declare(ftxui
|
||||
)
|
||||
FetchContent_MakeAvailable(ftxui)
|
||||
|
||||
find_package(libzip REQUIRED)
|
||||
find_package(pugixml REQUIRED)
|
||||
|
||||
file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")
|
||||
|
||||
add_executable(essim ${ALL_SOURCES})
|
||||
@@ -34,4 +37,6 @@ target_link_libraries(essim
|
||||
ftxui::screen
|
||||
ftxui::dom
|
||||
ftxui::component
|
||||
libzip::zip
|
||||
pugixml::pugixml
|
||||
)
|
||||
|
||||
121
src/imports/import_ods.cpp
Normal file
121
src/imports/import_ods.cpp
Normal 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; }
|
||||
}
|
||||
}
|
||||
15
src/imports/import_ods.hpp
Normal file
15
src/imports/import_ods.hpp
Normal 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_
|
||||
@@ -1,6 +1,10 @@
|
||||
#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") {}
|
||||
|
||||
|
||||
@@ -3,10 +3,25 @@
|
||||
|
||||
#include "syselmts.hpp"
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class Module;
|
||||
class Part;
|
||||
class Pin;
|
||||
|
||||
class Connection : public SystemElement
|
||||
{
|
||||
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, Module *m1, Part *p1, Module *m2, Part *p2);
|
||||
};
|
||||
|
||||
class Connections : public SystemElementContainer<Connection>
|
||||
|
||||
@@ -13,6 +13,11 @@ Module::~Module() {
|
||||
delete signals;
|
||||
}
|
||||
|
||||
void Module::add(Part *part) {
|
||||
SystemElementContainer<Part>::add(part);
|
||||
part->prnt = this;
|
||||
}
|
||||
|
||||
Modules::Modules(void): SystemElementContainer<Module>("modules") {}
|
||||
|
||||
Modules::Modules(std::vector<Module *> modules): SystemElementContainer<Module>("modules", modules) {}
|
||||
|
||||
@@ -16,6 +16,8 @@ public:
|
||||
Modules *prnt;
|
||||
Module(std::string name);
|
||||
~Module();
|
||||
using SystemElementContainer<Part>::add;
|
||||
void add(Part *part) override;
|
||||
};
|
||||
|
||||
class Modules : public SystemElementContainer<Module>
|
||||
@@ -27,4 +29,5 @@ public:
|
||||
~Modules();
|
||||
};
|
||||
|
||||
|
||||
#endif // _MODULES_HPP_
|
||||
@@ -1,6 +1,6 @@
|
||||
#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)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ public:
|
||||
Part(std::string name);
|
||||
~Part();
|
||||
Module *prnt; ///< Pointer to the parent module.
|
||||
std::string connector_type; ///< Tag used by the transform registry; empty = untyped.
|
||||
void add(Pin *pin) override;
|
||||
};
|
||||
|
||||
|
||||
153
src/system/persist.cpp
Normal file
153
src/system/persist.cpp
Normal 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
16
src/system/persist.hpp
Normal 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_
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
Pin(std::string name);
|
||||
Part *prnt; ///< Pointer to the parent part.
|
||||
bool connected();
|
||||
Signal *signal() const { return sig; }
|
||||
void connect(Signal *signal);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "connect.hpp"
|
||||
#include "modules.hpp"
|
||||
#include "imports/import_mentor.hpp"
|
||||
#include "imports/import_ods.hpp"
|
||||
|
||||
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);
|
||||
} else if (type == ImportType::IMPORT_ODS)
|
||||
{
|
||||
// imp = new ImportOds(file_name);
|
||||
imp = new ImportOds(file_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ public:
|
||||
System();
|
||||
void Load(std::string module_name, std::string filename, ImportType type);
|
||||
Modules *modules() const { return mods; }
|
||||
Connections *connections() const { return conns; }
|
||||
~System();
|
||||
};
|
||||
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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_
|
||||
|
||||
204
src/system/transform_vpx.cpp
Normal file
204
src/system/transform_vpx.cpp
Normal 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.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));
|
||||
}
|
||||
52
src/system/transform_vpx.hpp
Normal file
52
src/system/transform_vpx.hpp
Normal 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 ®);
|
||||
|
||||
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
417
src/tui/commands.cpp
Normal 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 ® = 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
111
src/tui/completion.cpp
Normal 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
146
src/tui/screen_connect.cpp
Normal 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 ® = 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
207
src/tui/screen_explore.cpp
Normal 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
42
src/tui/screen_main.cpp
Normal 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
83
src/tui/screen_search.cpp
Normal 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
129
src/tui/screen_settype.cpp
Normal 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
236
src/tui/shell.cpp
Normal 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 ¶m = 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';
|
||||
}
|
||||
509
src/tui/tui.cpp
509
src/tui/tui.cpp
@@ -1,29 +1,26 @@
|
||||
#include "tui/tui.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/event.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;
|
||||
|
||||
Tui::Tui()
|
||||
: cursor_pos(0), history_idx(-1), quit(false),
|
||||
: cursor_pos(0), history_idx(-1), quit(false), in_source(false),
|
||||
screen_idx(0),
|
||||
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();
|
||||
RegisterCommands();
|
||||
@@ -32,450 +29,68 @@ Tui::Tui()
|
||||
|
||||
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 ¶m = 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() {
|
||||
auto screen = ScreenInteractive::Fullscreen();
|
||||
|
||||
// ---- Main TUI ----
|
||||
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);
|
||||
auto main_screen = BuildMainScreen(screen);
|
||||
auto search_screen = BuildSearchScreen();
|
||||
auto connect_screen = BuildConnectScreen();
|
||||
auto settype_screen = BuildSettypeScreen();
|
||||
auto explore_screen = BuildExploreScreen();
|
||||
|
||||
auto main_renderer = 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;
|
||||
});
|
||||
|
||||
// ---- 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 tab = Container::Tab(
|
||||
{main_screen, search_screen, connect_screen, settype_screen, explore_screen},
|
||||
&screen_idx);
|
||||
|
||||
auto root = CatchEvent(tab, [this](Event e) {
|
||||
if (screen_idx == 1) {
|
||||
// Search mode
|
||||
switch (screen_idx) {
|
||||
case 4: // explore
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
// Cycle focus query → modules → type → query (Menu eats Tab otherwise).
|
||||
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; // let menus / input handle the rest
|
||||
}
|
||||
if (e == Event::Tab) { explore_focus_idx = (explore_focus_idx + 1) % 6; return true; }
|
||||
if (e == Event::TabReverse) { explore_focus_idx = (explore_focus_idx + 5) % 6; return true; }
|
||||
return false;
|
||||
|
||||
// Main mode
|
||||
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();
|
||||
case 3: // set-type
|
||||
if (e == Event::Escape) { screen_idx = 0; return true; }
|
||||
if (e == Event::Tab) { settype_focus_idx = (settype_focus_idx + 1) % 5; return true; }
|
||||
if (e == Event::TabReverse) { settype_focus_idx = (settype_focus_idx + 4) % 5; return true; }
|
||||
return false;
|
||||
|
||||
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 (input.find(' ') == std::string::npos) CompleteCommand();
|
||||
} else if (pending.front().path_completion) {
|
||||
CompletePath();
|
||||
if (e == Event::Tab) {
|
||||
if (pending.empty()) {
|
||||
CompleteInline();
|
||||
} else {
|
||||
switch (pending.front().completion) {
|
||||
case Completion::Path: CompletePath(); break;
|
||||
case Completion::Command: CompleteCommand(); break;
|
||||
case Completion::None: break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
screen.Loop(root);
|
||||
|
||||
@@ -8,36 +8,50 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
|
||||
class System;
|
||||
|
||||
class Tui {
|
||||
enum class Completion { None, Path, Command };
|
||||
|
||||
struct Prompt {
|
||||
std::string question;
|
||||
std::function<void(const std::string &)> on_answer;
|
||||
bool path_completion = false;
|
||||
Completion completion = Completion::None;
|
||||
};
|
||||
|
||||
struct CommandSpec {
|
||||
struct Param {
|
||||
std::string name;
|
||||
bool path_completion = false;
|
||||
Completion completion = Completion::None;
|
||||
};
|
||||
std::vector<Param> params;
|
||||
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> recorded; // commands since the last 'new', for script-save
|
||||
std::vector<std::string> output;
|
||||
std::string input;
|
||||
int cursor_pos;
|
||||
int history_idx;
|
||||
bool quit;
|
||||
bool in_source;
|
||||
|
||||
std::unique_ptr<System> sys;
|
||||
std::deque<Prompt> pending;
|
||||
std::map<std::string, CommandSpec> commands;
|
||||
|
||||
// ---- Screen orchestration ----
|
||||
int screen_idx;
|
||||
|
||||
// ---- Search screen state ----
|
||||
std::vector<std::string> search_modules;
|
||||
std::vector<std::string> search_types;
|
||||
int search_module_idx;
|
||||
@@ -45,31 +59,84 @@ class Tui {
|
||||
int search_focus_idx;
|
||||
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:
|
||||
Tui();
|
||||
~Tui();
|
||||
void Run();
|
||||
|
||||
private:
|
||||
// Lifecycle (commands.cpp)
|
||||
void RegisterCommands();
|
||||
|
||||
// Shell (shell.cpp)
|
||||
void Print(const std::string &line);
|
||||
void Submit();
|
||||
void Dispatch(const std::string &raw);
|
||||
void Finalize(const std::string &name,
|
||||
const CommandSpec &spec,
|
||||
const std::vector<std::string> &args);
|
||||
|
||||
void HistoryUp();
|
||||
void HistoryDown();
|
||||
void CancelPending();
|
||||
void Print(const std::string &line);
|
||||
|
||||
void CompleteCommand();
|
||||
void CompletePath();
|
||||
|
||||
void LoadHistory();
|
||||
void AppendHistory(const std::string &cmd);
|
||||
|
||||
void Source(const std::string &filename);
|
||||
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_
|
||||
|
||||
54
src/tui/tui_helpers.cpp
Normal file
54
src/tui/tui_helpers.cpp
Normal 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
17
src/tui/tui_helpers.hpp
Normal 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_
|
||||
Reference in New Issue
Block a user