Skip to content

Instantly share code, notes, and snippets.

@hjdivad
Created May 22, 2014 22:31
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hjdivad/d7f79b45ac2922336fec to your computer and use it in GitHub Desktop.
Save hjdivad/d7f79b45ac2922336fec to your computer and use it in GitHub Desktop.

I like the idea of unifying navigation between tmux panes and vim windows. @mislav did a great job in this gist but it depends on using C-{h,j,k,l} for navigation instead of vim's default C-W {h,j,k,l}.

Tmux's bind-key doesn't support multiple keys: just a single key with a modifier, so if we want to keep using C-w we have to be a bit tricky.

This approach binds C-w to set up keybindings that a) navigate and b) unset themselves. It turns out you can't have a bind-key statement in your .tmux.conf that's too long or tmux will segfault, which is one of the reasons a lot of the work is done in bash scripts.

Installation

Dependencies

Install vim-tmux-navigator

.tmux.conf

Here's what you'd put in .tmux.conf

bind -n C-W \
	bind -n   h run "tmux-vim-select-pane -L; tmux-unbind" \; \
	bind -n c-h run "tmux-vim-select-pane -L; tmux-unbind" \; \
	bind -n   j run "tmux-vim-select-pane -D; tmux-unbind" \; \
	bind -n c-j run "tmux-vim-select-pane -D; tmux-unbind" \; \
	bind -n   k run "tmux-vim-select-pane -U; tmux-unbind" \; \
	bind -n c-k run "tmux-vim-select-pane -U; tmux-unbind" \; \
	bind -n   l run "tmux-vim-select-pane -R; tmux-unbind" \; \
	bind -n c-l run "tmux-vim-select-pane -R; tmux-unbind" \; \
	bind -n   v send-keys c-w v\\; run "tmux-unbind" \; \
	bind -n c-v send-keys c-w v\\; run "tmux-unbind" \; \
	bind -n   o send-keys c-w o\\; run "tmux-unbind" \; \
	bind -n c-o send-keys c-w o\\; run "tmux-unbind" \; \
	bind -n   c send-keys c-w c\\; run "tmux-unbind" \; \
	bind -n c-c send-keys c-w c\\; run "tmux-unbind" \; \
	bind -n   r send-keys c-w r\\; run "tmux-unbind" \; \
	bind -n c-r send-keys c-w r\\; run "tmux-unbind" \; \
	bind -n   n send-keys c-w n\\; run "tmux-unbind" \; \
	bind -n c-n send-keys c-w n\\; run "tmux-unbind" \; \

tmux-vim-select-pane

tmux-vim-select-pane is exactly the same as the one @mislav uses, and

tmux-unbind

tmux-unbind is pretty straightforward:

#!/usr/bin/env bash

tmux unbind -n h
tmux unbind -n c-h

tmux unbind -n j
tmux unbind -n c-j

tmux unbind -n k
tmux unbind -n c-k

tmux unbind -n l
tmux unbind -n c-l



tmux unbind -n v
tmux unbind -n c-v

tmux unbind -n o
tmux unbind -n c-o

tmux unbind -n c
tmux unbind -n c-c

tmux unbind -n r
tmux unbind -n c-r

tmux unbind -n n
tmux unbind -n c-n

Addendum

Note that we want to passthrough some C-w combinations, like C-w n (for new window in vim).

I've included a simple Rakefile and templates that you can use to generate .tmux.conf and tmux-unbind.

One final note is that this doesn't work in tmate, although I'm not sure why yet.

<% require './keys.rb' %>
# tmux-vim navigation
<%
c_w_bind = "bind -n C-W \\\n\t"
MovementKeys.each_pair do |key, pane_opts|
c_w_bind << %Q{bind -n #{key} run "tmux-vim-select-pane #{pane_opts}; tmux-unbind" \\; \\\n\t}
c_w_bind << %Q{bind -n c-#{key} run "tmux-vim-select-pane #{pane_opts}; tmux-unbind" \\; \\\n\t}
end
PassthroughKeys.each do |key|
c_w_bind << %Q{bind -n #{key} send-keys c-w #{key}\\\\; run "tmux-unbind" \\; \\\n\t}
c_w_bind << %Q{bind -n c-#{key} send-keys c-w #{key}\\\\; run "tmux-unbind" \\; \\\n\t}
end
%>
<%= c_w_bind %>
::MovementKeys = {
h: "-L",
j: "-D",
k: "-U",
l: "-R",
}
::PassthroughKeys = %w(v o c r n)
require 'erb'
tmux_unbind = "#{ENV['HOME']}/bin/tmux-unbind"
tmux_unbind_erb = "#{File.basename(tmux_unbind)}.erb"
tmux_conf = "#{ENV['HOME']}/.tmux.conf"
tmux_conf_erb = "#{File.basename(tmux_conf)}.erb"
desc "Rebuild everything"
task build: [tmux_unbind, tmux_conf]
# Build ~/bin/tmux-unbind
file tmux_unbind => tmux_unbind_erb do
template = ERB.new(File.read(tmux_unbind_erb))
File.open(tmux_unbind, 'w') do |file|
file.print template.result
end
File.chmod(0744, tmux_unbind)
end
# Build ~/.tmux.conf
file tmux_conf => tmux_conf_erb do
template = ERB.new(File.read(tmux_conf_erb))
File.open(tmux_conf, 'w') do |file|
file.print template.result
end
end
task default: :build
#!/usr/bin/env bash
<% require './keys.rb' %>
<% MovementKeys.keys.each do |key| %>
tmux unbind -n <%= key %>
tmux unbind -n c-<%= key %>
<% end %>
<% PassthroughKeys.each do |key| %>
tmux unbind -n <%= key %>
tmux unbind -n c-<%= key %>
<% end %>
#!/usr/bin/env bash
# Like `tmux select-pane`, but if Vim is running in the current pane it sends
# a `<C-h/j/k/l>` keystroke to Vim instead.
set -e
cmd="$(tmux display -p '#{pane_current_command}')"
cmd="$(basename "$cmd" | tr A-Z a-z)"
if [ "${cmd%m}" = "vi" ]; then
direction="$(echo "${1#-}" | tr 'lLDUR' '\\hjkl')"
# forward the keystroke to Vim
tmux send-keys "C-$direction"
else
tmux select-pane "$@"
fi
@brandonwillard
Copy link

This is great stuff, many thanks!
Any idea why tmux apparently doesn't stop listening for input after ctrl+w+"some key that's not bound"?
For example, given the setup here, if I press ctrl+w+esc+h/j/k/l it will take one of the latter motions. Would be great if the binding failed/stopped listening after esc, or any other unbound key for the ctrl+w sequence.

@brandonwillard
Copy link

Found a hack that seems to work: this tmux.conf change and this script.

@hjdivad
Copy link
Author

hjdivad commented Aug 7, 2014

hey @brandonwillard so i think that's a fundamental problem with the approach: there isn't an easy way to unbind everything except to bind a "reset" to a bunch of keys. The whole thing is a bit of a hack: what's really needed is for tmux to support more complicated key bindings.

That said, the way to avoid the problem today would be to add a third class of bindings: in addition to movement keys and passthrough, you'd want "everything else" to only do a reset and then repeat the key. I suspect that's more work than is worthwhile though.

@droustchev
Copy link

droustchev commented Feb 7, 2018

While trying to achieve something similar, I stumbled upon this gist. Thank you for putting it up here, however I thought there has to be a better way and after some more digging I came up with my solution, which I wanted to share here:

Instead of relying on key bindings that unbind themselves, my solution uses tmux's key-tables. I wasn't able to confirm this 100%, but my solution should work with tmux v1.6+. It works for sure in tmux v2.5, which is the version I'm currently running. It was inspired by this reddit post.

In vim, we'll use the excellent vim-tmux-navigator plugin, but we'll override the default mappings and configure our own:

let g:tmux_navigator_no_mappings=1
nnoremap <silent> <C-w>h :TmuxNavigateLeft<cr>
nnoremap <silent> <C-w>j :TmuxNavigateDown<cr>
nnoremap <silent> <C-w>k :TmuxNavigateUp<cr>
nnoremap <silent> <C-w>l :TmuxNavigateRight<cr>

In tmux, we won't use vim-tmux-navigator, but instead we'll define our own key bindings as follows:

is_vim="ps -o state= -o comm= -t '#{pane_tty}' | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'"
bind-key -n C-w switch-client -Tswitch-pane
bind-key -Tswitch-pane h if-shell "$is_vim" "send-keys C-w h" "select-pane -L"
bind-key -Tswitch-pane j if-shell "$is_vim" "send-keys C-w j" "select-pane -D"
bind-key -Tswitch-pane k if-shell "$is_vim" "send-keys C-w k" "select-pane -U"
bind-key -Tswitch-pane l if-shell "$is_vim" "send-keys C-w l" "select-pane -R"

I re-used the check to find out if we're in vim from the vim-tmux-navigator plugin. The crux is the first bind-key, where we map C-w to switch the client key-table to switch-pane. This is an arbitrarily chosen name. Below that, we define new key bindings in that key-table, by adding the -T flag followed by the name of the key-table, here switch-pane. The rest is similar to how the plugin does it:
we check if we're in vim, if yes, we send the mapping we defined in the .vimrc via send-keys, if not, we execute the appropriate tmux's select-pane command.

Bonus:
Add the following to your tmux status line to display the active key-table unless it's the default root:

#{?#{==:#{client_key_table},root},'',#{client_key_table}}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment