A scriptable Python library — and a native Wayland GTK4 viewer — for DIYLC circuit layouts.
- Write layouts in Python or JSON, save them as
.diy(opens in DIYLC). - Read existing
.diyfiles back into Python (100% component recognition on the DIYLC community corpus). - Preview natively — SVG for browsers, PNG via cairo, or a Wayland-native GTK4 viewer with pan/zoom/click-select and drag-to-move-with-diff.
- CLI (
pydiylc convert/render/info) and an MCP server for LLM clients.
DIYLC itself remains the canonical renderer; pydiylc produces and consumes the
same .diy format and adds a scriptable, Java-free, Wayland-friendly workflow
on top.
New here?
docs/GETTING_STARTED.mdis a 15-minute beginner walkthrough — assumes no Python virtual-environment experience and gets you fromgit cloneto an open editor window.
Beta (v0.2.0). 99 component types spanning boards, passives, semiconductors,
connectivity, electromechanical hardware, tubes, chassis cutouts, guitar
hardware, schematic symbols, shapes, SMD, and labels. The full taxonomy lives
in LLMS.txt and the machine-readable schema in
catalog.json.
Corpus coverage: 100% component recognition on the DIYLC regression
corpus — all 106,599 components across 709 of 711 real community layouts
(including legacy v1 <Layout> files) parse cleanly. All 709 readable files
round-trip with identical component count + type signature through read →
save → re-read; zero drift, zero errors. The 2 unreadable files are physically
truncated XML in the upstream corpus, not a parser limitation.
LLM stress matrix: 8 end-to-end stress tests build real layouts through the MCP tool surface — LPB-1 booster, Champ amp schematic, Big Muff fuzz, two-stage tube preamp with negative feedback, two-channel switching amp, Stratocaster-style guitar wiring, tube-amp power supply, and a stompbox enclosure. Each test surfaced and locked in 1–21 real bugs ranging from single-component pin counts to systematic LLM-correction error messages.
pydiylc convert layout.py layout.diy # build from Python
pydiylc convert layout.diy layout.json # extract to JSON
pydiylc render layout.diy --out preview.svg # SVG preview
pydiylc info layout.diy # summary + warningspydiylc convert accepts .py, .json, or .diy in and writes .diy,
.json, or .svg out. Full py → diy → json → diy round-trip is exercised
in the test suite. The Python loader recognizes project = Project(...),
def build(), def main(), or any top-level Project assignment.
A complete pedal layout — stripboard, transistor, pot, 3PDT bypass, DC and 1/4" jacks — is buildable in code or via JSON (see examples/demo_lpb1_stripboard.py).
.diy files can also be read back into a Project:
p = Project.read("downloaded_pedal.diy")Tolerates three on-disk formats: modern (4.x, <project> root), v3 XStream (<org.diylc.core.Project> root with nested measures + cross-element references), and legacy v1 (<Layout> root, integer perfboard-hole coordinates, attribute-only elements). v1 layouts get an implicit PerfBoard / VeroBoard / BlankBoard synthesized from Layout.Type. Unknown component types are dropped with a warning instead of failing the parse.
pydiylc.svg.render_svg(project) produces a quick-preview SVG of the layout. Component shape, position, and color are right; it is not pixel-identical to DIYLC's renderer. Good for iterating in a browser without launching DIYLC.
from pydiylc.svg import render_svg_file
render_svg_file(p, "preview.svg") # open in any browserSee examples/demo_render_svg.py for a complete LPB-1 → SVG example.
A native viewer ships with the package. It draws via Cairo (no Java, no Swing, no XWayland), reloads when your source file changes, and supports pan/zoom + click-to-select.
pip install -e ".[viewer]" # installs PyGObject + pycairo
# you also need GTK 4 from the system: e.g. `sudo apt install gir1.2-gtk-4.0`
pydiylc-view examples/layout_for_viewer.py
pydiylc-view examples/layout.jsonKeyboard:
- R — manual reload
- 0 — reset zoom + pan
- +/- — zoom in/out
- ? — keyboard-shortcut help dialog (which-key style)
- Q / Esc — quit
- right-drag / middle-drag — pan anywhere (left-drag starting on a component also pans)
- scroll — zoom
- click — select component (name shown in header bar)
- Ctrl+click / Shift+click — toggle / add a component in the multi-selection; drag on empty canvas — rubber-band rectangle select (Shift+drag adds to the existing selection)
- Ctrl+G — snap the selection to the 0.1 in grid (whole project if nothing is selected); Ctrl+L — align the multi-selection horizontally (y mean). Both work in and out of edit mode.
- Right-click — context menu: Add component here / Edit value / Duplicate / Rotate / Delete / Send to / Focus. The "Add here" option places the new component at the cursor (snapped to the project grid). The menu opens on release, so a right-drag pans instead of popping the menu.
- Ctrl + drag — move a component; on release the viewer proposes the
source edit (snapped to the grid) and shows a diff with an Apply button.
Auto-apply works when the layout is a
.pyfile whose components are built with keyword args (e.g.Resistor(name="R1", x1=1.0, ...)); otherwise the move stays in-memory and the new coordinates are shown for manual editing.
Your .py file should expose either a top-level project = Project(...) or
a def build() -> Project. The viewer calls it on every reload.
Outside edit mode the viewer is read-only (mouse pan/zoom/click). Press T
to open the component side panel and enable the full keyboard editing
surface. The status bar shows ✎ EDIT while you're in.
- Tab / Shift-Tab — next / previous component; once you drill in, walks the focused component's nodes
- Space — drill into the focused component's nodes / pop back out
- PgUp / PgDn — page 10 components at a time
- arrows — move the focused node by one board hole (grid step off-board)
- Ctrl+arrows — nudge by one grid step; Ctrl+Shift+arrows — fine (1/10)
- / — fuzzy-search to focus any node (searchable navigation)
- g — fuzzy-search to send the focused node onto another node
- a — add a component (auto-wires to focused pin); A — add without wiring
- v — edit the focused component's primary value (value/text/resistance/tube_type)
- D — duplicate the focused component (offset 0.3in right, incremented name)
- dd — delete the focused component (press
dtwice) - u / Ctrl+Z — undo the last edit
- U / Ctrl+Y — redo the last undone edit
- R / Shift+R — rotate 90° CW / CCW (cycles
orientationfor oriented parts, rotates coordinates otherwise) - Multi-selection bulk edits — with several components selected (Ctrl/Shift+click or rubber-band), arrows nudge them together, R / Shift+R rotates each one, v edits the shared value field, D duplicates them all (the clones become the new selection), and dd deletes them all
- Enter — save the working buffer to disk (silent)
- Ctrl+S — save with the diff-on-save dialog (gated by a "don't show
again" preference, persisted in
~/.config/pydiylc/prefs.json) - Esc / Q — exit edit mode
Moves and rotations carry what's soldered to them. Move a pad and the wire joined to it comes along — the far end stays anchored, so the lead stretches. Move a board and the components mounted on it ride with it. Nothing else follows.
In particular a wire never drags another wire: both ends are elastic, so two of them sharing a coordinate are merely touching, not joined. Parking a line on top of a bus and taking it off again leaves the bus exactly where it was.
Hold Alt to detach — move or rotate a part out of its joints and leave every wire behind. A node-level move (Tab into a node, then Ctrl+arrow) always moves just that one point, peeling it off its junction; drop it on a pin and it solders there.
Joints are remembered, not guessed. Stack two parts and a wire endpoint at
that coordinate touches both of them — but it is soldered to exactly one, and
which one is a fact about history, not position. pydiylc records the joint
(links.py) while the layout still says it unambiguously and carries it from
there, so you can park a part on top of another and move it off again without
either one stealing the other's wires. The map is runtime state rebuilt from
geometry on load, since the DIYLC file format has nowhere to record it.
See docs/keyboard-tree-editor.md for the full design.
Note: the tree-editor GTK UI is built against a fully unit-tested core (graph, move engine, rotation, navigation — 60+ tests) but the GTK panel and key wiring themselves haven't yet been run on real hardware. Report issues.
For LLM clients (Claude Desktop, Claude Code, mcp-cli, etc.):
pip install -e ".[mcp]"
pydiylc-mcp # stdio transport40 tools spanning the full lifecycle:
- Catalog browsing:
list_component_types,describe_component_type,enum_values,list_categories(passive / boards / tube / …),list_components_in_category,list_enums - Project lifecycle:
create_project,create_project_from_dict,list_projects,delete_project,set_project_metadata - Inspection:
list_components,get_component,find_components(fuzzy),get_pins(control-point coordinates by name),stats(counts + bounding box) - Edits (all undoable, all record history):
add_component,add_components(batch),remove_component,move_component,move_node,move_node_to,rotate_component,duplicate_component,set_value,set_field(any attribute, with type coercion),set_fields(atomic multi-field update),add_wire,connect(wire two named components by pin index — picks nearest pair if pins omitted),align(line components up on x or y),snap_to_grid(clean up off-grid coords) - Validation:
validate(duplicate names, off-canvas geometry, etc.) — run before save - History:
undo,redo,history— full undo stack per project, mirrors the GUI's session history - I/O:
save,render_svg,render_png,to_json,read_diy— all rendering tools acceptreturn_content=Truefor inline content (no disk access needed)
Errors include "did you mean X?" hints for component types, component names, project ids, category slugs, and field names. Mutating tools support dry_run=True to preview without committing.
Resources for the static reference data: pydiylc://catalog and pydiylc://llms.txt.
Prompts for common workflows: build_pedal_layout, modify_existing_layout.
In-memory state is keyed by a project_id argument so a client can manage multiple parallel layouts. Full list with field signatures in LLMS.txt.
LLMS.txt— one-screen overview meant to be fed to a coding assistant. Lists every component, the values its enum fields accept, and the value-string parsing rules.catalog.json— machine-readable schema of every component, regenerated withpython -m pydiylc.catalog. Fields, types, defaults, enum choices, and Measure units. Use this for codegen, prompt-stuffing, or downstream validation.- Every component validates its enum fields at construction with a clear
ValueErrorlisting the allowed values.
pip install -e .from pydiylc import Project, PerfBoard, Resistor, SolderPad, CopperTrace
p = Project(title="Booster", width_cm=10, height_cm=8)
p.add(PerfBoard("Board1", x1=1.0, y1=1.0, x2=3.0, y2=2.5))
p.add(Resistor("R1", x1=1.2, y1=1.2, x2=1.2, y2=1.8, value="10K"))
p.add(SolderPad("P1", x=1.2, y=1.2))
p.add(SolderPad("P2", x=1.2, y=1.8))
p.add(CopperTrace("T1", points=[(1.2, 1.8), (2.0, 1.8)]))
p.save("booster.diy")Or from JSON (the preferred path for LLMs / external tools):
from pydiylc import Project
Project.from_dict({
"title": "Booster",
"components": [
{"type": "PerfBoard", "name": "Board1", "x1": 1.0, "y1": 1.0, "x2": 3.0, "y2": 2.5},
{"type": "Resistor", "name": "R1", "x1": 1.2, "y1": 1.2, "x2": 1.2, "y2": 1.8, "value": "10K"},
],
}).save("booster.diy")Open booster.diy in DIYLC.
DIYLC ships file format v4.x. v5.x of the application reuses the same on-disk
schema with additive components. pydiylc targets that schema and currently
writes <fileVersion>5.7.0</fileVersion> so recent DIYLC builds open without a
"created with older version" warning.
Coordinates are in inches by default (DIYLC's grid is 0.1 in).
GPL-3.0-or-later, matching upstream DIYLC.