make my zsh prompt show mode in vi mode

33,730

Solution 1

I found this via SU. Here's the basic example, though I'm still customizing it for myself:

function zle-line-init zle-keymap-select {
    RPS1="${${KEYMAP/vicmd/-- NORMAL --}/(main|viins)/-- INSERT --}"
    RPS2=$RPS1
    zle reset-prompt
}

zle -N zle-line-init
zle -N zle-keymap-select

I'd explain it except I don't really understand it yet

Solution 2

You've already found zle-keymap-select which is executed whenever the mode changes. You could use it to set some other visual indicator than the prompt, depending on what your terminal supports it (and your taste in mode indicator display, of course).

There is a standard terminfo capability to change the shape of the cursor. However some terminals display the same cursor in both modes. Xterm's notion of a less visible cursor is to make it blink (and this must be enabled with the -bc command line argument or cursorBlink resource).

zle-keymap-select () {
  case $KEYMAP in
    vicmd) print -rn -- $terminfo[cvvis];; # block cursor
    viins|main) print -rn -- $terminfo[cnorm];; # less visible cursor
  esac
}

With some terminals, you can also change the cursor color with print -n '\e]12;pink\a' (by color name) or print -n '\e]12;#abcdef\a' (by RGB specification). These sequences are described in the xterm documentation, in the ctlseqs file; modern terminal emulators typically emulate xterm, though they might not support all its features.

Solution 3

For the people having problems using reset-prompt with multiline prompts, in combination with https://stackoverflow.com/questions/3622943/zsh-vi-mode-status-line I ended up doing:

terminfo_down_sc=$terminfo[cud1]$terminfo[cuu1]$terminfo[sc]$terminfo[cud1]

function insert-mode () { echo "-- INSERT --" }
function normal-mode () { echo "-- NORMAL --" }

precmd () {
    # yes, I actually like to have a new line, then some stuff and then 
    # the input line
    print -rP "
[%D{%a, %d %b %Y, %H:%M:%S}] %n %{$fg[blue]%}%m%{$reset_color%}"

    # this is required for initial prompt and a problem I had with Ctrl+C or
    # Enter when in normal mode (a new line would come up in insert mode,
    # but normal mode would be indicated)
    PS1="%{$terminfo_down_sc$(insert-mode)$terminfo[rc]%}%~ $ "
}
function set-prompt () {
    case ${KEYMAP} in
      (vicmd)      VI_MODE="$(normal-mode)" ;;
      (main|viins) VI_MODE="$(insert-mode)" ;;
      (*)          VI_MODE="$(insert-mode)" ;;
    esac
    PS1="%{$terminfo_down_sc$VI_MODE$terminfo[rc]%}%~ $ "
}

function zle-line-init zle-keymap-select {
    set-prompt
    zle reset-prompt
}
preexec () { print -rn -- $terminfo[el]; }

zle -N zle-line-init
zle -N zle-keymap-select

Solution 4

This is what I use to change the cursor between 'Block' and 'Beam' shape in zsh:

(Tested with Termite, gnome-terminal and mate-terminal)

# vim mode config
# ---------------

# Activate vim mode.
bindkey -v

# Remove mode switching delay.
KEYTIMEOUT=5

# Change cursor shape for different vi modes.
function zle-keymap-select {
  if [[ ${KEYMAP} == vicmd ]] ||
     [[ $1 = 'block' ]]; then
    echo -ne '\e[1 q'

  elif [[ ${KEYMAP} == main ]] ||
       [[ ${KEYMAP} == viins ]] ||
       [[ ${KEYMAP} = '' ]] ||
       [[ $1 = 'beam' ]]; then
    echo -ne '\e[5 q'
  fi
}
zle -N zle-keymap-select

# Use beam shape cursor on startup.
echo -ne '\e[5 q'

# Use beam shape cursor for each new prompt.
preexec() {
   echo -ne '\e[5 q'
}

Solution 5

Another solution for changing the cursor shape between I-beam and block (for underscore, use \033[4 q). Add this to your ~/.zshrc.

zle-keymap-select () {
if [ $KEYMAP = vicmd ]; then
    printf "\033[2 q"
else
    printf "\033[6 q"
fi
}
zle -N zle-keymap-select
zle-line-init () {
zle -K viins
printf "\033[6 q"
}
zle -N zle-line-init
bindkey -v

Modified from https://bbs.archlinux.org/viewtopic.php?id=95078. Tested in gnome-terminal 3.22.


Update

Yet another solution to changing the cursor shapes can be found here. This one apparently works for iTerm2, which I don't have the means to test, but adding it in here in case it is useful for someone else. The final addition to your ~/.zshrc would be

function zle-keymap-select zle-line-init
{
    # change cursor shape in iTerm2
    case $KEYMAP in
        vicmd)      print -n -- "\E]50;CursorShape=0\C-G";;  # block cursor
        viins|main) print -n -- "\E]50;CursorShape=1\C-G";;  # line cursor
    esac

    zle reset-prompt
    zle -R
}

function zle-line-finish
{
    print -n -- "\E]50;CursorShape=0\C-G"  # block cursor
}

zle -N zle-line-init
zle -N zle-line-finish
zle -N zle-keymap-select
Share:
33,730

Related videos on Youtube

xenoterracide
Author by

xenoterracide

Former Linux System Administrator, now full time Java Software Engineer.

Updated on September 17, 2022

Comments

  • xenoterracide
    xenoterracide over 1 year

    I use bindkey -v (for bash-ers set -o vi I think that works in zsh too) or vi(m) mode. but it bugs me that I don't have any visual cue to tell me whether I'm in insert mode or command mode. Does anyone know how I can make my prompt display the mode?

  • xenoterracide
    xenoterracide over 13 years
    I was hoping for something a bit less linkish and a bit more explanatory. I like to know how things work.
  • Sjark
    Sjark over 12 years
    Actually it is all there. Look at the comments the functions and how they are bound to the mode change events.
  • phemmer
    phemmer about 12 years
    I tried this approach but found one issue. If you do something like CTRL+C while in vi-command mode, the prompt will reset, but indicate youre in command mode when youre really in insert mode. zle-line-init should always change the indicator to insert mode. For some reason $KEYMAP is not updated properly when zle-line-init is called.
  • Paweł Gościcki
    Paweł Gościcki over 11 years
    zle reset-prompt will delete 1 (or more) lines above the prompt (if your prompt is multiline) when redrawing :( This is a showstopper for me.
  • Metaphox
    Metaphox about 11 years
    @PawełGościcki it seems to be an issue when you have two or more lines of PS1.
  • Paweł Gościcki
    Paweł Gościcki about 11 years
    @Metaphox I know, that why I've said "(if your prompt is multiline)". Any fix for that?
  • Metaphox
    Metaphox about 11 years
    @PawełGościcki aww sorry i somehow skipped the words in parentheses , bad habit. No, I didn't find a fix for that. What platform are you on? Was wondering if this is OS X specific.
  • Paweł Gościcki
    Paweł Gościcki about 11 years
    I believe it's ZSH specific. I'm on Ubuntu 12.10.
  • Graeme
    Graeme about 10 years
    For some reason I get main for KEYMAP instead of viins, not sure why.
  • Gilles 'SO- stop being evil'
    Gilles 'SO- stop being evil' about 10 years
    @Graeme main is an alias for viins or emacs depending on whether zsh thought your favorite editor was vi or not when it started. I thought it would use viins when switching the mode back from vicmd, but it seems that it uses main instead. Updated.
  • Graeme
    Graeme about 10 years
    You still need to double up with zle-line-init (or whatever alternative) since zle-keymap-select does not get called if hitting enter changes the keymap.
  • Jason Denney
    Jason Denney about 6 years
    I confirmed that the updated script for iTerm2 indeed worked.
  • JdeBP
    JdeBP almost 6 years
    This will only work on terminals and terminal emulators that understand DECSCUSR.
  • JdeBP
    JdeBP almost 6 years
    The first script will only work on terminals and terminal emulators that understand DECSCUSR.
  • tsturzl
    tsturzl over 5 years
    This is a really elegant solution that doesn't clutter up my shell
  • jdhao
    jdhao about 5 years
    This plugin is great.
  • schu34
    schu34 about 5 years
    this post has a good explanation (though the code from the article doesn't work) dougblack.io/words/zsh-vi-mode.html