Using the Zsh shell in Vim mode

Whenever available I love to reach to the Vim keybindings. The Zsh line editor not being an exception. Vim all the things!

Enabling the Vim keybindings is a one-liner in the ~/.zshrc config file. But to get it working somewhat more smoothy some other tweaks are necessary too in my opinion. I’m sure there are some oh-my-zsh plugins available for this too if you use that.

First off, let’s indeed add that one-liner to the ~/.zshrc config file.

# ~/.zshrc

# Use vi mode
bindkey -v

The next time you open your shell you are using vi mode. You can exit insert mode with ESC and enter it again with i, and other keybindings you’re expecting are available too.

There is somewhat of a delay between pressing a key combination and the actual effect. For me this timeout takes a bit too long. You can change it by setting the KEYTIMEOUT environment variable in your ~/.zshrc file too.

# ~/.zshrc

# Use vi mode
bindkey -v
export KEYTIMEOUT=1

What is missing for me is a proper mode indicator. I’ve moved the code for the indicator to a separate zsh theme.

The code that did the trick for me is pasted in bellow.

## Init
setopt PROMPT_SUBST

## Options
THEME_PROMPT_PREFIX=${THEME_PROMPT_PREFIX:-''}
THEME_VI_INS_MODE_SYMBOL=${THEME_VI_INS_MODE_SYMBOL:-'λ'}
THEME_VI_CMD_MODE_SYMBOL=${THEME_VI_CMD_MODE_SYMBOL:-'ᐅ'}

## Set symbol for the initial mode
THEME_VI_MODE_SYMBOL="${THEME_VI_INS_MODE_SYMBOL}"

# on keymap change, define the mode and redraw prompt
zle-keymap-select() {
  if [ "${KEYMAP}" = 'vicmd' ]; then
    THEME_VI_MODE_SYMBOL="${THEME_VI_CMD_MODE_SYMBOL}"
  else
    THEME_VI_MODE_SYMBOL="${THEME_VI_INS_MODE_SYMBOL}"
  fi
  zle reset-prompt
}
zle -N zle-keymap-select

# reset to default mode at the end of line input reading
zle-line-finish() {
  THEME_VI_MODE_SYMBOL="${THEME_VI_INS_MODE_SYMBOL}"
}
zle -N zle-line-finish

# Fix a bug when you C-c in CMD mode, you'd be prompted with CMD mode indicator
# while in fact you would be in INS mode.
# Fixed by catching SIGINT (C-c), set mode to INS and repropagate the SIGINT,
# so if anything else depends on it, we will not break it.
TRAPINT() {
  THEME_VI_MODE_SYMBOL="${THEME_VI_INS_MODE_SYMBOL}"
  return $(( 128 + $1 ))
}

PROMPT='$THEME_PROMPT_PREFIX%f%B%F{240}%1~%f%b %(?.%F{green}$THEME_VI_MODE_SYMBOL.%F{red}$THEME_VI_MODE_SYMBOL) '

See Also

6 Non-Programming Books for Programmers