aboutsummaryrefslogtreecommitdiff
path: root/tilemux.conf
blob: 5969013811eb6886ff1cc9c92e6a6210f7d8dc17 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
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,}]'