Zsh Prompt – Fix Full Width Issue with User Input

promptzsh

I'd like to have the left and right prompt in one line and a background color spanning the full line. The user input should be in the next line.

hostname | ~/path/to/cwd                              branch master | insert mode
$ _

For the linebreak I tried to use $'\n' in PS1 but that causes the right prompt to be drawn in the second line as well.

For coloring the whole line, I tried to have %K{green} in PS1 without closing it, but the background coloring stops right after the last character of the left prompt.

How can I realize this prompt?

Best Answer

You could do something like:

branch=master mode=insert
setopt promptsubst
left='%m | %~'
PS1='%K{green}$left${(l,COLUMNS-${#${(%)left}},)${${:-$branch | $mode}//[%]/%%}}%k$ '

We use the ${(l,length,)...} left-pad operator to pad the right hand side of the prompt with $COLUMNS minus the length of what was displayed on the left hand side.

For $mode to be updated when you press Insert, you'd do:

update-mode() {
  case $KEYMAP in
    (main)
      case $ZLE_STATE in
        (*insert*) mode=insert;;
        (*) mode=overwrite
      esac;;
    (*) mode=$KEYMAP
  esac
  [[ $mode = $oldmode ]] || zle reset-prompt
}

overwrite-mode() {
   zle ".$WIDGET"
   update-mode
}
zle -N overwrite-mode

That causes the prompt to be redrawn when you switch mode.

For vi mode, you'd also need to consider all the cases where the insert mode is entered (in insert mode (a/A, i/I, c/C, s/S) or overwrite (R)), which you can do with the zle-keymap-select special hook widget (as you found out):

zle -N zle-keymap-select update-mode

I'd rewrite your final solution as:

bindkey -v

# Bind the <Insert> key.
bindkey $terminfo[kich1] overwrite-mode
setopt promptsubst

(){ # local scope

  local left right invisible leftcontent

  # User name.
  left+='%B%F{black}%K{green} %n '
  # Current working directory.
  left+='%K{yellow} %~ '

  # Version control branch.
  right='${vcs_info_msg_0_:+${vcs_info_msg_0_//[%]/%%} }'
  # Virtualenv.
  export VIRTUAL_ENV_DISABLE_PROMP=1
  right+='${VIRTUAL_ENV:+venv }'

  # Editing mode. $ZLE_MODE shouldn't contain %, no need to escape
  ZLE_MODE=insert
  right+='%K{green} $ZLE_MODE'

  # closing
  right+=$' %k%f%b'

  # Combine left and right prompt with spacing in between.
  invisible='%([BSUbfksu]|([FBK]|){*})'

  leftcontent=${(S)left//$~invisible}
  rightcontent=${(S)right//$~invisible}

  PS1="$left\${(l,COLUMNS-\${#\${(%):-$leftcontent$rightcontent}},)}$right%{"$'\n%}$ '
}

autoload vcs_info
precmd() vcs_info

update-mode() {
  case $KEYMAP in
    (main)
      case $ZLE_STATE in
        (*insert*) ZLE_MODE=insert;;
        (*) ZLE_MODE=overwrite
      esac;;
    (*) ZLE_MODE=$KEYMAP
  esac
  [[ $ZLE_MODE = $oldmode ]] || zle reset-prompt
}

overwrite-mode() {
   zle ".$WIDGET"
   update-mode
}
zle -N overwrite-mode
zle -N zle-keymap-select update-mode

That would minimise the number of unnecessary forks and prompt redraws, and allow the shell to draw the prompt correctly in all circumstances including when the windows is being resized.

Screenshot:

enter image description here

Related Question