Pikchr Generator
Author technical diagrams in Pikchr — Brian Kernighan's PIC, modernized for the web — and compile them to themed SVG. One engine. One workflow. Every shape, line, and label flows through the 16-theme palette so the same source renders cleanly in dark mode, light mode, or any other theme.
Why Pikchr
- Deterministic. Same source → same SVG, byte-for-byte. No layout engine roulette.
- Self-contained. Output is a single
<svg>block — no external fonts, scripts, or assets. - Source-controlled. Diff-friendly text. Commit
.pikchrnext to.svgso reviewers see both intent and result. - Native rendering in Fossil, mdBook (with
mdbook-pikchr), Sphinx (sphinxcontrib-kroki), AsciiDoctor, and Obsidian (Adamantine Pick). Pre-render to SVG for everywhere else.
Resolving the skill directory
Every command below uses $SKILL_DIR. Set it once per session:
SKILL_DIR="$(
for c in \
"$HOME/.claude/skills/pikchr-generator" \
"$PWD/.claude/skills/pikchr-generator"; do
[[ -f "$c/SKILL.md" ]] && echo "$c" && break
done
)"
[[ -n "$SKILL_DIR" ]] || { echo "pikchr-generator skill not found"; exit 1; }
First-run check: install the binary
ls "$SKILL_DIR/bin/pikchr" 2>/dev/null || bash "$SKILL_DIR/bin/install-pikchr.sh"
The installer compiles pikchr.c (single-file C source) and drops the binary at $SKILL_DIR/bin/pikchr. Needs a C compiler (cc).
The one workflow
.pikchr (source) → bin/compile.sh --theme NAME → themed .svg
# File input, themed output to stdout
"$SKILL_DIR/bin/compile.sh" --theme tokyo-night diagram.pikchr > diagram.svg
# Stdin
echo 'box "hi"' | "$SKILL_DIR/bin/compile.sh" --theme dracula -
# Prepend stdlib macros (db, actor, lambda, queue, decision, note, ...)
"$SKILL_DIR/bin/compile.sh" --theme nord --with-stdlib diagram.pikchr > diagram.svg
# Offline build offline? Fall back to Kroki HTTP
"$SKILL_DIR/bin/compile.sh" --theme github-dark --kroki diagram.pikchr > diagram.svg
That's the entire interface. No dispatcher, no engine selection, no per-format compiler — compile.sh is the only entry point.
Themes (16)
default (= zinc-dark), zinc-light, zinc-dark, tokyo-night, tokyo-storm, tokyo-light, catppuccin, latte, nord, nord-light, dracula, github, github-dark, solarized, solar-dark, one-dark, cursor-dark.
Themes work via sentinel-color substitution: the SVG body uses 7 fixed hex sentinels (#010203 … #505152); lib/themeize.sh rewrites each one to the chosen theme's concrete hex, injects a <style> block (so default-colored text follows the theme via currentColor), and prepends a full-viewport <rect> painted with the theme's bg. The output is fully baked hex — no var(), no color-mix() — so it renders identically in browsers, librsvg, ImageMagick, kitten icat, GitHub preview, and every other SVG consumer. Templates and the stdlib already use sentinels — your sources should too.
See references/theming.md for the palette structure and how to add a theme.
Templates: start here
The fastest path to a high-quality diagram is to copy a template and adapt it. Every template uses stdlib macros + sentinel colors so themes apply uniformly.
| Template | Use for |
|---|---|
templates/architecture.pikchr |
System architecture (web → API → service → DB, with cache + queue side-branches) |
templates/flowchart.pikchr |
Top-down decision flow with a yes/no branch |
templates/sequence.pikchr |
Sequence-ish (actors + dashed lifelines + horizontal messages — pikchr has no native sequence type) |
templates/state-machine.pikchr |
States + labelled transitions, with backedges |
templates/data-pipeline.pikchr |
Source → transform → sink chain with a side-monitoring branch |
templates/swim-lane.pikchr |
3-lane process diagram with cross-lane arrows |
Authoring discipline
Pikchr rewards a small number of strong habits. Follow these for diagrams that read cleanly and re-theme cleanly.
1. Use the stdlib macros for visual hierarchy
--with-stdlib prepends lib/stdlib.pikchr, which defines pseudo-primitives that map to roles, not shapes:
| Macro | Role | Visual |
|---|---|---|
actor("X") |
primary actors / entry points | accent fill, inverted text |
lambda("X") |
services / functions | accent fill, rounded |
decision("X") |
branches | accent fill, diamond |
db("X") |
persistent stores | surface fill, cylinder |
datastore("X") |
passive files / artifacts | surface fill, file shape |
queue("X") |
queues / streams | muted fill, oval |
cloud("X") |
external services | surface fill, ellipse |
note("X") |
annotations | muted fill, dashed |
When you need a custom shape, use sentinel colors directly (fill 0x202122, color 0x0a0b0c, etc.) — never raw RGB. Raw colors will not theme.
2. Position relatively, not absolutely
Anchor every node off another node's edge. Use compass anchors (A.e, B.s, C.ne) and offsets (A.e + (0.5, 0)) instead of absolute coordinates. When you change a label and a node grows, the rest of the diagram still lines up.
# Good
API: lambda("Service") at Web.e + (1.5, 0)
# Brittle
API: lambda("Service") at (3.5, 0)
3. Single pass — define before reference
Pikchr parses top-to-bottom in a single pass. Forward references break. Put labels (capitalized: Web, API, DB) in the order the reader's eye will travel.
4. Macro args are unquoted
Token-level lexical substitution runs before the parser. "$1" inside a macro body is one literal string token — substitution never fires inside it.
# Correct — caller supplies quotes
define step { box $1 fit fill 0x202122 color 0x010203 }
step("Validate") # → box "Validate" fit ...
# Wrong — $1 is literal
define step { box "$1" fit }
step("Validate") # → box "$1" fit (literal label)
5. Manhattan routing for arrows that turn
arrow from A.s down 0.3in then right until even with B then to B.n produces a clean L-shape. Use \ for line continuation when the path gets long.
6. Five strings max per object
Each object can carry up to 5 string labels (multi-line). For more, use a separate text primitive at the desired position.
7. Containers, not curly braces
[ ... ] groups objects. { ... } is only for define macro bodies. Mixing them is a common first-time error.
8. One diagram, one direction
Set right / down / left / up once at the top. Containers ([ ... ]) get their own local direction that doesn't leak out.
Visual style — the SQLite-arch idiom
The official pikchr examples (SQLite architecture, VCS graphs, Fossil swim-lane) share a distinct, polished feel. These nine patterns produce that look.
1. Globals tuning block at the top
Every serious diagram opens with a tuning block. Pick density before shapes land.
scale = 0.85 # overall scale (0.7–1.0 for dense, >1 for sparse)
lineht *= 0.4 # tighten arrow length (default = boxht)
$margin = lineht * 2.5 # one variable for region/group padding everywhere
fontscale = 1.05 # nudge labels up if they look small
charht *= 1.15 # extra leading on multi-line labels
fill = 0x010203 # default fill (sentinel = theme bg)
down # primary direction
2. box same chain for uniform sizing
The single biggest "this looks designed" trick. The first named box sets wid/ht/fill/thickness/rad; every sibling repeats with box same "Label". The whole row of shapes carries the same dimensions.
In: box "Interface" wid 150% ht 75% thickness 1.5px rad 5px fill 0x010203
arrow
CP: box same "Command" "Processor" # inherits wid, ht, fill, rad, thickness
arrow
VM: box same "Virtual Machine"
To inherit from a non-adjacent shape: box same as 1st box.
3. Group regions = invisible rect + italic side label
Cluster related shapes inside a colored region drawn behind them. Compute the rect's extent from named anchors so it auto-resizes when labels grow.
# A region wrapping In..VM with $margin breathing room
box ht (In.n.y - VM.s.y) + $margin wid In.wid + $margin \
at CP fill 0x404142 behind In thin
# Italic side label running up the rect's left edge
line invis from 0.25*$margin east of last.sw up last.ht \
"Core" italic aligned
Use sentinel hex (0x404142 = surface, 0x303132 = muted) so themes apply. Stack 2–3 regions for hierarchy (foreground / mid / background).
4. Edge-fraction anchors for arrows
When two arrows enter the same shape, don't both hit the same anchor — split the edge with a fraction.
arrow from CP to 1/4<Tokenizer.sw, Tokenizer.nw> chop # quarter down west edge
arrow from 1/3<CG.nw, CG.sw> to CP chop # third down west edge of CG
5. chop every connector
Default arrows draw to shape centers and pierce the edge. chop clips to the edge — every connection in the official examples uses it.
arrow from A to B chop # endpoint clipped at B's edge
arrow from A to B.s chop # ditto, even when targeting a compass anchor
6. Polar positioning for branches
For VCS-style diagrams or anything with consistent angled branches, place via heading from a known node:
circle "C3" at dist(C2, C4) heading 30 from C2
Distance and angle stay consistent as upstream nodes shift.
7. Italic for meta-labels, plain for content
Group labels, axis names, region tags use italic. Content labels (node names, edge text) stay plain. Visual hierarchy without color.
"Backend" italic aligned # group label
arrow "200 OK" above # content label
8. Restrained palette
Use the sentinel palette for all theme-driven color (0x010203–0x505152). For semantic accents you intentionally want fixed (errors, warnings), pick desaturated values: 0xfecaca light red, 0xc6e2ff pale blue, 0xd8ecd0 pale green. Avoid named colors (red, lightcyan) — pikchr maps them to vivid hex that clashes.
Group regions: layer 2–3 pastels, each thin color gray (or sentinel 0x101112 line) for a stroke.
9. Two-letter capitalized anchor names
Pikchr labels must start with a capital. The official examples use 2–3 letters: In:, CP:, VM:, BT:, CG:, A1:, B3:. Forces conciseness, reads like math, leaves arrow lines short and aligned.
# Good
DB: db("Postgres")
Svc: lambda("Service")
# Worse
DatabasePrimary: db("Postgres")
ServiceTier: lambda("Service")
Anti-patterns
| Don't | Why |
|---|---|
Hard-coded RGB (fill 0x4a90e2) |
Bypasses the theme pipeline; renders the same in every theme |
Color names (fill lightcyan) |
Same problem — pikchr maps these to fixed hex |
Absolute coordinates (at (3, -2)) |
Breaks when any upstream label resizes |
| Forward references | Single-pass parser; the second pass doesn't exist |
{ ... } for grouping |
That's macro-body syntax. Use [ ... ] |
Quoted $1 in a macro body |
Caller supplies quotes — "$1" is a literal token |
| 6+ strings on one shape | Hard limit of 5; use a text primitive for the rest |
Delivering the diagram
Did the user ask to see the diagram NOW?
├── YES → compile.sh → Read the SVG (multimodal display)
└── NO → Where will it live?
├── GitHub README / generic markdown → compile.sh → commit the .svg
├── Fossil / mdBook+pikchr-plugin / Obsidian+Adamantine
│ → leave as `.pikchr` source in a fenced code block
└── Custom site → compile.sh → embed the <svg> inline
For the full surface-by-surface compatibility matrix and gotchas (GitHub strips <script>, <img src> doesn't inherit currentColor, etc.), see references/renderers.md.
Quick start
# 1. Copy a template
cp "$SKILL_DIR/templates/architecture.pikchr" /tmp/diagram.pikchr
# 2. Edit it (labels, structure)
# 3. Compile + theme
"$SKILL_DIR/bin/compile.sh" --with-stdlib --theme tokyo-night /tmp/diagram.pikchr > /tmp/diagram.svg
# 4. Display via multimodal Read (or commit /tmp/diagram.svg into the repo)
Reference materials
references/syntax.md— pikchr language reference (primitives, attributes, layout, cheatsheet)references/theming.md— palette structure, sentinel mapping, adding new themesreferences/renderers.md— where pikchr renders natively vs. needs pre-renderreferences/stdlib-reference.md— the macro stdlib, every role explainedlib/stdlib.pikchr— read the source; it's 30 lines and shows every macro
Layout
SKILL.md — this file
bin/
pikchr — compiled binary
install-pikchr.sh — builds pikchr.c → bin/pikchr
compile.sh — only entry point: source → themed SVG
lib/
stdlib.pikchr — pseudo-primitive macros (actor, lambda, db, …)
themeize.sh — SVG post-processor: sentinel hex → theme hex + <style> + bg rect
themes.json — 16 themes × 7 tokens
templates/ — 6 starting points (all use stdlib + sentinels)
references/ — language, theming, renderers, stdlib docs
test/ — bash smoke tests (run.sh runs all)