@@ -638,7 +638,7 @@ class TextAlignment(Enum):
638638
639639
640640def 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 each line will be shortened to fit within the display width. The truncated
656+ portions are 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_line (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_line (line : 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 line: 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: line 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+ line = line .replace ('\t ' , ' ' * tab_width )
821+
822+ if ansi .style_aware_wcswidth (line ) == - 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 (line ) > max_width :
829+ # Remove characters until we fit. Leave room for the ellipsis.
830+ line = line [:max_width - 1 ]
831+ while ansi .style_aware_wcswidth (line ) > max_width - 1 :
832+ line = line [:- 1 ]
833+
834+ line += "\N{HORIZONTAL ELLIPSIS} "
835+
836+ return line
0 commit comments