Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.