Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall/lib/menu/table_selection_menu.py
blob: fec6ae59bc800fa546207c05fb5af7825712ecc5 (plain)
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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