diff --git a/Brewfile b/Brewfile index e278eda..8b91dad 100644 --- a/Brewfile +++ b/Brewfile @@ -6,6 +6,7 @@ brew 'bash' brew 'macvim' brew 'neovim' brew 'the_silver_searcher' +brew 'tmux' brew 'zsh' brew 'zsh-autosuggestions' brew 'zsh-syntax-highlighting' diff --git a/README.md b/README.md index 4422b42..5e17622 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # dotfiles Personal macOS development environment managed with [Homesick][homesick_link]. Configures Zsh (with -[Oh My Zsh][oh_my_zsh_link]), Vim via [Vundle][vundle_link], Ruby development tools via +[Oh My Zsh][oh_my_zsh_link]), Vim via [Vundle][vundle_link], tmux, Ruby development tools via [rbenv][rbenv_link], and a curated set of [Homebrew][homebrew_link] packages for web development. Uses [mise][mise_link] for managing non-Ruby tool versions. diff --git a/home/.tmux.conf b/home/.tmux.conf new file mode 100644 index 0000000..b05b232 --- /dev/null +++ b/home/.tmux.conf @@ -0,0 +1,97 @@ +# Advertise tmux's own terminfo rather than inheriting the outer TERM. Programs then look up +# capabilities tmux actually implements; `xterm-256color` promises xterm features tmux lacks, which +# is what produces stray escape sequences in vim and full-screen apps. Truecolor is a separate +# capability the outer terminal has to be trusted for, so it is declared for that TERM specifically. +set -g default-terminal "tmux-256color" +set -as terminal-features ",xterm-256color:RGB" + +# How long tmux waits after an Escape to see whether more bytes follow (a real escape sequence) +# before forwarding it as a bare Esc. Recent tmux already defaults to 10ms, but it spent years at +# 500ms — long enough to put a visible delay in front of every Esc, including the vi-mode keys +# below and Claude Code's interrupt. Pinned so an older tmux behaves the same as this one. +set -s escape-time 10 + +# Let vim and Claude Code see focus gain/loss so they can redraw or autosave on tab switches +set -g focus-events on + +set -g history-limit 50000 +set -g display-time 2000 +set -g mouse on + +# Copying tmux's buffer out to the terminal's clipboard over OSC 52 already happens at the default +# `external` — that is the half that carries a yank back through SSH to the local clipboard, +# including an iPhone's. `on` adds the other direction: an application running in a pane may use +# OSC 52 to populate a tmux buffer, so a copy made inside vim reaches tmux's own paste buffer. +set -g set-clipboard on + +# Permit escape sequences tmux does not understand to reach the outer terminal, wrapped in tmux's +# DCS passthrough. The iTerm2 tab-title system in ~/.zsh/iterm2-tab-title.zsh depends on this to +# keep publishing its user variables from inside a tmux pane. +set -g allow-passthrough on + +# Windows and panes count from 1: the 0 key sits past 9 on a keyboard, so a 0-indexed first window +# is the one furthest from the prefix. Renumbering keeps the sequence gapless after a close. +set -g base-index 1 +setw -g pane-base-index 1 +set -g renumber-windows on + +# Size each window to the smallest client *currently looking at it*, rather than to the smallest +# client attached to the session at all. This is what keeps a desktop window full-size while a +# phone is also attached to the same session but viewing something else. +setw -g aggressive-resize on + +# Match the shell's vi-mode plugin. tmux already infers this from $EDITOR, which is `mvim -v` here +# and so reads as vi — pinning it keeps copy mode from changing keys if $EDITOR ever does. +setw -g mode-keys vi +bind -T copy-mode-vi v send -X begin-selection +bind -T copy-mode-vi C-v send -X rectangle-toggle +bind -T copy-mode-vi y send -X copy-pipe-and-cancel "pbcopy" +bind -T copy-mode-vi Escape send -X cancel + +# Splits that keep the current directory, on keys shaped like the split they make. The defaults +# (% and ") are unbound rather than left as duplicates so there is one way to do it. +bind | split-window -h -c "#{pane_current_path}" +bind - split-window -v -c "#{pane_current_path}" +bind c new-window -c "#{pane_current_path}" +unbind % +unbind '"' + +# Pane movement on vim's home row. This takes over the default prefix+l (last-window), which moves +# to prefix+Tab below. +bind h select-pane -L +bind j select-pane -D +bind k select-pane -U +bind l select-pane -R +bind Tab last-window + +# -r makes these repeat without re-pressing the prefix, so a pane can be dragged to size in one go +bind -r H resize-pane -L 5 +bind -r J resize-pane -D 5 +bind -r K resize-pane -U 5 +bind -r L resize-pane -R 5 + +bind r source-file ~/.tmux.conf \; display-message "~/.tmux.conf reloaded" + +# Inside tmux the shell's own title sequences terminate at tmux, so without this the outer terminal +# title freezes at whatever was set before tmux started. `#T` is the active pane's title, which +# ~/.zsh/iterm2-tab-title.zsh sets to the git branch (or the running command) — republishing that +# unchanged keeps iTerm2 tabs reading exactly as they do outside tmux, branch first. +set -g set-titles on +set -g set-titles-string "#T" + +# Status line: `bg=default` inherits the terminal background instead of painting a colored bar, so +# this reads correctly against any iTerm2 profile. The current window is marked by weight and +# color rather than a highlight block, for the same reason. +set -g status-position bottom +set -g status-style "bg=default,fg=colour245" +set -g status-left "#[fg=colour109,bold] #S #[default]" +set -g status-left-length 30 +set -g status-right "#[fg=colour245] %-d %b %-I:%M %p " +set -g status-right-length 30 +setw -g window-status-format " #I #W " +setw -g window-status-current-format " #I #W " +setw -g window-status-current-style "fg=colour223,bold" +setw -g window-status-bell-style "fg=colour167,bold" +set -g pane-border-style "fg=colour238" +set -g pane-active-border-style "fg=colour109" +set -g message-style "fg=colour223,bg=colour238" diff --git a/home/.zsh/aliases.zsh b/home/.zsh/aliases.zsh index 31d2271..e426c25 100644 --- a/home/.zsh/aliases.zsh +++ b/home/.zsh/aliases.zsh @@ -1,6 +1,10 @@ # Shell alias e='exec' alias ta='tmux -2 attach || tn' +alias tn='tmux -2 new' +# Attach if it exists, create it otherwise: one command for starting and for resuming after a +# dropped connection, which is the whole point of the session surviving on the far end +alias tm='tmux -2 new -A -s mobile' alias vim='mvim -v' # Ruby diff --git a/home/.zsh/iterm2-tab-title.zsh b/home/.zsh/iterm2-tab-title.zsh index b7d32db..1bbbc9c 100644 --- a/home/.zsh/iterm2-tab-title.zsh +++ b/home/.zsh/iterm2-tab-title.zsh @@ -13,7 +13,25 @@ DISABLE_AUTO_TITLE="true" _set_iterm2_user_var() { local b64 b64=$(printf '%s' "$2" | base64 | tr -d '\n') - printf '\e]1337;SetUserVar=%s=%s\a' "$1" "$b64" + # tmux swallows escape sequences it does not implement, so inside a pane this one has to be + # wrapped in tmux's DCS passthrough (with its own ESC doubled) to reach iTerm2. Depends on + # `allow-passthrough on` in ~/.tmux.conf. + if [[ -n $TMUX ]]; then + printf '\ePtmux;\e\e]1337;SetUserVar=%s=%s\a\e\\' "$1" "$b64" + else + printf '\e]1337;SetUserVar=%s=%s\a' "$1" "$b64" + fi +} + +# tmux ignores OSC 1 (icon name) but acts on OSC 2, so inside a pane the title has to go out as +# OSC 2 to land in tmux's pane title; `set-titles-string "#T"` then republishes it to iTerm2. +# Outside tmux, OSC 1 remains the one that names the tab without also retitling the window. +_tab_title_emit() { + if [[ -n $TMUX ]]; then + printf '\e]2;%s\a' "$1" + else + printf '\e]1;%s\a' "$1" + fi } _tab_title_idle() { @@ -23,12 +41,12 @@ _tab_title_idle() { [[ -z $branch ]] && branch="no-git" dir=${${PWD/#$HOME/~}:t} _set_iterm2_user_var dir "$dir" - printf '\e]1;%s\a' "$branch" + _tab_title_emit "$branch" } # Fallback title for processes that don't emit their own OSC title. Claude Code, vim with # titlestring, ssh, tmux, etc. will overwrite this almost immediately, which is what we want. -_tab_title_running() { printf '\e]1;▶ %s\a' "$1" } +_tab_title_running() { _tab_title_emit "▶ $1" } autoload -Uz add-zsh-hook add-zsh-hook precmd _tab_title_idle