Many concurrent sessions with many panes each make it hard to track what is running where, and moving a pane between windows means join-pane with a hand-typed target, so it rarely gets done. tmux-fzf gives a fuzzy-searchable list of every session, window and pane with a live preview of each, and implements break/join/swap as pane actions -- which is the pane-movement gap. fzf is already in apt.global, arch.global and brew.global, so no new packages are needed. Checked out as a submodule rather than left for TPM to clone on first prefix+I: ~/.tmux symlinks to config/tmux, so a TPM-installed plugin lands inside this repo as untracked files. The submodule pins a version and installs via the existing "git submodule update --init". TPM still sources it from the @plugin line and can still update it. prefix+F is the plugin's own top-level menu. prefix+P/W/S skip straight to the pane, window and session panels. TMUX_FZF_CLIENT pins each action to the client that opened it, as the plugin's own binding does -- run-shell has no client context, so tmux otherwise targets the most recently active client, which is wrong when a second client is attached to the same session. No env-var configuration was needed: preview is already on by default, and the TMUX_FZF_* variables are read from the environment only, so the README's suggestion to set them in .tmux.conf is not valid tmux syntax. Tested against tmux 3.5a and fzf 0.38.0: config parses with no errors, all three bindings register, existing bindings are unaffected, TPM sources the plugin and binds prefix+F, the three scripts pass bash -n, and the plugin's fzf-version detection resolves to fzf-tmux with preview enabled. The interactive picker itself needs a real terminal and is left to manual check.
186 lines
9.8 KiB
Bash
186 lines
9.8 KiB
Bash
# ----------------------------------------------------------------------------
|
|
# Prefix
|
|
# ----------------------------------------------------------------------------
|
|
set -g prefix C-a
|
|
unbind C-b
|
|
bind C-a send-prefix
|
|
bind Tab last-window
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# General behavior
|
|
# ----------------------------------------------------------------------------
|
|
set-option -g set-titles on
|
|
set -g default-terminal "screen-256color"
|
|
set -g history-limit 10000
|
|
set -sg escape-time 1
|
|
set-option -g base-index 1
|
|
setw -g pane-base-index 1
|
|
setw -g aggressive-resize on
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Windows and panes
|
|
# ----------------------------------------------------------------------------
|
|
bind T swap-window -t 1
|
|
bind | split-window -h -c "#{pane_current_path}"
|
|
bind - split-window -v -c "#{pane_current_path}"
|
|
bind c new-window -c "#{pane_current_path}"
|
|
|
|
bind h select-pane -L
|
|
bind j select-pane -D
|
|
bind k select-pane -U
|
|
bind l select-pane -R
|
|
|
|
# Smart pane switching with awareness of vim splits
|
|
is_vim='echo "#{pane_current_command}" | grep -iqE "(^|\/)g?(view|n?vim?)(diff)?$"'
|
|
# Claude Code also wants its own use of C-j (insert newline), same as vim
|
|
# wants C-h/j/k/l for its own window navigation -- forward it raw instead of
|
|
# letting tmux swallow it for pane-switching.
|
|
is_vim_or_claude='echo "#{pane_current_command}" | grep -iqE "(^|\/)(g?(view|n?vim?)(diff)?|claude)$"'
|
|
bind -n C-h if-shell "$is_vim" "send-keys C-h" "select-pane -L"
|
|
bind -n C-j if-shell "$is_vim_or_claude" "send-keys C-j" "select-pane -D"
|
|
bind -n C-k if-shell "$is_vim" "send-keys C-k" "select-pane -U"
|
|
bind -n C-l if-shell "$is_vim" "send-keys C-l" "select-pane -R"
|
|
bind -n 'C-\' if-shell "$is_vim" "send-keys 'C-\\'" "select-pane -l"
|
|
|
|
bind -r C-h select-window -t :-
|
|
bind -r C-l select-window -t :+
|
|
|
|
bind a setw synchronize-panes
|
|
|
|
# Fuzzy pickers over every session/window/pane, each with a live preview of
|
|
# what it's running (tmux-fzf). prefix+F opens the plugin's own top-level
|
|
# menu; these skip straight to the panels used most -- pane.sh is where
|
|
# break/join/swap live, i.e. moving a pane into another window without
|
|
# hand-typing a target the way join-pane needs.
|
|
#
|
|
# TMUX_FZF_CLIENT pins the action to the client that opened it, exactly as
|
|
# the plugin's own binding does: a command run via run-shell has no client
|
|
# context of its own, so tmux otherwise falls back to the most recently
|
|
# active client -- the wrong terminal whenever a second client (e.g. a
|
|
# browser one over ttyd) is attached to the same session.
|
|
bind P run-shell -b "TMUX_FZF_CLIENT='#{client_tty}' ~/.tmux/plugins/tmux-fzf/scripts/pane.sh"
|
|
bind W run-shell -b "TMUX_FZF_CLIENT='#{client_tty}' ~/.tmux/plugins/tmux-fzf/scripts/window.sh"
|
|
bind S run-shell -b "TMUX_FZF_CLIENT='#{client_tty}' ~/.tmux/plugins/tmux-fzf/scripts/session.sh"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Copy mode and mouse
|
|
# ----------------------------------------------------------------------------
|
|
setw -g mode-keys vi
|
|
set-option -g -q mouse on
|
|
|
|
bind y run 'tmux save-buffer - | $HOME/dotfiles/bin/clipboard-copy'
|
|
bind C-y run 'tmux save-buffer - | $HOME/dotfiles/bin/clipboard-copy'
|
|
|
|
# Let escape sequences a pane explicitly DCS-wraps for passthrough (e.g.
|
|
# sixel image data, see the iTerm2 terminal-features line below) reach the
|
|
# real terminal, even when nested behind ssh/tmux layers. Not used for the
|
|
# clipboard -- that has its own relay via the pane-set-clipboard hook above.
|
|
# allow-passthrough was added in tmux 3.3; older tmux doesn't understand the
|
|
# option, so only set it when the running tmux is new enough.
|
|
if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 >= 3003) exit 0; else exit 1}'" 'set -g allow-passthrough on'
|
|
|
|
# Let tmux tell Shift+Enter apart from plain Enter and forward that
|
|
# distinction inward, so Claude Code's Shift+Enter-inserts-newline shortcut
|
|
# works. extended-keys/terminal-features need tmux >= 3.2. The terminal
|
|
# name is matched with '*' rather than the commonly-suggested 'xterm*',
|
|
# because a nested tmux's "terminal" is the outer tmux, which reports
|
|
# itself using default-terminal above (screen-256color) -- 'xterm*' would
|
|
# never match that, breaking the nested case this is meant to support.
|
|
#
|
|
# Investigated as a suspect for multi-line paste getting corrupted with
|
|
# literal CSI 27;<mod>;<key>~ garbage in place of newlines (see Claude Code
|
|
# issues #43169 and #30239) -- turning this off did not fix that paste
|
|
# corruption, so it's an upstream Claude Code paste-tokenizer bug, not
|
|
# something caused by this setting. Restored.
|
|
if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 >= 3002) exit 0; else exit 1}'" 'set -s extended-keys on; set -as terminal-features "*:extkeys"'
|
|
|
|
# Accept pane-originated OSC 52 clipboard-set attempts into a real tmux
|
|
# buffer. Only on-vs-off matters here -- off would refuse to even look at
|
|
# these escapes; "on" (rather than "external") is needed so the pane-set-
|
|
# clipboard hook below has an actual buffer to read.
|
|
set -g set-clipboard on
|
|
|
|
# tmux's own mechanism for forwarding an accepted clipboard-set (or its own
|
|
# copy-mode copies) to the real terminal is generating a fresh OSC 52 write
|
|
# via the "Ms" terminfo capability -- confirmed unreliable here (both
|
|
# pane-forwarded and tmux-generated OSC 52 silently failed to reach the
|
|
# clipboard even with Ms correctly registered against Alacritty). Relay it
|
|
# ourselves instead: whenever any pane sets the clipboard, grab what was
|
|
# just captured above and write it straight to this client's own terminal
|
|
# device. If that's a real terminal, this is the final hop and we're done.
|
|
# If it's actually another tmux one layer further out (e.g. this tmux is
|
|
# itself running inside an outer tmux over ssh), that data becomes pane
|
|
# output there, and the *same* hook -- present there too, since it's the
|
|
# same dotfiles -- fires again. That recurses cleanly through however many
|
|
# tmux layers are actually in the chain, without any of them needing to
|
|
# know the total count in advance.
|
|
set-hook -g pane-set-clipboard 'run-shell "tmux save-buffer - | $HOME/dotfiles/bin/tmux-clipboard-relay #{client_tty}"'
|
|
|
|
# The default/no-argument form of copy-pipe (used by double/triple-click and
|
|
# a couple of default bindings) pipes to this option instead of a literal
|
|
# command -- route it through the same working script as the explicit
|
|
# copy-pipe bindings in tmux/modern.conf.
|
|
set -g copy-command '$HOME/dotfiles/bin/clipboard-copy'
|
|
|
|
# iTerm2 supports SIXEL images (since 3.3.0) but doesn't set a distinct TERM
|
|
# to identify itself, so this matches on its usual "xterm-256color" report --
|
|
# any other client reporting that same TERM will also be treated as
|
|
# sixel-capable. Alacritty is deliberately left off: upstream Alacritty has
|
|
# no sixel support, so telling tmux otherwise would replace today's clean
|
|
# text placeholder with garbled escape data.
|
|
set -as terminal-features ",xterm-256color:sixel"
|
|
|
|
# tmux renamed the copy-mode key table (vi-copy -> copy-mode-vi) and changed
|
|
# bind-key's table flag (-t -> -T) in 2.4. Source the file matching the
|
|
# running tmux so old tmux never parses syntax it doesn't understand.
|
|
if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 < 2004) exit 0; else exit 1}'" 'source-file ~/.tmux/legacy.conf' 'source-file ~/.tmux/modern.conf'
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Misc bindings
|
|
# ----------------------------------------------------------------------------
|
|
bind-key C run-shell "cal $(date +%Y)"
|
|
bind r source-file ~/.tmux.conf \; display "Reloaded config"
|
|
|
|
# A pane whose connection died mid-redraw (e.g. an SSH session killed by the
|
|
# laptop sleeping) can get stuck with tmux thinking the cursor is hidden,
|
|
# since that's tracked from the pane's own output and nothing ever sent the
|
|
# matching show-cursor sequence. Re-run it in the active pane to fix it.
|
|
bind R send-keys "printf '\\033[?25h'" Enter
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Powerline (guarded so machines without it don't error on load)
|
|
# ----------------------------------------------------------------------------
|
|
run-shell "command -v powerline-daemon >/dev/null 2>&1 && powerline-daemon -q; true"
|
|
run-shell "command -v powerline-config >/dev/null 2>&1 && powerline-config tmux setup; true"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Status bar
|
|
# ----------------------------------------------------------------------------
|
|
# Flash the status bar when this client's prefix key is armed, so nested
|
|
# tmux sessions (e.g. laptop -> ssh -> devbox tmux) make it obvious which
|
|
# layer is about to receive the next keystroke. Falls back to whatever
|
|
# status-style powerline set above (captured here) the rest of the time.
|
|
# Colors match gruvbox's own visual/selection convention (bg0 on orange).
|
|
set -g @status_style_armed "bg=#fe8019,fg=#282828"
|
|
run-shell 'tmux set-option -g @status_style_normal "$(tmux show-options -gv status-style)"'
|
|
set -g status-style "#{?client_prefix,#{@status_style_armed},#{@status_style_normal}}"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Plugins (TPM)
|
|
# ----------------------------------------------------------------------------
|
|
set -g @plugin 'tmux-plugins/tpm'
|
|
set -g @plugin 'tmux-plugins/tmux-sensible'
|
|
# Checked out as a submodule under config/tmux/plugins rather than left for
|
|
# TPM to clone on first prefix+I: ~/.tmux is a symlink to that directory, so
|
|
# a TPM-installed plugin lands inside this repo as untracked files. The
|
|
# submodule pins a version, installs with the existing "git submodule
|
|
# update --init" in install.sh, and keeps git status clean. TPM still sources
|
|
# it from the @plugin line below and can still update it.
|
|
set -g @plugin 'sainnhe/tmux-fzf'
|
|
|
|
# Local overrides
|
|
if-shell "[ -f ~/.tmux_local.conf ]" 'source ~/.tmux_local.conf'
|
|
|
|
# Initialize TPM (must be the last line)
|
|
run '~/.tmux/plugins/tpm/tpm'
|