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, ): """ 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 ) 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