8

After solving the first issue with my custom prompt I still have another one left.

When I cycle through my last used commands via the arrow-up and arrow-down keys I will sometimes have some characters from a previous command stay visible although the are not accessible nor actually in the commandline. They are just a visual bug very annoying and confusing.

Looks like this:
enter image description here

Here I went a bit up in the command history and then back down to the current (empty) prompt and typed echo. the pip i (coming from a previous pip install command) is not accessible with my cursor. It's visible there but not actually existent.

My .bashrc has this code for customizing the prompt:

set_PS1()
{
    local Reset="\\[$(tput sgr0 )\\]"
    local Bold="\\[$(tput bold )\\]"
    local Red="\\[$(tput setaf 1 )\\]"
    local Green="\\[$(tput setaf 2 )\\]"
    local Yellow="\\[$(tput setaf 3 )\\]"
    local Blue="\\[$(tput setaf 4 )\\]"
    local MagentaBG="\\[$(tput setab 5 )\\]"
    local Cyan="\\[$(tput setaf 6 )\\]"
local Whoami='\u'
local Where='\w'
local Hostname='\h'
local Time='\D{%H:%M:%S}'
local Exit_Code="$?"

exit_code_prompt() {
    local Exit_Code="$?"
    local Red="$(tput setaf 1 )"
    local Green="$(tput setaf 2 )"
    if (($Exit_Code == 0 )); then
        printf '%s\xE2\x9C\x93 \xE2\x86\x92 ' "$Green" # Green checkmark symbol
    else
        printf '%s\xE2\x9C\x98 %s \xE2\x86\x92 ' "$Red" "$Exit_Code" # Red cross mark symbol and exit code
    fi
}

local Line_1="$Bold$Yellow$Time $Cyan$Whoami$Blue@$Cyan$Hostname$Reset$Bold":" $Blue$Where$Reset"
local Line_2="$Bold\$(exit_code_prompt)$Reset$Bold \$: $Reset"
#local Line_2="$Bold \$: $Reset"

PS1="$Line_1\n$Line_2"

unset -f set_PS1

}

set_PS1

I already narrowed the problem down to the exit_code_prompt function since the problem doesn't appear if I remove it from $Line_2

EDIT: When I put the color definitions inside of the function into brackets like on the outside like this:

exit_code_prompt() {
        local Exit_Code="$?"
        local Red="\\[$(tput setaf 1 )\\]"
        local Green="\\[$(tput setaf 2 )\\]"
        if (($Exit_Code == 0 )); then
            printf '%s\xE2\x9C\x93 \xE2\x86\x92 ' "$Green" # Green checkmark symbol
        else
            printf '%s\xE2\x9C\x98 %s \xE2\x86\x92 ' "$Red" "$Exit_Code" # Red cross mark symbol and exit code
        fi
    }

I get this result:

enter image description here
Same result if I put only single backslashes \
Plus the inital problem is still there!

Daniel T
  • 5,339
reneas
  • 365

1 Answers1

9

New solution

It turns out it's actually possible to do this with just PS1 and not PROMPT_COMMAND. We don't need to set any additional global variables. Here's a one-liner:

PS1=$'\e[1;33m\\t \e[36m\u\e[34m@\e[36m\h\e[;1m: \e[34m\w\n \[\b\e[;1;31m✘\] ${?/#0/\[\b\b\e[32m✓ \]}${?/#[1-9]*/ }\[\b→\e[;1m\] \$: \[\e[m\]'

The improvements from my original solution below are the following:

  • Only the last line needs \[\]
  • I expanded the tput to \e[...m. This is compatible across Ubuntu and is not a concern because the default colored .bashrc also does this
  • Successive \e[Xm\e[Ym collapse into \e[X;Ym, and the previous bold doesn't always need to be reset
  • \D{%H:%M:%S} = \\t
  • Unknown \X sequences don't need the backslash escaped
  • I rely on $? being 0 or positive without leading zeroes: ?/#0*/ = ?/#0/
  • ${?/#[1-9]*/ }\[\b inserts a space then any character (currently space) for non-zero, and any character (currently 0) for zero. Then the additional character is deleted, to implement inserting exactly one space only for non-zero.
  • [1-9]* is a glob that means any digit 1-9 then anything, not a RegEx that means any number of the preceding
  • ✘\] ${?/#0/\[\b\b\e[32m✓ \]} always prints the red cross mark symbol first. If zero, we go back and replace it with a green checkmark. If non-zero we print the error code instead

Guided solution

Here is a working prompt:

set_PS1() {
    local Exit_Code="$?"
    local Reset="\\[$(tput sgr0 )\\]"
    local Bold="\\[$(tput bold )\\]"
    local Red="\\[$(tput setaf 1 )\\]"
    local Green="\\[$(tput setaf 2 )\\]"
    local Yellow="\\[$(tput setaf 3 )\\]"
    local Blue="\\[$(tput setaf 4 )\\]"
    local MagentaBG="\\[$(tput setab 5 )\\]"
    local Cyan="\\[$(tput setaf 6 )\\]"
local Whoami='\u'
local Where='\w'
local Hostname='\h'
local Time='\D{%H:%M:%S}'
# Emoji are multi-byte, but shells think 1 byte = 1 character
# We add a 1-character space, then enable color-code mode to ignore
# the upcoming emoji, then backspace to draw the emoji on top of the space.
local emoji_start=$' \\[\b'
local emoji_end=$'\\]'

if (($Exit_Code == 0 )); then
    local exit_code_prompt="$Green$emoji_start"$'\xE2\x9C\x93'"$emoji_end" # Green checkmark symbol
else
    local exit_code_prompt="$Red$emoji_start"$'\xE2\x9C\x98'"$emoji_end $Exit_Code" # Red cross mark symbol and exit code
fi


local Line_1="$Bold$Yellow$Time $Cyan$Whoami$Blue@$Cyan$Hostname$Reset$Bold":" $Blue$Where$Reset"
local Line_2="$Bold$exit_code_prompt $emoji_start"$'\xE2\x86\x92'"$emoji_end$Reset$Bold \\$: $Reset"
#local Line_2="$Bold \$: $Reset"

PS1="$Line_1\\n$Line_2"

}

We need to expand exit_code_prompt BEFORE PS1 prompt string expansion (see shopt promptvars in man bash)

so we want to move it to PROMPT_COMMAND which runs earlier.

PROMPT_COMMAND=set_PS1

  • PS1 undergoes prompt expansion then substitution, so we need PROMPT_COMMAND
  • This leaks the set_PS1 global, but you already have the exit_code_prompt global leaked
  • "\$" needs to be "\\$" so it undergoes prompt evaluation instead of immediately
  • Emojis are multi-byte and need to be wrapped as well, except this time we need to insert a fake space because they are 1-character wide unlike 0-character wide color codes
  • printf is unnecessarily complicated when you could just do $''

Now / command history works. Scrolling a multiline prompt with Ctrl+/ also jumps to the correct position now.

Here are some examples:

  • Prompt demo:

    prompt demo

  • Long string:

    long string

  • Long string then Ctrl+:

    long string then ←

  • pip install then echo:

    pip install then echo

  • echo then in history:

    echo then ↑ in history

Daniel T
  • 5,339