Introduce ctrl+c and other bug fixes (#1152)

* Intergrate ctrl+c

* stash

* Update

* Fix profile reset

* flake8

Co-authored-by: Daniel Girtler <girtler.daniel@gmail.com>
This commit is contained in:
Daniel Girtler 2022-05-09 20:02:48 +10:00 committed by GitHub
parent 20ffebac50
commit 0fa52a5424
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 564 additions and 302 deletions

View File

@ -29,11 +29,11 @@ def suggest_single_disk_layout(block_device :BlockDevice,
if default_filesystem == 'btrfs': if default_filesystem == 'btrfs':
prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?')) prompt = str(_('Would you like to use BTRFS subvolumes with a default structure?'))
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
using_subvolumes = choice == Menu.yes() using_subvolumes = choice.value == Menu.yes()
prompt = str(_('Would you like to use BTRFS compression?')) prompt = str(_('Would you like to use BTRFS compression?'))
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
compression = choice == Menu.yes() compression = choice.value == Menu.yes()
layout = { layout = {
block_device.path : { block_device.path : {
@ -90,7 +90,7 @@ def suggest_single_disk_layout(block_device :BlockDevice,
if not using_subvolumes and block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART: if not using_subvolumes and block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART:
prompt = str(_('Would you like to create a separate partition for /home?')) prompt = str(_('Would you like to create a separate partition for /home?'))
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
using_home_partition = choice == Menu.yes() using_home_partition = choice.value == Menu.yes()
# Set a size for / (/root) # Set a size for / (/root)
if using_subvolumes or block_device.size < MIN_SIZE_TO_ALLOW_HOME_PART or not using_home_partition: if using_subvolumes or block_device.size < MIN_SIZE_TO_ALLOW_HOME_PART or not using_home_partition:
@ -173,7 +173,7 @@ def suggest_multi_disk_layout(block_devices :List[BlockDevice], default_filesyst
prompt = str(_('Would you like to use BTRFS compression?')) prompt = str(_('Would you like to use BTRFS compression?'))
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
compression = choice == Menu.yes() compression = choice.value == Menu.yes()
log(f"Suggesting multi-disk-layout using {len(block_devices)} disks, where {root_device} will be /root and {home_device} will be /home", level=logging.DEBUG) log(f"Suggesting multi-disk-layout using {len(block_devices)} disks, where {root_device} will be /root and {home_device} will be /home", level=logging.DEBUG)

View File

@ -2,13 +2,15 @@ from __future__ import annotations
from typing import Any, List, Optional, Union from typing import Any, List, Optional, Union
import archinstall
from ..menu import Menu from ..menu import Menu
from ..menu.selection_menu import Selector, GeneralMenu from ..menu.selection_menu import Selector, GeneralMenu
from ..general import SysCommand, secret from ..general import SysCommand, secret
from ..hardware import has_uefi from ..hardware import has_uefi
from ..models import NetworkConfiguration from ..models import NetworkConfiguration
from ..storage import storage from ..storage import storage
from ..profiles import is_desktop_profile from ..profiles import is_desktop_profile, Profile
from ..disk import encrypted_partitions from ..disk import encrypted_partitions
from ..user_interaction import get_password, ask_for_a_timezone, save_config from ..user_interaction import get_password, ask_for_a_timezone, save_config
@ -41,28 +43,38 @@ class GlobalMenu(GeneralMenu):
self._menu_options['archinstall-language'] = \ self._menu_options['archinstall-language'] = \
Selector( Selector(
_('Select Archinstall language'), _('Select Archinstall language'),
lambda x: self._select_archinstall_language('English'), lambda x: self._select_archinstall_language(x),
default='English') default='English')
self._menu_options['keyboard-layout'] = \ self._menu_options['keyboard-layout'] = \
Selector(_('Select keyboard layout'), lambda preset: select_language('us',preset), default='us') Selector(
_('Select keyboard layout'),
lambda preset: select_language(preset),
default='us')
self._menu_options['mirror-region'] = \ self._menu_options['mirror-region'] = \
Selector( Selector(
_('Select mirror region'), _('Select mirror region'),
select_mirror_regions, lambda preset: select_mirror_regions(preset),
display_func=lambda x: list(x.keys()) if x else '[]', display_func=lambda x: list(x.keys()) if x else '[]',
default={}) default={})
self._menu_options['sys-language'] = \ self._menu_options['sys-language'] = \
Selector(_('Select locale language'), lambda preset: select_locale_lang('en_US',preset), default='en_US') Selector(
_('Select locale language'),
lambda preset: select_locale_lang(preset),
default='en_US')
self._menu_options['sys-encoding'] = \ self._menu_options['sys-encoding'] = \
Selector(_('Select locale encoding'), lambda preset: select_locale_enc('utf-8',preset), default='utf-8') Selector(
_('Select locale encoding'),
lambda preset: select_locale_enc(preset),
default='UTF-8')
self._menu_options['harddrives'] = \ self._menu_options['harddrives'] = \
Selector( Selector(
_('Select harddrives'), _('Select harddrives'),
self._select_harddrives) lambda preset: self._select_harddrives(preset))
self._menu_options['disk_layouts'] = \ self._menu_options['disk_layouts'] = \
Selector( Selector(
_('Select disk layout'), _('Select disk layout'),
lambda x: select_disk_layout( lambda preset: select_disk_layout(
preset,
storage['arguments'].get('harddrives', []), storage['arguments'].get('harddrives', []),
storage['arguments'].get('advanced', False) storage['arguments'].get('advanced', False)
), ),
@ -112,7 +124,7 @@ class GlobalMenu(GeneralMenu):
self._menu_options['profile'] = \ self._menu_options['profile'] = \
Selector( Selector(
_('Specify profile'), _('Specify profile'),
lambda x: self._select_profile(), lambda preset: self._select_profile(preset),
display_func=lambda x: x if x else 'None') display_func=lambda x: x if x else 'None')
self._menu_options['audio'] = \ self._menu_options['audio'] = \
Selector( Selector(
@ -247,41 +259,73 @@ class GlobalMenu(GeneralMenu):
return ntp return ntp
def _select_harddrives(self, old_harddrives : list) -> list: def _select_harddrives(self, old_harddrives : list) -> list:
# old_haddrives = storage['arguments'].get('harddrives', [])
harddrives = select_harddrives(old_harddrives) harddrives = select_harddrives(old_harddrives)
# in case the harddrives got changed we have to reset the disk layout as well if len(harddrives) == 0:
if old_harddrives != harddrives:
self._menu_options.get('disk_layouts').set_current_selection(None)
storage['arguments']['disk_layouts'] = {}
if not harddrives:
prompt = _( prompt = _(
"You decided to skip harddrive selection\nand will use whatever drive-setup is mounted at {} (experimental)\n" "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" "WARNING: Archinstall won't check the suitability of this setup\n"
"Do you wish to continue?" "Do you wish to continue?"
).format(storage['MOUNT_POINT']) ).format(storage['MOUNT_POINT'])
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), skip=False).run()
if choice == Menu.no(): if choice.value == Menu.no():
exit(1) 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.get('disk_layouts').set_current_selection(None)
storage['arguments']['disk_layouts'] = {}
return harddrives return harddrives
def _select_profile(self): def _select_profile(self, preset):
profile = select_profile() 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. # Check the potentially selected profiles preparations to get early checks if some additional questions are needed.
if profile and profile.has_prep_function(): if profile and profile.has_prep_function():
namespace = f'{profile.namespace}.py' namespace = f'{profile.namespace}.py'
with profile.load_instructions(namespace=namespace) as imported: with profile.load_instructions(namespace=namespace) as imported:
if imported._prep_function(): if imported._prep_function(servers=servers, desktop=desktop, desktop_env=desktop_env, gfx_driver=gfx_driver):
return profile ret: Profile = profile
else:
return self._select_profile()
return self._data_store.get('profile', None) 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_superuser_account(self): def _create_superuser_account(self):
superusers = ask_for_superuser_account(str(_('Manage superuser accounts: '))) superusers = ask_for_superuser_account(str(_('Manage superuser accounts: ')))

View File

@ -86,7 +86,7 @@ The contents in the base class of this methods serve for a very basic usage, and
""" """
from .text_input import TextInput from .text_input import TextInput
from .menu import Menu from .menu import Menu, MenuSelectionType
from os import system from os import system
from copy import copy from copy import copy
from typing import Union, Any, TYPE_CHECKING, Dict from typing import Union, Any, TYPE_CHECKING, Dict
@ -167,6 +167,7 @@ class ListManager:
options += self.bottom_list options += self.bottom_list
system('clear') system('clear')
target = Menu( target = Menu(
self._prompt, self._prompt,
options, options,
@ -174,27 +175,31 @@ class ListManager:
clear_screen=False, clear_screen=False,
clear_menu_on_exit=False, clear_menu_on_exit=False,
header=self.header, header=self.header,
skip_empty_entries=True).run() skip_empty_entries=True
).run()
if not target or target in self.bottom_list: if target.type_ == MenuSelectionType.Esc:
return self.run()
if not target.value or target.value in self.bottom_list:
self.action = target self.action = target
break break
if target and target in self._default_action: if target.value and target.value in self._default_action:
self.action = target self.action = target.value
self.target = None self.target = None
self.exec_action(self._data) self.exec_action(self._data)
continue continue
if isinstance(self._data,dict): if isinstance(self._data,dict):
data_key = data_formatted[target] data_key = data_formatted[target.value]
key = self._data[data_key] key = self._data[data_key]
self.target = {data_key: key} self.target = {data_key: key}
else: else:
self.target = self._data[data_formatted[target]] self.target = self._data[data_formatted[target.value]]
# Possible enhacement. If run_actions returns false a message line indicating the failure # 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 if not target or target == self.cancel_action: # TODO dubious
return self.base_data # return the original list return self.base_data # return the original list
@ -204,14 +209,18 @@ class ListManager:
def run_actions(self,prompt_data=None): def run_actions(self,prompt_data=None):
options = self.action_list() + self.bottom_item options = self.action_list() + self.bottom_item
prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target) prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target)
self.action = Menu( choice = Menu(
prompt, prompt,
options, options,
sort=False, sort=False,
clear_screen=False, clear_screen=False,
clear_menu_on_exit=False, clear_menu_on_exit=False,
preset_values=self.bottom_item, preset_values=self.bottom_item,
show_search_hint=False).run() show_search_hint=False
).run()
self.action = choice.value
if not self.action or self.action == self.cancel_action: if not self.action or self.action == self.cancel_action:
return False return False
else: else:

View File

@ -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 from archinstall.lib.menu.simple_menu import TerminalMenu
@ -13,6 +15,18 @@ if TYPE_CHECKING:
_: Any _: 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): class Menu(TerminalMenu):
@classmethod @classmethod
@ -33,14 +47,16 @@ class Menu(TerminalMenu):
p_options :Union[List[str], Dict[str, Any]], p_options :Union[List[str], Dict[str, Any]],
skip :bool = True, skip :bool = True,
multi :bool = False, multi :bool = False,
default_option :str = None, default_option : Optional[str] = None,
sort :bool = True, sort :bool = True,
preset_values :Union[str, List[str]] = None, preset_values :Union[str, List[str]] = None,
cursor_index :int = None, cursor_index : Optional[int] = None,
preview_command=None, preview_command=None,
preview_size=0.75, preview_size=0.75,
preview_title='Info', preview_title='Info',
header :Union[List[str],str] = None, header :Union[List[str],str] = None,
explode_on_interrupt :bool = False,
explode_warning :str = '',
**kwargs **kwargs
): ):
""" """
@ -80,9 +96,15 @@ class Menu(TerminalMenu):
:param preview_title: Title of the preview window :param preview_title: Title of the preview window
:type preview_title: str :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 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 :param kwargs : any SimpleTerminal parameter
""" """
# we guarantee the inmutability of the options outside the class. # we guarantee the inmutability of the options outside the class.
@ -99,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) 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.") raise RequirementError("Menu() requires an iterable as option.")
self._default_str = str(_('(default)'))
if isinstance(p_options,dict): if isinstance(p_options,dict):
options = list(p_options.keys()) options = list(p_options.keys())
else: else:
@ -117,27 +141,40 @@ class Menu(TerminalMenu):
if sort: if sort:
options = sorted(options) options = sorted(options)
self.menu_options = options self._menu_options = options
self.skip = skip self._skip = skip
self.default_option = default_option self._default_option = default_option
self.multi = multi self._multi = multi
self._explode_on_interrupt = explode_on_interrupt
self._explode_warning = explode_warning
menu_title = f'\n{title}\n\n' menu_title = f'\n{title}\n\n'
if header: if header:
separator = '\n '
if not isinstance(header,(list,tuple)): if not isinstance(header,(list,tuple)):
header = [header,] header = [header]
if skip: header = '\n'.join(header)
menu_title += str(_("Use ESC to skip\n")) menu_title += f'\n{header}\n'
menu_title += separator + separator.join(header)
elif skip: action_info = ''
menu_title += str(_("Use ESC to skip\n\n")) 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 default_option:
# if a default value was specified we move that one # if a default value was specified we move that one
# to the top of the list and mark it as default as well # to the top of the list and mark it as default as well
default = f'{default_option} (default)' default = f'{default_option} {self._default_str}'
self.menu_options = [default] + [o for o in self.menu_options if default_option != o] 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 = "> " cursor = "> "
main_menu_cursor_style = ("fg_cyan", "bold") main_menu_cursor_style = ("fg_cyan", "bold")
main_menu_style = ("bg_blue", "fg_gray") main_menu_style = ("bg_blue", "fg_gray")
@ -145,8 +182,9 @@ class Menu(TerminalMenu):
kwargs['clear_screen'] = kwargs.get('clear_screen',True) kwargs['clear_screen'] = kwargs.get('clear_screen',True)
kwargs['show_search_hint'] = kwargs.get('show_search_hint',True) kwargs['show_search_hint'] = kwargs.get('show_search_hint',True)
kwargs['cycle_cursor'] = kwargs.get('cycle_cursor',True) kwargs['cycle_cursor'] = kwargs.get('cycle_cursor',True)
super().__init__( super().__init__(
menu_entries=self.menu_options, menu_entries=self._menu_options,
title=menu_title, title=menu_title,
menu_cursor=cursor, menu_cursor=cursor,
menu_cursor_style=main_menu_cursor_style, menu_cursor_style=main_menu_cursor_style,
@ -160,32 +198,46 @@ class Menu(TerminalMenu):
preview_command=preview_command, preview_command=preview_command,
preview_size=preview_size, preview_size=preview_size,
preview_title=preview_title, preview_title=preview_title,
explode_on_interrupt=self._explode_on_interrupt,
multi_select_select_on_accept=False, multi_select_select_on_accept=False,
**kwargs, **kwargs,
) )
def _show(self): def _show(self) -> MenuSelection:
idx = self.show() 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 idx is not None:
if isinstance(idx, (list, tuple)): 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: else:
selected = self.menu_options[idx] result = check_default(self._menu_options[idx])
if ' (default)' in selected and self.default_option: return MenuSelection(type_=MenuSelectionType.Selection, value=result)
return self.default_option
return selected
else: else:
if self.default_option: return MenuSelection(type_=MenuSelectionType.Esc)
if self.multi:
return [self.default_option]
else:
return self.default_option
return None
def run(self): def run(self) -> MenuSelection:
ret = self._show() 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 self.run()
return ret return ret
@ -200,15 +252,15 @@ class Menu(TerminalMenu):
pos = self._menu_entries.index(value) pos = self._menu_entries.index(value)
self.set_cursor_pos(pos) 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(): def from_preset_to_cursor():
if preset_values: if preset_values:
# if the value is not extant return 0 as cursor index # if the value is not extant return 0 as cursor index
try: try:
if isinstance(preset_values,str): 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 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: except ValueError:
self.cursor_index = 0 self.cursor_index = 0
@ -218,13 +270,13 @@ class Menu(TerminalMenu):
return return
self.preset_values = preset_values self.preset_values = preset_values
if self.default_option: if self._default_option:
if isinstance(preset_values,str) and self.default_option == preset_values: if isinstance(preset_values,str) and self._default_option == preset_values:
self.preset_values = f"{preset_values} (default)" self.preset_values = f"{preset_values} {self._default_str}"
elif isinstance(preset_values,(list,tuple)) and self.default_option in preset_values: elif isinstance(preset_values,(list,tuple)) and self._default_option in preset_values:
idx = preset_values.index(self.default_option) idx = preset_values.index(self._default_option)
self.preset_values[idx] = f"{preset_values[idx]} (default)" self.preset_values[idx] = f"{preset_values[idx]} {self._default_str}"
if cursor_index is None or not self.multi: if cursor_index is None or not self._multi:
from_preset_to_cursor() 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 self.preset_values = None

View File

@ -4,7 +4,7 @@ import logging
import sys import sys
from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING 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 ..locale_helpers import set_keyboard_language
from ..output import log from ..output import log
from ..translation import Translation from ..translation import Translation
@ -12,13 +12,15 @@ from ..translation import Translation
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
def select_archinstall_language(default='English'):
def select_archinstall_language(preset_value: str) -> Optional[str]:
""" """
copied from user_interaction/general_conf.py as a temporary measure copied from user_interaction/general_conf.py as a temporary measure
""" """
languages = Translation.get_all_names() languages = Translation.get_all_names()
language = Menu(_('Select Archinstall language'), languages, default_option=default).run() language = Menu(_('Select Archinstall language'), languages, preset_values=preset_value).run()
return language return language.value
class Selector: class Selector:
def __init__( def __init__(
@ -307,22 +309,27 @@ class GeneralMenu:
skip_empty_entries=True skip_empty_entries=True
).run() ).run()
if selection and self.auto_cursor: if selection.type_ == MenuSelectionType.Selection:
cursor_pos = menu_options.index(selection) + 1 # before the strip otherwise fails value = selection.value
# in case the new position lands on a "placeholder" we'll skip them as well if self.auto_cursor:
while True: cursor_pos = menu_options.index(value) + 1 # before the strip otherwise fails
if cursor_pos >= len(menu_options):
cursor_pos = 0 # in case the new position lands on a "placeholder" we'll skip them as well
if len(menu_options[cursor_pos]) > 0: 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 break
cursor_pos += 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 not self.is_context_mgr: if not self.is_context_mgr:
self.__exit__() self.__exit__()
@ -443,15 +450,17 @@ class GeneralMenu:
def mandatory_overview(self) -> Tuple[int, int]: def mandatory_overview(self) -> Tuple[int, int]:
mandatory_fields = 0 mandatory_fields = 0
mandatory_waiting = 0 mandatory_waiting = 0
for field in self._menu_options: for field, option in self._menu_options.items():
option = self._menu_options[field]
if option.is_mandatory(): if option.is_mandatory():
mandatory_fields += 1 mandatory_fields += 1
if not option.has_selection(): if not option.has_selection():
mandatory_waiting += 1 mandatory_waiting += 1
return mandatory_fields, mandatory_waiting return mandatory_fields, mandatory_waiting
def _select_archinstall_language(self, default_lang): def _select_archinstall_language(self, preset_value: str) -> str:
language = select_archinstall_language(default_lang) language = select_archinstall_language(preset_value)
self._translation.activate(language) if language is not None:
return language self._translation.activate(language)
return language
return preset_value

View File

@ -596,7 +596,8 @@ class TerminalMenu:
status_bar: Optional[Union[str, Iterable[str], Callable[[str], str]]] = None, status_bar: Optional[Union[str, Iterable[str], Callable[[str], str]]] = None,
status_bar_below_preview: bool = DEFAULT_STATUS_BAR_BELOW_PREVIEW, status_bar_below_preview: bool = DEFAULT_STATUS_BAR_BELOW_PREVIEW,
status_bar_style: Optional[Iterable[str]] = DEFAULT_STATUS_BAR_STYLE, 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( def extract_shortcuts_menu_entries_and_preview_arguments(
entries: Iterable[str], entries: Iterable[str],
@ -718,6 +719,7 @@ class TerminalMenu:
self._search_case_sensitive = search_case_sensitive 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_highlight_style = tuple(search_highlight_style) if search_highlight_style is not None else ()
self._search_key = search_key self._search_key = search_key
self._explode_on_interrupt = explode_on_interrupt
self._shortcut_brackets_highlight_style = ( self._shortcut_brackets_highlight_style = (
tuple(shortcut_brackets_highlight_style) if shortcut_brackets_highlight_style is not None else () 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 # Only append `next_key` if it is a printable character and the first character is not the
# `search_start` key # `search_start` key
self._search.search_text += next_key self._search.search_text += next_key
except KeyboardInterrupt: except KeyboardInterrupt as e:
if self._explode_on_interrupt:
raise e
menu_was_interrupted = True menu_was_interrupted = True
finally: finally:
reset_signal_handling() reset_signal_handling()
@ -1841,6 +1845,12 @@ def get_argumentparser() -> argparse.ArgumentParser:
), ),
) )
parser.add_argument("-t", "--title", action="store", dest="title", help="menu title") 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( parser.add_argument(
"-V", "--version", action="store_true", dest="print_version", help="print the version number and exit" "-V", "--version", action="store_true", dest="print_version", help="print the version number and exit"
) )
@ -1971,6 +1981,7 @@ def main() -> None:
status_bar_below_preview=args.status_bar_below_preview, status_bar_below_preview=args.status_bar_below_preview,
status_bar_style=args.status_bar_style, status_bar_style=args.status_bar_style,
title=args.title, title=args.title,
explode_on_interrupt=args.explode_on_interrupt,
) )
except (InvalidParameterCombinationError, InvalidStyleError, UnknownMenuEntryError) as e: except (InvalidParameterCombinationError, InvalidStyleError, UnknownMenuEntryError) as e:
print(str(e), file=sys.stderr) print(str(e), file=sys.stderr)

View File

@ -207,6 +207,10 @@ class Profile(Script):
def __repr__(self, *args :str, **kwargs :str) -> str: def __repr__(self, *args :str, **kwargs :str) -> str:
return f'Profile({os.path.basename(self.profile)})' return f'Profile({os.path.basename(self.profile)})'
@property
def name(self) -> str:
return os.path.basename(self.profile)
def install(self) -> ModuleType: def install(self) -> ModuleType:
# Before installing, revert any temporary changes to the namespace. # Before installing, revert any temporary changes to the namespace.
# This ensures that the namespace during installation is the original initiation namespace. # This ensures that the namespace during installation is the original initiation namespace.

View File

@ -6,13 +6,14 @@ from .partitioning_conf import manage_new_and_existing_partitions, get_default_p
from ..disk import BlockDevice from ..disk import BlockDevice
from ..exceptions import DiskError from ..exceptions import DiskError
from ..menu import Menu from ..menu import Menu
from ..menu.menu import MenuSelectionType
from ..output import log from ..output import log
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
def ask_for_main_filesystem_format(advanced_options=False): def ask_for_main_filesystem_format(advanced_options=False) -> str:
options = {'btrfs': 'btrfs', 'ext4': 'ext4', 'xfs': 'xfs', 'f2fs': 'f2fs'} options = {'btrfs': 'btrfs', 'ext4': 'ext4', 'xfs': 'xfs', 'f2fs': 'f2fs'}
advanced = {'ntfs': 'ntfs'} advanced = {'ntfs': 'ntfs'}
@ -22,7 +23,7 @@ def ask_for_main_filesystem_format(advanced_options=False):
prompt = _('Select which filesystem your main partition should use') prompt = _('Select which filesystem your main partition should use')
choice = Menu(prompt, options, skip=False).run() choice = Menu(prompt, options, skip=False).run()
return choice return choice.value
def select_individual_blockdevice_usage(block_devices: list) -> Dict[str, Any]: def select_individual_blockdevice_usage(block_devices: list) -> Dict[str, Any]:
@ -30,24 +31,33 @@ def select_individual_blockdevice_usage(block_devices: list) -> Dict[str, Any]:
for device in block_devices: for device in block_devices:
layout = manage_new_and_existing_partitions(device) layout = manage_new_and_existing_partitions(device)
result[device.path] = layout result[device.path] = layout
return result return result
def select_disk_layout(block_devices: list, advanced_options=False) -> Optional[Dict[str, Any]]: def select_disk_layout(preset: Optional[Dict[str, Any]], block_devices: list, advanced_options=False) -> Optional[Dict[str, Any]]:
wipe_mode = str(_('Wipe all selected drives and use a best-effort default partition layout')) wipe_mode = str(_('Wipe all selected drives and use a best-effort default partition layout'))
custome_mode = str(_('Select what to do with each individual drive (followed by partition usage)')) custome_mode = str(_('Select what to do with each individual drive (followed by partition usage)'))
modes = [wipe_mode, custome_mode] modes = [wipe_mode, custome_mode]
mode = Menu(_('Select what you wish to do with the selected block devices'), modes).run() warning = str(_('Are you sure you want to reset this setting?'))
if mode: choice = Menu(
if mode == wipe_mode: _('Select what you wish to do with the selected block devices'),
return get_default_partition_layout(block_devices, advanced_options) modes,
else: explode_on_interrupt=True,
return select_individual_blockdevice_usage(block_devices) explode_warning=warning
).run()
match choice.type_:
case MenuSelectionType.Esc: return preset
case MenuSelectionType.Ctrl_c: return None
case MenuSelectionType.Selection:
if choice.value == wipe_mode:
return get_default_partition_layout(block_devices, advanced_options)
else:
return select_individual_blockdevice_usage(block_devices)
def select_disk(dict_o_disks: Dict[str, BlockDevice]) -> BlockDevice: def select_disk(dict_o_disks: Dict[str, BlockDevice]) -> BlockDevice:

View File

@ -3,6 +3,9 @@ from __future__ import annotations
import logging import logging
from typing import List, Any, Optional, Dict, TYPE_CHECKING from typing import List, Any, Optional, Dict, TYPE_CHECKING
import archinstall
from ..menu.menu import MenuSelectionType
from ..menu.text_input import TextInput from ..menu.text_input import TextInput
from ..locale_helpers import list_keyboard_languages, list_timezones from ..locale_helpers import list_keyboard_languages, list_timezones
@ -26,7 +29,8 @@ def ask_ntp(preset: bool = True) -> bool:
else: else:
preset_val = Menu.no() preset_val = Menu.no()
choice = Menu(prompt, Menu.yes_no(), skip=False, preset_values=preset_val, default_option=Menu.yes()).run() choice = Menu(prompt, Menu.yes_no(), skip=False, preset_values=preset_val, default_option=Menu.yes()).run()
return False if choice == Menu.no() else True
return False if choice.value == Menu.no() else True
def ask_hostname(preset: str = None) -> str: def ask_hostname(preset: str = None) -> str:
@ -38,23 +42,31 @@ def ask_for_a_timezone(preset: str = None) -> str:
timezones = list_timezones() timezones = list_timezones()
default = 'UTC' default = 'UTC'
selected_tz = Menu(_('Select a timezone'), choice = Menu(
list(timezones), _('Select a timezone'),
skip=False, list(timezones),
preset_values=preset, preset_values=preset,
default_option=default).run() default_option=default
).run()
return selected_tz match choice.type_:
case MenuSelectionType.Esc: return preset
case MenuSelectionType.Selection: return choice.value
def ask_for_audio_selection(desktop: bool = True, preset: str = None) -> str: def ask_for_audio_selection(desktop: bool = True, preset: str = None) -> str:
audio = 'pipewire' if desktop else 'none' no_audio = str(_('No audio server'))
choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', 'none'] choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio]
selected_audio = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=audio, skip=False).run() default = 'pipewire' if desktop else no_audio
return selected_audio
choice = Menu(_('Choose an audio server'), choices, preset_values=preset, default_option=default).run()
match choice.type_:
case MenuSelectionType.Esc: return preset
case MenuSelectionType.Selection: return choice.value
def select_language(default_value: str, preset_value: str = None) -> str: def select_language(preset_value: str = None) -> str:
""" """
Asks the user to select a language Asks the user to select a language
Usually this is combined with :ref:`archinstall.list_keyboard_languages`. Usually this is combined with :ref:`archinstall.list_keyboard_languages`.
@ -64,16 +76,19 @@ def select_language(default_value: str, preset_value: str = None) -> str:
""" """
kb_lang = list_keyboard_languages() kb_lang = list_keyboard_languages()
# sort alphabetically and then by length # sort alphabetically and then by length
# it's fine if the list is big because the Menu
# allows for searching anyways
sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len)
selected_lang = Menu(_('Select Keyboard layout'), selected_lang = Menu(
sorted_kb_lang, _('Select Keyboard layout'),
default_option=default_value, sorted_kb_lang,
preset_values=preset_value, preset_values=preset_value,
sort=False).run() sort=False
return selected_lang ).run()
if selected_lang.value is None:
return preset_value
return selected_lang.value
def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]: def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]:
@ -89,15 +104,18 @@ def select_mirror_regions(preset_values: Dict[str, Any] = {}) -> Dict[str, Any]:
else: else:
preselected = list(preset_values.keys()) preselected = list(preset_values.keys())
mirrors = list_mirrors() mirrors = list_mirrors()
selected_mirror = Menu(_('Select one of the regions to download packages from'), selected_mirror = Menu(
list(mirrors.keys()), _('Select one of the regions to download packages from'),
preset_values=preselected, list(mirrors.keys()),
multi=True).run() preset_values=preselected,
multi=True,
explode_on_interrupt=True
).run()
if selected_mirror is not None: match selected_mirror.type_:
return {selected: mirrors[selected] for selected in selected_mirror} case MenuSelectionType.Ctrl_c: return {}
case MenuSelectionType.Esc: return preset_values
return {} case _: return {selected: mirrors[selected] for selected in selected_mirror.value}
def select_archinstall_language(default='English'): def select_archinstall_language(default='English'):
@ -106,7 +124,7 @@ def select_archinstall_language(default='English'):
return language return language
def select_profile() -> Optional[Profile]: def select_profile(preset) -> Optional[Profile]:
""" """
# Asks the user to select a profile from the available profiles. # Asks the user to select a profile from the available profiles.
# #
@ -125,12 +143,27 @@ def select_profile() -> Optional[Profile]:
title = _('This is a list of pre-programmed profiles, they might make it easier to install things like desktop environments') title = _('This is a list of pre-programmed profiles, they might make it easier to install things like desktop environments')
selection = Menu(title=title, p_options=list(options.keys())).run() warning = str(_('Are you sure you want to reset this setting?'))
if selection is not None: selection = Menu(
return options[selection] title=title,
p_options=list(options.keys()),
explode_on_interrupt=True,
explode_warning=warning
).run()
return None match selection.type_:
case MenuSelectionType.Selection:
return options[selection.value] if selection.value is not None else None
case MenuSelectionType.Ctrl_c:
archinstall.storage['profile_minimal'] = False
archinstall.storage['_selected_servers'] = []
archinstall.storage['_desktop_profile'] = None
archinstall.arguments['desktop-environment'] = None
archinstall.arguments['gfx_driver_packages'] = None
return None
case MenuSelectionType.Esc:
return None
def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]:
@ -171,14 +204,16 @@ def select_additional_repositories(preset: List[str]) -> List[str]:
repositories = ["multilib", "testing"] repositories = ["multilib", "testing"]
additional_repositories = Menu(_('Choose which optional additional repositories to enable'), choice = Menu(
repositories, _('Choose which optional additional repositories to enable'),
sort=False, repositories,
multi=True, sort=False,
preset_values=preset, multi=True,
default_option=[]).run() preset_values=preset,
explode_on_interrupt=True
).run()
if additional_repositories is not None: match choice.type_:
return additional_repositories case MenuSelectionType.Esc: return preset
case MenuSelectionType.Ctrl_c: return []
return [] case MenuSelectionType.Selection: return choice.value

View File

@ -4,32 +4,39 @@ from typing import Any, TYPE_CHECKING
from ..locale_helpers import list_locales from ..locale_helpers import list_locales
from ..menu import Menu from ..menu import Menu
from ..menu.menu import MenuSelectionType
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
def select_locale_lang(default: str, preset: str = None) -> str: def select_locale_lang(preset: str = None) -> str:
locales = list_locales() locales = list_locales()
locale_lang = set([locale.split()[0] for locale in locales]) locale_lang = set([locale.split()[0] for locale in locales])
selected_locale = Menu(_('Choose which locale language to use'), selected_locale = Menu(
locale_lang, _('Choose which locale language to use'),
sort=True, list(locale_lang),
preset_values=preset, sort=True,
default_option=default).run() preset_values=preset
).run()
return selected_locale match selected_locale.type_:
case MenuSelectionType.Selection: return selected_locale.value
case MenuSelectionType.Esc: return preset
def select_locale_enc(default: str, preset: str = None) -> str: def select_locale_enc(preset: str = None) -> str:
locales = list_locales() locales = list_locales()
locale_enc = set([locale.split()[1] for locale in locales]) locale_enc = set([locale.split()[1] for locale in locales])
selected_locale = Menu(_('Choose which locale encoding to use'), selected_locale = Menu(
locale_enc, _('Choose which locale encoding to use'),
sort=True, list(locale_enc),
preset_values=preset, sort=True,
default_option=default).run() preset_values=preset
).run()
return selected_locale match selected_locale.type_:
case MenuSelectionType.Selection: return selected_locale.value
case MenuSelectionType.Esc: return preset

View File

@ -4,6 +4,7 @@ import ipaddress
import logging import logging
from typing import Any, Optional, TYPE_CHECKING, List, Union from typing import Any, Optional, TYPE_CHECKING, List, Union
from ..menu.menu import MenuSelectionType
from ..menu.text_input import TextInput from ..menu.text_input import TextInput
from ..models.network_configuration import NetworkConfiguration, NicType from ..models.network_configuration import NetworkConfiguration, NicType
@ -66,8 +67,12 @@ class ManualNetworkConfig(ListManager):
def _select_iface(self, existing_ifaces: List[str]) -> Optional[str]: def _select_iface(self, existing_ifaces: List[str]) -> Optional[str]:
all_ifaces = list_interfaces().values() all_ifaces = list_interfaces().values()
available = set(all_ifaces) - set(existing_ifaces) available = set(all_ifaces) - set(existing_ifaces)
iface = Menu(str(_('Select interface to add')), list(available), skip=True).run() choice = Menu(str(_('Select interface to add')), list(available), skip=True).run()
return iface
if choice.type_ == MenuSelectionType.Esc:
return None
return choice.value
def _edit_iface(self, edit_iface :NetworkConfiguration): def _edit_iface(self, edit_iface :NetworkConfiguration):
iface_name = edit_iface.iface iface_name = edit_iface.iface
@ -75,9 +80,9 @@ class ManualNetworkConfig(ListManager):
default_mode = 'DHCP (auto detect)' default_mode = 'DHCP (auto detect)'
prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode) prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(iface_name, default_mode)
mode = Menu(prompt, modes, default_option=default_mode).run() mode = Menu(prompt, modes, default_option=default_mode, skip=False).run()
if mode == 'IP (static)': if mode.value == 'IP (static)':
while 1: while 1:
prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name)
ip = TextInput(prompt, edit_iface.ip).run().strip() ip = TextInput(prompt, edit_iface.ip).run().strip()
@ -107,6 +112,7 @@ class ManualNetworkConfig(ListManager):
display_dns = None display_dns = None
dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip() dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '), display_dns).run().strip()
dns = []
if len(dns_input): if len(dns_input):
dns = dns_input.split(' ') dns = dns_input.split(' ')
@ -135,23 +141,28 @@ def ask_to_configure_network(preset: Union[None, NetworkConfiguration, List[Netw
elif preset.type == 'network_manager': elif preset.type == 'network_manager':
cursor_idx = 1 cursor_idx = 1
nic = Menu(_( warning = str(_('Are you sure you want to reset this setting?'))
'Select one network interface to configure'),
choice = Menu(
_('Select one network interface to configure'),
list(network_options.values()), list(network_options.values()),
cursor_index=cursor_idx, cursor_index=cursor_idx,
sort=False sort=False,
explode_on_interrupt=True,
explode_warning=warning
).run() ).run()
if not nic: match choice.type_:
return preset case MenuSelectionType.Esc: return preset
case MenuSelectionType.Ctrl_c: return None
if nic == network_options['none']: if choice.value == network_options['none']:
return None return None
elif nic == network_options['iso_config']: elif choice.value == network_options['iso_config']:
return NetworkConfiguration(NicType.ISO) return NetworkConfiguration(NicType.ISO)
elif nic == network_options['network_manager']: elif choice.value == network_options['network_manager']:
return NetworkConfiguration(NicType.NM) return NetworkConfiguration(NicType.NM)
elif nic == network_options['manual']: elif choice.value == network_options['manual']:
manual = ManualNetworkConfig('Configure interfaces', preset) manual = ManualNetworkConfig('Configure interfaces', preset)
return manual.run_manual() return manual.run_manual()

View File

@ -1,8 +1,10 @@
from __future__ import annotations from __future__ import annotations
import copy
from typing import List, Any, Dict, Union, TYPE_CHECKING, Callable, Optional from typing import List, Any, Dict, Union, TYPE_CHECKING, Callable, Optional
from ..menu import Menu from ..menu import Menu
from ..menu.menu import MenuSelectionType
from ..output import log from ..output import log
from ..disk.validators import fs_types from ..disk.validators import fs_types
@ -80,21 +82,26 @@ def _get_partitions(partitions :List[Partition], filter_ :Callable = None) -> Li
return partition_indexes return partition_indexes
def select_partition(title :str, partitions :List[Partition], multiple :bool = False, filter_ :Callable = None) -> Union[int, List[int], None]: def select_partition(
title :str,
partitions :List[Partition],
multiple :bool = False,
filter_ :Callable = None
) -> Optional[int, List[int]]:
partition_indexes = _get_partitions(partitions, filter_) partition_indexes = _get_partitions(partitions, filter_)
if len(partition_indexes) == 0: if len(partition_indexes) == 0:
return None return None
partition = Menu(title, partition_indexes, multi=multiple).run() choice = Menu(title, partition_indexes, multi=multiple).run()
if partition is not None: if choice.type_ == MenuSelectionType.Esc:
if isinstance(partition, list): return None
return [int(p) for p in partition]
else:
return int(partition)
return None if isinstance(choice.value, list):
return [int(p) for p in choice.value]
else:
return int(choice.value)
def get_default_partition_layout( def get_default_partition_layout(
@ -114,14 +121,15 @@ def select_individual_blockdevice_usage(block_devices: list) -> Dict[str, Any]:
for device in block_devices: for device in block_devices:
layout = manage_new_and_existing_partitions(device) layout = manage_new_and_existing_partitions(device)
result[device.path] = layout result[device.path] = layout
return result return result
def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, Any]: # noqa: max-complexity: 50 def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str, Any]: # noqa: max-complexity: 50
block_device_struct = {"partitions": [partition.__dump__() for partition in block_device.partitions.values()]} block_device_struct = {"partitions": [partition.__dump__() for partition in block_device.partitions.values()]}
original_layout = copy.deepcopy(block_device_struct)
# Test code: [part.__dump__() for part in block_device.partitions.values()] # Test code: [part.__dump__() for part in block_device.partitions.values()]
# TODO: Squeeze in BTRFS subvolumes here # TODO: Squeeze in BTRFS subvolumes here
@ -136,6 +144,8 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
mark_bootable = str(_('Mark/Unmark a partition as bootable (automatic for /boot)')) mark_bootable = str(_('Mark/Unmark a partition as bootable (automatic for /boot)'))
set_filesystem_partition = str(_('Set desired filesystem for a partition')) set_filesystem_partition = str(_('Set desired filesystem for a partition'))
set_btrfs_subvolumes = str(_('Set desired subvolumes on a btrfs partition')) set_btrfs_subvolumes = str(_('Set desired subvolumes on a btrfs partition'))
save_and_exit = str(_('Save and exit'))
cancel = str(_('Cancel'))
while True: while True:
modes = [new_partition, suggest_partition_layout] modes = [new_partition, suggest_partition_layout]
@ -166,11 +176,15 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
if len(block_device_struct["partitions"]): if len(block_device_struct["partitions"]):
title += _current_partition_layout(block_device_struct['partitions']) + '\n' title += _current_partition_layout(block_device_struct['partitions']) + '\n'
task = Menu(title, modes, sort=False).run() modes += [save_and_exit, cancel]
if not task: task = Menu(title, modes, sort=False, skip=False).run()
task = task.value
if task == cancel:
return original_layout
elif task == save_and_exit:
break break
if task == new_partition: if task == new_partition:
from ..disk import valid_parted_position from ..disk import valid_parted_position
@ -179,9 +193,9 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
# # https://www.gnu.org/software/parted/manual/html_node/mklabel.html # # https://www.gnu.org/software/parted/manual/html_node/mklabel.html
# name = input("Enter a desired name for the partition: ").strip() # name = input("Enter a desired name for the partition: ").strip()
fstype = Menu(_('Enter a desired filesystem type for the partition'), fs_types()).run() fs_choice = Menu(_('Enter a desired filesystem type for the partition'), fs_types()).run()
if not fstype: if fs_choice.type_ == MenuSelectionType.Esc:
continue continue
prompt = _('Enter the start sector (percentage or block number, default: {}): ').format( prompt = _('Enter the start sector (percentage or block number, default: {}): ').format(
@ -214,7 +228,7 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
"mountpoint": None, "mountpoint": None,
"wipe": True, "wipe": True,
"filesystem": { "filesystem": {
"format": fstype "format": fs_choice.value
} }
}) })
else: else:
@ -225,16 +239,13 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
from ..disk import suggest_single_disk_layout from ..disk import suggest_single_disk_layout
if len(block_device_struct["partitions"]): if len(block_device_struct["partitions"]):
prompt = _('{} contains queued partitions, this will remove those, are you sure?').format(block_device) prompt = _('{}\ncontains queued partitions, this will remove those, are you sure?').format(block_device)
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no()).run() choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run()
if choice == Menu.no(): if choice.value == Menu.no():
continue continue
block_device_struct.update(suggest_single_disk_layout(block_device)[block_device.path]) block_device_struct.update(suggest_single_disk_layout(block_device)[block_device.path])
elif task is None:
return block_device_struct
else: else:
current_layout = _current_partition_layout(block_device_struct['partitions'], with_idx=True) current_layout = _current_partition_layout(block_device_struct['partitions'], with_idx=True)
@ -265,10 +276,8 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
partition = select_partition(title, block_device_struct["partitions"]) partition = select_partition(title, block_device_struct["partitions"])
if partition is not None: if partition is not None:
print( print(_(' * Partition mount-points are relative to inside the installation, the boot would be /boot as an example.'))
_(' * Partition mount-points are relative to inside the installation, the boot would be /boot as an example.')) mountpoint = input(_('Select where to mount partition (leave blank to remove mountpoint): ')).strip()
mountpoint = input(
_('Select where to mount partition (leave blank to remove mountpoint): ')).strip()
if len(mountpoint): if len(mountpoint):
block_device_struct["partitions"][partition]['mountpoint'] = mountpoint block_device_struct["partitions"][partition]['mountpoint'] = mountpoint
@ -290,10 +299,10 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
if not block_device_struct["partitions"][partition].get('filesystem', None): if not block_device_struct["partitions"][partition].get('filesystem', None):
block_device_struct["partitions"][partition]['filesystem'] = {} block_device_struct["partitions"][partition]['filesystem'] = {}
fstype = Menu(_('Enter a desired filesystem type for the partition'), fs_types()).run() fs_choice = Menu(_('Enter a desired filesystem type for the partition'), fs_types()).run()
if fstype: if fs_choice.type_ == MenuSelectionType.Selection:
block_device_struct["partitions"][partition]['filesystem']['format'] = fstype block_device_struct["partitions"][partition]['filesystem']['format'] = fs_choice.value
# Negate the current wipe marking # Negate the current wipe marking
block_device_struct["partitions"][partition]['wipe'] = not block_device_struct["partitions"][partition].get('wipe', False) block_device_struct["partitions"][partition]['wipe'] = not block_device_struct["partitions"][partition].get('wipe', False)
@ -304,16 +313,16 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
if partition is not None: if partition is not None:
# Negate the current encryption marking # Negate the current encryption marking
block_device_struct["partitions"][partition][ block_device_struct["partitions"][partition]['encrypted'] = \
'encrypted'] = not block_device_struct["partitions"][partition].get('encrypted', False) not block_device_struct["partitions"][partition].get('encrypted', False)
elif task == mark_bootable: elif task == mark_bootable:
title = _('{}\n\nSelect which partition to mark as bootable').format(current_layout) title = _('{}\n\nSelect which partition to mark as bootable').format(current_layout)
partition = select_partition(title, block_device_struct["partitions"]) partition = select_partition(title, block_device_struct["partitions"])
if partition is not None: if partition is not None:
block_device_struct["partitions"][partition][ block_device_struct["partitions"][partition]['boot'] = \
'boot'] = not block_device_struct["partitions"][partition].get('boot', False) not block_device_struct["partitions"][partition].get('boot', False)
elif task == set_filesystem_partition: elif task == set_filesystem_partition:
title = _('{}\n\nSelect which partition to set a filesystem on').format(current_layout) title = _('{}\n\nSelect which partition to set a filesystem on').format(current_layout)
@ -324,10 +333,10 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
block_device_struct["partitions"][partition]['filesystem'] = {} block_device_struct["partitions"][partition]['filesystem'] = {}
fstype_title = _('Enter a desired filesystem type for the partition: ') fstype_title = _('Enter a desired filesystem type for the partition: ')
fstype = Menu(fstype_title, fs_types()).run() fs_choice = Menu(fstype_title, fs_types()).run()
if fstype: if fs_choice.type_ == MenuSelectionType.Selection:
block_device_struct["partitions"][partition]['filesystem']['format'] = fstype block_device_struct["partitions"][partition]['filesystem']['format'] = fs_choice.value
elif task == set_btrfs_subvolumes: elif task == set_btrfs_subvolumes:
from .subvolume_config import SubvolumeList from .subvolume_config import SubvolumeList

View File

@ -5,6 +5,7 @@ from typing import Any, Dict, TYPE_CHECKING
from ..configuration import ConfigurationOutput from ..configuration import ConfigurationOutput
from ..menu import Menu from ..menu import Menu
from ..menu.menu import MenuSelectionType
from ..output import log from ..output import log
if TYPE_CHECKING: if TYPE_CHECKING:
@ -45,14 +46,16 @@ def save_config(config: Dict):
'all': str(_('Save all')) 'all': str(_('Save all'))
} }
selection = Menu(_('Choose which configuration to save'), choice = Menu(
list(options.values()), _('Choose which configuration to save'),
sort=False, list(options.values()),
skip=True, sort=False,
preview_size=0.75, skip=True,
preview_command=preview).run() preview_size=0.75,
preview_command=preview
).run()
if not selection: if choice.type_ == MenuSelectionType.Esc:
return return
while True: while True:
@ -62,13 +65,13 @@ def save_config(config: Dict):
break break
log(_('Not a valid directory: {}').format(dest_path), fg='red') log(_('Not a valid directory: {}').format(dest_path), fg='red')
if options['user_config'] == selection: if options['user_config'] == choice.value:
config_output.save_user_config(dest_path) config_output.save_user_config(dest_path)
elif options['user_creds'] == selection: elif options['user_creds'] == choice.value:
config_output.save_user_creds(dest_path) config_output.save_user_creds(dest_path)
elif options['disk_layout'] == selection: elif options['disk_layout'] == choice.value:
config_output.save_disk_layout(dest_path) config_output.save_disk_layout(dest_path)
elif options['all'] == selection: elif options['all'] == choice.value:
config_output.save_user_config(dest_path) config_output.save_user_config(dest_path)
config_output.save_user_creds(dest_path) config_output.save_user_creds(dest_path)
config_output.save_disk_layout config_output.save_disk_layout(dest_path)

View File

@ -6,10 +6,9 @@ from ..disk import all_blockdevices
from ..exceptions import RequirementError from ..exceptions import RequirementError
from ..hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics from ..hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics
from ..menu import Menu from ..menu import Menu
from ..menu.menu import MenuSelectionType
from ..storage import storage from ..storage import storage
from ..translation import DeferredTranslation
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -25,13 +24,22 @@ def select_kernel(preset: List[str] = None) -> List[str]:
kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"]
default_kernel = "linux" default_kernel = "linux"
selected_kernels = Menu(_('Choose which kernels to use or leave blank for default "{}"').format(default_kernel), warning = str(_('Are you sure you want to reset this setting?'))
kernels,
sort=True, choice = Menu(
multi=True, _('Choose which kernels to use or leave blank for default "{}"').format(default_kernel),
preset_values=preset, kernels,
default_option=default_kernel).run() sort=True,
return selected_kernels multi=True,
preset_values=preset,
explode_on_interrupt=True,
explode_warning=warning
).run()
match choice.type_:
case MenuSelectionType.Esc: return preset
case MenuSelectionType.Ctrl_c: return []
case MenuSelectionType.Selection: return choice.value
def select_harddrives(preset: List[str] = []) -> List[str]: def select_harddrives(preset: List[str] = []) -> List[str]:
@ -49,15 +57,24 @@ def select_harddrives(preset: List[str] = []) -> List[str]:
else: else:
preset_disks = {} preset_disks = {}
selected_harddrive = Menu(_('Select one or more hard drives to use and configure'), title = str(_('Select one or more hard drives to use and configure\n'))
list(options.keys()), title += str(_('Any modifications to the existing setting will reset the disk layout!'))
preset_values=list(preset_disks.keys()),
multi=True).run()
if selected_harddrive and len(selected_harddrive) > 0: warning = str(_('If you reset the harddrive selection this will also reset the current disk layout. Are you sure?'))
return [options[i] for i in selected_harddrive]
return [] selected_harddrive = Menu(
title,
list(options.keys()),
preset_values=list(preset_disks.keys()),
multi=True,
explode_on_interrupt=True,
explode_warning=warning
).run()
match selected_harddrive.type_:
case MenuSelectionType.Ctrl_c: return []
case MenuSelectionType.Esc: return preset
case MenuSelectionType.Selection: return [options[i] for i in selected_harddrive.value]
def select_driver(options: Dict[str, Any] = AVAILABLE_GFX_DRIVERS) -> str: def select_driver(options: Dict[str, Any] = AVAILABLE_GFX_DRIVERS) -> str:
@ -73,34 +90,34 @@ def select_driver(options: Dict[str, Any] = AVAILABLE_GFX_DRIVERS) -> str:
if drivers: if drivers:
arguments = storage.get('arguments', {}) arguments = storage.get('arguments', {})
title = DeferredTranslation('') title = ''
if has_amd_graphics(): if has_amd_graphics():
title += _( title += str(_(
'For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.' 'For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.'
) + '\n' )) + '\n'
if has_intel_graphics(): if has_intel_graphics():
title += _( title += str(_(
'For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n' 'For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n'
) ))
if has_nvidia_graphics(): if has_nvidia_graphics():
title += _( title += str(_(
'For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n' 'For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n'
) ))
title += _('\n\nSelect a graphics driver or leave blank to install all open-source drivers') title += str(_('\n\nSelect a graphics driver or leave blank to install all open-source drivers'))
arguments['gfx_driver'] = Menu(title, drivers).run() choice = Menu(title, drivers).run()
if arguments.get('gfx_driver', None) is None: if choice.type_ != MenuSelectionType.Selection:
arguments['gfx_driver'] = _("All open-source (default)") return arguments.get('gfx_driver')
return options.get(arguments.get('gfx_driver')) arguments['gfx_driver'] = choice.value
return options.get(choice.value)
raise RequirementError("Selecting drivers require a least one profile to be given as an option.") raise RequirementError("Selecting drivers require a least one profile to be given as an option.")
def ask_for_bootloader(advanced_options: bool = False, preset: str = None) -> str: def ask_for_bootloader(advanced_options: bool = False, preset: str = None) -> str:
if preset == 'systemd-bootctl': if preset == 'systemd-bootctl':
preset_val = 'systemd-boot' if advanced_options else Menu.no() preset_val = 'systemd-boot' if advanced_options else Menu.no()
elif preset == 'grub-install': elif preset == 'grub-install':
@ -109,26 +126,36 @@ def ask_for_bootloader(advanced_options: bool = False, preset: str = None) -> st
preset_val = preset preset_val = preset
bootloader = "systemd-bootctl" if has_uefi() else "grub-install" bootloader = "systemd-bootctl" if has_uefi() else "grub-install"
if has_uefi(): if has_uefi():
if not advanced_options: if not advanced_options:
bootloader_choice = Menu(_('Would you like to use GRUB as a bootloader instead of systemd-boot?'), selection = Menu(
Menu.yes_no(), _('Would you like to use GRUB as a bootloader instead of systemd-boot?'),
preset_values=preset_val, Menu.yes_no(),
default_option=Menu.no()).run() preset_values=preset_val,
default_option=Menu.no()
).run()
if bootloader_choice == Menu.yes(): match selection.type_:
bootloader = "grub-install" case MenuSelectionType.Esc: return preset
case MenuSelectionType.Selection: bootloader = 'grub-install' if selection.value == Menu.yes() else bootloader
else: else:
# We use the common names for the bootloader as the selection, and map it back to the expected values. # We use the common names for the bootloader as the selection, and map it back to the expected values.
choices = ['systemd-boot', 'grub', 'efistub'] choices = ['systemd-boot', 'grub', 'efistub']
selection = Menu(_('Choose a bootloader'), choices, preset_values=preset_val).run() selection = Menu(_('Choose a bootloader'), choices, preset_values=preset_val).run()
if selection != "":
if selection == 'systemd-boot': value = ''
match selection.type_:
case MenuSelectionType.Esc: value = preset_val
case MenuSelectionType.Selection: value = selection.value
if value != "":
if value == 'systemd-boot':
bootloader = 'systemd-bootctl' bootloader = 'systemd-bootctl'
elif selection == 'grub': elif value == 'grub':
bootloader = 'grub-install' bootloader = 'grub-install'
else: else:
bootloader = selection bootloader = value
return bootloader return bootloader
@ -138,6 +165,10 @@ def ask_for_swap(preset: bool = True) -> bool:
preset_val = Menu.yes() preset_val = Menu.yes()
else: else:
preset_val = Menu.no() preset_val = Menu.no()
prompt = _('Would you like to use swap on zram?') prompt = _('Would you like to use swap on zram?')
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run() choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), preset_values=preset_val).run()
return False if choice == Menu.no() else True
match choice.type_:
case MenuSelectionType.Esc: return preset
case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True

View File

@ -30,7 +30,7 @@ def check_password_strong(passwd: str) -> bool:
if symbol_count**len(passwd) < 10e20: if symbol_count**len(passwd) < 10e20:
prompt = str(_("The password you are using seems to be weak, are you sure you want to use it?")) prompt = str(_("The password you are using seems to be weak, are you sure you want to use it?"))
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run()
return choice == Menu.yes() return choice.value == Menu.yes()
return True return True
@ -40,7 +40,6 @@ def get_password(prompt: str = '') -> Optional[str]:
prompt = _("Enter a password: ") prompt = _("Enter a password: ")
while passwd := getpass.getpass(prompt): while passwd := getpass.getpass(prompt):
if len(passwd.strip()) <= 0: if len(passwd.strip()) <= 0:
break break
@ -82,11 +81,12 @@ def do_countdown() -> bool:
if SIG_TRIGGER: if SIG_TRIGGER:
prompt = _('Do you really want to abort?') prompt = _('Do you really want to abort?')
choice = Menu(prompt, Menu.yes_no(), skip=False).run() choice = Menu(prompt, Menu.yes_no(), skip=False).run()
if choice == 'yes': if choice.value == Menu.yes():
exit(0) exit(0)
if SIG_TRIGGER is False: if SIG_TRIGGER is False:
sys.stdin.read() sys.stdin.read()
SIG_TRIGGER = False SIG_TRIGGER = False
signal.signal(signal.SIGINT, sig_handler) signal.signal(signal.SIGINT, sig_handler)

View File

@ -45,9 +45,8 @@ def ask_user_questions():
# Set which region to download packages from during the installation # Set which region to download packages from during the installation
global_menu.enable('mirror-region') global_menu.enable('mirror-region')
if archinstall.arguments.get('advanced', False): global_menu.enable('sys-language')
global_menu.enable('sys-language', True) global_menu.enable('sys-encoding')
global_menu.enable('sys-encoding', True)
# Ask which harddrives/block-devices we will install to # Ask which harddrives/block-devices we will install to
# and convert them into archinstall.BlockDevice() objects. # and convert them into archinstall.BlockDevice() objects.

View File

@ -161,7 +161,7 @@ class SetupMenu(archinstall.GeneralMenu):
self.set_option('archinstall-language', self.set_option('archinstall-language',
archinstall.Selector( archinstall.Selector(
_('Select Archinstall language'), _('Select Archinstall language'),
lambda x: self._select_archinstall_language('English'), lambda x: self._select_archinstall_language(x),
default='English', default='English',
enabled=True)) enabled=True))
self.set_option('ntp', self.set_option('ntp',

View File

@ -1,6 +1,12 @@
# A desktop environment selector. # A desktop environment selector.
from typing import Any, TYPE_CHECKING
import archinstall import archinstall
from archinstall import log from archinstall import log, Menu
from archinstall.lib.menu.menu import MenuSelectionType
if TYPE_CHECKING:
_: Any
is_top_level_profile = True is_top_level_profile = True
@ -46,23 +52,26 @@ def _prep_function(*args, **kwargs) -> bool:
other code in this stage. So it's a safe way to ask the user other code in this stage. So it's a safe way to ask the user
for more input before any other installer steps start. for more input before any other installer steps start.
""" """
desktop = archinstall.Menu(str(_('Select your desired desktop environment')), __supported__).run() choice = Menu(str(_('Select your desired desktop environment')), __supported__).run()
if desktop: if choice.type_ != MenuSelectionType.Selection:
return False
if choice.value:
# Temporarily store the selected desktop profile # Temporarily store the selected desktop profile
# in a session-safe location, since this module will get reloaded # in a session-safe location, since this module will get reloaded
# the next time it gets executed. # the next time it gets executed.
if not archinstall.storage.get('_desktop_profile', None): if not archinstall.storage.get('_desktop_profile', None):
archinstall.storage['_desktop_profile'] = desktop archinstall.storage['_desktop_profile'] = choice.value
if not archinstall.arguments.get('desktop-environment', None): if not archinstall.arguments.get('desktop-environment', None):
archinstall.arguments['desktop-environment'] = desktop archinstall.arguments['desktop-environment'] = choice.value
profile = archinstall.Profile(None, desktop) profile = archinstall.Profile(None, choice.value)
# Loading the instructions with a custom namespace, ensures that a __name__ comparison is never triggered. # Loading the instructions with a custom namespace, ensures that a __name__ comparison is never triggered.
with profile.load_instructions(namespace=f"{desktop}.py") as imported: with profile.load_instructions(namespace=f"{choice.value}.py") as imported:
if hasattr(imported, '_prep_function'): if hasattr(imported, '_prep_function'):
return imported._prep_function() return imported._prep_function()
else: else:
log(f"Deprecated (??): {desktop} profile has no _prep_function() anymore") log(f"Deprecated (??): {choice.value} profile has no _prep_function() anymore")
exit(1) exit(1)
return False return False

View File

@ -1,6 +1,8 @@
# Common package for i3, lets user select which i3 configuration they want. # Common package for i3, lets user select which i3 configuration they want.
import archinstall import archinstall
from archinstall import Menu
from archinstall.lib.menu.menu import MenuSelectionType
is_top_level_profile = False is_top_level_profile = False
@ -27,13 +29,16 @@ def _prep_function(*args, **kwargs):
supported_configurations = ['i3-wm', 'i3-gaps'] supported_configurations = ['i3-wm', 'i3-gaps']
desktop = archinstall.Menu('Select your desired configuration', supported_configurations).run() choice = Menu('Select your desired configuration', supported_configurations).run()
if desktop: if choice.type_ != MenuSelectionType.Selection:
return False
if choice.value:
# Temporarily store the selected desktop profile # Temporarily store the selected desktop profile
# in a session-safe location, since this module will get reloaded # in a session-safe location, since this module will get reloaded
# the next time it gets executed. # the next time it gets executed.
archinstall.storage['_i3_configuration'] = desktop archinstall.storage['_i3_configuration'] = choice.value
# i3 requires a functioning Xorg installation. # i3 requires a functioning Xorg installation.
profile = archinstall.Profile(None, 'xorg') profile = archinstall.Profile(None, 'xorg')
@ -43,6 +48,8 @@ def _prep_function(*args, **kwargs):
else: else:
print('Deprecated (??): xorg profile has no _prep_function() anymore') print('Deprecated (??): xorg profile has no _prep_function() anymore')
return False
if __name__ == 'i3': if __name__ == 'i3':
""" """

View File

@ -1,4 +1,5 @@
# Used to do a minimal install # Used to do a minimal install
import archinstall
is_top_level_profile = True is_top_level_profile = True
@ -12,6 +13,7 @@ def _prep_function(*args, **kwargs):
we don't need to do anything special here, but it we don't need to do anything special here, but it
needs to exist and return True. needs to exist and return True.
""" """
archinstall.storage['profile_minimal'] = True
return True # Do nothing and just return True return True # Do nothing and just return True

View File

@ -1,8 +1,14 @@
# Used to select various server application profiles on top of a minimal installation. # Used to select various server application profiles on top of a minimal installation.
import logging import logging
from typing import Any, TYPE_CHECKING
import archinstall import archinstall
from archinstall import Menu
from archinstall.lib.menu.menu import MenuSelectionType
if TYPE_CHECKING:
_: Any
is_top_level_profile = True is_top_level_profile = True
@ -26,15 +32,18 @@ def _prep_function(*args, **kwargs):
Magic function called by the importing installer Magic function called by the importing installer
before continuing any further. before continuing any further.
""" """
servers = archinstall.Menu(str(_( choice = Menu(str(_(
'Choose which servers to install, if none then a minimal installation wil be done')), 'Choose which servers to install, if none then a minimal installation wil be done')),
available_servers, available_servers,
preset_values=archinstall.storage.get('_selected_servers', []), preset_values=kwargs['servers'],
multi=True multi=True
).run() ).run()
if servers: if choice.type_ != MenuSelectionType.Selection:
archinstall.storage['_selected_servers'] = servers return False
if choice.value:
archinstall.storage['_selected_servers'] = choice.value
return True return True
return False return False

View File

@ -23,8 +23,9 @@ def _check_driver() -> bool:
if packages and "nvidia" in packages: if packages and "nvidia" in packages:
prompt = 'The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?' prompt = 'The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?'
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no()).run() choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run()
if choice == Menu.no():
if choice.value == Menu.no():
return False return False
return True return True