diff options
Diffstat (limited to 'tilemux.conf')
| -rw-r--r-- | tilemux.conf | 408 |
1 files changed, 408 insertions, 0 deletions
diff --git a/tilemux.conf b/tilemux.conf new file mode 100644 index 0000000..5969013 --- /dev/null +++ b/tilemux.conf @@ -0,0 +1,408 @@ +# ~/.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,}]' |