Raw capture โ raw/zellij.md
url: https://github.com/zellij-org/zellij/ title: Zellij author: Aram Drevekenin and contributors date_fetched: 2026-07-18 date_published: 2021-04-06
Zellij is a terminal multiplexer (like tmux) written in Rust, but with a fundamentally different architecture: it replaces tmux's monolithic C server with a multi-threaded Rust server daemon, adds a WASM-based plugin system for extensible UI, and includes its own built-in terminal emulator. As of v0.45.0, it totals ~296,550 lines of Rust across a workspace of 18+ crates.
The system splits into two processes: a server daemon and a client. Communication between them runs over Unix domain sockets (or Windows named pipes) using protobuf-serialized messages. The client handles terminal I/O and rendering; the server owns all session state, PTY management, and plugin execution.
The server runs six dedicated long-lived threads, each a separate event loop, communicating via multi-producer channels (a custom Bus<T> abstraction wrapping crossbeam-style channels):
Main server loop (zellij-server/src/lib.rs start_server_impl): Accepts IPC connections from clients, spawns one route thread per client, and dispatches ServerInstruction messages to coordinate the other threads. Also manages session state (SessionState with client tracking, pipe associations, watchers, host-query forwarding tokens).
Screen thread (zellij-server/src/screen.rs, 10,698 lines): The largest module. Manages the tab/pane hierarchy, layout application, rendering pipeline, mouse event routing, copy/paste, search, session resurrection, and mobile mode. Holds BTreeMap<usize, Tab> keyed by stable tab IDs with position-based display ordering.
PTY thread (zellij-server/src/pty.rs): Manages pseudo-terminal lifecycle โ spawning commands, reading PTY output, forwarding bytes to the screen thread. Uses the OS's native PTY facilities.
Plugin thread (zellij-server/src/plugins/mod.rs): Runs the WASM runtime (wasmi interpreter). Loads, updates, renders, and unloads plugins. Communicates with plugins via protobuf messages over stdin/stdout through a WasmBridge abstraction.
PTY writer thread (zellij-server/src/pty_writer.rs): Dedicated to writing input bytes into PTYs. Separated from reading to avoid blocking.
Background jobs thread (zellij-server/src/background_jobs.rs): Handles session serialization (periodic snapshots for resurrection), web server management, and other async tasks.
Each thread gets a Bus<T> โ a container holding its channel receivers plus cloned senders to all other threads. The ThreadSenders struct (zellij-server/src/thread_bus.rs) provides typed send_to_screen(), send_to_pty(), send_to_plugin(), etc. methods with error propagation.
Each connected client gets a dedicated route thread (zellij-server/src/route.rs) that reads ClientToServerMsg protobuf messages from the IPC stream, translates user actions into the appropriate thread instructions, and dispatches them. The route thread handles action completion timeouts (1 second default) and coordinates between the client and server threads.
The client (zellij-client/src/lib.rs, 1,517 lines) runs in a separate process. It handles:
- Terminal I/O: Reading from stdin (with ANSI/vt parsing), writing to stdout
- Kitty keyboard protocol: Full implementation for enhanced key event reporting (ENTER_KITTY_KEYBOARD_MODE / EXIT_KITTY_KEYBOARD_MODE)
- Host query forwarding: OSC 99 notification protocol โ the client subscribes to host terminal theme notifications (CSI 2031) and queries the host theme (DSR 996), forwarding responses to the server
- Web client support: WebSocket-based client for browser access to sessions
- Remote attach: Connects to sessions over HTTP/WebSocket
vte::Parser which advances a Grid state machineGrid (zellij-server/src/panes/grid.rs, 5,044 lines) is Zellij's built-in terminal emulator โ maintains character cells, cursor position, scrollback, selection state, and Sixel image supportrender() function returns bytes that get composited into the outputTab (zellij-server/src/tab/mod.rs, 7,547 lines) composes pane grids, adds UI chrome (pane frames, tab bar), and produces per-client render outputOutput module (zellij-server/src/output/mod.rs) serializes rendered content as CharacterChunk and SixelImageChunk structuresUnlike tmux which largely passes through terminal escape sequences, Zellij has its own full terminal emulator. The Grid struct maintains a 2D character buffer with attributes (colors, bold, italic, underline, etc.), scrollback, cursor state, and handles the full VT/xterm escape sequence repertoire via the vte crate. This enables:
- Reliable copy/paste from scrollback across all panes
- Per-pane scrollback with search
- Sixel image rendering (inline graphics in the terminal)
- Hyperlink tracking with OSC 8 support
- Pane content diffing for efficient rendering
Zellij implements a clever multiplexer-aware notification system using OSC 99 escape sequences:
Grid namespaces the i= (notification ID) field with the pane ID: i=p<pane_id>[r][q].<original_id>forward_paused flag on TerminalPane suspends processing of further PTY output until the host reply arrives, ensuring correct stream orderingThis is more sophisticated than tmux's approach to terminal queries and enables proper integration with modern terminal features.
Plugins compile to WASM and run in a sandboxed interpreter (wasmi, NOT a JIT). Communication between the host and plugin uses protobuf messages through stdin/stdout:
Plugin API (zellij-tile/src/lib.rs):
- ZellijPlugin trait: load(), update(Event) -> bool, pipe(PipeMessage) -> bool, render(rows, cols)
- ZellijWorker trait: Background workers with message passing โ on_message(message, payload)
- register_plugin!() macro: Generates WASM export functions (load, update, pipe, render, plugin_version) that deserialize protobuf from stdin and call the trait methods
- shim module: Host function calls (subscribe, unsubscribe, set_selectable, open_file, switch_tab, etc.) serialized as protobuf and written to stdout
Plugin loading (zellij-server/src/plugins/wasm_bridge.rs):
- PluginLoader: Handles downloading and caching plugins from URLs/filesystem
- PluginMap: Tracks running plugins, their subscriptions, and client associations
- WasmBridge: Orchestrates plugin lifecycle โ load, update, render, resize, unload
- zellij_exports.rs: WASI host functions exposed to plugins (file I/O, stdin/stdout)
- pipes.rs: CLI pipe mechanism โ external processes can pipe data to plugins
Built-in plugins (in default-plugins/): tab-bar, status-bar, compact-bar, session-manager, configuration (setup wizard), plugin-manager, layout-manager, strider (file browser), about, share, multiple-select, link, mobile.
Zellij supports per-client runtime configuration that overrides the saved config. The SessionConfiguration struct maintains:
- saved_config: The config as it exists on disk
- runtime_config: HashMap<ClientId, Config>: Per-client overrides
When a client reconfigures (e.g., changes theme), only that client's view changes. Writing to disk propagates the change to all clients. This enables the "configuration" plugin to act as a visual settings editor.
The background jobs thread periodically serializes session state (tab layout, pane positions, working directories, running commands) to disk. On restart, Zellij can reconstruct the session layout. The serialization uses a custom format in zellij-utils/src/session_serialization.rs.
Layouts are defined in KDL (KDL Document Language) โ a document-oriented configuration language. Zellij parses KDL config files for both session layouts and user configuration. The layout system supports: - Tiled layouts (split panes in various directions) - Floating panes (overlay windows with x/y/width/height) - Swap layouts (predefined alternative arrangements) - Template-based layouts (parameterized configurations) - Tab groups (multiple tabs in a single layout definition)
Zellij adapts to small screens via a mobile mode (zellij-server/src/mobile_mode.rs). When the viewport falls below configurable threshold dimensions (default 60 columns ร 30 rows), Zellij can enter a simplified single-pane-at-a-time view. The MobileLayoutConfiguration routes different client types (terminal vs. web) to mobile mode based on configurable rules.
Zellij can serve sessions over HTTP/WebSocket, enabling browser-based terminal access. Features include: - Authentication tokens (read-only and read-write) - Session sharing (multiple clients viewing/interacting with the same session) - Web client connection management - HTTPS with optional certificate enforcement
Zellij uses OS threads with blocking channel operations rather than an async runtime for its core server components. Each major subsystem (screen, PTY, plugins, PTY writer, background jobs) gets its own dedicated thread with an event loop. This is a deliberate choice: terminal multiplexing is I/O-bound but CPU-light, and threads provide simpler reasoning about state ownership and backpressure than async tasks. The async runtime (tokio) is used only for auxiliary concerns: file watching, web server, and the client's async runtime.
By choosing WASM (and the wasmi interpreter specifically, not a JIT), Zellij gets: - Sandboxing: Plugins cannot access the filesystem, network, or OS unless explicitly granted - Portability: Plugins work identically on all platforms - Crash isolation: A crashing plugin cannot bring down the server - Hot reloading: Plugins can be updated without restarting the session
The trade-off is performance โ WASM interpretation is slower than native code, but for UI plugins (status bars, tab bars) this is negligible. The zellij_exports.rs file gatekeeps all host resources.
Tmux mostly passes escape sequences through to the host terminal, adding its own wrapping. Zellij opted to implement a full terminal emulator (the Grid). This is more work but enables:
- Reliable scrollback and copy/paste independent of the host terminal's capabilities
- Per-pane rendering that can be diffed (only changed cells are sent to the client)
- Proper Sixel image support
- Future-proof terminal feature support (kitty keyboard protocol, OSC 99 notifications)
The trade-off is correctness burden โ Zellij must correctly implement the entire VT/xterm escape sequence repertoire, and bugs in the emulator manifest as visual glitches.
Zellij switched from YAML to KDL for configuration files. KDL is a document-oriented language that's more human-writable than JSON/TOML but more structured than YAML. This choice reflects Zellij's design philosophy: terminal tools should have readable, maintainable configuration.
The PTY writer thread is a dedicated thread for writing input to PTYs, separated from the PTY reader thread. This eliminates a class of deadlocks where the reader thread blocks on a write while holding state the writer needs. It's a concurrency pattern explicitly designed for correctness over minimalism.
display-message hacksWASM plugin system with protobuf IPC: The most architecturally novel aspect. No other terminal multiplexer has a sandboxed, language-agnostic plugin system. The register_plugin!() macro compiles to WASM exports that communicate via protobuf โ the host never trusts the plugin's memory.
Host query forwarding with pane routing: The OSC 99 notification namespacing is a genuinely clever solution to the multiplexer problem โ how does a terminal app inside a multiplexer communicate with the host terminal? Zellij's answer (namespace queries with pane IDs, route responses back) is more sophisticated than tmux's approach.
Per-client rendering: Each attached client gets independently rendered output, enabling different clients to see different themes, pane frame styles, and UI configurations simultaneously.
Session resurrection with layout preservation: Not just saving pane layouts but attempting to reconstruct working directories and running commands.
Mobile mode with automatic detection: Terminal multiplexers typically assume large screens. Zellij's mobile mode acknowledges that people SSH from phones and tablets.
Grid complexity: The built-in terminal emulator means Zellij must reimplement what every terminal already does. Bugs in escape sequence handling (especially edge cases with cursor positioning, scroll regions, or alternate screen) are notoriously hard to fix.
WASM performance ceiling: wasmi interpretation limits plugin performance. Complex plugins (e.g., a file browser rendering large directory trees) may feel sluggish compared to native implementations.
Thread count: Six+ dedicated threads per session plus one per client adds up. While threads are cheap on modern OSes, a system running many Zellij sessions could face scheduler pressure.
Protobuf overhead: Using protobuf for IPC between client and server (rather than a simpler text or binary protocol) adds serialization/deserialization overhead to every keypress and render frame. This is mitigated by the fact that terminal I/O is low-bandwidth.
Single binary coupling: Despite the crate split, all server components compile into one binary. The WASM plugin system is the only truly isolated extension point.