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!

4
  • Why are the local {Red,Green}=... different inside and outside?
    – Daniel T
    Feb 21 at 19:15
  • @DanielT The only difference are the \\[ and ```\]`` brackets if I'm seeing it correctly. When I tried putting them in brackets inside the function, they ended up being printed along the checkmark. This was the case for single quotes ', for double quotes ", for single backslash \ and for double backslash \\ and all combinations of the above. Theoretically I could remove the outer ones since they don't get used anyways.
    – reneas
    Feb 21 at 19:20
  • Ctrl+F local Red="$(tput setaf 1 )" . You completely forgot the backslash you added to the other one
    – Daniel T
    Feb 21 at 19:23
  • @DanielT see my comment above and the question edit
    – reneas
    Feb 21 at 19:33

1 Answer 1

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

8
  • 2
    I was thinking it was the multi-byte characters, but then you went ahead and solved it. Kudos! Feb 21 at 20:37
  • 1
    works like a charm, thanks for this! I still don't completely understand why it behaves like this but I guess that's fine :)
    – reneas
    Feb 21 at 21:12
  • wow nice one! this is definitely a nice variation, wish i could upvote again x) only issue I'm having with this is that on a failed command the tip of the red arrow is cut off
    – reneas
    Feb 23 at 0:39
  • @reneas Can you edit the question with a screenshot of the cut off arrow? You could also switch the gnome-terminal font to JetBrains Mono or another font that has 1-character-wide arrows
    – Daniel T
    Feb 23 at 1:02
  • 1
    also after installing the other font it looks the same. I dont't think I will ask another question, since the other version (the longer one) works well and is also more human readable so i can understand better what's happening there. thank for your help @DanielT!!
    – reneas
    Feb 26 at 12:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .