Stratus3D

Software Engineering, Web Development and 3D Graphics

Better Vi Mode in Zshell

Many Vim users don’t realize that their shell offers a Vi mode, but both Zshell and Bash do. I’ve been a big fan of Vim for many years now and use it exclusively for all editing work I do. In addition to using Vim for code I also write all my blog post in Vim with vim-pencil and goyo.vim. I only recently decided to switch to Vi mode in my Zsh and Bash shells. While I was happy to move away from Emacs mode, which is the default in Zsh and Bash, I was disappointed with the key mappings present in Zsh’s Vi mode. Many of the features I’d come to love from Zsh’s Emacs mode were gone. After some tinkering and reading several blog posts (links at the bottom of the page) I figured out how to add these features back into Vi mode.

I found that oh-my-zsh offers a Vi mode plugin but I gave up using it when I realized it didn’t offer many of the things I needed. It also made doing some of the customizations I cover in this blog post much harder to do.

Incremental Searching

One of the features I loved most in Zsh’s Emacs mode was the incremental searching. I could type out the start of a command, say tmu, hit the up arrow and scroll through all the tmux commands I had previously run. This feature came in very handy when I needed to frequently rerun a command. All I’d have to do would be to type out the start of the command and then hit the up arrow a couple times. It was much easier than typing out the full command each time or searching back through history with Ctrl-R. Adding this back into Vi mode is easy. Just add something like this to your .zshrc:

# Better searching in command mode
bindkey -M vicmd '?' history-incremental-search-backward
bindkey -M vicmd '/' history-incremental-search-forward

# Beginning search with arrow keys
bindkey "^[OA" up-line-or-beginning-search
bindkey "^[OB" down-line-or-beginning-search
bindkey -M vicmd "k" up-line-or-beginning-search
bindkey -M vicmd "j" down-line-or-beginning-search

First we map the ? and / characters to incremental search when in Vi command mode, so we can use the same key sequences we would use in Vim to find something. Note that these mappings are only in command mode. In insert mode we want to be able to literally type ? or /.

Next we map the up and down arrows (^[OA and ^[OB) to incremental searching when in either insert or command mode. This allows for searching based on a partially typed command like I described above. Since the arrow keys are normally not used in Vi I can safely have these mappings in both insert and command mode.

In addition to mapping the arrow keys to the incremental searching commands I also map k and j in Vi command mode to the same commands, so I can use more Vi like mappings for incremental searching.

Visual Mode Hack

In Vim we have visual mode which makes it easy to select text to yank or manipulate. In Zshell prior to version 5.0.8 there wasn’t a proper visual mode, so if you are using an older version of Zshell you won’t have a way to visualize selections. Instead you can map the v key to open the command in Vim itself for editing. Just add this to your .zshrc:

# Easier, more vim-like editor opening
bindkey -M vicmd v edit-command-line

If you are using Zshell 5.0.8 or newer and have visual mode available you may still want to be able to open Vim to edit a command. I found Ctrl-V to be a mapping that doesn’t interfere with any of the existing mappings.

# `v` is already mapped to visual mode, so we need to use a different key to
# open Vim
bindkey -M vicmd "^V" edit-command-line

Both of these mappings will open the command for editing in the editor you’ve specified in $EDITOR, and from there you can use visual mode to manipulate the command. If you are using Vim and don’t want to load your entire .vimrc just to edit this command you can set $EDITOR to Vim with no config:

export EDITOR='vim -u NONE'

Or you can specify another vim file configuration to use:

export EDITOR='vim -u alternate_profile.vim'

Faster Mode Switching

When you press ESC Zsh normally waits 0.4 seconds before switching to command mode. This is a really long time to wait when typing in a command sequence but we can make it shorter with the KEYTIMEOUT setting. Setting it to 1 makes the wait only 10 milliseconds, which is much more reasonable. Add the following to your .zshrc file:

# Make Vi mode transitions faster (KEYTIMEOUT is in hundredths of a second)
export KEYTIMEOUT=1

Mode Indicator

This isn’t something I use myself, but it is a nice enhancement. With this code the text [NORMAL] or [INSERT] will be shown on the right side of the prompt. The code that adds the mode to the prompt is a bit more involved:

# Updates editor information when the keymap changes.
function zle-keymap-select() {
  zle reset-prompt
  zle -R
}

zle -N zle-keymap-select

function vi_mode_prompt_info() {
  echo "${${KEYMAP/vicmd/[% NORMAL]%}/(main|viins)/[% INSERT]%}"
}

# define right prompt, regardless of whether the theme defined it
RPS1='$(vi_mode_prompt_info)'
RPS2=$RPS1

Basically we call the zle-keymap-select function whenever the input changes, and then vi_mode_prompt_info function determines what mode should be displayed based off of the output from the vicmd, main and viins functions. We assign the resulting string to RPS1 and RPS2 so it’s presented on the right side of the prompt.

Conclusion

With a few tweaks I’ve found Vi mode in Zsh to be very powerful. I’m very happy with my setup and I’m not going back to Emacs mode. I wish I had started using Vi mode earlier as I spent a lot of time learning the Emacs mappings prior to switching. If you’ve got any questions about my configuration you can always find my .zshrc and all my other config files in my dotfiles repository on Github or you can ask me directly.

11/5/2017 Update

Previous versions of this blog post incorrectly stated that the Zshell’s Vi mode lacked a visual mode. That is incorrect. Zshell versions 5.0.8 later have a visual mode that works almost identical to Vi’s actual visual mode. This means the custom v mapping isn’t necessary in versions 5.0.8 and later.

References