- ftxui v6.1.9 fetched at configure time; vendored .a + headers dropped. - TUI: visualisation area on top, input prompt at the bottom; ↑/↓ history, Tab completion (commands + file paths), Esc cancels prompts. History is persisted (XDG on Linux, %LOCALAPPDATA% on Windows when ported). - Command registry: `new`, `load`, `search`, `clear`, `help`, `quit`/`exit`. Inline params or interactive prompts; either way the canonical inline form is what gets stored in history. - `search`: second full-screen mode with module + parts/signals menus and a live-filtered list; Tab cycles focus, Esc returns to the main shell. - Domain: `System::modules()` accessor + `SystemElementContainer::size()` to support load summary + search. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7.3 KiB
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
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 inbuild/_deps/. - Sources are collected with
file(GLOB_RECURSE ALL_SOURCES "src/*.cpp"). After adding a new.cpp, re-runcmake -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
tui.{hpp,cpp} Tui: visualisation area on top, input + history at the bottom
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 (commitd8122d1: "everything is pointer"). Containers storeT*, ownership lives with the container. SystemElementContainer<T>::merge(name)is the get-or-create primitive — call it instead ofaddwhen you don't know whether the element already exists.addthrows on duplicate names or empty names.using namespace std;is present insyselmts.hpp— pre-existing, don't add moreusing namespacein headers.- Include guards
_NAME_HPP_and#pragma onceare both used. Match the existing style.
TUI
Tui::Run() enters an FTXUI fullscreen loop:
- Top: scrollable visualisation area (
window+vscroll_indicator | yframe, last line getsfocusso it auto-scrolls). - Bottom: single-line
Inputinside a border. Label switches between>(normal) and<question>?(during a multi-step prompt). CatchEventis wrapped outside theRenderer(not betweenRendererandInput). Pattern:Renderer(input_component, lambda)thenCatchEvent(renderer, handler). WrappingCatchEvent(input, …)inside aContainer::Vertical({…})and thenRenderer(container, …)was found to break theInput's content rendering on at least one terminal — typed characters didn't show even thoughon_enterfired correctly.InputOption::transformis overridden to avoid hard-coded colors (default setsWhite on Blackwhen focused, which is unreadable on light terminal themes). The custom transform only appliesdimto the placeholder; everything else inherits terminal default fg/bg, so the UI adapts to any theme.InputOption::multilinemust be set tofalsefor the command prompt. Default istrue, and FTXUI'sHandleReturn()then both inserts\ninto the content and fireson_enter— soSubmit()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 inRegisterCommands(). EachCommandSpecdeclares a list ofParam{name, path_completion}and anaction(args)lambda.Dispatch(raw)tokenises the input (whitespace split with"…"quoting), takes inline args first, and pushes aPromptonto the queue for each missing param. The last prompt's callback callsFinalize(). Finalize()rebuilds the canonical inline form (name arg1 "arg with spaces" arg3) and writes that to history — so aloadcommand answered via interactive prompts still shows up inhistory(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 inRegisterCommands(); 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 withpath_completion = true(e.g. thefilenamestep ofload), completes file paths viastd::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.
search switches to a second full-screen layout (handled by Container::Tab({main, search}, &screen_idx)). Layout:
- Left column:
Menufor the module list andMenufor the type (parts/signals). - Right column:
Inputfor the live filter query, plus a results panel rebuilt every frame. Tabcycles focus between the query input and the menus. Implemented manually in the outerCatchEvent:Menu::OnEventconsumesEvent::Tabto cycle its own entries and returnstrue, which preventsContainer::Verticalfrom ever seeing the event (Container only cycles between children when the active child returnsfalse). So we short-circuit Tab/TabReverse upstream and mutatesearch_focus_idxdirectly.Escexits the search mode (flipsscreen_idxback to 0). The search state (selected module/type, query) is preserved across re-entries untilsearchis 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.
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::LoadforIMPORT_ALTIUM/IMPORT_ODS: the corresponding constructor lines are commented out, soimpstays uninitialised → UB onimp->parse(...). OnlyIMPORT_MENTORis safe today. Wrap calls intry/catch(the TUI does).System::Loadthrowsstd::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 onSystemElementContainer<T>are available butbegin()/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.