Seamless Pane-Window Switching Between Tmux and Vim
I can't be arsed with an extra keystroke when switching between windows in vim, so I've had this in my vimrc for years:
" Move the cursor to the next window left/down/up/right
nnoremap <C-H> <C-W><C-H>
nnoremap <C-J> <C-W><C-J>
nnoremap <C-K> <C-W><C-K>
nnoremap <C-L> <C-W><C-L>
And because the arrow keys are a stretch, I have this in my tmux.conf:
# Select the next pane left/down/up/right
bind h selectp -L
bind j selectp -D
bind k selectp -U
bind l selectp -R
With this configuration, jumping out of vim into another tmux pane only requires one extra keystroke: the tmux prefix. Fortunately, Vim Tmux Navigator solves that problem. But it's a bit complicated and messy. Turns out all it requires is a tiny vimscript and a few relatively simple tmux bindings.
Here's the vimscript:
" Only load this plugin if running inside tmux
if empty($TMUX)
finish
endif
function! NextWindow(direction)
let l:win_num = winnr()
execute "wincmd" a:direction
" If the window number changed, then the command succeeded. Otherwise,
" we're at an edge window and we'll let tmux handle it.
if l:win_num != winnr()
return
endif
let l:tmux_direction = tr(a:direction, 'hjkl', 'LDUR')
call system('tmux select-pane -' . l:tmux_direction)
endfunction
nmap <silent> <C-H> :call NextWindow('h')<CR>
nmap <silent> <C-J> :call NextWindow('j')<CR>
nmap <silent> <C-K> :call NextWindow('k')<CR>
nmap <silent> <C-L> :call NextWindow('l')<CR>
The tmux bindings are a little tricky. At first, I tried this:
bind -n C-h if -F "#{==:#{pane_current_command},nvim}" "send C-h" "selectp -L"
That funky nested format is required for comparing strings in tmux commands. Other than that, it's pretty straightforward. The problem is this doesn't work if vim is used as a pager, is reading from stdin, or was started from a script. In those cases, pane_current_command
might be bash, less, man, or whatever command was entered at the command line. Instead, we have to get a list of processes belonging to the terminal attached to the pane:
# If nvim is running in the foreground of the current pane, let it handle these
# keys. Otherwise, select the next pane left/down/up/right.
is_nvim="[ $(ps -o comm -t #{pane_tty} | tail -1) = nvim ]"
bind -n C-h if "$is_nvim" "send C-h" "selectp -L"
bind -n C-j if "$is_nvim" "send C-j" "selectp -D"
bind -n C-k if "$is_nvim" "send C-k" "selectp -U"
bind -n C-l if "$is_nvim" "send C-l" "selectp -R"
Obviously, if you don't use nvim, just change the bindings accordingly.
Finally, if you use emacs-style shortcuts in your shell, you might prefer to use the alt key instead of the control key to keep the default behavior of Ctrl-H
(backspace) and Ctrl-K
(delete from cursor to end of line). In that case, just replace C-
with M-
in the vim mappings and the tmux bindings. Also, make sure to use lowercase -h
, -j
, etc. in the vim mappings due to how vim/xterm handles the alt key.