# Forward modified keys (Shift/Ctrl/Alt+Enter) to apps like pi-coding-agent. # Without this, tmux collapses them to a plain Enter. Requires tmux 3.5+ for csi-u. set -g extended-keys on set -g extended-keys-format csi-u # termux reports xterm-256color: without RGB every #rrggbb below is approximated to the 256 # palette on the phone, and without extkeys tmux never asks the terminal for Shift/Ctrl+Enter. # (kitty is xterm-kitty, whose terminfo already carries both.) set -as terminal-features 'xterm-256color:RGB:extkeys' # Let apps wrap escape sequences in DCS tmux passthrough (Claude Code / pi notifications, # Claude's progress bar) so they reach the outer terminal instead of being swallowed. set -g allow-passthrough on # Mouse/touch reporting. Chiefly so the window list in the status bar is # tappable — tmux binds MouseDown1Status to `switch-client -t =` out of the box, # and status-format[1] below keeps the `range=window|…` markers that make each # entry a hit target, so a tap on a tab switches to it with no extra binding. # Wheel over the bar cycles windows; right-click a tab or the session name for # tmux's built-in menus; tmux-menus (plugin below) puts the same menus on prefix+\, # which a phone can tap through. # # The cost: drag-select now goes to tmux instead of kitty, so hold Shift when you # want kitty's own selection. Scroll wheel enters copy-mode on the pane rather # than kitty's scrollback, and a click focuses the pane under it. set -g mouse on # Number windows (and panes) from 1 instead of 0 — easier to reach on the # keyboard. renumber-windows keeps them gap-free when a window is closed. set -g base-index 1 setw -g pane-base-index 1 set -g renumber-windows on # One long agent turn scrolls past tmux's default 2000 lines; resurrect captures pane # contents from this buffer too. Applies to panes created after the change. set -g history-limit 50000 # Copy-mode: vi keys, v/y like an editor. set-clipboard on pushes every copy — tmux's own and # those apps inside make (nvim, pi) — over OSC 52 to the CLIENT terminal, so a yank on the # phone lands on the phone and on kitty lands on the desktop, wherever the server runs. setw -g mode-keys vi bind -T copy-mode-vi v send -X begin-selection bind -T copy-mode-vi y send -X copy-selection-and-cancel set -s set-clipboard on # --- Session persistence (survives reboot) --------------------------------- # resurrect: save/restore sessions, windows, panes, layout and working dirs # (prefix + Ctrl-s to save, prefix + Ctrl-r to restore). # continuum: auto-saves every 15 min and auto-restores when the tmux server # starts, so sessions come back after a reboot. Both via tpm. set -g @plugin 'tmux-plugins/tpm' set -g @plugin 'tmux-plugins/tmux-resurrect' set -g @plugin 'tmux-plugins/tmux-continuum' # menus: tmux's context menus on prefix+\ (tappable; no right-click needed on a phone). set -g @plugin 'jaclu/tmux-menus' # Also capture pane scrollback and restore running programs (nvim, …). # nvim 'session' strategy restores via Session.vim when present, else plain nvim. set -g @resurrect-capture-pane-contents 'on' set -g @resurrect-strategy-nvim 'session' # Auto-restore the last saved environment on tmux server start. set -g @continuum-restore 'on' # Auto-start a detached tmux server at login: continuum writes and enables a # systemd --user unit (~/.config/systemd/user/tmux.service). With the restore # option above, sessions are already back and waiting after a reboot+login. set -g @continuum-boot 'on' # --- Status bar (Catppuccin Mocha, flat) ----------------------------------- # Palette: bg #1e1e2e · surface #313244 · divider #45475a · muted #6c7086/#7f849c # text #cdd6f4 · subtext #a6adc8 · accent(mauve) #cba6f7 set -g status on set -g status-position bottom # (also tmux's default) set -g status-interval 5 # refresh clock + CPU every 5s set -g status-justify left set -g status-style "bg=default,fg=#cdd6f4" # transparent: no bar background set -g status-left-length 40 set -g status-right-length 100 # --- Mobile view ----------------------------------------------------------- # status-left and status-right are re-evaluated per client on every draw, and # #{client_width} is that client's own width — so in `auto` mode a phone attached # to the same session as the desktop renders the compact bars while the desktop # keeps the full ones, simultaneously and with no intervention. # # @status_mode is auto (per-client, by width) | full | compact, cycled with # prefix+S. Rather than branch each bar three ways, the mode picks the width a # client has to beat — 0 nothing can lose, 9999 nothing can win — so each # decision below stays a single comparison. The 100 is the switch point: the full # right segment is ~60 columns before the window list even starts. Set # @status_mode without -g to scope an override to one session, so a grouped # mobile session can sit on `compact` while the desktop session stays on `auto`. # # Each variant lives in its own option instead of inline because #{?a,b,c} splits # its branches on commas and the styles are full of them (#[fg=…,bold]); option # values are substituted after the ternary is parsed, so those commas are never # seen as separators. # # NOTE: the comparisons MUST use #{e|>=:…}. Plain #{>=:…} compares as strings, so # "144" >= "100" is false and every client would get the compact bar. set -g @status_mode auto set -g @status_cutoff "#{?#{==:#{@status_mode},full},0,#{?#{==:#{@status_mode},compact},9999,100}}" # Left: session name (mauve/bold) + thin divider. Compact clips the name to two # characters — enough to tell sessions apart when every column counts. #{=2:…} # takes a format NAME, so it's session_name here rather than the #S shorthand. set -g @left_full " #[fg=#cba6f7,bold]#S #[fg=#45475a]│ " set -g @left_compact " #[fg=#cba6f7,bold]#{=2:session_name} #[fg=#45475a]│ " set -g status-left "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@left_full},#{E:@left_compact}}" # Middle: flat window list — inactive muted, active mauve/bold, 2-space gaps. set -g window-status-separator " " setw -g window-status-format "#[fg=#7f849c]#I#{P:#{E:@agent_badge}} #[fg=#6c7086]#W#[default]" setw -g window-status-current-format "#[fg=#cba6f7,bold]#I #[fg=#cdd6f4,bold]#W" setw -g window-status-activity-style "fg=#f9e2af" # The agent-state bell flags the window (monitor-bell) so the outer terminal buzzes; tmux's # default bell style is `reverse`, which painted the window entry — only the glyph gets colour. setw -g window-status-bell-style default # Right: net dl/ul · cpu · mem · temp │ date │ clock. One #() job per refresh: metrics.sh # prints the whole coloured segment (it was five separate scripts per tick). continuum # prepends its invisible save hook to status-right (this block sits above the tpm `run` # line, so the hook survives every reload). The hook lands ahead of the #{?} below, i.e. # outside it, so it keeps running for every client regardless of which variant is drawn. set -g @right_full "#(~/.config/tmux/scripts/metrics.sh full)#[fg=#45475a]│ #[fg=#a6adc8]%a %b %-d #[fg=#45475a]│ #[fg=#cba6f7,bold]%H:%M " # Compact: cpu + mem only. Net rates and the temp glyph go first (widest, least # useful on a phone), then the date, then the clock — a phone shows its own. set -g @right_compact "#(~/.config/tmux/scripts/metrics.sh compact) " set -g status-right "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@right_full},#{E:@right_compact}}" # Cycle auto -> compact -> full -> auto. Braces instead of nested quoted strings # so the styles' commas and the inner if-shell don't need escaping. bind S { if -F '#{==:#{@status_mode},auto}' { set -g @status_mode compact display 'status bar: compact (forced)' } { if -F '#{==:#{@status_mode},compact}' { set -g @status_mode full display 'status bar: full (forced)' } { set -g @status_mode auto display 'status bar: auto — switches at 100 columns' } } refresh-client -S } # Two rows: [0] drawn top-border rule · [1] the bar content. The rule doubles as # the separator from the pane content above; tmux status height is whole rows, so # one row above the content is the smallest gap that still shows a border. # The rule is a literal run of ─ in @rule (300 columns; tmux clips it to the client # width), so drawing it costs no process. # status-format[1] is tmux 3.7's default content template (status-left/windows/right). set -g status 2 set -g @rule "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────" set -g status-format[0] "#[fg=#6c7086,bg=default]#{@rule}" set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[range=user|newwin] #[fg=#6c7086]+#[default] #[norange]#[range=user|agentnext] #[fg=#6c7086]»#[default] #[norange]#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]" # The `+` appended to the window list above sits in a `range=user|newwin` # marker. Per tmux(1), a user range triggers the same `Status` mouse key as a # window range and exposes its argument in #{mouse_status_range} — window ranges # report the literal string "window" — so one binding can serve both: click a # tab to select it (tmux's stock behaviour, kept in the else branch), click the # `+` to open a new window, click the `»` to jump to the next window with an agent # mark (scripts/agent-next.sh, also prefix+Tab). Padded with spaces on both sides so # each is a wide enough target to hit with a thumb. bind -n MouseDown1Status { if -F '#{==:#{mouse_status_range},newwin}' { new-window } { if -F '#{==:#{mouse_status_range},agentnext}' { run-shell '~/.config/tmux/scripts/agent-next.sh' } { switch-client -t = } } } # Agent state per pane — set by ~/.config/tmux/scripts/agent-state.sh from the # Claude Code / codex / pi / opencode hooks: @agent = busy|done|input|error. @agent_badge draws # one glyph glued to the window number ("2✓ name") in the window list, the same for every tool: ✓ done (green), # ? waiting for input (yellow), ! error (red); busy draws nothing. Visiting the pane only takes # the colour away (pane-focus-in sets @agent_seen; needs focus-events): the glyph stays, grey, # until the next prompt turns the state busy. prefix K clears every mark; prefix Tab (or the » # in the bar) jumps to the next window with a mark, coloured ones first. A coloured glyph # always means "this happened while you were not looking". set -g @agent_badge "#{?#{==:#{@agent},done},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#a6e3a1]}✓,#{?#{==:#{@agent},input},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#f9e2af]}?,#{?#{==:#{@agent},error},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#f38ba8]}!,}}}" set -g focus-events on set-hook -g pane-focus-in 'set -p @agent_seen 1' bind Tab run-shell '~/.config/tmux/scripts/agent-next.sh' bind K run-shell 'for p in $(tmux list-panes -a -F "##{pane_id}"); do tmux set -p -t $p -u @agent; tmux set -p -t $p -u @agent_seen; done' \; display-message "agent marks cleared" # Round out the theme so no default green leaks into borders/messages/modes. set -g pane-border-style "fg=#313244" set -g pane-active-border-style "fg=#cba6f7" set -g message-style "bg=#313244,fg=#cdd6f4" set -g message-command-style "bg=#313244,fg=#cdd6f4" set -g mode-style "bg=#45475a,fg=#cdd6f4" set -g clock-mode-colour "#cba6f7" # Initialize tpm — KEEP THIS LINE AT THE VERY BOTTOM of tmux.conf. run '~/.config/tmux/plugins/tpm/tpm'