154 lines
5.0 KiB
Python
154 lines
5.0 KiB
Python
from typing import Any, Tuple, List, Dict, Optional, Callable
|
|
|
|
from .menu import MenuSelectionType, MenuSelection, Menu
|
|
from ..output import FormattedOutput
|
|
|
|
|
|
class TableMenu(Menu):
|
|
def __init__(
|
|
self,
|
|
title: str,
|
|
data: Optional[List[Any]] = None,
|
|
table_data: Optional[Tuple[List[Any], str]] = None,
|
|
preset: List[Any] = [],
|
|
custom_menu_options: List[str] = [],
|
|
default: Any = None,
|
|
multi: bool = False,
|
|
preview_command: Optional[Callable] = None,
|
|
preview_title: str = 'Info',
|
|
preview_size: float = 0.0,
|
|
allow_reset: bool = True,
|
|
allow_reset_warning_msg: Optional[str] = None,
|
|
skip: bool = True
|
|
):
|
|
"""
|
|
param title: Text that will be displayed above the menu
|
|
:type title: str
|
|
|
|
param data: List of objects that will be displayed as rows
|
|
:type data: List
|
|
|
|
param table_data: Tuple containing a list of objects and the corresponding
|
|
Table representation of the data as string; this can be used in case the table
|
|
has to be crafted in a more sophisticated manner
|
|
:type table_data: Optional[Tuple[List[Any], str]]
|
|
|
|
param custom_options: List of custom options that will be displayed under the table
|
|
:type custom_menu_options: List
|
|
|
|
:param preview_command: A function that should return a string that will be displayed in a preview window when a menu selection item is in focus
|
|
:type preview_command: Callable
|
|
"""
|
|
self._custom_options = custom_menu_options
|
|
self._multi = multi
|
|
|
|
if multi:
|
|
header_padding = 7
|
|
else:
|
|
header_padding = 2
|
|
|
|
if data is not None:
|
|
table_text = FormattedOutput.as_table(data)
|
|
rows = table_text.split('\n')
|
|
table = self._create_table(data, rows, header_padding=header_padding)
|
|
elif table_data is not None:
|
|
# we assume the table to be
|
|
# h1 | h2
|
|
# -----------
|
|
# r1 | r2
|
|
data = table_data[0]
|
|
rows = table_data[1].split('\n')
|
|
table = self._create_table(data, rows, header_padding=header_padding)
|
|
else:
|
|
raise ValueError('Either "data" or "table_data" must be provided')
|
|
|
|
self._options, header = self._prepare_selection(table)
|
|
|
|
preset_values = self._preset_values(preset)
|
|
|
|
extra_bottom_space = True if preview_command else False
|
|
|
|
super().__init__(
|
|
title,
|
|
self._options,
|
|
preset_values=preset_values,
|
|
header=header,
|
|
skip_empty_entries=True,
|
|
show_search_hint=False,
|
|
multi=multi,
|
|
default_option=default,
|
|
preview_command=lambda x: self._table_show_preview(preview_command, x),
|
|
preview_size=preview_size,
|
|
preview_title=preview_title,
|
|
extra_bottom_space=extra_bottom_space,
|
|
allow_reset=allow_reset,
|
|
allow_reset_warning_msg=allow_reset_warning_msg,
|
|
skip=skip
|
|
)
|
|
|
|
def _preset_values(self, preset: List[Any]) -> List[str]:
|
|
# when we create the table of just the preset values it will
|
|
# be formatted a bit different due to spacing, so to determine
|
|
# correct rows lets remove all the spaces and compare apples with apples
|
|
preset_table = FormattedOutput.as_table(preset).strip()
|
|
data_rows = preset_table.split('\n')[2:] # get all data rows
|
|
pure_data_rows = [self._escape_row(row.replace(' ', '')) for row in data_rows]
|
|
|
|
# the actual preset value has to be in non-escaped form
|
|
pure_option_rows = {o.replace(' ', ''): self._unescape_row(o) for o in self._options.keys()}
|
|
preset_rows = [row for pure, row in pure_option_rows.items() if pure in pure_data_rows]
|
|
|
|
return preset_rows
|
|
|
|
def _table_show_preview(self, preview_command: Optional[Callable], selection: Any) -> Optional[str]:
|
|
if preview_command:
|
|
row = self._escape_row(selection)
|
|
obj = self._options[row]
|
|
return preview_command(obj)
|
|
return None
|
|
|
|
def run(self) -> MenuSelection:
|
|
choice = super().run()
|
|
|
|
match choice.type_:
|
|
case MenuSelectionType.Selection:
|
|
if self._multi:
|
|
choice.value = [self._options[val] for val in choice.value] # type: ignore
|
|
else:
|
|
choice.value = self._options[choice.value] # type: ignore
|
|
|
|
return choice
|
|
|
|
def _escape_row(self, row: str) -> str:
|
|
return row.replace('|', '\\|')
|
|
|
|
def _unescape_row(self, row: str) -> str:
|
|
return row.replace('\\|', '|')
|
|
|
|
def _create_table(self, data: List[Any], rows: List[str], header_padding: int = 2) -> Dict[str, Any]:
|
|
# these are the header rows of the table and do not map to any data obviously
|
|
# we're adding 2 spaces as prefix because the menu selector '> ' will be put before
|
|
# the selectable rows so the header has to be aligned
|
|
padding = ' ' * header_padding
|
|
display_data = {f'{padding}{rows[0]}': None, f'{padding}{rows[1]}': None}
|
|
|
|
for row, entry in zip(rows[2:], data):
|
|
row = self._escape_row(row)
|
|
display_data[row] = entry
|
|
|
|
return display_data
|
|
|
|
def _prepare_selection(self, table: Dict[str, Any]) -> Tuple[Dict[str, Any], str]:
|
|
# header rows are mapped to None so make sure to exclude those from the selectable data
|
|
options = {key: val for key, val in table.items() if val is not None}
|
|
header = ''
|
|
|
|
if len(options) > 0:
|
|
table_header = [key for key, val in table.items() if val is None]
|
|
header = '\n'.join(table_header)
|
|
|
|
custom = {key: None for key in self._custom_options}
|
|
options.update(custom)
|
|
|
|
return options, header
|