diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-02-11 12:39:38 -0500 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-02-11 12:39:38 -0500 |
commit | 023d5afadabfba19d77dcc7ec6c7f8a5c8bd1651 (patch) | |
tree | 5f37b4f4e8d5d4aa8be3f56624a4138e8b7bc582 /cmd2/utils.py | |
parent | 8d9f97bdcaacf8465e8fe61f64e5d22753cc415c (diff) | |
download | cmd2-git-023d5afadabfba19d77dcc7ec6c7f8a5c8bd1651.tar.gz |
Fixed bug where ANSI style sequences were not correctly handled in utils.truncate_line()
Diffstat (limited to 'cmd2/utils.py')
-rw-r--r-- | cmd2/utils.py | 78 |
1 files changed, 61 insertions, 17 deletions
diff --git a/cmd2/utils.py b/cmd2/utils.py index cfe75f53..2add7c5e 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -682,8 +682,8 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False) -> str: """ Align text for display within a given width. Supports characters with display widths greater than 1. - ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is - supported. If text has line breaks, then each line is aligned independently. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned + independently. There are convenience wrappers around this function: align_left(), align_center(), and align_right() @@ -777,8 +777,8 @@ def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False) -> str: """ Left align text for display within a given width. Supports characters with display widths greater than 1. - ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is - supported. If text has line breaks, then each line is aligned independently. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned + independently. :param text: text to left align (can contain multiple lines) :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 tab_width: int = 4, truncate: bool = False) -> str: """ Center text for display within a given width. Supports characters with display widths greater than 1. - ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is - supported. If text has line breaks, then each line is aligned independently. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned + independently. :param text: text to center (can contain multiple lines) :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, tab_width: int = 4, truncate: bool = False) -> str: """ Right align text for display within a given width. Supports characters with display widths greater than 1. - ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is - supported. If text has line breaks, then each line is aligned independently. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned + independently. :param text: text to right align (can contain multiple lines) :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, def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: """ Truncate a single line to fit within a given display width. Any portion of the string that is truncated - is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences are - safely ignored and do not count toward the display width. This means colored text is supported. + is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences + do not count toward the display width. + + If there are ANSI style sequences in the string after where truncation occurs, this function will append them + to the returned string. + + This is done to prevent issues caused in cases like: truncate_string(fg.blue + hello + fg.reset, 3) + In this case, "hello" would be truncated before fg.reset resets the color from blue. Appending the remaining style + sequences makes sure the style is in the same state had the entire string been printed. :param line: text to truncate :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: :raises: ValueError if text contains an unprintable character like a new line ValueError if max_width is less than 1 """ + import io from . import ansi # Handle tabs @@ -866,12 +874,48 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: if max_width < 1: raise ValueError("max_width must be at least 1") - if ansi.style_aware_wcswidth(line) > max_width: - # Remove characters until we fit. Leave room for the ellipsis. - line = line[:max_width - 1] - while ansi.style_aware_wcswidth(line) > max_width - 1: - line = line[:-1] + if ansi.style_aware_wcswidth(line) <= max_width: + return line + + # Find all style sequences in the line + start = 0 + styles = collections.OrderedDict() + while True: + match = ansi.ANSI_STYLE_RE.search(line, start) + if match is None: + break + styles[match.start()] = match.group() + start += len(match.group()) + + # Add characters one by one and preserve all style sequences + done = False + index = 0 + total_width = 0 + truncated_buf = io.StringIO() + + while not done: + # Check if a style sequence is at this index. These don't count toward display width. + if index in styles: + truncated_buf.write(styles[index]) + style_len = len(styles[index]) + styles.pop(index) + index += style_len + continue + + char = line[index] + char_width = ansi.style_aware_wcswidth(char) + + # This char will make the text too wide, add the ellipsis instead + if char_width + total_width >= max_width: + char = constants.HORIZONTAL_ELLIPSIS + char_width = ansi.style_aware_wcswidth(char) + done = True + + total_width += char_width + truncated_buf.write(char) + index += 1 - line += "\N{HORIZONTAL ELLIPSIS}" + # Append remaining style sequences from original string + truncated_buf.write(''.join(styles.values())) - return line + return truncated_buf.getvalue() |