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>
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(~/.taiby default), started by the firstnew-session/start-server. A singleselectloop, no threads. It owns the ptys, aTuiVtScreenper 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, afterMPLEX_IDLE_EXITseconds). Its stdout and stderr go toplexer/server.log; tracebacks land there. - Client:
tai.py --mplex ...connects toplexer/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-serverends everything. - Sockets and files (all under
<TAI_HOME>/plexer/, directory0700, files0600):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.1atMPLEX_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:
- an open prompt, then an open overlay (
display-panes), take the key; - in the
roottable: a binding runs; - the prefix key or
prefix2switches the client to theprefixtable; the next key is looked up there (an unbound key is dropped); - otherwise the top mode of the pane takes it (copy mode uses
copy-modeorcopy-mode-viaccording tomode-keys; a key unbound there is ignored, not sent to the shell); - otherwise the bytes go to the pane's shell (cursor keys become
ESC O xwhen 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-land 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 inMplexLayoutTestscarry the values. - Windows: 26 scenarios (new-window
-a-b-kand 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 afterswap-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 isrows - 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-wordon blanks takes the previous word;copy-lineandcopy-end-of-linestart 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-wordorselect-lineis 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.splitdrops mouse sequences; a client enabling mouse mode and anMplexClientConnrouting table would be needed. - No reflow on resize, no wrapped-line flag.
TuiVtScreen.resizetruncates 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-lineand copies do not join a long line that the terminal wrapped, where tmux does. Fix: store a wrapped flag per row inTuiVtScreenand re-wrap inresize;MplexCopyMode.llen,next_wordandselection_textwould then follow it. - Wide characters count as one column (the existing
TuiVtScreengap intui.md); CJK and emoji misalign. - Copy mode corners that differ from tmux: the non-bracket fallback of
next-matching-bracketandprevious-matching-bracket(tmux behaves like several different word motions depending on the position, mplex uses next-space-end and previous-space); a failedjump-*followed byjump-reverse; word- and line-granular selection afterselect-wordorselect-lineplus cursor moves (mplex stays character based); thelastcx/lastsxbookkeeping when a backward word motion crosses a line (tmux records in some situations, mplex never does); a cursor placed beyond the line end bygoto-line; theswap-window -dlast-window flag.append-selectionwas not compared. - Window flags show
*,-,Z,M; there is no bell, activity or silence tracking, sonext-window -afinds no window and says so. Needs per-pane output/bell counters inMplexServer.pane_read. status-keys viis treated as emacs. The command prompt has no tab completion.- Status line: no multi-line status,
status-format, ranges orpane-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);latestis not offered. - Environment: new panes get the environment of the client that created the session, there is no
update-environmentorset-environment. - Choosers do not have tmux's sort order, filter,
-K,-Oand 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-terminalfalls back toscreen-256color,xterm-256color,xterm, thenvt100when 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) andMplexSpecTests, 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.