index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | archinstall/lib/menu/menu.py | 253 |
diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 09685c55..38301d3a 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -3,14 +3,11 @@ from enum import Enum, auto from os import system from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional, Callable -from .simple_menu import TerminalMenu +from simple_term_menu import TerminalMenu # type: ignore from ..exceptions import RequirementError -from ..output import log +from ..output import debug -from collections.abc import Iterable -import sys -import logging if TYPE_CHECKING: _: Any @@ -27,42 +24,61 @@ class MenuSelection: type_: MenuSelectionType value: Optional[Union[str, List[str]]] = None + @property + def single_value(self) -> Any: + return self.value # type: ignore + + @property + def multi_value(self) -> List[Any]: + return self.value # type: ignore + class Menu(TerminalMenu): + _menu_is_active: bool = False + + @staticmethod + def is_menu_active() -> bool: + return Menu._menu_is_active + + @classmethod + def back(cls) -> str: + return str(_('← Back')) @classmethod - def yes(cls): + def yes(cls) -> str: return str(_('yes')) @classmethod - def no(cls): + def no(cls) -> str: return str(_('no')) @classmethod - def yes_no(cls): + def yes_no(cls) -> List[str]: return [cls.yes(), cls.no()] def __init__( self, - title :str, - p_options :Union[List[str], Dict[str, Any]], - skip :bool = True, - multi :bool = False, - default_option : Optional[str] = None, - sort :bool = True, - preset_values :Union[str, List[str]] = None, - cursor_index : Optional[int] = None, - preview_command: Optional[Callable] = None, + title: str, + p_options: Union[List[str], Dict[str, Any]], + skip: bool = True, + multi: bool = False, + default_option: Optional[str] = None, + sort: bool = True, + preset_values: Optional[Union[str, List[str]]] = None, + cursor_index: Optional[int] = None, + preview_command: Optional[Callable[[Any], str | None]] = None, preview_size: float = 0.0, preview_title: str = 'Info', - header :Union[List[str],str] = None, - allow_reset :bool = False, - allow_reset_warning_msg :str = '', + header: Union[List[str], str] = [], + allow_reset: bool = False, + allow_reset_warning_msg: Optional[str] = None, clear_screen: bool = True, show_search_hint: bool = True, cycle_cursor: bool = True, clear_menu_on_exit: bool = True, - skip_empty_entries: bool = False + skip_empty_entries: bool = False, + display_back_option: bool = False, + extra_bottom_space: bool = False ): """ Creates a new menu @@ -72,7 +88,7 @@ class Menu(TerminalMenu): :param p_options: Options to be displayed in the menu to chose from; if dict is specified then the keys of such will be used as options - :type options: list, dict + :type p_options: list, dict :param skip: Indicate if the selection is not mandatory and can be skipped :type skip: bool @@ -101,46 +117,27 @@ class Menu(TerminalMenu): :param preview_title: Title of the preview window :type preview_title: str - param header: one or more header lines for the menu - type param: string or list + :param header: one or more header lines for the menu + :type header: string or list - param raise_error_on_interrupt: This will explicitly handle a ctrl+c instead and return that specific state - type param: bool + :param allow_reset: This will explicitly handle a ctrl+c instead and return that specific state + :type allow_reset: bool - param raise_error_warning_msg: If raise_error_on_interrupt is True and this is non-empty, there will be a warning with a user confirmation displayed - type param: str + param allow_reset_warning_msg: If raise_error_on_interrupt is True the warning is set, a user confirmation is displayed + type allow_reset_warning_msg: str - :param kwargs : any SimpleTerminal parameter + :param extra_bottom_space: Add an extra empty line at the end of the menu + :type extra_bottom_space: bool """ - # we guarantee the inmutability of the options outside the class. - # an unknown number of iterables (.keys(),.values(),generator,...) can't be directly copied, in this case - # we recourse to make them lists before, but thru an exceptions - # this is the old code, which is not maintenable with more types - # options = copy(list(p_options) if isinstance(p_options,(type({}.keys()),type({}.values()))) else p_options) - # We check that the options are iterable. If not we abort. Else we copy them to lists - # it options is a dictionary we use the values as entries of the list - # if options is a string object, each character becomes an entry - # if options is a list, we implictily build a copy to maintain immutability - if not isinstance(p_options,Iterable): - log(f"Objects of type {type(p_options)} is not iterable, and are not supported at Menu",fg="red") - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) - raise RequirementError("Menu() requires an iterable as option.") - - self._default_str = str(_('(default)')) - - if isinstance(p_options,dict): + if isinstance(p_options, Dict): options = list(p_options.keys()) else: options = list(p_options) if not options: - log(" * Menu didn't find any options to choose from * ", fg='red') - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) raise RequirementError('Menu.__init__() requires at least one option to proceed.') if any([o for o in options if not isinstance(o, str)]): - log(" * Menu options must be of type string * ", fg='red') - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING) raise RequirementError('Menu.__init__() requires the options to be of type string') if sort: @@ -152,7 +149,6 @@ class Menu(TerminalMenu): self._multi = multi self._raise_error_on_interrupt = allow_reset self._raise_error_warning_msg = allow_reset_warning_msg - self._preview_command = preview_command action_info = '' if skip: @@ -179,10 +175,28 @@ class Menu(TerminalMenu): if default_option: # if a default value was specified we move that one # to the top of the list and mark it as default as well - default = f'{default_option} {self._default_str}' - self._menu_options = [default] + [o for o in self._menu_options if default_option != o] + self._menu_options = [self._default_menu_value] + [o for o in self._menu_options if default_option != o] + + if display_back_option and not multi and skip: + skip_empty_entries = True + self._menu_options += ['', self.back()] + + if extra_bottom_space: + skip_empty_entries = True + self._menu_options += [''] + + preset_list: Optional[List[str]] = None + + if preset_values and isinstance(preset_values, str): + preset_list = [preset_values] - self._preselection(preset_values,cursor_index) + calc_cursor_idx = self._determine_cursor_pos(preset_list, cursor_index) + + # when we're not in multi selection mode we don't care about + # passing the pre-selection list to the menu as the position + # of the cursor is the one determining the pre-selection + if not self._multi: + preset_values = None cursor = "> " main_menu_cursor_style = ("fg_cyan", "bold") @@ -194,13 +208,10 @@ class Menu(TerminalMenu): menu_cursor=cursor, menu_cursor_style=main_menu_cursor_style, menu_highlight_style=main_menu_style, - # cycle_cursor=True, - # clear_screen=True, multi_select=multi, - # show_search_hint=True, - preselected_entries=self.preset_values, - cursor_index=self.cursor_index, - preview_command=lambda x: self._preview_wrapper(preview_command, x), + preselected_entries=preset_values, + cursor_index=calc_cursor_idx, + preview_command=lambda x: self._show_preview(preview_command, x), preview_size=preview_size, preview_title=preview_title, raise_error_on_interrupt=self._raise_error_on_interrupt, @@ -212,6 +223,28 @@ class Menu(TerminalMenu): skip_empty_entries=skip_empty_entries ) + @property + def _default_menu_value(self) -> str: + default_str = str(_('(default)')) + return f'{self._default_option} {default_str}' + + def _show_preview( + self, + preview_command: Optional[Callable[[Any], str | None]], + selection: str + ) -> Optional[str]: + if selection == self.back(): + return None + + if preview_command: + if self._default_option is not None and self._default_menu_value == selection: + selection = self._default_option + + if res := preview_command(selection): + return res.rstrip('\n') + + return None + def _show(self) -> MenuSelection: try: idx = self.show() @@ -219,45 +252,47 @@ class Menu(TerminalMenu): return MenuSelection(type_=MenuSelectionType.Reset) def check_default(elem): - if self._default_option is not None and f'{self._default_option} {self._default_str}' in elem: + if self._default_option is not None and self._default_menu_value in elem: return self._default_option else: return elem if idx is not None: - if isinstance(idx, (list, tuple)): + if isinstance(idx, (list, tuple)): # on multi selection results = [] for i in idx: option = check_default(self._menu_options[i]) results.append(option) return MenuSelection(type_=MenuSelectionType.Selection, value=results) - else: + else: # on single selection result = check_default(self._menu_options[idx]) return MenuSelection(type_=MenuSelectionType.Selection, value=result) else: return MenuSelection(type_=MenuSelectionType.Skip) - def _preview_wrapper(self, preview_command: Optional[Callable], current_selection: str) -> Optional[str]: - if preview_command: - if self._default_option is not None and f'{self._default_option} {self._default_str}' == current_selection: - current_selection = self._default_option - return preview_command(current_selection) - return None - def run(self) -> MenuSelection: - ret = self._show() + Menu._menu_is_active = True + + selection = self._show() - if ret.type_ == MenuSelectionType.Reset: - if self._raise_error_on_interrupt and len(self._raise_error_warning_msg) > 0: + if selection.type_ == MenuSelectionType.Reset: + if self._raise_error_on_interrupt and self._raise_error_warning_msg is not None: response = Menu(self._raise_error_warning_msg, Menu.yes_no(), skip=False).run() if response.value == Menu.no(): return self.run() - elif ret.type_ is MenuSelectionType.Skip: + elif selection.type_ is MenuSelectionType.Skip: if not self._skip: system('clear') return self.run() - return ret + if selection.type_ == MenuSelectionType.Selection: + if selection.value == self.back(): + selection.type_ = MenuSelectionType.Skip + selection.value = None + + Menu._menu_is_active = False + + return selection def set_cursor_pos(self,pos :int): if pos and 0 < pos < len(self._menu_entries): @@ -269,31 +304,47 @@ class Menu(TerminalMenu): pos = self._menu_entries.index(value) self.set_cursor_pos(pos) - def _preselection(self,preset_values :Union[str, List[str]] = [], cursor_index : Optional[int] = None): - def from_preset_to_cursor(): - if preset_values: - # if the value is not extant return 0 as cursor index + def _determine_cursor_pos( + self, + preset: Optional[List[str]] = None, + cursor_index: Optional[int] = None + ) -> Optional[int]: + """ + The priority order to determine the cursor position is: + 1. A static cursor position was provided + 2. Preset values have been provided so the cursor will be + positioned on those + 3. A default value for a selection is given so the cursor + will be placed on such + """ + if cursor_index: + return cursor_index + + if preset: + indexes = [] + + for p in preset: try: - if isinstance(preset_values,str): - self.cursor_index = self._menu_options.index(self.preset_values) - else: # should return an error, but this is smoother - self.cursor_index = self._menu_options.index(self.preset_values[0]) - except ValueError: - self.cursor_index = 0 - - self.cursor_index = cursor_index - if not preset_values: - self.preset_values = None - return - - self.preset_values = preset_values + # the options of the table selection menu + # are already escaped so we have to escape + # the preset values as well for the comparison + if '|' in p: + p = p.replace('|', '\\|') + + if p in self._menu_options: + idx = self._menu_options.index(p) + else: + idx = self._menu_options.index(self._default_menu_value) + indexes.append(idx) + except (IndexError, ValueError): + debug(f'Error finding index of {p}: {self._menu_options}') + + if len(indexes) == 0: + indexes.append(0) + + return indexes[0] + if self._default_option: - if isinstance(preset_values,str) and self._default_option == preset_values: - self.preset_values = f"{preset_values} {self._default_str}" - elif isinstance(preset_values,(list,tuple)) and self._default_option in preset_values: - idx = preset_values.index(self._default_option) - self.preset_values[idx] = f"{preset_values[idx]} {self._default_str}" - if cursor_index is None or not self._multi: - from_preset_to_cursor() - if not self._multi: # Not supported by the infraestructure - self.preset_values = None + return self._menu_options.index(self._default_menu_value) + + return None |