Intelligently navigating tmux panes and Vim splits

Suraj N. Kurapati

  1. Problem
    1. Approach
      1. Solution

        Problem

        When you run tmux select-pane inside a zoomed pane, the originally zoomed pane becomes unzoomed. This causes frequent, unintentional pane unzooming during seamless navigation (using the same shortcut keys) of tmux panes and Vim splits.

        To solve this, one might use the g:tmux_navigator_disable_when_zoomed option of the vim-tmux-navigator plugin, but that just exposes another problem: the plugin assumes that both Vim and tmux are running on the very same machine.

        For instance, I often run Vim on remote machines through SSH from my tmux session. Here, the vim-tmux-navigator plugin cannot perform any tmux navigation because the tmux select-pane commands that it runs on my behalf cannot locate my tmux session, which happens to be running on a completely different machine!

        Approach

        We’ll enhance the seamless navigation key bindings to be a bit more intelligent:

        1. If the current pane is zoomed but isn’t running Vim, then do nothing.

        2. If the current pane is zoomed and is also running Vim, then only navigate between Vim splits inside the zoomed pane: don’t venture outside of it.

        3. If the current pane isn’t zoomed, seamlessly navigate between tmux and Vim.

        We can codify these behaviors as a “truth table” composed of four major clauses:

        inside Vim? is Zoomed? Action taken by key binding
        No No Focus directional tmux pane
        No Yes Nothing: ignore key binding
        Yes No Seamlessly focus Vim / tmux
        Yes Yes Focus directional Vim split

        We’ll also place the decision-making authority of where and how to navigate at the hands of tmux (and not at the hands of Vim, as the vim-tmux-navigator does) because we should be able to run and control Vim on/through any remote machines.

        Solution

        This is a pure tmux scripting solution that uses Vim’s titlestring feature to detect when Vim changes focus between its split windows and/or tabs. It doesn’t depend on the assistance of any special Vim plugins, such as vim-tmux-navigator.

        First, add this snippet to your ~/.vimrc file and then restart your Vim.

        " Intelligently navigate tmux panes and Vim splits using the same keys.
        " See https://sunaku.github.io/tmux-select-pane.html for documentation.
        let progname = substitute($VIM, '.*[/\\]', '', '')
        set title titlestring=%{progname}\ %f\ +%l\ #%{tabpagenr()}.%{winnr()}
        if &term =~ '^screen' && !has('nvim') | exe "set t_ts=\e]2; t_fs=\7" | endif
        

        Second, add this snippet to your ~/.tmux.conf file and then reload your tmux.

        # Intelligently navigate tmux panes and Vim splits using the same keys.
        # See https://sunaku.github.io/tmux-select-pane.html for documentation.
        #
        #      +-------------+------------+-----------------------------+
        #      | inside Vim? | is Zoomed? | Action taken by key binding |
        #      +-------------+------------+-----------------------------+
        #      | No          | No         | Focus directional tmux pane |
        #      | No          | Yes        | Nothing: ignore key binding |
        #      | Yes         | No         | Seamlessly focus Vim / tmux |
        #      | Yes         | Yes        | Focus directional Vim split |
        #      +-------------+------------+-----------------------------+
        #
        vim_navigation_timeout=0.05 # number of seconds we give Vim to navigate
        navigate='                                                             \
          pane_is_zoomed() {                                                   \
            test #{window_zoomed_flag} -eq 1;                                  \
          };                                                                   \
          pane_title_changed() {                                               \
            test "#{pane_title}" != "$(tmux display -p "##{pane_title}")";     \
          };                                                                   \
          command_is_vim() {                                                   \
            case "${1%% *}" in                                                 \
              (vi|?vi|vim*|?vim*|view|?view|vi??*) true ;;                     \
              (*) false ;;                                                     \
            esac;                                                              \
          };                                                                   \
          pane_contains_vim() {                                                \
            case "#{=3:pane_current_command}" in                               \
              (ssh|sh) command_is_vim "#{=5:pane_title}" ;;                    \
              (*) command_is_vim "#{=5:pane_current_command}" ;;               \
            esac;                                                              \
          };                                                                   \
          pane_contains_neovim_terminal() {                                    \
            test "#{=12:pane_title}" = "nvim term://";                         \
          };                                                                   \
          navigate() {                                                         \
            tmux_navigation_command=$1;                                        \
            vim_navigation_command=$2;                                         \
            vim_navigation_only_if=${3:-true};                                 \
            if pane_contains_vim && eval "$vim_navigation_only_if"; then       \
              if pane_contains_neovim_terminal; then                           \
                tmux send-keys C-\\ C-n;                                       \
              fi;                                                              \
              eval "$vim_navigation_command";                                  \
              if ! pane_is_zoomed; then                                        \
                sleep $vim_navigation_timeout; : wait for Vim to change title; \
                if ! pane_title_changed; then                                  \
                  eval "$tmux_navigation_command";                             \
                fi;                                                            \
              fi;                                                              \
            elif ! pane_is_zoomed; then                                        \
              eval "$tmux_navigation_command";                                 \
            fi;                                                                \
          };                                                                   \
        navigate '
        navigate_left=" $navigate 'tmux select-pane -L'  'tmux send-keys C-w h'"
        navigate_down=" $navigate 'tmux select-pane -D'  'tmux send-keys C-w j'"
        navigate_up="   $navigate 'tmux select-pane -U'  'tmux send-keys C-w k'"
        navigate_right="$navigate 'tmux select-pane -R'  'tmux send-keys C-w l'"
        navigate_back=" $navigate 'tmux select-pane -l || tmux select-pane -t1'\
                                  'tmux send-keys C-w p'                       \
                                  'pane_is_zoomed'                             "
        
        # QWERTY keys - comment these out if you don't use QWERTY layout!
        bind-key -n M-h run-shell -b "$navigate_left"
        bind-key -n M-j run-shell -b "$navigate_down"
        bind-key -n M-k run-shell -b "$navigate_up"
        bind-key -n M-l run-shell -b "$navigate_right"
        bind-key -n M-\ run-shell -b "$navigate_back"
        
        # Dvorak keys - comment these out if you don't use Dvorak layout!
        bind-key -n M-d run-shell -b "$navigate_back"
        bind-key -n M-h run-shell -b "$navigate_left"
        bind-key -n M-t run-shell -b "$navigate_up"
        bind-key -n M-n run-shell -b "$navigate_down"
        bind-key -n M-s run-shell -b "$navigate_right"
        

        Finally, adjust the key bindings and Vim navigation timeout per your situation.


        Updates