Skip to content

Commit 70f10c6

Browse files
committed
Added string truncation function and support for it in the alignment functions
1 parent 2ffdefb commit 70f10c6

File tree

1 file changed

+71
-14
lines changed

1 file changed

+71
-14
lines changed

cmd2/utils.py

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ class TextAlignment(Enum):
638638

639639

640640
def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
641-
width: Optional[int] = None, tab_width: int = 4) -> str:
641+
width: Optional[int] = None, tab_width: int = 4, truncate: bool = False) -> str:
642642
"""
643643
Align text for display within a given width. Supports characters with display widths greater than 1.
644644
ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
@@ -652,15 +652,24 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
652652
:param width: display width of the aligned text. Defaults to width of the terminal.
653653
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
654654
be converted to a space.
655+
:param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
656+
replaced by a '…' character. Defaults to False.
655657
:return: aligned text
656658
:raises: TypeError if fill_char is more than one character
657659
ValueError if text or fill_char contains an unprintable character
660+
ValueError if width is less than 1
658661
"""
659662
import io
660663
import shutil
661664

662665
from . import ansi
663666

667+
if width is None:
668+
width = shutil.get_terminal_size().columns
669+
670+
if width <= 1:
671+
raise ValueError("width must be at least 1")
672+
664673
# Handle tabs
665674
text = text.replace('\t', ' ' * tab_width)
666675
if fill_char == '\t':
@@ -678,23 +687,21 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
678687
else:
679688
lines = ['']
680689

681-
if width is None:
682-
width = shutil.get_terminal_size().columns
683-
684690
text_buf = io.StringIO()
685691

686692
for index, line in enumerate(lines):
687693
if index > 0:
688694
text_buf.write('\n')
689695

690-
# Use style_aware_wcswidth to support characters with display widths
691-
# greater than 1 as well as ANSI style sequences
696+
if truncate:
697+
line = truncate_string(line, width)
698+
692699
line_width = ansi.style_aware_wcswidth(line)
693700
if line_width == -1:
694701
raise(ValueError("Text to align contains an unprintable character"))
695702

696-
# Check if line is wider than the desired final width
697-
if width <= line_width:
703+
elif line_width >= width:
704+
# No need to add fill characters
698705
text_buf.write(line)
699706
continue
700707

@@ -725,7 +732,8 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
725732
return text_buf.getvalue()
726733

727734

728-
def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str:
735+
def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
736+
tab_width: int = 4, truncate: bool = False) -> str:
729737
"""
730738
Left align text for display within a given width. Supports characters with display widths greater than 1.
731739
ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
@@ -736,14 +744,19 @@ def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
736744
:param width: display width of the aligned text. Defaults to width of the terminal.
737745
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
738746
be converted to a space.
747+
:param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
748+
replaced by a '…' character. Defaults to False.
739749
:return: left-aligned text
740750
:raises: TypeError if fill_char is more than one character
741751
ValueError if text or fill_char contains an unprintable character
752+
ValueError if width is less than 1
742753
"""
743-
return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=tab_width)
754+
return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width,
755+
tab_width=tab_width, truncate=truncate)
744756

745757

746-
def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str:
758+
def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
759+
tab_width: int = 4, truncate: bool = False) -> str:
747760
"""
748761
Center text for display within a given width. Supports characters with display widths greater than 1.
749762
ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
@@ -754,14 +767,19 @@ def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None
754767
:param width: display width of the aligned text. Defaults to width of the terminal.
755768
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
756769
be converted to a space.
770+
:param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
771+
replaced by a '…' character. Defaults to False.
757772
:return: centered text
758773
:raises: TypeError if fill_char is more than one character
759774
ValueError if text or fill_char contains an unprintable character
775+
ValueError if width is less than 1
760776
"""
761-
return align_text(text, TextAlignment.CENTER, fill_char=fill_char, width=width, tab_width=tab_width)
777+
return align_text(text, TextAlignment.CENTER, fill_char=fill_char, width=width,
778+
tab_width=tab_width, truncate=truncate)
762779

763780

764-
def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str:
781+
def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
782+
tab_width: int = 4, truncate: bool = False) -> str:
765783
"""
766784
Right align text for display within a given width. Supports characters with display widths greater than 1.
767785
ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is
@@ -772,8 +790,47 @@ def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
772790
:param width: display width of the aligned text. Defaults to width of the terminal.
773791
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
774792
be converted to a space.
793+
:param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
794+
replaced by a '…' character. Defaults to False.
775795
:return: right-aligned text
776796
:raises: TypeError if fill_char is more than one character
777797
ValueError if text or fill_char contains an unprintable character
798+
ValueError if width is less than 1
799+
"""
800+
return align_text(text, TextAlignment.RIGHT, fill_char=fill_char, width=width,
801+
tab_width=tab_width, truncate=truncate)
802+
803+
804+
def truncate_string(text: str, max_width: int, *, tab_width: int = 4) -> str:
778805
"""
779-
return align_text(text, TextAlignment.RIGHT, fill_char=fill_char, width=width, tab_width=tab_width)
806+
Truncate a single line to fit within a given display width. Any portion of the string that is truncated
807+
is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences are
808+
safely ignored and do not count toward the display width. This means colored text is supported.
809+
810+
:param text: text to truncate
811+
:param max_width: the maximum display width the resulting string is allowed to have
812+
:param tab_width: any tabs in the text will be replaced with this many spaces
813+
:return: string that has a display width less than or equal to width
814+
:raises: ValueError if text contains an unprintable character like a new line
815+
ValueError if max_width is less than 1
816+
"""
817+
from . import ansi
818+
819+
# Handle tabs
820+
text = text.replace('\t', ' ' * tab_width)
821+
822+
if ansi.style_aware_wcswidth(text) == -1:
823+
raise (ValueError("text contains an unprintable character"))
824+
825+
if max_width <= 1:
826+
raise ValueError("max_width must be at least 1")
827+
828+
if ansi.style_aware_wcswidth(text) > max_width:
829+
# Remove characters until we fit. Leave room for the ellipsis.
830+
text = text[:max_width - 1]
831+
while ansi.style_aware_wcswidth(text) > max_width - 1:
832+
text = text[:-1]
833+
834+
text += "\N{HORIZONTAL ELLIPSIS}"
835+
836+
return text

0 commit comments

Comments
 (0)