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.