Thursday, 23 February 2012

Tiling tmux Keybindings

When most people use a computer, they are are using either a compositing or stacking window manager - which basically means that windows can overlap. The major alternative to this model is known as a tiling window manager, where the window manager lays out and sizes windows such that they do not overlap each other.

I started using a tiling window manager called wmii some years ago after buying a 7" EeePC netbook and trying to find alternative software more suited to the characteristics of that machine. Most of the software I ended up using on that machine I now use on all of my Linux boxes, because I found that it suits my workflow so much better.

Wmii as a window manager primarily focuses on organising windows into tags (like multiple desktops) and columns. Within a column windows can either be sized evenly, or a single window can take up the whole height of the column, optionally with the title bars of the other windows visible (think minimised windows on steroids).

Wmii is very heavily keyboard driven (which is one of it's strengths from my point of view), though a mouse can be used for basic navigation as well. It is also heavily extensible with scripting languages and in fact almost all interactions with the window manager are actually driven by the script. It defaults to using a shell script, but also ships with equivalent python and ruby scripts (the base functionality is the same in each), and is easy to extend.

By default keyboard shortcuts provide ways to navigate left and right between columns, up and down between windows within a column, and to switch between 10 numbered tags (more tags are possible, but rarely needed). Moving a window is as simple as holding down shift while performing the same key combos used to navigate, and columns and tags are automatically created as needed (moving a window to the right of the rightmost column would create a new column for example), and automatically destroyed when no longer used.

Recent versions of wmii also work really well with multiple monitors (though there is still some room for improvement in this area) allowing windows to really easily be moved between monitors with the same shortcuts used to move windows between columns (and they way it differentiates between creating a new column on the right of the left monitor versus moving the window to the right monitor is pure genius).

Naturally with such a powerful window manager, I want to use it to manage all my windows and all my shells. The problem with this of course is SSH - specifically, when I have many remote shells open at the same time and what happens when the network goes away. You see, I've been opening a new terminal and SSH connection for each remote shell so I can use wmii to manage them, which works really great until I need to suspend my laptop or unplug it to go to a meeting, then have to spend some time re-establishing each session, getting it back to the right working directory, etc. And, I've lost the shell history specific to each terminal.

Normally people would start screen on the remote server if they expect their session to go away, and screen can also manage a number of shells simultaneously, which would be great... except that it is no where near as good at managing those shells as wmii can manage windows and if I'm going to switch it would need to be pretty darn close.

I've been aware for some time of an alternative to screen called tmux which seemed to be much more sane and feature-rich than screen, so the other day I decided to see if I could configure tmux to be a realistic option for managing many shells on a remote machine that I could detach and re-attach from when suspending my laptop.

Tmux supports multiple sessions, "windows" (like tags in wmii), and "panes" (like windows in wmii). I managed to come up with the below configuration file which sets up a bunch of keybindings similar to the ones I use in wmii (but using the Alt modifier instead of the Windows key) to move windows... err... "panes" and to navigate between them.

Unlike wmii, tmux is not focussed around columns, which technically gives it more flexibility in how the panes are arranged, but sacrifices some of the precision that the column focus gives wmii (in this regard tmux is more similar to some of the other tiling window managers available).

None of these shortcut keys need to have the tmux prefix key pressed first, as that would have defeated the whole point of this exercise:

Alt + ' - Split window vertically *
Alt + Shift + ' - Split window horizontally

Alt + h/j/k/l - Navigate left/down/up/right between panes within a window
Alt + Shift + h/j/k/l - Swap window with the one before or after it **

Alt + Ctrl + h/j/k/l - Resize pane *** - NOTE: Since many environments use Ctrl+Alt+L to lock the screen, you may want to change these to use the arrow keys instead.

Alt + number - Switch to this tag... err... "window" number, creating it if it doesn't already exist.
Alt + Shift + number - Send the currently selected pane to this window number, creating it if it doesn't already exist.

Alt + d - Tile all panes **
Alt + s - Make selected pane take up the maximum height and tile other panes off to the side **
Alt + m - Make selected pane take up the maximum width and tile other panes below **

Alt + f - Make the current pane take up the full window (actually, break it out into a new window). Reverse with Alt + Shift + number **

Alt + PageUp - Scroll pane back one page and enter copy mode. Release the alt and keep pressing page up/down to scroll and press enter when done.

* Win+Enter opens a new terminal in wmii, but Alt+Enter is already used by xterm, so I picked the key next to it

** These don't mirror the corresponding wmii bindings because I could find no exact equivalent, so I tried to make them do something similar and sensible instead.

*** By default there is no shortcut key to resize windows in wmii (though the python version of the wmiirc script provides a resize mode which is similar), so I added some to my scripts.

~/.tmux.conf (Download Latest Version Here)

# Split + spawn new shell:
# I would have used enter like wmii, but xterm already uses that, so I use the
# key next to it.
bind-key -n M-"'" split-window -v
bind-key -n M-'"' split-window -h

# Select panes:
bind-key -n M-h select-pane -L
bind-key -n M-j select-pane -D
bind-key -n M-k select-pane -U
bind-key -n M-l select-pane -R

# Move panes:
# These aren't quite what I want, as they *swap* panes *numerically* instead of
# *moving* the pane in a specified *direction*, but they will do for now.
bind-key -n M-H swap-pane -U
bind-key -n M-J swap-pane -D
bind-key -n M-K swap-pane -U
bind-key -n M-L swap-pane -D

# Resize panes (Note: Ctrl+Alt+L conflicts with the lock screen shortcut in
# many environments - you may want to consider the below alternative shortcuts
# for resizing instead):
bind-key -n M-C-h resize-pane -L
bind-key -n M-C-j resize-pane -D
bind-key -n M-C-k resize-pane -U
bind-key -n M-C-l resize-pane -R

# Alternative resize panes keys without ctrl+alt+l conflict:
# bind-key -n M-C-Left resize-pane -L
# bind-key -n M-C-Down resize-pane -D
# bind-key -n M-C-Up resize-pane -U
# bind-key -n M-C-Right resize-pane -R

# Window navigation (Oh, how I would like a for loop right now...):
bind-key -n M-0 if-shell "tmux list-windows|grep ^0" "select-window -t 0" "new-window -t 0"
bind-key -n M-1 if-shell "tmux list-windows|grep ^1" "select-window -t 1" "new-window -t 1"
bind-key -n M-2 if-shell "tmux list-windows|grep ^2" "select-window -t 2" "new-window -t 2"
bind-key -n M-3 if-shell "tmux list-windows|grep ^3" "select-window -t 3" "new-window -t 3"
bind-key -n M-4 if-shell "tmux list-windows|grep ^4" "select-window -t 4" "new-window -t 4"
bind-key -n M-5 if-shell "tmux list-windows|grep ^5" "select-window -t 5" "new-window -t 5"
bind-key -n M-6 if-shell "tmux list-windows|grep ^6" "select-window -t 6" "new-window -t 6"
bind-key -n M-7 if-shell "tmux list-windows|grep ^7" "select-window -t 7" "new-window -t 7"
bind-key -n M-8 if-shell "tmux list-windows|grep ^8" "select-window -t 8" "new-window -t 8"
bind-key -n M-9 if-shell "tmux list-windows|grep ^9" "select-window -t 9" "new-window -t 9"

# Window moving (the sleep 0.1 here is a hack, anyone know a better way?):
bind-key -n M-')' if-shell "tmux list-windows|grep ^0" "join-pane -d -t :0" "new-window -d -t 0 'sleep 0.1' \; join-pane -d -t :0"
bind-key -n M-'!' if-shell "tmux list-windows|grep ^1" "join-pane -d -t :1" "new-window -d -t 1 'sleep 0.1' \; join-pane -d -t :1"
bind-key -n M-'@' if-shell "tmux list-windows|grep ^2" "join-pane -d -t :2" "new-window -d -t 2 'sleep 0.1' \; join-pane -d -t :2"
bind-key -n M-'#' if-shell "tmux list-windows|grep ^3" "join-pane -d -t :3" "new-window -d -t 3 'sleep 0.1' \; join-pane -d -t :3"
bind-key -n M-'$' if-shell "tmux list-windows|grep ^4" "join-pane -d -t :4" "new-window -d -t 4 'sleep 0.1' \; join-pane -d -t :4"
bind-key -n M-'%' if-shell "tmux list-windows|grep ^5" "join-pane -d -t :5" "new-window -d -t 5 'sleep 0.1' \; join-pane -d -t :5"
bind-key -n M-'^' if-shell "tmux list-windows|grep ^6" "join-pane -d -t :6" "new-window -d -t 6 'sleep 0.1' \; join-pane -d -t :6"
bind-key -n M-'&' if-shell "tmux list-windows|grep ^7" "join-pane -d -t :7" "new-window -d -t 7 'sleep 0.1' \; join-pane -d -t :7"
bind-key -n M-'*' if-shell "tmux list-windows|grep ^8" "join-pane -d -t :8" "new-window -d -t 8 'sleep 0.1' \; join-pane -d -t :8"
bind-key -n M-'(' if-shell "tmux list-windows|grep ^9" "join-pane -d -t :9" "new-window -d -t 9 'sleep 0.1' \; join-pane -d -t :9"

# Set default window number to 1 instead of 0 for easier key combos:
set-option -g base-index 1

# Pane layouts (these use the same shortcut keys as wmii for similar actions,
# but don't really mirror it's behaviour):
bind-key -n M-d select-layout tiled
bind-key -n M-s select-layout main-vertical \; swap-pane -s 0
bind-key -n M-m select-layout main-horizontal \; swap-pane -s 0

# Make pane full-screen:
bind-key -n M-f break-pane
# This isn't right, it should go back where it came from:
# bind-key -n M-F join-pane -t :0

# We can't use shift+PageUp, so use Alt+PageUp then release Alt to keep
# scrolling:
bind-key -n M-PageUp copy-mode -u

# Don't interfere with vi keybindings:
set-option -s escape-time 0

# Enable mouse. Mostly to make selecting text within a pane not also grab pane
# borders or text from other panes. Unfortunately, tmux' mouse handling leaves
# something to be desired - no double/tripple click support to select a
# word/line, all mouse buttons are intercepted (middle click = I want to paste
# damnit!), no automatic X selection integration(*)...
set-window-option -g mode-mouse on
set-window-option -g mouse-select-pane on
set-window-option -g mouse-resize-pane on
set-window-option -g mouse-select-window on

# (*) This enables integration with the clipboard via termcap extensions. This
# relies on the terminal emulator passing this on to X, so to make this work
# you will need to edit your X resources to allow it - details below.
set-option -s set-clipboard on

You may also need to alter your ~/.Xresources file to make some things work (this is for xterm):

~/.Xresources (My Personal Version)

/* Make Alt+x shortcuts work in xterm */
XTerm*.metaSendsEscape: true
UXTerm*.metaSendsEscape: true

/* Allow tmux to set X selections (ie, the clipboard) */
XTerm*.disallowedWindowOps: 20,21,SetXprop
UXTerm*.disallowedWindowOps: 20,21,SetXprop

/* For some reason, this gets cleared when reloading this file: */
*customization: -color

To reload this file without logging out and back in, run:
xrdb ~/.Xresources

There's a pretty good chance that I'll continue to tweak this, so I'll try to update this post anytime I add something cool.

Edit 27/02/2012: Added mouse & clipboard integration & covered changes to .Xresources file.

No comments: