>_PARITY VS tmux
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 1643 — 764 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:
| Path | What |
|---|---|
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.rs | Crate roots: the module tree (which re-points every ported module's file into ported/) plus the binary entry. |
vendor/tmux | Read-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:
| Feature | Invocation | What |
|---|---|---|
| Structured output | list-* -o json|jsonl|csv|tsv|table | Typed, 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 dashboard | ztmux dashboard | A 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 switcher | ztmux switcher | Type-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:
| Command | What |
|---|---|
ztmux active | Focused window/pane of every session. |
ztmux age | Sessions ranked by creation age, oldest first. |
ztmux ahead | How far each repo is ahead of/behind upstream. |
ztmux alerts | Windows with a pending bell/activity/silence alert. |
ztmux autoname | Windows with a pinned name (automatic-rename off). |
ztmux borders | Windows that draw a status line on their pane borders. |
ztmux buffers | The server's paste buffers, largest first. |
ztmux busy | Panes running a program, not idle at a prompt. |
ztmux changes | Uncommitted file count per pane's repo, dirtiest first. |
ztmux cmd | Histogram of commands running across panes. |
ztmux commit | The last commit in every pane's repo. |
ztmux conflicts | Repos with unresolved merge conflicts. |
ztmux connected | Attached clients ranked by connection age. |
ztmux constrain | Attached clients ranked by screen size, smallest first. |
ztmux control | Clients attached in control mode (-CC). |
ztmux cwd | Working directories in use, busiest first. |
ztmux dead | Dead panes still held open in a window. |
ztmux dedup | Find redundant panes (same cwd + command). |
ztmux density | Windows ranked by pane count, most first. |
ztmux destroy | Sessions that self-destruct when the last client detaches. |
ztmux detached | Sessions with no client attached, freshest first. |
ztmux disk | Filesystem usage behind each pane's cwd. |
ztmux doctor | Environment / server health check. |
ztmux elapsed | How long each pane's process has been running. |
ztmux env | Per-session environment overrides. |
ztmux events | Stream server lifecycle events as JSONL. |
ztmux fanout | Sessions ranked by total pane count. |
ztmux finder | Search panes by command/path/title/window. |
ztmux focus | The active pane of every window. |
ztmux git | Git branch + dirty state of every pane's repo. |
ztmux gone | Panes whose working directory no longer exists. |
ztmux graph | Render the server tree as DOT/Mermaid/HTML. |
ztmux grep | Search the live contents of every pane. |
ztmux groups | Cluster sessions by session group. |
ztmux history | Rank panes by scrollback buffer size. |
ztmux hooks | The command hooks configured on each session. |
ztmux idle | Attached clients ranked by time since last activity. |
ztmux info | Deep inspector for a single pane. |
ztmux input | Panes that are ignoring keyboard input. |
ztmux keys | How many key bindings live in each key table. |
ztmux keytable | Clients parked on a non-root key table. |
ztmux layouts | The current layout string of every window. |
ztmux limit | Each session's scrollback capacity, largest first. |
ztmux linked | Windows linked into more than one session. |
ztmux autolock | Sessions set to lock themselves after idle time. |
ztmux marks | The marked pane(s). |
ztmux mem | Panes ranked by process resident memory. |
ztmux mode | Panes currently frozen in a mode. |
ztmux monitor | Windows armed to alert on activity or silence. |
ztmux mouse | Which sessions have mouse mode enabled. |
ztmux named | Windows with a deliberate name, not the running command. |
ztmux nested | Panes running a nested terminal multiplexer. |
ztmux net | Established outbound connections per pane. |
ztmux peek | Dump the visible contents of every pane. |
ztmux piped | Panes with an active pipe-pane capture. |
ztmux ports | Listening TCP ports mapped to panes. |
ztmux project | Project kind and root behind every pane. |
ztmux ps | One-shot pipeable per-pane process table. |
ztmux pstree | Process tree running under every pane. |
ztmux readonly | Clients attached in read-only mode. |
ztmux recent | List sessions ranked by last activity. |
ztmux remain | Windows that keep panes open after they exit. |
ztmux remote | The git remote each pane's repo points at. |
ztmux shells | Panes sitting at a bare shell prompt. |
ztmux size | Report pane geometry, smallest first. |
ztmux snapshot | Dump the whole server as one nested JSON document. |
ztmux solo | Windows holding a single, unsplit pane. |
ztmux ssh | Which panes hold an SSH connection, and where. |
ztmux startcmd | The command line each pane was launched with. |
ztmux stash | Repositories with stashed work behind a pane. |
ztmux state | Panes whose process is in an abnormal state. |
ztmux stats | One-shot server summary report. |
ztmux status | Sessions with the status line turned off. |
ztmux submodules | Repos with submodules and how many are out of sync. |
ztmux sync | Windows with synchronize-panes turned on. |
ztmux tag | The git describe/tag context of every pane's repo. |
ztmux term | Histogram of attached client terminal types. |
ztmux titlebar | Sessions that push a title to the outer terminal. |
ztmux titles | Every pane's advertised title. |
ztmux tree | Print the session/window/pane tree. |
ztmux tty | Map every pane to its terminal device. |
ztmux usage | Per-session CPU/MEM/RSS resource rollup. |
ztmux user | The owner of every pane's process. |
ztmux utf8 | The UTF-8 state of every attached client. |
ztmux vcs | Which version-control system each pane is under. |
ztmux viewers | How many clients are attached to each session. |
ztmux visual | Sessions that show alerts visually instead of just beeping. |
ztmux watch | Top-like live per-pane process monitor. |
ztmux who | Clients attached to the server, by session. |
ztmux winsize | Windows whose sizing mode differs from the default. |
ztmux worktree | Panes sitting in a linked git worktree. |
ztmux writable | Panes whose working directory is read-only. |
ztmux zoom | Windows with a zoomed pane. |
// ACTIONS — dry-run by default, apply with -f:
| Command | What |
|---|---|
ztmux bcast | Broadcast a command to many panes at once. |
ztmux clearall | Free the scrollback of every pane. |
ztmux equalize | Re-balance every multi-pane window's layout. |
ztmux layout | Apply a named layout preset to a window. |
ztmux pick | Batch ops over the multi-pane mark set (sync/unmark/clear/list). |
ztmux prune | Remove dead/empty/idle server objects. |
ztmux revive | Revive every dead pane in place. |
ztmux retitle | Label every pane with its running command. |
ztmux triggers | Run a ztmux command when a regex matches a pane's output (arm/disarm/list/test/wizard). |
// DISCOVERY & CONSOLE — no server required for verbs:
| Command | What |
|---|---|
ztmux verbs | Every 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 repl | A 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:
| Command | What |
|---|---|
ztmux modal | Zellij-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 open | Scans 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 resurrect | Saves 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 sessions | Zellij-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 stack | Zellij-style pane stack (in @ztmux-zellij-mode): the focused pane fills the column, the rest collapse to one-row title bars. |
ztmux tabs | Zellij-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:
| Option | Default | What |
|---|---|---|
@ztmux-ratatui | on | Master switch for the ratatui renderer — palette, menus, clock, display-panes, hint bar. off gives a classic plain-tmux server. |
@ztmux-hint | off | The which-key hint bar on the prefix. modal on turns it on and saves the prior value in @ztmux-modal-saved-hint. |
@ztmux-zellij-mode | off | The 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-autohide | off | Hide 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-bar | unset | Set by ztmux tabs; the status options it overwrote are saved in @ztmux-tab-saved-*. Drive it through tabs on/off. |
@ztmux-modal | unset | Set by ztmux modal. Drive it through modal on/off, which also installs and removes the root entry keys. |
@ztmux-stacked | unset | Per-window; set by ztmux stack to mark the window stacked. |
@ztmux-resurrect-auto | off | The first client to attach spawns a pidfile-guarded daemon that re-saves every 15 minutes. |
@ztmux-resurrect-restore | off | A fresh server also restores the last snapshot once, on start. |
@ztmux-repl-edit-mode | follows status-keys | Key set for the repl editor (vi/emacs); $ZTMUX_REPL_EDIT_MODE overrides it for one console. |
@ztmux_sel | unset | Per-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:
| Path | What | Role |
|---|---|---|
| vendor/tmux | upstream tmux C sources (next-3.7) | Source of truth — every ported module is diffed against its C counterpart |
| src/ported/ | the ztmux port | The 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 . The port report tracks per-function C→Rust coverage: 2,502 of tmux’s 2,696 functions ported so far.vendor/tmux/grid.c:320: grid_create()
>_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):
- A mouse event could kill a window — tmux dispatches keys with
switch (key)over the full 64-bitkey_code. Five ported handlers matchedkey as u8against byte literals, discarding the top bits.KEYC_*codes run sequentially fromKEYC_BASE(0x10e000), so 18 real keys alias an ASCII command letter:KEYC_MOUSEUP11_STATUS_DEFAULT(0x10e078) truncates to'x'— the Kill prompt — andKEYC_DOUBLECLICK11_PANEto'X', Kill Tagged. Each dispatch now gates onkey < 0x80, so only a bare ASCII byte reaches those arms; a build gate fails on anymatch key as u<N>. - "Not found" returned an arbitrary element — C's
TAILQ_FOREACHleaves the loop variable NULL when it runs to completion, and callers branch on that NULL; a Rustforloop that assigns each element retains the last one visited. Five ports had it.cmd_find_clientreturned a session-less client, solock-client -t nosuchdereferencedc->sessionand killed the server — and everyCMD_CLIENT_TFLAGcommand shared it, sodetach-client -t <typo>silently acted on the wrong client instead of erroring.window_pane_set_modereused a wrong-mode entry (type confusion on itsdata);session_group_synchronize_tosynced a lone session from itself and wiped its own window list. - Zeroing a struct that holds a Rust type —
Vec,String,CStringandBoxall need a non-null data pointer, butxcallochands back all-zero bytes.window_client_modedataholdsitem_list: Vec, so the firstdrain(..)inwindow_client_builddereferenced null andchoose-clientkilled the server. Nothing complains at the allocation; it detonates later, far from the cause. Now built withBox::newand reclaimed withBox::from_rawsoDropfrees them — and a build gate fails when any struct with such a field isxcalloc'd, because converting achar *field to an ownedCStringturns every C-style allocation of its struct into UB. - Destroying a pane rebuilt its mode against the dead window —
window_pane_destroycalledwindow_pane_reset_mode_all, the interactive teardown that resizes the next mode, redraws and notifies. C callswindow_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. - Freeing a pointer Rust never allocated —
session_group_findmirrored C's throwaway stack struct used as theRB_FINDkey. 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 gggcalledfree()on a junk pointer and aborted the server. Fixed by searching withrb_find_by: same descent, no fabricated key node. - Mouse-driven TUIs froze mid-pane — a stale
- 1in the SGR-mouse encoder (input_keys.rs) dropped the sequence'sM/mterminator afterxsnprintfwas corrected to exclude the NUL. ztmux wrote\033[<35;69;44with no terminator; crossterm blocked inread()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. - 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$TMUXfor resolution entirely — without-L/-Sit always resolves its ownztmux-<uid>/defaultsocket — while still exporting$TMUXto 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. - Server crash on
bind-key hthenl— 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. - Powerline glyphs rendered as
\202\202\202—utf8_strvis's inner loop never advanced the source pointer (the C iswhile (++src < end …)), so each multibyte UTF-8 character was re-read and mangled into raw bytes plus octal escapes. - even-* layouts off by one —
layout_spread_celldumped the leftover column on the last pane instead of handing it to the leading cells; an 80-col two-pane split came out39|40instead of tmux's40|39. - Pane spawn hung on macOS — the ported
closefromloopedclose()up to the server-raisedRLIMIT_NOFILE; replaced with thelibprocPROC_PIDLISTFDSpath tmux actually compiles on macOS, so the forked child reachesexecvp.
>_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.txt — 289 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:
tests/no_c_alloc_for_rust_types.rs— fails the build when a struct holding aVec/String/CString/Boxis allocated withxcalloc/zeroed/MaybeUninit. Zeroed bytes give those types a null data pointer, which they are not allowed to hold. This matters most during the migration: the moment achar *field becomes an ownedCString, every C-style allocation of its struct silently becomes UB.tests/no_key_code_truncation.rs— fails the build on anymatch key as u<N>, which is what let a mouse event alias the Kill prompt.
Both were verified by mutation: reintroducing the defect makes the gate fail with the offending
file:line and the fix.
>_TESTING
Several layers stack:
- Parity — byte-for-byte against the vendored tmux, 1643 cases (1631 gated), blocking in CI. Two of those surfaces only became testable late:
capture-panereads a pane's backing grid, so drawn output and the command prompt were invisible to it. Attaching a second server inside a pane of the first puts the drawn scene in the outer pane's grid, and makes its keystrokes the inner client's terminal input — which is how the scrollbar, tree-mode and prompt cases reach them. - Unit tests — in-tree, for pure logic: red-black tree invariants under randomized insert/delete, UTF-8 round-trips, layout arithmetic, and the extensions' rendering through ratatui's headless backend.
- Crash gate —
tests/server_survives_bad_targets.rsdrives the real binary on a private socket and asserts the server survives commands whose target resolves to nothing. - Anti-drift gates — the three build gates above (
ported_fn_names_match_c,no_c_alloc_for_rust_types,no_key_code_truncation). - Upstream regress scripts —
regress/carries 32 of tmux's own regression scripts, run against the ztmux binary viamake -C regress. ItsMakefilecurrently ignores failures, so it is a diagnostic harness rather than a gate, and it is not wired into CI. - Fuzzing, two kinds — an in-process
cargo-fuzz(libFuzzer) target overcolour_find_rgbfor panics and assertion failures, and differential fuzzing (fuzz/diff/) that treats real tmux as the oracle: the same generated input drives both binaries and their output is diffed byte-for-byte. Itsinputmode generates random streams of printables, C0 controls, CSI/SGR/OSC/DCS sequences and wide/combining UTF-8 and comparescapture-pane -p; itscopymode runs random copy-mode motion and selection commands and compares the resulting paste buffer. A divergence is minimised and saved underfuzz/diff/repros/(gitignored) for replay with./diff_fuzz.py repro.
>_"DONE RIGHT"
- Start from a working skeleton — a running program to refactor, not a blank page.
- Shrink the unsafe surface — replace raw-pointer intrusive lists and C-isms with safe Rust where behavior allows.
- Verify against C at every step — a module isn't "ported" until it matches the C reference (parity suite).
- Keep it green —
cargo buildandcargo clippystay 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:
| Command | Does |
|---|---|
cargo build --release | Builds the ztmux server + client binary. |
ztmux new-session -s work | Start a session (as tmux). |
ztmux list-panes -a -o table | Every pane, as an aligned table (or -o json). |
ztmux dashboard / ztmux switcher | Launch the live dashboard / the fuzzy switcher. |