├── .github └── workflows │ └── main.yml ├── .gitignore ├── License.md ├── README.md ├── doc └── tmux-navigator.txt ├── pattern-check ├── plugin └── tmux_navigator.vim └── vim-tmux-navigator.tmux /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | test: 10 | name: Unit tests on ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - macos-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Check bash shell version 21 | run: bash --version 22 | - name: Unit tests (file pattern-check) 23 | run: TERM='xterm-256color' bash pattern-check 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Chris Toomey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Vim Tmux Navigator 2 | ================== 3 | 4 | [![GitHub Actions](https://img.shields.io/github/actions/workflow/status/christoomey/vim-tmux-navigator/main.yml?style=flat-square&logo=github)](https://github.com/christoomey/vim-tmux-navigator/actions/workflows/main.yml) 5 | 6 | This plugin is a repackaging of [Mislav Marohnić's](https://mislav.net/) tmux-navigator 7 | configuration described in [this gist][]. When combined with a set of tmux 8 | key bindings, the plugin will allow you to navigate seamlessly between 9 | vim and tmux splits using a consistent set of hotkeys. 10 | 11 | **NOTE**: This requires tmux v1.8 or higher. 12 | 13 | Usage 14 | ----- 15 | 16 | This plugin provides the following mappings which allow you to move between 17 | Vim panes and tmux splits seamlessly. 18 | 19 | - `` => Left 20 | - `` => Down 21 | - `` => Up 22 | - `` => Right 23 | - `` => Previous split 24 | 25 | **Note** - you don't need to use your tmux `prefix` key sequence before using 26 | the mappings. 27 | 28 | If you want to use alternate key mappings, see the [configuration section 29 | below][]. 30 | 31 | Installation 32 | ------------ 33 | 34 | ### Vim 35 | 36 | If you don't have a preferred installation method, I recommend using [Vundle][]. 37 | Assuming you have Vundle installed and configured, the following steps will 38 | install the plugin: 39 | 40 | Add the following line to your `~/.vimrc` file 41 | 42 | ``` vim 43 | Plugin 'christoomey/vim-tmux-navigator' 44 | ``` 45 | 46 | Then run 47 | 48 | ``` 49 | :PluginInstall 50 | ``` 51 | 52 | If you are using Vim 8+, you don't need any plugin manager. Simply clone this repository inside `~/.vim/pack/plugin/start/` directory and restart Vim. 53 | 54 | ``` 55 | git clone git@github.com:christoomey/vim-tmux-navigator.git ~/.vim/pack/plugins/start/vim-tmux-navigator 56 | ``` 57 | 58 | ### lazy.nvim 59 | 60 | If you are using [lazy.nvim](https://github.com/folke/lazy.nvim). Add the following plugin to your configuration. 61 | 62 | ```lua 63 | { 64 | "christoomey/vim-tmux-navigator", 65 | cmd = { 66 | "TmuxNavigateLeft", 67 | "TmuxNavigateDown", 68 | "TmuxNavigateUp", 69 | "TmuxNavigateRight", 70 | "TmuxNavigatePrevious", 71 | "TmuxNavigatorProcessList", 72 | }, 73 | keys = { 74 | { "", "TmuxNavigateLeft" }, 75 | { "", "TmuxNavigateDown" }, 76 | { "", "TmuxNavigateUp" }, 77 | { "", "TmuxNavigateRight" }, 78 | { "", "TmuxNavigatePrevious" }, 79 | }, 80 | } 81 | ``` 82 | 83 | Then, restart Neovim and lazy.nvim will automatically install the plugin and configure the keybindings. 84 | 85 | ### tmux 86 | 87 | To configure the tmux side of this customization there are two options: 88 | 89 | #### Add a snippet 90 | 91 | Add the following to your `~/.tmux.conf` file: 92 | 93 | ``` tmux 94 | # Smart pane switching with awareness of Vim splits. 95 | # See: https://github.com/christoomey/vim-tmux-navigator 96 | vim_pattern='(\S+/)?g?\.?(view|l?n?vim?x?|fzf)(diff)?(-wrapped)?' 97 | is_vim="ps -o state= -o comm= -t '#{pane_tty}' \ 98 | | grep -iqE '^[^TXZ ]+ +${vim_pattern}$'" 99 | bind-key -n 'C-h' if-shell "$is_vim" 'send-keys C-h' 'select-pane -L' 100 | bind-key -n 'C-j' if-shell "$is_vim" 'send-keys C-j' 'select-pane -D' 101 | bind-key -n 'C-k' if-shell "$is_vim" 'send-keys C-k' 'select-pane -U' 102 | bind-key -n 'C-l' if-shell "$is_vim" 'send-keys C-l' 'select-pane -R' 103 | tmux_version='$(tmux -V | sed -En "s/^tmux ([0-9]+(.[0-9]+)?).*/\1/p")' 104 | if-shell -b '[ "$(echo "$tmux_version < 3.0" | bc)" = 1 ]' \ 105 | "bind-key -n 'C-\\' if-shell \"$is_vim\" 'send-keys C-\\' 'select-pane -l'" 106 | if-shell -b '[ "$(echo "$tmux_version >= 3.0" | bc)" = 1 ]' \ 107 | "bind-key -n 'C-\\' if-shell \"$is_vim\" 'send-keys C-\\\\' 'select-pane -l'" 108 | 109 | bind-key -T copy-mode-vi 'C-h' select-pane -L 110 | bind-key -T copy-mode-vi 'C-j' select-pane -D 111 | bind-key -T copy-mode-vi 'C-k' select-pane -U 112 | bind-key -T copy-mode-vi 'C-l' select-pane -R 113 | bind-key -T copy-mode-vi 'C-\' select-pane -l 114 | ``` 115 | 116 | #### TPM 117 | 118 | If you prefer, you can use the Tmux Plugin Manager ([TPM][]) instead of 119 | copying the snippet. 120 | When using TPM, add the following lines to your ~/.tmux.conf: 121 | 122 | ``` tmux 123 | set -g @plugin 'christoomey/vim-tmux-navigator' 124 | ``` 125 | 126 | To set a different key-binding, use the plugin configuration settings 127 | (remember to update your vim config accordingly). 128 | Multiple key bindings are possible, use a space to separate. 129 | 130 | ``` tmux 131 | set -g @vim_navigator_mapping_left "C-Left C-h" # use C-h and C-Left 132 | set -g @vim_navigator_mapping_right "C-Right C-l" 133 | set -g @vim_navigator_mapping_up "C-k" 134 | set -g @vim_navigator_mapping_down "C-j" 135 | set -g @vim_navigator_mapping_prev "" # removes the C-\ binding 136 | ``` 137 | 138 | To disable the automatic mapping of ` C-l` to `send C-l` (which is 139 | intended to restore the "clear screen" functionality): 140 | 141 | ```tmux 142 | set -g @vim_navigator_prefix_mapping_clear_screen "" 143 | ``` 144 | 145 | Don't forget to run tpm: 146 | 147 | ``` tmux 148 | run '~/.tmux/plugins/tpm/tpm' 149 | ``` 150 | 151 | Thanks to Christopher Sexton who provided the updated tmux configuration in 152 | [this blog post][]. 153 | 154 | Configuration 155 | ------------- 156 | 157 | ### Custom Key Bindings 158 | 159 | If you don't want the plugin to create any mappings, you can use the five 160 | provided functions to define your own custom maps. You will need to define 161 | custom mappings in your `~/.vimrc` as well as update the bindings in tmux to 162 | match. 163 | 164 | #### Vim 165 | 166 | Add the following to your `~/.vimrc` to define your custom maps: 167 | 168 | ``` vim 169 | let g:tmux_navigator_no_mappings = 1 170 | 171 | nnoremap {Left-Mapping} :TmuxNavigateLeft 172 | nnoremap {Down-Mapping} :TmuxNavigateDown 173 | nnoremap {Up-Mapping} :TmuxNavigateUp 174 | nnoremap {Right-Mapping} :TmuxNavigateRight 175 | nnoremap {Previous-Mapping} :TmuxNavigatePrevious 176 | ``` 177 | 178 | *Note* Each instance of `{Left-Mapping}` or `{Down-Mapping}` must be replaced 179 | in the above code with the desired mapping. Ie, the mapping for `` => 180 | Left would be created with `nnoremap :TmuxNavigateLeft`. 181 | 182 | ##### Autosave on leave 183 | 184 | You can configure the plugin to write the current buffer, or all buffers, when 185 | navigating from Vim to tmux. This functionality is exposed via the 186 | `g:tmux_navigator_save_on_switch` variable, which can have either of the 187 | following values: 188 | 189 | Value | Behavior 190 | ------ | ------ 191 | 1 | `:update` (write the current buffer, but only if changed) 192 | 2 | `:wall` (write all buffers) 193 | 194 | To enable this, add the following (with the desired value) to your ~/.vimrc: 195 | 196 | ```vim 197 | " Write all buffers before navigating from Vim to tmux pane 198 | let g:tmux_navigator_save_on_switch = 2 199 | ``` 200 | 201 | ##### Disable While Zoomed 202 | 203 | By default, if you zoom the tmux pane running Vim and then attempt to navigate 204 | "past" the edge of the Vim session, tmux will unzoom the pane. This is the 205 | default tmux behavior, but may be confusing if you've become accustomed to 206 | navigation "wrapping" around the sides due to this plugin. 207 | 208 | We provide an option, `g:tmux_navigator_disable_when_zoomed`, which can be used 209 | to disable this unzooming behavior, keeping all navigation within Vim until the 210 | tmux pane is explicitly unzoomed. 211 | 212 | To disable navigation when zoomed, add the following to your ~/.vimrc: 213 | 214 | ```vim 215 | " Disable tmux navigator when zooming the Vim pane 216 | let g:tmux_navigator_disable_when_zoomed = 1 217 | ``` 218 | 219 | ##### Preserve Zoom 220 | 221 | As noted above, navigating from a Vim pane to another tmux pane normally causes 222 | the window to be unzoomed. Some users may prefer the behavior of tmux's `-Z` 223 | option to `select-pane`, which keeps the window zoomed if it was zoomed. To 224 | enable this behavior, set the `g:tmux_navigator_preserve_zoom` option to `1`: 225 | 226 | ```vim 227 | " If the tmux window is zoomed, keep it zoomed when moving from Vim to another pane 228 | let g:tmux_navigator_preserve_zoom = 1 229 | ``` 230 | 231 | Naturally, if `g:tmux_navigator_disable_when_zoomed` is enabled, this option 232 | will have no effect. 233 | 234 | #### Tmux 235 | 236 | Alter each of the five lines of the tmux configuration listed above to use your 237 | custom mappings. **Note** each line contains two references to the desired 238 | mapping. 239 | 240 | ### Additional Customization 241 | 242 | #### Ignoring programs that use Ctrl+hjkl movement 243 | 244 | In interactive programs such as FZF or the built-in Vim terminal, Ctrl+hjkl can be used instead of the arrow keys to move the selection up and down. If vim-tmux-navigator is getting in your way trying to change the active window instead, you can make it be ignored and work as if this plugin were not enabled. Just modify the `is_vim` variable(that you have either on the snipped you pasted on `~/.tmux.conf` or on the `vim-tmux-navigator.tmux` file). For example, to add the program `foobar`: 245 | 246 | ```diff 247 | - vim_pattern='(\S+/)?g?\.?(view|l?n?vim?x?|fzf)(diff)?(-wrapped)?' 248 | + vim_pattern='(\S+/)?g?\.?(view|l?n?vim?x?|fzf|foobar)(diff)?(-wrapped)?' 249 | is_vim="ps -o state= -o comm= -t '#{pane_tty}' \ 250 | | grep -iqE '^[^TXZ ]+ +${vim_pattern}$'" 251 | ``` 252 | 253 | #### Restoring Clear Screen (C-l) 254 | 255 | The default key bindings include `` which is the readline key binding 256 | for clearing the screen. The following binding can be added to your `~/.tmux.conf` file to provide an alternate mapping to `clear-screen`. 257 | 258 | ``` tmux 259 | bind C-l send-keys 'C-l' 260 | ``` 261 | 262 | With this enabled you can use ` C-l` to clear the screen. 263 | 264 | Thanks to [Brian Hogan][] for the tip on how to re-map the clear screen binding. 265 | 266 | #### Restoring SIGQUIT (C-\\) 267 | 268 | The default key bindings also include `` which is the default method of 269 | sending SIGQUIT to a foreground process. Similar to "Clear Screen" above, a key 270 | binding can be created to replicate SIGQUIT in the prefix table. 271 | 272 | ``` tmux 273 | bind C-\\ send-keys 'C-\' 274 | ``` 275 | 276 | Alternatively, you can exclude the previous pane key binding from your `~/.tmux.conf`. If using TPM, the following line can be used to unbind the previous pane binding set by the plugin. 277 | 278 | ``` tmux 279 | unbind -n C-\\ 280 | ``` 281 | 282 | #### Disable Wrapping 283 | 284 | By default, if you try to move past the edge of the screen, tmux/vim will 285 | "wrap" around to the opposite side. To disable this, you'll need to 286 | configure both tmux and vim: 287 | 288 | For vim, you only need to enable this option: 289 | ```vim 290 | let g:tmux_navigator_no_wrap = 1 291 | ``` 292 | 293 | Tmux doesn't have an option, so whatever key bindings you have need to be set 294 | to conditionally wrap based on position on screen: 295 | 296 | ```tmux 297 | vim_pattern='(\S+/)?g?\.?(view|l?n?vim?x?|fzf)(diff)?(-wrapped)?' 298 | is_vim="ps -o state= -o comm= -t '#{pane_tty}' \ 299 | | grep -iqE '^[^TXZ ]+ +${vim_pattern}$'" 300 | bind-key -n 'C-h' if-shell "$is_vim" { send-keys C-h } { if-shell -F '#{pane_at_left}' {} { select-pane -L } } 301 | bind-key -n 'C-j' if-shell "$is_vim" { send-keys C-j } { if-shell -F '#{pane_at_bottom}' {} { select-pane -D } } 302 | bind-key -n 'C-k' if-shell "$is_vim" { send-keys C-k } { if-shell -F '#{pane_at_top}' {} { select-pane -U } } 303 | bind-key -n 'C-l' if-shell "$is_vim" { send-keys C-l } { if-shell -F '#{pane_at_right}' {} { select-pane -R } } 304 | 305 | bind-key -T copy-mode-vi 'C-h' if-shell -F '#{pane_at_left}' {} { select-pane -L } 306 | bind-key -T copy-mode-vi 'C-j' if-shell -F '#{pane_at_bottom}' {} { select-pane -D } 307 | bind-key -T copy-mode-vi 'C-k' if-shell -F '#{pane_at_top}' {} { select-pane -U } 308 | bind-key -T copy-mode-vi 'C-l' if-shell -F '#{pane_at_right}' {} { select-pane -R } 309 | ``` 310 | 311 | #### Nesting 312 | If you like to nest your tmux sessions, this plugin is not going to work 313 | properly. It probably never will, as it would require detecting when Tmux would 314 | wrap from one outermost pane to another and propagating that to the outer 315 | session. 316 | 317 | By default this plugin works on the outermost tmux session and the vim 318 | sessions it contains, but you can customize the behaviour by adding more 319 | commands to the expression used by the grep command. 320 | 321 | When nesting tmux sessions via ssh or mosh, you could extend it to look like 322 | `'(^|\/)g?(view|vim|ssh|mosh?)(diff)?$'`, which makes this plugin work within 323 | the innermost tmux session and the vim sessions within that one. This works 324 | better than the default behaviour if you use the outer Tmux sessions as relays 325 | to different hosts and have all instances of vim on remote hosts. 326 | 327 | Similarly, if you like to nest tmux locally, add `|tmux` to the expression. 328 | 329 | This behaviour means that you can't leave the innermost session with Ctrl-hjkl 330 | directly. These following fallback mappings can be targeted to the right Tmux 331 | session by escaping the prefix (Tmux' `send-prefix` command). 332 | 333 | ``` tmux 334 | bind -r C-h run "tmux select-pane -L" 335 | bind -r C-j run "tmux select-pane -D" 336 | bind -r C-k run "tmux select-pane -U" 337 | bind -r C-l run "tmux select-pane -R" 338 | bind -r C-\ run "tmux select-pane -l" 339 | ``` 340 | 341 | Another workaround is to configure tmux on the outer machine to send keys to 342 | the inner tmux session: 343 | 344 | ``` 345 | bind-key -n 'M-h' 'send-keys c-h' 346 | bind-key -n 'M-j' 'send-keys c-j' 347 | bind-key -n 'M-k' 'send-keys c-k' 348 | bind-key -n 'M-l' 'send-keys c-l' 349 | ``` 350 | 351 | Here we bind "meta" key (aka "alt" or "option" key) combinations for each of 352 | the four directions and send those along to the innermost session via 353 | `send-keys`. You use the normal `C-h,j,k,l` while in the outermost session and 354 | the alternative bindings to navigate the innermost session. Note that if you 355 | use the example above on a Mac, you may need to configure your terminal app to 356 | get the option key to work like a normal meta key. Consult your terminal app's 357 | manual for details. 358 | 359 | A third possible solution is to manually prevent the outermost tmux session 360 | from intercepting the navigation keystrokes by disabling the prefix table: 361 | 362 | ``` 363 | set -g pane-active-border-style 'fg=#000000,bg=#ffff00' 364 | bind -T root F12 \ 365 | set prefix None \;\ 366 | set key-table off \;\ 367 | if -F '#{pane_in_mode}' 'send-keys -X cancel' \;\ 368 | set -g pane-active-border-style 'fg=#000000,bg=#00ff00' 369 | refresh-client -S \;\ 370 | 371 | bind -T off F12 \ 372 | set -u prefix \;\ 373 | set -u key-table \;\ 374 | set -g pane-active-border-style 'fg=#000000,bg=#ffff00' 375 | refresh-client -S 376 | ``` 377 | 378 | This code, added to the machine running the outermost tmux session, toggles the 379 | outermost prefix table on and off with the `F12` key. When off, the active 380 | pane's border changes to green to indicate that the inner session receives 381 | navigation keystrokes. When toggled back on, the border returns to yellow and 382 | normal operation resumes and the outermost responds to the nav keystrokes. 383 | 384 | The code example above also toggles the prefix key (ctrl-b by default) for the 385 | outer session so that same prefix can be temporarily used on the inner session 386 | instead of having to use a different prefix (ctrl-a by default) which you may 387 | find convenient. If not, simply remove the lines that set/unset the prefix key 388 | from the code example above. 389 | 390 | #### netrw 391 | 392 | Vim's builtin file explorer, named the netrw plugin, has a default keymapping 393 | for ``. When using `vim-tmux-navigator` with default settings, 394 | `vim-tmux-navigator` will try to override the netrw mapping so that `` will 395 | still be mapped to `:TmuxNavigateRight` as it is for other buffers. If you 396 | prefer to keep the netrw mapping, set this variable in your vimrc: 397 | 398 | ``` vim 399 | let g:tmux_navigator_disable_netrw_workaround = 1 400 | ``` 401 | 402 | Alternatively, if you prefer to work around the issue yourself, you can add the 403 | following to your vimrc: 404 | 405 | ``` vim 406 | let g:tmux_navigator_disable_netrw_workaround = 1 407 | " g:Netrw_UserMaps is a list of lists. If you'd like to add other key mappings, 408 | " just add them like so: [['a', 'command1'], ['b', 'command2'], ...] 409 | let g:Netrw_UserMaps = [['', 'TmuxNavigateRight']] 410 | ``` 411 | 412 | Troubleshooting 413 | --------------- 414 | 415 | ### Vim -> Tmux doesn't work! 416 | 417 | This is likely due to conflicting key mappings in your `~/.vimrc`. You can check 418 | this by running `:verbose nmap ` (similar for each of the key bindings). 419 | You should see vim-tmux-runner as the source listed for the key binding, but if 420 | you see something else, you've got a conflict and will need to remove the other 421 | key binding or otherwise restructure your vim config. 422 | 423 | Another option is that the pattern matching included in the `.tmux.conf` is 424 | not recognizing that Vim is active. To check that tmux is properly recognizing 425 | Vim, use the provided Vim command `:TmuxNavigatorProcessList`. The output of 426 | that command should be a list like: 427 | 428 | ``` 429 | Ss -zsh 430 | S+ vim 431 | S+ tmux 432 | ``` 433 | 434 | If you encounter a different output please [open an issue][] with as much info 435 | about your OS, Vim version, and tmux version as possible. 436 | 437 | [open an issue]: https://github.com/christoomey/vim-tmux-navigator/issues/new 438 | 439 | ### Tmux Can't Tell if Vim Is Active 440 | 441 | This functionality requires tmux version 1.8 or higher. You can check your 442 | version to confirm with this shell command: 443 | 444 | ``` bash 445 | tmux -V # should return 'tmux 1.8' 446 | ``` 447 | 448 | ### Switching out of Vim Is Slow 449 | 450 | If you find that navigation within Vim (from split to split) is fine, but Vim 451 | to a non-Vim tmux pane is delayed, it might be due to a slow shell startup. 452 | Consider moving code from your shell's non-interactive rc file (e.g., 453 | `~/.zshenv`) into the interactive startup file (e.g., `~/.zshrc`) as Vim only 454 | sources the non-interactive config. 455 | 456 | ### It doesn't work in Vim's `terminal` mode 457 | 458 | Terminal mode is now supported :) 459 | 460 | ### It Doesn't Work in tmate 461 | 462 | [tmate][] is a tmux fork that aids in setting up remote pair programming 463 | sessions. It is designed to run alongside tmux without issue, but occasionally 464 | there are hiccups. Specifically, if the versions of tmux and tmate don't match, 465 | you can have issues. See [this 466 | issue](https://github.com/christoomey/vim-tmux-navigator/issues/27) for more 467 | detail. 468 | 469 | [tmate]: http://tmate.io/ 470 | 471 | ### Switching between host panes doesn't work when docker is running 472 | 473 | Images built from minimalist OSes may not have the `ps` command or have a 474 | simpler version of the command that is not compatible with this plugin. 475 | Try installing the `procps` package using the appropriate package manager 476 | command. For Alpine, you would do `apk add procps`. 477 | 478 | If this doesn't solve your problem, you can also try the following: 479 | 480 | Replace the `is_vim` variable in your `~/.tmux.conf` file with: 481 | ```tmux 482 | vim_pattern='(\S+/)?g?\.?(view|l?n?vim?x?|fzf)(diff)?(-wrapped)?' 483 | if-shell '[ -f /.dockerenv ]' \ 484 | "is_vim=\"ps -o state=,comm= -t '#{pane_tty}' \ 485 | | grep -iqE '^[^TXZ ]+ +${vim_pattern}$'\"" 486 | # Filter out docker instances of nvim from the host system to prevent 487 | # host from thinking nvim is running in a pseudoterminal when its not. 488 | "is_vim=\"ps -o state=,comm=,cgroup= -t '#{pane_tty}' \ 489 | | grep -ivE '^.+ +.+ +.+\\/docker\\/.+$' \ 490 | | grep -iqE '^[^TXZ ]+ +${vim_pattern}$'\"" 491 | ``` 492 | 493 | Details: The output of the ps command on the host system includes processes 494 | running within containers, but containers have their own instances of 495 | /dev/pts/\*. vim-tmux-navigator relies on /dev/pts/\* to determine if vim is 496 | running, so if vim is running in say /dev/pts/ in a container and there is a 497 | tmux pane (not running vim) in /dev/pts/ on the host system, then without 498 | the patch above vim-tmux-navigator will think vim is running when its not. 499 | 500 | ### It Still Doesn't Work!!! 501 | 502 | The tmux configuration uses an inlined grep pattern match to help determine if 503 | the current pane is running Vim. If you run into any issues with the navigation 504 | not happening as expected, you can try using [Mislav's original external 505 | script][] which has a more robust check. 506 | 507 | [Brian Hogan]: https://twitter.com/bphogan 508 | [Mislav's original external script]: https://github.com/mislav/dotfiles/blob/master/bin/tmux-vim-select-pane 509 | [Vundle]: https://github.com/gmarik/vundle 510 | [TPM]: https://github.com/tmux-plugins/tpm 511 | [configuration section below]: #custom-key-bindings 512 | [this blog post]: http://www.codeography.com/2013/06/19/navigating-vim-and-tmux-splits 513 | [this gist]: https://gist.github.com/mislav/5189704 514 | -------------------------------------------------------------------------------- /doc/tmux-navigator.txt: -------------------------------------------------------------------------------- 1 | *tmux-navigator.txt* Plugin to allow seamless navigation between tmux and vim 2 | 3 | ============================================================================== 4 | CONTENTS *tmux-navigator-contents* 5 | 6 | 7 | ============================================================================== 8 | INTRODUCTION *tmux-navigator* 9 | 10 | Vim-tmux-navigator is a little plugin which enables seamless navigation 11 | between tmux panes and vim splits. This plugin is a repackaging of Mislav 12 | Marohinc's tmux=navigator configuration. When combined with a set of tmux key 13 | bindings, the plugin will allow you to navigate seamlessly between vim and 14 | tmux splits using a consistent set of hotkeys. 15 | 16 | NOTE: This requires tmux v1.8 or higher. 17 | 18 | ============================================================================== 19 | CONFIGURATION *tmux-navigator-configuration* 20 | 21 | * Activate autoupdate on exit 22 | let g:tmux_navigator_save_on_switch = 1 23 | 24 | * Disable vim->tmux navigation when the Vim pane is zoomed in tmux 25 | let g:tmux_navigator_disable_when_zoomed = 1 26 | 27 | * If the Vim pane is zoomed, stay zoomed when moving to another tmux pane 28 | let g:tmux_navigator_preserve_zoom = 1 29 | 30 | * Custom Key Bindings 31 | let g:tmux_navigator_no_mappings = 1 32 | 33 | nnoremap {Left-mapping} :TmuxNavigateLeft 34 | nnoremap {Down-Mapping} :TmuxNavigateDown 35 | nnoremap {Up-Mapping} :TmuxNavigateUp 36 | nnoremap {Right-Mapping} :TmuxNavigateRight 37 | nnoremap {Previous-Mapping} :TmuxNavigatePrevious 38 | 39 | * Disable the remapping on netrw buffers (use netrw's mapping) 40 | let g:tmux_navigator_disable_netrw_workaround = 1 41 | 42 | vim:tw=78:ts=8:ft=help:norl: 43 | -------------------------------------------------------------------------------- /pattern-check: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Collection of various test strings that could be the output of the tmux 4 | # 'pane_current_comamnd' message. Included as regression test for updates to 5 | # the inline grep pattern used in the `.tmux.conf` configuration 6 | 7 | set -e 8 | 9 | RED=$(tput setaf 1) 10 | GREEN=$(tput setaf 2) 11 | YELLOW=$(tput setaf 3) 12 | NORMAL=$(tput sgr0) 13 | 14 | # Import 'vim_pattern' 15 | source 'vim-tmux-navigator.tmux' 16 | 17 | match_tests=( 18 | vim 19 | Vim 20 | VIM 21 | vimdiff 22 | lvim 23 | /usr/local/bin/vim 24 | vi 25 | gvim 26 | view 27 | gview 28 | nvim 29 | vimx 30 | fzf 31 | .vim-wrapped 32 | .nvim-wrapped 33 | ) 34 | no_match_tests=( 35 | /Users/christoomey/.vim/thing 36 | /usr/local/bin/start-vim 37 | ) 38 | 39 | MATCH_RESULT="${GREEN}match${NORMAL}" 40 | NO_MATCH_RESULT="${RED}not match${NORMAL}" 41 | 42 | display_matches() { 43 | local result 44 | local final_status=0 45 | for process_name in "$@"; do 46 | result="$(matches_vim_pattern $process_name)" 47 | if [[ "${result}" != "${expect_result}" ]]; then 48 | final_status=1 49 | fi 50 | printf "%s %s\n" "${result}" "$process_name" 51 | done 52 | return "${final_status}" 53 | } 54 | 55 | matches_vim_pattern() { 56 | if echo "$1" | grep -iqE "^${vim_pattern}$"; then 57 | echo "${MATCH_RESULT}" 58 | else 59 | echo "${NO_MATCH_RESULT}" 60 | fi 61 | } 62 | 63 | main() { 64 | echo -e "Testing against pattern: ${YELLOW}$vim_pattern${NORMAL}\n" 65 | 66 | local final_status=0 67 | echo -e "These should all ${MATCH_RESULT}\n----------------------" 68 | local expect_result="${MATCH_RESULT}" 69 | display_matches "${match_tests[@]}" || final_status=1 70 | 71 | echo -e "\nThese should all ${NO_MATCH_RESULT}\n--------------------------" 72 | expect_result="${NO_MATCH_RESULT}" 73 | display_matches "${no_match_tests[@]}" || final_status=1 74 | 75 | if [[ "${final_status}" == 0 ]]; then 76 | echo -e "\n${GREEN}All test cases passed!${NORMAL}" 77 | else 78 | echo -e "\n${RED}Some test cases are failing${NORMAL}" 79 | fi 80 | return "${final_status}" 81 | } 82 | 83 | main 84 | -------------------------------------------------------------------------------- /plugin/tmux_navigator.vim: -------------------------------------------------------------------------------- 1 | " Maps to switch vim splits in the given direction. If there are no more windows in that direction, forwards the operation to tmux. 2 | " Additionally, toggles between last active vim splits/tmux panes. 3 | 4 | if exists("g:loaded_tmux_navigator") || &cp || v:version < 700 5 | finish 6 | endif 7 | let g:loaded_tmux_navigator = 1 8 | 9 | function! s:VimNavigate(direction) 10 | try 11 | execute 'wincmd ' . a:direction 12 | catch 13 | echohl ErrorMsg | echo 'E11: Invalid in command-line window; executes, CTRL-C quits: wincmd k' | echohl None 14 | endtry 15 | endfunction 16 | 17 | if !get(g:, 'tmux_navigator_no_mappings', 0) 18 | nnoremap :TmuxNavigateLeft 19 | nnoremap :TmuxNavigateDown 20 | nnoremap :TmuxNavigateUp 21 | nnoremap :TmuxNavigateRight 22 | nnoremap :TmuxNavigatePrevious 23 | 24 | if !empty($TMUX) 25 | function! IsFZF() 26 | return &ft == 'fzf' 27 | endfunction 28 | tnoremap IsFZF() ? "\" : "\:\ TmuxNavigateLeft\" 29 | tnoremap IsFZF() ? "\" : "\:\ TmuxNavigateDown\" 30 | tnoremap IsFZF() ? "\" : "\:\ TmuxNavigateUp\" 31 | tnoremap IsFZF() ? "\" : "\:\ TmuxNavigateRight\" 32 | endif 33 | 34 | if !get(g:, 'tmux_navigator_disable_netrw_workaround', 0) 35 | if !exists('g:Netrw_UserMaps') 36 | let g:Netrw_UserMaps = [['', 'TmuxNavigateRight']] 37 | else 38 | echohl ErrorMsg | echo 'vim-tmux-navigator conflicts with netrw mapping. See https://github.com/christoomey/vim-tmux-navigator#netrw or add `let g:tmux_navigator_disable_netrw_workaround = 1` to suppress this warning.' | echohl None 39 | endif 40 | endif 41 | endif 42 | 43 | if empty($TMUX) 44 | command! TmuxNavigateLeft call s:VimNavigate('h') 45 | command! TmuxNavigateDown call s:VimNavigate('j') 46 | command! TmuxNavigateUp call s:VimNavigate('k') 47 | command! TmuxNavigateRight call s:VimNavigate('l') 48 | command! TmuxNavigatePrevious call s:VimNavigate('p') 49 | finish 50 | endif 51 | 52 | command! TmuxNavigateLeft call s:TmuxAwareNavigate('h') 53 | command! TmuxNavigateDown call s:TmuxAwareNavigate('j') 54 | command! TmuxNavigateUp call s:TmuxAwareNavigate('k') 55 | command! TmuxNavigateRight call s:TmuxAwareNavigate('l') 56 | command! TmuxNavigatePrevious call s:TmuxAwareNavigate('p') 57 | 58 | if !exists("g:tmux_navigator_save_on_switch") 59 | let g:tmux_navigator_save_on_switch = 0 60 | endif 61 | 62 | if !exists("g:tmux_navigator_disable_when_zoomed") 63 | let g:tmux_navigator_disable_when_zoomed = 0 64 | endif 65 | 66 | if !exists("g:tmux_navigator_preserve_zoom") 67 | let g:tmux_navigator_preserve_zoom = 0 68 | endif 69 | 70 | if !exists("g:tmux_navigator_no_wrap") 71 | let g:tmux_navigator_no_wrap = 0 72 | endif 73 | 74 | let s:pane_position_from_direction = {'h': 'left', 'j': 'bottom', 'k': 'top', 'l': 'right'} 75 | 76 | function! s:TmuxOrTmateExecutable() 77 | return (match($TMUX, 'tmate') != -1 ? 'tmate' : 'tmux') 78 | endfunction 79 | 80 | function! s:TmuxVimPaneIsZoomed() 81 | return s:TmuxCommand("display-message -p '#{window_zoomed_flag}'") == 1 82 | endfunction 83 | 84 | function! s:TmuxSocket() 85 | " The socket path is the first value in the comma-separated list of $TMUX. 86 | return split($TMUX, ',')[0] 87 | endfunction 88 | 89 | function! s:TmuxCommand(args) 90 | let cmd = s:TmuxOrTmateExecutable() . ' -S ' . s:TmuxSocket() . ' ' . a:args 91 | let l:x=&shellcmdflag 92 | let &shellcmdflag='-c' 93 | let retval=system(cmd) 94 | let &shellcmdflag=l:x 95 | return retval 96 | endfunction 97 | 98 | function! s:TmuxNavigatorProcessList() 99 | echo s:TmuxCommand("run-shell 'ps -o state= -o comm= -t ''''#{pane_tty}'''''") 100 | endfunction 101 | command! TmuxNavigatorProcessList call s:TmuxNavigatorProcessList() 102 | 103 | let s:tmux_is_last_pane = 0 104 | augroup tmux_navigator 105 | au! 106 | autocmd WinEnter * let s:tmux_is_last_pane = 0 107 | augroup END 108 | 109 | function! s:NeedsVitalityRedraw() 110 | return exists('g:loaded_vitality') && v:version < 704 && !has("patch481") 111 | endfunction 112 | 113 | function! s:ShouldForwardNavigationBackToTmux(tmux_last_pane, at_tab_page_edge) 114 | if g:tmux_navigator_disable_when_zoomed && s:TmuxVimPaneIsZoomed() 115 | return 0 116 | endif 117 | return a:tmux_last_pane || a:at_tab_page_edge 118 | endfunction 119 | 120 | 121 | function! s:TmuxAwareNavigate(direction) 122 | let nr = winnr() 123 | let tmux_last_pane = (a:direction == 'p' && s:tmux_is_last_pane) 124 | if !tmux_last_pane 125 | call s:VimNavigate(a:direction) 126 | endif 127 | let at_tab_page_edge = (nr == winnr()) 128 | " Forward the switch panes command to tmux if: 129 | " a) we're toggling between the last tmux pane; 130 | " b) we tried switching windows in vim but it didn't have effect. 131 | if s:ShouldForwardNavigationBackToTmux(tmux_last_pane, at_tab_page_edge) 132 | if g:tmux_navigator_save_on_switch == 1 133 | try 134 | update " save the active buffer. See :help update 135 | catch /^Vim\%((\a\+)\)\=:E32/ " catches the no file name error 136 | endtry 137 | elseif g:tmux_navigator_save_on_switch == 2 138 | try 139 | wall " save all the buffers. See :help wall 140 | catch /^Vim\%((\a\+)\)\=:E141/ " catches the no file name error 141 | endtry 142 | endif 143 | let args = 'select-pane -t ' . shellescape($TMUX_PANE) . ' -' . tr(a:direction, 'phjkl', 'lLDUR') 144 | if g:tmux_navigator_preserve_zoom == 1 145 | let l:args .= ' -Z' 146 | endif 147 | if g:tmux_navigator_no_wrap == 1 && a:direction != 'p' 148 | let args = 'if -F "#{pane_at_' . s:pane_position_from_direction[a:direction] . '}" "" "' . args . '"' 149 | endif 150 | silent call s:TmuxCommand(args) 151 | if s:NeedsVitalityRedraw() 152 | redraw! 153 | endif 154 | let s:tmux_is_last_pane = 1 155 | else 156 | let s:tmux_is_last_pane = 0 157 | endif 158 | endfunction 159 | -------------------------------------------------------------------------------- /vim-tmux-navigator.tmux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | get_tmux_option() { 4 | local option value default 5 | option="$1" 6 | default="$2" 7 | # NOTE: Older tmux versions (eg. 3.2a on Ubuntu 22.04) do not exit with an 8 | # error code when an option is not defined. Therefore we need to first 9 | # test if the option exists, and only then try to get its value or fall 10 | # back to the default. 11 | value="$([[ -n $(tmux show-options -gq "$option") ]] \ 12 | && tmux show-option -gqv "$option" \ 13 | || echo "$default")" 14 | 15 | # Deprecated, for backward compatibility 16 | if [[ $value == 'null' ]]; then 17 | echo "" 18 | return 19 | fi 20 | 21 | echo "$value" 22 | } 23 | 24 | # Export 'vim_pattern' so that it can be tested in pattern-check 25 | declare vim_pattern='(\S+/)?g?\.?(view|l?n?vim?x?|fzf)(diff)?(-wrapped)?' 26 | 27 | bind_key_vim() { 28 | local key tmux_cmd is_vim 29 | key="$1" 30 | tmux_cmd="$2" 31 | is_vim="ps -o state= -o comm= -t '#{pane_tty}' \ 32 | | grep -iqE '^[^TXZ ]+ +${vim_pattern}$'" 33 | # sending C-/ according to https://github.com/tmux/tmux/issues/1827 34 | tmux bind-key -n "$key" if-shell "$is_vim" "send-keys '$key'" "$tmux_cmd" 35 | # tmux < 3.0 cannot parse "$tmux_cmd" as one argument, thus copying as multiple arguments 36 | tmux bind-key -T copy-mode-vi "$key" $tmux_cmd 37 | } 38 | 39 | main() { 40 | move_left="$(get_tmux_option "@vim_navigator_mapping_left" 'C-h')" 41 | move_right="$(get_tmux_option "@vim_navigator_mapping_right" 'C-l')" 42 | move_up="$(get_tmux_option "@vim_navigator_mapping_up" 'C-k')" 43 | move_down="$(get_tmux_option "@vim_navigator_mapping_down" 'C-j')" 44 | move_prev="$(get_tmux_option "@vim_navigator_mapping_prev" 'C-\')" 45 | 46 | for k in $(echo "$move_left"); do bind_key_vim "$k" "select-pane -L"; done 47 | for k in $(echo "$move_down"); do bind_key_vim "$k" "select-pane -D"; done 48 | for k in $(echo "$move_up"); do bind_key_vim "$k" "select-pane -U"; done 49 | for k in $(echo "$move_right"); do bind_key_vim "$k" "select-pane -R"; done 50 | for k in $(echo "$move_prev"); do bind_key_vim "$k" "select-pane -l"; done 51 | 52 | # Restoring clear screen 53 | clear_screen="$(get_tmux_option "@vim_navigator_prefix_mapping_clear_screen" 'C-l')" 54 | for k in $(echo "$clear_screen"); do tmux bind "$k" send-keys 'C-l'; done 55 | } 56 | 57 | if [ "$BASH_SOURCE" == "$0" ]; then 58 | main "$@" 59 | fi 60 | --------------------------------------------------------------------------------