// ZTMUX — ENGINEERING REPORT

A Rust port of tmux — full server + client, ported against the tmux C sources.

Status: 100% functional — building, running, and self-hosting its own tooling. ztmux is the tmux program itself — server and client — reimplemented from the C in Rust. It builds and runs (ztmux new-session, ztmux attach), passes its 1643-case parity suite at 100% (byte-for-byte vs upstream tmux; 1631 gated, 12 quarantined, and two divergences recorded as known gaps), logs every bug its harnesses catch in BUGS.md — fixed, or still open and named — and ships original extensions the C tmux has no equivalent for.

>_PARITY VS tmux

1631
Gated cases passing
1643
Total parity cases
100%
vs tmux next-3.7
2
Known divergences (recorded gaps)

ztmux is a port of tmux, so "correct" means tmux itself. The parity suite runs the same inputs through the vendored tmux (reference) and ztmux (port) and compares byte-for-byte — the same shape as the sibling ports (zshrs vs zsh, strykelang vs perl). The suite has grown from a handful of format cases to 1643764 single-format cases (*.fmt) and 879 multi-command shell scenarios (*.sh) — covering formats, arithmetic, conditionals, string ops, options, buffers, windows, panes, layouts, copy mode and capture-pane. Every gated case passes byte-for-byte; 12 are quarantined (they run and are diffed, but a divergence that only appears on the Linux CI runner keeps them out of the gate), and two proven divergences live in parity/known_gaps/ with their reproductions rather than being papered over.

Behaviour ztmux does not implement yet was tracked in a mirror-image suite, parity/known_gaps/, holding cases expected to diverge from the reference — that divergence being the proof a feature was unported. That directory is now empty. The last six gaps closed in order: the OSC 9;4 progress bar, copy-mode line numbers, the prompt cursor, pane scrollbars, tree-mode preview, and switch-mode — the last of which first required porting prompt.c as a reusable struct prompt, since ztmux still carried the pre-split design with the prompt spread across struct client.

>_REPOSITORY LAYOUT

Original code is kept apart from the port so the fidelity gate only judges what is meant to mirror C:

PathWhat
src/ported/The tmux port — 144 modules plus 23 compat shims that mirror vendor/tmux one-to-one, 132k lines. Held to C-name fidelity by the anti-drift gate.
src/extensions/Original ztmux features with no C counterpart, 34k lines — structured output, the Rust event loop, the ratatui UI layer, the dashboard and fuzzy-switcher TUIs, and the family of pipeable client subcommands (see Extensions). Exempt from the gate.
src/lib.rs, src/main.rsCrate roots: the module tree (which re-points every ported module's file into ported/) plus the binary entry.
vendor/tmuxRead-only, SHA-pinned reference — see How the port is built.

>_EXTENSIONS — BEYOND tmux

Because ztmux owns the whole stack, it adds capabilities upstream tmux does not have. ztmux verbs counts 114 of them today, alongside the 92 ported tmux commands and their 78 aliases. Most are plain clients — they re-invoke the ztmux binary and read its own machine-readable output, so they need no linkage to the server internals; the ratatui UI layer is the exception and lives in the server's draw path (see below). The foundation is structured output plus the two interactive TUIs:

FeatureInvocationWhat
Structured outputlist-* -o json|jsonl|csv|tsv|tableTyped, machine-readable output for every list-* command, driven by the same #{…} format engine. table is aligned columns for humans; json feeds scripts and the TUIs below.
Live dashboardztmux dashboardA ratatui TUI: the session→window→pane tree, per-pane detail table, live server stats with a pane-count sparkline, kill-with-confirm, and 1 s auto-refresh.
Fuzzy switcherztmux switcherType-to-filter picker over every session, window, and pane; Enter runs the right switch-client / select-window / select-pane to jump there.

On top of that sits a family of one-shot client subcommands, each resolving the running server over the list-* -o json query layer. The inspection verbs are pipeable tables that also emit -o json / --json; the action verbs are dry-run by default and only mutate when passed -f / --force.

// INSPECTION — read-only, pipeable:

CommandWhat
ztmux activeFocused window/pane of every session.
ztmux ageSessions ranked by creation age, oldest first.
ztmux aheadHow far each repo is ahead of/behind upstream.
ztmux alertsWindows with a pending bell/activity/silence alert.
ztmux autonameWindows with a pinned name (automatic-rename off).
ztmux bordersWindows that draw a status line on their pane borders.
ztmux buffersThe server's paste buffers, largest first.
ztmux busyPanes running a program, not idle at a prompt.
ztmux changesUncommitted file count per pane's repo, dirtiest first.
ztmux cmdHistogram of commands running across panes.
ztmux commitThe last commit in every pane's repo.
ztmux conflictsRepos with unresolved merge conflicts.
ztmux connectedAttached clients ranked by connection age.
ztmux constrainAttached clients ranked by screen size, smallest first.
ztmux controlClients attached in control mode (-CC).
ztmux cwdWorking directories in use, busiest first.
ztmux deadDead panes still held open in a window.
ztmux dedupFind redundant panes (same cwd + command).
ztmux densityWindows ranked by pane count, most first.
ztmux destroySessions that self-destruct when the last client detaches.
ztmux detachedSessions with no client attached, freshest first.
ztmux diskFilesystem usage behind each pane's cwd.
ztmux doctorEnvironment / server health check.
ztmux elapsedHow long each pane's process has been running.
ztmux envPer-session environment overrides.
ztmux eventsStream server lifecycle events as JSONL.
ztmux fanoutSessions ranked by total pane count.
ztmux finderSearch panes by command/path/title/window.
ztmux focusThe active pane of every window.
ztmux gitGit branch + dirty state of every pane's repo.
ztmux gonePanes whose working directory no longer exists.
ztmux graphRender the server tree as DOT/Mermaid/HTML.
ztmux grepSearch the live contents of every pane.
ztmux groupsCluster sessions by session group.
ztmux historyRank panes by scrollback buffer size.
ztmux hooksThe command hooks configured on each session.
ztmux idleAttached clients ranked by time since last activity.
ztmux infoDeep inspector for a single pane.
ztmux inputPanes that are ignoring keyboard input.
ztmux keysHow many key bindings live in each key table.
ztmux keytableClients parked on a non-root key table.
ztmux layoutsThe current layout string of every window.
ztmux limitEach session's scrollback capacity, largest first.
ztmux linkedWindows linked into more than one session.
ztmux autolockSessions set to lock themselves after idle time.
ztmux marksThe marked pane(s).
ztmux memPanes ranked by process resident memory.
ztmux modePanes currently frozen in a mode.
ztmux monitorWindows armed to alert on activity or silence.
ztmux mouseWhich sessions have mouse mode enabled.
ztmux namedWindows with a deliberate name, not the running command.
ztmux nestedPanes running a nested terminal multiplexer.
ztmux netEstablished outbound connections per pane.
ztmux peekDump the visible contents of every pane.
ztmux pipedPanes with an active pipe-pane capture.
ztmux portsListening TCP ports mapped to panes.
ztmux projectProject kind and root behind every pane.
ztmux psOne-shot pipeable per-pane process table.
ztmux pstreeProcess tree running under every pane.
ztmux readonlyClients attached in read-only mode.
ztmux recentList sessions ranked by last activity.
ztmux remainWindows that keep panes open after they exit.
ztmux remoteThe git remote each pane's repo points at.
ztmux shellsPanes sitting at a bare shell prompt.
ztmux sizeReport pane geometry, smallest first.
ztmux snapshotDump the whole server as one nested JSON document.
ztmux soloWindows holding a single, unsplit pane.
ztmux sshWhich panes hold an SSH connection, and where.
ztmux startcmdThe command line each pane was launched with.
ztmux stashRepositories with stashed work behind a pane.
ztmux statePanes whose process is in an abnormal state.
ztmux statsOne-shot server summary report.
ztmux statusSessions with the status line turned off.
ztmux submodulesRepos with submodules and how many are out of sync.
ztmux syncWindows with synchronize-panes turned on.
ztmux tagThe git describe/tag context of every pane's repo.
ztmux termHistogram of attached client terminal types.
ztmux titlebarSessions that push a title to the outer terminal.
ztmux titlesEvery pane's advertised title.
ztmux treePrint the session/window/pane tree.
ztmux ttyMap every pane to its terminal device.
ztmux usagePer-session CPU/MEM/RSS resource rollup.
ztmux userThe owner of every pane's process.
ztmux utf8The UTF-8 state of every attached client.
ztmux vcsWhich version-control system each pane is under.
ztmux viewersHow many clients are attached to each session.
ztmux visualSessions that show alerts visually instead of just beeping.
ztmux watchTop-like live per-pane process monitor.
ztmux whoClients attached to the server, by session.
ztmux winsizeWindows whose sizing mode differs from the default.
ztmux worktreePanes sitting in a linked git worktree.
ztmux writablePanes whose working directory is read-only.
ztmux zoomWindows with a zoomed pane.

// ACTIONS — dry-run by default, apply with -f:

CommandWhat
ztmux bcastBroadcast a command to many panes at once.
ztmux clearallFree the scrollback of every pane.
ztmux equalizeRe-balance every multi-pane window's layout.
ztmux layoutApply a named layout preset to a window.
ztmux pickBatch ops over the multi-pane mark set (sync/unmark/clear/list).
ztmux pruneRemove dead/empty/idle server objects.
ztmux reviveRevive every dead pane in place.
ztmux retitleLabel every pane with its running command.
ztmux triggersRun a ztmux command when a regex matches a pane's output (arm/disarm/list/test/wizard).

// DISCOVERY & CONSOLE — no server required for verbs:

CommandWhat
ztmux verbsEvery verb ztmux answers to — ported commands, aliases, extensions and console builtins — with a one-line description, filterable and -o json capable. Built from the command table and the extension list themselves.
ztmux replA reedline console that runs each line as ztmux <line> against the selected socket: Tab completes the verb, a --prefixed flag, an option's fixed value set and an extension's subcommand; history persists to ~/.ztmux/repl_history, and non-terminal stdin falls back to a plain line reader. Emacs or vi keys, from the first of $ZTMUX_REPL_EDIT_MODE, @ztmux-repl-edit-mode, status-keys and $VISUAL/$EDITOR that names a mode.

// INTERACTIVE & ZELLIJ-STYLE — ratatui surfaces, modal keys, persistence:

CommandWhat
ztmux modalZellij-style modal keybindings — Ctrl-p pane, Ctrl-t tab, Ctrl-n resize, Ctrl-s scroll, Ctrl-o session, Ctrl-g lock — each a sticky key table entered without a prefix. Off by default, since the entry keys are intercepted globally.
ztmux openScans the current pane for URLs and file paths and shows a ratatui picker: Enter opens the selection (URL in open/xdg-open, file in $EDITOR at its file:line), y copies it to the tmux buffer and OS clipboard.
ztmux resurrectSaves every session/window/pane — layout, cwd and command — to ~/.ztmux/resurrect/ and restores them into a fresh server. @ztmux-resurrect-auto on spawns a pidfile-guarded daemon that re-saves every 15 minutes.
ztmux sessionsZellij-style session manager: a ratatui list of sessions — type to filter, Enter switches, Ctrl-r renames, Ctrl-x kills with confirm, Ctrl-n makes a new one.
ztmux stackZellij-style pane stack (in @ztmux-zellij-mode): the focused pane fills the column, the rest collapse to one-row title bars.
ztmux tabsZellij-style top tab bar of windows — session badge, active tab highlighted; restores your prior status settings on tabs off.

>_THE RATATUI UI LAYER

tmux paints its interactive surfaces — menus, clock mode, display-panes — by the server writing cells into a screen or straight to the tty, not by a program that owns a terminal. src/extensions/ratatui_ui.rs bridges that: it lays a ratatui widget tree into a Buffer, then translates every Cell into a tmux grid_cell emitted through screen_write_cell (overlay and mode screens) or tty_cell (direct-to-tty overlays). That bridge is what lets ztmux add surfaces the C tmux has no equivalent for without leaving its draw model: a which-key hint bar on the prefix, a floating command palette with inline completion, ratatui clock and display-panes, edit-scrollback-in-$EDITOR, and multi-pane selective sync whose state is shown on the pane border (synced red, selected orange, trigger-armed cyan) where pane output can never overwrite it.

A separate opt-in, @ztmux-zellij-mode on, insets every pane by a one-cell ring and draws a rounded frame with the pane's name — the zellij model, where a program cannot draw on the frame. prefix C-f toggles a floating pane: a real pane on a floating layout cell above the tiled layout, so it moves and resizes like any other pane (move-pane -P centre, resize-pane -x50% -y50%) and is fully draggable with mouse on. Panes underneath are clipped around it, and a client redraw composites every visible cell — content, borders, pane status lines, floats — into a cached scene of spans before writing, so a float never costs a second pass over the terminal. Rendered screens are compared against the vendored tmux through a real VT emulator, not just model state. @ztmux-ratatui off disables the whole renderer for a classic plain-tmux server, which leaves the default draw path and the byte-for-byte parity suite untouched.

Everything here is configured with ordinary tmux user options, so it all sets from the config file. Unless noted these are global (set -g) and read live, taking effect on the next redraw:

OptionDefaultWhat
@ztmux-ratatuionMaster switch for the ratatui renderer — palette, menus, clock, display-panes, hint bar. off gives a classic plain-tmux server.
@ztmux-hintoffThe which-key hint bar on the prefix. modal on turns it on and saves the prior value in @ztmux-modal-saved-hint.
@ztmux-zellij-modeoffThe framed look: every pane inset by a one-cell ring, drawn with a rounded named box. @ztmux-pane-names is a back-compat alias.
@ztmux-pane-name-format#{pane_index}: #{pane_current_command}tmux format expanded per pane for the name in that frame.
@ztmux-float-autohideoffHide a floating pane while a tiled pane has focus (the zellij model), bringing it back on prefix C-f. Also settable per window.
@ztmux-tab-barunsetSet by ztmux tabs; the status options it overwrote are saved in @ztmux-tab-saved-*. Drive it through tabs on/off.
@ztmux-modalunsetSet by ztmux modal. Drive it through modal on/off, which also installs and removes the root entry keys.
@ztmux-stackedunsetPer-window; set by ztmux stack to mark the window stacked.
@ztmux-resurrect-autooffThe first client to attach spawns a pidfile-guarded daemon that re-saves every 15 minutes.
@ztmux-resurrect-restoreoffA fresh server also restores the last snapshot once, on start.
@ztmux-repl-edit-modefollows status-keysKey set for the repl editor (vi/emacs); $ZTMUX_REPL_EDIT_MODE overrides it for one console.
@ztmux_selunsetPer-pane; set by prefix C-s to mark the pane a member of the multi-pane sync set.

>_HOW THE PORT IS BUILT

Two references, both vendored under vendor/ as plain, read-only, SHA-pinned copies so the clone is self-contained:

PathWhatRole
vendor/tmuxupstream tmux C sources (next-3.7)Source of truth — every ported module is diffed against its C counterpart
src/ported/the ztmux portThe living code we own and evolve toward safe, idiomatic Rust

Every ported function carries a back-link to its C origin as a doc comment, e.g. /// C vendor/tmux/grid.c:320: grid_create(). The port report tracks per-function C→Rust coverage: 2,495 of tmux's 2,696 functions ported so far.

>_BUGS THE HARNESS CAUGHT

Verifying against C at every step turns "looks right" into "is right." A sample of faults the harnesses root-caused to a single line — the parity suite and unit tests, plus a sweep that drives every command against a private socket and, for the paths that only run with a client attached (modes, redraw, status), against a real client on a pty. Aborts are keyed on crash reports rather than on "is the server still up," since a dying client can legitimately take the server with it. Each fault below is now locked by a regression test or a build gate (the full log, including what is still open, is in BUGS.md):

  1. A mouse event could kill a window — tmux dispatches keys with switch (key) over the full 64-bit key_code. Five ported handlers matched key as u8 against byte literals, discarding the top bits. KEYC_* codes run sequentially from KEYC_BASE (0x10e000), so 18 real keys alias an ASCII command letter: KEYC_MOUSEUP11_STATUS_DEFAULT (0x10e078) truncates to 'x' — the Kill prompt — and KEYC_DOUBLECLICK11_PANE to 'X', Kill Tagged. Each dispatch now gates on key < 0x80, so only a bare ASCII byte reaches those arms; a build gate fails on any match key as u<N>.
  2. "Not found" returned an arbitrary element — C's TAILQ_FOREACH leaves the loop variable NULL when it runs to completion, and callers branch on that NULL; a Rust for loop that assigns each element retains the last one visited. Five ports had it. cmd_find_client returned a session-less client, so lock-client -t nosuch dereferenced c->session and killed the server — and every CMD_CLIENT_TFLAG command shared it, so detach-client -t <typo> silently acted on the wrong client instead of erroring. window_pane_set_mode reused a wrong-mode entry (type confusion on its data); session_group_synchronize_to synced a lone session from itself and wiped its own window list.
  3. Zeroing a struct that holds a Rust typeVec, String, CString and Box all need a non-null data pointer, but xcalloc hands back all-zero bytes. window_client_modedata holds item_list: Vec, so the first drain(..) in window_client_build dereferenced null and choose-client killed the server. Nothing complains at the allocation; it detonates later, far from the cause. Now built with Box::new and reclaimed with Box::from_raw so Drop frees them — and a build gate fails when any struct with such a field is xcalloc'd, because converting a char * field to an owned CString turns every C-style allocation of its struct into UB.
  4. Destroying a pane rebuilt its mode against the dead windowwindow_pane_destroy called window_pane_reset_mode_all, the interactive teardown that resizes the next mode, redraws and notifies. C calls window_pane_free_modes, which the port was missing entirely, so tearing down a pane rebuilt the customize-mode tree against a window that was already gone.
  5. Freeing a pointer Rust never allocatedsession_group_find mirrored C's throwaway stack struct used as the RB_FIND key. In Rust (*sg).name = … is a place assignment, so it drops the previous value — uninitialized stack garbage that happened to look like an owned string. ztmux new-session -t ggg called free() on a junk pointer and aborted the server. Fixed by searching with rb_find_by: same descent, no fabricated key node.
  6. Mouse-driven TUIs froze mid-pane — a stale - 1 in the SGR-mouse encoder (input_keys.rs) dropped the sequence's M/m terminator after xsnprintf was corrected to exclude the NUL. ztmux wrote \033[<35;69;44 with no terminator; crossterm blocked in read() waiting for an end byte that never came, so rich ratatui/crossterm panes locked up on any click or focus change while keyboard input still worked.
  7. ztmux hijacked real tmux's socket — it resolved its default socket from $TMUX, so launched inside a tmux pane it connected to tmux's server and spoke protocol 8 at it (server exited unexpectedly). Fixed to ignore $TMUX for resolution entirely — without -L/-S it always resolves its own ztmux-<uid>/default socket — while still exporting $TMUX to its panes, pointed at that socket, so ecosystem tools that only test whether the variable is set keep working. The two multiplexers now run side by side, or nested.
  8. Server crash on bind-key h then l — the red-black tree's delete rebalance (rb_remove_color, compat/tree.rs) rotated around the wrong node. Rebinding a key that already had a default removed the old node first, corrupting the key-bindings tree and segfaulting the server on startup.
  9. Powerline glyphs rendered as \202\202\202utf8_strvis's inner loop never advanced the source pointer (the C is while (++src < end …)), so each multibyte UTF-8 character was re-read and mangled into raw bytes plus octal escapes.
  10. even-* layouts off by onelayout_spread_cell dumped the leftover column on the last pane instead of handing it to the leading cells; an 80-col two-pane split came out 39|40 instead of tmux's 40|39.
  11. Pane spawn hung on macOS — the ported closefrom looped close() up to the server-raised RLIMIT_NOFILE; replaced with the libproc PROC_PIDLISTFDS path tmux actually compiles on macOS, so the forked child reaches execvp.

>_ANTI-DRIFT GATES

A port can be faked by inventing Rust-only "helper" functions that don't exist in tmux, inflating apparent completeness. tests/ported_fn_names_match_c.rs fails the build when a free fn is added under src/ported/ whose name has no counterpart in vendor/tmux. src/extensions/ is exempt by design — it is original code, not a mirror of C — exactly as #[cfg(test)] blocks are skipped. Pre-existing exceptions (libc and event-loop wrappers, Rust glue) are frozen in an allowlist, tests/data/fake_fn_allowlist.txt289 entries today: an audit trail to burn down, not a free pass.

Two more gates grew out of the bugs above, because each was a class rather than a one-off — the kind that the C→Rust ownership migration keeps re-creating:

Both were verified by mutation: reintroducing the defect makes the gate fail with the offending file:line and the fix.

>_TESTING

1631/1631
Gated parity cases (100%)
1,858
Unit tests
2,502
C fns ported (of 2,696)
180k
Lines of Rust

Several layers stack:

>_"DONE RIGHT"

  1. Start from a working skeleton — a running program to refactor, not a blank page.
  2. Shrink the unsafe surface — replace raw-pointer intrusive lists and C-isms with safe Rust where behavior allows.
  3. Verify against C at every step — a module isn't "ported" until it matches the C reference (parity suite).
  4. Keep it greencargo build and cargo clippy stay clean as code comes over.

>_BUILD & RUN

Requires a Rust toolchain and a terminfo database (ncurses) — no C libraries, since the event loop is Rust (src/extensions/event_loop, replacing tmux's libevent). Then the binary ztmux speaks the same commands as tmux — on its own socket namespace (ztmux-<uid>/ under $TMUX_TMPDIR or /tmp, never adopted from $TMUX) so it never collides with a running tmux — plus the extension subcommands:

CommandDoes
cargo build --releaseBuilds the ztmux server + client binary.
ztmux new-session -s workStart a session (as tmux).
ztmux list-panes -a -o tableEvery pane, as an aligned table (or -o json).
ztmux dashboard / ztmux switcherLaunch the live dashboard / the fuzzy switcher.