How to fix the colour bash prompt wrapping

bash

I have defined a bash prompt (using PROMPT_FUNCTION) like so:

function get_hg_prompt_prefix() {
    local APPLIED_COLOR=$1; shift
    local UNAPPLIED_COLOR=$1; shift
    local ALERT_COLOUR=$1; shift
    local TEXTCOLOR=$1; shift
    local mercurial_prompt_line="{{patches|join(:)|pre_applied(${APPLIED_COLOR})|post_applied(${TEXTCOLOR})|pre_unapplied(${UNAPPLIED_COLOR})|post_unapplied(${TEXTCOLOR})}\n\r}"
    local mercurial_status_prompt="{ ${ALERT_COLOUR}{status}${TEXTCOLOR}}"

    echo "$(hg prompt "${mercurial_prompt_line}" 2>/dev/null)$(hg prompt "${mercurial_status_prompt}" 2>/dev/null)"
}

function set_prompt() {
    bright='\[[01m\]'
    colors_reset='\[[00m\]'
    HOSTCOLOR=${colors_reset}='\[[34m\]'
    USERCOLOR=${colors_reset}='\[[01m\]'
    TEXTCOLOR=${colors_reset}='\[[32m\]'
    APPLIED_COLOR=${colors_reset}='\[[32m\]'
    UNAPPLIED_COLOR=${colors_reset}='\[[37m\]'
    ALERT_COLOUR=${colors_reset}='\[[31m\]'

    hg_status="$(get_hg_prompt_prefix $APPLIED_COLOR $UNAPPLIED_COLOR $ALERT_COLOUR $TEXTCOLOR)"
    ps1_prefix="${hg_status}$colors_reset($bright$(basename $VIRTUAL_ENV)$colors_reset) "
    PROMPTEND='$'
    PS1="${ps1_prefix}${USERCOLOR}\u${colors_reset}${TEXTCOLOR}@${colors_reset}${HOSTCOLOR}\h${colors_reset}${TEXTCOLOR} (\W) ${PROMPTEND}${colors_reset} "
}

PROMPT_COMMAND=set_prompt

In general, this gives me a multi-line prompt that displays some hg status info as well as my current virtualenv, looking (without colour) like this:

buggy-wins.patch
 ! (saas) user@computer (~) $ 

The problem is, this is screwing with the calculation of the length of the prompt (I think!) and causing weird terminal wrapping issues and cursor placement. For example, in an 80-char terminal, here's the prompt I see (the **-surrounded character is the cursor location):

~) $ **a**nis) crose@chris-rose (~

In terminals wide enough to display the prompt, line wrapping occurs much earlier than it should; here's the most text I can fit on the first line of the prompt in a 108-char-wide terminal window (again, the ** marks my cursor location):

 **(**advanis) crose@chris-rose (~) $ sdkfjlskdjflksdjff

When the line wraps over, it overwrites the prompt. The second line of input runs right to the edge of the terminal, however, and then wraps correctly.

So, clearly something is messing with the width of the prompt. How can I cause bash to determine the length of the PS1 string not according to the ANSI escape codes, but according to the actual displayed length of the prompt?

Best Answer

bash uses \[ \] to determine the "displayed length": text between those two escapes is considered unprintable and not counted in the total length; everything else is.

There seems to be a problem with your variables: bright='\[[01m\]' doesn't actually include an ESC character, so [01m gets printed as normal text but is not counted in the length. It should be '\[\e[01m\]'. Same for all other variables.


Related:

  • in Bash, you can put \$(hg_status) to $PS1 directly, without the need for a separate PROMPT_COMMAND.
Related Question