Architecture¶
This page is for anyone who wants to read or change kigit's code. It explains how the pieces fit together and where to find things. It doesn't go into every function: for that, the headers in include/ are short and commented.
Bird's-eye view¶
kigit takes two versions of a KiCad project, works out what changed, and writes one HTML page that shows it.
It's built from four parts:
| Part | Language | What it does |
|---|---|---|
Core (kigit_core) |
C++20 | Reads, parses, matches and diffs the files, and builds the data for the page |
Command line (kigit) |
C++20 | Parses options, runs the core, prints the summary, writes the page |
Viewer (resources/) |
HTML + JS | Draws the board and schematic in the browser from that data |
GUI (gui/) |
Python (NiceGUI) | A front end that calls the kigit command line |
The core never draws anything, and the viewer never reads KiCad files. They only meet through JSON.
The pipeline¶
Every comparison runs the same steps:
flowchart TD
subgraph UI["① User interaction"]
direction LR
GUI["GUI (NiceGUI)<br/>open a project, pick two commits"] -->|runs| CLI["kigit command<br/>kigit A..B"]
end
subgraph BE["② Backend: kigit (C++)"]
direction LR
R["Read<br/>git show / files"] --> P["Parse<br/>s-expressions"]
P --> M["Model<br/>Board + Schematic"]
M --> D["Diff<br/>match old vs new"]
D --> RP["Report<br/>JSON → HTML"]
end
subgraph FE["③ Frontend: viewer (browser)"]
direction LR
V["viewer.html<br/>embedded JSON"] --> PCB["PCB tab<br/>pcb.js"]
V --> SCH["Schematic tab<br/>sch.js"]
V --> SRC["Source panel<br/>source.js"]
end
UI -->|two revisions| BE
BE -->|one HTML file| FE
The backend also prints a summary to the terminal straight from the diff, and the GUI shows the finished page in its own window. In the viewer, hovering, pinning and zooming all happen in the browser: nothing goes back to the backend.
| Layer | What it is | Where |
|---|---|---|
| ① User interaction | How you ask for a comparison: a kigit command, or the GUI, which runs that same command for you |
src/commands/, gui/ |
| ② Backend | Everything that touches KiCad files: read, parse, model, diff, and write the page. Never draws anything | src/git/, src/utils/, src/pcb/, src/sch/, src/report/ |
| ③ Frontend | The page in the browser: draws the board and schematic from the JSON and handles hover, pinning and zoom. Never reads KiCad files | resources/ |
The backend's steps in more detail:
| Step | What happens | Where |
|---|---|---|
| 1. Read | git show <rev>:<path> for the board and every schematic sheet, or the file on disk for LOCAL / --files. If the board moved in history, it finds the most similar board in that revision. |
git/git.cpp, git/locate.cpp |
| 2. Parse | The text becomes a tree of Nodes (lists and atoms). The parser knows nothing about KiCad; it also records each list's byte span in the file. |
utils/sexpr.cpp |
| 3. Model | The tree becomes KiCad things: a Board (tracks, arcs, vias, pads, footprints, zones) and a Schematic (sheets, symbols, wires, labels, buses), whose nets kigit works out itself. |
pcb/board.cpp, sch/schematic.cpp |
| 4. Diff | Old and new are matched (by KiCad ID, then by geometry) and every item gets a status. Totals are summed per net. | pcb/diff.cpp, sch/sch_diff.cpp |
| 5. Report | The diff becomes JSON for the viewer, plus the old and new text of changed items with --source. |
report/html.cpp, report/source.cpp |
| 6. Page | The JSON is put into the viewer template, which is compiled into the binary, and written as one file. | report/html.cpp (render_html) |
| 7. Draw | The browser runs the page's JS, which draws SVG from the JSON. | resources/*.js |
The terminal summary (the nets table, the schematic changes) is printed from the diff in step 4, not from the JSON. With --json, steps 6 and 7 are skipped and the JSON is written instead.
The node trees and how they become Board and Schematic are shown in detail under From nodes to structs.
Nodes¶
The parser only knows two shapes of node, and every KiCad file is built from them:
| Shape | Looks like | In Node |
|---|---|---|
| List | (width 0.25), (segment ...) |
is_list = true; name() is its first atom ("width"), children holds the rest |
| Atom | width, 0.25, "F.Cu" |
atom holds the text; quoted = true if it was written in quotes |
So (layer "F.Cu") is a list named layer with one quoted atom, and node.arg(0) returns F.Cu. Lists also carry their span, so the source panel can show the exact lines later.
From nodes to structs¶
The KiCad meaning comes in step 3, where each kind of list is turned into a field of Board or Schematic:
flowchart LR
P0["(kicad_pcb)"]
P0 --> P1["(segment)"]
P0 --> P2["(arc)"]
P0 --> P3["(via)"]
P0 --> P4["(footprint)"]
P4 --> P5["(pad)"]
P4 --> P6["(property)"]
P4 --> P7["(fp_line) (fp_arc) (fp_rect)<br/>(fp_circle) (fp_poly) (fp_text)"]
P0 --> P8["(zone)"]
P8 --> P9["(polygon)"]
P8 --> P10["(filled_polygon)"]
P0 --> P11["(gr_line) (gr_arc) (gr_rect)<br/>(gr_circle) (gr_poly) (gr_text)"]
P0 --> P12["(net)"]
P1 -.-> I["items"]
P2 -.-> I
P3 -.-> I
P5 -.-> I
P4 -.-> FP["footprints"]
P6 -.-> FP
P7 -.-> SK["silk<br/>(--silkscreen)"]
P9 -.-> Z["zones"]
P10 -.-> X["skipped"]
P11 -.-> O["outline<br/>(Edge.Cuts)"]
P11 -.-> SK
P12 -.-> N["net names"]
classDef field fill:#7e57c2,stroke:#7e57c2,color:#fff
classDef skip stroke-dasharray:4 3
class I,FP,SK,Z,O,N field
class X skip
Solid lines are the file's nesting; dotted lines show which Board field each node ends up in.
| Node | Becomes | Notes |
|---|---|---|
(segment) |
items, kind Track |
|
(arc) |
items, kind Arc |
|
(via) |
items, kind Via |
|
(pad) inside (footprint) |
items, kind Pad |
Position made absolute from the footprint's (at) |
(footprint) |
footprints |
Reference and value from its (property) lists; its fp_* graphics are compared too |
(zone) |
zones |
Its (filled_polygon) lists are skipped: KiCad recomputes them on every refill |
(gr_*) on Edge.Cuts |
outline |
The board shape |
(gr_*) / (fp_*) on silkscreen |
silk |
Only with --silkscreen |
(net) |
Net names | Older files list nets by number at the top |
flowchart LR
S0["(kicad_sch)"]
S0 --> S1["(lib_symbols)"]
S1 --> S2["(symbol)<br/>part definition"]
S0 --> S3["(symbol)<br/>placed part"]
S0 --> S4["(wire)"]
S0 --> S5["(junction)<br/>(no_connect)"]
S0 --> S6["(label) (global_label)<br/>(hierarchical_label)"]
S0 --> S7["(bus) (bus_entry)<br/>(bus_alias)"]
S0 --> S8["(sheet)"]
S8 --> S9["(pin)"]
S0 --> S10["(text)"]
S0 --> S11["(polyline) (rectangle)<br/>(circle) (arc) (bezier)"]
S2 -.-> D["defs"]
S3 -.-> SY["symbols"]
S4 -.-> W["wires"]
S5 -.-> J["junctions<br/>no_connects"]
S6 -.-> L["labels"]
S9 -.-> L
S7 -.-> B["buses, bus_entries<br/>bus_aliases"]
S8 -.-> SB["sheet_boxes<br/>+ child sheet loaded"]
S10 -.-> T["texts"]
S11 -.-> DR["drawings"]
classDef field fill:#7e57c2,stroke:#7e57c2,color:#fff
class D,SY,W,J,L,B,SB,T,DR field
Solid lines are the file's nesting; dotted lines show which Schematic field each node ends up in.
| Node | Becomes | Notes |
|---|---|---|
(lib_symbols) → (symbol) |
defs |
Each part type's pins and drawing, stored in the file |
(symbol) |
symbols |
A placed part. Power symbols (+3.3V, GND) are symbols too |
(wire) |
wires |
|
(junction), (no_connect) |
junctions, no_connects |
|
(label), (global_label), (hierarchical_label) |
labels |
Their names join nets across wires and sheets |
(bus), (bus_entry), (bus_alias) |
buses, bus_entries, bus_aliases |
|
(sheet) |
sheet_boxes, and its file is loaded as a new sheet |
Through the FileReader |
(pin) inside (sheet) |
labels, kind SheetPin |
Connects the parent sheet to the child's hierarchical labels |
(text) |
texts |
|
(polyline), (rectangle), (circle), (arc), (bezier) |
drawings |
Drawn only; they don't connect anything |
After every sheet is loaded, kigit works out nets from the wires, labels, pins and buses.
Code map¶
Headers live in include/<dir>/, and their sources in src/<dir>/ with the same names.
utils/¶
The building blocks with no KiCad knowledge.
sexpr: the s-expression parser.sexpr::parse(text)returns aNodetree, with each list'sspan(where it starts and ends in the text).platform: everything that differs between Windows and Linux: running programs, finding them on PATH, opening a browser, home and state folders.log: the session log (spdlog), one file per run.
git/¶
git: a thin wrapper around thegitcommand line: resolve a revision,showa file at a revision,log, list files. kigit runs the realgitprogram rather than a library, so it behaves exactly like the user's git.locate: finding the board in a revision when it was moved or renamed (find_similar_board), by comparing footprints and nets.
pcb/¶
board:load_board(text)turns a.kicad_pcbinto aBoard. Tracks, arcs, vias and pads are all oneItemtype with akind, because the diff treats them the same way.diff:diff_boards(old, new)matches items and footprints and returns aDiff: each item with its status, plus per-net and per-footprint totals.
sch/¶
schematic:load_schematic(root_file, reader)loads a schematic and all its sheets, then works out the nets: which pins each wire, label, power symbol and bus connects. It reads sheets through aFileReaderfunction, not from disk, so the same code works for a git revision, the working tree, or a folder.sch_diff:diff_schematics(old, new)matches parts (by KiCad ID, then reference) and nets (by label, then shared pins), and sorts each change into electrical, fields only or cosmetic.netcheck:--check-netlist, which compares kigit's nets with a netlist exported by KiCad. This is how the net logic is tested against KiCad itself.
report/¶
html: turns boards, schematics and diffs into the viewer's JSON (report_data,schematic_view_json), andrender_htmlputs that JSON into the page template.source: the text snippets for the source panel.SourceFileturns aSpaninto whole lines with line numbers;SourceTablecollects one entry per changed item.
commands/¶
The command line, one file per command, in namespace kigit::cmd. Only the kigit executable has these; they aren't part of kigit_core.
| File | Command |
|---|---|
options |
Parsing the command line and --help |
diff |
The main job: steps 1 to 6 above, plus --check-netlist |
summary |
The tables printed to the terminal |
history |
--log: the commit list for the GUI |
projects |
--projects: KiCad projects in a folder, for the GUI |
gui |
kigit gui: finds uv and starts the Python app |
common |
Helpers they share, like finding the project folder |
src/main.cpp starts the log, then picks the command.
resources/ (the viewer)¶
| File | What it does |
|---|---|
viewer.html |
The page shell: layout, styles, and slots for the data and scripts |
pcb.js |
The PCB tab: drawing, hover, pinning, the net list |
sch.js |
The schematic tab: sheets, parts, wires, nets |
source.js |
The source panel: the side-by-side text diff (line diff done in JS) |
shell.js |
The tabs and everything shared between them |
At build time, CMake puts the four scripts into viewer.html and compiles the result into the binary as a byte array. At run time, render_html replaces the /*KIGIT_DATA*/ slot with the JSON. That's why the output is a single file with no dependencies.
gui/¶
A NiceGUI app (gui/kigit_gui/). It doesn't link to the core: it runs the kigit command line (kigit.py) and uses its JSON output (--projects, --log, --json) or its page (-o -).
app.py: the screens (open a project, pick commits, compare).kigit.py: calling thekigitbinary.folder_picker.py,logs.py: the folder dialog and the log view.
kigit gui starts it with uv, which sets up the Python environment on first run.
Rules that hold everywhere¶
These are worth keeping when you change things.
- The core doesn't draw, and the viewer doesn't parse. All drawing is in
resources/*.js; all KiCad file reading is in C++. They only share JSON. - The parser knows nothing about KiCad.
sexprreturns lists and atoms. Meaning (what asegmentis) belongs inpcb/andsch/. - OS-specific code lives in
utils/platformonly. Everything else is plain C++ and builds the same on Windows and Linux. - git is only used through
git/. Nothing else runs git. - Schematics are read through a
FileReader. That keeps the loader independent of where the files come from. - stdout stays clean. With
-o -the page or JSON goes to stdout, so messages go to stderr or the log. - The page is one self-contained file. No server, no external scripts. It must open from disk, years later.
- Big extras are opt-in. Silkscreen (
--silkscreen) and the source panel (--source) make pages bigger, so they're off by default.
Build and dependencies¶
- CMake builds
kigit_core(a static library with everything except the command line) and thekigitexecutable. Tests linkkigit_coredirectly. - Libraries are git submodules in
submodules/: nlohmann_json (JSON), spdlog (logging), doctest (tests). - CI (GitHub Actions) builds and tests on Windows (MSVC) and Linux (GCC) on every push. Tagged releases build the Windows installer, the Windows zip and the Linux tarball.
Where to start¶
| You want to... | Look at |
|---|---|
| Support a new KiCad item on the board | pcb/board.cpp (load it), pcb/diff.cpp (match it), report/html.cpp (send it), resources/pcb.js (draw it) |
| Change how items are matched | pcb/diff.cpp or sch/sch_diff.cpp |
| Fix a schematic net that's wrong | sch/schematic.cpp, and test it with --check-netlist against KiCad's netlist |
| Change what the terminal prints | commands/summary.cpp |
| Add a command-line option | commands/options.cpp, then use it in the command |
| Change the look of the viewer | resources/viewer.html (styles) and the matching .js file |
| Change the GUI | gui/kigit_gui/app.py |