"""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