From f06aabb4d4a50d3a8ab7c07bef8390f72b091293 Mon Sep 17 00:00:00 2001 From: Werner Llácer Date: Mon, 28 Feb 2022 15:02:39 +0100 Subject: enhacements to the menu infraestructure (#978) * Correct definition of btrfs standard layout * Solve issue #936 * make ask_for_a_timezone as synonym to ask_timezone * Some refining in GeneralMenu secret is now a general function * Revert "Some refining in GeneralMenu" This reverts commit e6e131cb19795e0ddc169e897ae4df57a1c7f9fb. * Activate load of preset values in GeneralMenu Changed all select_functions definitions to the need of passing the preset value Corrected problems at ask_to_configure_network, and management of preset values added * minor glitches in menu processing, plus flake8 complains * Changes to ask_to_configure_network following @svartkanin code * select_language adapted to preset value. changes to the infraestructure to solve bugs * functions adapted for preset values * select_mirror_regions * select_locale_lang * select_locale_enc * ask_for_swap * Updated to preset values * ask_for_bootloader Won't use it * set_root_password() * Updated to preset values * ask_for_audio_selection * select_kernel * ask_for_a_timezone * Updated to use preset values * select_ntp * ask_ntp * ask_for_swap flake8 complains * Adapted to preset values * ask_additional_packages_to_install (from svartkanin) * ask_to_configure_network (adapted from svartkanin version) * Updated to preset values * ask_hostname * select_additional_repositories * bug in nic conversion _select_harddrives adapted to preset_menu --- archinstall/lib/menu/menu.py | 29 +++++---- archinstall/lib/menu/selection_menu.py | 66 ++++++++++---------- archinstall/lib/user_interaction.py | 109 +++++++++++++++++++++++++-------- 3 files changed, 134 insertions(+), 70 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index ee4b87e3..962dcc4d 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -84,8 +84,6 @@ class Menu(TerminalMenu): self.skip = skip self.default_option = default_option self.multi = multi - self.preselection(preset_values,cursor_index) - menu_title = f'\n{title}\n\n' if skip: @@ -97,6 +95,7 @@ class Menu(TerminalMenu): default = f'{default_option} (default)' self.menu_options = [default] + [o for o in self.menu_options if default_option != o] + self.preselection(preset_values,cursor_index) cursor = "> " main_menu_cursor_style = ("fg_cyan", "bold") main_menu_style = ("bg_blue", "fg_gray") @@ -154,22 +153,28 @@ class Menu(TerminalMenu): def preselection(self,preset_values :list = [],cursor_index :int = None): def from_preset_to_cursor(): if preset_values: - if isinstance(preset_values,str): - self.cursor_index = self.menu_options.index(preset_values) - else: # should return an error, but this is smoother - self.cursor_index = self.menu_options.index(preset_values[0]) + # 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) + else: # should return an error, but this is smoother + self.cursor_index = self.menu_options.index(self.preset_values[0]) + except ValueError: + self.cursor_index = 0 - self.preset_values = preset_values self.cursor_index = cursor_index - if preset_values and cursor_index is None: - from_preset_to_cursor() - if preset_values and not self.multi: # Not supported by the infraestructure + if not preset_values: self.preset_values = None - from_preset_to_cursor() + return - if self.default_option and self.multi: + 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: + from_preset_to_cursor() + 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 97dfd2a7..a82e8a70 100644 --- a/archinstall/lib/menu/selection_menu.py +++ b/archinstall/lib/menu/selection_menu.py @@ -252,6 +252,7 @@ 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 cursor_pos = None while True: # Before continuing, set the preferred keyboard layout/language in the current terminal. @@ -278,7 +279,7 @@ class GeneralMenu: Returns true if the menu shall continue, False if it has ended """ # find the selected option in our option list - option = [[k, v] for k, v in self._menu_options.items() if v.text.strip() == selection] + option = [[k, v] for k, v in self._menu_options.items() if v.text.strip() == selection.strip()] if len(option) != 1: raise ValueError(f'Selection not found: {selection}') selector_name = option[0][0] @@ -303,8 +304,8 @@ class GeneralMenu: result = None if selector.func: - # TODO insert code to allow selector functions with preset value - result = selector.func() + presel_val = self.option(selector_name).get_selection() + result = selector.func(presel_val) self._menu_options[selector_name].set_current_selection(result) self._data_store[selector_name] = result exec_ret_val = selector.exec_func(selector_name,result) if selector.exec_func else False @@ -412,32 +413,33 @@ class GlobalMenu(GeneralMenu): super().__init__(data_store=data_store, auto_cursor=True) def _setup_selection_menu_options(self): + # archinstall.Language will not use preset values self._menu_options['archinstall-language'] = \ Selector( _('Select Archinstall language'), - lambda: self._select_archinstall_language('English'), + lambda x: self._select_archinstall_language('English'), default='English', enabled=True) self._menu_options['keyboard-layout'] = \ - Selector(_('Select keyboard layout'), lambda: select_language('us'), default='us') + Selector(_('Select keyboard layout'), lambda preset: select_language('us',preset), default='us') self._menu_options['mirror-region'] = \ Selector( _('Select mirror region'), - lambda: select_mirror_regions(), + select_mirror_regions, display_func=lambda x: list(x.keys()) if x else '[]', default={}) self._menu_options['sys-language'] = \ - Selector(_('Select locale language'), lambda: select_locale_lang('en_US'), default='en_US') + Selector(_('Select locale language'), lambda preset: select_locale_lang('en_US',preset), default='en_US') self._menu_options['sys-encoding'] = \ - Selector(_('Select locale encoding'), lambda: select_locale_enc('utf-8'), default='utf-8') + Selector(_('Select locale encoding'), lambda preset: select_locale_enc('utf-8',preset), default='utf-8') self._menu_options['harddrives'] = \ Selector( _('Select harddrives'), - lambda: self._select_harddrives()) + self._select_harddrives) self._menu_options['disk_layouts'] = \ Selector( _('Select disk layout'), - lambda: select_disk_layout( + lambda x: select_disk_layout( storage['arguments'].get('harddrives', []), storage['arguments'].get('advanced', False) ), @@ -445,80 +447,82 @@ class GlobalMenu(GeneralMenu): self._menu_options['!encryption-password'] = \ Selector( _('Set encryption password'), - lambda: self._select_encrypted_password(), + lambda x: self._select_encrypted_password(), display_func=lambda x: secret(x) if x else 'None', dependencies=['harddrives']) self._menu_options['swap'] = \ Selector( _('Use swap'), - lambda: ask_for_swap(), + lambda preset: ask_for_swap(preset), default=True) self._menu_options['bootloader'] = \ Selector( _('Select bootloader'), - lambda: ask_for_bootloader(storage['arguments'].get('advanced', False)), + 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'), - lambda: ask_hostname(), + ask_hostname, default='archlinux') + # root password won't have preset value self._menu_options['!root-password'] = \ Selector( _('Set root password'), - lambda: self._set_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: self._create_superuser_account(), + lambda x: self._create_superuser_account(), dependencies_not=['!root-password'], display_func=lambda x: list(x.keys()) if x else '') self._menu_options['!users'] = \ Selector( _('Specify user account'), - lambda: self._create_user_account(), + lambda x: self._create_user_account(), default={}, display_func=lambda x: list(x.keys()) if x else '[]') self._menu_options['profile'] = \ Selector( _('Specify profile'), - lambda: self._select_profile(), + lambda x: self._select_profile(), display_func=lambda x: x if x else 'None') self._menu_options['audio'] = \ Selector( _('Select audio'), - lambda: ask_for_audio_selection(is_desktop_profile(storage['arguments'].get('profile', None)))) + lambda preset: ask_for_audio_selection(is_desktop_profile(storage['arguments'].get('profile', None)),preset)) self._menu_options['kernels'] = \ Selector( _('Select kernels'), - lambda: select_kernel(), + lambda preset: select_kernel(preset), default=['linux']) self._menu_options['packages'] = \ Selector( _('Additional packages to install'), - lambda: ask_additional_packages_to_install(storage['arguments'].get('packages', None)), + # 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'), - lambda: select_additional_repositories(), + select_additional_repositories, default=[]) self._menu_options['nic'] = \ Selector( _('Configure network'), - lambda: ask_to_configure_network(), + ask_to_configure_network, display_func=lambda x: x if x else _('Not configured, unavailable unless setup manually'), default={}) self._menu_options['timezone'] = \ Selector( _('Select timezone'), - lambda: ask_for_a_timezone(), + lambda preset: ask_for_a_timezone(preset), default='UTC') self._menu_options['ntp'] = \ Selector( _('Set automatic time sync (NTP)'), - lambda: self._select_ntp(), + lambda preset: self._select_ntp(preset), default=True) self._menu_options['install'] = \ Selector( @@ -591,20 +595,20 @@ class GlobalMenu(GeneralMenu): else: return None - def _select_ntp(self) -> bool: - ntp = ask_ntp() + def _select_ntp(self, preset :bool = True) -> bool: + ntp = ask_ntp(preset) value = str(ntp).lower() SysCommand(f'timedatectl set-ntp {value}') return ntp - def _select_harddrives(self): - old_haddrives = storage['arguments'].get('harddrives', []) - harddrives = select_harddrives() + def _select_harddrives(self, old_harddrives : list) -> list: + # old_haddrives = storage['arguments'].get('harddrives', []) + harddrives = select_harddrives(old_harddrives) # in case the harddrives got changed we have to reset the disk layout as well - if old_haddrives != harddrives: + if old_harddrives != harddrives: self._menu_options.get('disk_layouts').set_current_selection(None) storage['arguments']['disk_layouts'] = {} diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index f01f7597..ddd388dc 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -9,6 +9,7 @@ import signal import sys import time from collections.abc import Iterable +from copy import copy from typing import List, Any, Optional, Dict, Union, TYPE_CHECKING # https://stackoverflow.com/a/39757388/929999 @@ -274,21 +275,29 @@ class MiniCurses: return response -def ask_for_swap(): +def ask_for_swap(preset :bool = True) -> bool: + if preset: + preset_val = 'yes' + else: + preset_val = 'no' prompt = _('Would you like to use swap on zram?') - choice = Menu(prompt, ['yes', 'no'], default_option='yes').run() + choice = Menu(prompt, ['yes', 'no'], default_option='yes', preset_values=preset_val).run() return False if choice == 'no' else True -def ask_ntp() -> bool: +def ask_ntp(preset :bool = True) -> bool: prompt = str(_('Would you like to use automatic time synchronization (NTP) with the default time servers?\n')) prompt += str(_('Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki')) - choice = Menu(prompt, ['yes', 'no'], skip=False, default_option='yes').run() + if preset: + preset_val = 'yes' + else: + preset_val = 'no' + choice = Menu(prompt, ['yes', 'no'], skip=False, preset_values=preset_val, default_option='yes').run() return False if choice == 'no' else True -def ask_hostname(): - hostname = input(_('Desired hostname for the installation: ')).strip(' ') +def ask_hostname(preset :str = None) -> str : + hostname = TextInput(_('Desired hostname for the installation: '),preset).run().strip(' ') return hostname @@ -341,7 +350,7 @@ def ask_for_additional_users(prompt :str = '') -> tuple[dict[str, dict[str, str return users, superusers -def ask_for_a_timezone() -> str: +def ask_for_a_timezone(preset :str = None) -> str: timezones = list_timezones() default = 'UTC' @@ -349,18 +358,28 @@ def ask_for_a_timezone() -> str: _('Select a timezone'), list(timezones), skip=False, + preset_values=preset, default_option=default ).run() return selected_tz -def ask_for_bootloader(advanced_options :bool = False) -> str: +def ask_for_bootloader(advanced_options :bool = False, preset :str = None) -> str: + + if preset == 'systemd-bootctl': + preset_val = 'systemd-boot' if advanced_options else 'no' + elif preset == 'grub-install': + preset_val = 'grub' if advanced_options else 'yes' + else: + preset_val = preset + bootloader = "systemd-bootctl" if has_uefi() else "grub-install" if has_uefi(): if not advanced_options: bootloader_choice = Menu( _('Would you like to use GRUB as a bootloader instead of systemd-boot?'), ['yes', 'no'], + preset_values=preset_val, default_option='no' ).run() @@ -369,7 +388,7 @@ def ask_for_bootloader(advanced_options :bool = False) -> str: else: # We use the common names for the bootloader as the selection, and map it back to the expected values. choices = ['systemd-boot', 'grub', 'efistub'] - selection = Menu(_('Choose a bootloader'), choices).run() + selection = Menu(_('Choose a bootloader'), choices,preset_values=preset_val).run() if selection != "": if selection == 'systemd-boot': bootloader = 'systemd-bootctl' @@ -381,12 +400,13 @@ def ask_for_bootloader(advanced_options :bool = False) -> str: return bootloader -def ask_for_audio_selection(desktop :bool = True) -> str: +def ask_for_audio_selection(desktop :bool = True, preset :str = None) -> str: audio = 'pipewire' if desktop else 'none' choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', 'none'] selected_audio = Menu( _('Choose an audio server'), choices, + preset_values=preset, default_option=audio, skip=False ).run() @@ -424,7 +444,7 @@ def ask_additional_packages_to_install(pre_set_packages :List[str] = []) -> List return packages -def ask_to_configure_network() -> Dict[str, Any]: +def ask_to_configure_network(preset :Dict[str, Any] = {}) -> Dict[str, Any]: # Optionally configure one network interface. # while 1: # {MAC: Ifname} @@ -433,12 +453,27 @@ def ask_to_configure_network() -> Dict[str, Any]: 'network_manager': str(_('Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)')), **list_interfaces() } + # for this routine it's easier to set the cursor position rather than a preset value + cursor_idx = None + if preset: + if preset['type'] == 'iso_config': + cursor_idx = 0 + elif preset['type'] == 'network_manager': + cursor_idx = 1 + else: + try : + # let's hope order in dictionaries stay + cursor_idx = list(interfaces.values()).index(preset.get('type')) + except ValueError: + pass - nic = Menu(_('Select one network interface to configure'), list(interfaces.values())).run() + nic = Menu(_('Select one network interface to configure'), interfaces.values(),cursor_index=cursor_idx).run() if not nic: return {} + # nic = network_manager_nt + if nic == interfaces['iso_config']: return {'type': 'iso_config'} elif nic == interfaces['network_manager']: @@ -448,16 +483,23 @@ def ask_to_configure_network() -> Dict[str, Any]: # For selecting modes without entering text within brackets, # printing out this part separate from options, passed in # `generic_select` + # we only keep data if it is the same nic as before + if preset.get('type') != nic: + preset_d = {'type': nic, 'dhcp': True, 'ip': None, 'gateway': None, 'dns': []} + else: + preset_d = copy(preset) + modes = ['DHCP (auto detect)', 'IP (static)'] default_mode = 'DHCP (auto detect)' + cursor_idx = 0 if preset_d.get('dhcp',True) else 1 prompt = _('Select which mode to configure for "{}" or skip to use default mode "{}"').format(nic, default_mode) - mode = Menu(prompt, modes, default_option=default_mode).run() - + mode = Menu(prompt, modes, default_option=default_mode, cursor_index=cursor_idx).run() + # TODO preset values for ip and gateway if mode == 'IP (static)': while 1: prompt = _('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(nic) - ip = input(prompt).strip() + ip = TextInput(prompt,preset_d.get('ip')).run().strip() # Implemented new check for correct IP/subnet input try: ipaddress.ip_interface(ip) @@ -471,7 +513,7 @@ def ask_to_configure_network() -> Dict[str, Any]: # Implemented new check for correct gateway IP address while 1: - gateway = input(_('Enter your gateway (router) IP address or leave blank for none: ')).strip() + gateway = TextInput(_('Enter your gateway (router) IP address or leave blank for none: '),preset_d.get('gateway')).run().strip() try: if len(gateway) == 0: gateway = None @@ -486,7 +528,11 @@ def ask_to_configure_network() -> Dict[str, Any]: ) dns = None - dns_input = input(_('Enter your DNS servers (space separated, blank for none): ')).strip() + if preset_d.get('dns'): + preset_d['dns'] = ' '.join(preset_d['dns']) + else: + preset_d['dns'] = None + dns_input = TextInput(_('Enter your DNS servers (space separated, blank for none): '),preset_d['dns']).run().strip() if len(dns_input): dns = dns_input.split(' ') @@ -847,7 +893,7 @@ def select_profile() -> Optional[Profile]: return None -def select_language(default_value :str) -> str: +def select_language(default_value :str, preset_value :str = None) -> str: """ Asks the user to select a language Usually this is combined with :ref:`archinstall.list_keyboard_languages`. @@ -861,11 +907,11 @@ def select_language(default_value :str) -> str: # allows for searching anyways sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) - selected_lang = Menu(_('Select Keyboard layout'), sorted_kb_lang, default_option=default_value, sort=False).run() + selected_lang = Menu(_('Select Keyboard layout'), sorted_kb_lang, default_option=default_value, preset_values=preset_value, sort=False).run() return selected_lang -def select_mirror_regions() -> Dict[str, Any]: +def select_mirror_regions(preset_values :Dict[str, Any] = {}) -> Dict[str, Any]: """ Asks the user to select a mirror or region Usually this is combined with :ref:`archinstall.list_mirrors`. @@ -873,11 +919,15 @@ def select_mirror_regions() -> Dict[str, Any]: :return: The dictionary information about a mirror/region. :rtype: dict """ - + if preset_values is None: + preselected = None + else: + preselected = list(preset_values.keys()) mirrors = list_mirrors() selected_mirror = Menu( _('Select one of the regions to download packages from'), list(mirrors.keys()), + preset_values=preselected, multi=True ).run() @@ -887,7 +937,7 @@ def select_mirror_regions() -> Dict[str, Any]: return {} -def select_harddrives() -> Optional[str]: +def select_harddrives(preset : List[str] = []) -> List[str]: """ Asks the user to select one or multiple hard drives @@ -896,10 +946,11 @@ def select_harddrives() -> Optional[str]: """ hard_drives = all_blockdevices(partitions=False).values() options = {f'{option}': option for option in hard_drives} - + preset_disks = {f'{option}':option for option in preset} selected_harddrive = Menu( _('Select one or more hard drives to use and configure'), list(options.keys()), + preset_values=list(preset_disks.keys()), multi=True ).run() @@ -943,7 +994,7 @@ def select_driver(options :Dict[str, Any] = AVAILABLE_GFX_DRIVERS, force_ask :bo raise RequirementError("Selecting drivers require a least one profile to be given as an option.") -def select_kernel() -> List[str]: +def select_kernel(preset :List[str] = None) -> List[str]: """ Asks the user to select a kernel for system. @@ -959,11 +1010,12 @@ def select_kernel() -> List[str]: kernels, sort=True, multi=True, + preset_values=preset, default_option=default_kernel ).run() return selected_kernels -def select_additional_repositories() -> List[str]: +def select_additional_repositories(preset :List[str]) -> List[str]: """ Allows the user to select additional repositories (multilib, and testing) if desired. @@ -978,6 +1030,7 @@ def select_additional_repositories() -> List[str]: repositories, sort=False, multi=True, + preset_values=preset, default_option=[] ).run() @@ -986,7 +1039,7 @@ def select_additional_repositories() -> List[str]: return [] -def select_locale_lang(default): +def select_locale_lang(default :str,preset :str = None) -> str : locales = list_locales() locale_lang = set([locale.split()[0] for locale in locales]) @@ -994,13 +1047,14 @@ def select_locale_lang(default): _('Choose which locale language to use'), locale_lang, sort=True, + preset_values=preset, default_option=default ).run() return selected_locale -def select_locale_enc(default): +def select_locale_enc(default :str,preset :str = None) -> str: locales = list_locales() locale_enc = set([locale.split()[1] for locale in locales]) @@ -1008,6 +1062,7 @@ def select_locale_enc(default): _('Choose which locale encoding to use'), locale_enc, sort=True, + preset_values=preset, default_option=default ).run() -- cgit v1.2.3-70-g09d2