Image of the glider from the Game of Life by John Conway
Skip to content

Add Vim Editing Mode To Your ZSH Prompt

I've decided to go back to vi mode with my shell. Of course, by default, BASH, ZSH and others use emacs mode for the keyboard bindings. This is fine for the generic case, but if I'm using Vim for my default editor, it makes sense to use the Vi keyboard bindings in my shell as well. However, I have an extremely dynamic shell, so if I'm going to start using Vi mode, I better have my shell tell me when I'm in command mode or insert mode.

On Vim, the default mode is command mode, and when you go into a different mode, you're notified, such as going into insert mode. However, with the ZSH, when you start up your prompt, you are in insert mode by default, and you must press your ESC key to go into command mode. So, seeing as though insert mode is default, rather than be notified constantly that I'm in insert mode, I'd rather be notified when I enter command mode. Also, I wanted a way to do this dynamically, so when I pressed my ESC key, I got an immediate visual that I entered command mode. The same event should happen when I go back into insert mode- my visual should disappear.

I wasn't sure how to approach this, so after a bit of Googling, I discovered that I need to write a ZSH widget. I've never written a ZSH widget before. In fact, to be honest, I didn't even know they existed. However, I was curious, and learned that I need to use the 'zle -N' command on a widget. By doing so, this widget will operate on the shell in real time, without waiting for the enter key to be pressed. This is exactly what I'm after. So, the only thing left to do was write a bit of logic handling when I'm in command mode, and when I'm not. Here's the relevant bit of code:

# If I am using vi keys, I want to know what mode I'm currently using.
# zle-keymap-select is executed every time KEYMAP changes.
# From
function zle-keymap-select {
    VIMODE="${${KEYMAP/vicmd/ M:command}/(main|viins)/}"
    zle reset-prompt

zle -N zle-keymap-select

Fairly straight forward I think. If the KEYMAP variable is set to "vicmd", then we're in command mode with vi on the shell, and we can set our VIMODE variable to " M:command" (to match the rest of my prompt). If the KEYMAP variable is set to "main" or "viins", then we can unset VIMODE. The way we do this is using inline ZSH substitution using ${VARIABLE/PATTERN} syntax. This says to search for our pattern anywhere within our variable. Check out the zshexpn(1) man page for more info and examples. Notice I'm running 'zle reset-prompt'. Because this is a widget, I can take advantage of redrawing the prompt whenever I need. Now, the only thing left to do is put the $VIMODE variable in my prompt, and I'll have a dynamic visual of when I enter command mode and when I leave it. Here's some screenshot candy:

Screenshot showing a ZSH prompt without the command mode visual. Screenshot showing a ZSH prompt with the command mode visual

For the need-to-have-it readers, here's the full source to my prompt. Just source the file in your .zshrc, and make sure that your TERM variable is set appropriately to handle terminal colors.

{ 9 } Comments

  1. Federico | March 30, 2009 at 11:07 am | Permalink

    I've tried your amazing .zshrc but vi keys don't work for me. If i press ESC nothing appen and zsh continue to work normally with the standard emacs keybinding

  2. Aaron | March 30, 2009 at 11:35 am | Permalink

    The file I'm linking to isn't my .zshrc, but just the prompt. If you want to add Vi keyboard bindings in your ZSH, then in your .zshrc:

    bindkeys -v
  3. NicDumZ | October 1, 2009 at 3:30 am | Permalink

    I was unhappy with this solution, becausing hitting "enter" in command mode would not reset the VIMODE variable.
    Use case: enter command mode, search for a particular command, hit enter to run it.

    Here is how I solved this problem:

    function accept_line {
        builtin zle .accept-line
    zle -N accept_line
    bindkey -M vicmd "^M" accept_line
  4. nasrulah | October 14, 2009 at 10:11 am | Permalink

    Thanks a lot....

  5. joecan | November 21, 2009 at 11:42 pm | Permalink

    Is there a way to do this in bash?

  6. anon | December 29, 2009 at 3:52 am | Permalink

    I was using emacs mode, since I did not know what mode I was in when using vi mode. Finally I can use vi mode and know what mode I'm in. Thank You

  7. dbbolton | May 16, 2010 at 2:14 pm | Permalink

    How could you set $VIMODE to "INSERT" when not in command mode?

  8. Anonymous | April 10, 2012 at 8:07 am | Permalink

    been a while but I still find this useful
    if you want to actually see "M:command" you need to have ${VIMODE} somewhere in your PROMPT or RPROMPT

    also, in comments
    1) it's "bindkey -v" not "bindkeys -v"
    2) solution by NicDumZ does nothing for me except complaining about "doprompt"

  9. Paweł Gościcki | September 22, 2012 at 2:33 pm | Permalink

    Here's how I solved the enter key problem:

    function zle-line-finish {
    zle -N zle-line-finish

{ 1 } Trackback

  1. [...] Aaron Toponce : Add Vim Editing Mode To Your ZSH Prompt [...]

Post a Comment

Your email is never published nor shared.