Skip to content

Instantly share code, notes, and snippets.

@hjdivad hjdivad/+Readme.md
Created May 22, 2014

Embed
What would you like to do?

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

This comment has been minimized.

Copy link

brandonwillard commented Jun 18, 2014

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

This comment has been minimized.

Copy link

brandonwillard commented Jun 19, 2014

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

@hjdivad

This comment has been minimized.

Copy link
Owner 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

This comment has been minimized.

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
You can’t perform that action at this time.