summaryrefslogtreecommitdiffstats
path: root/terminaltables
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2022-09-16 09:09:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2022-09-16 09:09:35 +0000
commit0dfe1c9e2780469e3a4696e8fb3e6f717a7ebeb7 (patch)
treea0b651b55ea02e3b00bbc5eedba566fdd6bd7c08 /terminaltables
parentInitial commit. (diff)
downloadterminaltables-0dfe1c9e2780469e3a4696e8fb3e6f717a7ebeb7.tar.xz
terminaltables-0dfe1c9e2780469e3a4696e8fb3e6f717a7ebeb7.zip
Adding upstream version 3.1.0.upstream/3.1.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'terminaltables')
-rw-r--r--terminaltables/__init__.py17
-rw-r--r--terminaltables/ascii_table.py55
-rw-r--r--terminaltables/base_table.py217
-rw-r--r--terminaltables/build.py151
-rw-r--r--terminaltables/github_table.py70
-rw-r--r--terminaltables/other_tables.py177
-rw-r--r--terminaltables/terminal_io.py98
-rw-r--r--terminaltables/width_and_alignment.py160
8 files changed, 945 insertions, 0 deletions
diff --git a/terminaltables/__init__.py b/terminaltables/__init__.py
new file mode 100644
index 0000000..6cea813
--- /dev/null
+++ b/terminaltables/__init__.py
@@ -0,0 +1,17 @@
+"""Generate simple tables in terminals from a nested list of strings.
+
+Use SingleTable or DoubleTable instead of AsciiTable for box-drawing characters.
+
+https://github.com/Robpol86/terminaltables
+https://pypi.python.org/pypi/terminaltables
+"""
+
+from terminaltables.ascii_table import AsciiTable # noqa
+from terminaltables.github_table import GithubFlavoredMarkdownTable # noqa
+from terminaltables.other_tables import DoubleTable # noqa
+from terminaltables.other_tables import SingleTable # noqa
+from terminaltables.other_tables import PorcelainTable # noqa
+
+__author__ = '@Robpol86'
+__license__ = 'MIT'
+__version__ = '3.1.0'
diff --git a/terminaltables/ascii_table.py b/terminaltables/ascii_table.py
new file mode 100644
index 0000000..3623918
--- /dev/null
+++ b/terminaltables/ascii_table.py
@@ -0,0 +1,55 @@
+"""AsciiTable is the main table class. To be inherited by other tables. Define convenience methods here."""
+
+from terminaltables.base_table import BaseTable
+from terminaltables.terminal_io import terminal_size
+from terminaltables.width_and_alignment import column_max_width, max_dimensions, table_width
+
+
+class AsciiTable(BaseTable):
+ """Draw a table using regular ASCII characters, such as ``+``, ``|``, and ``-``.
+
+ :ivar iter table_data: List (empty or list of lists of strings) representing the table.
+ :ivar str title: Optional title to show within the top border of the table.
+ :ivar bool inner_column_border: Separates columns.
+ :ivar bool inner_footing_row_border: Show a border before the last row.
+ :ivar bool inner_heading_row_border: Show a border after the first row.
+ :ivar bool inner_row_border: Show a border in between every row.
+ :ivar bool outer_border: Show the top, left, right, and bottom border.
+ :ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
+ :ivar int padding_left: Number of spaces to pad on the left side of every cell.
+ :ivar int padding_right: Number of spaces to pad on the right side of every cell.
+ """
+
+ def column_max_width(self, column_number):
+ """Return the maximum width of a column based on the current terminal width.
+
+ :param int column_number: The column number to query.
+
+ :return: The max width of the column.
+ :rtype: int
+ """
+ inner_widths = max_dimensions(self.table_data)[0]
+ outer_border = 2 if self.outer_border else 0
+ inner_border = 1 if self.inner_column_border else 0
+ padding = self.padding_left + self.padding_right
+ return column_max_width(inner_widths, column_number, outer_border, inner_border, padding)
+
+ @property
+ def column_widths(self):
+ """Return a list of integers representing the widths of each table column without padding."""
+ if not self.table_data:
+ return list()
+ return max_dimensions(self.table_data)[0]
+
+ @property
+ def ok(self): # Too late to change API. # pylint: disable=invalid-name
+ """Return True if the table fits within the terminal width, False if the table breaks."""
+ return self.table_width <= terminal_size()[0]
+
+ @property
+ def table_width(self):
+ """Return the width of the table including padding and borders."""
+ outer_widths = max_dimensions(self.table_data, self.padding_left, self.padding_right)[2]
+ outer_border = 2 if self.outer_border else 0
+ inner_border = 1 if self.inner_column_border else 0
+ return table_width(outer_widths, outer_border, inner_border)
diff --git a/terminaltables/base_table.py b/terminaltables/base_table.py
new file mode 100644
index 0000000..281d5a3
--- /dev/null
+++ b/terminaltables/base_table.py
@@ -0,0 +1,217 @@
+"""Base table class. Define just the bare minimum to build tables."""
+
+from terminaltables.build import build_border, build_row, flatten
+from terminaltables.width_and_alignment import align_and_pad_cell, max_dimensions
+
+
+class BaseTable(object):
+ """Base table class.
+
+ :ivar iter table_data: List (empty or list of lists of strings) representing the table.
+ :ivar str title: Optional title to show within the top border of the table.
+ :ivar bool inner_column_border: Separates columns.
+ :ivar bool inner_footing_row_border: Show a border before the last row.
+ :ivar bool inner_heading_row_border: Show a border after the first row.
+ :ivar bool inner_row_border: Show a border in between every row.
+ :ivar bool outer_border: Show the top, left, right, and bottom border.
+ :ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
+ :ivar int padding_left: Number of spaces to pad on the left side of every cell.
+ :ivar int padding_right: Number of spaces to pad on the right side of every cell.
+ """
+
+ CHAR_F_INNER_HORIZONTAL = '-'
+ CHAR_F_INNER_INTERSECT = '+'
+ CHAR_F_INNER_VERTICAL = '|'
+ CHAR_F_OUTER_LEFT_INTERSECT = '+'
+ CHAR_F_OUTER_LEFT_VERTICAL = '|'
+ CHAR_F_OUTER_RIGHT_INTERSECT = '+'
+ CHAR_F_OUTER_RIGHT_VERTICAL = '|'
+ CHAR_H_INNER_HORIZONTAL = '-'
+ CHAR_H_INNER_INTERSECT = '+'
+ CHAR_H_INNER_VERTICAL = '|'
+ CHAR_H_OUTER_LEFT_INTERSECT = '+'
+ CHAR_H_OUTER_LEFT_VERTICAL = '|'
+ CHAR_H_OUTER_RIGHT_INTERSECT = '+'
+ CHAR_H_OUTER_RIGHT_VERTICAL = '|'
+ CHAR_INNER_HORIZONTAL = '-'
+ CHAR_INNER_INTERSECT = '+'
+ CHAR_INNER_VERTICAL = '|'
+ CHAR_OUTER_BOTTOM_HORIZONTAL = '-'
+ CHAR_OUTER_BOTTOM_INTERSECT = '+'
+ CHAR_OUTER_BOTTOM_LEFT = '+'
+ CHAR_OUTER_BOTTOM_RIGHT = '+'
+ CHAR_OUTER_LEFT_INTERSECT = '+'
+ CHAR_OUTER_LEFT_VERTICAL = '|'
+ CHAR_OUTER_RIGHT_INTERSECT = '+'
+ CHAR_OUTER_RIGHT_VERTICAL = '|'
+ CHAR_OUTER_TOP_HORIZONTAL = '-'
+ CHAR_OUTER_TOP_INTERSECT = '+'
+ CHAR_OUTER_TOP_LEFT = '+'
+ CHAR_OUTER_TOP_RIGHT = '+'
+
+ def __init__(self, table_data, title=None):
+ """Constructor.
+
+ :param iter table_data: List (empty or list of lists of strings) representing the table.
+ :param title: Optional title to show within the top border of the table.
+ """
+ self.table_data = table_data
+ self.title = title
+
+ self.inner_column_border = True
+ self.inner_footing_row_border = False
+ self.inner_heading_row_border = True
+ self.inner_row_border = False
+ self.outer_border = True
+
+ self.justify_columns = dict() # {0: 'right', 1: 'left', 2: 'center'}
+ self.padding_left = 1
+ self.padding_right = 1
+
+ def horizontal_border(self, style, outer_widths):
+ """Build any kind of horizontal border for the table.
+
+ :param str style: Type of border to return.
+ :param iter outer_widths: List of widths (with padding) for each column.
+
+ :return: Prepared border as a tuple of strings.
+ :rtype: tuple
+ """
+ if style == 'top':
+ horizontal = self.CHAR_OUTER_TOP_HORIZONTAL
+ left = self.CHAR_OUTER_TOP_LEFT
+ intersect = self.CHAR_OUTER_TOP_INTERSECT if self.inner_column_border else ''
+ right = self.CHAR_OUTER_TOP_RIGHT
+ title = self.title
+ elif style == 'bottom':
+ horizontal = self.CHAR_OUTER_BOTTOM_HORIZONTAL
+ left = self.CHAR_OUTER_BOTTOM_LEFT
+ intersect = self.CHAR_OUTER_BOTTOM_INTERSECT if self.inner_column_border else ''
+ right = self.CHAR_OUTER_BOTTOM_RIGHT
+ title = None
+ elif style == 'heading':
+ horizontal = self.CHAR_H_INNER_HORIZONTAL
+ left = self.CHAR_H_OUTER_LEFT_INTERSECT if self.outer_border else ''
+ intersect = self.CHAR_H_INNER_INTERSECT if self.inner_column_border else ''
+ right = self.CHAR_H_OUTER_RIGHT_INTERSECT if self.outer_border else ''
+ title = None
+ elif style == 'footing':
+ horizontal = self.CHAR_F_INNER_HORIZONTAL
+ left = self.CHAR_F_OUTER_LEFT_INTERSECT if self.outer_border else ''
+ intersect = self.CHAR_F_INNER_INTERSECT if self.inner_column_border else ''
+ right = self.CHAR_F_OUTER_RIGHT_INTERSECT if self.outer_border else ''
+ title = None
+ else:
+ horizontal = self.CHAR_INNER_HORIZONTAL
+ left = self.CHAR_OUTER_LEFT_INTERSECT if self.outer_border else ''
+ intersect = self.CHAR_INNER_INTERSECT if self.inner_column_border else ''
+ right = self.CHAR_OUTER_RIGHT_INTERSECT if self.outer_border else ''
+ title = None
+ return build_border(outer_widths, horizontal, left, intersect, right, title)
+
+ def gen_row_lines(self, row, style, inner_widths, height):
+ r"""Combine cells in row and group them into lines with vertical borders.
+
+ Caller is expected to pass yielded lines to ''.join() to combine them into a printable line. Caller must append
+ newline character to the end of joined line.
+
+ In:
+ ['Row One Column One', 'Two', 'Three']
+ Out:
+ [
+ ('|', ' Row One Column One ', '|', ' Two ', '|', ' Three ', '|'),
+ ]
+
+ In:
+ ['Row One\nColumn One', 'Two', 'Three'],
+ Out:
+ [
+ ('|', ' Row One ', '|', ' Two ', '|', ' Three ', '|'),
+ ('|', ' Column One ', '|', ' ', '|', ' ', '|'),
+ ]
+
+ :param iter row: One row in the table. List of cells.
+ :param str style: Type of border characters to use.
+ :param iter inner_widths: List of widths (no padding) for each column.
+ :param int height: Inner height (no padding) (number of lines) to expand row to.
+
+ :return: Yields lines split into components in a list. Caller must ''.join() line.
+ """
+ cells_in_row = list()
+
+ # Resize row if it doesn't have enough cells.
+ if len(row) != len(inner_widths):
+ row = row + [''] * (len(inner_widths) - len(row))
+
+ # Pad and align each cell. Split each cell into lines to support multi-line cells.
+ for i, cell in enumerate(row):
+ align = (self.justify_columns.get(i),)
+ inner_dimensions = (inner_widths[i], height)
+ padding = (self.padding_left, self.padding_right, 0, 0)
+ cells_in_row.append(align_and_pad_cell(cell, align, inner_dimensions, padding))
+
+ # Determine border characters.
+ if style == 'heading':
+ left = self.CHAR_H_OUTER_LEFT_VERTICAL if self.outer_border else ''
+ center = self.CHAR_H_INNER_VERTICAL if self.inner_column_border else ''
+ right = self.CHAR_H_OUTER_RIGHT_VERTICAL if self.outer_border else ''
+ elif style == 'footing':
+ left = self.CHAR_F_OUTER_LEFT_VERTICAL if self.outer_border else ''
+ center = self.CHAR_F_INNER_VERTICAL if self.inner_column_border else ''
+ right = self.CHAR_F_OUTER_RIGHT_VERTICAL if self.outer_border else ''
+ else:
+ left = self.CHAR_OUTER_LEFT_VERTICAL if self.outer_border else ''
+ center = self.CHAR_INNER_VERTICAL if self.inner_column_border else ''
+ right = self.CHAR_OUTER_RIGHT_VERTICAL if self.outer_border else ''
+
+ # Yield each line.
+ for line in build_row(cells_in_row, left, center, right):
+ yield line
+
+ def gen_table(self, inner_widths, inner_heights, outer_widths):
+ """Combine everything and yield every line of the entire table with borders.
+
+ :param iter inner_widths: List of widths (no padding) for each column.
+ :param iter inner_heights: List of heights (no padding) for each row.
+ :param iter outer_widths: List of widths (with padding) for each column.
+ :return:
+ """
+ # Yield top border.
+ if self.outer_border:
+ yield self.horizontal_border('top', outer_widths)
+
+ # Yield table body.
+ row_count = len(self.table_data)
+ last_row_index, before_last_row_index = row_count - 1, row_count - 2
+ for i, row in enumerate(self.table_data):
+ # Yield the row line by line (e.g. multi-line rows).
+ if self.inner_heading_row_border and i == 0:
+ style = 'heading'
+ elif self.inner_footing_row_border and i == last_row_index:
+ style = 'footing'
+ else:
+ style = 'row'
+ for line in self.gen_row_lines(row, style, inner_widths, inner_heights[i]):
+ yield line
+ # If this is the last row then break. No separator needed.
+ if i == last_row_index:
+ break
+ # Yield heading separator.
+ if self.inner_heading_row_border and i == 0:
+ yield self.horizontal_border('heading', outer_widths)
+ # Yield footing separator.
+ elif self.inner_footing_row_border and i == before_last_row_index:
+ yield self.horizontal_border('footing', outer_widths)
+ # Yield row separator.
+ elif self.inner_row_border:
+ yield self.horizontal_border('row', outer_widths)
+
+ # Yield bottom border.
+ if self.outer_border:
+ yield self.horizontal_border('bottom', outer_widths)
+
+ @property
+ def table(self):
+ """Return a large string of the entire table ready to be printed to the terminal."""
+ dimensions = max_dimensions(self.table_data, self.padding_left, self.padding_right)[:3]
+ return flatten(self.gen_table(*dimensions))
diff --git a/terminaltables/build.py b/terminaltables/build.py
new file mode 100644
index 0000000..6b23b2f
--- /dev/null
+++ b/terminaltables/build.py
@@ -0,0 +1,151 @@
+"""Combine cells into rows."""
+
+from terminaltables.width_and_alignment import visible_width
+
+
+def combine(line, left, intersect, right):
+ """Zip borders between items in `line`.
+
+ e.g. ('l', '1', 'c', '2', 'c', '3', 'r')
+
+ :param iter line: List to iterate.
+ :param left: Left border.
+ :param intersect: Column separator.
+ :param right: Right border.
+
+ :return: Yields combined objects.
+ """
+ # Yield left border.
+ if left:
+ yield left
+
+ # Yield items with intersect characters.
+ if intersect:
+ try:
+ for j, i in enumerate(line, start=-len(line) + 1):
+ yield i
+ if j:
+ yield intersect
+ except TypeError: # Generator.
+ try:
+ item = next(line)
+ except StopIteration: # Was empty all along.
+ pass
+ else:
+ while True:
+ yield item
+ try:
+ peek = next(line)
+ except StopIteration:
+ break
+ yield intersect
+ item = peek
+ else:
+ for i in line:
+ yield i
+
+ # Yield right border.
+ if right:
+ yield right
+
+
+def build_border(outer_widths, horizontal, left, intersect, right, title=None):
+ """Build the top/bottom/middle row. Optionally embed the table title within the border.
+
+ Title is hidden if it doesn't fit between the left/right characters/edges.
+
+ Example return value:
+ ('<', '-----', '+', '------', '+', '-------', '>')
+ ('<', 'My Table', '----', '+', '------->')
+
+ :param iter outer_widths: List of widths (with padding) for each column.
+ :param str horizontal: Character to stretch across each column.
+ :param str left: Left border.
+ :param str intersect: Column separator.
+ :param str right: Right border.
+ :param title: Overlay the title on the border between the left and right characters.
+
+ :return: Returns a generator of strings representing a border.
+ :rtype: iter
+ """
+ length = 0
+
+ # Hide title if it doesn't fit.
+ if title is not None and outer_widths:
+ try:
+ length = visible_width(title)
+ except TypeError:
+ title = str(title)
+ length = visible_width(title)
+ if length > sum(outer_widths) + len(intersect) * (len(outer_widths) - 1):
+ title = None
+
+ # Handle no title.
+ if title is None or not outer_widths or not horizontal:
+ return combine((horizontal * c for c in outer_widths), left, intersect, right)
+
+ # Handle title fitting in the first column.
+ if length == outer_widths[0]:
+ return combine([title] + [horizontal * c for c in outer_widths[1:]], left, intersect, right)
+ if length < outer_widths[0]:
+ columns = [title + horizontal * (outer_widths[0] - length)] + [horizontal * c for c in outer_widths[1:]]
+ return combine(columns, left, intersect, right)
+
+ # Handle wide titles/narrow columns.
+ columns_and_intersects = [title]
+ for width in combine(outer_widths, None, bool(intersect), None):
+ # If title is taken care of.
+ if length < 1:
+ columns_and_intersects.append(intersect if width is True else horizontal * width)
+ # If title's last character overrides an intersect character.
+ elif width is True and length == 1:
+ length = 0
+ # If this is an intersect character that is overridden by the title.
+ elif width is True:
+ length -= 1
+ # If title's last character is within a column.
+ elif width >= length:
+ columns_and_intersects[0] += horizontal * (width - length) # Append horizontal chars to title.
+ length = 0
+ # If remainder of title won't fit in a column.
+ else:
+ length -= width
+
+ return combine(columns_and_intersects, left, None, right)
+
+
+def build_row(row, left, center, right):
+ """Combine single or multi-lined cells into a single row of list of lists including borders.
+
+ Row must already be padded and extended so each cell has the same number of lines.
+
+ Example return value:
+ [
+ ['>', 'Left ', '|', 'Center', '|', 'Right', '<'],
+ ['>', 'Cell1', '|', 'Cell2 ', '|', 'Cell3', '<'],
+ ]
+
+ :param iter row: List of cells for one row.
+ :param str left: Left border.
+ :param str center: Column separator.
+ :param str right: Right border.
+
+ :return: Yields other generators that yield strings.
+ :rtype: iter
+ """
+ if not row or not row[0]:
+ yield combine((), left, center, right)
+ return
+ for row_index in range(len(row[0])):
+ yield combine((c[row_index] for c in row), left, center, right)
+
+
+def flatten(table):
+ """Flatten table data into a single string with newlines.
+
+ :param iter table: Padded and bordered table data.
+
+ :return: Joined rows/cells.
+ :rtype: str
+ """
+ return '\n'.join(''.join(r) for r in table)
diff --git a/terminaltables/github_table.py b/terminaltables/github_table.py
new file mode 100644
index 0000000..7eb1be7
--- /dev/null
+++ b/terminaltables/github_table.py
@@ -0,0 +1,70 @@
+"""GithubFlavoredMarkdownTable class."""
+
+from terminaltables.ascii_table import AsciiTable
+from terminaltables.build import combine
+
+
+class GithubFlavoredMarkdownTable(AsciiTable):
+ """Github flavored markdown table.
+
+ https://help.github.com/articles/github-flavored-markdown/#tables
+
+ :ivar iter table_data: List (empty or list of lists of strings) representing the table.
+ :ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
+ """
+
+ def __init__(self, table_data):
+ """Constructor.
+
+ :param iter table_data: List (empty or list of lists of strings) representing the table.
+ """
+ # Github flavored markdown table won't support title.
+ super(GithubFlavoredMarkdownTable, self).__init__(table_data)
+
+ def horizontal_border(self, _, outer_widths):
+ """Handle the GitHub heading border.
+
+ E.g.:
+ |:---|:---:|---:|----|
+
+ :param _: Unused.
+ :param iter outer_widths: List of widths (with padding) for each column.
+
+ :return: Prepared border strings in a generator.
+ :rtype: iter
+ """
+ horizontal = str(self.CHAR_INNER_HORIZONTAL)
+ left = self.CHAR_OUTER_LEFT_VERTICAL
+ intersect = self.CHAR_INNER_VERTICAL
+ right = self.CHAR_OUTER_RIGHT_VERTICAL
+
+ columns = list()
+ for i, width in enumerate(outer_widths):
+ justify = self.justify_columns.get(i)
+ width = max(3, width) # Width should be at least 3 so justification can be applied.
+ if justify == 'left':
+ columns.append(':' + horizontal * (width - 1))
+ elif justify == 'right':
+ columns.append(horizontal * (width - 1) + ':')
+ elif justify == 'center':
+ columns.append(':' + horizontal * (width - 2) + ':')
+ else:
+ columns.append(horizontal * width)
+
+ return combine(columns, left, intersect, right)
+
+ def gen_table(self, inner_widths, inner_heights, outer_widths):
+ """Combine everything and yield every line of the entire table with borders.
+
+ :param iter inner_widths: List of widths (no padding) for each column.
+ :param iter inner_heights: List of heights (no padding) for each row.
+ :param iter outer_widths: List of widths (with padding) for each column.
+ :return:
+ """
+ for i, row in enumerate(self.table_data):
+ # Yield the row line by line (e.g. multi-line rows).
+ for line in self.gen_row_lines(row, 'row', inner_widths, inner_heights[i]):
+ yield line
+ # Yield heading separator.
+ if i == 0:
+ yield self.horizontal_border(None, outer_widths)
diff --git a/terminaltables/other_tables.py b/terminaltables/other_tables.py
new file mode 100644
index 0000000..50c0bcd
--- /dev/null
+++ b/terminaltables/other_tables.py
@@ -0,0 +1,177 @@
+"""Additional simple tables defined here."""
+
+from terminaltables.ascii_table import AsciiTable
+from terminaltables.terminal_io import IS_WINDOWS
+
+
+class UnixTable(AsciiTable):
+ """Draw a table using box-drawing characters on Unix platforms. Table borders won't have any gaps between lines.
+
+ Similar to the tables shown on PC BIOS boot messages, but not double-lined.
+ """
+
+ CHAR_F_INNER_HORIZONTAL = '\033(0\x71\033(B'
+ CHAR_F_INNER_INTERSECT = '\033(0\x6e\033(B'
+ CHAR_F_INNER_VERTICAL = '\033(0\x78\033(B'
+ CHAR_F_OUTER_LEFT_INTERSECT = '\033(0\x74\033(B'
+ CHAR_F_OUTER_LEFT_VERTICAL = '\033(0\x78\033(B'
+ CHAR_F_OUTER_RIGHT_INTERSECT = '\033(0\x75\033(B'
+ CHAR_F_OUTER_RIGHT_VERTICAL = '\033(0\x78\033(B'
+ CHAR_H_INNER_HORIZONTAL = '\033(0\x71\033(B'
+ CHAR_H_INNER_INTERSECT = '\033(0\x6e\033(B'
+ CHAR_H_INNER_VERTICAL = '\033(0\x78\033(B'
+ CHAR_H_OUTER_LEFT_INTERSECT = '\033(0\x74\033(B'
+ CHAR_H_OUTER_LEFT_VERTICAL = '\033(0\x78\033(B'
+ CHAR_H_OUTER_RIGHT_INTERSECT = '\033(0\x75\033(B'
+ CHAR_H_OUTER_RIGHT_VERTICAL = '\033(0\x78\033(B'
+ CHAR_INNER_HORIZONTAL = '\033(0\x71\033(B'
+ CHAR_INNER_INTERSECT = '\033(0\x6e\033(B'
+ CHAR_INNER_VERTICAL = '\033(0\x78\033(B'
+ CHAR_OUTER_BOTTOM_HORIZONTAL = '\033(0\x71\033(B'
+ CHAR_OUTER_BOTTOM_INTERSECT = '\033(0\x76\033(B'
+ CHAR_OUTER_BOTTOM_LEFT = '\033(0\x6d\033(B'
+ CHAR_OUTER_BOTTOM_RIGHT = '\033(0\x6a\033(B'
+ CHAR_OUTER_LEFT_INTERSECT = '\033(0\x74\033(B'
+ CHAR_OUTER_LEFT_VERTICAL = '\033(0\x78\033(B'
+ CHAR_OUTER_RIGHT_INTERSECT = '\033(0\x75\033(B'
+ CHAR_OUTER_RIGHT_VERTICAL = '\033(0\x78\033(B'
+ CHAR_OUTER_TOP_HORIZONTAL = '\033(0\x71\033(B'
+ CHAR_OUTER_TOP_INTERSECT = '\033(0\x77\033(B'
+ CHAR_OUTER_TOP_LEFT = '\033(0\x6c\033(B'
+ CHAR_OUTER_TOP_RIGHT = '\033(0\x6b\033(B'
+
+ @property
+ def table(self):
+ """Return a large string of the entire table ready to be printed to the terminal."""
+ ascii_table = super(UnixTable, self).table
+ optimized = ascii_table.replace('\033(B\033(0', '')
+ return optimized
+
+
+class WindowsTable(AsciiTable):
+ """Draw a table using box-drawing characters on Windows platforms. This uses Code Page 437. Single-line borders.
+
+ From: http://en.wikipedia.org/wiki/Code_page_437#Characters
+ """
+
+ CHAR_F_INNER_HORIZONTAL = b'\xc4'.decode('ibm437')
+ CHAR_F_INNER_INTERSECT = b'\xc5'.decode('ibm437')
+ CHAR_F_INNER_VERTICAL = b'\xb3'.decode('ibm437')
+ CHAR_F_OUTER_LEFT_INTERSECT = b'\xc3'.decode('ibm437')
+ CHAR_F_OUTER_LEFT_VERTICAL = b'\xb3'.decode('ibm437')
+ CHAR_F_OUTER_RIGHT_INTERSECT = b'\xb4'.decode('ibm437')
+ CHAR_F_OUTER_RIGHT_VERTICAL = b'\xb3'.decode('ibm437')
+ CHAR_H_INNER_HORIZONTAL = b'\xc4'.decode('ibm437')
+ CHAR_H_INNER_INTERSECT = b'\xc5'.decode('ibm437')
+ CHAR_H_INNER_VERTICAL = b'\xb3'.decode('ibm437')
+ CHAR_H_OUTER_LEFT_INTERSECT = b'\xc3'.decode('ibm437')
+ CHAR_H_OUTER_LEFT_VERTICAL = b'\xb3'.decode('ibm437')
+ CHAR_H_OUTER_RIGHT_INTERSECT = b'\xb4'.decode('ibm437')
+ CHAR_H_OUTER_RIGHT_VERTICAL = b'\xb3'.decode('ibm437')
+ CHAR_INNER_HORIZONTAL = b'\xc4'.decode('ibm437')
+ CHAR_INNER_INTERSECT = b'\xc5'.decode('ibm437')
+ CHAR_INNER_VERTICAL = b'\xb3'.decode('ibm437')
+ CHAR_OUTER_BOTTOM_HORIZONTAL = b'\xc4'.decode('ibm437')
+ CHAR_OUTER_BOTTOM_INTERSECT = b'\xc1'.decode('ibm437')
+ CHAR_OUTER_BOTTOM_LEFT = b'\xc0'.decode('ibm437')
+ CHAR_OUTER_BOTTOM_RIGHT = b'\xd9'.decode('ibm437')
+ CHAR_OUTER_LEFT_INTERSECT = b'\xc3'.decode('ibm437')
+ CHAR_OUTER_LEFT_VERTICAL = b'\xb3'.decode('ibm437')
+ CHAR_OUTER_RIGHT_INTERSECT = b'\xb4'.decode('ibm437')
+ CHAR_OUTER_RIGHT_VERTICAL = b'\xb3'.decode('ibm437')
+ CHAR_OUTER_TOP_HORIZONTAL = b'\xc4'.decode('ibm437')
+ CHAR_OUTER_TOP_INTERSECT = b'\xc2'.decode('ibm437')
+ CHAR_OUTER_TOP_LEFT = b'\xda'.decode('ibm437')
+ CHAR_OUTER_TOP_RIGHT = b'\xbf'.decode('ibm437')
+
+
+class WindowsTableDouble(AsciiTable):
+ """Draw a table using box-drawing characters on Windows platforms. This uses Code Page 437. Double-line borders."""
+
+ CHAR_F_INNER_HORIZONTAL = b'\xcd'.decode('ibm437')
+ CHAR_F_INNER_INTERSECT = b'\xce'.decode('ibm437')
+ CHAR_F_INNER_VERTICAL = b'\xba'.decode('ibm437')
+ CHAR_F_OUTER_LEFT_INTERSECT = b'\xcc'.decode('ibm437')
+ CHAR_F_OUTER_LEFT_VERTICAL = b'\xba'.decode('ibm437')
+ CHAR_F_OUTER_RIGHT_INTERSECT = b'\xb9'.decode('ibm437')
+ CHAR_F_OUTER_RIGHT_VERTICAL = b'\xba'.decode('ibm437')
+ CHAR_H_INNER_HORIZONTAL = b'\xcd'.decode('ibm437')
+ CHAR_H_INNER_INTERSECT = b'\xce'.decode('ibm437')
+ CHAR_H_INNER_VERTICAL = b'\xba'.decode('ibm437')
+ CHAR_H_OUTER_LEFT_INTERSECT = b'\xcc'.decode('ibm437')
+ CHAR_H_OUTER_LEFT_VERTICAL = b'\xba'.decode('ibm437')
+ CHAR_H_OUTER_RIGHT_INTERSECT = b'\xb9'.decode('ibm437')
+ CHAR_H_OUTER_RIGHT_VERTICAL = b'\xba'.decode('ibm437')
+ CHAR_INNER_HORIZONTAL = b'\xcd'.decode('ibm437')
+ CHAR_INNER_INTERSECT = b'\xce'.decode('ibm437')
+ CHAR_INNER_VERTICAL = b'\xba'.decode('ibm437')
+ CHAR_OUTER_BOTTOM_HORIZONTAL = b'\xcd'.decode('ibm437')
+ CHAR_OUTER_BOTTOM_INTERSECT = b'\xca'.decode('ibm437')
+ CHAR_OUTER_BOTTOM_LEFT = b'\xc8'.decode('ibm437')
+ CHAR_OUTER_BOTTOM_RIGHT = b'\xbc'.decode('ibm437')
+ CHAR_OUTER_LEFT_INTERSECT = b'\xcc'.decode('ibm437')
+ CHAR_OUTER_LEFT_VERTICAL = b'\xba'.decode('ibm437')
+ CHAR_OUTER_RIGHT_INTERSECT = b'\xb9'.decode('ibm437')
+ CHAR_OUTER_RIGHT_VERTICAL = b'\xba'.decode('ibm437')
+ CHAR_OUTER_TOP_HORIZONTAL = b'\xcd'.decode('ibm437')
+ CHAR_OUTER_TOP_INTERSECT = b'\xcb'.decode('ibm437')
+ CHAR_OUTER_TOP_LEFT = b'\xc9'.decode('ibm437')
+ CHAR_OUTER_TOP_RIGHT = b'\xbb'.decode('ibm437')
+
+
+class SingleTable(WindowsTable if IS_WINDOWS else UnixTable):
+ """Cross-platform table with single-line box-drawing characters.
+
+ :ivar iter table_data: List (empty or list of lists of strings) representing the table.
+ :ivar str title: Optional title to show within the top border of the table.
+ :ivar bool inner_column_border: Separates columns.
+ :ivar bool inner_footing_row_border: Show a border before the last row.
+ :ivar bool inner_heading_row_border: Show a border after the first row.
+ :ivar bool inner_row_border: Show a border in between every row.
+ :ivar bool outer_border: Show the top, left, right, and bottom border.
+ :ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
+ :ivar int padding_left: Number of spaces to pad on the left side of every cell.
+ :ivar int padding_right: Number of spaces to pad on the right side of every cell.
+ """
+
+ pass
+
+
+class DoubleTable(WindowsTableDouble):
+ """Cross-platform table with box-drawing characters. On Windows it's double borders, on Linux/OSX it's unicode.
+
+ :ivar iter table_data: List (empty or list of lists of strings) representing the table.
+ :ivar str title: Optional title to show within the top border of the table.
+ :ivar bool inner_column_border: Separates columns.
+ :ivar bool inner_footing_row_border: Show a border before the last row.
+ :ivar bool inner_heading_row_border: Show a border after the first row.
+ :ivar bool inner_row_border: Show a border in between every row.
+ :ivar bool outer_border: Show the top, left, right, and bottom border.
+ :ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
+ :ivar int padding_left: Number of spaces to pad on the left side of every cell.
+ :ivar int padding_right: Number of spaces to pad on the right side of every cell.
+ """
+
+ pass
+
+
+class PorcelainTable(AsciiTable):
+ """An AsciiTable stripped to a minimum.
+
+ Meant to be machine passable and roughly follow format set by git --porcelain option (hence the name).
+
+ :ivar iter table_data: List (empty or list of lists of strings) representing the table.
+ """
+
+ def __init__(self, table_data):
+ """Constructor.
+
+ :param iter table_data: List (empty or list of lists of strings) representing the table.
+ """
+ # Porcelain table won't support title since it has no outer birders.
+ super(PorcelainTable, self).__init__(table_data)
+
+ # Removes outer border, and inner footing and header row borders.
+ self.inner_footing_row_border = False
+ self.inner_heading_row_border = False
+ self.outer_border = False
diff --git a/terminaltables/terminal_io.py b/terminaltables/terminal_io.py
new file mode 100644
index 0000000..8b8c10d
--- /dev/null
+++ b/terminaltables/terminal_io.py
@@ -0,0 +1,98 @@
+"""Get info about the current terminal window/screen buffer."""
+
+import ctypes
+import struct
+import sys
+
+DEFAULT_HEIGHT = 24
+DEFAULT_WIDTH = 79
+INVALID_HANDLE_VALUE = -1
+IS_WINDOWS = sys.platform == 'win32'
+STD_ERROR_HANDLE = -12
+STD_OUTPUT_HANDLE = -11
+
+
+def get_console_info(kernel32, handle):
+ """Get information about this current console window (Windows only).
+
+ https://github.com/Robpol86/colorclass/blob/ab42da59/colorclass/windows.py#L111
+
+ :raise OSError: When handle is invalid or GetConsoleScreenBufferInfo API call fails.
+
+ :param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance.
+ :param int handle: stderr or stdout handle.
+
+ :return: Width (number of characters) and height (number of lines) of the terminal.
+ :rtype: tuple
+ """
+ if handle == INVALID_HANDLE_VALUE:
+ raise OSError('Invalid handle.')
+
+ # Query Win32 API.
+ lpcsbi = ctypes.create_string_buffer(22) # Populated by GetConsoleScreenBufferInfo.
+ if not kernel32.GetConsoleScreenBufferInfo(handle, lpcsbi):
+ raise ctypes.WinError() # Subclass of OSError.
+
+ # Parse data.
+ left, top, right, bottom = struct.unpack('hhhhHhhhhhh', lpcsbi.raw)[5:-2]
+ width, height = right - left, bottom - top
+ return width, height
+
+
+def terminal_size(kernel32=None):
+ """Get the width and height of the terminal.
+
+ http://code.activestate.com/recipes/440694-determine-size-of-console-window-on-windows/
+ http://stackoverflow.com/questions/17993814/why-the-irrelevant-code-made-a-difference
+
+ :param kernel32: Optional mock kernel32 object. For testing.
+
+ :return: Width (number of characters) and height (number of lines) of the terminal.
+ :rtype: tuple
+ """
+ if IS_WINDOWS:
+ kernel32 = kernel32 or ctypes.windll.kernel32
+ try:
+ return get_console_info(kernel32, kernel32.GetStdHandle(STD_ERROR_HANDLE))
+ except OSError:
+ try:
+ return get_console_info(kernel32, kernel32.GetStdHandle(STD_OUTPUT_HANDLE))
+ except OSError:
+ return DEFAULT_WIDTH, DEFAULT_HEIGHT
+
+ try:
+ device = __import__('fcntl').ioctl(0, __import__('termios').TIOCGWINSZ, '\0\0\0\0\0\0\0\0')
+ except IOError:
+ return DEFAULT_WIDTH, DEFAULT_HEIGHT
+ height, width = struct.unpack('hhhh', device)[:2]
+ return width, height
+
+
+def set_terminal_title(title, kernel32=None):
+ """Set the terminal title.
+
+ :param title: The title to set (string, unicode, bytes accepted).
+ :param kernel32: Optional mock kernel32 object. For testing.
+
+ :return: If title changed successfully (Windows only, always True on Linux/OSX).
+ :rtype: bool
+ """
+ try:
+ title_bytes = title.encode('utf-8')
+ except AttributeError:
+ title_bytes = title
+
+ if IS_WINDOWS:
+ kernel32 = kernel32 or ctypes.windll.kernel32
+ try:
+ is_ascii = all(ord(c) < 128 for c in title) # str/unicode.
+ except TypeError:
+ is_ascii = all(c < 128 for c in title) # bytes.
+ if is_ascii:
+ return kernel32.SetConsoleTitleA(title_bytes) != 0
+ else:
+ return kernel32.SetConsoleTitleW(title) != 0
+
+ # Linux/OSX.
+ sys.stdout.write(b'\033]0;' + title_bytes + b'\007')
+ return True
diff --git a/terminaltables/width_and_alignment.py b/terminaltables/width_and_alignment.py
new file mode 100644
index 0000000..057e800
--- /dev/null
+++ b/terminaltables/width_and_alignment.py
@@ -0,0 +1,160 @@
+"""Functions that handle alignment, padding, widths, etc."""
+
+import re
+import unicodedata
+
+from terminaltables.terminal_io import terminal_size
+
+RE_COLOR_ANSI = re.compile(r'(\033\[[\d;]+m)')
+
+
+def visible_width(string):
+ """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/terminaltables/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, align, inner_dimensions, padding, space=' '):
+ """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(l) for l 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, column_number, outer_border, inner_border, padding):
+ """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, outer_border, inner_border):
+ """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