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.py264
-rw-r--r--archinstall/lib/menu/list_manager.py97
-rw-r--r--archinstall/lib/menu/menu.py159
-rw-r--r--archinstall/lib/menu/selection_menu.py121
-rw-r--r--archinstall/lib/menu/simple_menu.py15
5 files changed, 447 insertions, 209 deletions
diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py
index afccd45b..5cb27cab 100644
--- a/archinstall/lib/menu/global_menu.py
+++ b/archinstall/lib/menu/global_menu.py
@@ -1,6 +1,8 @@
from __future__ import annotations
-from typing import Any, List, Optional, Union
+from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING
+
+import archinstall
from ..menu import Menu
from ..menu.selection_menu import Selector, GeneralMenu
@@ -8,8 +10,7 @@ from ..general import SysCommand, secret
from ..hardware import has_uefi
from ..models import NetworkConfiguration
from ..storage import storage
-from ..output import log
-from ..profiles import is_desktop_profile
+from ..profiles import is_desktop_profile, Profile
from ..disk import encrypted_partitions
from ..user_interaction import get_password, ask_for_a_timezone, save_config
@@ -20,7 +21,6 @@ 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_to_configure_network
-from ..user_interaction import ask_for_superuser_account
from ..user_interaction import ask_for_additional_users
from ..user_interaction import select_language
from ..user_interaction import select_mirror_regions
@@ -32,126 +32,147 @@ from ..user_interaction import select_encrypted_partitions
from ..user_interaction import select_harddrives
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
+
class GlobalMenu(GeneralMenu):
def __init__(self,data_store):
- super().__init__(data_store=data_store, auto_cursor=True)
+ super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3)
def _setup_selection_menu_options(self):
# archinstall.Language will not use preset values
self._menu_options['archinstall-language'] = \
Selector(
- _('Select Archinstall language'),
- lambda x: self._select_archinstall_language('English'),
+ _('Archinstall language'),
+ lambda x: self._select_archinstall_language(x),
default='English')
self._menu_options['keyboard-layout'] = \
- Selector(_('Select keyboard layout'), lambda preset: select_language('us',preset), default='us')
+ Selector(
+ _('Keyboard layout'),
+ lambda preset: select_language(preset),
+ default='us')
self._menu_options['mirror-region'] = \
Selector(
- _('Select mirror region'),
- select_mirror_regions,
+ _('Mirror region'),
+ lambda preset: select_mirror_regions(preset),
display_func=lambda x: list(x.keys()) if x else '[]',
default={})
self._menu_options['sys-language'] = \
- Selector(_('Select locale language'), lambda preset: select_locale_lang('en_US',preset), default='en_US')
+ Selector(
+ _('Locale language'),
+ lambda preset: select_locale_lang(preset),
+ default='en_US')
self._menu_options['sys-encoding'] = \
- Selector(_('Select locale encoding'), lambda preset: select_locale_enc('utf-8',preset), default='utf-8')
+ Selector(
+ _('Locale encoding'),
+ lambda preset: select_locale_enc(preset),
+ default='UTF-8')
self._menu_options['harddrives'] = \
Selector(
- _('Select harddrives'),
- self._select_harddrives)
+ _('Drive(s)'),
+ lambda preset: self._select_harddrives(preset),
+ display_func=lambda x: f'{len(x)} ' + str(_('Drive(s)')) if x is not None and len(x) > 0 else '',
+ preview_func=self._prev_harddrives,
+ )
self._menu_options['disk_layouts'] = \
Selector(
- _('Select disk layout'),
- lambda x: select_disk_layout(
+ _('Disk layout'),
+ lambda preset: select_disk_layout(
+ preset,
storage['arguments'].get('harddrives', []),
storage['arguments'].get('advanced', False)
),
+ preview_func=self._prev_disk_layouts,
+ display_func=lambda x: self._display_disk_layout(x),
dependencies=['harddrives'])
self._menu_options['!encryption-password'] = \
Selector(
- _('Set encryption password'),
+ _('Encryption password'),
lambda x: self._select_encrypted_password(),
display_func=lambda x: secret(x) if x else 'None',
dependencies=['harddrives'])
+ self._menu_options['HSM'] = Selector(
+ description=_('Use HSM to unlock encrypted drive'),
+ func=lambda preset: self._select_hsm(preset),
+ dependencies=['!encryption-password'],
+ default=None
+ )
self._menu_options['swap'] = \
Selector(
- _('Use swap'),
+ _('Swap'),
lambda preset: ask_for_swap(preset),
default=True)
self._menu_options['bootloader'] = \
Selector(
- _('Select bootloader'),
+ _('Bootloader'),
lambda preset: ask_for_bootloader(storage['arguments'].get('advanced', False),preset),
default="systemd-bootctl" if has_uefi() else "grub-install")
self._menu_options['hostname'] = \
Selector(
- _('Specify hostname'),
+ _('Hostname'),
ask_hostname,
default='archlinux')
# root password won't have preset value
self._menu_options['!root-password'] = \
Selector(
- _('Set root password'),
+ _('Root password'),
lambda preset:self._set_root_password(),
display_func=lambda x: secret(x) if x else 'None')
- self._menu_options['!superusers'] = \
- Selector(
- _('Specify superuser account'),
- lambda preset: self._create_superuser_account(),
- default={},
- exec_func=lambda n,v:self._users_resynch(),
- dependencies_not=['!root-password'],
- display_func=lambda x: self._display_superusers())
self._menu_options['!users'] = \
Selector(
- _('Specify user account'),
- lambda x: self._create_user_account(),
+ _('User account'),
+ lambda x: self._create_user_account(x),
default={},
- exec_func=lambda n,v:self._users_resynch(),
- display_func=lambda x: list(x.keys()) if x else '[]')
+ display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else None,
+ preview_func=self._prev_users)
self._menu_options['profile'] = \
Selector(
- _('Specify profile'),
- lambda x: self._select_profile(),
- display_func=lambda x: x if x else 'None')
+ _('Profile'),
+ lambda preset: self._select_profile(preset),
+ display_func=lambda x: x if x else 'None'
+ )
self._menu_options['audio'] = \
Selector(
- _('Select audio'),
+ _('Audio'),
lambda preset: ask_for_audio_selection(is_desktop_profile(storage['arguments'].get('profile', None)),preset),
display_func=lambda x: x if x else 'None',
default=None
)
self._menu_options['kernels'] = \
Selector(
- _('Select kernels'),
+ _('Kernels'),
lambda preset: select_kernel(preset),
default=['linux'])
self._menu_options['packages'] = \
Selector(
- _('Additional packages to install'),
+ _('Additional packages'),
# lambda x: ask_additional_packages_to_install(storage['arguments'].get('packages', None)),
ask_additional_packages_to_install,
default=[])
self._menu_options['additional-repositories'] = \
Selector(
- _('Additional repositories to enable'),
+ _('Optional repositories'),
select_additional_repositories,
default=[])
self._menu_options['nic'] = \
Selector(
- _('Configure network'),
+ _('Network configuration'),
ask_to_configure_network,
display_func=lambda x: self._prev_network_configuration(x),
default={})
self._menu_options['timezone'] = \
Selector(
- _('Select timezone'),
+ _('Timezone'),
lambda preset: ask_for_a_timezone(preset),
default='UTC')
self._menu_options['ntp'] = \
Selector(
- _('Set automatic time sync (NTP)'),
+ _('Automatic time sync (NTP)'),
lambda preset: self._select_ntp(preset),
default=True)
self._menu_options['__separator__'] = \
@@ -172,7 +193,7 @@ class GlobalMenu(GeneralMenu):
def _update_install_text(self, name :str = None, result :Any = None):
text = self._install_text()
- self._menu_options.get('install').update_description(text)
+ self._menu_options['install'].update_description(text)
def post_callback(self,name :str = None ,result :Any = None):
self._update_install_text(name, result)
@@ -182,14 +203,21 @@ class GlobalMenu(GeneralMenu):
# If no partitions was marked as encrypted, but a password was supplied and we have some disks to format..
# 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:
- storage['arguments']['disk_layouts'] = select_encrypted_partitions(
- storage['arguments']['disk_layouts'], storage['arguments']['!encryption-password'])
+ 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']
def _install_text(self):
missing = len(self._missing_configs())
if missing > 0:
return _('Install ({} config(s) missing)').format(missing)
- return 'Install'
+ return _('Install')
def _prev_network_configuration(self, cur_value: Union[NetworkConfiguration, List[NetworkConfiguration]]) -> str:
if not cur_value:
@@ -201,6 +229,35 @@ class GlobalMenu(GeneralMenu):
else:
return str(cur_value)
+ 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 None
+
+ def _prev_disk_layouts(self) -> Optional[str]:
+ selector = self._menu_options['disk_layouts']
+ if selector.has_selection():
+ layouts: Dict[str, Dict[str, Any]] = selector.current_selection
+
+ output = ''
+ for device, layout in layouts.items():
+ output += f'{_("Device")}: {device}\n\n'
+ output += current_partition_layout(layout['partitions'], with_title=False)
+ output += '\n\n'
+
+ return output.rstrip()
+
+ return None
+
+ def _display_disk_layout(self, current_value: Optional[Dict[str, Any]]) -> str:
+ if current_value:
+ total_partitions = [entry['partitions'] for entry in current_value.values()]
+ total_nr = sum([len(p) for p in total_partitions])
+ return f'{total_nr} {_("Partitions")}'
+ return ''
+
def _prev_install_missing_config(self) -> Optional[str]:
if missing := self._missing_configs():
text = str(_('Missing configurations:\n'))
@@ -209,31 +266,42 @@ class GlobalMenu(GeneralMenu):
return text[:-1] # remove last new line
return None
+ def _prev_users(self) -> Optional[str]:
+ selector = self._menu_options['!users']
+ if selector.has_selection():
+ users: List[User] = selector.current_selection
+ return FormattedOutput.as_table(users)
+ return None
+
def _missing_configs(self) -> List[str]:
def check(s):
return self._menu_options.get(s).has_selection()
+ def has_superuser() -> bool:
+ users = self._menu_options['!users'].current_selection
+ return any([u.sudo for u in users])
+
missing = []
if not check('bootloader'):
missing += ['Bootloader']
if not check('hostname'):
missing += ['Hostname']
- if not check('!root-password') and not check('!superusers'):
- missing += [str(_('Either root-password or at least 1 superuser must be specified'))]
+ 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.get('harddrives').is_empty() and not check('disk_layouts'):
+ if not self._menu_options['harddrives'].is_empty() and not check('disk_layouts'):
missing += ['Disk layout']
return missing
- def _set_root_password(self):
+ def _set_root_password(self) -> Optional[str]:
prompt = str(_('Enter root password (leave blank to disable root): '))
password = get_password(prompt=prompt)
return password
- def _select_encrypted_password(self):
+ def _select_encrypted_password(self) -> Optional[str]:
if passwd := get_password(prompt=str(_('Enter disk encryption password (leave blank for no encryption): '))):
return passwd
else:
@@ -247,59 +315,75 @@ class GlobalMenu(GeneralMenu):
return ntp
- def _select_harddrives(self, old_harddrives : list) -> list:
- # old_haddrives = storage['arguments'].get('harddrives', [])
+ def _select_harddrives(self, old_harddrives : list) -> List:
harddrives = select_harddrives(old_harddrives)
- # in case the harddrives got changed we have to reset the disk layout as well
- if old_harddrives != harddrives:
- self._menu_options.get('disk_layouts').set_current_selection(None)
- storage['arguments']['disk_layouts'] = {}
-
- if not 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'])
- choice = Menu(prompt, ['yes', 'no'], default_option='yes').run()
+ choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), skip=False).run()
- if choice == 'no':
- exit(1)
+ if choice.value == Menu.no():
+ return self._select_harddrives(old_harddrives)
+
+ # 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
- def _select_profile(self):
- profile = select_profile()
+ def _select_profile(self, preset):
+ profile = select_profile(preset)
+ ret = None
+
+ if profile is None:
+ if any([
+ archinstall.storage.get('profile_minimal', False),
+ archinstall.storage.get('_selected_servers', None),
+ archinstall.storage.get('_desktop_profile', None),
+ archinstall.arguments.get('desktop-environment', None),
+ archinstall.arguments.get('gfx_driver_packages', None)
+ ]):
+ return preset
+ else: # ctrl+c was actioned and all profile settings have been reset
+ return None
+
+ servers = archinstall.storage.get('_selected_servers', [])
+ desktop = archinstall.storage.get('_desktop_profile', None)
+ desktop_env = archinstall.arguments.get('desktop-environment', None)
+ gfx_driver = archinstall.arguments.get('gfx_driver_packages', None)
# Check the potentially selected profiles preparations to get early checks if some additional questions are needed.
if profile and profile.has_prep_function():
namespace = f'{profile.namespace}.py'
with profile.load_instructions(namespace=namespace) as imported:
- if not imported._prep_function():
- log(' * Profile\'s preparation requirements was not fulfilled.', fg='red')
- exit(1)
-
- return profile
-
- def _create_superuser_account(self):
- superusers = ask_for_superuser_account(str(_('Manage superuser accounts: ')))
- return superusers if superusers else None
-
- def _create_user_account(self):
- users = ask_for_additional_users(str(_('Manage ordinary user accounts: ')))
+ if imported._prep_function(servers=servers, desktop=desktop, desktop_env=desktop_env, gfx_driver=gfx_driver):
+ ret: Profile = profile
+
+ match ret.name:
+ case 'minimal':
+ reset = ['_selected_servers', '_desktop_profile', 'desktop-environment', 'gfx_driver_packages']
+ case 'server':
+ reset = ['_desktop_profile', 'desktop-environment']
+ case 'desktop':
+ reset = ['_selected_servers']
+ case 'xorg':
+ reset = ['_selected_servers', '_desktop_profile', 'desktop-environment']
+
+ for r in reset:
+ archinstall.storage[r] = None
+ else:
+ return self._select_profile(preset)
+ elif profile:
+ ret = profile
+
+ return ret
+
+ def _create_user_account(self, defined_users: List[User]) -> List[User]:
+ users = ask_for_additional_users(defined_users=defined_users)
return users
-
- def _display_superusers(self):
- superusers = self._data_store.get('!superusers', {})
-
- if self._menu_options.get('!root-password').has_selection():
- return list(superusers.keys()) if superusers else '[]'
- else:
- return list(superusers.keys()) if superusers else ''
-
- def _users_resynch(self):
- self.synch('!superusers')
- self.synch('!users')
- return False
diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py
index 4e6dffbe..cb567093 100644
--- a/archinstall/lib/menu/list_manager.py
+++ b/archinstall/lib/menu/list_manager.py
@@ -84,12 +84,12 @@ The contents in the base class of this methods serve for a very basic usage, and
```
"""
+import copy
+from os import system
+from typing import Union, Any, TYPE_CHECKING, Dict, Optional
from .text_input import TextInput
from .menu import Menu
-from os import system
-from copy import copy
-from typing import Union, Any, List, TYPE_CHECKING
if TYPE_CHECKING:
_: Any
@@ -144,82 +144,99 @@ class ListManager:
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.base_data = base_list
- self._data = copy(base_list) # as refs, changes are immediate
+ 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 = None
+ self.target: Optional[Any] = None
self.action = self._null_action
+
if len(self._data) == 0 and self._null_action:
- self.exec_action(self._data)
+ self._data = self.exec_action(self._data)
def run(self):
while True:
- self._data_formatted = self.reformat(self._data)
- options = self._data_formatted + [self.separator]
+ # 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
+
system('clear')
- target = Menu(self._prompt,
+
+ target = Menu(
+ self._prompt,
options,
sort=False,
clear_screen=False,
clear_menu_on_exit=False,
header=self.header,
- skip_empty_entries=True).run()
+ skip_empty_entries=True,
+ skip=False
+ ).run()
- if not target or target in self.bottom_list:
+ if not target.value or target.value in self.bottom_list:
self.action = target
break
- if target and target == self.separator:
- continue
- if target and target in self._default_action:
- self.action = target
- target = None
+
+ if target.value and target.value in self._default_action:
+ self.action = target.value
self.target = None
- self.exec_action(self._data)
+ self._data = self.exec_action(self._data)
continue
+
if isinstance(self._data,dict):
- key = list(self._data.keys())[self._data_formatted.index(target)]
- self.target = {key: self._data[key]}
+ 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[self._data_formatted.index(target)]
+ 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)
+ self.run_actions(target.value)
- if not target or target == self.cancel_action: # TODO dubious
- return self.base_data # return the original list
+ if target.value == self.cancel_action: # TODO dubious
+ 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)
- self.action = Menu(
+ choice = Menu(
prompt,
options,
sort=False,
clear_screen=False,
clear_menu_on_exit=False,
preset_values=self.bottom_item,
- show_search_hint=False).run()
- if not self.action or self.action == self.cancel_action:
- return False
- else:
- return self.exec_action(self._data)
+ show_search_hint=False
+ ).run()
+
+ self.action = choice.value
+
+ if self.action and self.action != self.cancel_action:
+ self._data = self.exec_action(self._data)
+
"""
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: Any) -> List[Any]:
+ 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 list(map(lambda x:f"{x} : {data[x]}",data))
+ return {f'{k}: {v}': k for k, v in data.items()}
else:
- return list(map(lambda x:str(x),data))
+ return {str(k): k for k in data}
def action_list(self):
"""
@@ -238,18 +255,18 @@ class ListManager:
# TODO guarantee unicity
if isinstance(self._data,list):
if self.action == str(_('Add')):
- self.target = TextInput(_('Add :'),None).run()
+ 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()
+ 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(_('Edite :'),tgt).run()
+ result = TextInput(_('Edit: '),tgt).run()
self._data[idx] = result
elif self.action == str(_('Delete')):
del self._data[self._data.index(self.target)]
@@ -261,8 +278,8 @@ class ListManager:
origkey = None
origval = None
if self.action == str(_('Add')):
- key = TextInput(_('Key :'),None).run()
- value = TextInput(_('Value :'),None).run()
+ key = TextInput(_('Key: '),None).run()
+ value = TextInput(_('Value: '),None).run()
self._data[key] = value
if self.action == str(_('Copy')):
while True:
@@ -271,7 +288,9 @@ class ListManager:
self._data[key] = origval
break
elif self.action == str(_('Edit')):
- value = TextInput(_(f'Edit {origkey} :'),origval).run()
+ value = TextInput(_('Edit {}: ').format(origkey), origval).run()
self._data[origkey] = value
elif self.action == str(_('Delete')):
del self._data[origkey]
+
+ return self._data
diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py
index d254e0f9..c34814eb 100644
--- a/archinstall/lib/menu/menu.py
+++ b/archinstall/lib/menu/menu.py
@@ -1,4 +1,6 @@
-from typing import Dict, List, Union, Any, TYPE_CHECKING
+from dataclasses import dataclass
+from enum import Enum, auto
+from typing import Dict, List, Union, Any, TYPE_CHECKING, Optional
from archinstall.lib.menu.simple_menu import TerminalMenu
@@ -12,21 +14,49 @@ import logging
if TYPE_CHECKING:
_: Any
+
+class MenuSelectionType(Enum):
+ Selection = auto()
+ Esc = auto()
+ Ctrl_c = auto()
+
+
+@dataclass
+class MenuSelection:
+ type_: MenuSelectionType
+ value: Optional[Union[str, List[str]]] = None
+
+
class Menu(TerminalMenu):
+
+ @classmethod
+ def yes(cls):
+ return str(_('yes'))
+
+ @classmethod
+ def no(cls):
+ return str(_('no'))
+
+ @classmethod
+ def yes_no(cls):
+ return [cls.yes(), cls.no()]
+
def __init__(
self,
title :str,
p_options :Union[List[str], Dict[str, Any]],
skip :bool = True,
multi :bool = False,
- default_option :str = None,
+ default_option : Optional[str] = None,
sort :bool = True,
preset_values :Union[str, List[str]] = None,
- cursor_index :int = None,
+ cursor_index : Optional[int] = None,
preview_command=None,
preview_size=0.75,
preview_title='Info',
header :Union[List[str],str] = None,
+ explode_on_interrupt :bool = False,
+ explode_warning :str = '',
**kwargs
):
"""
@@ -66,9 +96,15 @@ class Menu(TerminalMenu):
:param preview_title: Title of the preview window
:type preview_title: str
- param: header one or more header lines for the menu
+ 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
+ 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
+ type param: str
+
:param kwargs : any SimpleTerminal parameter
"""
# we guarantee the inmutability of the options outside the class.
@@ -85,6 +121,8 @@ class Menu(TerminalMenu):
log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING)
raise RequirementError("Menu() requires an iterable as option.")
+ self._default_str = str(_('(default)'))
+
if isinstance(p_options,dict):
options = list(p_options.keys())
else:
@@ -103,27 +141,40 @@ class Menu(TerminalMenu):
if sort:
options = sorted(options)
- self.menu_options = options
- self.skip = skip
- self.default_option = default_option
- self.multi = multi
+ self._menu_options = options
+ self._skip = skip
+ self._default_option = default_option
+ self._multi = multi
+ self._explode_on_interrupt = explode_on_interrupt
+ self._explode_warning = explode_warning
+
menu_title = f'\n{title}\n\n'
+
if header:
- separator = '\n '
if not isinstance(header,(list,tuple)):
- header = [header,]
- if skip:
- menu_title += str(_("Use ESC to skip\n"))
- menu_title += separator + separator.join(header)
- elif skip:
- menu_title += str(_("Use ESC to skip\n\n"))
+ header = [header]
+ header = '\n'.join(header)
+ menu_title += f'\n{header}\n'
+
+ action_info = ''
+ if skip:
+ action_info += str(_("Use 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'))
+
+ menu_title += action_info
+
if default_option:
# if a default value was specified we move that one
# to the top of the list and mark it as default as well
- default = f'{default_option} (default)'
- self.menu_options = [default] + [o for o in self.menu_options if default_option != o]
+ default = f'{default_option} {self._default_str}'
+ self._menu_options = [default] + [o for o in self._menu_options if default_option != o]
+
+ self._preselection(preset_values,cursor_index)
- self.preselection(preset_values,cursor_index)
cursor = "> "
main_menu_cursor_style = ("fg_cyan", "bold")
main_menu_style = ("bg_blue", "fg_gray")
@@ -131,8 +182,9 @@ class Menu(TerminalMenu):
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,
+ menu_entries=self._menu_options,
title=menu_title,
menu_cursor=cursor,
menu_cursor_style=main_menu_cursor_style,
@@ -146,31 +198,46 @@ class Menu(TerminalMenu):
preview_command=preview_command,
preview_size=preview_size,
preview_title=preview_title,
+ explode_on_interrupt=self._explode_on_interrupt,
+ multi_select_select_on_accept=False,
**kwargs,
)
- def _show(self):
- idx = self.show()
+ def _show(self) -> MenuSelection:
+ try:
+ idx = self.show()
+ except KeyboardInterrupt:
+ return MenuSelection(type_=MenuSelectionType.Ctrl_c)
+
+ def check_default(elem):
+ if self._default_option is not None and f'{self._default_option} {self._default_str}' in elem:
+ return self._default_option
+ else:
+ return elem
+
if idx is not None:
if isinstance(idx, (list, tuple)):
- return [self.default_option if ' (default)' in self.menu_options[i] else self.menu_options[i] for i in idx]
+ results = []
+ for i in idx:
+ option = check_default(self._menu_options[i])
+ results.append(option)
+ return MenuSelection(type_=MenuSelectionType.Selection, value=results)
else:
- selected = self.menu_options[idx]
- if ' (default)' in selected and self.default_option:
- return self.default_option
- return selected
+ result = check_default(self._menu_options[idx])
+ return MenuSelection(type_=MenuSelectionType.Selection, value=result)
else:
- if self.default_option:
- if self.multi:
- return [self.default_option]
- else:
- return self.default_option
- return None
-
- def run(self):
+ return MenuSelection(type_=MenuSelectionType.Esc)
+
+ def run(self) -> MenuSelection:
ret = self._show()
- if ret is None and not self.skip:
+ 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 response.value == Menu.no():
+ return self.run()
+
+ if ret.type_ is not MenuSelectionType.Selection and not self._skip:
return self.run()
return ret
@@ -185,15 +252,15 @@ class Menu(TerminalMenu):
pos = self._menu_entries.index(value)
self.set_cursor_pos(pos)
- def preselection(self,preset_values :list = [],cursor_index :int = None):
+ def _preselection(self,preset_values :Union[str, List[str]] = [], cursor_index : Optional[int] = None):
def from_preset_to_cursor():
if preset_values:
# if the value is not extant return 0 as cursor index
try:
if isinstance(preset_values,str):
- self.cursor_index = self.menu_options.index(self.preset_values)
+ self.cursor_index = self._menu_options.index(self.preset_values)
else: # should return an error, but this is smoother
- self.cursor_index = self.menu_options.index(self.preset_values[0])
+ self.cursor_index = self._menu_options.index(self.preset_values[0])
except ValueError:
self.cursor_index = 0
@@ -203,13 +270,13 @@ class Menu(TerminalMenu):
return
self.preset_values = preset_values
- if self.default_option:
- if isinstance(preset_values,str) and self.default_option == preset_values:
- self.preset_values = f"{preset_values} (default)"
- elif isinstance(preset_values,(list,tuple)) and self.default_option in preset_values:
- idx = preset_values.index(self.default_option)
- self.preset_values[idx] = f"{preset_values[idx]} (default)"
- if cursor_index is None or not self.multi:
+ if self._default_option:
+ if isinstance(preset_values,str) and self._default_option == preset_values:
+ self.preset_values = f"{preset_values} {self._default_str}"
+ elif isinstance(preset_values,(list,tuple)) and self._default_option in preset_values:
+ idx = preset_values.index(self._default_option)
+ self.preset_values[idx] = f"{preset_values[idx]} {self._default_str}"
+ if cursor_index is None or not self._multi:
from_preset_to_cursor()
- if not self.multi: # Not supported by the infraestructure
+ if not self._multi: # Not supported by the infraestructure
self.preset_values = None
diff --git a/archinstall/lib/menu/selection_menu.py b/archinstall/lib/menu/selection_menu.py
index b2f99423..57e290f1 100644
--- a/archinstall/lib/menu/selection_menu.py
+++ b/archinstall/lib/menu/selection_menu.py
@@ -2,23 +2,27 @@ from __future__ import annotations
import logging
import sys
+import pathlib
from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING
-from .menu import Menu
+from .menu import Menu, MenuSelectionType
from ..locale_helpers import set_keyboard_language
from ..output import log
from ..translation import Translation
+from ..hsm.fido import get_fido2_devices
if TYPE_CHECKING:
_: Any
-def select_archinstall_language(default='English'):
+
+def select_archinstall_language(preset_value: str) -> Optional[Any]:
"""
copied from user_interaction/general_conf.py as a temporary measure
"""
- languages = Translation.get_all_names()
- language = Menu(_('Select Archinstall language'), languages, default_option=default).run()
- return language
+ languages = Translation.get_available_lang()
+ language = Menu(_('Archinstall language'), languages, preset_values=preset_value).run()
+ return language.value
+
class Selector:
def __init__(
@@ -91,6 +95,10 @@ class Selector:
self._no_store = no_store
@property
+ def description(self) -> str:
+ return self._description
+
+ @property
def dependencies(self) -> List:
return self._dependencies
@@ -115,7 +123,7 @@ class Selector:
def update_description(self, description :str):
self._description = description
- def menu_text(self) -> str:
+ def menu_text(self, padding: int = 0) -> str:
if self._description == '': # special menu option for __separator__
return ''
@@ -128,14 +136,14 @@ class Selector:
current = str(self._current_selection)
if current:
- padding = 35 - len(str(self._description))
- current = ' ' * padding + f'SET: {current}'
-
- return f'{self._description} {current}'
+ padding += 5
+ description = str(self._description).ljust(padding, ' ')
+ current = str(_('set: {}').format(current))
+ else:
+ description = self._description
+ current = ''
- @property
- def text(self):
- return self.menu_text()
+ return f'{description} {current}'
def set_current_selection(self, current :Optional[str]):
self._current_selection = current
@@ -262,8 +270,14 @@ class GeneralMenu:
return preview()
return None
+ def _get_menu_text_padding(self, entries: List[Selector]):
+ return max([len(str(selection.description)) for selection in entries])
+
def _find_selection(self, selection_name: str) -> Tuple[str, Selector]:
- option = [(k, v) for k, v in self._menu_options.items() if v.text.strip() == selection_name.strip()]
+ 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()]
+
if len(option) != 1:
raise ValueError(f'Selection not found: {selection_name}')
config_name = option[0][0]
@@ -275,14 +289,18 @@ class GeneralMenu:
# 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.post_callback() # as all the values can vary i have to exec this 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()
- menu_options = [m.text for m in enabled_menus.values()]
+
+ padding = self._get_menu_text_padding(list(enabled_menus.values()))
+ menu_options = [m.menu_text(padding) for m in enabled_menus.values()]
selection = Menu(
_('Set/Modify the below options'),
@@ -291,18 +309,31 @@ class GeneralMenu:
cursor_index=cursor_pos,
preview_command=self._preview_display,
preview_size=self.preview_size,
- skip_empty_entries=True
+ skip_empty_entries=True,
+ skip=False
).run()
- if selection and self.auto_cursor:
- cursor_pos = menu_options.index(selection) + 1 # before the strip otherwise fails
- if cursor_pos >= len(menu_options):
- cursor_pos = len(menu_options) - 1
- selection = selection.strip()
- if selection:
- # 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(selection):
- break
+ if selection.type_ == MenuSelectionType.Selection:
+ value = selection.value
+
+ if self.auto_cursor:
+ cursor_pos = menu_options.index(value) + 1 # before the strip otherwise fails
+
+ # in case the new position lands on a "placeholder" we'll skip them as well
+ while True:
+ if cursor_pos >= len(menu_options):
+ cursor_pos = 0
+ if len(menu_options[cursor_pos]) > 0:
+ break
+ cursor_pos += 1
+
+ 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 not self.is_context_mgr:
self.__exit__()
@@ -423,15 +454,41 @@ class GeneralMenu:
def mandatory_overview(self) -> Tuple[int, int]:
mandatory_fields = 0
mandatory_waiting = 0
- for field in self._menu_options:
- option = self._menu_options[field]
+ 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, default_lang):
- language = select_archinstall_language(default_lang)
- self._translation.activate(language)
- return language
+ 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_hsm(self, preset :Optional[pathlib.Path] = None) -> Optional[pathlib.Path]:
+ title = _('Select which partitions to mark for formatting:')
+ title += '\n'
+
+ fido_devices = get_fido2_devices()
+
+ indexes = []
+ for index, path in enumerate(fido_devices.keys()):
+ title += f"{index}: {path} ({fido_devices[path]['manufacturer']} - {fido_devices[path]['product']})"
+ indexes.append(f"{index}|{fido_devices[path]['product']}")
+
+ title += '\n'
+
+ choice = Menu(title, indexes, multi=False).run()
+
+ match choice.type_:
+ case MenuSelectionType.Esc: return preset
+ case MenuSelectionType.Selection:
+ selection: Any = choice.value
+ index = int(selection.split('|',1)[0])
+ return pathlib.Path(list(fido_devices.keys())[index])
+
+ return None
diff --git a/archinstall/lib/menu/simple_menu.py b/archinstall/lib/menu/simple_menu.py
index a0a241bd..947259eb 100644
--- a/archinstall/lib/menu/simple_menu.py
+++ b/archinstall/lib/menu/simple_menu.py
@@ -596,7 +596,8 @@ 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
+ title: Optional[Union[str, Iterable[str]]] = None,
+ explode_on_interrupt: bool = False
):
def extract_shortcuts_menu_entries_and_preview_arguments(
entries: Iterable[str],
@@ -718,6 +719,7 @@ class TerminalMenu:
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 ()
)
@@ -1538,7 +1540,9 @@ class TerminalMenu:
# Only append `next_key` if it is a printable character and the first character is not the
# `search_start` key
self._search.search_text += next_key
- except KeyboardInterrupt:
+ except KeyboardInterrupt as e:
+ if self._explode_on_interrupt:
+ raise e
menu_was_interrupted = True
finally:
reset_signal_handling()
@@ -1842,6 +1846,12 @@ 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")
@@ -1971,6 +1981,7 @@ 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)