Architecture
High-level architecture overview for contributors.
Last updated: June 2, 2026
Architecture
zurm is a GPU-accelerated terminal emulator for macOS, built on Ebiten (OpenGL game engine) and Go’s os/exec for PTY management.
System Overview
┌──────────────────────────────────────────────────────────┐
│ Ebiten Game Loop │
│ │
│ Update() Draw() Layout() │
│ ─ input routing ─ Renderer.DrawAll ─ DPI scaling │
│ ─ channel drains ─ layer compositing │
│ ─ state updates ─ cache skip logic │
└──────┬─────────────────────┬─────────────────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────────┐
│ Terminal │ │ Renderer │
│ ─ Buffer │◄────│ ─ offscreen │ RLock
│ ─ Parser │ │ ─ blocksLayer │
│ ─ Cursor │ │ ─ modalLayer │
└──────┬───────┘ └──────────────────┘
│ Lock
▼
┌─────────────┐
│ PtyBackend │ ◄── Strategy Pattern
│ (interface)│
├─────────────┤
│ PTYManager │ Mode A: local PTY (os/exec + pty)
│ ServerBack. │ Mode B: zurm-server over HTTP
└─────────────┘
Data Flow
PTY output → PtyBackend.StartReader() goroutine
→ Parser.Feed() [under write lock]
→ ScreenBuffer mutation (cells, cursor, scrollback)
→ RenderGen++ (atomic)
Game.Draw() → Renderer.DrawAll(DrawState)
→ per-pane cache check (skip if gen unchanged)
→ ScreenBuffer.RLock → read cells → RUnlock
→ draw to offscreen → composite layers → GPU
Package Responsibilities
| Package | Responsibility |
|---|---|
main (root) |
Game struct, Update/Draw/Layout, decomposed into 19 focused files (see below) |
terminal/ |
VT500 parser, screen buffer (cells + scrollback), cursor state, PTY backend interface |
renderer/ |
GPU rendering via Ebiten — decomposed into 17+ files (see below) |
pane/ |
Binary tree layout, pane view (NewPane DI constructor), factory for terminal creation |
tab/ |
Tab management, layout node ownership, activity detection |
config/ |
TOML parsing, defaults, hot-reload, theme merge |
session/ |
Session save/restore (JSON) — tab CWDs, titles, layout tree |
zserver/ |
Persistent PTY server — session management, ring buffer replay, subscriber pattern |
fileexplorer/ |
File tree domain logic — pure functions, no rendering dependency |
vault/ |
Encrypted command history, ghost text suggestions, AES-256 encryption |
markdown/ |
Markdown AST parser — headings, code blocks, tables, bold/italic |
help/ |
Keybinding definitions, command palette entries |
recorder/ |
Screenshot (PNG) and screen recording |
Main Package File Structure
The root main package was decomposed from a single main.go into focused files:
| File | Responsibility |
|---|---|
main.go |
Game struct, Update/Draw/Layout, init, entry point |
game_input.go |
Keyboard routing, overlay priority cascade |
game_mouse.go |
Mouse dispatch, selection, divider drag, URL click |
game_drain.go |
PTY drains, polling, clipboard, paste |
game_lifecycle.go |
Focus, resize, suspend, dropped files |
game_tabs.go |
Tab create/close/switch/pin/search/switcher |
game_panes.go |
Pane splits, focus, resize, zoom |
game_overlays.go |
Menus, palette, confirm, overlay handlers |
game_search.go |
SearchController + search input |
game_explorer.go |
File explorer input handlers |
game_viewer.go |
Markdown viewer, llms.txt, URL input |
game_server.go |
Server session management |
game_misc.go |
Rename, notes, session, config reload, shell hooks |
tab_manager.go |
TabManager — owns tab slice, active index, focus history, drag/pin state |
palette_controller.go |
PaletteController — filter state, selection |
explorer_controller.go |
ExplorerController — tree state, expand/collapse |
mouse_state.go |
SelectionDragger + DividerDragHandler value types |
key_repeat.go |
Shared KeyRepeatHandler for auto-repeat across controllers |
status_poller.go |
Background git status + foreground process polling |
Renderer Package File Structure
The renderer/ package was decomposed into focused sub-renderers:
| File | Responsibility |
|---|---|
renderer.go |
Renderer struct, DrawAll orchestration, layer compositing, cache logic |
pane_render.go |
DrawPane, drawPaneTo, drawDividers |
tabbar.go |
drawTabBar |
tabhover.go |
Tab hover thumbnails, cache keys |
tabswitcher.go |
Tab switcher overlay |
tabsearch.go |
Tab search overlay |
statusbar.go |
Status bar rendering |
overlay.go |
Help overlay, markdown viewer overlay |
palette.go |
Command palette rendering |
search.go |
In-buffer search bar |
fileexplorer.go |
File explorer panel |
menu.go |
Context menu rendering |
blocks.go |
OSC 133 command block decorations |
stats.go |
Debug stats overlay |
font.go |
Font renderer, glyph drawing, display width |
renderconfig.go |
RenderConfig — decouples renderer from config package |
state.go |
All 15 UI state types consolidated (DrawState, SearchState, etc.) |
helptypes.go |
OverlayMenuItem, OverlayKeyBinding — decouples from help package |
mdtypes.go |
MdSpanStyle, MdSpan, MdStyledLine — decouples from markdown package |
uicolors.go |
UIColors derivation, color helpers |
Key Design Patterns
Composite — Pane Layout Tree
LayoutNode is a binary tree. Each node is a Leaf (holds a Pane), HSplit (left | right), or VSplit (top / bottom). Ratio controls the split proportion.
HSplit (0.5)
/ \
Leaf(pane1) VSplit(0.6)
/ \
Leaf(pane2) Leaf(pane3)
ComputeRects() recursively assigns pixel rectangles. Leaves() returns all panes in DFS order.
Pane creation uses DI constructors in pane/factory.go: NewLocal() and NewServer() build terminals with the correct backend, while NewPane() wraps a terminal into a Pane view without importing config.
Strategy — PTY Backend
PtyBackend interface abstracts local vs. remote terminal I/O:
type PtyBackend interface {
Write(p []byte) (int, error)
Resize(cols, rows int) error
Dead() <-chan struct{}
Close()
Pid() int
ForegroundPgid() (int, error)
StartReader(parser *Parser, buf *ScreenBuffer, paused *atomic.Bool)
}
- PTYManager (Mode A) — local shell via
os/exec+creack/pty - ServerBackend (Mode B) — connects to zurm-server over HTTP, PTY persists across app restarts
State Machine — VT500 Parser
The parser has 8 states dispatched via method calls:
| State | Trigger | Handles |
|---|---|---|
Ground |
default | printable characters, C0 controls |
Escape |
ESC | escape sequence start |
EscapeInterm |
ESC + intermediate | two-byte escape sequences |
CSIEntry |
ESC [ | control sequence introducer |
CSIParam |
digits/semicolons | CSI parameter collection |
CSIInterm |
intermediate in CSI | CSI with intermediate bytes |
OSC |
ESC ] | operating system commands (title, CWD, shell integration) |
DCS |
ESC P | device control strings (ignored) |
Parser.Feed() processes raw PTY bytes and mutates the ScreenBuffer under the caller’s write lock.
Observer — Server Session Subscriptions
zserver.Session uses a pub-sub pattern for multi-client output:
- PTY reader writes output to all subscriber channels
subscribe()returns a bufferedchan []byte- Slow clients drop packets (non-blocking send with
select/default) ringBuf(64KB circular buffer) stores recent output for replay on reconnect
Snapshot — Lock Minimization
The renderer minimizes lock duration by taking snapshots:
DrawStateis built inGame.Draw()with current stateblockSnapcopies block data underRLock, then renders without holding the lockpaneCacheEntrytracksrenderGen— unchanged panes skipDrawPaneentirely
Concurrency Model
Main Thread (Ebiten)
├── Game.Update() — input handling, state mutations
├── Game.Draw() — renderer calls (single-threaded GPU)
│
PTY Reader Goroutines (one per pane)
├── Read PTY bytes
├── Lock ScreenBuffer
├── Parser.Feed() — mutates cells, cursor, scrollback
├── Unlock ScreenBuffer
│
Async Goroutines
├── Git status polling (background, result via channel)
├── Clipboard operations (paste via channel)
├── Screenshot encoding (PNG in background)
└── CWD/foreground process detection
Synchronization:
sync.RWMutexonScreenBuffer— parser writes, renderer readsatomic.Boolforpausedflag — spin-wait during resize (avoids race between resize and parse)atomic.Uint64forrenderGen— activity detection without lockingatomic.Boolforosc7Active,osc133Active— feature detection flags- Buffered channels for async results (clipboard, git, screenshots)
Rendering Pipeline
Three composited layers:
- offscreen — pane cell grids, tab bar, status bar, pane labels
- blocksLayer — OSC 133 command block decorations (borders, duration badges, copy buttons)
- modalLayer — overlays (search bar, palette, file explorer, help, confirm dialogs)
Final composite: offscreen → blocksLayer (alpha blend) → modalLayer (alpha blend) → screen.
Performance optimizations:
- Per-pane render cache skips unchanged panes (tracks renderGen, viewOffset, cursor position)
layoutDirtyflag triggers full offscreen clear only when layout changesoverlayBg(1×1 backdrop image) created once, scaled to screen size- Auto-idle reduces TPS when window is unfocused
allKeyscomputed once at package init (avoids per-frame allocation)
Session Persistence
~/.config/zurm/session.json
├── version: 1
├── activeTab: int
└── tabs[]
├── cwd, title, pinnedSlot, note
└── layout (recursive PaneLayout tree)
├── kind: "leaf" | "hsplit" | "vsplit"
├── ratio: float64
├── cwd, customName, serverSessionID
└── left, right (children)
Saved on quit (if session.enabled), restored on launch (if restore_on_launch).
Configuration
config.Load()reads~/.config/zurm/config.toml, starts fromDefaults, overlays user valuesconfig.LoadWithMeta()re-reads and returns TOML MetaData for theme merge- Theme merge:
MergeColorsWithMeta()uses TOML metadata to determine user-explicit vs. theme-default colors - Hot-reload via
Cmd+,— callsLoadWithMeta(), applies theme, updates renderer colors, clears pane cache