If you spend any significant time in a terminal, you’ve probably used tmux.

I’m writing this post for a few reasons:

  • People have asked for my config.
  • I see too many people wasting their time in the morning, rebuilding their session from the previous day.
  • I felt I should justify the configuration to myself rather than setting options ad-hoc.

tmux is often paired with a persistent connection. There are two popular choices: Eternal Terminal and Mosh. The goal is to close your laptop at the end of the day, open it the next morning, and have everything where it was so you can immediately get back into flow.

Note: There are other options. WezTerm has built-in multiplexing, for example.

macOS + iTerm2 + Eternal Terminal + tmux Control Mode

If you use macOS, iTerm2 has deep tmux integration in the form of tmux Control Mode.

tmux windows map to iTerm2 tabs. Native tab navigation and scrollback (both scrolling and find) work just as you’d expect.

tmux control mode does expect a reliable stream channel, so if you want a connection that persists even when network connections are dropped, Mosh will not work. You’ll need Eternal Terminal.

If you use a Mac, this is an excellent configuration. I worked on EdenFS and Watchman for almost five years this way.

mosh + tmux

But now I use Windows and Linux and can’t use iTerm2, so tmux within Mosh it is.

I find tmux’s default keybindings a little awkward, and the colors simultaneously harsh and too minimal, so I made a configuration to match my tastes.

You can download the full .tmux.conf here.

(You can go crazy, but avoiding too much fanciness was my goal. If you want all the bling, install the Tmux Plugin Manager and things like tmux-powerline.

.tmux.conf

First, a small demonstration.

Despite all of the work I put into my recent post about 24-bit color in terminals, I do still use some with limited color support. macOS’s Terminal.app only supports the 256-color palette, and the Linux console only really supports 8. The following selects the correct tmux terminfo entry.

# Detect the correct TERM value for new sessions.
# if-shell uses /bin/sh, so bashisms like [[ do not work.
if "[ $(tput colors) = 16777216 ]" {
  set -g default-terminal "tmux-direct"
} {
  if "[ $(tput colors) = 256 ]" {
    set -g default-terminal "tmux-256color"
  } {
    set -g default-terminal "tmux"
  }
}

I prefer Emacs keybindings in both bash (readline) and tmux.

setw -g mode-keys emacs

The next setting is more legacy terminal insanity. On some (most?) terminals, programs cannot differentiate between a user pressing the escape key and the beginning of an escape sequence. readline and tmux default to 500 ms, which adds noticeable latency in some terminals when using programs like vi.

There’s no correct value here. Ideally, your terminal would use an unambiguous code for the escape key, like MinTTY.

set -s escape-time 200

Let’s not be stingy with scrollback! Searching lots of history is worth spending megabytes.

# I can afford 50 MB of scrollback.
# Measured on WSL 1 with:
# yes $(python3 -c "print('y' * 80)")
set -g history-limit 100000

By default, if multiple clients connect to one tmux session, tmux will resize all of the windows to the smallest connected terminal.

This behavior is annoying, and it’s always an accident. Sometimes I’ll leave a temporary connection to a server from home and then another fullscreen connection from work will cram each window into 80x25.

The aggressive-resize option applies this logic only to the currently-viewed window, not everything in the session.

setw -g aggressive-resize on

Window titles don’t automatically forward to the whatever graphical terminal you’re using. Do that, and add the hostname, but keep it concise.

set -g set-titles on
set -g set-titles-string "#h: #W"

iTerm2 has this nice behavior where active tabs are visually marked so you can see, at a glance, which had recent activity. The following two options offer similar behavior. Setting activity-action to none disables any audible ding or visible flash, leaving just a subtle indication in the status bar.

set -g monitor-activity on
set -g activity-action none

The following is perhaps the most important part of my configuration: tab management. Like browsers and iTerm2, I want my tabs numbered. I want a single (modified) keypress to select a tab, and I want tabs automatically renumbered as they’re created, destroyed, and reordered.

I also want iTerm2-style previous- and next-tab keybindings.

# Match window numbers to the order of the keys on a keyboard.
set -g base-index 1
setw -g pane-base-index 1

setw -g renumber-windows on

# My tmux muscle memory still wants C-b 0 to select the first window.
bind 0 select-window -t ":^"
# Other terminals and web browsers use 9 to focus the final tab.
bind 9 select-window -t ":$"

bind -n "M-0" select-window -t ":^"
bind -n "M-1" select-window -t ":1"
bind -n "M-2" select-window -t ":2"
bind -n "M-3" select-window -t ":3"
bind -n "M-4" select-window -t ":4"
bind -n "M-5" select-window -t ":5"
bind -n "M-6" select-window -t ":6"
bind -n "M-7" select-window -t ":7"
bind -n "M-8" select-window -t ":8"
# Browsers also select last tab with M-9.
bind -n "M-9" select-window -t ":$"
# Match iTerm2.
bind -n "M-{" previous-window
bind -n "M-}" next-window

Note that Emacs assigns meaning to Alt-number. If it matters to you, pick a different modifier.

Now let’s optimize the window ordering. By default, C-b c creates a new window at the end. That’s a fine default. But sometimes I want a new window right after the current one, so define C-b C-c. Also, add some key bindings for sliding the current window around.

bind "C-c" new-window -a

bind "S-Left" {
  swap-window -t -1
  select-window -t -1
}
bind "S-Right" {
  swap-window -t +1
  select-window -t +1
}

I wanted “C-{“ and “C-}” but terminal key encoding doesn’t work like that.

Next, let’s define some additional key bindings for very common operations.

By default, searching in the scrollback requires entering “copy mode” with C-b [ and then entering reverse search mode with C-r. Searching is common, so give it a dedicated C-b r.

bind r {
  copy-mode
  command-prompt -i -p "(search up)" \
    "send-keys -X search-backward-incremental '%%%'"
}

And some convenient toggles:

# Toggle terminal mouse support.
bind m set-option -g mouse \; display "Mouse: #{?mouse,ON,OFF}"

# Toggle status bar. Useful for fullscreen focus.
bind t set-option status

Now the status bar. The default status bar is okay, but we can do better.

tmux status bar: before
tmux status bar: before
  • Move the tmux session ID next to the hostname on the right side.
  • Move the current time to the far right corner.
  • Keep the date, but I think I can remember what year it is.
  • Ensure there is a single space between the windows and the left edge. Without a space at the edge, it looks weird.
tmux status bar: after
tmux status bar: after

The other half of that improvement is the color scheme. Instead of a harsh black-on-green, I chose a scheme that evokes old amber CRT phosphors or gas plasma displays My dad had a “laptop” with one of those when I was young.

The following color scheme mildly highlights the current window and uses a dark blue for the hostname-and-time section. These colors don’t distract me when I’m not working, but if I do look, the important information is there.

if "[ $(tput colors) -ge 256 ]" {
  set -g status-left-style "fg=black bg=colour130"
  set -g status-right-style "bg=colour17 fg=orange"
  set -g status-style "fg=black bg=colour130"
  set -g message-style "fg=black bg=colour172"
  # Current window should be slightly brighter.
  set -g window-status-current-style "fg=black bg=colour172"
  # Windows with activity should be very subtly highlighted.
  set -g window-status-activity-style "fg=colour17 bg=colour130"
  set -g mode-style "fg=black bg=colour172"
}

And that’s it!

Again, feel free to copy the complete .tmux.conf.

Shell Integration

There’s one more config to mention: adding some shell aliases to .bashrc.

I sometimes want to look at or edit a file right next to my shell.

if [[ "$TMUX" ]]; then
    function lv() {
        tmux split-window -h less "$@"
    }
    function ev() {
        tmux split-window -h emacs "$@"
    }
    function lh() {
        tmux split-window -v less "$@"
    }
    function eh() {
        tmux split-window -v emacs "$@"
    }
fi

(You may notice the aliases use different meanings of horizontal and vertical than tmux. I don’t know, it feels like tmux is backwards, but that could be my brain.)

Happy multiplexing!