VI mode indicator in ZSH prompt

Here is my take on VI mode indicator in zsh’s prompt. This is useful only for people who use the vi mode (bindkey -v) in ZSH.

vim_ins_mode="%{$fg[cyan]%}[INS]%{$reset_color%}"
vim_cmd_mode="%{$fg[green]%}[CMD]%{$reset_color%}"
vim_mode=$vim_ins_mode

function zle-keymap-select {
  vim_mode="${${KEYMAP/vicmd/${vim_cmd_mode}}/(main|viins)/${vim_ins_mode}}"
  zle reset-prompt
}
zle -N zle-keymap-select

function zle-line-finish {
  vim_mode=$vim_ins_mode
}
zle -N zle-line-finish

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

And then it’s a matter of adding ${vim_mode} somewhere in your prompt. For example like this:

RPROMPT='${vim_mode}'

Other examples on the web use zle reset-prompt in the zle-line-init, which has a very nasty side effect of deleting last couple of lines on mode change (when going from ins to cmd mode) when using multi-line prompt. Using zle-line-finish works around that.

Also see my current ~/.zshrc, which includes those tweaks (and many others!).

10 Responses to “VI mode indicator in ZSH prompt”

  1. opk says:

    The other problem I have with this is that I have the last command return status in my prompt and this gets changed to 0 causing the first line to jump move. Not sure how to avoid that. I’m actually tempted to wipe the left prompt in command mode.

  2. I’m using left prompt with last command return status as well and I cannot experience this issue. There is one situation when prompt jumps 1 line above, deleting what was on the screen, but it’s zsh’s bug and it happens so rarely that I just ignore it.

  3. NAS says:

    Thank you! This worked great. I am now close to achieving command prompt perfection.

  4. Sacha says:

    Hey, thanks for the great zsh tips! This is the best solution I’ve found so far to the multiline + vim mode problem.

    However, with your approach, even though you’ve solved the deleting lines problem, I’m still finding that I get a rather annoying flicker whenever I switch modes. The prompt lines blank for some fraction of a second, Have you had this experience as well? Have you managed to fix it?

  5. @Sacha: I do not experience any blinking whatsoever. I’d recommend tweaking your prompt (removing all stuff and adding back in one by one) to find the culprit.

  6. Ron says:

    Regarding the issue where a ctrl-c in command mode makes the next prompt think it started in command mode, add the function to your .zshrc:

    function TRAPINT() {
    vim_mode=$vim_ins_mode
    return $(( 128 + $1 ))
    }

    The ctrl-c sends a SIGINT to the shell, which (as you’ve seen) appears to skip the {zle-line-finish, zle-line-init, zle-keymap-select} functions/widgets. My TRAPINT function above works around that fact by trapping on the SIGINT, manually resetting the prompt back to vim_ins_mode, and then re-propagating the SIGINT*, so anything else that depends on the SIGINT shouldn’t break.

    Hope this helps!

    *from the “return” section of “man zshbuiltins” : “If return was executed from a trap in a TRAPNAL function[...] the numeric value of the signal which caused the trap is passed as the first argument, so the statement ‘return $((128+$1))’ will return the same status as if the signal had not been trapped.”

  7. Ron says:

    One more thing…
    With the TRAPINT function I listed, if you ctrl-C out of a prompt while in command-mode, the mode indicator (“[CMD]” in your case) will stick around in the terminal scrollback. This is not what I wanted to happen, so here’s the solution.

    If you add “zle reset-prompt” right before the “return $(( 128 + $1 ))” statement, then the ctrl-C’d prompt will be reset to display the insert-mode indicator before the new prompt appears.

  8. Ron says:

    FINAL(?) UPDATE: change my “zle reset-prompt” to:
    “zle && zle reset-prompt”.

    Otherwise you’ll get something like “TRAPINT:zle:2: widgets can only be called when ZLE is active” every time you interrupt anything BUT zsh all by itself. The initial no-argument call to zle will return 0 only if zle is not active, which lets us work right around the annoying behavior I’d accidentally written in there.

    So the whole function is…

    function TRAPINT() {
    vim_mode=$vim_ins_mode
    zle && zle reset-prompt
    return $(( 128 + $1 ))
    }

  9. TobiSGD says:

    I just stumbled over this and this is exactly what I needed.
    Thanks Paul, thanks Ron.

  10. Wow! That is awesome, Ron! And the fix is so nice and simple! Thank you very much!

    As a side note, If I C-c while in CMD mode, I don’t mind my prompt saying that I was in CMD mode when I did that. This is, in fact, what I would expect it to say, so I very much prefer your original solution.

    Thanks again!