Intelligently navigating tmux panes and Vim splits
NOTE: The code in this article is also available as an installable, dual tmux and Vim plugin at https://github.com/sunaku/tmux-navigate.
Problem
Running tmux select-pane in a zoomed tmux pane causes it to become unzoomed.
This causes frequent, unintentional pane unzooming when I perform seamless
navigation (which refers to using the same shortcut keys to navigate
tmux panes and Vim splits alike) as provided by the vim-tmux-navigator plugin
for Vim.
Thankfully, I learned that the plugin contains an undocumented option to disable pane
unzooming, called g:tmux_navigator_disable_when_zoomed.  However, this victory
was short-lived as I soon discovered that the plugin doesn’t work when Vim is
running remotely, tunneled in from a different machine through SSH.
For example, I often have tmux panes that run Vim on remote machines through SSH
(sometimes several layers deep!).  Here, the plugin fails to navigate because the
tmux select-pane commands that it runs on my behalf (from Vim) can’t locate
my tmux session, which happens to be running on a completely different machine!
Approach
First, we’ll enhance the seamless navigation shortcuts to be more intelligent:
- If the current pane is zoomed but isn’t running Vim, then do nothing. 
- 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. 
- If the current pane is unzoomed, 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 | 
Second, we’ll place the decision-making authority of where and how to navigate in tmux’s hands (and not in Vim’s hands, as the vim-tmux-navigator plugin does) because we should be able to run and control Vim on/through any remote machines.
To achieve this, we’ll inspect #{pane_current_command} to discover the local
commands currently running inside tmux panes.  And for panes running SSH, we’ll
inspect #{pane_title} to help us detect which remote command they’re
currently running, since their local command is always going to be an
uninformative “ssh”.
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, like 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_title="#{q:pane_title}";                                        \
  pane_current_command="#{q:pane_current_command}";                    \
  pane_is_zoomed() {                                                   \
    test #{window_zoomed_flag} -eq 1;                                  \
  };                                                                   \
  pane_title_changed() {                                               \
    test "$pane_title" != "$(tmux display -p "##{q:pane_title}")";     \
  };                                                                   \
  command_is_vim() {                                                   \
    case "${1%% *}" in                                                 \
      (vi|?vi|vim*|?vim*|view|?view|vi??*) true ;;                     \
      (*) false ;;                                                     \
    esac;                                                              \
  };                                                                   \
  pane_contains_vim() {                                                \
    case "$pane_current_command" in                                    \
      (git|*sh) command_is_vim "$pane_title" ;;                        \
      (*) command_is_vim "$pane_current_command" ;;                    \
    esac;                                                              \
  };                                                                   \
  pane_contains_neovim_terminal() {                                    \
    case "$pane_title" in                                              \
      (nvim?term://*) true ;;                                          \
      (*) false ;;                                                     \
    esac;                                                              \
  };                                                                   \
  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                                  \
          tmux send-keys BSpace;                                       \
          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.