~kris/dots

srice

ref: 9f828eb14bdd54d2c4fd8a3b2c90253021df3152 srice/doc/tmux-mastery.md -rw-r--r-- 22.3 KiB
9f828eb1 — Kris Yotam mksh: backslash-escape commands in history hook to bypass module aliases (wc=tokei, tr=transmission-remote, cat=bat) 2 months ago

#tmux Mastery Reference

Power-user techniques from the tmux manual, mailing lists, and years of terminal living.


#Quick-Win Settings (Do These First)

# ~/.tmux.conf

set -g escape-time 0          # no delay on Escape (critical for Vim/Neovim)
set -g base-index 1           # windows start at 1, not 0
setw -g pane-base-index 1     # panes start at 1 too
set -g renumber-windows on    # close window 2, window 3 becomes 2 automatically
set -g focus-events on        # pass focus events to apps (needed by Neovim autoread)
set -g mouse on               # scroll, click panes, resize splits
set -g history-limit 50000    # bigger scrollback
set -g display-time 2000      # status messages stay visible 2s
set -g status-keys vi
setw -g mode-keys vi

Reload without restart:

tmux source-file ~/.tmux.conf

Or bind it:

bind R source-file ~/.tmux.conf \; display "Config reloaded"

#Floating Popups (display-popup)

display-popup spawns a floating terminal over the current pane. Available since tmux 3.2.

#Scratch Terminal Toggle

# Toggle a scratch terminal with prefix-g
bind g display-popup -E -w 80% -h 80% -d "#{pane_current_path}" \
  "tmux new-session -A -s scratch"

-A means attach if session exists, create if not. -d inherits the current directory.

#Lazygit Popup

bind G display-popup -E -w 95% -h 95% -d "#{pane_current_path}" lazygit

No persistent session needed: lazygit exits when you quit, popup disappears.

#fzf Session Switcher

bind s display-popup -E -w 60% -h 40% \
  "tmux list-sessions -F '#{session_name}' \
   | fzf --reverse --prompt='session: ' \
   | xargs -r tmux switch-client -t"

Replaces the default prefix-s tree view with a fuzzy picker.

#htop Popup

bind H display-popup -E -w 90% -h 85% htop

#Notes on display-popup Flags

Flag Meaning
-E close popup when command exits
-w width (pixels or %)
-h height (pixels or %)
-x x position (default: center)
-y y position (default: center)
-d working directory
-e set environment variable
-T popup border title

Close a popup from inside: Ctrl-c or just exit the shell.


#capture-pane

capture-pane dumps pane content to a buffer. It captures what is visible, or the full scrollback with -S.

#Extract Scrollback to File

tmux capture-pane -p -S -50000 > ~/scrollback.txt

-p prints to stdout. -S -50000 goes 50000 lines back (negative = from start of history).

#Capture Specific Pane

tmux capture-pane -p -t %3       # by pane id
tmux capture-pane -p -t 2.1      # window 2, pane 1

#Pipe Scrollback Through fzf to Open URLs

# Pull URLs from scrollback, pick one, open it
tmux capture-pane -p -S -5000 \
  | grep -oE 'https?://[^ ]+' \
  | sort -u \
  | fzf --reverse \
  | xargs -r xdg-open

Bind it:

bind u run-shell \
  "tmux capture-pane -p -S -5000 \
   | grep -oE 'https?://[^ ]+' \
   | sort -u \
   | fzf-tmux -p 80%,40% --reverse \
   | xargs -r xdg-open"

#Capture Without ANSI Escape Codes

tmux capture-pane -p -e > with-colors.txt    # -e includes escape sequences
tmux capture-pane -p    > plain.txt          # default strips them

To strip ANSI from captured output after the fact:

sed 's/\x1b\[[0-9;]*m//g' with-colors.txt

#pipe-pane

pipe-pane redirects a pane's output to a shell command. It stays running until toggled off.

#Toggle-able Logging

# Start logging
tmux pipe-pane -o 'cat >> ~/logs/tmux-pane-%Y%m%d.log'

# Stop logging (run same command again when -o is set)
tmux pipe-pane

-o means "toggle": if already piping, stop; if not, start.

Bind it:

bind P pipe-pane -o 'cat >> ~/logs/pane-#S-#W-#P-$(date +%F).log' \; \
  display "Logging toggled"

#S = session name, #W = window name, #P = pane index.

#Filter Errors Only

tmux pipe-pane 'grep --line-buffered -iE "error|warn|fatal" >> ~/logs/errors.log'

--line-buffered is critical: without it grep buffers output and you see nothing.

#Strip ANSI and Log

tmux pipe-pane "sed -u 's/\x1b\[[0-9;]*m//g' >> ~/logs/clean-$(date +%F).log"

-u (unbuffered) on sed keeps output flowing in real time.

#Pipe to System Logger

tmux pipe-pane 'logger -t tmux-pane'

All pane output lands in syslog under the tag tmux-pane.


#Custom Key Tables

tmux has three built-in tables: root (no prefix), prefix (after pressing prefix), and copy-mode-vi. You can create your own named tables for modal interaction.

# Enter resize mode with prefix-r, then use arrow keys to resize
bind r switch-client -T resize

bind -T resize Left  resize-pane -L 5
bind -T resize Right resize-pane -R 5
bind -T resize Up    resize-pane -U 5
bind -T resize Down  resize-pane -D 5

# Stay in resize mode after each keypress until you press Enter or Escape
bind -T resize Enter switch-client -T prefix
bind -T resize Escape switch-client -T prefix

Without repeating back to prefix you stay stuck. Add this to exit cleanly.

#Layout Mode

bind L switch-client -T layout

bind -T layout e select-layout even-horizontal
bind -T layout E select-layout even-vertical
bind -T layout m select-layout main-horizontal
bind -T layout M select-layout main-vertical
bind -T layout t select-layout tiled
bind -T layout Escape switch-client -T prefix

#Root Table Bindings (No Prefix)

Use sparingly -- root bindings fire on every keystroke without a prefix.

# Alt+h/j/k/l to navigate panes without prefix
bind -n M-h select-pane -L
bind -n M-j select-pane -D
bind -n M-k select-pane -U
bind -n M-l select-pane -R

# Alt+n / Alt+p for next/prev window
bind -n M-n next-window
bind -n M-p previous-window

-n is shorthand for -T root.

#Checking Key Tables

tmux list-keys -T resize
tmux list-keys -T root | grep -v '^bind -n Mouse'

#Hooks

Hooks run tmux commands automatically on events. Set with set-hook (session scope) or set-hook -g (global).

#after-select-pane

Fires after you switch to a pane. Useful for updating status or running per-pane commands.

set-hook -g after-select-pane "run-shell 'echo #{pane_current_command} > /tmp/tmux-active-cmd'"

#pane-focus-in / pane-focus-out

Requires focus-events on. These fire when a pane gains or loses focus.

# Dim inactive panes via pane border style
set-hook -g pane-focus-in  "set -g pane-active-border-style fg=colour208"
set-hook -g pane-focus-out "set -g pane-border-style fg=colour240"

A more dramatic version changes the pane background:

set-hook -g pane-focus-out  "select-pane -P bg=colour235"
set-hook -g pane-focus-in   "select-pane -P bg=default"

#session-created

Runs once when a new session starts. Use it to bootstrap an environment.

set-hook -g session-created "run-shell '~/.local/bin/tmux-session-init.sh #{session_name}'"
# ~/.local/bin/tmux-session-init.sh
#!/bin/sh
SESSION="$1"
case "$SESSION" in
  dev)   tmux send-keys -t "$SESSION" 'nvim' Enter ;;
  logs)  tmux send-keys -t "$SESSION" 'journalctl -f' Enter ;;
esac

#after-split-window

Fires after every split. Use it to ensure new panes inherit the current directory.

set-hook -g after-split-window "run-shell 'tmux send-keys -t #{pane_id} \"cd #{pane_current_path}\" Enter'"

Or simpler: tmux already respects -c "#{pane_current_path}" if you bind your splits that way:

bind '"' split-window -c "#{pane_current_path}"
bind % split-window -h -c "#{pane_current_path}"

#Listing Active Hooks

tmux show-hooks -g
tmux show-hooks          # session-local hooks

#send-keys

send-keys types text into any pane as if you typed it. This is how you automate tmux from scripts.

#Basic Usage

tmux send-keys -t mysession:1.1 "ls -la" Enter
tmux send-keys -t mysession:2   "htop" Enter     # targets pane 1 of window 2
tmux send-keys -t %3            "cd /tmp" Enter   # targets pane by id

#Automation Across All Panes in a Window

# Run a command in every pane of window 2
tmux list-panes -t mysession:2 -F "#{pane_id}" | while read pane; do
  tmux send-keys -t "$pane" "git pull" Enter
done

#Synchronized Panes

Turn on sync and every keystroke goes to all panes simultaneously:

bind S setw synchronize-panes \; \
  display "Sync: #{?pane_synchronized,ON,OFF}"

From the command line:

tmux setw synchronize-panes on
# ... type in any pane, all panes receive it ...
tmux setw synchronize-panes off

Use case: deploy the same command across 8 servers each in its own pane.

#Sending Special Keys

tmux send-keys -t %1 "q"                  # literal q
tmux send-keys -t %1 "C-c"               # Ctrl-c
tmux send-keys -t %1 "C-l"               # clear screen
tmux send-keys -t %1 "Escape"
tmux send-keys -t %1 ""                  # BEL

Named keys: Enter, Escape, Tab, BSpace, Up, Down, Left, Right, F1-F12, C-a through C-z.

#send-keys Without Running (No Enter)

tmux send-keys -t %2 "git commit -m 'fix'"
# leaves the text in the prompt, user hits Enter manually

#command-prompt

command-prompt opens an interactive prompt inside tmux for scripted user input.

#Prompted Session Creation

bind N command-prompt -p "New session name:" "new-session -s '%%'"

%% is the placeholder for what the user types.

#Prompted Window Rename

bind , command-prompt -I "#{window_name}" -p "Rename window:" "rename-window '%%'"

-I pre-fills the prompt with the current window name.

#join-pane Prompt

bind j command-prompt -p "Join pane from (window.pane):" "join-pane -s '%%'"

Lets you interactively pull any pane into the current window.

#move-window Prompt

bind m command-prompt -p "Move window to session:" "move-window -t '%%'"

#Multi-field Prompts

bind C command-prompt -p "Session name:","Window name:" \
  "new-session -s '%1' -n '%2'"

%1, %2 map to the first and second answers.


#Pure Shell Session Scripts (No tmuxinator)

tmuxinator is not needed. A 20-line shell script does the same thing with no Ruby dependency and no YAML.

#dev.sh -- Full Dev Environment

#!/bin/sh
# ~/.local/bin/dev-session.sh
# Usage: dev-session.sh [project-dir]

DIR="${1:-$HOME/dev/myproject}"
SESSION="dev"

tmux has-session -t "$SESSION" 2>/dev/null && tmux attach -t "$SESSION" && exit

tmux new-session -d -s "$SESSION" -c "$DIR" -x 220 -y 50

# Window 1: editor
tmux rename-window -t "$SESSION:1" "editor"
tmux send-keys -t "$SESSION:1" "nvim ." Enter

# Window 2: two-pane terminal
tmux new-window -t "$SESSION:2" -n "shell" -c "$DIR"
tmux split-window -t "$SESSION:2" -h -c "$DIR"

# Window 3: server
tmux new-window -t "$SESSION:3" -n "server" -c "$DIR"
tmux send-keys -t "$SESSION:3" "npm run dev" Enter

# Window 4: git
tmux new-window -t "$SESSION:4" -n "git" -c "$DIR"
tmux send-keys -t "$SESSION:4" "lazygit" Enter

tmux select-window -t "$SESSION:1"
tmux attach -t "$SESSION"

#Minimal Version for Any Project

#!/bin/sh
# Usage: t [name] [dir]
NAME="${1:-work}"
DIR="${2:-$(pwd)}"
tmux has-session -t "$NAME" 2>/dev/null \
  && tmux attach -t "$NAME" \
  || tmux new-session -A -s "$NAME" -c "$DIR"

Save as ~/bin/t, chmod +x. Type t anywhere to attach or create.

#Tear Down a Session Script

#!/bin/sh
# Kill all windows but confirm first
SESSION="${1:-dev}"
echo "Kill session '$SESSION'? [y/N]"
read -r ans
[ "$ans" = "y" ] && tmux kill-session -t "$SESSION"

#if-shell

if-shell runs a tmux command if a shell command exits 0, optionally with an else branch.

#macOS vs Linux

if-shell "uname | grep -q Darwin" {
  bind C-v run "pbpaste | tmux load-buffer - && tmux paste-buffer"
  bind C-c run "tmux save-buffer - | pbcopy"
} {
  bind C-v run "xclip -o | tmux load-buffer - && tmux paste-buffer"
  bind C-c run "tmux save-buffer - | xclip -i"
}

#SSH Detection

# If inside SSH, change status bar color to signal remote context
if-shell '[ -n "$SSH_CLIENT" ]' {
  set -g status-bg colour52
  set -g status-fg colour255
}

#Nested tmux Detection

# If running inside another tmux, use a different prefix
if-shell '[ -n "$TMUX" ]' {
  set -g prefix C-a
} {
  set -g prefix C-b
}

This lets you nest sessions: outer uses C-b, inner uses C-a.

#Load Local Overrides

# At the end of ~/.tmux.conf
if-shell "[ -f ~/.tmux.local.conf ]" "source ~/.tmux.local.conf"

Machine-specific overrides go in ~/.tmux.local.conf, never in the main config.

#Check tmux Version

# display-popup requires tmux >= 3.2
if-shell "tmux -V | awk '{exit ($2 < 3.2)}'" {
  bind g display-popup -E -w 80% -h 80% zsh
}

#OSC 52 Clipboard (Remote Copy Through SSH+tmux)

OSC 52 is a terminal escape sequence that instructs the local terminal emulator to set the clipboard. It works through SSH and through tmux, allowing remote processes to write to your local clipboard with no X11 forwarding.

#Requirements

  • Local terminal must support OSC 52: Alacritty, kitty, WezTerm, iTerm2, xterm (with allowWindowOps).
  • tmux must be told to pass the sequence through.

#tmux.conf Settings

set -g set-clipboard on           # tmux handles OSC 52 itself (tmux >= 3.2)
set -as terminal-features ",xterm-256color:clipboard"

If tmux is older or set-clipboard causes issues, use pass-through:

set -g set-clipboard off
set -as terminal-overrides ',*:Ms=\E]52;c;%p2%s\007'

#Copy Script (Portable)

#!/bin/sh
# ~/.local/bin/osc52copy
# Reads from stdin, sends OSC 52 sequence
buf=$(cat)
encoded=$(printf '%s' "$buf" | base64 | tr -d '\n')
printf '\033]52;c;%s\a' "$encoded"

Usage on a remote machine over SSH:

cat file.txt | osc52copy
echo "some text" | osc52copy

#tmux + OSC 52 Integration

With set-clipboard on, tmux intercepts the OSC 52 sequence and routes it to the local terminal. This means y in copy-mode-vi can copy to your local clipboard automatically:

set -g set-clipboard on
bind -T copy-mode-vi y send -X copy-selection-and-cancel

No xclip/xsel/pbcopy needed on the remote end.

#Verifying It Works

echo "test" | base64 | xargs -I{} printf '\033]52;c;{}\a'

Open your local clipboard manager -- "test" should appear.


#abduco + dvtm (Plan 9 Philosophy Alternative)

tmux is monolithic. The Plan 9 philosophy: one tool, one job. abduco handles session detach/attach. dvtm handles the multiplexer. They compose.

#Install

# Arch
pacman -S abduco dvtm

# Build from source (suckless.org)
git clone https://git.suckless.org/abduco
git clone https://git.suckless.org/dvtm

#Basic Usage

# Create a named session running dvtm
abduco -c mysession dvtm

# Detach: Ctrl-\
# Reattach
abduco -a mysession

# List sessions
abduco

#dvtm Keybindings (Mod = Ctrl-g by default)

Key Action
Mod-c create new window
Mod-x close window
Mod-j/k focus next/prev
Mod-Space cycle layouts
Mod-f fullscreen toggle
Mod-[1-9] switch to window n

#Why Use abduco+dvtm

  • dvtm follows suckless design: no config file, patch-based customization
  • abduco has a 200-line codebase; you can audit it in an afternoon
  • Separate concerns: swap dvtm for any other program (e.g., abduco -c foo bash)
  • Works in extremely constrained environments where tmux is unavailable

#dvtm Layout Modes

Layout Description
tiled all panes tiled
bstack one large pane, rest stacked at bottom
grid equal grid
fullscreen one pane, full
vertical two columns

#GNU screen Serial Console Trick

screen predates tmux by 15 years and has one feature tmux lacks: direct serial port access.

#Connect to a Serial Device

screen /dev/ttyUSB0 115200
screen /dev/ttyS0 9600
screen /dev/ttyACM0 115200    # Arduino, etc.

First argument is the device, second is baud rate. No additional software needed.

#Common Baud Rates

Device Type Baud Rate
Arduino 9600 or 115200
Raspberry Pi UART 115200
Cisco console 9600
Network switches (varies) 9600 or 115200
Embedded Linux 115200

#screen Commands in Serial Session

Key Action
Ctrl-a k kill session (disconnect)
Ctrl-a d detach (session keeps running)
Ctrl-a H toggle logging to screenlog.0
Ctrl-a [ enter copy/scroll mode
Ctrl-a : command prompt

#Log the Entire Session

screen -L /dev/ttyUSB0 115200
# creates screenlog.0 in current directory

Or toggle mid-session: Ctrl-a H.

#Permissions

If you get "Permission denied" on the device:

sudo usermod -aG dialout $USER   # Debian/Ubuntu
sudo usermod -aG uucp $USER      # Arch
# log out and back in, or:
sudo chmod a+rw /dev/ttyUSB0     # temporary

#Why Not tmux for Serial

tmux has no serial device support. screen is the standard tool for this. Keep both installed.


#capture-pane Advanced: Structured Extraction

#Count Lines Matching a Pattern in Scrollback

tmux capture-pane -p -S -10000 | grep -c "ERROR"

#Watch for a String in Any Pane

# Poll until "READY" appears in pane %3
while ! tmux capture-pane -p -t %3 | grep -q "READY"; do
  sleep 0.5
done
echo "Pane is ready"

Useful in CI scripts or shell scripts that drive a long-running process in another pane.

#Diff Two Pane States

tmux capture-pane -p -t %1 > /tmp/pane1-before.txt
# ... wait ...
tmux capture-pane -p -t %1 > /tmp/pane1-after.txt
diff /tmp/pane1-before.txt /tmp/pane1-after.txt

#Formats and Variables

tmux exposes state through format strings. Use them in config, bindings, and scripts.

tmux display-message -p "#{session_name}"
tmux display-message -p "#{pane_current_path}"
tmux display-message -p "#{pane_current_command}"
tmux display-message -p "#{window_index} #{window_name}"

Useful variables:

Variable Value
#{session_name} current session name
#{window_index} current window number
#{pane_id} pane unique id (e.g. %3)
#{pane_current_path} cwd of pane
#{pane_current_command} foreground process
#{pane_width} / #{pane_height} dimensions
#{client_termname} terminal type
#{host} hostname
#{cursor_x} / #{cursor_y} cursor position

Conditionals in formats:

# Show "SSH" in status if inside an SSH session
set -g status-right "#{?SSH_CLIENT,SSH,local} | %H:%M"

#Practical Workflow: Multi-Server Deployment

#!/bin/sh
# deploy-all.sh -- run a command across multiple servers in parallel panes

SERVERS="web1 web2 web3 db1"
CMD="sudo systemctl restart app"
SESSION="deploy-$(date +%s)"

tmux new-session -d -s "$SESSION" -n "servers"

first=1
for host in $SERVERS; do
  if [ "$first" = "1" ]; then
    tmux send-keys -t "$SESSION:1" "ssh $host '$CMD'" Enter
    first=0
  else
    tmux split-window -t "$SESSION:1" -h "ssh $host '$CMD'"
    tmux select-layout -t "$SESSION:1" tiled
  fi
done

tmux setw -t "$SESSION:1" synchronize-panes on
tmux attach -t "$SESSION"

All servers run the command simultaneously. You watch all output at once.


#Ergonomics

#Smarter Splits

# Always split relative to current directory
bind '"' split-window -v -c "#{pane_current_path}"
bind % split-window -h -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"

#Swap Windows

bind -r < swap-window -d -t -1
bind -r > swap-window -d -t +1

-r makes the binding repeatable: hold prefix and tap < multiple times to move a window left.

#Break Pane to New Window

bind b break-pane -d

Pulls the current pane out into its own window without switching to it (-d).

#Jump to Last Window

bind Tab last-window

#Zoom Toggle

prefix-z is built in. Add a visual indicator to status:

set -g window-status-current-format "#{?window_zoomed_flag,[Z] ,}#I:#W"

#Nested Sessions

When SSH-ing into a remote machine that has tmux, you end up with two tmux layers. Handle cleanly:

# Outer tmux: prefix is C-b
# Inner tmux: prefix is C-a (set via if-shell or hardcoded)

# To send prefix to inner tmux: press outer prefix twice
# e.g., press C-b C-b to send C-b to the remote session

# Or bind a passthrough key
bind -n C-o send-prefix   # outer: C-o passes next key to inner

The common pattern: outer machine uses C-b, remote (SSH) sessions use C-a. Different prefixes eliminate ambiguity.


#Tmux + fzf Integration Points

Task Command
Switch session tmux ls -F '#S' | fzf | xargs tmux switch-client -t
Kill session tmux ls -F '#S' | fzf | xargs tmux kill-session -t
Switch window tmux list-windows -F '#I #W' | fzf | awk '{print $1}' | xargs tmux select-window -t
Select pane tmux list-panes -a -F '#S:#I.#P #{pane_current_command}' | fzf | cut -d' ' -f1 | xargs tmux switch-client -t
Open recent dir tmux capture-pane -p -S -3000 | grep -oE '/[^ ]+' | sort -u | fzf | xargs tmux send-keys -t $TMUX_PANE " cd " Enter

#Status Bar One-Liners

# Left: session name in bold
set -g status-left "#[bold]#S#[default] "

# Right: date, time, hostname
set -g status-right "%Y-%m-%d %H:%M #H"

# Show CPU load (requires external command)
set -g status-right "#(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}') | %H:%M"

# Window list: show activity flag
set -g window-status-activity-style "fg=colour208,bold"
setw -g monitor-activity on
set -g visual-activity off    # no message, just style change

#Clipboard Integration Summary

Platform Tool tmux command
Linux (X11) xclip run "tmux save-buffer - | xclip -i -selection clipboard"
Linux (Wayland) wl-copy run "tmux save-buffer - | wl-copy"
macOS pbcopy run "tmux save-buffer - | pbcopy"
SSH remote OSC 52 set -g set-clipboard on

Bind copy in copy-mode-vi to use the right tool:

if-shell "uname | grep -q Darwin" {
  bind -T copy-mode-vi y send -X copy-pipe-and-cancel "pbcopy"
} {
  bind -T copy-mode-vi y send -X copy-pipe-and-cancel "xclip -i -selection clipboard"
}