tmux


1 tmux configuration

🎯 tmux/.tmux.conf
global-variables
keybindings
appearance
misc

2 Global variables

Tmux supports rudimentary templating, somewhat akin to the C preprocessor. We call them "global variables" here for simplicity. But basically you can do

FOO="bar"

and then you can use $FOO elsewhere in the configuration to substitute in the value of "bar".

global-variables
global-variable-emacslike

2.1 EMACSLIKE

The EMACSLIKE variable is a tmux predicate to check if the current window_name is running either emacs, emacsclient, or git. We include git because automatic-rename sets the name to "git" if we run git commit --amend from the terminal. We also include tig because we can invoke Git interactive rebases (which invokes our emacs editor) from tig.

IOW, if EMACSLIKE returns a truthy value, we are most likely looking at Emacs. We use this predicate in a few places to determine if we should send CSI-u encoded keys (because Emacs understands CSI-u keys but other terminal applications may not). So in a sense the variable could have been named UNDERSTANDS_CSIU or similar but because we are almost always only concerned with terminal emacs, we've named it EMACSLIKE.

global-variable-emacslike
EMACSLIKE="#{m/r:emacs.*|git|tig,#{window_name}}"

3 Keybindings

3.1 Reference information

Note that the following bindings are equivalent:

bind-key -T root         C-S-l      send-keys -H 1b 5b 37 36 3b 36 75
bind-key -T root         C-S-l      send-keys Escape "[76;6u"

Using the raw hex version may be easier to debug.

Another thing worth mentioning is the following ASCII manpage (see man ascii) portion (converted to Org table format):

Table 1.
OctDecHexCharOctDecHexChar
000000NUL \0 (null character)1006440@
001101SOH (start of heading)1016541A
002202STX (start of text)1026642B
003303ETX (end of text)1036743C
004404EOT (end of transmission)1046844D
005505ENQ (enquiry)1056945E
006606ACK (acknowledge)1067046F
007707BEL \a (bell)1077147G
010808BS \b (backspace)1107248H
011909HT \t (horizontal tab)1117349I
012100ALF \n (new line)112744AJ
013110BVT \v (vertical tab)113754BK
014120CFF \f (form feed)114764CL
015130DCR \r (carriage ret)115774DM
016140ESO (shift out)116784EN
017150FSI (shift in)117794FO
0201610DLE (data link escape)1208050P
0211711DC1 (device control 1)1218151Q
0221812DC2 (device control 2)1228252R
0231913DC3 (device control 3)1238353S
0242014DC4 (device control 4)1248454T
0252115NAK (negative ack.)1258555U
0262216SYN (synchronous idle)1268656V
0272317ETB (end of trans. blk)1278757W
0302418CAN (cancel)1308858X
0312519EM (end of medium)1318959Y
032261ASUB (substitute)132905AZ
033271BESC (escape)133915B[
034281CFS (file separator)134925C\
035291DGS (group separator)135935D]
036301ERS (record separator)136945E^
037311FUS (unit separator)137955F_
0403220SPACE1409660`
0413321!1419761a
0423422"1429862b
0433523#1439963c
0443624$14410064d
0453725%14510165e
0463826&14610266f
0473927'14710367g
0504028(15010468h
0514129)15110569i
052422A*1521066Aj
053432B+1531076Bk
054442C,1541086Cl
055452D-1551096Dm
056462E.1561106En
057472F/1571116Fo
0604830016011270p
0614931116111371q
0625032216211472r
0635133316311573s
0645234416411674t
0655335516511775u
0665436616611876v
0675537716711977w
0705638817012078x
0715739917112179y
072583A:1721227Az
073593B;1731237B{
074603C<1741247C|
075613D=1751257D}
076623E>1761267E~
077633F?1771277FDEL

The historical equivalence of C-i and TAB, C-[ and ESC, C-j and \n come from the fact that pressing the CTRL key usually meant clearing the 7th bit in the 7-bit code used for ASCII. The 7th bit encodes a numerical value of 64. So, e.g. J by itself is encoded as 74, and doing C-j gets us \(74 - 64 = 10\), or \n.

Note that in the calculations above, there is no distinction between upper and lower case. That is, both C-j and C-S-j traditionally encode the value of 10 for \n. Presumably this is because lower-case letters weren't introduced until much later; for example Morse code (which predates ASCII and would have been the standard used in telegraph transmissions).

3.2 Our keybinding system in tmux

Tmux has a notion of key-tables. A key-table is just a namespace which groups together keybindings that belong together. Only one key-table is active at any given time. By default tmux starts out with the root key-table. There are others like prefix (which is activated by C-b) and copy-mode-vi. Users are free to add their own key-tables as they like.

Here we treat these key-tables as different "modes", similar to modes in Vi.1 The table below maps the key-tables to their modes:

Table 2.
tmux key-tableVi-style modeStatus bar symbolSticky?
rootinsert<I>yes
prefixnormal<N>no
copy-modevisual<V>yes
Passthrough modeN/A<P>yes

We display the current key-table with this bash script on the status bar. This helps us avoid getting lost because tmux by default does not give any sort of visual indication of which key-table is currently active.

The "sticky" column in the table above refers to whether the mode is designed to be used in a "sticky" (aka modal) fashion (where the mode sticks around even after pressing some key combination, unless we explicitly exit that mode).

By default, we are in "insert" mode, which corresponds to the root key-table. The C-b default binding is used to make the next command run inside "normal" mode (prefix key-table). We treat copy-mode as Vim's visual selection mode, because the similarities are striking, at least with the additional customizations we have.

The passthrough mode allows us to work with nested tmux sessions more easily without hitting C-b twice every time we want to manipulate the inner tmux session.

3.3 "Insert" mode bindings (root key-table)

3.3.1 Navigation and layout

3.3.1.1 Sessions

Move across tmux sessions.

keybindings-sessions
bind-key -T root         C-M-n      switch-client -n
3.3.1.2 Windows

Create a new window (with a single pane) to the right (and spawn a new shell in the pane there).

keybindings-window-create
bind-key -T root         C-M-o      new-window -a -c "#{pane_current_path}"

Navigate across windows.

keybindings-window-navigate
bind-key -T root         C-M-h      previous-window
bind-key -T root         C-M-l      next-window
3.3.1.3 Panes

Create a new pane by spawning a new shell inside the current window, via a horizontal split.2 The C-M-S-e variant splits it vertically.

keybindings-pane-create
bind-key -T root         C-M-e      split-window -c "#{pane_current_path}"
bind-key -T root         C-M-S-e    split-window -h -c "#{pane_current_path}"

Use C-M-{j,k} to switch to the next/previous panes. The cryptic commands (":.+" and ":.-") used here were derived from the default configuration from tmux list-keys and the "special tokens" discussion in the "COMMANDS" section of the manpage.

keybindings-pane-navigate
bind-key -T root         C-M-j      select-pane -Z -t :.+
bind-key -T root         C-M-k      select-pane -Z -t :.-

Move the current pane up/down inside the current window.

keybindings-pane-move
bind-key -T root         C-M-S-j    swap-pane -Z -D
bind-key -T root         C-M-S-k    swap-pane -Z -U

Move the current pane to the window on the left or right. Note that this "detaches" the current pane out of the current window and moves it into the adjacent window. It was determined (at some point) that this is more useful than moving the entire window (with all of its panes) over to the left/right.

keybindings-pane-move-across-windows
bind-key -T root         C-M-S-h  run-shell '~/syscfg/script/tmux_move_pane.sh \
	"#{session_name}" \
	"#{window_index}" \
	"LEFT"'
bind-key -T root         C-M-S-l  run-shell '~/syscfg/script/tmux_move_pane.sh \
	"#{session_name}" \
	"#{window_index}" \
	"RIGHT"'
tmux_move_pane.sh
🎯 script/tmux_move_pane.sh
# Usage: $0 <SESSION_ID> <WINDOW_INDEX> <L|R>
#
# Move the current tmux pane in the given session either left or right, across
# windows --- cycling to the front or back as necessary.

set -euo pipefail

main()
{
	local session_name
	local window_index
	local direction

	local target_window_index
	local window_index_last

	session_name="${1}"
	window_index="${2}"
	direction="${3}"

	# This only works because we turn on "renumber-windows" in tmux.
	window_index_last="$(tmux list-windows -F \#I | tail -n1)"

	if [[ "${direction}" == LEFT ]]; then
		# If we're already at the leftmost window, move the pane to the last
		# window.
		if (( "${window_index}" == 0 )); then
			target_window_index="${window_index_last}"
		else
			target_window_index="$((window_index - 1))"
		fi
	else
		# If we're at the rightmost window, then we have to move the pane to the
		# first window.
		if (( "${window_index}" == "${window_index_last}" )); then
			target_window_index="0"
		else
			target_window_index="$((window_index + 1))"
		fi
	fi

	tmux join-pane -t "${session_name}:${target_window_index}"
	tmux set key-table root
}

main "$@"

Move the current pane out to a new window on the right. This only works if there is more than one pane in the current window.

This can be done to undo the effect of C-M-S-h, to an extent.

keybindings-pane-move-across-windows-new-window
bind-key -T root         C-M-S-o    break-pane -a -t : \; set key-table root

Change the layout of the panes. This is useful for converting horizontal splits into vertical splits (if we're switching from/to a display in portrait mode, for example).

keybindings-pane-next-layout
bind-key -T root         C-M-m      next-layout

3.4 "Normal" mode bindings (prefix key-table)

We don't have many customizations for the prefix key-table, because tmux by default gives us many keybindings (such as : to run tmux commands by typing them out).

keybindings-normal-mode
keybindings-normal-mode-exit
keybindings-normal-mode-enter
keybindings-reload-config

3.4.1 Exit normal mode

These bindings exit "normal" mode by setting the key-table back to the root (or "insert" mode). There is more than one binding, for convenience.

keybindings-normal-mode-exit
bind-key -T prefix       i          set key-table root
bind-key -T prefix       q          set key-table root
bind-key -T prefix       Enter      set key-table root

3.4.2 Enter normal mode when detaching

When detaching from the current tmux session, change back the keymap to root, so that when we reattach, we start out with root (insert mode). This is more intuitive as it makes insert mode the "default" regardless of how we start tmux (either by running tmux or by reattaching to an existing tmux session).

keybindings-normal-mode-enter
set-hook -g client-detached[0] "set key-table root"

3.4.3 Reload configuration

keybindings-reload-config
bind-key -T prefix r {
    source-file ~/.tmux.conf
    display-message "\~/tmux.conf reloaded."
}

3.5 copy-mode bindings

copy-mode is amazing because it lets us copy things out of terminal applications, where those applications may not allow us to copy things out. For example, we can use this mode to copy things out of emacs's minibuffer completion menu.

This mode also lets us effectively treat all tmux panes as a kind of read-only buffer, because inside each pane we can always scroll back to see what we missed or need to copy out for reference.

keybindings-copy-mode
keybindings-copy-mode-enter
keybindings-copy-mode-vi
keybindings-copy-mode-selection
keybindings-copy-mode-navigation
keybindings-copy-mode-paste

3.5.1 Enter copy-mode

keybindings-copy-mode-enter
bind-key -T root C-M-y {
  copy-mode
  set key-table root
}

3.5.2 Vi-mode bindings in copy-mode

Use vi-style key bindings in copy mode. That is, turn on copy-mode-vi bindings in copy-mode that already come out of the box for tmux; see tmux list-keys.

keybindings-copy-mode-vi
set -gw mode-keys vi

3.5.3 Selection and copying

keybindings-copy-mode-selection
bind-key -T copy-mode-vi y          send-keys -X copy-selection
bind-key -T copy-mode-vi v          send-keys -X begin-selection
bind-key -T copy-mode-vi Escape     send-keys -X clear-selection

3.5.4 Navigation

Move around more quickly. We can also use PGUP and PGDOWN to move by entire pages if needed.

keybindings-copy-mode-navigation
bind-key -T copy-mode-vi BSpace     send-keys -X -N 10 cursor-up
bind-key -T copy-mode-vi Space      send-keys -X -N 10 cursor-down

3.5.5 Pasting

Every time we copy something from copy-mode, tmux saves it as the latest entry in its "buffers". The paste-buffer command just pastes the latest buffer into the current terminal application.

In practice this isn't used very frequently, as we almost always use Super-v. It's here just in case we need to reach inside tmux's buffer instead of the system clipboard (if the two ever go out of sync).

keybindings-copy-mode-paste
bind-key -T root         C-M-p      paste-buffer

3.6.1 Mouse mode

Allow binding of mouse events as keys. As a side effect, this also changes the behavior of the mouse wheel from scrolling through the command history to scrolling back into the buffer (copy-mode).

The main reason we want to enable the mouse is because it allows us to resize tmux panes and emacs windows with the mouse. While we don't have to do this frequently, it does come in handy when we need it.

mouse-mode
set -gw mouse on

3.6.2 Disable mouse wheel

Disable the mouse wheel in the status bar, because by default it is used to switch windows (why would anyone want to work this way?).

See https://superuser.com/a/1492302.

disable-mouse-wheel
unbind-key -T root WheelUpStatus
unbind-key -T root WheelDownStatus

3.7 Miscellaneous

keybindings-misc
esc-no-delay

3.7.1 Don't wait for additional keys after pressing ESC

By default tmux waits a little bit after the ESC key is pressed, presumably to wait for events that trickle in from terminals that might send the ESC key a bit prematurely.

However on WezTerm (and probably other modern terminals), there has been no noticeable usability issues when we disable this delay. So disable it altogether.

esc-no-delay
set -s escape-time 0

3.8 CSI-u bindings

Make tmux encode C-S-{a-z} sequences (and others) with the "CSI u" scheme.3 Otherwise, applications that do support it like terminal emacs don't even get these events (probably because tmux swallows those keys and does not send them).

Note that there are some regressions and funny behaviors from tmux. For example, C-[ cannot be bound (it is indistinguishable from ESC). The same goes for C-j RET and C-i TAB. For a discussion about this regression, see https://github.com/tmux/tmux/issues/2705.

Also, C-# is allowed (notice that it is not defined as C-S-3), but C-$ is not allowed and we have to use C-S-4 (this is a cosmetic oddity for our configuration and does not materially affect us).

3.8.1 Meta+Shift and letters

keybindings-meta-shift-letters
bind-key -T root         M-S-a      send-keys Escape  "[97;4u"
bind-key -T root         M-S-b      send-keys Escape  "[97;4u"
bind-key -T root         M-S-c      send-keys Escape  "[99;4u"
bind-key -T root         M-S-d      send-keys Escape  "[100;4u"
bind-key -T root         M-S-e      send-keys Escape  "[101;4u"
bind-key -T root         M-S-f      send-keys Escape  "[102;4u"
bind-key -T root         M-S-g      send-keys Escape  "[103;4u"
bind-key -T root         M-S-h      send-keys Escape  "[104;4u"
bind-key -T root         M-S-i      send-keys Escape  "[105;4u"
bind-key -T root         M-S-j      send-keys Escape  "[106;4u"
bind-key -T root         M-S-k      send-keys Escape  "[107;4u"
bind-key -T root         M-S-l      send-keys Escape  "[108;4u"
bind-key -T root         M-S-m      send-keys Escape  "[109;4u"
bind-key -T root         M-S-n      send-keys Escape  "[110;4u"
bind-key -T root         M-S-o      send-keys Escape  "[111;4u"
bind-key -T root         M-S-p      send-keys Escape  "[112;4u"
bind-key -T root         M-S-q      send-keys Escape  "[113;4u"
bind-key -T root         M-S-r      send-keys Escape  "[114;4u"
bind-key -T root         M-S-s      send-keys Escape  "[115;4u"
bind-key -T root         M-S-t      send-keys Escape  "[116;4u"
bind-key -T root         M-S-u      send-keys Escape  "[117;4u"
bind-key -T root         M-S-v      send-keys Escape  "[118;4u"
bind-key -T root         M-S-w      send-keys Escape  "[119;4u"
bind-key -T root         M-S-x      send-keys Escape  "[120;4u"
bind-key -T root         M-S-y      send-keys Escape  "[121;4u"
bind-key -T root         M-S-z      send-keys Escape  "[122;4u"

3.8.2 Control+Shift and letters

keybindings-ctrl-shift-letters
bind-key -T root         C-S-a      send-keys Escape  "[97;6u"
bind-key -T root         C-S-b      send-keys Escape  "[97;6u"
bind-key -T root         C-S-c      send-keys Escape  "[99;6u"
bind-key -T root         C-S-d      send-keys Escape  "[100;6u"
bind-key -T root         C-S-e      send-keys Escape  "[101;6u"
bind-key -T root         C-S-f      send-keys Escape  "[102;6u"
bind-key -T root         C-S-g      send-keys Escape  "[103;6u"
bind-key -T root         C-S-h      send-keys Escape  "[104;6u"
bind-key -T root         C-S-i      send-keys Escape  "[105;6u"
bind-key -T root         C-S-j      send-keys Escape  "[106;6u"
bind-key -T root         C-S-k      send-keys Escape  "[107;6u"
bind-key -T root         C-S-l      send-keys Escape  "[108;6u"
bind-key -T root         C-S-m      send-keys Escape  "[109;6u"
bind-key -T root         C-S-n      send-keys Escape  "[110;6u"
bind-key -T root         C-S-o      send-keys Escape  "[111;6u"
bind-key -T root         C-S-p      send-keys Escape  "[112;6u"
bind-key -T root         C-S-q      send-keys Escape  "[113;6u"
bind-key -T root         C-S-r      send-keys Escape  "[114;6u"
bind-key -T root         C-S-s      send-keys Escape  "[115;6u"
bind-key -T root         C-S-t      send-keys Escape  "[116;6u"
bind-key -T root         C-S-u      send-keys Escape  "[117;6u"
bind-key -T root         C-S-v      send-keys Escape  "[118;6u"
bind-key -T root         C-S-w      send-keys Escape  "[119;6u"
bind-key -T root         C-S-x      send-keys Escape  "[120;6u"
bind-key -T root         C-S-y      send-keys Escape  "[121;6u"
bind-key -T root         C-S-z      send-keys Escape  "[122;6u"

3.8.3 Control and punctuation

keybindings-ctrl-punc
bind-key -T root         C-!        send-keys Escape "[33;5u"
# tmux cannot parse C-", so we send C-S-' from wezterm.
bind-key -T root         C-S-\'     send-keys Escape "[39;6u"
bind-key -T root         C-\#       send-keys Escape "[35;5u"
bind-key -T root         C-S-4      send-keys Escape "[52;6u"
bind-key -T root         C-S-5      send-keys Escape "[53;6u"
bind-key -T root         C-S-7      send-keys Escape "[55;6u"
bind-key -T root         C-\'       send-keys Escape "[39;5u"
bind-key -T root         C-(        send-keys Escape "[40;5u"
bind-key -T root         C-)        send-keys Escape "[41;5u"
bind-key -T root         C-S-8      send-keys Escape "[56;6u"
bind-key -T root         C-+        send-keys Escape "[43;5u"
bind-key -T root         C-,        send-keys Escape "[44;5u"
bind-key -T root         C--        send-keys Escape "[45;5u"
bind-key -T root         C-.        send-keys Escape "[46;5u"
bind-key -T root         C-/        send-keys Escape "[47;5u"
bind-key -T root         C-0        send-keys Escape "[48;5u"
bind-key -T root         C-1        send-keys Escape "[49;5u"
bind-key -T root         C-2        send-keys Escape "[50;5u"
bind-key -T root         C-3        send-keys Escape "[51;5u"
bind-key -T root         C-4        send-keys Escape "[52;5u"
bind-key -T root         C-5        send-keys Escape "[53;5u"
bind-key -T root         C-6        send-keys Escape "[54;5u"
bind-key -T root         C-7        send-keys Escape "[55;5u"
bind-key -T root         C-8        send-keys Escape "[56;5u"
bind-key -T root         C-9        send-keys Escape "[57;5u"
bind-key -T root         C-:        send-keys Escape "[58;5u"
bind-key -T root         C-\;       send-keys Escape "[59;5u"
bind-key -T root         C-<        send-keys Escape "[60;5u"
bind-key -T root         C-=        send-keys Escape "[61;5u"
bind-key -T root         C->        send-keys Escape "[62;5u"
bind-key -T root         C-S-/      send-keys Escape "[47;6u"
bind-key -T root         C-@        send-keys Escape "[64;5u"
bind-key -T root         C-\\       send-keys Escape "[92;5u"
bind-key -T root         C-]        send-keys Escape "[93;5u"
bind-key -T root         C-^        send-keys Escape "[94;5u"
bind-key -T root         C-_        send-keys Escape "[95;5u"
bind-key -T root         C-`        send-keys Escape "[96;5u"
bind-key -T root         C-S-[      send-keys Escape "[91;6u"
bind-key -T root         C-S-\\     send-keys Escape "[92;6u"
bind-key -T root         C-S-]      send-keys Escape "[93;6u"
bind-key -T root         C-S-`      send-keys Escape "[96;6u"

3.8.4 Meta and punctuation

keybindings-meta-punc
bind-key -T root         M-!        send-keys Escape "[33;3u"
bind-key -T root         M-S-\'     send-keys Escape "[39;4u"
bind-key -T root         M-\#       send-keys Escape "[35;3u"
bind-key -T root         M-S-4      send-keys Escape "[52;4u"
bind-key -T root         M-S-5      send-keys Escape "[53;4u"
bind-key -T root         M-S-7      send-keys Escape "[55;4u"
bind-key -T root         M-\'       send-keys Escape "[39;3u"
bind-key -T root         M-(        send-keys Escape "[40;3u"
bind-key -T root         M-)        send-keys Escape "[41;3u"
bind-key -T root         M-S-8      send-keys Escape "[56;4u"
bind-key -T root         M-+        send-keys Escape "[43;3u"
bind-key -T root         M-,        send-keys Escape "[44;3u"
bind-key -T root         M--        send-keys Escape "[45;3u"
bind-key -T root         M-.        send-keys Escape "[46;3u"
bind-key -T root         M-/        send-keys Escape "[47;3u"
bind-key -T root         M-0        send-keys Escape "[48;3u"
bind-key -T root         M-1        send-keys Escape "[49;3u"
bind-key -T root         M-2        send-keys Escape "[50;3u"
bind-key -T root         M-3        send-keys Escape "[51;3u"
bind-key -T root         M-4        send-keys Escape "[52;3u"
bind-key -T root         M-5        send-keys Escape "[53;3u"
bind-key -T root         M-6        send-keys Escape "[54;3u"
bind-key -T root         M-7        send-keys Escape "[55;3u"
bind-key -T root         M-8        send-keys Escape "[56;3u"
bind-key -T root         M-9        send-keys Escape "[57;3u"
bind-key -T root         M-:        send-keys Escape "[58;3u"
bind-key -T root         M-\;       send-keys Escape "[59;3u"
bind-key -T root         M-<        send-keys Escape "[60;3u"
bind-key -T root         M-=        send-keys Escape "[61;3u"
bind-key -T root         M->        send-keys Escape "[62;3u"
bind-key -T root         M-S-/      send-keys Escape "[47;4u"
bind-key -T root         M-@        send-keys Escape "[64;3u"
bind-key -T root         M-[        send-keys Escape "[91;3u"
bind-key -T root         M-\\       send-keys Escape "[92;3u"
bind-key -T root         M-]        send-keys Escape "[93;3u"
bind-key -T root         M-^        send-keys Escape "[94;3u"
bind-key -T root         M-_        send-keys Escape "[95;3u"
bind-key -T root         M-`        send-keys Escape "[96;3u"
bind-key -T root         M-S-\\     send-keys Escape "[92;4u"
bind-key -T root         M-S-]      send-keys Escape "[93;4u"
bind-key -T root         M-S-`      send-keys Escape "[96;4u"

3.8.5 Control+Meta and punctuation

keybindings-ctrl-meta-punc
bind-key -T root         C-M-!      send-keys Escape "[33;7u"
bind-key -T root         C-M-S-\'   send-keys Escape "[39;8u"
bind-key -T root         C-M-\#     send-keys Escape "[35;7u"
bind-key -T root         C-M-S-4    send-keys Escape "[52;8u"
bind-key -T root         C-M-S-5    send-keys Escape "[53;8u"
bind-key -T root         C-M-S-7    send-keys Escape "[55;8u"
bind-key -T root         C-M-\'     send-keys Escape "[39;7u"
bind-key -T root         C-M-(      send-keys Escape "[40;7u"
bind-key -T root         C-M-)      send-keys Escape "[41;7u"
bind-key -T root         C-M-S-8    send-keys Escape "[56;8u"
bind-key -T root         C-M-+      send-keys Escape "[43;7u"
bind-key -T root         C-M-,      send-keys Escape "[44;7u"
bind-key -T root         C-M--      send-keys Escape "[45;7u"
bind-key -T root         C-M-.      send-keys Escape "[46;7u"
bind-key -T root         C-M-/      send-keys Escape "[47;7u"
bind-key -T root         C-M-0      send-keys Escape "[48;7u"
bind-key -T root         C-M-1      send-keys Escape "[49;7u"
bind-key -T root         C-M-2      send-keys Escape "[50;7u"
bind-key -T root         C-M-3      send-keys Escape "[51;7u"
bind-key -T root         C-M-4      send-keys Escape "[52;7u"
bind-key -T root         C-M-5      send-keys Escape "[53;7u"
bind-key -T root         C-M-6      send-keys Escape "[54;7u"
bind-key -T root         C-M-7      send-keys Escape "[55;7u"
bind-key -T root         C-M-8      send-keys Escape "[56;7u"
bind-key -T root         C-M-9      send-keys Escape "[57;7u"
bind-key -T root         C-M-:      send-keys Escape "[58;7u"
bind-key -T root         C-M-\;     send-keys Escape "[59;7u"
bind-key -T root         C-M-<      send-keys Escape "[60;7u"
bind-key -T root         C-M-=      send-keys Escape "[61;7u"
bind-key -T root         C-M->      send-keys Escape "[62;7u"
bind-key -T root         C-M-S-/    send-keys Escape "[47;8u"
bind-key -T root         C-M-@      send-keys Escape "[64;7u"
bind-key -T root         C-M-\\     send-keys Escape "[92;7u"
bind-key -T root         C-M-]      send-keys Escape "[93;7u"
bind-key -T root         C-M-^      send-keys Escape "[94;7u"
bind-key -T root         C-M-_      send-keys Escape "[95;7u"
bind-key -T root         C-M-`      send-keys Escape "[96;7u"
bind-key -T root         C-M-S-\\   send-keys Escape "[92;8u"
bind-key -T root         C-M-S-]    send-keys Escape "[93;8u"
bind-key -T root         C-M-S-`    send-keys Escape "[96;8u"

3.8.6 Special keys

3.8.6.1 Tab
keybindings-special-tab
bind-key -T root         M-Tab      send-keys Escape "[9;3u"
bind-key -T root         M-S-Tab    send-keys Escape "[9;4u"
bind-key -T root         C-Tab      send-keys Escape "[9;5u"
bind-key -T root         C-S-Tab    send-keys Escape "[9;6u"
bind-key -T root         C-M-Tab    send-keys Escape "[9;7u"
bind-key -T root         C-M-S-Tab  send-keys Escape "[9;8u"
3.8.6.2 Enter (RET)
keybindings-special-enter
bind-key -T root         S-Enter     send-keys Escape "[13;2u"
bind-key -T root         M-Enter     send-keys Escape "[13;3u"
bind-key -T root         M-S-Enter   send-keys Escape "[13;4u"
bind-key -T root         C-Enter     send-keys Escape "[13;5u"
bind-key -T root         C-S-Enter   send-keys Escape "[13;6u"
bind-key -T root         C-M-Enter   send-keys Escape "[13;7u"
bind-key -T root         C-M-S-Enter send-keys Escape "[13;8u"

Special handling of "m" to avoid conflicting with Enter key. Only let Emacs handle the CSI-u binding of "C-m". For other applications, just send the usual "ENTER" key because they might not know how to deal with this.

keybindings-special-m
bind-key -T root         C-m        if-shell -F $EMACSLIKE "send-keys Escape '[109;5u'" "send-keys -H 0d"
3.8.6.3 Backspace
keybindings-special-backspace
bind-key -T root         S-BSpace     send-keys Escape "[127;2u"
bind-key -T root         M-BSpace     send-keys Escape "[127;3u"
bind-key -T root         M-S-BSpace   send-keys Escape "[127;4u"
bind-key -T root         C-BSpace     send-keys Escape "[127;5u"
bind-key -T root         C-S-BSpace   send-keys Escape "[127;6u"
bind-key -T root         C-M-BSpace   send-keys Escape "[127;7u"
bind-key -T root         C-M-S-BSpace send-keys Escape "[127;8u"
3.8.6.4 Space

Note how we don't encode S-SPC. This is because when we type in capital letters without CAPSLOCK, we want to be able to hold down the Shift key while typing out the words (with spaces in between them).

keybindings-special-space
bind-key -T root         M-Space     send-keys Escape "[32;3u"
bind-key -T root         M-S-Space   send-keys Escape "[32;4u"
bind-key -T root         C-Space     send-keys Escape "[32;5u"
bind-key -T root         C-S-Space   send-keys Escape "[32;6u"
bind-key -T root         C-M-Space   send-keys Escape "[32;7u"
bind-key -T root         C-M-S-Space send-keys Escape "[32;8u"

3.9 Passthrough mode

The sole point of this mode is to allow us to avoid typing in C-b twice to manipulate a nested tmux session over ssh. That is, by default if you run tmux, then ssh into another machine, and then run tmux inside that ssh session again, you will have a nested tmux instance. In order to manipulate the inner instance, you have to press C-b twice, where the first C-b is captured by the outer tmux instance and the second C-b is sent into the inner tmux instance. Then you can press another key combination to manipulate the inner tmux instance. Needless to say, this "press C-b twice" ceremony is too unwieldy.

Passthrough mode (for the outer tmux instance) allows us to send more keys directly to the inner tmux instance, by making the outer instance relay those keys (using send-keys) that it would have normally intercepted and acted upon. Tmux by default relays "regular" keys like letters (without modifiers) to the underlying terminal application, so we don't have to define those bindings here. But tmux does not always relay keys for certain key combinations, notably those keys pressed with Control or Control+Shift modifiers. This is why we have to explicitly tell tmux to relay those troublesome keys while passthrough mode is active.

We want this mode to be sticky. Entering and leaving this mode can be done with C-M-b. Once passthrough mode is active, we can freely use our existing "insert" mode bindings directly on the nested tmux session.

keybindings-passthrough-mode
bind-key -T root         C-M-b      {
  set key-table passthrough
  set prefix None
  set -F status-style "bg=cyan fg=#{L_TMUX_COLOR_CURSOR} none"
}
bind-key -T passthrough  C-M-b      {
  set key-table root
  set prefix C-b
  set -F status-style "bg=#{L_TMUX_COLOR_TEXT} fg=#{L_TMUX_COLOR_CURSOR} none"
}

keybindings-passthrough-mode-ctrl-shift-letters
keybindings-passthrough-mode-ctrl-meta-shift-letters
keybindings-passthrough-mode-other

3.9.1 Control+Shift and letters

keybindings-passthrough-mode-ctrl-shift-letters
bind-key -T passthrough  C-S-a      send-keys Escape "[97;6u"
bind-key -T passthrough  C-S-b      send-keys Escape "[98;6u"
bind-key -T passthrough  C-S-c      send-keys Escape "[99;6u"
bind-key -T passthrough  C-S-d      send-keys Escape "[100;6u"
bind-key -T passthrough  C-S-e      send-keys Escape "[101;6u"
bind-key -T passthrough  C-S-f      send-keys Escape "[102;6u"
bind-key -T passthrough  C-S-g      send-keys Escape "[103;6u"
bind-key -T passthrough  C-S-h      send-keys Escape "[104;6u"
bind-key -T passthrough  C-S-i      send-keys Escape "[105;6u"
bind-key -T passthrough  C-S-j      send-keys Escape "[106;6u"
bind-key -T passthrough  C-S-k      send-keys Escape "[107;6u"
bind-key -T passthrough  C-S-l      send-keys Escape "[108;6u"
bind-key -T passthrough  C-S-m      send-keys Escape "[109;6u"
bind-key -T passthrough  C-S-n      send-keys Escape "[110;6u"
bind-key -T passthrough  C-S-o      send-keys Escape "[111;6u"
bind-key -T passthrough  C-S-p      send-keys Escape "[112;6u"
bind-key -T passthrough  C-S-q      send-keys Escape "[113;6u"
bind-key -T passthrough  C-S-r      send-keys Escape "[114;6u"
bind-key -T passthrough  C-S-s      send-keys Escape "[115;6u"
bind-key -T passthrough  C-S-t      send-keys Escape "[116;6u"
bind-key -T passthrough  C-S-u      send-keys Escape "[117;6u"
bind-key -T passthrough  C-S-v      send-keys Escape "[118;6u"
bind-key -T passthrough  C-S-w      send-keys Escape "[119;6u"
bind-key -T passthrough  C-S-x      send-keys Escape "[120;6u"
bind-key -T passthrough  C-S-y      send-keys Escape "[121;6u"
bind-key -T passthrough  C-S-z      send-keys Escape "[122;6u"

3.9.2 Control+Meta+Shift and letters

keybindings-passthrough-mode-ctrl-meta-shift-letters
bind-key -T passthrough  C-M-S-a    send-keys Escape "[97;8u"
bind-key -T passthrough  C-M-S-b    send-keys Escape "[98;8u"
bind-key -T passthrough  C-M-S-c    send-keys Escape "[99;8u"
bind-key -T passthrough  C-M-S-d    send-keys Escape "[100;8u"
bind-key -T passthrough  C-M-S-e    send-keys Escape "[101;8u"
bind-key -T passthrough  C-M-S-f    send-keys Escape "[102;8u"
bind-key -T passthrough  C-M-S-g    send-keys Escape "[103;8u"
bind-key -T passthrough  C-M-S-h    send-keys Escape "[104;8u"
bind-key -T passthrough  C-M-S-i    send-keys Escape "[105;8u"
bind-key -T passthrough  C-M-S-j    send-keys Escape "[106;8u"
bind-key -T passthrough  C-M-S-k    send-keys Escape "[107;8u"
bind-key -T passthrough  C-M-S-l    send-keys Escape "[108;8u"
bind-key -T passthrough  C-M-S-m    send-keys Escape "[109;8u"
bind-key -T passthrough  C-M-S-n    send-keys Escape "[110;8u"
bind-key -T passthrough  C-M-S-o    send-keys Escape "[111;8u"
bind-key -T passthrough  C-M-S-p    send-keys Escape "[112;8u"
bind-key -T passthrough  C-M-S-q    send-keys Escape "[113;8u"
bind-key -T passthrough  C-M-S-r    send-keys Escape "[114;8u"
bind-key -T passthrough  C-M-S-s    send-keys Escape "[115;8u"
bind-key -T passthrough  C-M-S-t    send-keys Escape "[116;8u"
bind-key -T passthrough  C-M-S-u    send-keys Escape "[117;8u"
bind-key -T passthrough  C-M-S-v    send-keys Escape "[118;8u"
bind-key -T passthrough  C-M-S-w    send-keys Escape "[119;8u"
bind-key -T passthrough  C-M-S-x    send-keys Escape "[120;8u"
bind-key -T passthrough  C-M-S-y    send-keys Escape "[121;8u"
bind-key -T passthrough  C-M-S-z    send-keys Escape "[122;8u"

3.9.3 Others

keybindings-passthrough-mode-other
bind-key -T passthrough  C-,        send-keys Escape "[44;5u"
bind-key -T passthrough  C-.        send-keys Escape "[46;5u"
bind-key -T passthrough  C-\;       send-keys Escape "[59;5u"
bind-key -T passthrough  C-Enter    send-keys Escape "[13;5u"

4 Appearance

Use 24-bit colors by setting the TERM environment variable for newly spawned shells to wezterm.

appearance
set -g default-terminal "wezterm"

appearance-messages
appearance-status-bar
appearance-windows
appearance-panes-borders
appearance-copy-mode

4.1 Messages

Make messages appear for 4 seconds by default.

appearance-messages
set -g display-time 4000

4.2 Status bar

Explicitly set status line height, where "on" means 1 row. This can be "2, 3, 4 or 5". In the future if we want to show more information we can set this to "2". See https://stackoverflow.com/a/56215242/437583.

appearance-status-bar
set -g status on

# 1
set -g status-left-length 0
set -g status-right-length 0

# 2
set -g status-left "#(~/syscfg/script/tmux_get_status.sh \
    '#{session_name}'\
    '#{pane_mode}'\
    '#{client_key_table}')"
set -g status-right ""

# 3
set -gF status-style "bg=#{L_TMUX_COLOR_TEXT} fg=#{L_TMUX_COLOR_CURSOR} none"

# 4
set -gF status-left-style "bg=#{L_TMUX_COLOR_CURSOR} fg=#{L_TMUX_COLOR_TEXT} bold"

Don't set limits on status line portion lengths 1. This way, if either status-left or status-right gets too long, they don't get truncated by tmux.

Set status bar contents 2.

Set background color for status bar 3.

Set the color for the name of the session (#{session_name} tmux variable) 4.

4.2.1 Windows

Automatically renumber windows if one is closed, so that there are no "gaps" in between. See https://unix.stackexchange.com/a/51879/72230. We don't actually use window numbers at all, but this makes things more consistent semantically underneath our appearance layer (for example, these numbers matter when we want to do refer to windows programmatically from a script).

appearance-windows-renumber
set -g renumber-windows on

Rename windows every second (both active and inactive) so that we get accurate window names depending on what the active pane in each window is doing.

appearance-windows-naming-interval
set -g status-interval 1

We have windows listed in the status bar. Separate them with the space character.

appearance-windows-separator
set -g window-status-separator " "
4.2.1.1 Automatic window renaming

Set window name contents by automatically renaming them. We use get_window_format.bb for this, but basically if the current command is zsh (our usual shell), then use the current path as the name. Otherwise display the name of the running command.

See https://stackoverflow.com/a/68043814/437583 for the inspiration.

appearance-windows-automatic-naming
set -gw automatic-rename on
set -g automatic-rename-format \
    "#(~/syscfg/script/tmux/get_window_format.bb \
    '#{pane_current_command}'\
    '#{pane_current_path}'\
    '#{window_name}'\
    '#{window_id}'\
    '#{pane_id}'\
    '#{window_flags}'\
    '#{window_panes}'\
    0)"

Colorize window names, for active and inactive windows.

appearance-windows-automatic-active-inactive
set -gw window-status-current-format \
    "#(~/syscfg/script/tmux/get_window_format.bb \
    '#{pane_current_command}'\
    '#{pane_current_path}'\
    '#{window_name}'\
    '#{window_id}'\
    '#{pane_id}'\
    '#{window_flags}'\
    '#{window_panes}'\
    1)"
set -gw window-status-format \
    "#(~/syscfg/script/tmux/get_window_format.bb \
    '#{pane_current_command}'\
    '#{pane_current_path}'\
    '#{window_name}'\
    '#{window_id}'\
    '#{pane_id}'\
    '#{window_flags}'\
    '#{window_panes}'\
    0)"

4.3 Panes and borders

Ask terminal if focus events are supported and if so, pass focus events to applications. This makes terminal emacs aware that it loses focus when we switch away from it from tmux (e.g., when we move to a different pane).

appearance-panes-borders-focus
set -g focus-events on

When we attach into a remote tmux session, it may be the case that the local terminal size is smaller than the size of the terminal on the remote machine. The difference in these sizes are shown by tmux with the fill-character. By default it is a ".", but we change it here to "╳" (aka "BOX DRAWINGS LIGHT DIAGONAL CROSS", or Unicode codepoint 2573) because of the interesting quality it has where there is virtually no gap between these characters written in succession, next to each other:

╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
appearance-panes-borders-bg-fill
set -gw fill-character "╳"

Automatically re-balance panes after they are created and/or deleted.

appearance-panes-borders-balance
set-hook -g after-split-window  "select-layout -E"
set-hook -g pane-exited         "select-layout -E"

Set colors for pane split borders.

appearance-panes-borders-splits
set -g pane-active-border-style "bg=white  fg=white"
set -g pane-border-style        "bg=black  fg=white"

Set colors for the active pane. For some reason this option is named "window-active-style" but it actually only affects the active pane within a window.

appearance-panes-borders-active
set -gw window-active-style "bg=#343c48 fg=white"
set -gw window-style "bg=black fg=brightwhite"

4.4 Copy mode

Set colors for copy-mode selections.

appearance-copy-mode
set -gw mode-style "fg=#343c48 bg=brightgreen bold"

5 Miscellaneous

misc
# Turn off wrapping when searching in copy-mode (mimic behavior in less(1)).
set -g wrap-search off

# Set history to 10K lines.
set -g history-limit 10000

# Avoid "clipetty--emit: Opening output file: Permission denied, /dev/pts/0".
# See https://github.com/hlissner/doom-emacs/issues/3674#issuecomment-670840781
# and
# https://github.com/spudlyo/clipetty/blame/01b39044b9b65fa4ea7d3166f8b1ffab6f740362/README.md#L160.
set -ag update-environment "SSH_TTY"

# Allow OSC52 to set the clipboard.
set -g set-clipboard on

6 Scripts

6.1 Window name and format

This script is called by Automatic window renaming.

We want to be able to name windows on the status bar a certain way. Each window can have more than one pane, and we only care about the current (active) pane and what's running in there. So we are getting pane-specific information; however for simplicity we just call this the window name here.

get_window_format.bb
🎯 script/tmux/get_window_format.bb
(ns get-window-format
  (:require [clojure.string :as str]
            [babashka.process :refer [shell]]))
f:get-window-type
f:get-tmux-pane-pwd-cached
f:get-modified-window-name
f:get-window-style
f:get-window-name-format
f:main

6.1.1 Get window name and format

6.1.1.1 Window type

There are 4 types of windows based on what the currently active pane inside that window is doing:

  1. manually named (windows that were manually named (via tmux rename-window) as mnw->NAME where the mnw-> is our own magic prefix denoting such a window),

  2. ssh (typically a remote (nested) tmux session),

  3. zsh (our interactive shell of choice), and

  4. other (where the window is running some other command other than ssh or zsh).

f:get-window-type
(defn get-window-type
  [window-name
   pane-current-command]
  (cond
    (str/starts-with? window-name "mnw->") :manually-named
    (or (str/starts-with? window-name "ssh")
        (= pane-current-command "ssh")) :ssh
    (= pane-current-command "zsh") :zsh
    :else :other-command))
6.1.1.1.1 Test
t:get-window-type
(deftest get-window-type
  (is (= :other-command (sut/get-window-type "" "")))
  (is (= :other-command (sut/get-window-type "" "some-command")))
  (is (= :zsh (sut/get-window-type "" "zsh")))
  (is (= :ssh (sut/get-window-type "ssh" "some-command")))
  (is (= :ssh (sut/get-window-type "" "ssh")))
  (is (= :manually-named (sut/get-window-type "mnw->foo" "some-command"))))
6.1.1.2 Shorten PWD

This shells out to a shell script that does the path shortening. It's an external command, so we should probably mock it but we're too lazy.

f:get-tmux-pane-pwd-cached
(defn- get-tmux-pane-pwd-cached
  [window-id
   pane-id
   pane-current-path]
  (-> (shell {:out :string
              :err :string}
             "tmux_pane_pwd_cached.sh"
             window-id
             pane-id
             pane-current-path)
      :out
      str/trimr))
6.1.1.3 Get (modified) window name

This is the final name of the window and is what will be most of what's displayed in the tmux status bar, other than some extra decorations around it.

The default window-name in tmux is the name of the current command.

f:get-modified-window-name
(defn get-modified-window-name
  [window-type
   window-name
   window-id
   pane-id
   pane-current-command
   pane-current-path]
  (case window-type
    :manually-named (subs window-name 5)
    :ssh (if (str/starts-with? window-name "ssh") window-name "ssh")
    :zsh (get-tmux-pane-pwd-cached window-id pane-id pane-current-path)
    pane-current-command))
6.1.1.3.1 Test
t:get-modified-window-name
(deftest get-modified-window-name
  (is (= "foo" (sut/get-modified-window-name :manually-named "mnw->foo" "@1" "%2" "some-command" "/path/foo/bar")))
  (is (= "ssh" (sut/get-modified-window-name :ssh "ssh" "@1" "%2" "some-command" "/path/foo/bar")))
  (is (= "ssh->remote" (sut/get-modified-window-name :ssh "ssh->remote" "@1" "%2" "some-command" "/path/foo/bar")))
  (is (= "/path/foo/bar" (sut/get-modified-window-name :zsh "ssh" "@1" "%2" "some-command" "/path/foo/bar")))
  (is (= "some-command" (sut/get-modified-window-name :other-command "" "@1" "%2" "some-command" "/path/foo/bar"))))
6.1.1.4 Colors for window types

Note how the colors are inversed when the window becomes active. This makes it much easier to visually see which window is the current window in the status bar.

f:get-window-style
(defn get-window-style
  [window-type
   is-current-window?]
  (case window-type
    :manually-named
    (if is-current-window?
      {:style1 "#[bold bg=cyan fg=#343c48]"
       :style2 "#[bold bg=brightcyan fg=#343c48]"}
      {:style1 "#[bg=black fg=cyan]"
       :style2 "#[bg=black fg=cyan]"})

    :ssh
    (if is-current-window?
      {:style1 "#[bold bg=blue fg=#343c48]"
       :style2 "#[bold bg=brightblue fg=#343c48]"}
      {:style1 "#[bg=black fg=blue]"
       :style2 "#[bg=black fg=blue]"})

    :zsh
    (if is-current-window?
      {:style1 "#[bold bg=yellow fg=#343c48]"
       :style2 "#[bold bg=brightyellow fg=#343c48]"}
      {:style1 "#[bg=black fg=yellow]"
       :style2 "#[bg=black fg=yellow]"})

    (if is-current-window?
      {:style1 "#[bold bg=green fg=#343c48]"
       :style2 "#[bold bg=brightgreen fg=#343c48]"}
      {:style1 "#[bg=black fg=green]"
       :style2 "#[bg=black fg=green]"})))
6.1.1.5 Bringing it all together

The window name is augmented with additional formatting and colors. The trailing #[default] at the end is there to reset colors back to the default.

Here's a description of the tmux variables that we send over from Automatic window renaming:

The is-current-window is a boolean "1" or "0" we denote ourselves (to save get-window-name-format the trouble of figuring out what the active window is).

f:get-window-name-format
(defn get-window-name-format
  [pane-current-command
    pane-current-path
    window-name
    window-id
    pane-id
    window-flags
    window-panes
    is-current-window]
  (let [window-flags-str (if (= "" window-flags)
                           " "
                           window-flags)
        window-weight (dec (Integer/parseInt window-panes))
        window-weight-str (if (pos? window-weight)
                            (str window-weight)
                            " ")
        window-type (get-window-type window-name pane-current-command)
        current-window? (= "1" is-current-window)
        {:keys [style1 style2]} (get-window-style window-type current-window?)
        name (get-modified-window-name window-type
                                       window-name
                                       window-id
                                       pane-id
                                       pane-current-command
                                       pane-current-path)]
    (format "%s%s%s%s %s #[default]"
            style1
            window-flags-str
            window-weight-str
            style2
            name)))
6.1.1.5.1 Test

We don't really test things exhaustively because there are just too many combinations. We just test a few key examples.

t:get-window-name-format
(deftest get-window-name-format
  (testing "manually named window"
    (is (= "#[bold bg=cyan fg=#343c48]*2#[bold bg=brightcyan fg=#343c48] foo #[default]"
           (sut/get-window-name-format "zsh" "/path/foo/bar" "mnw->foo" "@1" "%2" "*" "3" "1"))))
  (testing "zsh shows path"
    (is (= "#[bold bg=yellow fg=#343c48]*2#[bold bg=brightyellow fg=#343c48] /path #[default]"
           (sut/get-window-name-format "zsh" "/path" "zsh" "@1" "%2" "*" "3" "1"))))
  (testing "ssh"
    (is (= "#[bold bg=blue fg=#343c48]*2#[bold bg=brightblue fg=#343c48] ssh #[default]"
           (sut/get-window-name-format "ssh" "/path" "ssh" "@1" "%2" "*" "3" "1"))))
  (testing "other-command"
    (is (= "#[bold bg=green fg=#343c48]*2#[bold bg=brightgreen fg=#343c48] some-command #[default]"
           (sut/get-window-name-format "some-command" "/path" "some-command" "@1" "%2" "*" "3" "1")))))

6.1.2 Main

We expect exactly 8 arguments:

  1. pane-current-command

  2. pane-current-path

  3. window-name

  4. window-id

  5. pane-id

  6. window-flags

  7. window-panes

  8. is-current-window

f:main
(defn -main [& args]
  (when (not= 8 (count args))
    (println (format "Need exactly 8 arguments; got %d." (count args)))
    (System/exit 1))
  (println (apply get-window-name-format args)))
(when (= *file* (System/getProperty "babashka.file"))
  (apply -main *command-line-args*))

The System/getProperty stuff is from the Babashka book to make it nicer to interact with the file in the CLI and also the REPL.

6.1.3 Tests

get-window-format-test
🎯 script/tmux/get_window_format_test.clj
(ns get-window-format-test
  (:require [clojure.test :refer [deftest is testing]]
            [get-window-format :as sut]))
t:get-window-type
t:get-modified-window-name
t:get-window-name-format

6.2 Historical (original) Bash script

Below is the original shell script which the Babashka version get_window_format.bb replaces.

tmux_get_window_format.sh
🎯 script/tmux_get_window_format.sh
# Usage: $0 <PANE_CURRENT_COMMAND> <PANE_CURRENT_PATH> <WINDOW_NAME> <WINDOW_ID> <PANE_ID> <WINDOW_FLAGS> <WINDOW_PANES> <IS_CURRENT_WINDOW>
#
# Generate the window name and formatting/style/colors for all active and inactive windows.

set -euo pipefail

main()
{
	local pane_current_command
	local pane_current_path
	local pane_id
	local window_name
	local window_id
	local window_name_generated
	local window_flags
	local window_panes
	local is_current_window

	pane_current_command="${1}"
	pane_current_path="${2}"
	window_name="${3}"
	window_id="${4}"
	pane_id="${5}"
	window_flags="${6}"
	window_panes="${7}"
	is_current_window="${8}"

	local pane_count
	local style1
	local style2

	# Manually-named windows ("mnw").
	if [[ -n "${window_name:-}" ]] && [[ "${window_name}" =~ mnw-\> ]]; then
		window_name_generated="${window_name#mnw->}"
		style1="#[bg=black fg=cyan]"
		style2="#[bg=black fg=cyan]"
		if ((is_current_window)); then
			style1="#[bold bg=cyan fg=black]"
			style2="#[bold bg=brightcyan fg=black]"
		fi
	# SSH sessions are named (and colored) specially because they almost always
	# entail a nested tmux session.
	elif [[ "${window_name}" =~ ^ssh-\> ]]; then
		window_name_generated="${window_name}"
		style1="#[bg=black fg=blue]"
		style2="#[bg=black fg=blue]"
		if ((is_current_window)); then
			style1="#[bold bg=blue fg=black]"
			style2="#[bold bg=brightblue fg=black]"
		fi
	# Plain shell session --- show the shortened $PWD.
	elif [[ "${pane_current_command}" == zsh ]]; then
		window_name_generated="$(~/syscfg/script/tmux_pane_pwd_cached.sh "${window_id}" "${pane_id}" "${pane_current_path}")"
		style1="#[bg=black fg=yellow]"
		style2="#[bg=black fg=yellow]"
		if ((is_current_window)); then
			style1="#[bold bg=yellow fg=black]"
			style2="#[bold bg=brightyellow fg=black]"
		fi
	# Long-running command. Show the command name.
	else
		window_name_generated="${pane_current_command}"
		style1="#[bg=black fg=green]"
		style2="#[bg=black fg=green]"
		if ((is_current_window)); then
			style1="#[bold bg=green fg=black]"
			style2="#[bold bg=brightgreen fg=black]"
		fi
	fi

	if (("${window_panes}" > 1)); then
		pane_count="$((window_panes - 1))"
	else
		pane_count=" "
	fi

	echo "${style1}${window_flags:- }${pane_count}${style2} ${window_name_generated} #[default]"
}

main "$@"

7 Scripts

7.1 Status bar (key table)

tmux_get_status.sh
🎯 script/tmux_get_status.sh
# Usage: $0 <SESSION_NAME> <PANE_MODE> <CLIENT_KEY_TABLE>
#
# Generate the status line for tmux.

set -euo pipefail

# Imitate emacs evil-mode's "<N>" and "<I>" modal signifiers to help
# distinguish between modes. When we're in "prefix mode", it is normal mode
# "<N>", where we can press any number of other keys without having to first
# press C-b (the prefix key). By default we start out in "insert" mode "<I>"
# which is when keys are passed through to the underlying application.
#
# "<V>" signifies copy-mode. We use "<V>" because copy-mode resembles Vim's
# visual selection mode.
#
# For other key tables, use the format "<key-table-name>" if L_TMUX_DEBUG is set
# to 1.
#
# Summary
#
# key-table or mode   | indicator
# --------------------+----------
# root                | <I>
# prefix              | <N>
# copy-mode/view-mode | <V>
# passthrough         | <P>
# other               | <other>
main()
{
	local session_name
	local pane_mode
	local client_key_table

	session_name="${1}"
	pane_mode="${2}"
	client_key_table="${3}"

	local session_format
	local style1

	if [[ "${pane_mode}" =~ (copy|view)-mode ]]; then
		style1=" #[bg=green fg=${L_TMUX_COLOR_TEXT}] <V> "
	elif [[ "${client_key_table}" == prefix ]]; then
		style1=" #[bg=blue fg=${L_TMUX_COLOR_TEXT}] <N> "
	elif [[ "${client_key_table}" == root ]]; then
		style1=" #[bg=${L_TMUX_COLOR_TEXT} fg=${L_TMUX_COLOR_CURSOR}] <I> "
	elif [[ "${client_key_table}" == passthrough ]]; then
		style1=" #[bg=cyan fg=${L_TMUX_COLOR_TEXT}] <P> "
	else
		# Display unrecognized key table names directly.
		style1=" #[bg=red fg=${L_TMUX_COLOR_TEXT}] <${client_key_table}> "
	fi

	session_format=" ${session_name} "
	# If we're SSH'ed into a nested tmux session, colorize the session name a
	# bit differently.
	if [[ -n "${SSH_CONNECTION:-}" ]]; then
		session_format="#[bg=blue] ${session_name} "
	fi

	echo "${session_format}#[bg=${L_TMUX_COLOR_TEXT}]${style1}#[bg=${L_TMUX_COLOR_TEXT}] "
}

main "$@"

7.2 Startup

tmux_startup.sh
🎯 script/tmux_startup.sh
# Usage: $0
#
# Start up various pre-defined tmux windows.

set -euo pipefail

# Check if the given tmux session is blank (has only 1 window and 1 pane). We
# check for equality to 0 because all windows and panes are 0-indexed. Use like
# this:
#
#     session_name="$(tmux display-message -p "#{session_name}")"
#     is_blank_session "${session_name}"
#
is_blank_session()
{
	local session_name="${1:-}"
	tmux list-windows -t "${session_name}" -F "#{window_index}" \
		| tail -n1 | grep -q 0
	tmux list-panes -t "${session_name}" -F "#{pane_index}" \
		| tail -n1 | grep -q 0
}

# Create windows for personal notes.
setup_notes()
{
	local window_emacs_notes

	window_emacs_notes="$(tmux new-window -c ~/lo -P)"
	tmux send-keys -t "${window_emacs_notes}" "./note/open.sh" Enter

}

setup_lilac()
{
	local window_emacs_lilac
	local window_jj_lilac
	local maybe_sleep

	window_emacs_lilac="$(tmux new-window -c ~/prog/lilac -P)"
	maybe_sleep="pgrep -f 'emacs --bg-daemon' >/dev/null || sleep 20"
	tmux send-keys -t "${window_emacs_lilac}" \
		"${maybe_sleep}; d base/lilac.org" Enter

	window_jj_lilac="$(tmux new-window -c ~/prog/lilac -P)"
	tmux send-keys -t "${window_jj_lilac}" "jl" Enter
}

init_session_1()
{
	# Start up main window with Emacs for personal notes.
	setup_notes

	# Create window for Emacs opened up at Lilac, with another extra blank
	# window for jj.
	setup_lilac

	# Go back to main window.
	tmux previous-window
	tmux previous-window
}

init_session_2()
{
	local current_window
	local window_fe_server

	# Navigate to Lilac folder, and start up tangle loop and frontend
	# server. Use the current window for the tangle loop script.
	current_window="$(tmux display-message \
		-p '#{session_name}:#{window_index}')"
	tmux send-keys -t "${current_window}" \
		"cd ~/prog/lilac && ./tangle-loop.sh" Enter

	# Start up frontend server.
	window_fe_server="$(tmux new-window -c ~/prog/lilac -P)"
	tmux send-keys -t "${window_fe_server}" "make dev-cljs" Enter

	# Show tangle loop window.
	tmux previous-window
}

main()
{
	case "${1:-1}" in
	1) init_session_1 ;;
	2) init_session_2 ;;
	*)
		echo >&2 "Unrecognized session ${1}"
		return 1
		;;
	esac
}

main "$@"

7.3 PWD-based window name

tmux_pane_pwd_cached.sh
🎯 script/tmux_pane_pwd_cached.sh
# Script used by tmux to figure out how to display the status bar for the
# current window. Note that this is different from setting the window name (aka
# "#{window_name}", which we purposely avoid due to race conditions.
#
# The point of this script is to avoid spamming melby with too many requests;
# doing additional requests here would mean we would basically double the number
# of requests because currently we already spam melby every second to get the
# prompt. Ideally we could be feeding the shortened pwd information from melby
# back to tmux, but we don't do that on purpose because we want to have a
# separate implementation just in case melby is not working.

set -o errexit
set -o nounset
set -o pipefail
set -o xtrace

pane_pwd_same()
{
  local window_id
  local pane_id

  local pwd_new

  window_id="${1}"
  pane_id="${2}"
  pwd_new="${3}"

  # Format is L_TMUX_PANE_PWD_<WINDOW_ID>=<PWD_LONG>;<PWD_SHORT>
  __pwd_old="$(tmux show-environment L_TMUX_PANE_PWD_"${window_id}")"
  __pwd_old_short="${__pwd_old}"
  __pwd_old="${__pwd_old#*=}"
  __pwd_old="${__pwd_old%;*}"

  __pwd_old_short="${__pwd_old_short#*;}"

  if [[ "${pwd_new}" == "${__pwd_old}" ]]; then
    return 0
  fi

  return 1
}

main()
{
  local window_id
  local pane_id
  local pwd_new
  local pwd_shortened

  window_id="${1}"
  # Get rid of leading "@" symbol, as it is unnecessary.
  window_id="${window_id#@}"
  pane_id="${2}"
  pwd_new="${3}"

  # If there is no change to the active pane's pwd, then there's nothing to do;
  # just reuse the existing window name as-is.
  if pane_pwd_same "${window_id}" "${pane_id}" "${pwd_new}" && [[ -n "${__pwd_old_short}" ]]; then
    # window_name_old="${tmux display-message -p -t "${pane_id}" '#{window_name}'}"
    # if [[ "${window_name_old:0:1}" != "<" ]]; then
    #   echo "${window_name_old}"
    #   return
    # fi
    echo "${__pwd_old_short}"
    return
  fi

  # If the active pane's pwd did change, then we must use a newly shortened
  # window name.
  pwd_shortened="$(~/syscfg/script/simplify_path.sh "${pwd_new}")"
  echo "$pwd_shortened"

  # TODO: Garbage-collect these tmux environment variables when the window is
  # closed.
  #
  # Unfortunately as of tmux 3.2a (Nov 2021) there is no way to set a hook (with
  # set-hook) that runs just before a window is closed. So technically this is a
  # memory leak but it is OK because:
  #
  #   (1) we don't create that many windows (maybe a few hundred, if we have tmux running for months on end);
  #   (2) the rate of the leak is negligible due to (1); and
  #   (3) tmux already garbage-collects these variables when the session is
  #   closed.
  #
  # But this is worth revisiting in the future.
  tmux set-environment "L_TMUX_PANE_PWD_${window_id}" "${pwd_new};${pwd_shortened}"
}

main "$@"

8 Footnotes

1. The idea to make tmux modal comes from this blog post. There, they make the prefix key-table sticky by disabling the C-b key altogether. We don't make the same customization, but we do apply the same idea for our passthrough key-table. ↑ ¶ (Cell 23)

2. Tmux calls this a "vertical" split but I like to think of it as a horizontal split because I imagine a sword running horizontally across the screen to create the split border. ↑ ¶ (Cell 41)

3. https://iterm2.com/documentation-csiu.html ↑ ¶ (Cell 101)

Page metrics

Tangled files (8)

  1. script/tmux/get_window_format.bb
  2. script/tmux/get_window_format_test.clj
  3. script/tmux_get_status.sh
  4. script/tmux_get_window_format.sh
  5. script/tmux_move_pane.sh
  6. script/tmux_pane_pwd_cached.sh
  7. script/tmux_startup.sh
  8. tmux/.tmux.conf

Named cells (78)

  1. Footnote (fn:csi-u)
  2. Footnote (fn:modal-tmux)
  3. Footnote (fn:split-naming-convention)
  4. appearance
  5. appearance-copy-mode
  6. appearance-messages
  7. appearance-panes-borders
  8. appearance-panes-borders-active
  9. appearance-panes-borders-balance
  10. appearance-panes-borders-bg-fill
  11. appearance-panes-borders-focus
  12. appearance-panes-borders-splits
  13. appearance-status-bar
  14. appearance-windows
  15. appearance-windows-automatic-active-inactive
  16. appearance-windows-automatic-naming
  17. appearance-windows-naming-interval
  18. appearance-windows-renumber
  19. appearance-windows-separator
  20. disable-mouse-wheel
  21. esc-no-delay
  22. f:get-modified-window-name
  23. f:get-tmux-pane-pwd-cached
  24. f:get-window-name-format
  25. f:get-window-style
  26. f:get-window-type
  27. f:main
  28. get-window-format-test
  29. get_window_format.bb
  30. global-variable-emacslike
  31. global-variables
  32. keybindings
  33. keybindings-copy-mode
  34. keybindings-copy-mode-enter
  35. keybindings-copy-mode-navigation
  36. keybindings-copy-mode-paste
  37. keybindings-copy-mode-selection
  38. keybindings-copy-mode-vi
  39. keybindings-ctrl-meta-punc
  40. keybindings-ctrl-punc
  41. keybindings-ctrl-shift-letters
  42. keybindings-insert-mode
  43. keybindings-meta-punc
  44. keybindings-meta-shift-letters
  45. keybindings-misc
  46. keybindings-mouse
  47. keybindings-normal-mode
  48. keybindings-normal-mode-enter
  49. keybindings-normal-mode-exit
  50. keybindings-pane-create
  51. keybindings-pane-move
  52. keybindings-pane-move-across-windows
  53. keybindings-pane-move-across-windows-new-window
  54. keybindings-pane-navigate
  55. keybindings-pane-next-layout
  56. keybindings-passthrough-mode
  57. keybindings-passthrough-mode-ctrl-meta-shift-letters
  58. keybindings-passthrough-mode-ctrl-shift-letters
  59. keybindings-passthrough-mode-other
  60. keybindings-reload-config
  61. keybindings-sessions
  62. keybindings-special-backspace
  63. keybindings-special-enter
  64. keybindings-special-m
  65. keybindings-special-space
  66. keybindings-special-tab
  67. keybindings-window-create
  68. keybindings-window-navigate
  69. misc
  70. mouse-mode
  71. t:get-modified-window-name
  72. t:get-window-name-format
  73. t:get-window-type
  74. tmux_get_status.sh
  75. tmux_get_window_format.sh
  76. tmux_move_pane.sh
  77. tmux_pane_pwd_cached.sh
  78. tmux_startup.sh