summaryrefslogtreecommitdiffstats
path: root/terminaltables3/width_and_alignment.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-12-23 18:53:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-12-23 18:53:19 +0000
commit38d39f536e5a6cff5a218c5dc16994d6eee087e9 (patch)
tree151b916ddadb35113bac2cd954d99ea26ec64cc0 /terminaltables3/width_and_alignment.py
parentUpdating source url in copyright. (diff)
downloadterminaltables-38d39f536e5a6cff5a218c5dc16994d6eee087e9.tar.xz
terminaltables-38d39f536e5a6cff5a218c5dc16994d6eee087e9.zip
Merging upstream version 4.0.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'terminaltables3/width_and_alignment.py')
-rw-r--r--terminaltables3/width_and_alignment.py196
1 files changed, 196 insertions, 0 deletions
diff --git a/terminaltables3/width_and_alignment.py b/terminaltables3/width_and_alignment.py
new file mode 100644
index 0000000..6f7d807
--- /dev/null
+++ b/terminaltables3/width_and_alignment.py
@@ -0,0 +1,196 @@
+"""Functions that handle alignment, padding, widths, etc."""
+
+import re
+import unicodedata
+from typing import Sequence, Tuple
+
+from terminaltables3.terminal_io import terminal_size
+
+RE_COLOR_ANSI = re.compile(r"(\033\[[\d;]+m)")
+
+
+def visible_width(string: str) -> int:
+ """Get the visible width of a unicode string.
+
+ Some CJK unicode characters are more than one byte unlike ASCII and latin unicode characters.
+
+ From: https://github.com/Robpol86/terminaltables3/pull/9
+
+ :param str string: String to measure.
+
+ :return: String's width.
+ :rtype: int
+ """
+ if "\033" in string:
+ string = RE_COLOR_ANSI.sub("", string)
+
+ # Convert to unicode.
+ try:
+ string = string.decode("u8")
+ except (AttributeError, UnicodeEncodeError):
+ pass
+
+ width = 0
+ for char in string:
+ if unicodedata.east_asian_width(char) in ("F", "W"):
+ width += 2
+ else:
+ width += 1
+
+ return width
+
+
+def align_and_pad_cell(
+ string: str,
+ align: Tuple,
+ inner_dimensions: Tuple,
+ padding: Sequence[int],
+ space: str = " ",
+) -> list[str]:
+ """Align a string horizontally and vertically. Also add additional padding in both dimensions.
+
+ :param str string: Input string to operate on.
+ :param tuple align: Tuple that contains one of left/center/right and/or top/middle/bottom.
+ :param tuple inner_dimensions: Width and height ints to expand string to without padding.
+ :param iter padding: Number of space chars for left, right, top, and bottom (4 ints).
+ :param str space: Character to use as white space for resizing/padding (use single visible chars only).
+
+ :return: Padded cell split into lines.
+ :rtype: list
+ """
+ if not hasattr(string, "splitlines"):
+ string = str(string)
+
+ # Handle trailing newlines or empty strings, str.splitlines() does not satisfy.
+ lines = string.splitlines() or [""]
+ if string.endswith("\n"):
+ lines.append("")
+
+ # Vertically align and pad.
+ if "bottom" in align:
+ lines = (
+ ([""] * (inner_dimensions[1] - len(lines) + padding[2]))
+ + lines
+ + ([""] * padding[3])
+ )
+ elif "middle" in align:
+ delta = inner_dimensions[1] - len(lines)
+ lines = (
+ ([""] * (delta // 2 + delta % 2 + padding[2]))
+ + lines
+ + ([""] * (delta // 2 + padding[3]))
+ )
+ else:
+ lines = (
+ ([""] * padding[2])
+ + lines
+ + ([""] * (inner_dimensions[1] - len(lines) + padding[3]))
+ )
+
+ # Horizontally align and pad.
+ for i, line in enumerate(lines):
+ new_width = inner_dimensions[0] + len(line) - visible_width(line)
+ if "right" in align:
+ lines[i] = line.rjust(padding[0] + new_width, space) + (space * padding[1])
+ elif "center" in align:
+ lines[i] = (
+ (space * padding[0])
+ + line.center(new_width, space)
+ + (space * padding[1])
+ )
+ else:
+ lines[i] = (space * padding[0]) + line.ljust(new_width + padding[1], space)
+
+ return lines
+
+
+def max_dimensions(
+ table_data, padding_left=0, padding_right=0, padding_top=0, padding_bottom=0
+):
+ """Get maximum widths of each column and maximum height of each row.
+
+ :param iter table_data: List of list of strings (unmodified table data).
+ :param int padding_left: Number of space chars on left side of cell.
+ :param int padding_right: Number of space chars on right side of cell.
+ :param int padding_top: Number of empty lines on top side of cell.
+ :param int padding_bottom: Number of empty lines on bottom side of cell.
+
+ :return: 4-item tuple of n-item lists. Inner column widths and row heights, outer column widths and row heights.
+ :rtype: tuple
+ """
+ inner_widths = [0] * (max(len(r) for r in table_data) if table_data else 0)
+ inner_heights = [0] * len(table_data)
+
+ # Find max width and heights.
+ for j, row in enumerate(table_data):
+ for i, cell in enumerate(row):
+ if not hasattr(cell, "count") or not hasattr(cell, "splitlines"):
+ cell = str(cell)
+ if not cell:
+ continue
+ inner_heights[j] = max(inner_heights[j], cell.count("\n") + 1)
+ inner_widths[i] = max(
+ inner_widths[i],
+ *[visible_width(the_line) for the_line in cell.splitlines()]
+ )
+
+ # Calculate with padding.
+ outer_widths = [padding_left + i + padding_right for i in inner_widths]
+ outer_heights = [padding_top + i + padding_bottom for i in inner_heights]
+
+ return inner_widths, inner_heights, outer_widths, outer_heights
+
+
+def column_max_width(
+ inner_widths: Sequence[int],
+ column_number: int,
+ outer_border: int,
+ inner_border: int,
+ padding: int,
+) -> int:
+ """Determine the maximum width of a column based on the current terminal width.
+
+ :param iter inner_widths: List of widths (no padding) for each column.
+ :param int column_number: The column number to query.
+ :param int outer_border: Sum of left and right outer border visible widths.
+ :param int inner_border: Visible width of the inner border character.
+ :param int padding: Total padding per cell (left + right padding).
+
+ :return: The maximum width the column can be without causing line wrapping.
+ """
+ column_count = len(inner_widths)
+ terminal_width = terminal_size()[0]
+
+ # Count how much space padding, outer, and inner borders take up.
+ non_data_space = outer_border
+ non_data_space += inner_border * (column_count - 1)
+ non_data_space += column_count * padding
+
+ # Exclude selected column's width.
+ data_space = sum(inner_widths) - inner_widths[column_number]
+
+ return terminal_width - data_space - non_data_space
+
+
+def table_width(
+ outer_widths: Sequence[int], outer_border: int, inner_border: int
+) -> int:
+ """Determine the width of the entire table including borders and padding.
+
+ :param iter outer_widths: List of widths (with padding) for each column.
+ :param int outer_border: Sum of left and right outer border visible widths.
+ :param int inner_border: Visible width of the inner border character.
+
+ :return: The width of the table.
+ :rtype: int
+ """
+ column_count = len(outer_widths)
+
+ # Count how much space outer and inner borders take up.
+ non_data_space = outer_border
+ if column_count:
+ non_data_space += inner_border * (column_count - 1)
+
+ # Space of all columns and their padding.
+ data_space = sum(outer_widths)
+ return data_space + non_data_space