# tmux Mastery Reference Power-user techniques from the tmux manual, mailing lists, and years of terminal living. --- ## Quick-Win Settings (Do These First) ```tmux # ~/.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: ```bash tmux source-file ~/.tmux.conf ``` Or bind it: ```tmux 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 ```tmux # 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 ```tmux 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 ```tmux 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 ```tmux 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 ```bash 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 ```bash 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 ```bash # 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: ```tmux 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 ```bash 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: ```bash 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 ```bash # 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: ```tmux 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 ```bash 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 ```bash 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 ```bash 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. ### Modal Resize Mode ```tmux # 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 ```tmux 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. ```tmux # 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 ```bash 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. ```tmux 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. ```tmux # 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: ```tmux 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. ```tmux set-hook -g session-created "run-shell '~/.local/bin/tmux-session-init.sh #{session_name}'" ``` ```bash # ~/.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. ```tmux 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: ```tmux bind '"' split-window -c "#{pane_current_path}" bind % split-window -h -c "#{pane_current_path}" ``` ### Listing Active Hooks ```bash 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 ```bash 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 ```bash # 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: ```tmux bind S setw synchronize-panes \; \ display "Sync: #{?pane_synchronized,ON,OFF}" ``` From the command line: ```bash 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 ```bash 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) ```bash 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 ```tmux bind N command-prompt -p "New session name:" "new-session -s '%%'" ``` `%%` is the placeholder for what the user types. ### Prompted Window Rename ```tmux bind , command-prompt -I "#{window_name}" -p "Rename window:" "rename-window '%%'" ``` `-I` pre-fills the prompt with the current window name. ### join-pane Prompt ```tmux 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 ```tmux bind m command-prompt -p "Move window to session:" "move-window -t '%%'" ``` ### Multi-field Prompts ```tmux 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 ```bash #!/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 ```bash #!/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 ```bash #!/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 ```tmux 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 ```tmux # 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 ```tmux # 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 ```tmux # 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 ```tmux # 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 ```tmux 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: ```tmux set -g set-clipboard off set -as terminal-overrides ',*:Ms=\E]52;c;%p2%s\007' ``` ### Copy Script (Portable) ```bash #!/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: ```bash 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: ```tmux 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 ```bash 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 ```bash # 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 ```bash # 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 ```bash 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 ```bash 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: ```bash 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 ```bash tmux capture-pane -p -S -10000 | grep -c "ERROR" ``` ### Watch for a String in Any Pane ```bash # 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 ```bash 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. ```bash 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: ```tmux # Show "SSH" in status if inside an SSH session set -g status-right "#{?SSH_CLIENT,SSH,local} | %H:%M" ``` --- ## Practical Workflow: Multi-Server Deployment ```bash #!/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 ```tmux # 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 ```tmux 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 ```tmux bind b break-pane -d ``` Pulls the current pane out into its own window without switching to it (`-d`). ### Jump to Last Window ```tmux bind Tab last-window ``` ### Zoom Toggle `prefix-z` is built in. Add a visual indicator to status: ```tmux 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: ```tmux # 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 ```tmux # 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: ```tmux 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" } ```