Search code examples
linuxbashterminalansi-colorscolor-codes

How to wrap color coded text to fixed line length in terminal?


For text with color codes, how to wrap it to a fixed length in the terminal?

Text without color codes wraps nicely with fold:

echo -e "12345678901234567890" | fold -w 10
1234567890
1234567890

But this red text wraps wrong:

echo -e "\u001b[31m12345678901234567890" | fold -w 10
12345
6789012345
67890

Note: While the red text is wrapped wrong, it still is printed in red, which is the desired behavior.

(My use case is line wrapping the output of git log --color=always --oneline --graph.)


Solution

  • When determining the (printable) width of a prompt (eg, PS1) the special characters - \[ and \] - are used to designate a series of non-printing characters (see this, this, this and this).

    So far I've been unable to find a way to use \[ and \] outside the scope of a prompt hence this awk hack ...


    Assumptions:

    • we don't know the color codes in advance
    • for this exercise it is sufficient to deal with color codes of the format \e[...m (\e[m turns off color)
    • may have to deal with multiple color codes in the input

    We'll wrap one awk idea in a bash function (for easier use):

    myfold() {
    
    awk -v n="${1:-10}" '                                                  # default wrap is 10 (printable) characters
    BEGIN { regex="[[:cntrl:]][[][^m]*m"                                   # regex == "\e[*m"
           #regex="\x1b[[][^m]*m"                                          # alternatives
           #regex="\033[[][^m]*m"
          }
          { input=$0
    
            while (input != "" ) {                                         # repeatedly strip off "n" characters until we have processed the entire line
                  count=n
                  output=""
    
                  while ( count > 0 ) {                                    # repeatedly strip off color control codes and characters until we have stripped of "n" characters
                        match(input,regex)
    
                        if (RSTART && RSTART <= count) {
                           output=output substr(input,1,RSTART+RLENGTH-1)
                           input=substr(input,RSTART+RLENGTH)
                           count=count - (RSTART > 1 ? RSTART-1 : 0)
                        }
                        else {
                           output=output substr(input,1,count)
                           input=substr(input,count+1)
                           count=0
                        }
                  }
                  print output
            }
          }
    '
    }
    

    NOTES:

    • other non-color, non-printing characters will throw off the count
    • the regex could be expanded to address other non-printing color and/or character codes

    Test run:

    $ echo -e "\e[31m123456789012345\e[m67890\e[32mABCD\e[m"
    12345678901234567890ABCD
    
    
    $ echo -e "\e[31m123456789012345\e[m67890\e[32mABCD\e[m" | myfold 10
    1234567890
    1234567890
    ABCD
    
    
    $ echo -e "\e[31m123456789012345\e[m67890\e[32mABCD\e[m" | myfold 7
    1234567
    8901234
    567890A
    BCD
    

    Displaying colors:

    enter image description here