index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | archinstall/lib/menu/global_menu.py | 134 | ||||
-rw-r--r-- | archinstall/lib/menu/list_manager.py | 314 | ||||
-rw-r--r-- | archinstall/lib/menu/menu.py | 74 | ||||
-rw-r--r-- | archinstall/lib/menu/selection_menu.py | 55 | ||||
-rw-r--r-- | archinstall/lib/menu/simple_menu.py | 23 |
diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py index 5cb27cab..d1bec189 100644 --- a/archinstall/lib/menu/global_menu.py +++ b/archinstall/lib/menu/global_menu.py @@ -3,38 +3,37 @@ from __future__ import annotations from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING import archinstall - -from ..menu import Menu -from ..menu.selection_menu import Selector, GeneralMenu +from ..disk import encrypted_partitions from ..general import SysCommand, secret from ..hardware import has_uefi +from ..menu import Menu +from ..menu.selection_menu import Selector, GeneralMenu from ..models import NetworkConfiguration -from ..storage import storage +from ..models.users import User +from ..output import FormattedOutput from ..profiles import is_desktop_profile, Profile -from ..disk import encrypted_partitions - -from ..user_interaction import get_password, ask_for_a_timezone, save_config -from ..user_interaction import ask_ntp -from ..user_interaction import ask_for_swap +from ..storage import storage +from ..user_interaction import add_number_of_parrallel_downloads +from ..user_interaction import ask_additional_packages_to_install +from ..user_interaction import ask_for_additional_users +from ..user_interaction import ask_for_audio_selection from ..user_interaction import ask_for_bootloader +from ..user_interaction import ask_for_swap from ..user_interaction import ask_hostname -from ..user_interaction import ask_for_audio_selection -from ..user_interaction import ask_additional_packages_to_install +from ..user_interaction import ask_ntp from ..user_interaction import ask_to_configure_network -from ..user_interaction import ask_for_additional_users -from ..user_interaction import select_language -from ..user_interaction import select_mirror_regions -from ..user_interaction import select_locale_lang -from ..user_interaction import select_locale_enc +from ..user_interaction import get_password, ask_for_a_timezone, save_config +from ..user_interaction import select_additional_repositories from ..user_interaction import select_disk_layout -from ..user_interaction import select_kernel from ..user_interaction import select_encrypted_partitions from ..user_interaction import select_harddrives +from ..user_interaction import select_kernel +from ..user_interaction import select_language +from ..user_interaction import select_locale_enc +from ..user_interaction import select_locale_lang +from ..user_interaction import select_mirror_regions from ..user_interaction import select_profile -from ..user_interaction import select_additional_repositories -from ..models.users import User from ..user_interaction.partitioning_conf import current_partition_layout -from ..output import FormattedOutput if TYPE_CHECKING: _: Any @@ -42,6 +41,7 @@ if TYPE_CHECKING: class GlobalMenu(GeneralMenu): def __init__(self,data_store): + self._disk_check = True super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3) def _setup_selection_menu_options(self): @@ -50,7 +50,8 @@ class GlobalMenu(GeneralMenu): Selector( _('Archinstall language'), lambda x: self._select_archinstall_language(x), - default='English') + display_func=lambda x: x.display_name, + default=self.translation_handler.get_language_by_abbr('en')) self._menu_options['keyboard-layout'] = \ Selector( _('Keyboard layout'), @@ -143,6 +144,15 @@ class GlobalMenu(GeneralMenu): display_func=lambda x: x if x else 'None', default=None ) + + self._menu_options['parallel downloads'] = \ + Selector( + _('Parallel Downloads'), + add_number_of_parrallel_downloads, + display_func=lambda x: x if x else '0', + default=0 + ) + self._menu_options['kernels'] = \ Selector( _('Kernels'), @@ -163,7 +173,8 @@ class GlobalMenu(GeneralMenu): Selector( _('Network configuration'), ask_to_configure_network, - display_func=lambda x: self._prev_network_configuration(x), + display_func=lambda x: self._display_network_conf(x), + preview_func=self._prev_network_config, default={}) self._menu_options['timezone'] = \ Selector( @@ -204,14 +215,21 @@ class GlobalMenu(GeneralMenu): # Then we need to identify which partitions to encrypt. This will default to / (root). if len(list(encrypted_partitions(storage['arguments'].get('disk_layouts', [])))) == 0: for blockdevice in storage['arguments']['disk_layouts']: - for partition_index in select_encrypted_partitions( - title="Select which partitions to encrypt:", - partitions=storage['arguments']['disk_layouts'][blockdevice]['partitions'] - ): - - partition = storage['arguments']['disk_layouts'][blockdevice]['partitions'][partition_index] - partition['encrypted'] = True - partition['!password'] = storage['arguments']['!encryption-password'] + if storage['arguments']['disk_layouts'][blockdevice].get('partitions'): + for partition_index in select_encrypted_partitions( + title=_('Select which partitions to encrypt:'), + partitions=storage['arguments']['disk_layouts'][blockdevice]['partitions'], + filter_=(lambda p: p['mountpoint'] != '/boot') + ): + + partition = storage['arguments']['disk_layouts'][blockdevice]['partitions'][partition_index] + partition['encrypted'] = True + partition['!password'] = storage['arguments']['!encryption-password'] + + # We make sure generate-encryption-key-file is set on additional partitions + # other than the root partition. Otherwise they won't unlock properly #1279 + if partition['mountpoint'] != '/': + partition['generate-encryption-key-file'] = True def _install_text(self): missing = len(self._missing_configs()) @@ -219,21 +237,28 @@ class GlobalMenu(GeneralMenu): return _('Install ({} config(s) missing)').format(missing) return _('Install') - def _prev_network_configuration(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str: + def _display_network_conf(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str: if not cur_value: return _('Not configured, unavailable unless setup manually') else: if isinstance(cur_value, list): - ifaces = [x.iface for x in cur_value] - return f'Configured ifaces: {ifaces}' + return str(_('Configured {} interfaces')).format(len(cur_value)) else: return str(cur_value) + def _prev_network_config(self) -> Optional[str]: + selector = self._menu_options['nic'] + if selector.has_selection(): + ifaces = selector.current_selection + if isinstance(ifaces, list): + return FormattedOutput.as_table(ifaces) + return None + def _prev_harddrives(self) -> Optional[str]: selector = self._menu_options['harddrives'] if selector.has_selection(): drives = selector.current_selection - return '\n\n'.join([d.display_info for d in drives]) + return FormattedOutput.as_table(drives) return None def _prev_disk_layouts(self) -> Optional[str]: @@ -288,11 +313,12 @@ class GlobalMenu(GeneralMenu): missing += ['Hostname'] if not check('!root-password') and not has_superuser(): missing += [str(_('Either root-password or at least 1 user with sudo privileges must be specified'))] - if not check('harddrives'): - missing += ['Hard drives'] - if check('harddrives'): - if not self._menu_options['harddrives'].is_empty() and not check('disk_layouts'): - missing += ['Disk layout'] + if self._disk_check: + if not check('harddrives'): + missing += [str(_('Drive(s)'))] + if check('harddrives'): + if not self._menu_options['harddrives'].is_empty() and not check('disk_layouts'): + missing += [str(_('Disk layout'))] return missing @@ -318,22 +344,26 @@ class GlobalMenu(GeneralMenu): def _select_harddrives(self, old_harddrives : list) -> List: harddrives = select_harddrives(old_harddrives) - if len(harddrives) == 0: - prompt = _( - "You decided to skip harddrive selection\nand will use whatever drive-setup is mounted at {} (experimental)\n" - "WARNING: Archinstall won't check the suitability of this setup\n" - "Do you wish to continue?" - ).format(storage['MOUNT_POINT']) + if harddrives is not None: + if len(harddrives) == 0: + prompt = _( + "You decided to skip harddrive selection\nand will use whatever drive-setup is mounted at {} (experimental)\n" + "WARNING: Archinstall won't check the suitability of this setup\n" + "Do you wish to continue?" + ).format(storage['MOUNT_POINT']) - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), skip=False).run() + choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), skip=False).run() - if choice.value == Menu.no(): - return self._select_harddrives(old_harddrives) + if choice.value == Menu.no(): + self._disk_check = True + return self._select_harddrives(old_harddrives) + else: + self._disk_check = False - # in case the harddrives got changed we have to reset the disk layout as well - if old_harddrives != harddrives: - self._menu_options['disk_layouts'].set_current_selection(None) - storage['arguments']['disk_layouts'] = {} + # in case the harddrives got changed we have to reset the disk layout as well + if old_harddrives != harddrives: + self._menu_options['disk_layouts'].set_current_selection(None) + storage['arguments']['disk_layouts'] = {} return harddrives diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index cb567093..ae3a6eb5 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -1,94 +1,7 @@ -#!/usr/bin/python -""" -# Purpose -ListManager is a widget based on `menu` which allows the handling of repetitive operations in a list. -Imagine you have a list and want to add/copy/edit/delete their elements. With this widget you will be shown the list -``` -Vamos alla - -Use ESC to skip - - -> uno : 1 -dos : 2 -tres : 3 -cuatro : 4 -==> -Confirm and exit -Cancel -(Press "/" to search) -``` -Once you select one of the elements of the list, you will be promted with the action to be done to the selected element -``` - -uno : 1 -dos : 2 -> tres : 3 -cuatro : 4 -==> -Confirm and exit -Cancel -(Press "/" to search) - -Select an action for < {'tres': 3} > - - -Add -Copy -Edit -Delete -> Cancel -``` -You execute the action for this element (which might or not involve user interaction) and return to the list main page -till you call one of the options `confirm and exit` which returns the modified list or `cancel` which returns the original list unchanged. -If the list is empty one action can be defined as default (usually Add). We call it **null_action** -YOu can also define a **default_action** which will appear below the separator, not tied to any element of the list. Barring explicit definition, default_action will be the null_action -``` -==> -Add -Confirm and exit -Cancel -(Press "/" to search) -``` -The default implementation can handle simple lists and a key:value dictionary. The default actions are the shown above. -A sample of basic usage is included at the end of the source. - -More sophisticaded uses can be achieved by -* changing the action list and the null_action during intialization -``` - opciones = ListManager('Vamos alla',opciones,[str(_('Add')),str(_('Delete'))],_('Add')).run() -``` -* And using following methods to overwrite/define user actions and other details: -* * `reformat`. To change the appearance of the list elements -* * `action_list`. To modify the content of the action list once an element is defined. F.i. to avoid Delete to appear for certain elements, or to add/modify action based in the value of the element. -* * `exec_action` which contains the actual code to be executed when an action is selected - -The contents in the base class of this methods serve for a very basic usage, and are to be taken as samples. Thus the best use of this class would be to subclass in your code - -``` - class ObjectList(archinstall.ListManager): - def __init__(prompt,list): - self.ObjectAction = [... list of actions ...] - self.ObjectNullAction = one ObjectAction - super().__init__(prompt,list,ObjectActions,ObjectNullAction) - def reformat(self): - ... beautfy the output of the list - def action_list(self): - ... if you need some changes to the action list based on self.target - def exec_action(self): - if self.action == self.ObjectAction[0]: - performFirstAction(self.target, ...) - - ... - resultList = ObjectList(prompt,originallist).run() -``` - -""" import copy from os import system -from typing import Union, Any, TYPE_CHECKING, Dict, Optional +from typing import Any, TYPE_CHECKING, Dict, Optional, Tuple, List -from .text_input import TextInput from .menu import Menu if TYPE_CHECKING: @@ -98,199 +11,132 @@ if TYPE_CHECKING: class ListManager: def __init__( self, - prompt :str, - base_list :Union[list,dict] , - base_actions :list = None, - null_action :str = None, - default_action :Union[str,list] = None, - header :Union[str,list] = None): + prompt: str, + entries: List[Any], + base_actions: List[str], + sub_menu_actions: List[str] + ): """ - param :prompt Text which will appear at the header + :param prompt: Text which will appear at the header type param: string | DeferredTranslation - param :base:_list list/dict of option to be shown / mainpulated - type param: list | dict - - param base_actions an alternate list of actions to the items of the object + :param entries: list/dict of option to be shown / manipulated type param: list - param: null_action action which will be taken (if any) when base_list is empty - type param: string - - param: default_action action which will be presented at the bottom of the list. Shouldn't need a target. If not present, null_action is set there. - Both Null and Default actions can be defined outside the base_actions list, as long as they are launched in exec_action - type param: string or list + :param base_actions: list of actions that is displayed in the main list manager, + usually global actions such as 'Add...' + type param: list - param: header one or more header lines for the list - type param: string or list + :param sub_menu_actions: list of actions available for a chosen entry + type param: list """ + self._original_data = copy.deepcopy(entries) + self._data = copy.deepcopy(entries) explainer = str(_('\n Choose an object from the list, and select one of the available actions for it to execute')) self._prompt = prompt + explainer if prompt else explainer - self._null_action = str(null_action) if null_action else None + self._separator = '' + self._confirm_action = str(_('Confirm and exit')) + self._cancel_action = str(_('Cancel')) - if not default_action: - self._default_action = [self._null_action] - elif isinstance(default_action,(list,tuple)): - self._default_action = default_action - else: - self._default_action = [str(default_action),] + self._terminate_actions = [self._confirm_action, self._cancel_action] + self._base_actions = base_actions + self._sub_menu_actions = sub_menu_actions - self.header = header if header else None - self.cancel_action = str(_('Cancel')) - self.confirm_action = str(_('Confirm and exit')) - self.separator = '' - self.bottom_list = [self.confirm_action,self.cancel_action] - self.bottom_item = [self.cancel_action] - self.base_actions = base_actions if base_actions else [str(_('Add')),str(_('Copy')),str(_('Edit')),str(_('Delete'))] - self._original_data = copy.deepcopy(base_list) - self._data = copy.deepcopy(base_list) # as refs, changes are immediate - # default values for the null case - self.target: Optional[Any] = None - self.action = self._null_action + self._last_choice = None - if len(self._data) == 0 and self._null_action: - self._data = self.exec_action(self._data) + @property + def last_choice(self): + return self._last_choice def run(self): while True: # this will return a dictionary with the key as the menu entry to be displayed # and the value is the original value from the self._data container data_formatted = self.reformat(self._data) - options = list(data_formatted.keys()) - options.append(self.separator) - - if self._default_action: - options += self._default_action - - options += self.bottom_list + options, header = self._prepare_selection(data_formatted) system('clear') - target = Menu( + choice = Menu( self._prompt, options, sort=False, clear_screen=False, clear_menu_on_exit=False, - header=self.header, + header=header, skip_empty_entries=True, - skip=False + skip=False, + show_search_hint=False ).run() - if not target.value or target.value in self.bottom_list: - self.action = target + if choice.value in self._base_actions: + self._data = self.handle_action(choice.value, None, self._data) + elif choice.value in self._terminate_actions: break + else: # an entry of the existing selection was choosen + selected_entry = data_formatted[choice.value] + self._run_actions_on_entry(selected_entry) - if target.value and target.value in self._default_action: - self.action = target.value - self.target = None - self._data = self.exec_action(self._data) - continue - - if isinstance(self._data,dict): - data_key = data_formatted[target.value] - key = self._data[data_key] - self.target = {data_key: key} - elif isinstance(self._data, list): - self.target = [d for d in self._data if d == data_formatted[target.value]][0] - else: - self.target = self._data[data_formatted[target.value]] - - # Possible enhacement. If run_actions returns false a message line indicating the failure - self.run_actions(target.value) - - if target.value == self.cancel_action: # TODO dubious + self._last_choice = choice + if choice.value == self._cancel_action: return self._original_data # return the original list else: return self._data - def run_actions(self,prompt_data=None): - options = self.action_list() + self.bottom_item - prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target) + def _prepare_selection(self, data_formatted: Dict[str, Any]) -> Tuple[List[str], str]: + # header rows are mapped to None so make sure + # to exclude those from the selectable data + options: List[str] = [key for key, val in data_formatted.items() if val is not None] + header = '' + + if len(options) > 0: + table_header = [key for key, val in data_formatted.items() if val is None] + header = '\n'.join(table_header) + + if len(options) > 0: + options.append(self._separator) + + options += self._base_actions + options += self._terminate_actions + + return options, header + + def _run_actions_on_entry(self, entry: Any): + options = self.filter_options(entry,self._sub_menu_actions) + [self._cancel_action] + display_value = self.selected_action_display(entry) + + prompt = _("Select an action for '{}'").format(display_value) + choice = Menu( prompt, options, sort=False, clear_screen=False, clear_menu_on_exit=False, - preset_values=self.bottom_item, show_search_hint=False ).run() - self.action = choice.value + if choice.value and choice.value != self._cancel_action: + self._data = self.handle_action(choice.value, entry, self._data) - if self.action and self.action != self.cancel_action: - self._data = self.exec_action(self._data) + def selected_action_display(self, selection: Any) -> str: + # this will return the value to be displayed in the + # "Select an action for '{}'" string + raise NotImplementedError('Please implement me in the child class') - """ - The following methods are expected to be overwritten by the user if the needs of the list are beyond the simple case - """ + def reformat(self, data: List[Any]) -> Dict[str, Any]: + # this should return a dictionary of display string to actual data entry + # mapping; if the value for a given display string is None it will be used + # in the header value (useful when displaying tables) + raise NotImplementedError('Please implement me in the child class') - def reformat(self, data: Any) -> Dict[str, Any]: - """ - method to get the data in a format suitable to be shown - It is executed once for run loop and processes the whole self._data structure - """ - if isinstance(data,dict): - return {f'{k}: {v}': k for k, v in data.items()} - else: - return {str(k): k for k in data} - - def action_list(self): - """ - can define alternate action list or customize the list for each item. - Executed after any item is selected, contained in self.target - """ - return self.base_actions - - def exec_action(self, data: Any): - """ - what's executed one an item (self.target) and one action (self.action) is selected. - Should be overwritten by the user - The result is expected to update self._data in this routine, else it is ignored - The basic code is useful for simple lists and dictionaries (key:value pairs, both strings) - """ - # TODO guarantee unicity - if isinstance(self._data,list): - if self.action == str(_('Add')): - self.target = TextInput(_('Add: '),None).run() - self._data.append(self.target) - if self.action == str(_('Copy')): - while True: - target = TextInput(_('Copy to: '),self.target).run() - if target != self.target: - self._data.append(self.target) - break - elif self.action == str(_('Edit')): - tgt = self.target - idx = self._data.index(self.target) - result = TextInput(_('Edit: '),tgt).run() - self._data[idx] = result - elif self.action == str(_('Delete')): - del self._data[self._data.index(self.target)] - elif isinstance(self._data,dict): - # allows overwrites - if self.target: - origkey,origval = list(self.target.items())[0] - else: - origkey = None - origval = None - if self.action == str(_('Add')): - key = TextInput(_('Key: '),None).run() - value = TextInput(_('Value: '),None).run() - self._data[key] = value - if self.action == str(_('Copy')): - while True: - key = TextInput(_('Copy to new key:'),origkey).run() - if key != origkey: - self._data[key] = origval - break - elif self.action == str(_('Edit')): - value = TextInput(_('Edit {}: ').format(origkey), origval).run() - self._data[origkey] = value - elif self.action == str(_('Delete')): - del self._data[origkey] + def handle_action(self, action: Any, entry: Optional[Any], data: List[Any]) -> List[Any]: + # this function is called when a base action or + # a specific action for an entry is triggered + raise NotImplementedError('Please implement me in the child class') - return self._data + def filter_options(self, selection :Any, options :List[str]) -> List[str]: + # filter which actions to show for an specific selection + return options diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index c34814eb..112bc0ae 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from enum import Enum, auto -from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional +from os import system +from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional, Callable from archinstall.lib.menu.simple_menu import TerminalMenu @@ -51,13 +52,17 @@ class Menu(TerminalMenu): sort :bool = True, preset_values :Union[str, List[str]] = None, cursor_index : Optional[int] = None, - preview_command=None, - preview_size=0.75, - preview_title='Info', + preview_command: Optional[Callable] = None, + preview_size: float = 0.75, + preview_title: str = 'Info', header :Union[List[str],str] = None, - explode_on_interrupt :bool = False, - explode_warning :str = '', - **kwargs + raise_error_on_interrupt :bool = False, + raise_error_warning_msg :str = '', + clear_screen: bool = True, + show_search_hint: bool = True, + cycle_cursor: bool = True, + clear_menu_on_exit: bool = True, + skip_empty_entries: bool = False ): """ Creates a new menu @@ -99,10 +104,10 @@ class Menu(TerminalMenu): param header: one or more header lines for the menu type param: string or list - param explode_on_interrupt: This will explicitly handle a ctrl+c instead and return that specific state + param raise_error_on_interrupt: This will explicitly handle a ctrl+c instead and return that specific state type param: bool - param explode_warning: If explode_on_interrupt is True and this is non-empty, there will be a warning with a user confirmation displayed + 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 kwargs : any SimpleTerminal parameter @@ -115,7 +120,7 @@ class Menu(TerminalMenu): # 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 mantain immutability + # 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) @@ -145,27 +150,30 @@ class Menu(TerminalMenu): self._skip = skip self._default_option = default_option self._multi = multi - self._explode_on_interrupt = explode_on_interrupt - self._explode_warning = explode_warning + self._raise_error_on_interrupt = raise_error_on_interrupt + self._raise_error_warning_msg = raise_error_warning_msg + self._preview_command = preview_command menu_title = f'\n{title}\n\n' if header: if not isinstance(header,(list,tuple)): header = [header] - header = '\n'.join(header) - menu_title += f'\n{header}\n' + menu_title += '\n'.join(header) action_info = '' if skip: - action_info += str(_("Use ESC to skip")) + action_info += str(_('ESC to skip')) - if self._explode_on_interrupt: - if len(action_info) > 0: - action_info += '\n' - action_info += str(_('Use CTRL+C to reset current selection\n\n')) + if self._raise_error_on_interrupt: + action_info += ', ' if len(action_info) > 0 else '' + action_info += str(_('CTRL+C to reset')) - menu_title += action_info + if multi: + action_info += ', ' if len(action_info) > 0 else '' + action_info += str(_('TAB to select')) + + menu_title += action_info + '\n' if default_option: # if a default value was specified we move that one @@ -178,10 +186,6 @@ class Menu(TerminalMenu): cursor = "> " main_menu_cursor_style = ("fg_cyan", "bold") main_menu_style = ("bg_blue", "fg_gray") - # defaults that can be changed up the stack - kwargs['clear_screen'] = kwargs.get('clear_screen',True) - kwargs['show_search_hint'] = kwargs.get('show_search_hint',True) - kwargs['cycle_cursor'] = kwargs.get('cycle_cursor',True) super().__init__( menu_entries=self._menu_options, @@ -195,12 +199,16 @@ class Menu(TerminalMenu): # show_search_hint=True, preselected_entries=self.preset_values, cursor_index=self.cursor_index, - preview_command=preview_command, + preview_command=lambda x: self._preview_wrapper(preview_command, x), preview_size=preview_size, preview_title=preview_title, - explode_on_interrupt=self._explode_on_interrupt, + raise_error_on_interrupt=self._raise_error_on_interrupt, multi_select_select_on_accept=False, - **kwargs, + clear_screen=clear_screen, + show_search_hint=show_search_hint, + cycle_cursor=cycle_cursor, + clear_menu_on_exit=clear_menu_on_exit, + skip_empty_entries=skip_empty_entries ) def _show(self) -> MenuSelection: @@ -228,16 +236,24 @@ class Menu(TerminalMenu): else: return MenuSelection(type_=MenuSelectionType.Esc) + 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() if ret.type_ == MenuSelectionType.Ctrl_c: - if self._explode_on_interrupt and len(self._explode_warning) > 0: - response = Menu(self._explode_warning, Menu.yes_no(), skip=False).run() + if self._raise_error_on_interrupt and len(self._raise_error_warning_msg) > 0: + response = Menu(self._raise_error_warning_msg, Menu.yes_no(), skip=False).run() if response.value == Menu.no(): return self.run() if ret.type_ is not MenuSelectionType.Selection and not self._skip: + system('clear') return self.run() return ret diff --git a/archinstall/lib/menu/selection_menu.py b/archinstall/lib/menu/selection_menu.py index 57e290f1..8a08812c 100644 --- a/archinstall/lib/menu/selection_menu.py +++ b/archinstall/lib/menu/selection_menu.py @@ -8,22 +8,15 @@ from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CH from .menu import Menu, MenuSelectionType from ..locale_helpers import set_keyboard_language from ..output import log -from ..translation import Translation +from ..translationhandler import TranslationHandler, Language from ..hsm.fido import get_fido2_devices +from ..user_interaction.general_conf import select_archinstall_language + if TYPE_CHECKING: _: Any -def select_archinstall_language(preset_value: str) -> Optional[Any]: - """ - copied from user_interaction/general_conf.py as a temporary measure - """ - languages = Translation.get_available_lang() - language = Menu(_('Archinstall language'), languages, preset_values=preset_value).run() - return language.value - - class Selector: def __init__( self, @@ -190,13 +183,18 @@ class GeneralMenu: """ self._enabled_order :List[str] = [] - self._translation = Translation.load_nationalization() + self._translation_handler = TranslationHandler() self.is_context_mgr = False self._data_store = data_store if data_store is not None else {} 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 + + @property + def last_choice(self): + return self._last_choice def __enter__(self, *args :Any, **kwargs :Any) -> GeneralMenu: self.is_context_mgr = True @@ -217,9 +215,13 @@ class GeneralMenu: self.exit_callback() + @property + def translation_handler(self) -> TranslationHandler: + return self._translation_handler + def _setup_selection_menu_options(self): """ Define the menu options. - Menu options can be defined here in a subclass or done per progam calling self.set_option() + Menu options can be defined here in a subclass or done per program calling self.set_option() """ return @@ -227,7 +229,7 @@ class GeneralMenu: """ will be called before each action in the menu """ return - def post_callback(self, selector_name :str, value :Any): + def post_callback(self, selection_name: str = None, value: Any = None): """ will be called after each action in the menu """ return True @@ -327,12 +329,16 @@ class GeneralMenu: break cursor_pos += 1 - value = value.strip() + value = value.strip() - # if this calls returns false, we exit the menu - # we allow for an callback for special processing on realeasing control - if not self._process_selection(value): - break + # if this calls returns false, we exit the menu + # we allow for an callback for special processing on realeasing 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()} + self._last_choice = actions[selection.value.strip()] if not self.is_context_mgr: self.__exit__() @@ -347,7 +353,7 @@ class GeneralMenu: return self.exec_option(config_name, selector) def exec_option(self, config_name :str, p_selector :Selector = None) -> bool: - """ processes the exection of a given menu entry + """ processes the execution of a given menu entry - pre process callback - selection function - post process callback @@ -461,13 +467,10 @@ class GeneralMenu: mandatory_waiting += 1 return mandatory_fields, mandatory_waiting - def _select_archinstall_language(self, preset_value: str) -> str: - language = select_archinstall_language(preset_value) - if language is not None: - self._translation.activate(language) - return language - - return preset_value + def _select_archinstall_language(self, preset_value: Language) -> Language: + language = select_archinstall_language(self.translation_handler.translated_languages, preset_value) + self._translation_handler.activate(language) + return language def _select_hsm(self, preset :Optional[pathlib.Path] = None) -> Optional[pathlib.Path]: title = _('Select which partitions to mark for formatting:') diff --git a/archinstall/lib/menu/simple_menu.py b/archinstall/lib/menu/simple_menu.py index 947259eb..1980e2ce 100644 --- a/archinstall/lib/menu/simple_menu.py +++ b/archinstall/lib/menu/simple_menu.py @@ -65,7 +65,7 @@ __author__ = "Ingo Meyer" __email__ = "i.meyer@fz-juelich.de" __copyright__ = "Copyright © 2021 Forschungszentrum Jülich GmbH. All rights reserved." __license__ = "MIT" -__version_info__ = (1, 4, 1) +__version_info__ = (1, 5, 0) __version__ = ".".join(map(str, __version_info__)) @@ -86,6 +86,7 @@ DEFAULT_MULTI_SELECT_SELECT_ON_ACCEPT = True DEFAULT_PREVIEW_BORDER = True DEFAULT_PREVIEW_SIZE = 0.25 DEFAULT_PREVIEW_TITLE = "preview" +DEFAULT_QUIT_KEYS = ("escape", "q") DEFAULT_SEARCH_CASE_SENSITIVE = False DEFAULT_SEARCH_HIGHLIGHT_STYLE = ("fg_black", "bg_yellow", "bold") DEFAULT_SEARCH_KEY = "/" @@ -581,6 +582,8 @@ class TerminalMenu: preview_command: Optional[Union[str, Callable[[str], str]]] = None, preview_size: float = DEFAULT_PREVIEW_SIZE, preview_title: str = DEFAULT_PREVIEW_TITLE, + quit_keys: Iterable[str] = DEFAULT_QUIT_KEYS, + raise_error_on_interrupt: bool = False, search_case_sensitive: bool = DEFAULT_SEARCH_CASE_SENSITIVE, search_highlight_style: Optional[Iterable[str]] = DEFAULT_SEARCH_HIGHLIGHT_STYLE, search_key: Optional[str] = DEFAULT_SEARCH_KEY, @@ -596,8 +599,7 @@ class TerminalMenu: status_bar: Optional[Union[str, Iterable[str], Callable[[str], str]]] = None, status_bar_below_preview: bool = DEFAULT_STATUS_BAR_BELOW_PREVIEW, status_bar_style: Optional[Iterable[str]] = DEFAULT_STATUS_BAR_STYLE, - title: Optional[Union[str, Iterable[str]]] = None, - explode_on_interrupt: bool = False + title: Optional[Union[str, Iterable[str]]] = None ): def extract_shortcuts_menu_entries_and_preview_arguments( entries: Iterable[str], @@ -716,10 +718,11 @@ class TerminalMenu: self._preview_command = preview_command self._preview_size = preview_size self._preview_title = preview_title + self._quit_keys = tuple(quit_keys) + self._raise_error_on_interrupt = raise_error_on_interrupt self._search_case_sensitive = search_case_sensitive self._search_highlight_style = tuple(search_highlight_style) if search_highlight_style is not None else () self._search_key = search_key - self._explode_on_interrupt = explode_on_interrupt self._shortcut_brackets_highlight_style = ( tuple(shortcut_brackets_highlight_style) if shortcut_brackets_highlight_style is not None else () ) @@ -787,6 +790,7 @@ class TerminalMenu: # backspace can be queried from the terminal database but is unreliable, query the terminal directly instead self._init_backspace_control_character() self._add_missing_control_characters_for_keys(self._accept_keys) + self._add_missing_control_characters_for_keys(self._quit_keys) self._init_terminal_codes() @staticmethod @@ -1477,7 +1481,7 @@ class TerminalMenu: "menu_down": set(("down", "ctrl-j", "j")), "accept": set(self._accept_keys), "multi_select": set(self._multi_select_keys), - "quit": set(("escape", "q")), + "quit": set(self._quit_keys), "search_start": set((self._search_key,)), "backspace": set(("backspace",)), } # type: Dict[str, Set[Optional[str]]] @@ -1541,7 +1545,7 @@ class TerminalMenu: # `search_start` key self._search.search_text += next_key except KeyboardInterrupt as e: - if self._explode_on_interrupt: + if self._raise_error_on_interrupt: raise e menu_was_interrupted = True finally: @@ -1846,12 +1850,6 @@ def get_argumentparser() -> argparse.ArgumentParser: ) parser.add_argument("-t", "--title", action="store", dest="title", help="menu title") parser.add_argument( - "--explode-on-interrupt", - action="store_true", - dest="explode_on_interrupt", - help="Instead of quitting the menu, this will raise the KeyboardInterrupt Exception", - ) - parser.add_argument( "-V", "--version", action="store_true", dest="print_version", help="print the version number and exit" ) parser.add_argument("entries", action="store", nargs="*", help="the menu entries to show") @@ -1981,7 +1979,6 @@ def main() -> None: status_bar_below_preview=args.status_bar_below_preview, status_bar_style=args.status_bar_style, title=args.title, - explode_on_interrupt=args.explode_on_interrupt, ) except (InvalidParameterCombinationError, InvalidStyleError, UnknownMenuEntryError) as e: print(str(e), file=sys.stderr) |