Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall/lib/menu
diff options
context:
space:
mode:
Diffstat (limited to 'archinstall/lib/menu')
-rw-r--r--archinstall/lib/menu/global_menu.py134
-rw-r--r--archinstall/lib/menu/list_manager.py314
-rw-r--r--archinstall/lib/menu/menu.py74
-rw-r--r--archinstall/lib/menu/selection_menu.py55
-rw-r--r--archinstall/lib/menu/simple_menu.py23
5 files changed, 246 insertions, 354 deletions
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)