Architecture notes
A look under the hood: how input becomes messages, how the loop drains and renders, and the two renderer strategies.
Event loop
text
stdin bytes
│
▼
TerminalInputDecoder
│ (KeyPressMsg, WindowSizeMsg, BackgroundColorMsg, …)
▼
Queue<Msg>
│
▼ drain all pending messages first
for msg in queue:
model = model.update(msg)
fire cmd (unawaited — result enqueues next message)
│
▼ render once per batch (FPS-throttled)
renderer.render(model.view())Key properties: all pending messages are drained before each render, so rapid key presses never block each other. Commands are fire-and-forget; their result arrives as the next message. The FPS cap only throttles screen output, not message processing.
Renderers
| Renderer | Strategy | When to use |
|---|---|---|
| AnsiRenderer (default) | Line-level diff | Most terminals |
| CellRenderer | Cell-level diff (per grapheme) | Terminals without ?2026 sync |
The cell-level diff renderer writes only changed cells, giving zero flicker. Synchronized updates (CSI ?2026) are used automatically where the terminal supports them.