tmux
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".
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.
EMACSLIKE="#{m/r:emacs.*|git|tig,#{window_name}}"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):
| Oct | Dec | Hex | Char | Oct | Dec | Hex | Char |
|---|---|---|---|---|---|---|---|
| 000 | 0 | 00 | NUL \0 (null character) | 100 | 64 | 40 | @ |
| 001 | 1 | 01 | SOH (start of heading) | 101 | 65 | 41 | A |
| 002 | 2 | 02 | STX (start of text) | 102 | 66 | 42 | B |
| 003 | 3 | 03 | ETX (end of text) | 103 | 67 | 43 | C |
| 004 | 4 | 04 | EOT (end of transmission) | 104 | 68 | 44 | D |
| 005 | 5 | 05 | ENQ (enquiry) | 105 | 69 | 45 | E |
| 006 | 6 | 06 | ACK (acknowledge) | 106 | 70 | 46 | F |
| 007 | 7 | 07 | BEL \a (bell) | 107 | 71 | 47 | G |
| 010 | 8 | 08 | BS \b (backspace) | 110 | 72 | 48 | H |
| 011 | 9 | 09 | HT \t (horizontal tab) | 111 | 73 | 49 | I |
| 012 | 10 | 0A | LF \n (new line) | 112 | 74 | 4A | J |
| 013 | 11 | 0B | VT \v (vertical tab) | 113 | 75 | 4B | K |
| 014 | 12 | 0C | FF \f (form feed) | 114 | 76 | 4C | L |
| 015 | 13 | 0D | CR \r (carriage ret) | 115 | 77 | 4D | M |
| 016 | 14 | 0E | SO (shift out) | 116 | 78 | 4E | N |
| 017 | 15 | 0F | SI (shift in) | 117 | 79 | 4F | O |
| 020 | 16 | 10 | DLE (data link escape) | 120 | 80 | 50 | P |
| 021 | 17 | 11 | DC1 (device control 1) | 121 | 81 | 51 | Q |
| 022 | 18 | 12 | DC2 (device control 2) | 122 | 82 | 52 | R |
| 023 | 19 | 13 | DC3 (device control 3) | 123 | 83 | 53 | S |
| 024 | 20 | 14 | DC4 (device control 4) | 124 | 84 | 54 | T |
| 025 | 21 | 15 | NAK (negative ack.) | 125 | 85 | 55 | U |
| 026 | 22 | 16 | SYN (synchronous idle) | 126 | 86 | 56 | V |
| 027 | 23 | 17 | ETB (end of trans. blk) | 127 | 87 | 57 | W |
| 030 | 24 | 18 | CAN (cancel) | 130 | 88 | 58 | X |
| 031 | 25 | 19 | EM (end of medium) | 131 | 89 | 59 | Y |
| 032 | 26 | 1A | SUB (substitute) | 132 | 90 | 5A | Z |
| 033 | 27 | 1B | ESC (escape) | 133 | 91 | 5B | [ |
| 034 | 28 | 1C | FS (file separator) | 134 | 92 | 5C | \ |
| 035 | 29 | 1D | GS (group separator) | 135 | 93 | 5D | ] |
| 036 | 30 | 1E | RS (record separator) | 136 | 94 | 5E | ^ |
| 037 | 31 | 1F | US (unit separator) | 137 | 95 | 5F | _ |
| 040 | 32 | 20 | SPACE | 140 | 96 | 60 | ` |
| 041 | 33 | 21 | ! | 141 | 97 | 61 | a |
| 042 | 34 | 22 | " | 142 | 98 | 62 | b |
| 043 | 35 | 23 | # | 143 | 99 | 63 | c |
| 044 | 36 | 24 | $ | 144 | 100 | 64 | d |
| 045 | 37 | 25 | % | 145 | 101 | 65 | e |
| 046 | 38 | 26 | & | 146 | 102 | 66 | f |
| 047 | 39 | 27 | ' | 147 | 103 | 67 | g |
| 050 | 40 | 28 | ( | 150 | 104 | 68 | h |
| 051 | 41 | 29 | ) | 151 | 105 | 69 | i |
| 052 | 42 | 2A | * | 152 | 106 | 6A | j |
| 053 | 43 | 2B | + | 153 | 107 | 6B | k |
| 054 | 44 | 2C | , | 154 | 108 | 6C | l |
| 055 | 45 | 2D | - | 155 | 109 | 6D | m |
| 056 | 46 | 2E | . | 156 | 110 | 6E | n |
| 057 | 47 | 2F | / | 157 | 111 | 6F | o |
| 060 | 48 | 30 | 0 | 160 | 112 | 70 | p |
| 061 | 49 | 31 | 1 | 161 | 113 | 71 | q |
| 062 | 50 | 32 | 2 | 162 | 114 | 72 | r |
| 063 | 51 | 33 | 3 | 163 | 115 | 73 | s |
| 064 | 52 | 34 | 4 | 164 | 116 | 74 | t |
| 065 | 53 | 35 | 5 | 165 | 117 | 75 | u |
| 066 | 54 | 36 | 6 | 166 | 118 | 76 | v |
| 067 | 55 | 37 | 7 | 167 | 119 | 77 | w |
| 070 | 56 | 38 | 8 | 170 | 120 | 78 | x |
| 071 | 57 | 39 | 9 | 171 | 121 | 79 | y |
| 072 | 58 | 3A | : | 172 | 122 | 7A | z |
| 073 | 59 | 3B | ; | 173 | 123 | 7B | { |
| 074 | 60 | 3C | < | 174 | 124 | 7C | | |
| 075 | 61 | 3D | = | 175 | 125 | 7D | } |
| 076 | 62 | 3E | > | 176 | 126 | 7E | ~ |
| 077 | 63 | 3F | ? | 177 | 127 | 7F | DEL |
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:
| tmux key-table | Vi-style mode | Status bar symbol | Sticky? |
|---|---|---|---|
| root | insert | <I> | yes |
| prefix | normal | <N> | no |
| copy-mode | visual | <V> | yes |
| Passthrough mode | N/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)
keybindings-sessions
keybindings-window-create
keybindings-window-navigate
keybindings-pane-create
keybindings-pane-navigate
keybindings-pane-move
keybindings-pane-move-across-windows
keybindings-pane-move-across-windows-new-window
keybindings-pane-next-layout
keybindings-meta-shift-letters
keybindings-ctrl-shift-letters
keybindings-ctrl-punc
keybindings-meta-punc
keybindings-ctrl-meta-punc
keybindings-special-tab
keybindings-special-enter
keybindings-special-m
keybindings-special-backspace
keybindings-special-space3.3.1.2 Windows
Create a new window (with a single pane) to the right (and spawn a new shell in the pane there).
bind-key -T root C-M-o new-window -a -c "#{pane_current_path}"Navigate across windows.
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.
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.
Move the current pane up/down inside the current window.
bind-key -T root C-M-S-j swap-pane -Z -D
bind-key -T root C-M-S-k swap-pane -Z -UMove 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.
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"'# 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.
bind-key -T root C-M-S-o break-pane -a -t : \; set key-table rootChange 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).
bind-key -T root C-M-m next-layout3.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).
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.
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 root3.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).
set-hook -g client-detached[0] "set key-table root"3.4.3 Reload configuration
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.
3.5.1 Enter copy-mode
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.
set -gw mode-keys vi3.5.3 Selection and copying
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-selection3.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).
bind-key -T root C-M-p paste-buffer3.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.
set -gw mouse on3.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.
unbind-key -T root WheelUpStatus
unbind-key -T root WheelDownStatus3.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.
set -s escape-time 03.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
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
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
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
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
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.1 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)
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.
bind-key -T root C-m if-shell -F $EMACSLIKE "send-keys Escape '[109;5u'" "send-keys -H 0d"3.8.6.3 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).
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.
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-other3.9.1 Control+Shift and 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
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
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.
set -g default-terminal "wezterm"
appearance-messages
appearance-status-bar
appearance-windows
appearance-panes-borders
appearance-copy-mode4.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.
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).
set -g renumber-windows onRename 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.
set -g status-interval 1We have windows listed in the status bar. Separate them with the space character.
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.
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.
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).
set -g focus-events onWhen 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:
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳set -gw fill-character "╳"Automatically re-balance panes after they are created and/or deleted.
set-hook -g after-split-window "select-layout -E"
set-hook -g pane-exited "select-layout -E"Set colors for pane split borders.
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.
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.
set -gw mode-style "fg=#343c48 bg=brightgreen bold"5 Miscellaneous
# 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 on6.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.
(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:main6.1.1.1 Window type
There are 4 types of windows based on what the currently active pane inside that window is doing:
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),
ssh (typically a remote (nested) tmux session),
zsh (our interactive shell of choice), and
other (where the window is running some other command other than ssh or zsh).
(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
(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.
(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.
(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
(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.
(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:
- pane-current-command
- 🔗 the command that's currently running in the pane
- pane-current-path
- 🔗 the current working directory (aka $PWD in shell) of the above command
- window-name
- 🔗 the name of the window; by default this should match the pane-current-command but we'll overwrite it with the output of get-window-name-format
- window-id
- 🔗 the unique ID of the window (e.g. @1)
- pane-id
- 🔗 the unique ID of the pane (e.g. %2)
- window-flag
- 🔗 a decorative string character that tmux uses to denote things like whether the window is the current one (*) or if was the previously-active one (-)
- window-panes
- 🔗 the number of panes a window has (at least 1)
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).
(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.
(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:
pane-current-command
pane-current-path
window-name
window-id
pane-id
window-flags
window-panes
is-current-window
(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
(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-format6.2 Historical (original) Bash script
Below is the original shell script which the Babashka version get_window_format.bb replaces.
# 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.1 Status bar (key table)
# 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
# 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
# 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)
Named cells (78)
- Footnote (fn:csi-u)
- Footnote (fn:modal-tmux)
- Footnote (fn:split-naming-convention)
- appearance
- appearance-copy-mode
- appearance-messages
- appearance-panes-borders
- appearance-panes-borders-active
- appearance-panes-borders-balance
- appearance-panes-borders-bg-fill
- appearance-panes-borders-focus
- appearance-panes-borders-splits
- appearance-status-bar
- appearance-windows
- appearance-windows-automatic-active-inactive
- appearance-windows-automatic-naming
- appearance-windows-naming-interval
- appearance-windows-renumber
- appearance-windows-separator
- disable-mouse-wheel
- esc-no-delay
- f:get-modified-window-name
- f:get-tmux-pane-pwd-cached
- f:get-window-name-format
- f:get-window-style
- f:get-window-type
- f:main
- get-window-format-test
- get_window_format.bb
- global-variable-emacslike
- global-variables
- keybindings
- keybindings-copy-mode
- keybindings-copy-mode-enter
- keybindings-copy-mode-navigation
- keybindings-copy-mode-paste
- keybindings-copy-mode-selection
- keybindings-copy-mode-vi
- keybindings-ctrl-meta-punc
- keybindings-ctrl-punc
- keybindings-ctrl-shift-letters
- keybindings-insert-mode
- keybindings-meta-punc
- keybindings-meta-shift-letters
- keybindings-misc
- keybindings-mouse
- keybindings-normal-mode
- keybindings-normal-mode-enter
- keybindings-normal-mode-exit
- keybindings-pane-create
- keybindings-pane-move
- keybindings-pane-move-across-windows
- keybindings-pane-move-across-windows-new-window
- keybindings-pane-navigate
- keybindings-pane-next-layout
- keybindings-passthrough-mode
- keybindings-passthrough-mode-ctrl-meta-shift-letters
- keybindings-passthrough-mode-ctrl-shift-letters
- keybindings-passthrough-mode-other
- keybindings-reload-config
- keybindings-sessions
- keybindings-special-backspace
- keybindings-special-enter
- keybindings-special-m
- keybindings-special-space
- keybindings-special-tab
- keybindings-window-create
- keybindings-window-navigate
- misc
- mouse-mode
- t:get-modified-window-name
- t:get-window-name-format
- t:get-window-type
- tmux_get_status.sh
- tmux_get_window_format.sh
- tmux_move_pane.sh
- tmux_pane_pwd_cached.sh
- tmux_startup.sh