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

42 KiB

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, 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:

[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.