Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall/lib/menu/menu.py
diff options
context:
space:
mode:
Diffstat (limited to 'archinstall/lib/menu/menu.py')
-rw-r--r--archinstall/lib/menu/menu.py253
1 files changed, 152 insertions, 101 deletions
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