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:
authorDaniel Girtler <blackrabbit256@gmail.com>2023-04-19 20:55:42 +1000
committerGitHub <noreply@github.com>2023-04-19 12:55:42 +0200
commit00b0ae7ba439a5a420095175b3bedd52c569db51 (patch)
treef02d081e361d5e65603f74dea3873dcc6606cf7c /archinstall/lib/menu/abstract_menu.py
parent5253e57e9f26cf3e59cb2460544af13f56e485bb (diff)
PyParted and a large rewrite of the underlying partitioning (#1604)
* Invert mypy files * Add optional pre-commit hooks * New profile structure * Serialize profiles * Use profile instead of classmethod * Custom profile setup * Separator between back * Support profile import via url * Move profiles module * Refactor files * Remove symlink * Add user to docker group * Update schema description * Handle list services * mypy fixes * mypy fixes * Rename profilesv2 to profiles * flake8 * mypy again * Support selecting DM * Fix mypy * Cleanup * Update greeter setting * Update schema * Revert toml changes * Poc external dependencies * Dependency support * New encryption menu * flake8 * Mypy and flake8 * Unify lsblk command * Update bootloader configuration * Git hooks * Fix import * Pyparted * Remove custom font setting * flake8 * Remove default preview * Manual partitioning menu * Update structure * Disk configuration * Update filesystem * luks2 encryption * Everything works until installation * Btrfsutil * Btrfs handling * Update btrfs * Save encryption config * Fix pipewire issue * Update mypy version * Update all pre-commit * Update package versions * Revert audio/pipewire * Merge master PRs * Add master changes * Merge master changes * Small renaming * Pull master changes * Reset disk enc after disk config change * Generate locals * Update naming * Fix imports * Fix broken sync * Fix pre selection on table menu * Profile menu * Update profile * Fix post_install * Added python-pyparted to PKGBUILD, this requires [testing] to be enabled in order to run makepkg. Package still works via python -m build etc. * Swaped around some setuptools logic in pyproject Since we define `package-data` and `packages` there should be no need for: ``` [tool.setuptools.packages.find] where = ["archinstall", "archinstall.*"] ``` * Removed pyproject collisions. Duplicate definitions. * Made sure pyproject.toml includes languages * Add example and update README * Fix pyproject issues * Generate locale * Refactor imports * Simplify imports * Add profile description and package examples * Align code * Fix mypy * Simplify imports * Fix saving config * Fix wrong luks merge * Refactor installation * Fix cdrom device loading * Fix wrongly merged code * Fix imports and greeter * Don't terminate on partprobe error * Use specific path on partprobe from luks * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update github workflow to test archinstall installation * Update sway merge * Generate locales * Update workflow --------- Co-authored-by: Daniel Girtler <girtler.daniel@gmail.com> Co-authored-by: Anton Hvornum <anton@hvornum.se> Co-authored-by: Anton Hvornum <anton.feeds+github@gmail.com> Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>
Diffstat (limited to 'archinstall/lib/menu/abstract_menu.py')
-rw-r--r--archinstall/lib/menu/abstract_menu.py200
1 files changed, 106 insertions, 94 deletions
diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py
index d659d709..53816655 100644
--- a/archinstall/lib/menu/abstract_menu.py
+++ b/archinstall/lib/menu/abstract_menu.py
@@ -7,7 +7,6 @@ from .menu import Menu, MenuSelectionType
from ..locale_helpers import set_keyboard_language
from ..output import log
from ..translationhandler import TranslationHandler, Language
-from ..user_interaction.general_conf import select_archinstall_language
if TYPE_CHECKING:
_: Any
@@ -16,17 +15,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[[str], 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
@@ -82,6 +81,11 @@ class Selector:
self._preview_func = preview_func
self.mandatory = mandatory
self._no_store = no_store
+ self._default = default
+
+ @property
+ def default(self) -> Any:
+ return self._default
@property
def description(self) -> str:
@@ -96,7 +100,7 @@ class Selector:
return self._dependencies_not
@property
- def current_selection(self):
+ def current_selection(self) -> Optional[Any]:
return self._current_selection
@property
@@ -106,14 +110,14 @@ class Selector:
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):
+ 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 = ''
@@ -134,7 +138,7 @@ class Selector:
return f'{description} {current}'
- def set_current_selection(self, current :Optional[str]):
+ def set_current_selection(self, current: Optional[Any]):
self._current_selection = current
def has_selection(self) -> bool:
@@ -158,14 +162,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,25 +186,28 @@ 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()
+
@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]:
@@ -216,7 +226,50 @@ 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 _missing_configs(self) -> List[str]:
+ def check(s):
+ return self._menu_options.get(s).has_selection()
+
+ def has_superuser() -> bool:
+ sel = self._menu_options['!users']
+ if sel.current_selection:
+ return any([u.sudo for u in sel.current_selection])
+ return False
+
+ mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items()))
+ missing = set()
+
+ for key, selector in mandatory_fields.items():
+ if key in ['!root-password', '!users']:
+ if not check('!root-password') and not has_superuser():
+ missing.add(
+ str(_('Either root-password or at least 1 user with sudo privileges must be specified'))
+ )
+ elif key == 'disk_config':
+ if not check('disk_config'):
+ missing.add(self._menu_options['disk_config'].description)
+
+ return list(missing)
+
+ 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()
"""
@@ -226,7 +279,7 @@ class AbstractMenu:
""" will be called before each action in the menu """
return
- def post_callback(self, selection_name: Optional[str] = None, value: Any = None):
+ def post_callback(self, selection_name: str, value: Any):
""" will be called after each action in the menu """
return True
@@ -234,31 +287,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 +312,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,12 +325,7 @@ 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()
cursor_pos = None
while True:
@@ -341,13 +378,13 @@ class AbstractMenu:
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 +393,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,17 +409,21 @@ 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)
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):
@@ -392,7 +433,7 @@ class AbstractMenu:
if self._data_store.get('keyboard-layout', None) and len(self._data_store['keyboard-layout']):
set_keyboard_language(self._data_store['keyboard-layout'])
- def _verify_selection_enabled(self, selection_name :str) -> bool:
+ def _verify_selection_enabled(self, selection_name: str) -> bool:
""" general """
if selection := self._menu_options.get(selection_name, None):
if not selection.enabled:
@@ -429,16 +470,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 +482,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:
+ from ..user_interaction.general_conf import select_archinstall_language
language = select_archinstall_language(self.translation_handler.translated_languages, preset_value)
self._translation_handler.activate(language)
return language
class AbstractSubMenu(AbstractMenu):
- def __init__(self, data_store: Optional[Dict[str, Any]] = None):
+ def __init__(self, data_store: Dict[str, Any] = {}):
super().__init__(data_store=data_store)
self._menu_options['__separator__'] = Selector('')
self._menu_options['back'] = \
Selector(
- _('Back'),
+ Menu.back(),
no_store=True,
enabled=True,
exec_func=lambda n, v: True,