The model tree was modules → parts only. Drill it down:
- each Part now lists its Pins, each labelled with the signal it is wired to
and that signal's type, or "(NC[, imported|dropped])";
- each Module gains a "Signals (N)" branch — the per-module net view, each
signal labelled with its type and fan-out.
Pin/part/signal lists sort in natural order ("2" < "10") via a small helper.
Modules expand to show their parts + Signals node; pins and the signal list
stay collapsed (revealed on demand) so large parts don't flood the view.
wx-only change (no core, no tui); builds clean, window opens with no asserts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth editing op into the wx frontend. Extract the type-name parse + apply
into core/app/edit.hpp::set_signal_type(Signal*, name) -> {ok, error, type},
failing without mutation on an unrecognised name. The interactive sigtype modal
keeps its own SignalType-cycling path (different interaction, trivial mutation).
The TUI `set-signal-type` command now renders that result (output unchanged).
The wx GUI gains Edit ▸ Set signal type…: a shared PickModule() helper (PickPart
now builds on it) + inline signal choice + a power/gnd/other dropdown, then the
core op, logged as "module/signal: signal type = …" and reflected.
tests/test_edit.cpp: name parsed and applied; unknown name refused without
mutation. 387 core assertions green; tui + wx build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third editing op in the wx GUI. No core change — app::connect_parts was already
extracted and unit-tested; this is pure wiring. Edit ▸ Connect parts… picks two
parts (PickPart twice, now caption-parameterised to label "first/second part"),
derives their parent modules from Part::prnt, calls app::connect_parts and
renders the same outcomes the TUI does: refused / identity NC fill / connected
(N wires) / failed.
wx builds clean, window opens with no asserts; tui + tests unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second editing op into the wx frontend. Extract the logic (parse the .bsd,
apply it to the part, record bsdl_path) into core/app/edit.hpp::attach_bsdl
(Part*, path) -> {ok, error, entity, bound, unbound, ports_total}, failing
without mutation when the file can't be parsed.
The TUI `attach-bsdl` command now renders that result (output unchanged); the
wx GUI gains an Edit ▸ Attach BSDL… menu reusing PickPart() + a .bsd file
dialog. Prune the now-dead bsdl_model include from commands.cpp.
tests/test_edit.cpp: parse failure leaves the part untouched; null part. The
success path is covered by a batch run (entity + bound/total ports). 381 core
assertions green; tui + wx build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First of the editing ops to reach the wx frontend. Extract the business logic
(validate the kind, tag the part, apply the connector model) into
core/app/edit.{hpp,cpp}: app::set_connector_type(Part*, kind) -> {ok, error,
materialised}, refusing without mutation when the kind is invalid for the part.
Both TUI call sites now use it: the `set-connector-type` command and the
interactive settype screen (de-dup) — output unchanged. The wx GUI gains an
Edit ▸ Set connector type… menu: a reusable PickPart() (module → part choice
dialogs) + a kind prompt, then the same core op, logged and reflected in the
model tree. Prune the now-dead pin_model/transform_vpx includes from
commands.cpp.
Unit-tested by tests/test_edit.cpp (free-form kind tags; invalid kind refused
without mutation; null part). tui + wx build clean; 376 core assertions green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the wxWidgets GUI frontend (wx dependency + -DESSIM_FRONTEND=wx in
README; layout tree + architecture note in DESIGN) and the factored
essim_add_frontend() per-frontend CMake helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A native wx GUI built entirely on essim_core via the Frontend interface — no
Tui reuse, no command shell. Demonstrates the core/frontends architecture by
adding a real second frontend:
- WxFrontend : public Frontend — owns the System + a console buffer; handles
boot headlessly (restore for --restore/--batch; honest note for source);
Run() boots wx without a generated main (SetInstance + wxEntryStart/
CallOnInit/OnRun) so the shared frontend_main stays in control.
- EssimFrame (wxFrame) — menu-driven window: Load (app::load_module), Restore/
Save (persist), Export (app::export_connections), Verify (app::verify),
rendered into a model tree (modules → parts), an overview + verify-health
panel, and a log. Each handler is a thin wrapper over a core/app op.
- main.cpp: construct WxFrontend, call frontend_main — same 4 lines as tui.
- CMakeLists.txt: find_package(wxWidgets) + essim_add_frontend(wx LIBS …);
select with -DESSIM_FRONTEND=wx. ESSIM_FRONTEND gains wx in its STRINGS.
Set LC_CTYPE from the environment at GUI init so wxString decodes the UTF-8 in
narrow literals (em dash, ellipsis); LC_NUMERIC stays "C". .gitignore: build*/.
Needs libwxgtk3.2-dev. Verified: builds clean (wx 3.2.9 GTK3), window opens and
renders with no wx asserts; --commands-md/--restore/--batch behave headlessly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every frontend repeated the same target wiring (glob sources minus main.cpp →
essim_<name> lib linking essim_core; essim exe from main.cpp linking the lib +
essim_frontend; RUNTIME_OUTPUT_DIRECTORY=build/). Move it into a reusable
helper cmake/EssimFrontend.cmake::essim_add_frontend(name LIBS ...), included
once at the top level.
The tui CMakeLists now just fetches FTXUI and calls
essim_add_frontend(tui LIBS ftxui::screen ftxui::dom ftxui::component). A new
frontend's CMakeLists is its toolkit setup + one call. No behaviour change;
binary stays ./build/essim, tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorganises essim around a hard core/frontends boundary so multiple GUI/TUI
engines can be built on the same business logic:
- src/core/{domain,imports,app} — frontend-agnostic essim_core (no GUI
toolkit); src/frontends/<name>/ — per-frontend UI. CMake selects one with
-DESSIM_FRONTEND=<name> (default tui; "none" = core+tests, no FTXUI).
- Application layer (core/app/): export / verify / connect / load extracted
as System-in, result-struct-out operations; TUI commands and screens are
thin wrappers. The analyze screen and dashboard now share app::verify
instead of recomputing the passes (de-dup).
- Frontend interface (frontends/frontend.hpp) + shared frontend_main: main.cpp
is frontend-agnostic; a new frontend is a 4-line main plus a Frontend impl.
- ImportBase hardened (read-only open, fail-fast on a missing file) and made
leak-free (valgrind: no leaks, 0 errors).
Tests: 78 core cases / 368 assertions + the TUI suite, all green. Binary stays
./build/essim; --batch/--commands-md/--help behaviour unchanged.
Reflect the new shared frontends layer (frontend.hpp / frontend_main) in the
Architecture section and layout tree, and list the verify/connect/load app ops
alongside export.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main.cpp was entirely TUI-specific (constructed Tui, parsed argv, drove
BootDispatch/DumpOutput/Run directly). Introduce a shared frontends layer so a
second frontend can reuse the whole launch flow:
- src/frontends/frontend.hpp — abstract Frontend interface (BootDispatch,
DumpCommandsMd, DumpOutput, Run), header-only, no GUI toolkit, no core dep.
- src/frontends/frontend_main.{hpp,cpp} — frontend_main(argc, argv, Frontend&):
all the argv parsing (--source/--restore/--batch/--commands-md/--help) and
the boot → batch/run flow, driving any frontend through the interface.
- Tui now implements Frontend (the four methods already matched; just marked
override).
- The TUI main.cpp shrinks to: construct Tui, call frontend_main. A second
frontend's main() is identical with its own Frontend type.
Build: a small GUI-toolkit-free static lib essim_frontend (frontend_main.cpp)
is added at the top level when a frontend is selected, and the essim exe links
it. ESSIM_FRONTEND=none still builds core+tests only (no essim_frontend, no
FTXUI). Binary stays ./build/essim.
Behaviour unchanged across --batch/--commands-md/--help/exit codes; only the
usage text is genericised ("the TUI" → "the interface", "console screen" →
"console") now that the launcher is shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
System::Load never deleted the ImportBase it allocated, and ~ImportBase was
defaulted so the Parts container it held leaked too. Give ~ImportBase a body
that deletes prts (the container only — the Part objects have been transferred
to the Module via add(), which owns them; the default ~Parts frees the map
without touching the elements, so no double free), and delete the importer at
the end of System::Load (and on the early unreadable-file error path).
Drop ImportMentor's explicit destructor, which called ImportBase::~ImportBase()
in its body — harmless when the base dtor was empty, but a double delete of
prts now that it frees memory. The implicit destructor calls the base once.
Verified with valgrind on a batch load (mentor + a missing-file error path):
"All heap blocks were freed — no leaks are possible", 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ImportBase opened the input with a default std::fstream (in|out), which had
two consequences: a missing file silently produced an empty module (no error),
and a present-but-read-only file failed to open and also loaded as empty. Open
the stream read-only (std::ios::in) instead, and expose is_open().
System::Load now builds the importer first, checks is_open(), and throws
"cannot open file: <path>" before creating the module — so a failed load
surfaces as `load failed: …` and leaves no empty module behind. A read-only
but present file now loads correctly.
Flip the test that pinned the old silent-empty behaviour to assert the clean
failure (error + no module created).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the import orchestration — System::Load + drop_singleton_signals +
infer_signal_types + the post-import counts — out of the `load` command into
core/app/load.{hpp,cpp}: app::load_module(System*, name, path, ImportType)
returns a LoadResult (ok/error, parts, signals, dropped, power/gnd/kept_other)
with no Print/dialog/FTXUI. The "mentor|altium|ods" string→enum mapping moves
to app::import_type_from_name (mirrors export_format_from_path). The command
only parses the type and renders the counts; output is byte-identical.
Add tests/test_load.cpp (core, no UI): the name mapping; a minimal Mentor
netlist that imports two parts and drops one singleton signal; and a pin test
of the pre-existing missing-file behaviour (ImportBase doesn't check is_open(),
so a missing file yields an empty module rather than an error — preserved by
the extraction and pinned so any future hardening is a deliberate change).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the wiring orchestration — transform lookup, identity-compatibility
check, identity NC fill, transform apply, Connection creation/add — out of
the `connect` command and the interactive connect screen into
core/app/connect.{hpp,cpp}: app::connect_parts(System*, m1,p1, m2,p2) returns
a ConnectResult (ok/refused/error, connection_name, transform_name, wires,
identity_info, nc_added) with no Print/dialog/FTXUI. Name/pattern resolution
and ambiguity reporting stay in the command — that is arg-parsing, not the op.
Both frontends now call the one core op, removing the duplicated logic. This
also unifies a divergence: the interactive screen previously called
CheckIdentityCompatible without the info pointer and so never filled identity
NC pins (unlike the command); routing it through app::connect_parts makes the
screen fill NCs and surface the same warning, matching the scriptable path.
Command output is unchanged. Prune the now-dead transform.hpp / domain
connect.hpp includes from the frontends (commands.cpp keeps transform_vpx.hpp
only for ValidatePartForKind).
Add tests/test_connect.cpp (core, no UI): identity-compatible pair wires,
unknown type pairing is refused with nothing created, subset side gets NC
pins filled and the warning reported.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The analyze Issues pane and the dashboard Health rows each recomputed the
same verify passes inline (pin-role mismatches, Power/Gnd net-mix, NC orphan
rollup, model-driven checks) — the third and second copies of what the verify
command also did. Route both screens through app::verify(System*) instead, so
the passes live in exactly one place.
Enrich VerifyReport with a per-pin OrphanPin detail list (module/part/pin +
dropped flag) so the dashboard can still nest its dropped-singleton breakdown
under the NC health line without re-walking modules/parts/pins. Output is
unchanged in both screens (same label formats, same numbers).
Prune the now-dead includes (nets/bsdl_check/connect/parts/pins as applicable,
<unordered_set>) from both screens. Extend tests/test_verify.cpp to cover the
new orphans detail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the 7-pass verify orchestration out of the TUI command lambda and
into core/app/verify.{hpp,cpp}: app::verify(System*) returns a structured
VerifyReport (role mismatches, net inconsistencies, orphan counts, the four
model-driven anomaly vectors) with no Print/dialog/FTXUI. The nets are
computed once and fed to the net-based checks.
The verify command is now a thin renderer over the report, byte-identical
output. Prune the now-dead nets.hpp / bsdl_check.hpp / <unordered_set>
includes from commands.cpp.
Add tests/test_verify.cpp: builds small systems by hand and asserts the
report (empty system, Power/GndShield bridged-net inconsistency, orphan
counts by import origin) — pure core, no UI.
This is the structuring extraction: the same VerifyReport can now back the
analyze screen's Issues pane and the dashboard health rows, removing the
triple duplication of passes 1-3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DESIGN.md: new layered Build section (essim_core + ESSIM_FRONTEND, FTXUI per
frontend, split tests), a rewritten Layout tree (src/core/{domain,imports,app},
src/frontends/tui, tests/{,tui}), and a new "Architecture — core vs frontends"
section stating the rule (core never depends on a frontend) with the export
operation as the worked example. README: layered-build note, FTXUI-is-the-tui-
frontend's dependency, core-vs-frontend test split, nested project-layout tree.
Also keep the binary at ./build/essim via RUNTIME_OUTPUT_DIRECTORY now that the
exe is produced from the frontend subdir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorganise the tree into business vs frontend as separate directories:
src/core/{domain,imports,app} (was system/, imports/, app/)
src/frontends/tui/ (was tui/ + main.cpp)
tests/tui/ (the FTXUI-coupled helper test)
All cross-dir #include paths rewritten; same-dir includes untouched.
CMake: essim_core is the frontend-agnostic business library — links libzip,
pugixml and bsdl, NO GUI toolkit. Each frontend is a self-contained
src/frontends/<name>/ (own CMakeLists, toolkit, main.cpp) that links
essim_core, selected with -DESSIM_FRONTEND=<name> (default tui; 'none' = core +
tests only, no toolkit fetched). FTXUI moved into the tui frontend. Tests are
split: essim_tests links essim_core (no FTXUI), essim_tui_tests links essim_tui.
Verified: default tui build green (ctest 2/2); ESSIM_FRONTEND=none builds the
core + tests with FTXUI never fetched and no `essim` binary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First step of separating business logic from the TUI. The export command built
the CSV/ODS file inside its lambda, mixed with Print/ShowError/dialog calls.
Move all of it — CSV + ODS building, sheet-name sanitising, file writing — into
src/app/export.{hpp,cpp} (namespace app, no FTXUI/console dependency):
export_connections(const System*, path, format) -> ExportResult. The TUI
command is now a thin wrapper (resolve args/dialog, call the core, render). The
core is unit-tested without any UI (test_export); 342 assertions pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Renderer-overlay approach to the global progress box didn't render. Use a
proper Modal like the palette / file dialog, driven by a plain bool
'computing_open' raised when a source starts and lowered when it ends or
aborts. The tick handler stays ahead of the modal guard, so the script keeps
running (and the screen behind it keeps updating) while the modal is shown;
computing_open is also added to the guard so stray keys are ignored mid-load.
The console screen's own Computing block was already removed, so no duplicate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A script opened from the dashboard (screen_idx 4) aborted after its first
line: the guard treated any non-console screen as 'an interactive command
opened a screen'. Record the screen the source started from and abort only
when a sourced line navigates away from it (what a bare interactive command
does). Now 'o' from the dashboard runs the whole script in place — the
dashboard populates live behind the global Computing overlay — while a bare
connect/explore inside a script still aborts. Batch unaffected (BootDispatch
pins screen 0, so origin 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opening a script from the dashboard did nothing visually: the tick driving the
line-by-line loader was only handled in the console case, and the Computing…
overlay was console-only. Move both to the global layer — ticks now process on
any screen (the dashboard updates live as the script loads) and the Computing…
box overlays whatever screen is active. Add an 'r' dashboard shortcut to
restore a snapshot via the file picker (open mode, like 'o'). Dashboard help
hints (loaded + empty state) updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The picker is built for saving, so it asks 'file exists — overwrite?' on
confirm. That's wrong when opening a script (you want an existing file). Add a
confirm_overwrite flag (default true; FileDialogState + OpenFileDialog param);
ConfirmFileDialog only prompts when it's set. The dashboard 'o' shortcut now
opens the dialog with confirm_overwrite=false. save/export keep the guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The empty-dashboard panel (early_help) is a separate hint list from the
loaded-system one, and didn't list the new o/s keys — so 'o' worked but wasn't
advertised when no system is loaded (exactly when you'd use it). Add 'o' to
early_help and mention it in the 'no system loaded' line. (s/x need a system,
so they stay out of the empty-state panel.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two new dashboard keys, mirroring the existing x=export: 'o' opens a file
picker to run a .essim script (-> source <path>), 's' opens a file picker to
write a system snapshot (-> save <path>, only when a system is loaded). Both
use OpenFileDialog with a per-key persisted last directory; the global
CatchEvent already yields to the file-dialog modal while it is open. Dashboard
help hints updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The detail block was rendered after *all* health rows, so it dangled below
the new model: row instead of the NC: row it explains — its indentation read
as broken. Build it into health_rows right after the NC row, with a tree
marker (↳ dropped (only 1 pin on the net):) and consistent nesting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace internal jargon and uncommon words in user-facing strings (better for
non-native English readers): drop "TransformRegistry-driven" / "drives
transforms" from the connect/set-connector-type subtitles; "transform lookup"
→ "tells connect how to wire its pins"; "populate pin specs" → "fills in each
pin's role and direction"; "clear the visualization area" → "clear the console
output"; "materialised" → "added"; refresh the verify description. Regenerated
commands.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"dropped detail:" said nothing about what those pins are. They were detached
by drop_singleton_signals because each was the lone pin on its net (nowhere to
connect → NC). Relabel to "dropped — lone pin on its net (→ NC):".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
check_bsdl_completeness(System*): for each BSDL-attached part, re-parse the
.bsd and report the device power/ground ports with no matching pin on the
netlist part (matched by port name or physical pad) — a rail the schematic
symbol is missing. One aggregated BsdlPinMissing per part; restricted to
power/ground so unused I/O balls don't create noise. Surfaced as a 7th verify
pass and in the analyze/dashboard model counts. 76 cases / 327 assertions
green; the real 8-card system reports 0 (all FPGA rails present). This closes
out P3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The analyze screen's Issues pane now lists the model-driven checks
(check_pin_specs / check_jtag_chain / check_source_conflicts) alongside the
pin-role, net-mix and structural ones, with an "N model" count in the header;
the dashboard gains a "model:" health row. check_pin_specs/check_jtag_chain
take an optional precomputed net list, so verify, analyze and the dashboard
each compute the nets once and reuse them across checks instead of redoing the
transitive closure per check. Unit tests (75) green; verify output unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rank the spec sources (spec_source_rank: UserOverride > Bsdl > ConnectorModel
> Inferred > Imported); apply_model now refuses to overwrite a spec owned by a
higher-rank source, so one model never clobbers a more authoritative one. New
check_source_conflicts(System*) emits SourceConflict for a pin the BSDL
declares power/ground (a must-connect rail) that the netlist leaves
unconnected — a rail floated in the schematic; surfaced as a sixth `verify`
pass. Unit tests (75 cases) green; the real 8-card system reports 0 conflicts
(its rails are all connected) while the JTAG findings remain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New PinModel interface (spec_for / layout / source) + a single apply_model(
Part*, const PinModel&) that materialises missing layout pins and sets each
pin's spec only where the model speaks (spec.source != None), so one source
never clobbers another's. ConnectorModel wraps pin_role/pin_layout;
BsdlPinModel wraps a parsed BsdlModel (indexed by port name and physical pad).
set-connector-type and screen_settype now use ConnectorModel + apply_model;
attach-bsdl and the restore re-apply keep calling apply_bsdl, now a thin
adapter over apply_model. Behaviour-preserving: unit tests (73 cases) green and
the real 8-card system re-runs identically (1517/1517 bound, same JTAG
findings). Covered by test_pin_model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DESIGN.md: libbsdl dependency and --batch headless mode; bsdl_model/bsdl_check
in the layout; the attach-bsdl command and the `B` persist tag; PinSpec is now
BSDL-populated; verify's five passes incl. the model-driven and JTAG checks
and the new AnomalyKinds. README: libbsdl dependency, --batch usage, tutorial
link. New doc/user/tutorial.md: end-to-end batch and TUI walkthroughs (load →
tag → connect → attach-bsdl → verify, with the pin/JTAG findings explained).
Regenerated commands.md (adds attach-bsdl); index.md links the tutorial.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real-data testing (3 BSDL-attached FPGAs in an 8-card system) showed undriven
over-fires when only one side of a net has a known direction: the driver sits
on an un-modelled part (direction Unknown). Require known == net pin count, so
"undriven" is concluded only when every pin on the net is modelled. Drops 216
false positives to 0 on the sample system while the genuine JTAG findings
remain; the unit test is unaffected (its net is fully modelled).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BootDispatch already runs --restore/--source synchronously before the TUI
starts (Source takes its headless drain branch when no screen is attached),
so the console buffer is complete by then. New --batch flag dumps that buffer
(Tui::DumpOutput) to stdout and exits without launching the TUI — enabling
scripted/CI runs and verify output capture (e.g. essim --batch --source s).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New check_jtag_chain(System*): collects TAP pins by PinSpec.function, resolves
each to its net, and flags JtagTapIncomplete (a device missing TDI/TDO/TMS/
TCK), JtagBusUnbridged (TMS or TCK not common to every TAP device), and
JtagChainBreak (dangling TDO/TDI, chain fan-out, or not a single head->tail
daisy chain). Surfaced as a pass in `verify`; AnomalyKind extended. Covered by
test_bsdl_check (healthy chain, broken chain + split bus, incomplete TAP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New bsdl_check.{hpp,cpp}: check_pin_specs(System*) walks the nets and uses
each pin's PinSpec direction/function to flag DriveContention (>=2 push-pull
output drivers), UndrivenNet (a multi-pin net with input(s) but no driver),
and NcWired (a no-connect pin wired onto a multi-pin net). Added as a pass in
`verify`; AnomalyKind extended accordingly. Nets with no direction data are
skipped, so un-modelled parts produce no noise. Covered by test_bsdl_check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New `attach-bsdl <module> <part> <file.bsd>` command: parse via BsdlModel,
apply_bsdl() onto the part, store the path on Part::bsdl_path, report bound/
unbound. Persist a `B\t<path>` line under the part; on restore, re-parse and
re-apply each attached model (the .bsd path is persisted, not the derived
pin specs). Round-trip covered by test_bsdl_apply.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Link libbsdl dynamically (add_subdirectory ../libbsdl, overridable via
-DBSDL_DIR). New BsdlModel wraps the C ABI and reduces a parsed .bsd to
essim's pin vocabulary; apply_bsdl() binds each port to a Pin (by name, then
by physical pad) and sets its spec: direction, function (TAP role / power /
ground / signal), pad, and source = Bsdl.
This feeds the PinSpec fields from P1, so verify's existing power/ground
placement pass now lights up for BSDL-modelled parts. Covered by
test_bsdl_apply (name + pad binding, TAP roles, linkage classification).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DESIGN.md: Pin now carries a PinSpec (function/direction/pad/source);
expected_signal_type() is a derived accessor; pin_role() returns a PinSpec.
README.md: dedicated Dependencies section with libzip/pugixml install
commands for Debian/Ubuntu, Arch and Fedora.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce PinSpec (function/direction/pad/source) as the "expected" half of
pin verification, and make Pin::expected_signal_type() a derived accessor over
spec.function. pin_role() now returns a PinSpec; the connector layout (and,
later, BSDL) feed the same structure.
Pure refactor, behaviour-preserving: vpx_3u_role is still a stub, so every pin
maps to Other exactly as before. The new `pad` field will carry the BSDL
physical pin; direction/function will unlock contention/undriven/NC checks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ODS sheets now carry a per-connection meta block (Connection / Transform
/ Left / Right) above the data; the header row anchors the freeze, the
auto-filter range, and the zebra striping. CSV stays a single flat
15-column table whose names match the ODS headers exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ODS writer:
- Header row styled bold on a grey background; data rows alternate
light-grey / white (zebra). Auto-filter buttons enabled per sheet
via `<table:database-range table:display-filter-buttons="true">`
— sheet names are now single-quoted in the target-range-address so
LibreOffice actually parses ranges that contain spaces.
- First row of every sheet frozen via `settings.xml` (view setting).
The settings layout follows LibreOffice's own writer exactly:
`xmlns:ooo` + `xmlns:xlink` declared on the root, `ActiveTable` and
view-level zoom/grid/headers placed *after* the Tables map (the
order LibreOffice expects to read back). HorizontalSplitMode = 0,
VerticalSplitMode = 2 → only the first row is frozen, the first
column scrolls normally. ActiveSplitRange = 2 (bottom-left pane).
`settings.xml` registered in `META-INF/manifest.xml`.
Reusable Yes/No modal:
- New `screen_confirm.cpp` exposing `Tui::ShowConfirm(msg, on_yes)`.
Centred `borderRounded` popup, `No` first (safer default), Enter
confirms the focused button, Esc cancels (treated as No).
- Stacked into the Modal chain in `Run()` (between file-dialog and
error). The outer `CatchEvent` cedes events when it's open.
File dialog:
- Picks `OK` button focus reliably — previously the focus indices in
the Renderer were remapped against the Container::Vertical child
indices, so the OK label only flagged "focused" while the actual
focused child was the filename input.
- OK button uses a custom `ButtonOption::transform` that renders the
label transparent when idle, inverted when focused. No more
cyan-fill, no more double-inversion via FocusLabel.
- After confirmation, if the picked path already exists, pops a
`ShowConfirm("File … already exists. Overwrite?")` before invoking
the caller's action.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LibreOffice rejected the generated `.ods` with `Format error at 2,39
in content.xml`. Root cause: `pugi::format_no_declaration` suppresses
only the *implicit* declaration auto-added at save time — the explicit
`node_declaration` I had appended to the document still got serialised,
on top of a manual `<?xml…?>` string prepend in the output. Two
declarations back-to-back, invalid XML.
Fix: let pugixml emit the explicit declaration node, drop the manual
prepend.
Also harden the sheet-name sanitiser in the export action: ODS / Excel
also forbid `< > &` in raw cell or table names, so the default
connection name `bp/J20 <-> payload1/P0` made content.xml entity-
escape `<` to `<`, which a few viewers handle but Excel rejects.
Clip to 31 chars too (Excel's hard limit) so multi-name connections
don't blow up the open.
Verified by `soffice --headless --convert-to csv` round-tripping the
output without errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two focused, behaviour-preserving moves:
1. `OpenSignalTypeDialog` + `ApplySignalTypeChoice` moved from
`shell.cpp` to `screen_sigtype_modal.cpp` so the popup owns all of
its logic instead of having its open/apply functions live in the
shell file.
2. The `export` command extracted from `commands.cpp` to a new
`commands_export.cpp` under a `Tui::RegisterExportCommands()`
member. `RegisterCommands()` calls it at the end. File-local
helpers (`csv_quote`, `pin_side`) move alongside in an anonymous
namespace.
Establishes the pattern for future per-group splits: declare a
`Register<X>Commands()` member, define it in its own file, call it
from the orchestrator. Other groups stay in `commands.cpp` for now —
nothing else has grown large enough to warrant the split.
Sizes: shell.cpp 497 → 448, commands.cpp 846 → 675 (+ 191 for the
new commands_export.cpp). DESIGN.md updated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New user-facing features:
- `export connections <file>` writes a tabular dump of every wire pair:
connection, transform, left/right module/part/pin/signal/type/suspect,
mixed-types flag. Dispatch on extension: `.csv` (flat file) or `.ods`
(one sheet per connection). Any other extension shows an error and
writes nothing.
- Bare `export` (or dashboard `[x]`, or palette `export`) opens an
interactive file-picker dialog with a CSV/ODS toggle at the top.
Picking a filter rewrites the filename's extension. Last-used
directory and filename are remembered per-call-site.
- Two new CLI flags on the binary: `--source FILE` to run a script at
boot, `--restore FILE` to restore a snapshot at boot. Combinable.
Reusable infrastructure:
- `OdsWriter` (`src/imports/ods_writer.{hpp,cpp}`): minimal .ods writer
using libzip + pugixml (already in the build for the importer).
Multi-sheet workbook of string cells. ~180 lines, no new dep.
- Generic file-picker dialog (`screen_filedialog.cpp`): one Modal
reused for any "pick a path" interaction via
`OpenFileDialog(title, persist_key, default_filename, filters, cb)`.
Validates the picked extension against the filter whitelist;
unknown ones stay in the dialog with a status message. Persists
(dir, filename) per `persist_key`.
- Generic error modal (`screen_error.cpp`, `ShowError(msg)`): centred
red-titled popup, dismissable with Esc/Enter. Used by the export
failures (open-for-write, ODS save, unknown extension/kind);
ready for adoption elsewhere.
- Per-key path persistence (`SaveLastUsed`/`LoadLastUsed` in
`shell.cpp`): two-line file per key under the user-data dir.
- `UserDataDir()` extracted from the history path helper so the new
per-key persistence shares the same XDG/AppData logic.
- New help-screen topic "Export"; user-facing `doc/user/analysis.md`
gains an "Exporting" section; `DESIGN.md` gains a generics
section covering the dialog / error modal / persistence / ODS
writer; `DumpCommandsMd` now respects the `hidden` flag (the
`connect` alias no longer appears in the auto-gen reference).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`help` bare went back to printing the textual list of commands rather
than opening the help screen — the screen is reachable from the
dashboard with `[h]`. This matches how every other CLI handles `help`
and avoids surprising script behaviour.
Added a `hidden` field on `CommandSpec` so registry-level aliases can
be excluded from the listing. `connect` is now hidden (the alias
`plug` is the user-facing name on the dashboard and in `help`).
Both names continue to resolve to the same action; existing scripts
that used `connect` still work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The analyze screen already used `[Suspect Power]`; the dashboard and
the `load` summary still said 'refuted', which felt harsher and was
out of sync. Now consistent everywhere.
- Dashboard module row: `power: X confirmed, Y suspect gnd: K`.
- Load summary: `types: N power, M gnd, K suspect Power (name only
— kept as Other)`.
Internal variable renamed `n_pwr_refuted` → `n_pwr_suspect`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>