# ~/.tilemux.conf - i3-like tiling for tmux # Tested design target: tmux 3.6 # # Main modifier: Alt (tmux calls it Meta, written M-). # These bindings do NOT require the normal tmux prefix. # # Layout model: a window is a row of columns, each column a stack of panes. # Alt+Shift+left/right move the pane to the neighbouring column # Alt+Shift+up/down move the pane within its own column # at the window edge the pane leaves its column and becomes a # full-width row or full-height column of its own # Alt+m then direction merge the pane into the neighbour, the way back # # Quick help: Alt+? ##### General ############################################################### # Treat tmux windows like numbered workspaces. set -g base-index 1 setw -g pane-base-index 1 set -g renumber-windows off # Put status at the top. set -g status-position top # Alt must not be read as Escape then a key. This is the default on 3.6; # setting it explicitly covers older tmux. set -sg escape-time 10 # Keep a normal tmux prefix as an escape hatch. unbind C-b set -g prefix C-Space bind C-Space send-prefix set -g history-limit 50000 set -g focus-events on ##### Help: Alt+? ########################################################### # Shift+/ produces ?, so Alt+? is M-? in tmux notation. bind -n 'M-?' display-popup -E -w 76 -h 23 -T ' tilemux - Enter to close ' 'printf "%s\n" \ " WORKSPACES" \ " Alt+1..9,0 select/create workspace (0 is workspace 10)" \ " Alt+Shift+1..9,0 move pane there; a lone pane takes its window" \ " Ctrl+Alt+Left/Right previous/next workspace" \ " Alt+Tab, Alt+backtick last workspace" \ " Alt+n rename workspace" \ " FOCUS" \ " Alt+h/j/k/l or Arrow focus left/down/up/right" \ " Alt+o last pane" \ " MOVE PANE" \ " Alt+Shift+h/j/k/l/Arrow left/right to the next column, up/down in it" \ " at the window edge: a full row or column" \ " Alt+m then a direction merge into the neighbour, the way back" \ " PANES" \ " Alt+Enter new pane, split along the longer side" \ " Alt+b / Alt+v split left|right / top|bottom" \ " Alt+f / Alt+Shift+q fullscreen/zoom / kill pane" \ " Alt+e cycle tmux preset layouts" \ " Alt+r resize mode: hjkl/arrows, = evens, Enter/Esc" \ " Ctrl+Space normal tmux prefix (Alt+? shows this list)"; read _' ##### Focus panes: Alt+hjkl AND Alt+arrows ################################## 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 bind -n M-Left select-pane -L bind -n M-Down select-pane -D bind -n M-Up select-pane -U bind -n M-Right select-pane -R # Back to the pane focused before this one. bind -n M-o last-pane ##### Move panes: Alt+Shift+hjkl AND Alt+Shift+arrows ####################### # # tmux exposes pane geometry to formats but not its layout tree, so the # rules are expressed in geometry: # # left/right pane is as tall as the window -> it is a whole column, so # swap it with the neighbour # otherwise -> leave this column and join # the neighbouring one # nothing in that direction -> become a full-height column # at that edge of the window # # up/down a pane in that direction -> swap with it # nothing in that direction -> become a full-width row at # that edge of the window # # A pane that already spans the window along the relevant axis does nothing. # That is what stops repeated presses from nesting forever. # # Notes: # - {left-of} and friends WRAP AROUND the window, so every use of them is # guarded by the matching pane_at_* flag. # - tmux takes the source of a join or swap from the marked pane whenever # one is set, whatever -s says, so the mark is cleared first, even when # the rules below then decide to do nothing. Marks belong to the join # and swap workflow these bindings replace. # - the pane joins above the neighbour, or below it when moving out of the # bottom row, so it keeps roughly the row it came from. # - select-layout -E gives every pane in the destination column an equal # share, the way i3 does when a window joins a container. # - the logic lives in command aliases because tmux cannot bind two keys # to one command list any other way. Aliases take no arguments. # - a zoomed pane covers the window, so every rule would read as "already # spans everything" and do nothing. Moving unzooms first, which is what # select-pane does for focus anyway. set -s command-alias[100] "tile-left=select-pane -M ; \ if -F '#{window_zoomed_flag}' { resize-pane -Z } ; \ if -F '#{pane_at_left}' { \ if -F '#{&&:#{!=:#{window_panes},1},#{!=:#{pane_height},#{window_height}}}' { \ join-pane -h -f -b -t '{next}' ; select-layout -E \ } \ } { \ if -F '#{==:#{pane_height},#{window_height}}' { \ swap-pane -d -t '{left-of}' \ } { \ if -F '#{pane_at_bottom}' { \ join-pane -v -t '{left-of}' ; select-layout -E \ } { \ join-pane -v -b -t '{left-of}' ; select-layout -E \ } \ } \ }" set -s command-alias[101] "tile-right=select-pane -M ; \ if -F '#{window_zoomed_flag}' { resize-pane -Z } ; \ if -F '#{pane_at_right}' { \ if -F '#{&&:#{!=:#{window_panes},1},#{!=:#{pane_height},#{window_height}}}' { \ join-pane -h -f -t '{next}' ; select-layout -E \ } \ } { \ if -F '#{==:#{pane_height},#{window_height}}' { \ swap-pane -d -t '{right-of}' \ } { \ if -F '#{pane_at_bottom}' { \ join-pane -v -t '{right-of}' ; select-layout -E \ } { \ join-pane -v -b -t '{right-of}' ; select-layout -E \ } \ } \ }" set -s command-alias[102] "tile-up=select-pane -M ; \ if -F '#{window_zoomed_flag}' { resize-pane -Z } ; \ if -F '#{pane_at_top}' { \ if -F '#{&&:#{!=:#{window_panes},1},#{!=:#{pane_width},#{window_width}}}' { \ join-pane -v -f -b -t '{next}' ; select-layout -E \ } \ } { \ swap-pane -d -t '{up-of}' \ }" set -s command-alias[103] "tile-down=select-pane -M ; \ if -F '#{window_zoomed_flag}' { resize-pane -Z } ; \ if -F '#{pane_at_bottom}' { \ if -F '#{&&:#{!=:#{window_panes},1},#{!=:#{pane_width},#{window_width}}}' { \ join-pane -v -f -t '{next}' ; select-layout -E \ } \ } { \ swap-pane -d -t '{down-of}' \ }" bind -n M-H tile-left bind -n M-J tile-down bind -n M-K tile-up bind -n M-L tile-right bind -n M-S-Left tile-left bind -n M-S-Down tile-down bind -n M-S-Up tile-up bind -n M-S-Right tile-right ##### Merge into the neighbour: Alt+m, then a direction ##################### # # Alt+Shift+direction pushes a pane out of its column, and once a pane is a # whole column of its own it swaps with its neighbours instead of joining # them. This is the way back in: merge always joins the neighbouring pane, # whatever the shape of either of them. # # Alt+m h/l stack into the column to the left/right # Alt+m j/k sit beside the pane below/above, turning a stack into a row # # Nothing happens at the window edge, because there is no neighbour there. # The key table reverts to root after one key, so this is a single chord, # not a mode that has to be left. set -s command-alias[104] "tile-merge-left=select-pane -M ; \ if -F '#{window_zoomed_flag}' { resize-pane -Z } ; \ if -F '#{==:#{pane_at_left},0}' { \ if -F '#{pane_at_bottom}' { \ join-pane -v -t '{left-of}' ; select-layout -E \ } { \ join-pane -v -b -t '{left-of}' ; select-layout -E \ } \ }" set -s command-alias[105] "tile-merge-right=select-pane -M ; \ if -F '#{window_zoomed_flag}' { resize-pane -Z } ; \ if -F '#{==:#{pane_at_right},0}' { \ if -F '#{pane_at_bottom}' { \ join-pane -v -t '{right-of}' ; select-layout -E \ } { \ join-pane -v -b -t '{right-of}' ; select-layout -E \ } \ }" set -s command-alias[106] "tile-merge-up=select-pane -M ; \ if -F '#{window_zoomed_flag}' { resize-pane -Z } ; \ if -F '#{==:#{pane_at_top},0}' { \ if -F '#{pane_at_right}' { \ join-pane -h -t '{up-of}' ; select-layout -E \ } { \ join-pane -h -b -t '{up-of}' ; select-layout -E \ } \ }" set -s command-alias[107] "tile-merge-down=select-pane -M ; \ if -F '#{window_zoomed_flag}' { resize-pane -Z } ; \ if -F '#{==:#{pane_at_bottom},0}' { \ if -F '#{pane_at_right}' { \ join-pane -h -t '{down-of}' ; select-layout -E \ } { \ join-pane -h -b -t '{down-of}' ; select-layout -E \ } \ }" bind -n M-m switch-client -T merge bind -T merge h tile-merge-left bind -T merge j tile-merge-down bind -T merge k tile-merge-up bind -T merge l tile-merge-right bind -T merge Left tile-merge-left bind -T merge Down tile-merge-down bind -T merge Up tile-merge-up bind -T merge Right tile-merge-right bind -T merge Enter switch-client -T root bind -T merge Escape switch-client -T root ##### Create panes / layout ################################################# # Alt+Enter: new pane, split along the longer side of the current pane. # Terminal cells are about twice as tall as they are wide, hence the *2. bind -n M-Enter if -F '#{e|>:#{pane_width},#{e|*:#{pane_height},2}}' { split-window -h -c '#{pane_current_path}' } { split-window -v -c '#{pane_current_path}' } # Explicit split orientation. bind -n M-b split-window -h -c '#{pane_current_path}' bind -n M-v split-window -v -c '#{pane_current_path}' bind -n M-e next-layout bind -n M-f resize-pane -Z # Shift+q arrives as Q. bind -n M-Q kill-pane ##### Resize mode: Alt+r, then hjkl OR arrows ############################### # # The mode stays active until Enter or Escape. Any other key leaves it too, # because unbound keys fall back to the root table. status-right names the # key table whenever it is not root, so the mode is visible. bind -n M-r switch-client -T resize bind -T resize h resize-pane -L 5 \; switch-client -T resize bind -T resize j resize-pane -D 2 \; switch-client -T resize bind -T resize k resize-pane -U 2 \; switch-client -T resize bind -T resize l resize-pane -R 5 \; switch-client -T resize bind -T resize Left resize-pane -L 5 \; switch-client -T resize bind -T resize Down resize-pane -D 2 \; switch-client -T resize bind -T resize Up resize-pane -U 2 \; switch-client -T resize bind -T resize Right resize-pane -R 5 \; switch-client -T resize # Give the panes around this one equal shares, without leaving the mode. bind -T resize = select-layout -E \; switch-client -T resize bind -T resize Enter switch-client -T root bind -T resize Escape switch-client -T root ##### Workspaces ############################################################ # # ":N" means window N in the current session. Alt+0 is workspace 10, so the # workspaces read 1..10 in order. list-panes is only a probe for "does this # window exist", so its output and its error are both discarded. It runs # the tmux binary, so tmux has to be on PATH in the server environment; # without it, selecting a workspace that exists fails with "index in use". # Command aliases cannot help here: tmux does not expand formats in -t. bind -n M-1 if-shell 'tmux list-panes -t :1 >/dev/null 2>&1' { select-window -t :1 } { new-window -t :1 -c '#{pane_current_path}' } bind -n M-2 if-shell 'tmux list-panes -t :2 >/dev/null 2>&1' { select-window -t :2 } { new-window -t :2 -c '#{pane_current_path}' } bind -n M-3 if-shell 'tmux list-panes -t :3 >/dev/null 2>&1' { select-window -t :3 } { new-window -t :3 -c '#{pane_current_path}' } bind -n M-4 if-shell 'tmux list-panes -t :4 >/dev/null 2>&1' { select-window -t :4 } { new-window -t :4 -c '#{pane_current_path}' } bind -n M-5 if-shell 'tmux list-panes -t :5 >/dev/null 2>&1' { select-window -t :5 } { new-window -t :5 -c '#{pane_current_path}' } bind -n M-6 if-shell 'tmux list-panes -t :6 >/dev/null 2>&1' { select-window -t :6 } { new-window -t :6 -c '#{pane_current_path}' } bind -n M-7 if-shell 'tmux list-panes -t :7 >/dev/null 2>&1' { select-window -t :7 } { new-window -t :7 -c '#{pane_current_path}' } bind -n M-8 if-shell 'tmux list-panes -t :8 >/dev/null 2>&1' { select-window -t :8 } { new-window -t :8 -c '#{pane_current_path}' } bind -n M-9 if-shell 'tmux list-panes -t :9 >/dev/null 2>&1' { select-window -t :9 } { new-window -t :9 -c '#{pane_current_path}' } bind -n M-0 if-shell 'tmux list-panes -t :10 >/dev/null 2>&1' { select-window -t :10 } { new-window -t :10 -c '#{pane_current_path}' } bind -n C-M-Left previous-window bind -n C-M-Right next-window # Back to the workspace used before this one. Alt+backtick is there because # desktops that are not i3 tend to swallow Alt+Tab before the terminal sees # it. bind -n M-Tab last-window bind -n 'M-`' last-window # %%% substitutes the answer with " \ $ ; ~ escaped, so a name cannot end # the command and start another one, and -- keeps a leading dash a name. # Two costs: a backslash in a name arrives doubled, and tmux expands #{} # and #() in the name itself, which no quoting here can prevent. bind -n M-n command-prompt -p 'rename workspace:' 'rename-window -- "%%%"' ##### Alt+Shift+number = move pane to workspace ############################# # # Shifted number encoding: # 1 ! 2 @ 3 # 4 $ 5 % 6 ^ 7 & 8 * 9 ( 0 ) # # Behaviour: # - if the destination exists: join the pane there with -d, so this # workspace stays selected and focus falls to a pane that remains here; # - if it does not exist and this window has other panes: break the pane # into window N with -d, again staying here; # - if this is the only pane in the window, the whole window has to move, # because no pane would be left here to stay focused on. That does # follow the window to workspace N. bind -n 'M-!' if -F '#{!=:#{window_index},1}' { if-shell 'tmux list-panes -t :1 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :1 } { if -F '#{==:#{window_panes},1}' { move-window -t :1 } { break-pane -d -t :1 } } } bind -n 'M-@' if -F '#{!=:#{window_index},2}' { if-shell 'tmux list-panes -t :2 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :2 } { if -F '#{==:#{window_panes},1}' { move-window -t :2 } { break-pane -d -t :2 } } } bind -n 'M-#' if -F '#{!=:#{window_index},3}' { if-shell 'tmux list-panes -t :3 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :3 } { if -F '#{==:#{window_panes},1}' { move-window -t :3 } { break-pane -d -t :3 } } } bind -n 'M-$' if -F '#{!=:#{window_index},4}' { if-shell 'tmux list-panes -t :4 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :4 } { if -F '#{==:#{window_panes},1}' { move-window -t :4 } { break-pane -d -t :4 } } } bind -n 'M-%' if -F '#{!=:#{window_index},5}' { if-shell 'tmux list-panes -t :5 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :5 } { if -F '#{==:#{window_panes},1}' { move-window -t :5 } { break-pane -d -t :5 } } } bind -n 'M-^' if -F '#{!=:#{window_index},6}' { if-shell 'tmux list-panes -t :6 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :6 } { if -F '#{==:#{window_panes},1}' { move-window -t :6 } { break-pane -d -t :6 } } } bind -n 'M-&' if -F '#{!=:#{window_index},7}' { if-shell 'tmux list-panes -t :7 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :7 } { if -F '#{==:#{window_panes},1}' { move-window -t :7 } { break-pane -d -t :7 } } } bind -n 'M-*' if -F '#{!=:#{window_index},8}' { if-shell 'tmux list-panes -t :8 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :8 } { if -F '#{==:#{window_panes},1}' { move-window -t :8 } { break-pane -d -t :8 } } } bind -n 'M-(' if -F '#{!=:#{window_index},9}' { if-shell 'tmux list-panes -t :9 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :9 } { if -F '#{==:#{window_panes},1}' { move-window -t :9 } { break-pane -d -t :9 } } } bind -n 'M-)' if -F '#{!=:#{window_index},10}' { if-shell 'tmux list-panes -t :10 >/dev/null 2>&1' { select-pane -M ; join-pane -d -h -t :10 } { if -F '#{==:#{window_panes},1}' { move-window -t :10 } { break-pane -d -t :10 } } } ##### Status bar ############################################################ # %Y etc are expanded by tmux itself, so the status line forks nothing. set -g status-left '[#S] ' set -g status-right '#{?#{==:#{client_key_table},root},,[#{client_key_table}] }%Y-%m-%d %H:%M' set -g window-status-format ' #I:#W ' set -g window-status-current-format '[#I:#W#{?window_zoomed_flag, Z,}]'