Skip to content

Instantly share code, notes, and snippets.

@mislav
Last active September 28, 2024 23:03
Show Gist options
  • Save mislav/5189704 to your computer and use it in GitHub Desktop.
Save mislav/5189704 to your computer and use it in GitHub Desktop.
tmux-vim integration to transparently switch between tmux panes and vim split windows

I use tmux splits (panes). Inside one of these panes there's a Vim process, and it has its own splits (windows).

In Vim I have key bindings C-h/j/k/l set to switch windows in the given direction. (Vim default mappings for windows switching are the same, but prefixed with C-W.) I'd like to use the same keystrokes for switching tmux panes.

An extra goal that I've solved with a dirty hack is to toggle between last active panes with C-\.

Here's how it should work:

  1. If I'm in "vim window 2", going in left (C-h) or down (C-j) direction should switch windows inside vim.
  2. However, if I'm in "vim window 3", going right (C-l) or down (C-j) should select the next tmux pane in that direction.

The solution

The solution has 3 parts:

  1. In ~/.tmux.conf, I bind the keys I want to execute a custom tmux-vim-select-pane command;
  2. tmux-vim-select-pane checks if the foreground process in the current tmux pane is Vim, then forwards the original keystroke to the vim process. Otherwise it simply switches tmux panes.
  3. In Vim, I set bindings for the same keystrokes to a custom function. The function tries to switch windows in the given direction. If the window didn't change, that means there are no more windows in the given direction inside vim, and it forwards the pane switching command to tmux by shelling out to tmux select-pane.

Installation

  • check tmux -V that you have v1.8

  • get the vim-tmux-navigator plugin

  • additional ~/.tmux.conf config & tmux-vim-select-pane script:

    curl -fsSL https://gist.github.com/mislav/5189704/raw/install.sh | bash -e
curl -fsSL https://gist.github.com/mislav/5189704/raw/tmux.conf \
>> ~/.tmux.conf
curl -fsSL https://raw.github.com/mislav/dotfiles/1500cd2/bin/tmux-vim-select-pane \
-o /usr/local/bin/tmux-vim-select-pane
chmod +x /usr/local/bin/tmux-vim-select-pane
# Smart pane switching with awareness of vim splits
bind -n C-k run-shell 'tmux-vim-select-pane -U'
bind -n C-j run-shell 'tmux-vim-select-pane -D'
bind -n C-h run-shell 'tmux-vim-select-pane -L'
bind -n C-l run-shell 'tmux-vim-select-pane -R'
bind -n "C-\\" run-shell 'tmux-vim-select-pane -l'
# Bring back clear screen under tmux prefix
bind C-l send-keys 'C-l'
@zhaohuaxishi
Copy link

Awesome, thanks @sshbio

@touilfarouk
Copy link

Best tmux config for ubuntu and vim ?

@droustchev
Copy link

I came up with a slightly simpler solution than hjdivad, which allows to use vim's default split navigation bindings C-w {h,j,k,l}, here's the gist.
My solution shows how to easily set up key bindings in tmux that require multiple keys, using key-tables.

@paulrougieux
Copy link

paulrougieux commented May 8, 2020

Seamless navigation between vim and tmux splits is great for interactive languages such as bash, python and R. It helps to navigate back and forth between code and experiments at the interactive prompt in combination with slime. Thank you for this, I'm a happier person today.

@TanishBansal
Copy link

Thank you for this amazing idea, ended up here after searching a while trying to solve this issue for a while.
Here's my solution without any plugins, using CTRL-Arrow-keys for seamless navigation between vim splits and tmux-panes.
CTRL-Arrow-keys can be changed hjkl, change keybindings in .vimrc accordingly.

~/.vimrc :

function! TmuxMove(direction)
        let wnr = winnr()
        silent! execute 'wincmd ' . a:direction
        " If the winnr is still the same after we moved, it is the last pane
        if wnr == winnr()
                call system('tmux select-pane -' . tr(a:direction, 'phjkl', 'lLDUR'))
        end
endfunction

nnoremap <silent> <C-Left> :call TmuxMove('h')<cr>
nnoremap <silent> <C-Down> :call TmuxMove('j')<cr>
nnoremap <silent> <C-Up> :call TmuxMove('k')<cr>
nnoremap <silent> <C-Right> :call TmuxMove('l')<cr>

nnoremap <silent> <C-h> :call TmuxMove('h')<cr>
nnoremap <silent> <C-j> :call TmuxMove('j')<cr>
nnoremap <silent> <C-k> :call TmuxMove('k')<cr>
nnoremap <silent> <C-l> :call TmuxMove('l')<cr>

~/.tmux.conf:

is_vim="ps -o state= -o comm= -t '#{pane_tty}' \
    | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'" 
bind -n C-Up run-shell "if $is_vim ; then tmux send-keys C-k ; else tmux select-pane -U; fi"
bind -n C-Down run-shell "if $is_vim ; then tmux send-keys C-j; else tmux select-pane -D; fi"
bind -n C-Left run-shell "if $is_vim ; then tmux send-keys C-h; else tmux select-pane -L; fi"
bind -n C-Right run-shell "if $is_vim ; then tmux send-keys C-l; else tmux select-pane -R; fi"

@eliasnorrby
Copy link

Thanks! I tweaked some of the suggestions here to allow for one set of bindings to navigate between bspwm nodes/tmux panes/vim windows (76c2d72).

@keks24
Copy link

keks24 commented Sep 27, 2022

In .tmux.conf:

bind -n C-k if "[ $(tmux display -p '#{pane_current_command}') = vim ]" "send-keys C-k" "select-pane -U"
bind -n C-j if "[ $(tmux display -p '#{pane_current_command}') = vim ]" "send-keys C-j" "select-pane -D"
bind -n C-h if "[ $(tmux display -p '#{pane_current_command}') = vim ]" "send-keys C-h" "select-pane -L"
bind -n C-l if "[ $(tmux display -p '#{pane_current_command}') = vim ]" "send-keys C-l" "select-pane -R"

I optimised this one in order to open as less subshells as possible. It also does a pseudo substring match by substituting the regular expression g?(view|n?vim?x?)(diff)?$ from the current command. This should be compatible with vi, vim, nvim and their variants:

bind -n C-k if-shell "[ '#{pane_current_command}' != '#{s/g?(view|n?vim?x?)(diff)?$//:#{pane_current_command}}' ]" "send-keys C-k" "select-pane -U"
bind -n C-j if-shell "[ '#{pane_current_command}' != '#{s/g?(view|n?vim?x?)(diff)?$//:#{pane_current_command}}' ]" "send-keys C-j" "select-pane -D"
bind -n C-h if-shell "[ '#{pane_current_command}' != '#{s/g?(view|n?vim?x?)(diff)?$//:#{pane_current_command}}' ]" "send-keys C-h" "select-pane -L"
bind -n C-l if-shell "[ '#{pane_current_command}' != '#{s/g?(view|n?vim?x?)(diff)?$//:#{pane_current_command}}' ]" "send-keys C-l" "select-pane -R"

There is still an issue, when the window panes are synchronised. C-l clears adjacent, open shells and C-j presses enter, which can be very dangerous.

@007kevin
Copy link

Sharing my emacs version with variable pane_current_command (e.g emacs, Emacs, Emacs-v1)

bind -n C-p if "[[ $(tmux display -p '#{pane_current_command}') = [Ee]macs* ]]" "send-keys C-p" "copy-mode; send-keys C-p"
bind -n C-n if "[[ $(tmux display -p '#{pane_current_command}') = [Ee]macs* ]]" "send-keys C-n" "copy-mode; send-keys C-n"
bind -n M-v if "[[ $(tmux display -p '#{pane_current_command}') = [Ee]macs* ]]" "send-keys M-v" "copy-mode; send-keys M-v"
bind -n C-Space if "[[ $(tmux display -p '#{pane_current_command}') = [Ee]macs* ]]" "send-keys C-Space" "copy-mode; send-keys C-Space"

When emacs is the current pane, movement keys will behave as normal. When pane is not emacs, you navigate in copy-mode with the same emacs bindings.

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