# mplex: tai's terminal multiplexer Specification of what the multiplexer does today. The code is the `Mplex*` classes in `tai.py`; the default key tables, command specs and option definitions in this file are checked against that code by `MplexSpecTests` in `test_tai.py`, so this page cannot silently drift. Gaps are listed in [Differences from tmux](#differences-from-tmux-and-gaps), next to the code that would close them. ## What it is A tmux-shaped multiplexer inside the single `tai.py` file, stdlib only. Key bindings, command names, command flags and option names follow tmux 3.6; the default tables were dumped from the real binary (research: `research/shell-multiplexer-auto-attach/local-tmux-3-6-keybinding-and-option-dumps.md`). The default prefix is `C-b`, as in tmux. Everything is rebindable in `~/.tai/mplex.toml`. ``` tai.py --mplex new session, attach this terminal (what the shell hook runs) tai.py --mplex attach attach to the most recently used session tai.py --mplex new -s work -d a detached session called work tai.py --mplex ls list sessions tai.py --mplex run a command against the running server (split-window -h, list-keys, ...) tai.py --plexer-wrap the same as bare --mplex; this is what the installed shell hooks exec ``` Commands follow tmux's grammar: `;` as its own argument separates commands (`\;` is a literal semicolon argument), targets use `-t session:window.pane`, `%pane`, `@window`, `$session`, `+`, `-`, `!`. Commands print their output and exit 0, or print the error and exit 1. Inside a pane `TAI_PLEXER_ACTIVE=1`, `TAI_PLEXER_SOCKET=` and `TAI_PLEXER_PANE=` are set, so the agent tools (`plexer_send`, `plexer_read`, `plexer_type`, `plexer_status`) reach the pane they run in. Attaching from inside a pane is refused ("sessions should be nested with care"), as in tmux. If you run mplex inside tmux, both use `C-b`: set `prefix = "C-a"` in `mplex.toml`, or send `C-b C-b`. ## Architecture - **Server**: one detached daemon per `TAI_HOME` (`~/.tai` by default), started by the first `new-session`/`start-server`. A single `select` loop, no threads. It owns the ptys, a `TuiVtScreen` per pane with scrollback, the layouts, sessions, paste buffers and the key state of every client. It exits when its last session ends (or, if it never had one, after `MPLEX_IDLE_EXIT` seconds). Its stdout and stderr go to `plexer/server.log`; tracebacks land there. - **Client**: `tai.py --mplex ...` connects to `plexer/server.sock`, sends one hello (size, `TERM`, cwd, environment, the command list), then forwards raw input bytes and resizes and writes the finished frames it receives. It never interprets a key. Key parsing, tables, prompts and drawing all happen in the server. - **Rendering**: per client the server composes a full grid (panes, borders, status row, prompt, overlays), diffs it against the previous frame by row and sends only changed rows, at most one frame per `MPLEX_RENDER_INTERVAL`. - **Sessions**: several clients may attach to one session; the window size is the smallest attached client. A detached session keeps running. `kill-server` ends everything. - **Sockets and files** (all under `/plexer/`, directory `0700`, files `0600`): `server.sock`, `server.lock` (flock, one server per home), `server.log`, and one log per pane, `-pane.log`, a verbatim byte log that rolls over to `.1` at `MPLEX_LOG_MAX`. ### Wire format Both directions use frames: 1 byte kind, 4 bytes big-endian length, payload. A connection starts with the single byte `0x01`; anything else is treated as one line of the JSON control protocol below. | Direction | Kind | Payload | |---|---|---| | client to server | `H` | hello JSON: `rows`, `cols`, `term`, `cwd`, `pid`, `tty`, `env`, `pane`, `commands` (list of word lists) | | client to server | `I` | raw input bytes | | client to server | `R` | resize JSON `{rows, cols}` | | server to client | `A` | attached: enter raw mode and the alternate screen | | server to client | `O` | terminal output | | server to client | `M` | text to print on stdout (command output) | | server to client | `E` | error text, client exits 1 | | server to client | `D` / `X` | detached / exited (and `[server exited]`): print the text, exit 0 | | server to client | `Z` | suspend the client (`C-z`) | ### Control socket (JSON lines) The same socket serves `NativePlexerController` (the agent tools). One request line, one reply line: `{"cmd": "send", "text": "...", "enter": true, "pane": "3"}`, `{"cmd": "read", "lines": 50}`, `{"cmd": "status"}`. `pane` is optional and defaults to the active pane of the most recently used session; an unknown pane is an error. `read` returns the tail of the pane's raw byte log. A malformed request returns `{"ok": false, "error": ...}` and never disturbs the server. ### Hard caps | Constant | Value | Limits | |---|---|---| | `MPLEX_MAX_SESSIONS` / `MPLEX_MAX_WINDOWS` / `MPLEX_MAX_PANES` / `MPLEX_MAX_CLIENTS` | 64 / 256 / 256 / 32 | objects the server will hold | | `MPLEX_FRAME_MAX` | 4 MiB | one frame; a larger length drops the client | | `MPLEX_CLIENT_OUT_MAX` | 4 MiB | unsent output per client; over it, frames are dropped and the next one repaints everything | | `MPLEX_PANE_IN_MAX` | 4 MiB | queued input per pane; over it, input is dropped and counted | | `MPLEX_HISTORY_CAP` | 200000 | the largest accepted `history-limit` | | `MPLEX_LOG_MAX` | 16 MiB | one pane log before it rolls over (one old generation kept) | | `MPLEX_BUFFERS_CAP` | 100 | paste buffers (`buffer-limit` default 50) | | `MPLEX_MESSAGES_MAX` / `MPLEX_PROMPT_HISTORY_CAP` | 200 / 100 | `show-messages` entries, entries per prompt history | | `MPLEX_COMMAND_DEPTH_MAX` | 16 | nesting of `if-shell`, `source-file`, `run-shell -C` | | `MPLEX_LOOP_ERRORS_MAX` | 50 | failed server loop iterations in a row before the server gives up (each failure is logged and survived) | | `MPLEX_SHELL_TIMEOUT` | 5 s | `#(...)`, `if-shell`, `run-shell`, copy commands (they block the server for that long at most) | | `MPLEX_CONFIG_MAX` | 1 MiB | size of a config or source file | | `MPLEX_PASTE_MAX` | 4 MiB | one paste buffer | | `MPLEX_RENDER_INTERVAL` | 12 ms | minimum time between frames for a client | | `MPLEX_REAP_KILL_AFTER` | 2 s | SIGHUP is escalated to SIGKILL for a pane process that ignored it | ## Keys Key names use tmux notation: `a`, `C-a`, `M-a`, `C-M-a`, `S-Up`, `Space`, `Enter`, `Tab`, `BTab`, `BSpace`, `Escape`, `Up Down Left Right Home End`, `PPage` (alias `PageUp`), `NPage` (`PageDown`), `IC` (`Insert`), `DC` (`Delete`), `F1` to `F12`, `^a` for `C-a`. Modifier order is normalized to `C-`, `M-`, `S-`. `C-m`, `C-i`, `C-[` are `Enter`, `Tab`, `Escape`. Lookup order for every key from an attached client: 1. an open prompt, then an open overlay (`display-panes`), take the key; 2. in the `root` table: a binding runs; 3. the prefix key or `prefix2` switches the client to the `prefix` table; the next key is looked up there (an unbound key is dropped); 4. otherwise the top mode of the pane takes it (copy mode uses `copy-mode` or `copy-mode-vi` according to `mode-keys`; a key unbound there is ignored, not sent to the shell); 5. otherwise the bytes go to the pane's shell (cursor keys become `ESC O x` when the application asked for it). A binding flagged `repeat` keeps the client in its table for `repeat-time` ms; another repeatable key within that time runs without the prefix, anything else leaves the table and is handled normally. A lone `Escape` is held for `escape-time` ms to tell it from `M-key`. `switch-client -T ` enters a custom table. ## Configuration: ~/.tai/mplex.toml Read when the server starts, and again by `source-file ~/.tai/mplex.toml` (applied on top of the running state). The file is `/mplex.toml`. It needs Python 3.11 or newer (`tomllib`); on older Pythons the defaults apply and the status line says why. Two tables are accepted: ```toml [options] # any option from the table below prefix = "C-a" # main key; "None" disables prefix2 = "F12" # second prefix base-index = 1 mode-keys = "vi" status-position = "top" status-left = "[#S] " status-right = "#H %H:%M" status-style = "bg=blue,fg=white" history-limit = 10000 [keys] clear = ["prefix"] # optional: empty these tables before applying the entries below [keys.prefix] # key tables: prefix, root, copy-mode, copy-mode-vi, or your own (switch-client -T name) "|" = "split-window -h" # a command line: the same syntax as the command prompt, ; chains commands "-" = "split-window -v" "C-b" = false # false (or "") unbinds a default r = { command = "source-file ~/.tai/mplex.toml", note = "reload config", repeat = false } Up = { command = "select-pane -U", repeat = true } [keys.root] # keys that need no prefix "M-Left" = "previous-window" "M-Right" = "next-window" [keys.copy-mode-vi] y = "send-keys -X copy-selection-and-cancel" ``` A key given as an inline table accepts exactly `command`, `repeat` and `note`. Keys are validated with the key grammar above; commands are checked for existence when loaded. Errors never abort the load and are never silent: the offending entry is skipped, counted, the first client sees `mplex.toml: N problem(s) skipped, see show-messages` on the status row, and each problem is listed by `show-messages` (at most 50 are kept). Unknown options, unknown top-level tables, bad key names, unknown commands, wrong value types, out of range numbers, invalid TOML, a file over `MPLEX_CONFIG_MAX` and a missing `tomllib` are all reported this way. Options set at runtime: `set-option [-g|-w] name value` (session scope without `-g`, window scope with `-w`), `set-option -u name` unsets, `-a` appends to strings, `-o` refuses to overwrite, `-q` ignores unknown names, a bare boolean name toggles. `bind-key`, `unbind-key` change tables at runtime but are not written back to the file. ### Style strings `fg=colour`, `bg=colour`, attributes `bold dim italics underscore blink reverse hidden strikethrough` and their `no` forms, `default`, `none`. Colours: the eight names, `bright` + name, `colour0` to `colour255`, `#rrggbb`, `default`. Inside format strings `#[style]` switches style from that point on; `#[default]` returns to the base style. ### Formats `#S #I #W #F #P #T #H #h #D`, `#{variable}`, `#{?cond,a,b}` (chains allowed), modifiers `=N: =-N: =/N/marker: E: T: q: n: b: d: t: ! !! l: s/re/with/:`, comparisons `==: !=: <: >: <=: >=: &&: ||:`, `m:` (glob) `m/r:` (regex), `e|+ - * / %:` arithmetic, `#(shell command)` (cached for `status-interval`), `##` for a literal `#`, `#,` and `#}` inside conditionals, and strftime codes (`%H:%M`). Variables include the session, window, pane, client, history and copy mode values used by the default bindings; every option name is also a variable (booleans give `1`/`0`). ## Options | Option | Type | Default | |---|---|---| | `allow-set-title` | bool | `on` | | `automatic-rename` | bool | `on` | | `automatic-rename-format` | str | `#{?pane_in_mode,[mplex],#{pane_current_command}}#{?pane_dead,[dead],}` | | `base-index` | number 0..9999 | `0` | | `buffer-limit` | number 1..100 | `50` | | `clock-mode-colour` | colour | `blue` | | `clock-mode-style` | 12\|24 | `24` | | `copy-command` | str | `(empty)` | | `copy-mode-current-match-style` | style | `bg=magenta,fg=black` | | `copy-mode-mark-style` | style | `bg=red,fg=black` | | `copy-mode-match-style` | style | `bg=cyan,fg=black` | | `default-command` | str | `(empty)` | | `default-shell` | str | `(empty)` | | `default-terminal` | str | `tmux-256color` | | `detach-on-destroy` | bool | `on` | | `display-panes-active-colour` | colour | `red` | | `display-panes-colour` | colour | `blue` | | `display-panes-time` | number 1..600000 | `1000` | | `display-time` | number 0..600000 | `750` | | `escape-time` | number 0..10000 | `10` | | `history-limit` | number 0..200000 | `2000` | | `main-pane-height` | number 1..10000 | `24` | | `main-pane-width` | number 1..10000 | `80` | | `message-command-style` | style | `bg=black,fg=yellow` | | `message-style` | style | `bg=yellow,fg=black` | | `mode-keys` | vi\|emacs | `emacs (vi when $VISUAL or $EDITOR contains "vi")` | | `mode-style` | style | `noattr,bg=yellow,fg=black` | | `pane-active-border-style` | str | `#{?pane_in_mode,fg=yellow,#{?synchronize-panes,fg=red,fg=green}}` | | `pane-base-index` | number 0..9999 | `0` | | `pane-border-lines` | single\|double\|heavy\|simple | `single` | | `pane-border-style` | style | `default` | | `prefix` | key | `C-b` | | `prefix2` | key | `None` | | `remain-on-exit` | bool | `off` | | `renumber-windows` | bool | `off` | | `repeat-time` | number 0..10000 | `500` | | `set-clipboard` | on\|external\|off | `off` | | `status` | bool | `on` | | `status-interval` | number 0..3600 | `15` | | `status-justify` | left\|centre\|center\|right | `left` | | `status-keys` | vi\|emacs | `emacs (vi when $VISUAL or $EDITOR contains "vi")` | | `status-left` | str | `[#{session_name}] ` | | `status-left-length` | number 0..1000 | `10` | | `status-left-style` | style | `default` | | `status-position` | top\|bottom | `bottom` | | `status-right` | str | `"#{=21:pane_title}" %H:%M %d-%b-%y` | | `status-right-length` | number 0..1000 | `40` | | `status-right-style` | style | `default` | | `status-style` | style | `bg=green,fg=black` | | `synchronize-panes` | bool | `off` | | `window-status-current-format` | str | `#I:#W#{?window_flags,#{window_flags}, }` | | `window-status-current-style` | style | `default` | | `window-status-format` | str | `#I:#W#{?window_flags,#{window_flags}, }` | | `window-status-last-style` | style | `default` | | `window-status-separator` | str | ` ` | | `window-status-style` | style | `default` | | `word-separators` | str | `!"#$%&'()*+,-./:;<=>?@[\]^`{\|}~` | | `wrap-search` | bool | `on` | `mouse`, `set-titles`, `lock-*`, `monitor-*`, `visual-*`, `update-environment` and the rest of tmux's options are not implemented and are rejected with an error, not accepted and ignored. ## Commands | Command | Aliases | Flags | Arguments | |---|---|---|---| | `attach-session` | `attach`, `a` | `-d` `-E` `-r` `-x` `-c arg` `-f arg` `-t arg` | 0 | | `bind-key` | `bind` | `-n` `-r` `-T arg` `-N arg` | 1..n | | `break-pane` | `breakp` | `-a` `-b` `-d` `-P` `-F arg` `-n arg` `-s arg` `-t arg` | 0 | | `capture-pane` | `capturep` | `-a` `-C` `-e` `-J` `-M` `-N` `-p` `-P` `-q` `-T` `-b arg` `-E arg` `-S arg` `-t arg` | 0 | | `choose-buffer` | | `-N` `-r` `-Z` `-F arg` `-f arg` `-K arg` `-O arg` `-t arg` | 0..1 | | `choose-client` | | `-N` `-r` `-Z` `-F arg` `-f arg` `-K arg` `-O arg` `-t arg` | 0..1 | | `choose-tree` | | `-G` `-N` `-r` `-s` `-w` `-Z` `-F arg` `-f arg` `-K arg` `-O arg` `-t arg` | 0..1 | | `clear-history` | `clearhist` | `-H` `-t arg` | 0 | | `clock-mode` | | `-t arg` | 0 | | `command-prompt` | | `-1` `-b` `-F` `-k` `-l` `-i` `-N` `-I arg` `-p arg` `-t arg` `-T arg` | 0..1 | | `confirm-before` | `confirm` | `-b` `-y` `-c arg` `-p arg` `-t arg` | 1 | | `copy-mode` | | `-d` `-e` `-H` `-M` `-q` `-S` `-u` `-s arg` `-t arg` | 0 | | `delete-buffer` | `deleteb` | `-b arg` | 0 | | `detach-client` | `detach` | `-a` `-P` `-E arg` `-s arg` `-t arg` | 0 | | `display-message` | `display` | `-a` `-C` `-I` `-l` `-N` `-p` `-v` `-c arg` `-d arg` `-F arg` `-t arg` | 0..1 | | `display-panes` | `displayp` | `-b` `-N` `-d arg` `-t arg` | 0..1 | | `find-window` | `findw` | `-C` `-i` `-N` `-r` `-T` `-Z` `-t arg` | 1 | | `has-session` | `has` | `-t arg` | 0 | | `if-shell` | `if` | `-b` `-F` `-t arg` | 2..3 | | `kill-pane` | `killp` | `-a` `-t arg` | 0 | | `kill-server` | | | 0 | | `kill-session` | | `-a` `-C` `-t arg` | 0 | | `kill-window` | `killw` | `-a` `-t arg` | 0 | | `last-pane` | `lastp` | `-d` `-e` `-Z` `-t arg` | 0 | | `last-window` | `last` | `-t arg` | 0 | | `list-buffers` | `lsb` | `-F arg` `-f arg` | 0 | | `list-clients` | `lsc` | `-F arg` `-f arg` `-t arg` | 0 | | `list-commands` | `lscm` | `-F arg` | 0..1 | | `list-keys` | `lsk` | `-1` `-a` `-N` `-P arg` `-T arg` | 0..1 | | `list-panes` | `lsp` | `-a` `-s` `-F arg` `-f arg` `-t arg` | 0 | | `list-sessions` | `ls` | `-F arg` `-f arg` | 0 | | `list-windows` | `lsw` | `-a` `-F arg` `-f arg` `-t arg` | 0 | | `load-buffer` | `loadb` | `-b arg` `-t arg` | 1 | | `move-window` | `movew` | `-a` `-b` `-d` `-k` `-r` `-s arg` `-t arg` | 0 | | `new-session` | `new` | `-A` `-d` `-D` `-E` `-P` `-X` `-c arg` `-e arg` `-F arg` `-f arg` `-n arg` `-s arg` `-t arg` `-x arg` `-y arg` | 0..n | | `new-window` | `neww` | `-a` `-b` `-d` `-k` `-P` `-S` `-c arg` `-e arg` `-F arg` `-n arg` `-t arg` | 0..n | | `next-layout` | `nextl` | `-t arg` | 0 | | `next-window` | `next` | `-a` `-t arg` | 0 | | `paste-buffer` | `pasteb` | `-d` `-p` `-r` `-s arg` `-b arg` `-t arg` | 0 | | `previous-layout` | `prevl` | `-t arg` | 0 | | `previous-window` | `prev` | `-a` `-t arg` | 0 | | `refresh-client` | `refresh` | `-c` `-D` `-l` `-L` `-R` `-S` `-U` `-A arg` `-B arg` `-C arg` `-f arg` `-r arg` `-t arg` | 0..1 | | `rename-session` | `rename` | `-t arg` | 1 | | `rename-window` | `renamew` | `-t arg` | 1 | | `resize-pane` | `resizep` | `-D` `-L` `-M` `-R` `-T` `-U` `-Z` `-x arg` `-y arg` `-t arg` | 0..1 | | `rotate-window` | `rotatew` | `-D` `-U` `-Z` `-t arg` | 0 | | `run-shell` | `run` | `-b` `-C` `-E` `-c arg` `-d arg` `-t arg` | 0..1 | | `save-buffer` | `saveb` | `-a` `-b arg` | 1 | | `select-layout` | `selectl` | `-E` `-n` `-o` `-p` `-t arg` | 0..1 | | `select-pane` | `selectp` | `-D` `-d` `-e` `-L` `-l` `-M` `-m` `-R` `-U` `-Z` `-T arg` `-t arg` | 0 | | `select-window` | `selectw` | `-l` `-n` `-p` `-T` `-t arg` | 0 | | `send-keys` | `send` | `-F` `-H` `-K` `-l` `-M` `-R` `-X` `-c arg` `-N arg` `-t arg` | 0..n | | `send-prefix` | | `-2` `-t arg` | 0 | | `set-buffer` | `setb` | `-a` `-w` `-b arg` `-n arg` `-t arg` | 0..1 | | `set-option` | `set` | `-a` `-F` `-g` `-o` `-p` `-q` `-s` `-u` `-U` `-w` `-t arg` | 1..2 | | `set-window-option` | `setw` | `-a` `-F` `-g` `-o` `-q` `-u` `-t arg` | 1..2 | | `show-buffer` | `showb` | `-b arg` | 0 | | `show-messages` | `showmsgs` | `-J` `-T` `-t arg` | 0 | | `show-options` | `show` | `-A` `-g` `-H` `-p` `-q` `-s` `-v` `-w` `-t arg` | 0..1 | | `show-window-options` | `showw` | `-g` `-v` `-t arg` | 0..1 | | `source-file` | `source` | `-F` `-n` `-q` `-v` `-t arg` | 1..n | | `split-window` | `splitw` | `-b` `-d` `-e` `-f` `-h` `-I` `-P` `-v` `-Z` `-c arg` `-e arg` `-F arg` `-l arg` `-t arg` | 0..n | | `start-server` | `start` | | 0 | | `suspend-client` | `suspendc` | `-t arg` | 0 | | `swap-pane` | `swapp` | `-d` `-D` `-U` `-Z` `-s arg` `-t arg` | 0 | | `swap-window` | `swapw` | `-d` `-s arg` `-t arg` | 0 | | `switch-client` | `switchc` | `-E` `-l` `-n` `-p` `-r` `-Z` `-c arg` `-t arg` `-T arg` | 0 | | `unbind-key` | `unbind` | `-a` `-n` `-q` `-T arg` | 0..1 | Commands that parse but are refused with "command not supported by mplex": `display-menu`, `display-popup`, `customize-mode`, `lock-server`, `lock-session`, `lock-client`, `pipe-pane`, `join-pane`, `move-pane`, `link-window`, `unlink-window`, `respawn-pane`, `respawn-window`, `resize-window`, `set-hook`, `show-hooks`, `wait-for`, `server-access`. Flags for mouse handling (`send-keys -M`, `copy-mode -M`, `resize-pane -M/-T`) and `select-pane -e/-d` are refused the same way. A unique prefix of a command name is accepted (`kill-ser`). `send-keys -X ` drives copy mode. The commands are: `cancel`, `cursor-up`, `cursor-down`, `cursor-down-and-cancel`, `cursor-left`, `cursor-right`, `start-of-line`, `end-of-line`, `back-to-indentation`, `top-line`, `middle-line`, `bottom-line`, `history-top`, `history-bottom`, `page-up`, `page-down`, `page-down-and-cancel`, `halfpage-up`, `halfpage-down`, `halfpage-down-and-cancel`, `scroll-up`, `scroll-down`, `scroll-down-and-cancel`, `scroll-middle`, `scroll-top`, `scroll-bottom`, `cursor-centre-vertical`, `cursor-centre-horizontal`, `goto-line`, `refresh-from-pane`, `toggle-position`, `next-word`, `next-word-end`, `next-space`, `next-space-end`, `previous-word`, `previous-space`, `next-paragraph`, `previous-paragraph`, `next-matching-bracket`, `previous-matching-bracket`, `jump-forward`, `jump-backward`, `jump-to-forward`, `jump-to-backward`, `jump-again`, `jump-reverse`, `begin-selection`, `stop-selection`, `clear-selection`, `select-line`, `select-word`, `rectangle-toggle`, `rectangle-on`, `rectangle-off`, `other-end`, `set-mark`, `jump-to-mark`, `copy-selection`, `copy-selection-and-cancel`, `copy-pipe`, `copy-pipe-and-cancel`, `append-selection`, `append-selection-and-cancel`, `copy-end-of-line`, `copy-end-of-line-and-cancel`, `copy-pipe-end-of-line`, `copy-pipe-end-of-line-and-cancel`, `copy-line`, `copy-line-and-cancel`, `copy-pipe-line`, `copy-pipe-line-and-cancel`, `search-forward`, `search-backward`, `search-forward-text`, `search-backward-text`, `search-forward-incremental`, `search-backward-incremental`, `search-again`, `search-reverse`. A copy goes to the top paste buffer, to `copy-command` (or the command given to `copy-pipe*`) when set, and as an OSC 52 clipboard sequence when `set-clipboard` is `on` or `external`. Search is a regular expression with smart case (case-insensitive unless the text has an uppercase letter), falling back to a literal when the expression is invalid. ## Default key bindings Copied from tmux 3.6 without mouse bindings. `prefix ?` shows the notes in the running multiplexer; `list-keys` shows the live tables, including your changes. ### prefix table (after `C-b`) | Key | Command | Repeat | Note | |---|---|---|---| | `Space` | `next-layout` | | Select next layout | | `!` | `break-pane` | | Break pane to a new window | | `"` | `split-window` | | Split window vertically | | `#` | `list-buffers` | | List all paste buffers | | `$` | `command-prompt -I "#S" { rename-session "%%" }` | | Rename current session | | `%` | `split-window -h` | | Split window horizontally | | `&` | `confirm-before -p "kill-window #W? (y/n)" kill-window` | | Kill current window | | `'` | `command-prompt -T window-target -p index { select-window -t ":%%" }` | | Prompt for window index to select | | `(` | `switch-client -p` | | Switch to previous client | | `)` | `switch-client -n` | | Switch to next client | | `,` | `command-prompt -I "#W" { rename-window "%%" }` | | Rename current window | | `-` | `delete-buffer` | | Delete the most recent paste buffer | | `.` | `command-prompt -T target { move-window -t "%%" }` | | Move the current window | | `/` | `command-prompt -k -p key { list-keys -1N "%%" }` | | Describe key binding | | `0` | `select-window -t :=0` | | Select window 0 | | `1` | `select-window -t :=1` | | Select window 1 | | `2` | `select-window -t :=2` | | Select window 2 | | `3` | `select-window -t :=3` | | Select window 3 | | `4` | `select-window -t :=4` | | Select window 4 | | `5` | `select-window -t :=5` | | Select window 5 | | `6` | `select-window -t :=6` | | Select window 6 | | `7` | `select-window -t :=7` | | Select window 7 | | `8` | `select-window -t :=8` | | Select window 8 | | `9` | `select-window -t :=9` | | Select window 9 | | `:` | `command-prompt` | | Prompt for a command | | `;` | `last-pane` | | Move to the previously active pane | | `<` | `display-menu` | | Display window menu | | `=` | `choose-buffer -Z` | | Choose a paste buffer from a list | | `>` | `display-menu` | | Display pane menu | | `?` | `list-keys -N` | | List key bindings | | `C` | `customize-mode -Z` | | Customize options | | `D` | `choose-client -Z` | | Choose and detach a client from a list | | `E` | `select-layout -E` | | Spread panes out evenly | | `L` | `switch-client -l` | | Switch to the last client | | `M` | `select-pane -M` | | Clear the marked pane | | `[` | `copy-mode` | | Enter copy mode | | `]` | `paste-buffer -p` | | Paste the most recent paste buffer | | `c` | `new-window` | | Create a new window | | `d` | `detach-client` | | Detach the current client | | `f` | `command-prompt { find-window -Z "%%" }` | | Search for a pane | | `i` | `display-message` | | Display window information | | `l` | `last-window` | | Select the previously current window | | `m` | `select-pane -m` | | Toggle the marked pane | | `n` | `next-window` | | Select the next window | | `o` | `select-pane -t :.+` | | Select the next pane | | `p` | `previous-window` | | Select the previous window | | `q` | `display-panes` | | Display pane numbers | | `r` | `refresh-client` | | Redraw the current client | | `s` | `choose-tree -Zs` | | Choose a session from a list | | `t` | `clock-mode` | | Show a clock | | `w` | `choose-tree -Zw` | | Choose a window from a list | | `x` | `confirm-before -p "kill-pane #P? (y/n)" kill-pane` | | Kill the active pane | | `z` | `resize-pane -Z` | | Zoom the active pane | | `{` | `swap-pane -U` | | Swap the active pane with the pane above | | `}` | `swap-pane -D` | | Swap the active pane with the pane below | | `~` | `show-messages` | | Show messages | | `DC` | `refresh-client -c` | yes | Reset so the visible part of the window follows the cursor | | `PPage` | `copy-mode -u` | | Enter copy mode and scroll up | | `Up` | `select-pane -U` | yes | Select the pane above the active pane | | `Down` | `select-pane -D` | yes | Select the pane below the active pane | | `Left` | `select-pane -L` | yes | Select the pane to the left of the active pane | | `Right` | `select-pane -R` | yes | Select the pane to the right of the active pane | | `M-1` | `select-layout even-horizontal` | | Set the even-horizontal layout | | `M-2` | `select-layout even-vertical` | | Set the even-vertical layout | | `M-3` | `select-layout main-horizontal` | | Set the main-horizontal layout | | `M-4` | `select-layout main-vertical` | | Set the main-vertical layout | | `M-5` | `select-layout tiled` | | Select the tiled layout | | `M-6` | `select-layout main-horizontal-mirrored` | | Set the main-horizontal-mirrored layout | | `M-7` | `select-layout main-vertical-mirrored` | | Set the main-vertical-mirrored layout | | `M-n` | `next-window -a` | | Select the next window with an alert | | `M-o` | `rotate-window -D` | | Rotate through the panes in reverse | | `M-p` | `previous-window -a` | | Select the previous window with an alert | | `M-Up` | `resize-pane -U 5` | yes | Resize the pane up by 5 | | `M-Down` | `resize-pane -D 5` | yes | Resize the pane down by 5 | | `M-Left` | `resize-pane -L 5` | yes | Resize the pane left by 5 | | `M-Right` | `resize-pane -R 5` | yes | Resize the pane right by 5 | | `C-b` | `send-prefix` | | Send the prefix key | | `C-o` | `rotate-window` | | Rotate through the panes | | `C-z` | `suspend-client` | | Suspend the current client | | `C-Up` | `resize-pane -U` | yes | Resize the pane up | | `C-Down` | `resize-pane -D` | yes | Resize the pane down | | `C-Left` | `resize-pane -L` | yes | Resize the pane left | | `C-Right` | `resize-pane -R` | yes | Resize the pane right | | `S-Up` | `refresh-client -U 10` | yes | Move the visible part of the window up | | `S-Down` | `refresh-client -D 10` | yes | Move the visible part of the window down | | `S-Left` | `refresh-client -L 10` | yes | Move the visible part of the window left | | `S-Right` | `refresh-client -R 10` | yes | Move the visible part of the window right | ### copy-mode table (`mode-keys emacs`) | Key | Command | Repeat | Note | |---|---|---|---| | `Escape` | `send-keys -X cancel` | | | | `Space` | `send-keys -X page-down` | | | | `,` | `send-keys -X jump-reverse` | | | | `;` | `send-keys -X jump-again` | | | | `F` | `command-prompt -1 -p "(jump backward)" { send-keys -X jump-backward -- "%%" }` | | | | `N` | `send-keys -X search-reverse` | | | | `P` | `send-keys -X toggle-position` | | | | `R` | `send-keys -X rectangle-toggle` | | | | `T` | `command-prompt -1 -p "(jump to backward)" { send-keys -X jump-to-backward -- "%%" }` | | | | `X` | `send-keys -X set-mark` | | | | `f` | `command-prompt -1 -p "(jump forward)" { send-keys -X jump-forward -- "%%" }` | | | | `g` | `command-prompt -p "(goto line)" { send-keys -X goto-line -- "%%" }` | | | | `n` | `send-keys -X search-again` | | | | `q` | `send-keys -X cancel` | | | | `r` | `send-keys -X refresh-from-pane` | | | | `t` | `command-prompt -1 -p "(jump to forward)" { send-keys -X jump-to-forward -- "%%" }` | | | | `Home` | `send-keys -X start-of-line` | | | | `End` | `send-keys -X end-of-line` | | | | `NPage` | `send-keys -X page-down` | | | | `PPage` | `send-keys -X page-up` | | | | `Up` | `send-keys -X cursor-up` | | | | `Down` | `send-keys -X cursor-down` | | | | `Left` | `send-keys -X cursor-left` | | | | `Right` | `send-keys -X cursor-right` | | | | `M-1` | `command-prompt -N -I 1 -p (repeat) { send-keys -N "%%" }` | | | | `M-2` | `command-prompt -N -I 2 -p (repeat) { send-keys -N "%%" }` | | | | `M-3` | `command-prompt -N -I 3 -p (repeat) { send-keys -N "%%" }` | | | | `M-4` | `command-prompt -N -I 4 -p (repeat) { send-keys -N "%%" }` | | | | `M-5` | `command-prompt -N -I 5 -p (repeat) { send-keys -N "%%" }` | | | | `M-6` | `command-prompt -N -I 6 -p (repeat) { send-keys -N "%%" }` | | | | `M-7` | `command-prompt -N -I 7 -p (repeat) { send-keys -N "%%" }` | | | | `M-8` | `command-prompt -N -I 8 -p (repeat) { send-keys -N "%%" }` | | | | `M-9` | `command-prompt -N -I 9 -p (repeat) { send-keys -N "%%" }` | | | | `M-<` | `send-keys -X history-top` | | | | `M->` | `send-keys -X history-bottom` | | | | `M-R` | `send-keys -X top-line` | | | | `M-b` | `send-keys -X previous-word` | | | | `M-f` | `send-keys -X next-word-end` | | | | `M-l` | `send-keys -X cursor-centre-horizontal` | | | | `M-m` | `send-keys -X back-to-indentation` | | | | `M-r` | `send-keys -X middle-line` | | | | `M-v` | `send-keys -X page-up` | | | | `M-w` | `send-keys -X copy-pipe-and-cancel` | | | | `M-x` | `send-keys -X jump-to-mark` | | | | `M-{` | `send-keys -X previous-paragraph` | | | | `M-}` | `send-keys -X next-paragraph` | | | | `M-Up` | `send-keys -X halfpage-up` | | | | `M-Down` | `send-keys -X halfpage-down` | | | | `C-Space` | `send-keys -X begin-selection` | | | | `C-a` | `send-keys -X start-of-line` | | | | `C-b` | `send-keys -X cursor-left` | | | | `C-c` | `send-keys -X cancel` | | | | `C-e` | `send-keys -X end-of-line` | | | | `C-f` | `send-keys -X cursor-right` | | | | `C-g` | `send-keys -X clear-selection` | | | | `C-k` | `send-keys -X copy-pipe-end-of-line-and-cancel` | | | | `C-l` | `send-keys -X cursor-centre-vertical` | | | | `C-n` | `send-keys -X cursor-down` | | | | `C-p` | `send-keys -X cursor-up` | | | | `C-r` | `command-prompt -i -I "#{pane_search_string}" -T search -p "(search up)" { send-keys -X search-backward-incremental -- "%%" }` | | | | `C-s` | `command-prompt -i -I "#{pane_search_string}" -T search -p "(search down)" { send-keys -X search-forward-incremental -- "%%" }` | | | | `C-v` | `send-keys -X page-down` | | | | `C-w` | `send-keys -X copy-pipe-and-cancel` | | | | `C-Up` | `send-keys -X scroll-up` | | | | `C-Down` | `send-keys -X scroll-down` | | | | `C-M-b` | `send-keys -X previous-matching-bracket` | | | | `C-M-f` | `send-keys -X next-matching-bracket` | | | ### copy-mode-vi table (`mode-keys vi`) | Key | Command | Repeat | Note | |---|---|---|---| | `Enter` | `send-keys -X copy-pipe-and-cancel` | | | | `Escape` | `send-keys -X clear-selection` | | | | `Space` | `send-keys -X begin-selection` | | | | `#` | `send-keys -FX search-backward -- "#{copy_cursor_word}"` | | | | `$` | `send-keys -X end-of-line` | | | | `%` | `send-keys -X next-matching-bracket` | | | | `*` | `send-keys -FX search-forward -- "#{copy_cursor_word}"` | | | | `,` | `send-keys -X jump-reverse` | | | | `/` | `command-prompt -T search -p "(search down)" { send-keys -X search-forward -- "%%" }` | | | | `0` | `send-keys -X start-of-line` | | | | `1` | `command-prompt -N -I 1 -p (repeat) { send-keys -N "%%" }` | | | | `2` | `command-prompt -N -I 2 -p (repeat) { send-keys -N "%%" }` | | | | `3` | `command-prompt -N -I 3 -p (repeat) { send-keys -N "%%" }` | | | | `4` | `command-prompt -N -I 4 -p (repeat) { send-keys -N "%%" }` | | | | `5` | `command-prompt -N -I 5 -p (repeat) { send-keys -N "%%" }` | | | | `6` | `command-prompt -N -I 6 -p (repeat) { send-keys -N "%%" }` | | | | `7` | `command-prompt -N -I 7 -p (repeat) { send-keys -N "%%" }` | | | | `8` | `command-prompt -N -I 8 -p (repeat) { send-keys -N "%%" }` | | | | `9` | `command-prompt -N -I 9 -p (repeat) { send-keys -N "%%" }` | | | | `:` | `command-prompt -p "(goto line)" { send-keys -X goto-line -- "%%" }` | | | | `;` | `send-keys -X jump-again` | | | | `?` | `command-prompt -T search -p "(search up)" { send-keys -X search-backward -- "%%" }` | | | | `A` | `send-keys -X append-selection-and-cancel` | | | | `B` | `send-keys -X previous-space` | | | | `D` | `send-keys -X copy-pipe-end-of-line-and-cancel` | | | | `E` | `send-keys -X next-space-end` | | | | `F` | `command-prompt -1 -p "(jump backward)" { send-keys -X jump-backward -- "%%" }` | | | | `G` | `send-keys -X history-bottom` | | | | `H` | `send-keys -X top-line` | | | | `J` | `send-keys -X scroll-down` | | | | `K` | `send-keys -X scroll-up` | | | | `L` | `send-keys -X bottom-line` | | | | `M` | `send-keys -X middle-line` | | | | `N` | `send-keys -X search-reverse` | | | | `P` | `send-keys -X toggle-position` | | | | `T` | `command-prompt -1 -p "(jump to backward)" { send-keys -X jump-to-backward -- "%%" }` | | | | `V` | `send-keys -X select-line` | | | | `W` | `send-keys -X next-space` | | | | `X` | `send-keys -X set-mark` | | | | `^` | `send-keys -X back-to-indentation` | | | | `b` | `send-keys -X previous-word` | | | | `e` | `send-keys -X next-word-end` | | | | `f` | `command-prompt -1 -p "(jump forward)" { send-keys -X jump-forward -- "%%" }` | | | | `g` | `send-keys -X history-top` | | | | `h` | `send-keys -X cursor-left` | | | | `j` | `send-keys -X cursor-down` | | | | `k` | `send-keys -X cursor-up` | | | | `l` | `send-keys -X cursor-right` | | | | `n` | `send-keys -X search-again` | | | | `o` | `send-keys -X other-end` | | | | `q` | `send-keys -X cancel` | | | | `r` | `send-keys -X refresh-from-pane` | | | | `t` | `command-prompt -1 -p "(jump to forward)" { send-keys -X jump-to-forward -- "%%" }` | | | | `v` | `send-keys -X rectangle-toggle` | | | | `w` | `send-keys -X next-word` | | | | `z` | `send-keys -X scroll-middle` | | | | `{` | `send-keys -X previous-paragraph` | | | | `}` | `send-keys -X next-paragraph` | | | | `BSpace` | `send-keys -X cursor-left` | | | | `Home` | `send-keys -X start-of-line` | | | | `End` | `send-keys -X end-of-line` | | | | `NPage` | `send-keys -X page-down` | | | | `PPage` | `send-keys -X page-up` | | | | `Up` | `send-keys -X cursor-up` | | | | `Down` | `send-keys -X cursor-down` | | | | `Left` | `send-keys -X cursor-left` | | | | `Right` | `send-keys -X cursor-right` | | | | `M-x` | `send-keys -X jump-to-mark` | | | | `C-b` | `send-keys -X page-up` | | | | `C-c` | `send-keys -X cancel` | | | | `C-d` | `send-keys -X halfpage-down` | | | | `C-e` | `send-keys -X scroll-down` | | | | `C-f` | `send-keys -X page-down` | | | | `C-h` | `send-keys -X cursor-left` | | | | `C-j` | `send-keys -X copy-pipe-and-cancel` | | | | `C-u` | `send-keys -X halfpage-up` | | | | `C-v` | `send-keys -X rectangle-toggle` | | | | `C-y` | `send-keys -X scroll-up` | | | | `C-Up` | `send-keys -X scroll-up` | | | | `C-Down` | `send-keys -X scroll-down` | | | `display-menu` (`<`, `>`) and `customize-mode` (`C`) are bound exactly as in tmux but report "not supported" when used. ## Pane modes Copy mode (`copy-mode`, and the read-only view that `list-keys`, `show-messages` and other long output open: `view-mode`), `clock-mode`, and the choosers `choose-tree`, `choose-buffer`, `choose-client`. Chooser keys: `Up/k/C-p`, `Down/j/C-n`, `PPage/C-b`, `NPage/C-f`, `Home/g`, `End/G`, `Right/l/+` expand, `Left/h/-` collapse, `t` tag, `v` preview, `x` kill (asks `y`), `Enter` choose, `0`-`9` choose the numbered entry, `q/Escape/C-c` leave. The chooser keys are fixed, not rebindable. `display-panes` is an overlay on the client (digit selects, any other key dismisses). The command prompt edits like tmux's emacs mode: `C-a C-e C-b C-f M-b M-f BSpace C-h DC C-d C-k C-u C-w C-y C-t`, `Up/Down` history (per prompt type, capped), `Enter`, `Escape/C-c/C-g`. The single key (`-1`), key name (`-k`), numeric (`-N`) and incremental (`-i`, `C-s`/`C-r` step) variants are implemented. ## Measurements Taken on the development machine (Linux, Python 3.14), one pane at 200x50 in an attached client: an idle server used 0.000 s of CPU over 5 s; `seq 1 300000` reached the attached terminal in 1.95 s wall with 1.72 s of server CPU. vim and htop render correctly inside a pane. No numbers exist for many panes, many clients, slow terminals or other machines. ## Checked against tmux 3.6 Behavior was compared with the tmux 3.6 installed on the development machine, command by command, instead of being written from memory. What exists as evidence: - Default key tables, notes, options and command flags: dumped from tmux (research `local-tmux-3-6-keybinding-and-option-dumps.md`). - Pane geometry: 69 scenarios (all seven layouts for 2 to 7 panes, splits with `-b` `-l` and percentages, resize in all directions and to an absolute size, kill, rotate, swap, directional select, last-pane, zoom, break-pane, kill-pane -a) give identical pane sizes, positions and active pane in tmux and in mplex. Unit fixtures in `MplexLayoutTests` carry the values. - Windows: 26 scenarios (new-window `-a` `-b` `-k` and explicit indexes, gap filling, kill and what becomes current, select, last, next, previous, `-T`, move, swap, renumber, base-index, break-pane) agree on indexes, names, flags and errors, except one flag after `swap-window -d` (tmux marks a different window as last). - Copy mode movement: a differential fuzz of random key sequences against tmux (12 steps per sequence; content with blanks, indentation, punctuation and history; the search string reset before each sequence) ran clean over 360 emacs-key sequences and over about 240 of 300 vi-key sequences; the remaining vi-key difference is the line-crossing bookkeeping described below. 842 recorded results (782 cursor and scroll states, 60 selections and copies, both key modes) are replayed by `MplexCopyModeVersusTmuxTests`. Rules learned that way and implemented: the cursor stands between characters and may sit one past the line end; vertical moves, pages and scrolls remember a column (`lastcx`/`lastsx`) and snap to the line end the way tmux does; a page is `rows - 2`; searches put the cursor at the match end for emacs keys and the match start for vi keys, and scroll the match into view by tmux's rule; a search string outlives copy mode and restarts upward; a failed search is a status message, not a command error; selections are half open for emacs keys and include the cursor character (and the newline at a line end) for vi keys; `select-word` on blanks takes the previous word; `copy-line` and `copy-end-of-line` start a fresh selection. - Copy mode selections: the same fuzz with selections (begin, select-word, select-line, rectangle, other-end, then copies) did not converge: roughly one random sequence in five still copies a different range, typically when `select-word` or `select-line` is followed by more cursor moves (tmux then extends by words or lines, mplex by characters). ## Differences from tmux and gaps Each item names the code that would have to change. - **No mouse.** No mouse reporting, selection, border dragging or wheel scroll. `MplexKeys.split` drops mouse sequences; a client enabling mouse mode and an `MplexClientConn` routing table would be needed. - **No reflow on resize, no wrapped-line flag.** `TuiVtScreen.resize` truncates rows when the pane gets narrower and cannot get the text back; tmux reflows. Copy mode treats each screen row as a line, so word motions, `end-of-line` and copies do not join a long line that the terminal wrapped, where tmux does. Fix: store a wrapped flag per row in `TuiVtScreen` and re-wrap in `resize`; `MplexCopyMode.llen`, `next_word` and `selection_text` would then follow it. - **Wide characters count as one column** (the existing `TuiVtScreen` gap in `tui.md`); CJK and emoji misalign. - **Copy mode corners that differ from tmux:** the non-bracket fallback of `next-matching-bracket` and `previous-matching-bracket` (tmux behaves like several different word motions depending on the position, mplex uses next-space-end and previous-space); a failed `jump-*` followed by `jump-reverse`; word- and line-granular selection after `select-word` or `select-line` plus cursor moves (mplex stays character based); the `lastcx`/`lastsx` bookkeeping when a backward word motion crosses a line (tmux records in some situations, mplex never does); a cursor placed beyond the line end by `goto-line`; the `swap-window -d` last-window flag. `append-selection` was not compared. - **Window flags** show `*`, `-`, `Z`, `M`; there is no bell, activity or silence tracking, so `next-window -a` finds no window and says so. Needs per-pane output/bell counters in `MplexServer.pane_read`. - **`status-keys vi`** is treated as emacs. The command prompt has no tab completion. - **Status line**: no multi-line status, `status-format`, ranges or `pane-border-status`. - **Layouts** are the seven named presets and the splits you make; layout strings from `list-windows -F '#{window_layout}'` are not parsed or produced. - **Windows are sized to the smallest attached client** (`window-size smallest`); `latest` is not offered. - **Environment**: new panes get the environment of the client that created the session, there is no `update-environment` or `set-environment`. - **Choosers** do not have tmux's sort order, filter, `-K`, `-O` and format flags (accepted by the parser, not used). - **Refresh panning** (`S-Up` ...) is accepted and does nothing, since a window is never larger than its client. - **Set-clipboard defaults to `off`** (tmux: `external`), because mplex cannot know whether the terminal understands OSC 52. - `default-terminal` falls back to `screen-256color`, `xterm-256color`, `xterm`, then `vt100` when its terminfo entry is missing. - Shortcuts that need a client, mouse or menu in tmux and are refused here are listed under Commands. ## Tests - `test_tai.py`: `Mplex*Tests`, unit level (keys, parser, defaults, config, formats, layout, canvas, VT screen extensions, copy mode and its tmux-recorded fixtures, sessions, prompt, templates, framing) and `MplexSpecTests`, which checks this file against the code. - `tests/mplex.py`: real client under a pty, real server, real bash: lifecycle, every keyboard shortcut with an effect, copy mode round trip, `mplex.toml`, two clients, the command line. - `tests/plexer.py`: control socket, per-pane targeting, permissions, large and malformed requests.