Domain - Signal carries a SignalType (Power/GndShield/Other), auto-inferred from the name in Signal::Signal via infer_signal_type. Override with the new `set-signal-type` command. - SignalType extracted to its own header so Pin can store an `expected_signal_type` without a pins↔signals include cycle. - pin_role(connector_type, pin_name) → SignalType lookup, called from set-type to populate each Pin's expected_signal_type. The VPX 3U table is currently a stub (returns Other). - New `verify` command walks typed parts and reports pins whose connected signal's type doesn't match the expectation. - ODS importer no longer drops pins with empty signal column — they stay in the part as NC, matching the rule "a pin is either NC or connected to a signal". - persist: new S tag for non-default signal type overrides. Tests - doctest v2.4.11 via FetchContent (with CMAKE_POLICY_VERSION_MINIMUM shim, doctest's CMakeLists has a too-old floor for current CMake). - Source files moved into a static library `essim_lib` so both `essim` and `essim_tests` reuse the same compilation. main.cpp is the only file kept out of the lib. - Layer 1 (pure helpers): ToLower, LongestCommonPrefix, Tokenize, NaturalLess (numeric/case/leading-zero edge cases + total-order invariants), signal_type round-trips and infer_signal_type families, VpxTransform registry + symmetry + reference-table mapping for connector P0 row 1, IdentityTransform same-name wiring. - Layer 2 (round-trip): build a synthetic 2-module system in code, save → restore → assert modules / parts / connector_types / NC pins / signal type overrides / connections + pin_map are all preserved. - Tui::Tokenize moved to a free function in tui_helpers so tests can call it without dragging ftxui into the unit-test layer. - 27 test cases, 123 assertions, ~150 ms. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
130 lines
16 KiB
Markdown
130 lines
16 KiB
Markdown
# essim — notes for Claude
|
|
|
|
System digital twin: simulator for the interconnections between cards/boards. C++17, FTXUI for the TUI, importers for external netlist formats.
|
|
|
|
## Build
|
|
|
|
```sh
|
|
cmake -S . -B build
|
|
cmake --build build -j
|
|
./build/essim
|
|
```
|
|
|
|
- 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
|
|
|
|
```
|
|
src/
|
|
main.cpp -- launches Tui
|
|
system/ -- domain model
|
|
syselmts.hpp SystemElement + SystemElementContainer<T> (templated, get/merge/iterate)
|
|
modules.{hpp,cpp} Module, Modules
|
|
parts.{hpp,cpp} Part, Parts
|
|
pins.{hpp,cpp} Pin, Pins
|
|
signals.{hpp,cpp} Signal, Signals
|
|
connect.{hpp,cpp} Connection, Connections
|
|
transform.{hpp,cpp} transforms applied to the model
|
|
system.{hpp,cpp} System: owns Modules + Connections, exposes Load()
|
|
imports/ -- adapters that populate the domain
|
|
import_base.hpp ImportBase interface
|
|
import_mentor.{hpp,cpp} Mentor Graphics netlist parser (done)
|
|
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
|
|
```
|
|
|
|
`include/` and `lib/` are kept empty by design — FTXUI used to live there as precompiled `.a` + headers, now it comes through FetchContent.
|
|
|
|
## Domain conventions
|
|
|
|
- Everything in `system/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container.
|
|
- `SystemElementContainer<T>::merge(name)` is the get-or-create primitive — call it instead of `add` when you don't know whether the element already exists. `add` throws on duplicate names or empty names.
|
|
- `using namespace std;` is present in `syselmts.hpp` — pre-existing, don't add more `using namespace` in headers.
|
|
- Include guards `_NAME_HPP_` *and* `#pragma once` are both used. Match the existing style.
|
|
|
|
## TUI
|
|
|
|
`Tui::Run()` enters an FTXUI fullscreen loop:
|
|
|
|
- Top: scrollable visualisation area (`window` + `vscroll_indicator | yframe`, last line gets `focus` so it auto-scrolls).
|
|
- Bottom: single-line `Input` inside a border. Label switches between `> ` (normal) and `<question>? ` (during a multi-step prompt).
|
|
- `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` 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`, `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), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection).
|
|
|
|
**Signals** carry a `type` (`SignalType::Power | GndShield | Other`) auto-inferred from the name in `Signal::Signal` via `infer_signal_type` (heuristic: GND/GROUND/SHIELD/CHASSIS → GndShield; PWR/VCC/VDD/VEE/VSS/VBAT/VS_/VS3_*/+/- prefixes → Power; else Other). Override with `set-signal-type <module> <signal> <power|gnd|other>`. The explore screen shows the type in the signal detail header.
|
|
|
|
**Pin role expectations**: every Pin carries an `expected_signal_type` populated by `set-type` from a per-(connector_type, pin_name) lookup (`src/system/pin_role.{hpp,cpp}`). The framework is wired end-to-end; the actual VPX 3U lookup table is currently a stub returning Other for all positions — fill in `vpx_3u_role(col, row, idx)` with the real VITA 46 layout when needed. The `verify` command walks all typed parts and reports pins whose connected signal's type doesn't match the expectation.
|
|
|
|
`SignalType` lives in its own header `src/system/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle).
|
|
|
|
**Pins** are either NC (`signal() == nullptr`) or connected to exactly one signal. The ODS importer creates a Pin for every row that has a non-empty pin name, even when the signal column is empty or `"NC"` — the pin stays in the Part as NC. `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`).
|
|
- Right column: `Input` for the live filter query, plus a results panel rebuilt every frame.
|
|
- `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 = 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`.
|
|
- Windows: `%LOCALAPPDATA%\essim\history`, falling back to `%APPDATA%\…` then `%USERPROFILE%\AppData\Local\…`.
|
|
|
|
Each successful submission appends a single line to the file (so a crash doesn't lose history). Multi-step prompt answers are NOT persisted — only top-level commands.
|
|
|
|
## Gotchas
|
|
|
|
- `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.
|
|
|
|
## Memory layout for sessions
|
|
|
|
Persistent notes live in `~/.claude/projects/-home-francois-Projets-essim/memory/`. Index: `MEMORY.md`. Add project/feedback/user/reference entries there when relevant — see top-level Claude Code memory rules.
|