tadhg.com
tadhg.com
 

Vim and tmux in OS X

12:30 Sun 26 Aug 2012
[, , , , , , ]

I’ve been experimenting with using terminal Vim[1] in a tmux environment recently. I like it as a programming setup, primarily because of the ease with which I can set up new workspaces[2] and switch between them—without, of course, having to move my hands off the keyboard. I did encounter some annoyances along the way, and my solutions for them are included below.

The main advantage of tmux is being able to have multiple consoles open at once, in a highly configurable pattern, and being able to switch between them easily. Lots of programming requires switching between various files and between various command line invocations, and that’s easier to deal with using tmux than anything else I’ve found so far.

Below is an only slightly contrived example, of what my tmux environment would look like while I worked on my reStructuredText scripts:

Sample tmux layout

  • Upper left is Vim, editing a Python file.
  • Upper right is Vim, editing a Python test file.
  • Lower left is the Python shell.
  • Lower right is a zsh shell in which I’ve just run tests and checked git status.
  • The very bottom has a status bar indicating which tmux window I’m in (out of one) and displaying the machine name and time & date.

Clearly, there are plenty of other uses, from remote sessions to tailing logs to watching the output of top and more.

Terminal Vim and Multiple Modifier Keys

I use a number of customizations in MacVim that rely on combining modifier keys, sometimes more than one at a time, with non-printing keys, for example:

Control-Tab
Enter bufexplorer window.

Shift-Space
Page up.

Shift-Backspace
Delete the rest of the line and don’t copy it into the register[3].

Consoles and terminals, however, emulate the hardware that had some significant restrictions, and these terminals cannot cope with combining modifier keys and non-printing keys, or with multiple modifier keys. This isn’t something that terminal Vim can solve on its own; the terminal emulator it’s running in never gets the key combination in question and instead receives the combination lacking some piece.

I’m too used to my customizations, however, and was determined to find a way around this. I’m sure it’s not the only way, but what I came up with was:

  • Tell my terminal (iTerm2) to map these key combinations to obscure UTF-8 characters that I’m unlikely to ever use.
  • Add settings in my .vimrc to map these back from the UTF-8 characters to the original key combinations (which are themselves mapped to other things elsewhere in my settings).

For example, to get Control-Tab to work, I mapped it to ? (a Maltese Cross) via iTerm2’s preferences[4], and then added the following lines to my .vimrc:

:map ? <S-Space>
:map! ? <S-Space>
:vmap ? <S-Space>

This is evidently dependent on UTF-8 support, and I haven’t tried it on many remote machines yet, but I make an effort to ensure that any environment I have to work in supports UTF-8.

I’m reconsidering the Shift-Space mapping. I use it a lot, and find Control-B an awkward chord, but one problem with this approach is that you can’t restrict it to Vim—the key mapping applies to the entire terminal. And because I’ve never had to worry about it before, I apparently don’t tend to release Shift fully before going on to hit Space, resulting in a lot of command-line entries similar to this:

cp -R?bar

If that persists, I might abandon that particular key combination. All of the others, however, work very well so far.

Cutting and Pasting

One of tmux’s great features is the ability to scroll back through your history (in each individual pane) and to select and copy text using the keyboard only. This doesn’t quite work properly in OS X; you need to download reattach-to-user-namespace and set it up to make tmux and OS X play nicely together.

By playing nicely together, I mean that it becomes possible to use the OS X command-line interfaces to its clipboard, pbcopy and pbpaste , from within a tmux session, and hence to get the text you copy from tmux’s copy mode into the OS X clipboard by something like this (which makes your tmux prefix key followed by Control-Y put the contents of the tmux paste buffer into the OS X clipboard):

bind-key C-y run-shell "reattach-to-user-namespace -l zsh -c ’tmux saveb - | pbcopy’"

I have Vim set up to use the system clipboard by default[5], so copying and pasting between Vim and OS X was already fine. But copying from a tmux pane into Vim, even if Vim was in another tmux pane, required those extra two keystrokes, or required moving into insert mode and then having tmux push the pasted text in. That felt wrong, and was very easy to forget, and so I decided that I’d essentially always want something I copied in tmux to go into the OS X clipboard.

Making that happen automatically was far more difficult than I expected, but I found a way to do it that, in the end, wasn’t too bad in terms of total amount of code. Some lines added to my .tmux.conf and a moderately-sized shell script. It works around various unfortunate tmux restrictions by mapping keys, executing tmux commands from the shell, and unmapping those keys again. Naturally this is all assuming the use of vi-mode for tmux[6].

The critical line in .tmux.conf really just calls the script when you enter copy-mode, in this case replacing the previous mapping of [ as the way to do that:

bind-key [ run-shell "tmux-copy.sh copymode"

The real work is done by tmux-copy.sh:

#!/bin/sh

if [ $# -ne 1 ];
then
    echo "Takes a single argument";
    exit 1;
fi

mode=$1

copymode() {
    tmux bind-key -n "Enter" run-shell "tmux-copy.sh exitcopymode";
    tmux bind-key -n "C-c" run-shell "tmux-copy.sh cancelcopymode";
    tmux bind-key -n "q" run-shell "tmux-copy.sh cancelcopymode";
    tmux bind-key -t vi-copy "C-t" copy-selection
    tmux bind-key -t vi-copy "C-r" cancel
    tmux copy-mode;
}
exitcopymode() {
    tmux send-keys "C-t"
    clearkeys
    reattach-to-user-namespace -l zsh -c "tmux saveb - | pbcopy";
}
cancelcopymode() {
    tmux send-keys "C-r"
    clearkeys
}
clearkeys() {
    tmux unbind-key -n "Enter"
    tmux unbind-key -n "C-c"
    tmux unbind-key -n "q"
    tmux unbind-key -t vi-copy "C-t"
    tmux unbind-key -t vi-copy "C-r"
}

if [ $mode == "copymode" ];
then
    copymode
elif [ $mode == "exitcopymode" ];
then
    exitcopymode
elif [ $mode == "cancelcopymode" ];
then
    cancelcopymode
else
    echo "Unrecognized argument";
    exit 1;
fi

And yes, that is an awful lot of work as compared to what I naively thought it’d be initially, i.e.:

bind-key -t vi-copy "Enter" copy-selection \; reattach-to-user-namespace -l zsh -c ’tmux saveb - | pbcopy’;

With those two particulars yaks shaved, Vim and tmux work quite well for me in combination, and I’m looking forward to using them regularly. I’m not planning to stop using MacVim, as it’s a better environment for dedicated prose writing, but I’m going to try moving my coding almost entirely into terminal Vim.

[1] That is, Vim running in a console or terminal rather than as a fully-fledged graphical application like MacVim or gVim.

[2] By which I really mean new console prompts, not something like a virtual desktop.

[3] Vim assumes that you want to copy whatever you delete. This often isn’t the case. Vim lets you state otherwise by prepending "_ to your command that would copy the text (" means “use the following register”, and _ is the “black hole” register), but I’ve found that awkward to type, so I did the following to make it a lot easier to avoid copying text I was deleting:

" Backspace means delete to blackhole register:
:nnoremap <BS> "_d
:nnoremap <S-BS> "_D
:vnoremap <BS> "_d
:vnoremap <S-BS> "_D
[4] Which ends up looking like this, deep in the iTerm2 plist controlling its settings:

<key>0x20-0x20000</key>
<dict>
    <key>Action</key>
    <integer>12</integer>
    <key>Text</key>
    <string>?</string>
</dict>
[5] set clipboard=unnamed, in case you were wondering.

[6] setw -g mode-keys vi.

Leave a Reply