1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
from typing import Any, Tuple, List, Dict, Optional
from .menu import MenuSelectionType, MenuSelection
from ..output import FormattedOutput
from ..menu import Menu
class TableMenu(Menu):
def __init__(
self,
title: str,
data: List[Any] = [],
table_data: Optional[Tuple[List[Any], str]] = None,
custom_menu_options: List[str] = [],
default: Any = None,
multi: bool = False
):
"""
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
"""
if not data and not table_data:
raise ValueError('Either "data" or "table_data" must be provided')
self._custom_options = custom_menu_options
self._multi = multi
if multi:
header_padding = 7
else:
header_padding = 2
if len(data):
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)
self._options, header = self._prepare_selection(table)
super().__init__(
title,
self._options,
header=header,
skip_empty_entries=True,
show_search_hint=False,
allow_reset=True,
multi=multi,
default_option=default
)
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 _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 = row.replace('|', '\\|')
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
|