Drawing in plain text
Whenever I need to explain a system, I'd rather draw it in the prose itself than link to a screenshot or a Figma board. Inline diagrams need no external assets, live in git history, are searchable, and don't break in five years.
The toolkit is a handful of box-drawing characters and a few arrows. Quietly tucked into the body, ready to show structure at a glance.
Architecture: boxes and arrows
For explaining how a system fits together. Components as boxes, communication as arrows.
┌─────────────────┐ ┌─────────────────┐
│ Browser (UI) │ ───► │ CDN (cache) │
└─────────────────┘ └────────┬────────┘
│ miss
▼
┌─────────────────┐
│ Workers (SSR) │
└────────┬────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ D1 (SQL) │ │ R2 (img) │ │ KV (sess)│
└──────────┘ └──────────┘ └──────────┘
The single direction of flow, where the cache miss happens, what each store is for — all visible in one screen.
Tree: hierarchies and directories
For file structures or any kind of nested data. The standard tree(1) shape is what most readers already know.
src/
├── lib/
│ ├── db.ts
│ ├── auth.ts
│ └── markdown.ts
├── pages/
│ ├── index.astro
│ ├── admin/
│ │ └── index.astro
│ └── posts/
│ └── [slug].astro
└── styles/
├── tokens.css
└── typography.css
Four glyphs do all the work: ├── │ └──. The last child of any node uses └──; everything else uses ├──. Indentation continues with │ if more siblings remain, or if not.
Pipeline: data through stages
When data is transformed in steps. A single horizontal line carries the spine, with side notes branching down.
[markdown] → [parse] → [render] → [cache] → [response]
│ │ │ │
│ │ │ └─► KV, 1 day TTL
│ │ └─► shiki theme applied
│ └─► markdown-it tokenizer
└─► PUT /api/posts body_md
The main path stays straight; details fall away below. Each side note answers the implicit question "what happens at this step".
Sequence: events over time
For showing how multiple actors exchange messages. The ASCII version of a UML sequence diagram.
Client Worker D1
│ │ │
│ PUT /post ──► │
│ │ UPDATE ────►│
│ │ │
│ │◄──── ok │
│ │ │
│◄─── 200 ───── │
│ │ │
Vertical lines are lifelines, horizontal arrows are messages, time flows top to bottom. Use a dashed arrow (╌►) for asynchronous calls.
Call stack: depth of invocation
For tracing a path through code, often when debugging. Indentation does the work.
handleRequest(req)
└─ updatePost(id, body)
├─ getById(id)
│ └─ db.prepare(...).first()
└─ renderMarkdownAsync(body_md)
├─ getHighlighter() ← cold start ~1.2s
└─ md.render(body_md)
Just ├─ and └─ are enough to read the depth. Annotations next to a frame double as a debugging note.
Decision tree: branching logic
For flows that fork on conditions. Often easier to read than a nested if/else in code.
incoming save request
│
├─ has If-Match header?
│ ├─ yes → compare updated_at
│ │ ├─ match → save
│ │ └─ stale → 409 conflict
│ └─ no → save without check (risky)
│
└─ revise reason ?
├─ 'manual' → 1 row in revisions
└─ 'publish' → 1 row with reason='publish'
I'll often draw this first, then write the route. The tree forces you to enumerate every branch before code locks in.
State machine: lifecycle
Which states an object can be in, and which transitions are legal.
┌─────────────────────────┐
│ │
▼ │
┌───────┐ publish ┌───────────┐ archive ┌──────────┐
│ draft │ ────────► │ published │ ────────► │ archived │
└───────┘ └─────┬─────┘ └──────────┘
▲ │
└────── unpublish ────┘
Boxes are states, arrows are transitions, labels name the trigger. One picture answers "how does this thing live".
Timeline: ordered events
For histories where the exact sequence matters. More legible than a markdown bullet list.
2026-04-20 ─┬─ project started (Astro + D1)
│
2026-04-22 ─┼─ TipTap editor wired in
│
2026-04-24 ─┼─ tried Shiki + ink theme
│
2026-04-25 ─┼─ settled on github-light
│
2026-04-26 ─┴─ first post shipped
Dots mark events, the spine carries time. The reader sees "what happened on which day" in a single glance.
Tables: comparison and properties
For option matrices and quick-reference cards. Markdown tables collapse in terminals, so I draw them inside a code block.
Option Latency Search Time Travel
────────────────────────────────────────────────────
Git-as-DB ~500ms grep commit log
D1 (SQLite) ~10ms FTS5 30 days PITR
File system ~1ms ripgrep snapshots
First row is the header, then a line of ─, then data. Column widths are chosen by hand — it takes a moment, but the table never breaks again.
Closing
The whole point of a text diagram is to fit the structure inside one screen. Too large and the meaning escapes; too small and information is missing. Eight to fifteen lines is usually the sweet spot.
Drawing with text alone is a small kind of freedom. No external tools, no image hosting, no broken links. The picture lives in the markdown file forever.