Using the same keys to select tmux panes and Vim splits

Suraj N. Kurapati

  1. Problem
    1. Approach
      1. Solution
        1. Dvorak layout (D-H-T-N-S)
          1. Standard layout (H-J-K-L-;)
          2. Explanation

            Problem

            When you run tmux select-pane inside a zoomed pane, to select a different pane, the originally zoomed pane becomes unzoomed. When this behavior is coupled with vim-tmux-navigator‘s power to seamlessly select tmux panes and Vim splits alike using the same key bindings, it makes unintentional pane unzooming a woefully frequent occasion.

            Approach

            To solve this problem, we’ll change our selection key bindings to behave nonlinearly, as follows:

            1. When the current pane is zoomed, do nothing.

            2. When the current pane is zoomed and running Vim, only select Vim splits inside the current pane: do not venture beyond it.

            3. When the current pane is unzoomed, seamlessly navigate between tmux panes and Vim splits alike using vim-tmux-navigator.

            Since the desired behavior has three cases, we can simply encode them as a pair of bits which together compose an integer value between 0 and 3:

            Encoding Bit pair Pane is Vim? Pane is zoomed? Action
            0 00 No No Select tmux pane.
            1 01 No Yes Do nothing.
            2 10 Yes No Use vim-tmux-navigator.
            3 11 Yes Yes Select Vim split.

            Now, we can simply store these cases in a lookup table and then index it using the encoded value we formed from our bit pair, as described above.

            Solution

            Dvorak layout (D-H-T-N-S)

            In your ~/.vimrc file:

            nnoremap <silent> <A-d> :TmuxNavigatePrevious<Return>
            nnoremap <silent> <A-h> :TmuxNavigateLeft<Return>
            nnoremap <silent> <A-t> :TmuxNavigateUp<Return>
            nnoremap <silent> <A-n> :TmuxNavigateDown<Return>
            nnoremap <silent> <A-s> :TmuxNavigateRight<Return>
            

            In your ~/.tmux.conf file:

            # Focus the directional pane (or split if inside Vim) unless maximized.
            # See https://sunaku.github.io/tmux-select-pane.html for documentation.
            #
            # +-------------+------------+----------+-----------------------------+
            # | inside Vim? | Maximized? | Encoding | Action taken by key binding |
            # +-------------+------------+----------+-----------------------------+
            # | no  (0)     | no  (0)    | 00       | focus directional tmux pane |
            # | no  (0)     | yes (1)    | 01       | nothing: ignore key binding |
            # | yes (1)     | no  (0)    | 10       | delegate vim-tmux-navigator |
            # | yes (1)     | yes (1)    | 11       | focus directional Vim split |
            # +-------------+------------+----------+-----------------------------+
            
            bind-key -n M-d run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (11) tmux send-keys C-w p                           ;; \
              (??) tmux select-pane -l || tmux select-pane -t 1   ;; \
              esac                                                   \
            '
            
            bind-key -n M-h run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (00) tmux select-pane -L                            ;; \
              (10) tmux send-keys M-h                             ;; \
              (11) tmux send-keys C-w h                           ;; \
              esac                                                   \
            '
            
            bind-key -n M-t run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (00) tmux select-pane -U                            ;; \
              (10) tmux send-keys M-t                             ;; \
              (11) tmux send-keys C-w k                           ;; \
              esac                                                   \
            '
            
            bind-key -n M-n run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (00) tmux select-pane -D                            ;; \
              (10) tmux send-keys M-n                             ;; \
              (11) tmux send-keys C-w j                           ;; \
              esac                                                   \
            '
            
            bind-key -n M-s run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (00) tmux select-pane -R                            ;; \
              (10) tmux send-keys M-s                             ;; \
              (11) tmux send-keys C-w l                           ;; \
              esac                                                   \
            '
            

            Standard layout (H-J-K-L-;)

            In your ~/.vimrc file:

            nnoremap <silent> <A-h> :TmuxNavigateLeft<Return>
            nnoremap <silent> <A-j> :TmuxNavigateDown<Return>
            nnoremap <silent> <A-k> :TmuxNavigateUp<Return>
            nnoremap <silent> <A-l> :TmuxNavigateRight<Return>
            nnoremap <silent> <A-;> :TmuxNavigatePrevious<Return>
            

            In your ~/.tmux.conf file:

            # Focus the directional pane (or split if inside Vim) unless maximized.
            # See https://sunaku.github.io/tmux-select-pane.html for documentation.
            #
            # +-------------+------------+----------+-----------------------------+
            # | inside Vim? | Maximized? | Encoding | Action taken by key binding |
            # +-------------+------------+----------+-----------------------------+
            # | no  (0)     | no  (0)    | 00       | focus directional tmux pane |
            # | no  (0)     | yes (1)    | 01       | nothing: ignore key binding |
            # | yes (1)     | no  (0)    | 10       | delegate vim-tmux-navigator |
            # | yes (1)     | yes (1)    | 11       | focus directional Vim split |
            # +-------------+------------+----------+-----------------------------+
            
            bind-key -n M-h run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (00) tmux select-pane -L                            ;; \
              (10) tmux send-keys M-h                             ;; \
              (11) tmux send-keys C-w h                           ;; \
              esac                                                   \
            '
            
            bind-key -n M-j run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (00) tmux select-pane -D                            ;; \
              (10) tmux send-keys M-j                             ;; \
              (11) tmux send-keys C-w j                           ;; \
              esac                                                   \
            '
            
            bind-key -n M-k run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (00) tmux select-pane -U                            ;; \
              (10) tmux send-keys M-k                             ;; \
              (11) tmux send-keys C-w k                           ;; \
              esac                                                   \
            '
            
            bind-key -n M-l run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (00) tmux select-pane -R                            ;; \
              (10) tmux send-keys M-l                             ;; \
              (11) tmux send-keys C-w l                           ;; \
              esac                                                   \
            '
            
            bind-key -n M-; run-shell                              ' \
              case "#{pane_current_command}" in (*vi*) vim=1;; esac; \
              case "${vim:-0}#{window_zoomed_flag}" in               \
              (11) tmux send-keys C-w p                           ;; \
              (??) tmux select-pane -l || tmux select-pane -t 1   ;; \
              esac                                                   \
            '
            

            Explanation

            Let’s examine the long, one-line shell commands in the tmux key bindings above.

            First, we record whether the current pane is zoomed in the lower bit:

            test #{window_zoomed_flag} -eq 0;
            max=$?;
            

            Second, we record whether the current pane is running Vim in the upper bit:

            cmd="#{pane_current_command}";
            test -n "${cmd#*vim}";
            vim=$?;
            

            Next, we build a lookup table. Since POSIX shell lacks arrays, as Rich Felker explains in “Working with arrays”, we shall use the command-line argument list as a makeshift array:

            # Encoding:       0           1            2                     3
            # Bit pair:      00          01           10                    11
            set -- "tmux select-pane -L" "" "tmux send-keys M-h" "tmux send-keys C-w h";
            

            Now we can index the lookup table using the encoded value by shifting (removing from the front of the array) that number of items to reach the command we are trying to index and then finally run the indexed command:

            shift $(( 2*vim + max ));
            eval "$1"
            

            When this happens, the appropriate tmux pane selection action is performed either directly by tmux or through the synthetic keystrokes sent to vim-tmux-navigator.

            That’s all! Enjoy the power of binary encoding and nonlinearity. :-)


            Updates