Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall/lib/menu/abstract_menu.py
diff options
context:
space:
mode:
Diffstat (limited to 'archinstall/lib/menu/abstract_menu.py')
-rw-r--r--archinstall/lib/menu/abstract_menu.py275
1 files changed, 118 insertions, 157 deletions
diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py
index d659d709..ee55f5c9 100644
--- a/archinstall/lib/menu/abstract_menu.py
+++ b/archinstall/lib/menu/abstract_menu.py
@@ -1,13 +1,11 @@
from __future__ import annotations
-import logging
from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING
from .menu import Menu, MenuSelectionType
-from ..locale_helpers import set_keyboard_language
-from ..output import log
+from ..output import error
+from ..output import unicode_ljust
from ..translationhandler import TranslationHandler, Language
-from ..user_interaction.general_conf import select_archinstall_language
if TYPE_CHECKING:
_: Any
@@ -16,17 +14,17 @@ if TYPE_CHECKING:
class Selector:
def __init__(
self,
- description :str,
- func :Optional[Callable] = None,
- display_func :Optional[Callable] = None,
- default :Any = None,
- enabled :bool = False,
- dependencies :List = [],
- dependencies_not :List = [],
- exec_func :Optional[Callable] = None,
- preview_func :Optional[Callable] = None,
- mandatory :bool = False,
- no_store :bool = False
+ description: str,
+ func: Optional[Callable[[Any], Any]] = None,
+ display_func: Optional[Callable] = None,
+ default: Optional[Any] = None,
+ enabled: bool = False,
+ dependencies: List = [],
+ dependencies_not: List = [],
+ exec_func: Optional[Callable] = None,
+ preview_func: Optional[Callable] = None,
+ mandatory: bool = False,
+ no_store: bool = False
):
"""
Create a new menu selection entry
@@ -71,84 +69,66 @@ class Selector:
:param no_store: A boolean which determines that the field should or shouldn't be stored in the data storage
:type no_store: bool
"""
- self._description = description
- self.func = func
self._display_func = display_func
- self._current_selection = default
+ self._no_store = no_store
+
+ self.description = description
+ self.func = func
+ self.current_selection = default
self.enabled = enabled
- self._dependencies = dependencies
- self._dependencies_not = dependencies_not
+ self.dependencies = dependencies
+ self.dependencies_not = dependencies_not
self.exec_func = exec_func
- self._preview_func = preview_func
+ self.preview_func = preview_func
self.mandatory = mandatory
- self._no_store = no_store
-
- @property
- def description(self) -> str:
- return self._description
-
- @property
- def dependencies(self) -> List:
- return self._dependencies
-
- @property
- def dependencies_not(self) -> List:
- return self._dependencies_not
-
- @property
- def current_selection(self):
- return self._current_selection
-
- @property
- def preview_func(self):
- return self._preview_func
+ self.default = default
def do_store(self) -> bool:
return self._no_store is False
- def set_enabled(self, status :bool = True):
+ def set_enabled(self, status: bool = True):
self.enabled = status
- def update_description(self, description :str):
- self._description = description
+ def update_description(self, description: str):
+ self.description = description
def menu_text(self, padding: int = 0) -> str:
- if self._description == '': # special menu option for __separator__
+ if self.description == '': # special menu option for __separator__
return ''
current = ''
if self._display_func:
- current = self._display_func(self._current_selection)
+ current = self._display_func(self.current_selection)
else:
- if self._current_selection is not None:
- current = str(self._current_selection)
+ if self.current_selection is not None:
+ current = str(self.current_selection)
if current:
padding += 5
- description = str(self._description).ljust(padding, ' ')
- current = str(_('set: {}').format(current))
+ description = unicode_ljust(str(self.description), padding, ' ')
+ current = current
else:
- description = self._description
+ description = self.description
current = ''
return f'{description} {current}'
- def set_current_selection(self, current :Optional[str]):
- self._current_selection = current
+ def set_current_selection(self, current: Optional[Any]):
+ self.current_selection = current
def has_selection(self) -> bool:
- if not self._current_selection:
+ if not self.current_selection:
return False
return True
def get_selection(self) -> Any:
- return self._current_selection
+ return self.current_selection
def is_empty(self) -> bool:
- if self._current_selection is None:
+ if self.current_selection is None:
return True
- elif isinstance(self._current_selection, (str, list, dict)) and len(self._current_selection) == 0:
+ elif isinstance(self.current_selection, (str, list, dict)) and len(self.current_selection) == 0:
return True
return False
@@ -158,14 +138,17 @@ class Selector:
def is_mandatory(self) -> bool:
return self.mandatory
- def set_mandatory(self, status :bool = True):
- self.mandatory = status
- if status and not self.is_enabled():
- self.set_enabled(True)
+ def set_mandatory(self, value: bool):
+ self.mandatory = value
class AbstractMenu:
- def __init__(self, data_store: Optional[Dict[str, Any]] = None, auto_cursor=False, preview_size :float = 0.2):
+ def __init__(
+ self,
+ data_store: Dict[str, Any] = {},
+ auto_cursor: bool = False,
+ preview_size: float = 0.2
+ ):
"""
Create a new selection menu.
@@ -179,29 +162,34 @@ class AbstractMenu:
;type preview_size: float (range 0..1)
"""
- self._enabled_order :List[str] = []
+ self._enabled_order: List[str] = []
self._translation_handler = TranslationHandler()
self.is_context_mgr = False
- self._data_store = data_store if data_store is not None else {}
+ self._data_store = data_store
self.auto_cursor = auto_cursor
self._menu_options: Dict[str, Selector] = {}
- self._setup_selection_menu_options()
self.preview_size = preview_size
self._last_choice = None
+ self.setup_selection_menu_options()
+ self._sync_all()
+ self._populate_default_values()
+
+ self.defined_text = str(_('Defined'))
+
@property
def last_choice(self):
return self._last_choice
- def __enter__(self, *args :Any, **kwargs :Any) -> AbstractMenu:
+ def __enter__(self, *args: Any, **kwargs: Any) -> AbstractMenu:
self.is_context_mgr = True
return self
- def __exit__(self, *args :Any, **kwargs :Any) -> None:
+ def __exit__(self, *args: Any, **kwargs: Any) -> None:
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
# TODO: skip processing when it comes from a planified exit
if len(args) >= 2 and args[1]:
- log(args[1], level=logging.ERROR, fg='red')
+ error(args[1])
print(" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")
raise args[1]
@@ -216,7 +204,25 @@ class AbstractMenu:
def translation_handler(self) -> TranslationHandler:
return self._translation_handler
- def _setup_selection_menu_options(self):
+ def _populate_default_values(self):
+ for config_key, selector in self._menu_options.items():
+ if selector.default is not None and config_key not in self._data_store:
+ self._data_store[config_key] = selector.default
+
+ def _sync_all(self):
+ for key in self._menu_options.keys():
+ self._sync(key)
+
+ def _sync(self, selector_name: str):
+ value = self._data_store.get(selector_name, None)
+ selector = self._menu_options.get(selector_name, None)
+
+ if value is not None:
+ self._menu_options[selector_name].set_current_selection(value)
+ elif selector is not None and selector.has_selection():
+ self._data_store[selector_name] = selector.current_selection
+
+ def setup_selection_menu_options(self):
""" Define the menu options.
Menu options can be defined here in a subclass or done per program calling self.set_option()
"""
@@ -234,31 +240,16 @@ class AbstractMenu:
""" will be called at the end of the processing of the menu """
return
- def synch(self, selector_name :str, omit_if_set :bool = False,omit_if_disabled :bool = False):
- """ loads menu options with data_store value """
- arg = self._data_store.get(selector_name, None)
- # don't display the menu option if it was defined already
- if arg is not None and omit_if_set:
- return
-
- if not self.option(selector_name).is_enabled() and omit_if_disabled:
- return
-
- if arg is not None:
- self._menu_options[selector_name].set_current_selection(arg)
-
def _update_enabled_order(self, selector_name: str):
self._enabled_order.append(selector_name)
- def enable(self, selector_name :str, omit_if_set :bool = False , mandatory :bool = False):
+ def enable(self, selector_name: str, mandatory: bool = False):
""" activates menu options """
if self._menu_options.get(selector_name, None):
self._menu_options[selector_name].set_enabled(True)
self._update_enabled_order(selector_name)
-
- if mandatory:
- self._menu_options[selector_name].set_mandatory(True)
- self.synch(selector_name,omit_if_set)
+ self._menu_options[selector_name].set_mandatory(mandatory)
+ self._sync(selector_name)
else:
raise ValueError(f'No selector found: {selector_name}')
@@ -274,7 +265,11 @@ class AbstractMenu:
def _find_selection(self, selection_name: str) -> Tuple[str, Selector]:
enabled_menus = self._menus_to_enable()
padding = self._get_menu_text_padding(list(enabled_menus.values()))
- option = [(k, v) for k, v in self._menu_options.items() if v.menu_text(padding).strip() == selection_name.strip()]
+
+ option = []
+ for k, v in self._menu_options.items():
+ if v.menu_text(padding).strip() == selection_name.strip():
+ option.append((k, v))
if len(option) != 1:
raise ValueError(f'Selection not found: {selection_name}')
@@ -283,18 +278,11 @@ class AbstractMenu:
return config_name, selector
def run(self, allow_reset: bool = False):
- """ Calls the Menu framework"""
- # we synch all the options just in case
- for item in self.list_options():
- self.synch(item)
-
- self.post_callback() # as all the values can vary i have to exec this callback
+ self._sync_all()
+ self.post_callback()
cursor_pos = None
while True:
- # Before continuing, set the preferred keyboard layout/language in the current terminal.
- # This will just help the user with the next following questions.
- self._set_kb_language()
enabled_menus = self._menus_to_enable()
padding = self._get_menu_text_padding(list(enabled_menus.values()))
@@ -336,18 +324,18 @@ class AbstractMenu:
value = value.strip()
# if this calls returns false, we exit the menu
- # we allow for an callback for special processing on realeasing control
+ # we allow for an callback for special processing on releasing control
if not self._process_selection(value):
break
# we get the last action key
- actions = {str(v.description):k for k,v in self._menu_options.items()}
+ actions = {str(v.description): k for k, v in self._menu_options.items()}
self._last_choice = actions[selection.value.strip()] # type: ignore
if not self.is_context_mgr:
self.__exit__()
- def _process_selection(self, selection_name :str) -> bool:
+ def _process_selection(self, selection_name: str) -> bool:
""" determines and executes the selection y
Can / Should be extended to handle specific selection issues
Returns true if the menu shall continue, False if it has ended
@@ -356,7 +344,7 @@ class AbstractMenu:
config_name, selector = self._find_selection(selection_name)
return self.exec_option(config_name, selector)
- def exec_option(self, config_name :str, p_selector :Optional[Selector] = None) -> bool:
+ def exec_option(self, config_name: str, p_selector: Optional[Selector] = None) -> bool:
""" processes the execution of a given menu entry
- pre process callback
- selection function
@@ -372,40 +360,42 @@ class AbstractMenu:
self.pre_callback(config_name)
result = None
+
if selector.func is not None:
- presel_val = self.option(config_name).get_selection()
- result = selector.func(presel_val)
+ cur_value = self.option(config_name).get_selection()
+ result = selector.func(cur_value)
self._menu_options[config_name].set_current_selection(result)
+
if selector.do_store():
self._data_store[config_name] = result
- exec_ret_val = selector.exec_func(config_name,result) if selector.exec_func is not None else False
- self.post_callback(config_name,result)
- if exec_ret_val and self._check_mandatory_status():
+ exec_ret_val = selector.exec_func(config_name, result) if selector.exec_func else False
+
+ self.post_callback(config_name, result)
+
+ if exec_ret_val:
return False
- return True
- def _set_kb_language(self):
- """ general for ArchInstall"""
- # Before continuing, set the preferred keyboard layout/language in the current terminal.
- # This will just help the user with the next following questions.
- if self._data_store.get('keyboard-layout', None) and len(self._data_store['keyboard-layout']):
- set_keyboard_language(self._data_store['keyboard-layout'])
+ return True
- def _verify_selection_enabled(self, selection_name :str) -> bool:
- """ general """
+ def _verify_selection_enabled(self, selection_name: str) -> bool:
if selection := self._menu_options.get(selection_name, None):
if not selection.enabled:
return False
if len(selection.dependencies) > 0:
- for d in selection.dependencies:
- if not self._verify_selection_enabled(d) or self._menu_options[d].is_empty():
- return False
+ for dep in selection.dependencies:
+ if isinstance(dep, str):
+ if not self._verify_selection_enabled(dep) or self._menu_options[dep].is_empty():
+ return False
+ elif callable(dep): # callable dependency eval
+ return dep()
+ else:
+ raise ValueError(f'Unsupported dependency: {selection_name}')
if len(selection.dependencies_not) > 0:
- for d in selection.dependencies_not:
- if not self._menu_options[d].is_empty():
+ for dep in selection.dependencies_not:
+ if not self._menu_options[dep].is_empty():
return False
return True
@@ -429,16 +419,10 @@ class AbstractMenu:
return ordered_menus
- def option(self,name :str) -> Selector:
+ def option(self, name: str) -> Selector:
# TODO check inexistent name
return self._menu_options[name]
- def list_options(self) -> Iterator:
- """ Iterator to retrieve the enabled menu option names
- """
- for item in self._menu_options:
- yield item
-
def list_enabled_options(self) -> Iterator:
""" Iterator to retrieve the enabled menu options at a given time.
The results are dynamic (if between calls to the iterator some elements -still not retrieved- are (de)activated
@@ -447,44 +431,21 @@ class AbstractMenu:
if item in self._menus_to_enable():
yield item
- def set_option(self, name :str, selector :Selector):
- self._menu_options[name] = selector
- self.synch(name)
-
- def _check_mandatory_status(self) -> bool:
- for field in self._menu_options:
- option = self._menu_options[field]
- if option.is_mandatory() and not option.has_selection():
- return False
- return True
-
- def set_mandatory(self, field :str, status :bool):
- self.option(field).set_mandatory(status)
-
- def mandatory_overview(self) -> Tuple[int, int]:
- mandatory_fields = 0
- mandatory_waiting = 0
- for field, option in self._menu_options.items():
- if option.is_mandatory():
- mandatory_fields += 1
- if not option.has_selection():
- mandatory_waiting += 1
- return mandatory_fields, mandatory_waiting
-
- def _select_archinstall_language(self, preset_value: Language) -> Language:
- language = select_archinstall_language(self.translation_handler.translated_languages, preset_value)
+ def _select_archinstall_language(self, preset: Language) -> Language:
+ from ..interactions.general_conf import select_archinstall_language
+ language = select_archinstall_language(self.translation_handler.translated_languages, preset)
self._translation_handler.activate(language)
return language
class AbstractSubMenu(AbstractMenu):
- def __init__(self, data_store: Optional[Dict[str, Any]] = None):
- super().__init__(data_store=data_store)
+ def __init__(self, data_store: Dict[str, Any] = {}, preview_size: float = 0.2):
+ super().__init__(data_store=data_store, preview_size=preview_size)
self._menu_options['__separator__'] = Selector('')
self._menu_options['back'] = \
Selector(
- _('Back'),
+ Menu.back(),
no_store=True,
enabled=True,
exec_func=lambda n, v: True,