Build a Smaller Zellij
Build a one-session terminal multiplexer, not a parser that merely recognizes Zellij commands.
What are we preserving?
- a client/session process boundary;
- one owner for screen state;
- typed messages between input, screen, PTY, and output;
- async PTY reading and isolated nonblocking PTY writing;
- ANSI parsing into a persistent terminal grid;
- debounced dirty rendering;
- action completion and coordinated shutdown.
Stage 1: one child and one PTY
Spawn a shell under a pseudo-terminal. Forward the user terminal’s bytes to it and its bytes back. Restore the host terminal mode on every exit path.
Stage 2: model a terminal grid
Feed child output through a VTE parser. Store cells, cursor, dimensions, and a scrollback deque. Render the model rather than blindly echoing child bytes.
Stage 3: make Screen the owner
Move the grid into a Screen thread receiving:
enum ScreenInstruction {
PtyBytes(PaneId, Vec<u8>),
Key(PaneId, Vec<u8>, Completion),
Resize(Size),
Render,
Exit,
}
Only this thread mutates panes, focus, and geometry.
Stage 4: add two panes
Give each pane an ID, PTY, grid, and write queue. Implement focus switching and a vertical split. The key route decides whether input changes focus or enters the active terminal.
Stage 5: async readers, fair writer
Run one async reader future per PTY and feed a bounded Screen channel. Create a
dedicated writer owner with one VecDeque per pane and nonblocking partial
writes. Cap pending bytes per pane.
Stage 6: debounce rendering
Mark panes dirty on VTE changes. Convert many render requests within 10–16 ms into one repaint. Record how many PTY chunks arrive per visible frame.
Stage 7: logical completion and shutdown
Attach a oneshot sender to focus/layout actions and resolve it by dropping an RAII token at the terminal owner. On shutdown, stop input admission, signal owners, close children, join threads, and restore the client terminal.
Failure exercises
- Make one child stop reading stdin; verify the other pane still receives keys.
- Flood stdout; verify bounded Screen admission.
- Emit one character per write; verify render coalescing.
- Kill a child mid-frame; keep the other pane alive.
- Detach the client while children run, then attach another client.
- Shut down with pending writes and verify every owner terminates.
Compare with production Zellij
Production adds rich layouts, floating and stacked panes, multiple clients and watchers, Wasm plugins, session serialization, images and hyperlinks, keyboard protocols, mouse handling, cross-platform PTYs, web sharing, configuration reload, nested sessions, extensive UI policy, and recovery behavior.
Your smaller version succeeds if its concurrency graph is genuine: each owner has a clear state boundary, PTY waits do not consume one thread each, a slow pane cannot freeze every pane, and repaint work is visibly coalesced.