@@ -682,8 +682,8 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
682682 width : Optional [int ] = None , tab_width : int = 4 , truncate : bool = False ) -> str :
683683 """
684684 Align text for display within a given width. Supports characters with display widths greater than 1.
685- ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
686- supported. If text has line breaks, then each line is aligned independently.
685+ ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned
686+ independently.
687687
688688 There are convenience wrappers around this function: align_left(), align_center(), and align_right()
689689
@@ -777,8 +777,8 @@ def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
777777 tab_width : int = 4 , truncate : bool = False ) -> str :
778778 """
779779 Left align text for display within a given width. Supports characters with display widths greater than 1.
780- ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
781- supported. If text has line breaks, then each line is aligned independently.
780+ ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned
781+ independently.
782782
783783 :param text: text to left align (can contain multiple lines)
784784 :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character)
@@ -800,8 +800,8 @@ def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None
800800 tab_width : int = 4 , truncate : bool = False ) -> str :
801801 """
802802 Center text for display within a given width. Supports characters with display widths greater than 1.
803- ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
804- supported. If text has line breaks, then each line is aligned independently.
803+ ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned
804+ independently.
805805
806806 :param text: text to center (can contain multiple lines)
807807 :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character)
@@ -823,8 +823,8 @@ def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
823823 tab_width : int = 4 , truncate : bool = False ) -> str :
824824 """
825825 Right align text for display within a given width. Supports characters with display widths greater than 1.
826- ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
827- supported. If text has line breaks, then each line is aligned independently.
826+ ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned
827+ independently.
828828
829829 :param text: text to right align (can contain multiple lines)
830830 :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character)
@@ -845,8 +845,15 @@ def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
845845def truncate_line (line : str , max_width : int , * , tab_width : int = 4 ) -> str :
846846 """
847847 Truncate a single line to fit within a given display width. Any portion of the string that is truncated
848- is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences are
849- safely ignored and do not count toward the display width. This means colored text is supported.
848+ is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences
849+ do not count toward the display width.
850+
851+ If there are ANSI style sequences in the string after where truncation occurs, this function will append them
852+ to the returned string.
853+
854+ This is done to prevent issues caused in cases like: truncate_string(fg.blue + hello + fg.reset, 3)
855+ In this case, "hello" would be truncated before fg.reset resets the color from blue. Appending the remaining style
856+ sequences makes sure the style is in the same state had the entire string been printed.
850857
851858 :param line: text to truncate
852859 :param max_width: the maximum display width the resulting string is allowed to have
@@ -855,6 +862,7 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str:
855862 :raises: ValueError if text contains an unprintable character like a new line
856863 ValueError if max_width is less than 1
857864 """
865+ import io
858866 from . import ansi
859867
860868 # Handle tabs
@@ -866,12 +874,48 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str:
866874 if max_width < 1 :
867875 raise ValueError ("max_width must be at least 1" )
868876
869- if ansi .style_aware_wcswidth (line ) > max_width :
870- # Remove characters until we fit. Leave room for the ellipsis.
871- line = line [:max_width - 1 ]
872- while ansi .style_aware_wcswidth (line ) > max_width - 1 :
873- line = line [:- 1 ]
877+ if ansi .style_aware_wcswidth (line ) <= max_width :
878+ return line
879+
880+ # Find all style sequences in the line
881+ start = 0
882+ styles = collections .OrderedDict ()
883+ while True :
884+ match = ansi .ANSI_STYLE_RE .search (line , start )
885+ if match is None :
886+ break
887+ styles [match .start ()] = match .group ()
888+ start += len (match .group ())
889+
890+ # Add characters one by one and preserve all style sequences
891+ done = False
892+ index = 0
893+ total_width = 0
894+ truncated_buf = io .StringIO ()
895+
896+ while not done :
897+ # Check if a style sequence is at this index. These don't count toward display width.
898+ if index in styles :
899+ truncated_buf .write (styles [index ])
900+ style_len = len (styles [index ])
901+ styles .pop (index )
902+ index += style_len
903+ continue
904+
905+ char = line [index ]
906+ char_width = ansi .style_aware_wcswidth (char )
907+
908+ # This char will make the text too wide, add the ellipsis instead
909+ if char_width + total_width >= max_width :
910+ char = constants .HORIZONTAL_ELLIPSIS
911+ char_width = ansi .style_aware_wcswidth (char )
912+ done = True
913+
914+ total_width += char_width
915+ truncated_buf .write (char )
916+ index += 1
874917
875- line += "\N{HORIZONTAL ELLIPSIS} "
918+ # Append remaining style sequences from original string
919+ truncated_buf .write ('' .join (styles .values ()))
876920
877- return line
921+ return truncated_buf . getvalue ()
0 commit comments