Files
tai/mplex.md
T
retoorandClaude Sonnet 5.5 46682d42c3 mplex: copy mode matches tmux 3.6 (cursor, scrolling, search, selections), window selection history, loop survives a failing iteration, spec mplex.md
Copy mode was compared with the installed tmux 3.6 by differential fuzzing and 842 recorded results instead of being written from memory: the cursor stands between characters and remembers its column (lastcx/lastsx), pages are rows - 2, searches land at the match end for emacs keys and the match start for vi keys and scroll by tmux's rule, a failed search is a status message, emacs selections are half open and vi selections include the cursor character and the line end, select-word on blanks takes the previous word, copy-line and copy-end-of-line start a fresh selection. Window selection keeps a history (last, then previous with wrap when the current window dies), swap-window keeps the current window current, new-session -d -x -y sizes the window as given. Server loop logs and survives a failing iteration (50 in a row end it).

mplex.md is the specification (architecture, wire format, caps, every option, command and default key, what was checked against tmux, known differences); MplexSpecTests fails when it drifts from the code. New: MplexCopyModeVersusTmuxTests (tmux-recorded fixtures), MplexSessionTests, MplexServerLoopTests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 17:47:14 +02:00

682 lines
42 KiB
Markdown

# 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 <any command> 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=<server socket>` and `TAI_PLEXER_PANE=<id>` 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 `<TAI_HOME>/plexer/`, directory `0700`, files `0600`):
`server.sock`, `server.lock` (flock, one server per home), `server.log`, and one log per pane,
`<server start>-pane<id>.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 <table>` 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 `<TAI_HOME>/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 <command>` 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.