Move vi mode string to end of bash prompt

bashcommand lineinputrcpromptreadline

I use vi editing mode in bash. I have "show-mode-in-prompt" set to "on" in ~/.inputrc.

show-mode-in-prompt (Off)

If set to On, add a string to the beginning of the prompt indicating the editing mode: emacs, vi command, or vi insertion. The mode strings are user-settable (e.g., emacs-mode-string).

So now I can see the editing mode at the beginning of my prompt.

i:$ # This is ins mode
c?$ # This is cmd mode
i:$ cat ~/.inputrc
set editing-mode vi
set show-mode-in-prompt on
set vi-ins-mode-string "\1\e[1;32m\2i:\1\e[0m\2"
set vi-cmd-mode-string "\1\e[1;31m\2c?\1\e[0m\2"
set colored-stats on
i:$ echo $PS1
$

I'd really like this at the end of my prompt though, and prepend the current directory, i.e.

/current/dir/here $ i:

But I"m not sure if this is achievable. My first thought was to use a carriage return in PS1, but unfortunately this overwrites the mode string.

i:$ PS1='\r\w $ '
~/projects $

My second thought was to somehow get the mode string and just insert it into $PROMPT_COMMAND, instead of letting readline write it to the prompt. I can get the current mode with:

i:$ bind -v | grep keymap | awk '{print $3}'
vi-insert

But the PROMPT_COMMAND value is only executed before the prompt is issued, so I wouldn't see the mode change.

Attempt number 3 included padding my mode strings so there's room for the directory. This could work, but I don't know how I'd make the amount of space variable. Also the prompt is going to start before the mode string, the mode string will get overwritten by my command, and if I switch modes the text I've typed gets replaced by the whole mode string, effectively clearing it out due to the spaces.

                                i:  cat ~/.inputrc
set editing-mode vi
set show-mode-in-prompt on
set vi-ins-mode-string "                                \1\e[1;32m\2i:\1\e[0m\2"
set vi-cmd-mode-string "                                \1\e[1;31m\2c?\1\e[0m\2"
set colored-stats on
                                i:  PS1="\r\w $ "
~/projects $                    i:
~/projects $ # typing stuff     i:
~/projects $ # typing more stuff overwrites the mode line
~/projects $                    c?verwrites the mode line

Is there some other clever way to do this? Maybe a control character like carriage return that jumps to the beginning of the line but does not overwrite the existing characters?

i:$ echo $BASH_VERSION
5.0.7(1)-release

Best Answer

You can achieve that by setting a very long mode string for cmd and insert modes, that include the other items you want in your prompt. Note that this means you won't be able to use special shortcuts available in PS1, such as \w for a "pretty" printing of your current directory. But you can probably achieve the same in shell code that does the same.

You can set the readline variables using the bind "set vi-ins-mode-string \"...\"" and bind "set vi-cmd-mode-string \"...\"" commands. (see help bind for more details.) And you can set that from your PROMPT_COMMAND, so that it will get reset before each prompt (so it will reflect change of directory, date and time if you include them, etc.)

Quoting can get tricky :-)

For the example you used yourself:

/current/dir/here $ i:

You could use this:

reset_readline_prompt_mode_strings () {
    bind "set vi-ins-mode-string \"$PWD \$ \1\e[1;32m\2i:\1\e[0m\2\""
    bind "set vi-cmd-mode-string \"$PWD \$ \1\e[1;31m\2c?\1\e[0m\2\""
}
PROMPT_COMMAND=reset_readline_prompt_mode_strings
PS1=' '

Like mentioned, the expansion of $PWD will include the full path and won't use relative paths from ~, etc.

You can get the same expansion used in prompts by using bash's ${variable@P} operator expansion:

EMBEDDED_PS1='\w $ '
reset_readline_prompt_mode_strings () {
    bind "set vi-ins-mode-string \"${EMBEDDED_PS1@P}\1\e[1;32m\2i:\1\e[0m\2\""
    bind "set vi-cmd-mode-string \"${EMBEDDED_PS1@P}\1\e[1;31m\2c?\1\e[0m\2\""
}

Please note that you need PS1 to be set to something non-empty, otherwise no prompt is printed at all. You can use a single space in this case. You might want to consider setting PS2 appropriately, for line continuations.

BTW, my recommendation for symbols and colors for the ins and cmd mode strings are:

  • cmd should use : (same as you can type : commands from normal mode in Vim) and should use a green color (since that's the default color set by lightline in the status line for "Normal" mode.)
  • ins should use + (which is the symbol used by Vim to show a buffer is modified, when you insert, you modify it) and should use a blue color (default color used by lightline in the status line for "Insert" mode.)

Personally, I think these below are pretty cool:

EMBEDDED_PS1='\w $ '
reset_readline_prompt_mode_strings () {
    bind "set vi-ins-mode-string \"${EMBEDDED_PS1@P}\1\e[32m\2: >\1\e[92m\2>\1\e[0m\2>\""
    bind "set vi-cmd-mode-string \"${EMBEDDED_PS1@P}\1\e[34m\2+ >\1\e[94m\2>\1\e[0m\2>\""
}
PROMPT_COMMAND=reset_readline_prompt_mode_strings
PS1=' '
PS2=' ... '
Related Question