diff options
Diffstat (limited to 'cmd2')
-rw-r--r-- | cmd2/argparse_completer.py | 45 | ||||
-rw-r--r-- | cmd2/cmd2.py | 4 | ||||
-rw-r--r-- | cmd2/table_creator.py | 62 |
3 files changed, 80 insertions, 31 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 61f173cc..a8bb6390 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -14,6 +14,8 @@ from collections import deque from typing import Dict, List, Optional, Union from . import ansi, cmd2, constants +from .table_creator import Column, SimpleTable + from .argparse_custom import ( ATTR_CHOICES_CALLABLE, ATTR_DESCRIPTIVE_COMPLETION_HEADER, @@ -467,29 +469,36 @@ class ArgparseCompleter: # If a metavar was defined, use that instead of the dest field destination = action.metavar if action.metavar else action.dest - token_width = ansi.style_aware_wcswidth(destination) - completions_with_desc = [] - - for item in completions: - item_width = ansi.style_aware_wcswidth(item) - if item_width > token_width: - token_width = item_width - - term_size = shutil.get_terminal_size() - fill_width = int(term_size.columns * .6) - (token_width + 2) - for item in completions: - entry = '{: <{token_width}}{: <{fill_width}}'.format(item, item.description, - token_width=token_width + 2, - fill_width=fill_width) - completions_with_desc.append(entry) desc_header = getattr(action, ATTR_DESCRIPTIVE_COMPLETION_HEADER, None) if desc_header is None: desc_header = DEFAULT_DESCRIPTIVE_HEADER - header = '\n{: <{token_width}}{}'.format(destination.upper(), desc_header, token_width=token_width + 2) - self._cmd2_app.completion_header = header - self._cmd2_app.display_matches = completions_with_desc + # Calculate needed widths for the token and description columns of the table + token_width = ansi.style_aware_wcswidth(destination) + desc_width = ansi.style_aware_wcswidth(desc_header) + + for item in completions: + token_width = max(ansi.style_aware_wcswidth(item), token_width) + desc_width = max(ansi.style_aware_wcswidth(item.description), desc_width) + + # Create a table that's over half the width of the terminal. + # This will force readline to place each entry on its own line. + divider_char = None + min_width = int(shutil.get_terminal_size().columns * 0.6) + base_width = SimpleTable.base_width(2, divider_char=divider_char) + initial_width = base_width + token_width + desc_width + + if initial_width < min_width: + desc_width += (min_width - initial_width) + + cols = list() + cols.append(Column(destination.upper(), width=token_width)) + cols.append(Column(desc_header, width=desc_width)) + + hint_table = SimpleTable(cols, divider_char=divider_char) + self._cmd2_app.completion_header = hint_table.generate_header() + self._cmd2_app.display_matches = [hint_table.generate_data_row([item, item.description]) for item in completions] return completions diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 49c181f1..7245273a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1147,7 +1147,7 @@ class Cmd(cmd.Cmd): # Print the header if one exists if self.completion_header: - sys.stdout.write('\n' + self.completion_header) + sys.stdout.write('\n\n' + self.completion_header) # Call readline's display function # rl_display_match_list(strings_array, number of completion matches, longest match length) @@ -1176,7 +1176,7 @@ class Cmd(cmd.Cmd): # Print the header if one exists if self.completion_header: # noinspection PyUnresolvedReferences - readline.rl.mode.console.write('\n' + self.completion_header) + readline.rl.mode.console.write('\n\n' + self.completion_header) # Display matches using actual display function. This also redraws the prompt and line. orig_pyreadline_display(matches_to_display) diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 70b7c1eb..dce9462e 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -465,8 +465,9 @@ class TableCreator: if cell_index == len(self.cols) - 1: row_buf.write(post_line) - # Add a newline if this is not the last row - row_buf.write('\n') + # Add a newline if this is not the last line + if line_index < total_lines - 1: + row_buf.write('\n') return row_buf.getvalue() @@ -480,6 +481,9 @@ class SimpleTable(TableCreator): Implementation of TableCreator which generates a borderless table with an optional divider row after the header. This class can be used to create the whole table at once or one row at a time. """ + # Num chars between cells + INTER_CELL_CHARS = 2 + def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, divider_char: Optional[str] = '-') -> None: """ SimpleTable initializer @@ -495,24 +499,50 @@ class SimpleTable(TableCreator): self.divider_char = divider_char self.empty_data = [EMPTY for _ in self.cols] - def generate_header(self) -> str: + @staticmethod + def base_width(num_cols: int, divider_char: Optional[str] = '-') -> int: """ - Generate header with an optional divider row + Utility method to calculate the width required for a table before data is added to it. + This is useful to know how much room is left for data with creating a table of a specific width. + + :param num_cols: how many columns the table will have + :param divider_char: optional character used to build the header divider row. If provided, its value must meet the + same requirements as fill_char in TableCreator.generate_row() or exceptions will be raised. + Set this to None if you don't want a divider row. (Defaults to dash) + :return: base width + :raises: ValueError if num_cols is less than 1 """ + if num_cols < 1: + raise ValueError("Column count cannot be less than 1") + + # Generate a line to validate divider_char. If invalid, an exception will be raised. + st = SimpleTable([Column('')], divider_char=divider_char) + st.generate_header() + + if divider_char is None: + inter_cell = SimpleTable.INTER_CELL_CHARS * SPACE + else: + inter_cell = SPACE * ansi.style_aware_wcswidth(SimpleTable.INTER_CELL_CHARS * divider_char) + + return (num_cols - 1) * ansi.style_aware_wcswidth(inter_cell) + + def generate_header(self) -> str: + """Generate table header with an optional divider row""" header_buf = io.StringIO() # Create the header labels if self.divider_char is None: - inter_cell = 2 * SPACE + inter_cell = SimpleTable.INTER_CELL_CHARS * SPACE else: - inter_cell = SPACE * ansi.style_aware_wcswidth(2 * self.divider_char) + inter_cell = SPACE * ansi.style_aware_wcswidth(SimpleTable.INTER_CELL_CHARS * self.divider_char) header = self.generate_row(inter_cell=inter_cell) header_buf.write(header) # Create the divider. Use empty strings for the row_data. if self.divider_char is not None: divider = self.generate_row(row_data=self.empty_data, fill_char=self.divider_char, - inter_cell=(2 * self.divider_char)) + inter_cell=(SimpleTable.INTER_CELL_CHARS * self.divider_char)) + header_buf.write('\n') header_buf.write(divider) return header_buf.getvalue() @@ -548,6 +578,8 @@ class SimpleTable(TableCreator): if include_header: header = self.generate_header() table_buf.write(header) + if len(table_data) > 0: + table_buf.write('\n') for index, row_data in enumerate(table_data): if index > 0 and row_spacing > 0: @@ -555,6 +587,8 @@ class SimpleTable(TableCreator): row = self.generate_data_row(row_data) table_buf.write(row) + if index < len(table_data) - 1: + table_buf.write('\n') return table_buf.getvalue() @@ -643,10 +677,7 @@ class BorderedTable(TableCreator): inter_cell=inter_cell, post_line=post_line) def generate_header(self) -> str: - """ - Generate header - :return: header string - """ + """Generate table header""" pre_line = '║' + self.padding * SPACE inter_cell = self.padding * SPACE @@ -659,7 +690,9 @@ class BorderedTable(TableCreator): # Create the bordered header header_buf = io.StringIO() header_buf.write(self.generate_table_top_border()) + header_buf.write('\n') header_buf.write(self.generate_row(pre_line=pre_line, inter_cell=inter_cell, post_line=post_line)) + header_buf.write('\n') header_buf.write(self.generate_header_bottom_border()) return header_buf.getvalue() @@ -699,13 +732,17 @@ class BorderedTable(TableCreator): top_border = self.generate_table_top_border() table_buf.write(top_border) + table_buf.write('\n') + for index, row_data in enumerate(table_data): if index > 0: row_bottom_border = self.generate_row_bottom_border() table_buf.write(row_bottom_border) + table_buf.write('\n') row = self.generate_data_row(row_data) table_buf.write(row) + table_buf.write('\n') table_buf.write(self.generate_table_bottom_border()) return table_buf.getvalue() @@ -797,9 +834,12 @@ class AlternatingTable(BorderedTable): top_border = self.generate_table_top_border() table_buf.write(top_border) + table_buf.write('\n') + for row_data in table_data: row = self.generate_data_row(row_data) table_buf.write(row) + table_buf.write('\n') table_buf.write(self.generate_table_bottom_border()) return table_buf.getvalue() |