Threads, Async I/O, Concurrency, and Backpressure
Zellij’s concurrency follows state ownership.
Dedicated owner threads
One session starts threads for Screen, PTY commands, Wasm plugins, PTY writes,
and background jobs. Each blocks on typed channel instructions and mutates its
own state serially. Pane layout changes therefore do not require many fine-
grained locks inside Screen.
Async work where readiness matters
Spawning a terminal returns an AsyncReader. A Tokio task per terminal awaits
PTY readability using the platform implementation (AsyncFd on Unix). It
sends ScreenInstruction::PtyBytes into a bounded Screen channel of capacity
50. Async I/O lets a few runtime threads wait on many child outputs.
The shared Tokio runtime also supplies timers, HTTP/download work, and oneshot completion waiting. It is not the owner of tabs or pane geometry.
A deliberate sync/async bridge
The internal instruction channels are synchronous. A PTY-reader task crosses
that boundary with spawn_blocking when it sends to Screen. If the bounded
channel fills, pressure reaches the blocking pool rather than stalling a Tokio
worker directly.
Independent write fairness
PTY input uses a separate writer thread because reading and writing the same
terminal in one execution path can deadlock some programs. It keeps a
VecDeque per terminal, writes until the kernel would block, then tries the
next terminal. Each pane’s pending bytes are capped at 10 MiB.
Render coalescing
Every PTY chunk updates pane state, but visible rendering is debounced through
the background-jobs owner. Many bursts within roughly 10 ms become one
RenderToClients. Screen renders only dirty state and serializes ANSI output
per client.
The complete feedback loop is:
child readiness → bounded Screen admission → terminal state mutation
→ debounced render → client output → user input → fair PTY writer
Threads isolate mutable domains. Async tasks multiplex readiness. Bounds and debouncing prevent either mechanism from creating unlimited work.