summaryrefslogtreecommitdiff
path: root/cmd2/utils.py
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2020-04-09 16:37:46 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2020-04-09 16:37:46 -0400
commitd815d2cd19a24bac89bd19416fb2b7cd0dadfe03 (patch)
tree2570d13b2561822911b000b36519be742ec89345 /cmd2/utils.py
parent0d4be64b6ec76fcf5f87933293dbc7c134a32cf0 (diff)
downloadcmd2-git-d815d2cd19a24bac89bd19416fb2b7cd0dadfe03.tar.gz
Initial commit of table creation API
Diffstat (limited to 'cmd2/utils.py')
-rw-r--r--cmd2/utils.py73
1 files changed, 56 insertions, 17 deletions
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 8b5e9cc8..78d39863 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -717,7 +717,7 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
:param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character)
:param width: display width of the aligned text. Defaults to width of the terminal.
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
- be converted to a space.
+ be converted to one space.
:param truncate: if True, then each line will be shortened to fit within the display width. The truncated
portions are replaced by a '…' character. Defaults to False.
:return: aligned text
@@ -738,8 +738,7 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
# Handle tabs
text = text.replace('\t', ' ' * tab_width)
- if fill_char == '\t':
- fill_char = ' '
+ fill_char = fill_char.replace('\t', ' ')
if len(ansi.strip_style(fill_char)) != 1:
raise TypeError("Fill character must be exactly one character long")
@@ -755,6 +754,28 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
text_buf = io.StringIO()
+ # ANSI style sequences that may affect future lines will be cancelled by the fill_char's style.
+ # To avoid this, we save the state of a line's style so we can restore it when beginning the next line.
+ # This also allows the lines to be used independently and still have their style. TableCreator does this.
+ aggregate_styles = ''
+
+ # Save the ANSI style sequences in fill_char
+ fill_char_styles = get_styles_in_text(fill_char)
+
+ # Create a space with the same style as fill_char for cases in which
+ # fill_char does not divide evenly into the gap.
+ styled_space = ''
+ char_index = 0
+ while char_index < len(fill_char):
+ if char_index in fill_char_styles:
+ # Preserve this style in styled_space
+ styled_space += fill_char_styles[char_index]
+ char_index += len(fill_char_styles[char_index])
+ else:
+ # We've reached the visible fill_char. Replace it with a space.
+ styled_space += ' '
+ char_index += 1
+
for index, line in enumerate(lines):
if index > 0:
text_buf.write('\n')
@@ -766,13 +787,16 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
if line_width == -1:
raise(ValueError("Text to align contains an unprintable character"))
- elif line_width >= width:
- # No need to add fill characters
- text_buf.write(line)
- continue
+ # Get the styles in this line
+ line_styles = get_styles_in_text(line)
# Calculate how wide each side of filling needs to be
- total_fill_width = width - line_width
+ if line_width >= width:
+ # Don't return here even though the line needs no fill chars.
+ # There may be styles sequences to restore.
+ total_fill_width = 0
+ else:
+ total_fill_width = width - line_width
if alignment == TextAlignment.LEFT:
left_fill_width = 0
@@ -789,11 +813,25 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
right_fill = (right_fill_width // fill_char_width) * fill_char
# In cases where the fill character display width didn't divide evenly into
- # the gaps being filled, pad the remainder with spaces.
- left_fill += ' ' * (left_fill_width - ansi.style_aware_wcswidth(left_fill))
- right_fill += ' ' * (right_fill_width - ansi.style_aware_wcswidth(right_fill))
+ # the gap being filled, pad the remainder with styled_space.
+ left_fill += styled_space * (left_fill_width - ansi.style_aware_wcswidth(left_fill))
+ right_fill += styled_space * (right_fill_width - ansi.style_aware_wcswidth(right_fill))
+
+ # Don't allow styles in fill_char and text to affect one another
+ if fill_char_styles or aggregate_styles or line_styles:
+ if left_fill:
+ left_fill = ansi.RESET_ALL + left_fill
+ left_fill += ansi.RESET_ALL
+
+ if right_fill:
+ right_fill = ansi.RESET_ALL + right_fill
+ right_fill += ansi.RESET_ALL
+
+ # Write the line and restore any styles from previous lines
+ text_buf.write(left_fill + aggregate_styles + line + right_fill)
- text_buf.write(left_fill + line + right_fill)
+ # Update the aggregate with styles in this line
+ aggregate_styles += ''.join(line_styles.values())
return text_buf.getvalue()
@@ -809,7 +847,7 @@ def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
:param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character)
:param width: display width of the aligned text. Defaults to width of the terminal.
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
- be converted to a space.
+ be converted to one space.
:param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
replaced by a '…' character. Defaults to False.
:return: left-aligned text
@@ -832,7 +870,7 @@ def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None
:param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character)
:param width: display width of the aligned text. Defaults to width of the terminal.
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
- be converted to a space.
+ be converted to one space.
:param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
replaced by a '…' character. Defaults to False.
:return: centered text
@@ -855,7 +893,7 @@ def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None,
:param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character)
:param width: display width of the aligned text. Defaults to width of the terminal.
:param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
- be converted to a space.
+ be converted to one space.
:param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is
replaced by a '…' character. Defaults to False.
:return: right-aligned text
@@ -878,13 +916,14 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str:
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.
+ sequences makes sure the style is in the same state had the entire string been printed. align_text() relies on this
+ behavior when preserving style over multiple lines.
:param line: text to truncate
:param max_width: the maximum display width the resulting string is allowed to have
:param tab_width: any tabs in the text will be replaced with this many spaces
:return: line that has a display width less than or equal to width
- :raises: ValueError if text contains an unprintable character like a new line
+ :raises: ValueError if text contains an unprintable character like a newline
ValueError if max_width is less than 1
"""
import io