summaryrefslogtreecommitdiff
path: root/cmd2
diff options
context:
space:
mode:
Diffstat (limited to 'cmd2')
-rw-r--r--cmd2/argparse_completer.py45
-rw-r--r--cmd2/cmd2.py4
-rw-r--r--cmd2/table_creator.py62
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()