index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | archinstall/lib/user_interaction/__init__.py | 16 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/backwards_compatible_conf.py | 95 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/disk_conf.py | 403 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/general_conf.py | 59 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/locale_conf.py | 3 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/manage_users_conf.py | 21 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/network_conf.py | 6 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/partitioning_conf.py | 362 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/save_conf.py | 74 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/subvolume_config.py | 98 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/system_conf.py | 133 | ||||
-rw-r--r-- | archinstall/lib/user_interaction/utils.py | 47 |
diff --git a/archinstall/lib/user_interaction/__init__.py b/archinstall/lib/user_interaction/__init__.py index 2bc46759..5ee89de0 100644 --- a/archinstall/lib/user_interaction/__init__.py +++ b/archinstall/lib/user_interaction/__init__.py @@ -1,12 +1,10 @@ -from .save_conf import save_config from .manage_users_conf import ask_for_additional_users -from .backwards_compatible_conf import generic_select, generic_multi_select from .locale_conf import select_locale_lang, select_locale_enc -from .system_conf import select_kernel, select_harddrives, select_driver, ask_for_bootloader, ask_for_swap +from .system_conf import select_kernel, select_driver, ask_for_bootloader, ask_for_swap from .network_conf import ask_to_configure_network -from .partitioning_conf import select_partition -from .general_conf import (ask_ntp, ask_for_a_timezone, ask_for_audio_selection, select_language, select_mirror_regions, - select_profile, select_archinstall_language, ask_additional_packages_to_install, - select_additional_repositories, ask_hostname, add_number_of_parrallel_downloads) -from .disk_conf import ask_for_main_filesystem_format, select_individual_blockdevice_usage, select_disk_layout, select_disk -from .utils import get_password, do_countdown +from .general_conf import ( + ask_ntp, ask_for_a_timezone, ask_for_audio_selection, select_language, select_mirror_regions, + select_archinstall_language, ask_additional_packages_to_install, + select_additional_repositories, ask_hostname, add_number_of_parrallel_downloads +) +from .utils import get_password diff --git a/archinstall/lib/user_interaction/backwards_compatible_conf.py b/archinstall/lib/user_interaction/backwards_compatible_conf.py deleted file mode 100644 index 296572d2..00000000 --- a/archinstall/lib/user_interaction/backwards_compatible_conf.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import annotations - -import logging -import sys -from collections.abc import Iterable -from typing import Any, Union, TYPE_CHECKING - -from ..exceptions import RequirementError -from ..menu import Menu -from ..output import log - -if TYPE_CHECKING: - _: Any - - -def generic_select( - p_options: Union[list, dict], - input_text: str = '', - allow_empty_input: bool = True, - options_output: bool = True, # function not available - sort: bool = False, - multi: bool = False, - default: Any = None) -> Any: - """ - A generic select function that does not output anything - other than the options and their indexes. As an example: - - generic_select(["first", "second", "third option"]) - > first - second - third option - When the user has entered the option correctly, - this function returns an item from list, a string, or None - - Options can be any iterable. - Duplicate entries are not checked, but the results with them are unreliable. Which element to choose from the duplicates depends on the return of the index() - Default value if not on the list of options will be added as the first element - sort will be handled by Menu() - """ - # We check that the options are iterable. If not we abort. Else we copy them to lists - # it options is a dictionary we use the values as entries of the list - # if options is a string object, each character becomes an entry - # if options is a list, we implictily build a copy to maintain immutability - if not isinstance(p_options, Iterable): - log(f"Objects of type {type(p_options)} is not iterable, and are not supported at generic_select", fg="red") - log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>", level=logging.WARNING) - raise RequirementError("generic_select() requires an iterable as option.") - - input_text = input_text if input_text else _('Select one of the values shown below: ') - - if isinstance(p_options, dict): - options = list(p_options.values()) - else: - options = list(p_options) - # check that the default value is in the list. If not it will become the first entry - if default and default not in options: - options.insert(0, default) - - # one of the drawbacks of the new interface is that in only allows string like options, so we do a conversion - # also for the default value if it exists - soptions = list(map(str, options)) - default_value = options[options.index(default)] if default else None - - selected_option = Menu(input_text, - soptions, - skip=allow_empty_input, - multi=multi, - default_option=default_value, - sort=sort).run() - # we return the original objects, not the strings. - # options is the list with the original objects and soptions the list with the string values - # thru the map, we get from the value selected in soptions it index, and thu it the original object - if not selected_option: - return selected_option - elif isinstance(selected_option, list): # for multi True - selected_option = list(map(lambda x: options[soptions.index(x)], selected_option)) - else: # for multi False - selected_option = options[soptions.index(selected_option)] - return selected_option - - -def generic_multi_select(p_options: Union[list, dict], - text: str = '', - sort: bool = False, - default: Any = None, - allow_empty: bool = False) -> Any: - - text = text if text else _("Select one or more of the options below: ") - - return generic_select(p_options, - input_text=text, - allow_empty_input=allow_empty, - sort=sort, - multi=True, - default=default) diff --git a/archinstall/lib/user_interaction/disk_conf.py b/archinstall/lib/user_interaction/disk_conf.py index 554d13ef..a77e950a 100644 --- a/archinstall/lib/user_interaction/disk_conf.py +++ b/archinstall/lib/user_interaction/disk_conf.py @@ -1,86 +1,391 @@ from __future__ import annotations -from typing import Any, Dict, TYPE_CHECKING, Optional +import logging +from pathlib import Path +from typing import Any, TYPE_CHECKING, Optional, List, Tuple -from .partitioning_conf import manage_new_and_existing_partitions, get_default_partition_layout -from ..disk import BlockDevice -from ..exceptions import DiskError -from ..menu import Menu -from ..menu.menu import MenuSelectionType +from .. import disk +from ..hardware import has_uefi +from ..menu import Menu, MenuSelectionType, TableMenu +from ..output import FormattedOutput +from ..output import log +from ..utils.util import prompt_dir if TYPE_CHECKING: _: Any -def ask_for_main_filesystem_format(advanced_options=False) -> str: - options = {'btrfs': 'btrfs', 'ext4': 'ext4', 'xfs': 'xfs', 'f2fs': 'f2fs'} +def select_devices(preset: List[disk.BDevice] = []) -> List[disk.BDevice]: + """ + Asks the user to select one or multiple devices - advanced = {'ntfs': 'ntfs'} + :return: List of selected devices + :rtype: list + """ - if advanced_options: - options.update(advanced) + def _preview_device_selection(selection: disk._DeviceInfo) -> Optional[str]: + dev = disk.device_handler.get_device(selection.path) + if dev and dev.partition_infos: + return FormattedOutput.as_table(dev.partition_infos) + return None - prompt = _('Select which filesystem your main partition should use') - choice = Menu(prompt, options, skip=False).run() - return choice.value + if preset is None: + preset = [] + + title = str(_('Select one or more devices to use and configure')) + warning = str(_('If you reset the device selection this will also reset the current disk layout. Are you sure?')) + + devices = disk.device_handler.devices + options = [d.device_info for d in devices] + preset_value = [p.device_info for p in preset] + + choice = TableMenu( + title, + data=options, + multi=True, + preset=preset_value, + preview_command=_preview_device_selection, + preview_title=str(_('Existing Partitions')), + preview_size=0.2, + allow_reset=True, + allow_reset_warning_msg=warning + ).run() + + match choice.type_: + case MenuSelectionType.Reset: return [] + case MenuSelectionType.Skip: return preset + case MenuSelectionType.Selection: + selected_device_info: List[disk._DeviceInfo] = choice.value # type: ignore + selected_devices = [] + + for device in devices: + if device.device_info in selected_device_info: + selected_devices.append(device) + + return selected_devices -def select_individual_blockdevice_usage(block_devices: list) -> Dict[str, Any]: - result = {} +def get_default_partition_layout( + devices: List[disk.BDevice], + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_option: bool = False +) -> List[disk.DeviceModification]: - for device in block_devices: - layout = manage_new_and_existing_partitions(device) - result[device.path] = layout + if len(devices) == 1: + device_modification = suggest_single_disk_layout( + devices[0], + filesystem_type=filesystem_type, + advanced_options=advanced_option + ) + return [device_modification] + else: + return suggest_multi_disk_layout( + devices, + filesystem_type=filesystem_type, + advanced_options=advanced_option + ) - return result +def _manual_partitioning( + preset: List[disk.DeviceModification], + devices: List[disk.BDevice] +) -> List[disk.DeviceModification]: + modifications = [] + for device in devices: + mod = next(filter(lambda x: x.device == device, preset), None) + if not mod: + mod = disk.DeviceModification(device, wipe=False) -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')) - custome_mode = str(_('Select what to do with each individual drive (followed by partition usage)')) - modes = [wipe_mode, custome_mode] + if partitions := disk.manual_partitioning(device, preset=mod.partitions): + mod.partitions = partitions + modifications.append(mod) + return modifications + + +def select_disk_config( + preset: Optional[disk.DiskLayoutConfiguration] = None, + advanced_option: bool = False +) -> Optional[disk.DiskLayoutConfiguration]: + default_layout = disk.DiskLayoutType.Default.display_msg() + manual_mode = disk.DiskLayoutType.Manual.display_msg() + pre_mount_mode = disk.DiskLayoutType.Pre_mount.display_msg() + + options = [default_layout, manual_mode, pre_mount_mode] + preset_value = preset.config_type.display_msg() if preset else None warning = str(_('Are you sure you want to reset this setting?')) choice = Menu( - _('Select what you wish to do with the selected block devices'), - modes, + _('Select a partitioning option'), + options, allow_reset=True, - allow_reset_warning_msg=warning + allow_reset_warning_msg=warning, + sort=False, + preview_size=0.2, + preset_values=preset_value ).run() match choice.type_: case MenuSelectionType.Skip: return preset case MenuSelectionType.Reset: return None case MenuSelectionType.Selection: - if choice.value == wipe_mode: - return get_default_partition_layout(block_devices, advanced_options) + if choice.single_value == pre_mount_mode: + output = "You will use whatever drive-setup is mounted at the specified directory\n" + output += "WARNING: Archinstall won't check the suitability of this setup\n" + + path = prompt_dir(str(_('Enter the root directory of the mounted devices: ')), output) + mods = disk.device_handler.detect_pre_mounted_mods(path) + + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Pre_mount, + relative_mountpoint=path, + device_modifications=mods + ) + + preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] + + devices = select_devices(preset_devices) + + if not devices: + return None + + if choice.value == default_layout: + modifications = get_default_partition_layout(devices, advanced_option=advanced_option) + if modifications: + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Default, + device_modifications=modifications + ) + elif choice.value == manual_mode: + preset_mods = preset.device_modifications if preset else [] + modifications = _manual_partitioning(preset_mods, devices) + + if modifications: + return disk.DiskLayoutConfiguration( + config_type=disk.DiskLayoutType.Manual, + device_modifications=modifications + ) + + return None + + +def _boot_partition() -> disk.PartitionModification: + if has_uefi(): + start = disk.Size(1, disk.Unit.MiB) + size = disk.Size(512, disk.Unit.MiB) + else: + start = disk.Size(3, disk.Unit.MiB) + size = disk.Size(203, disk.Unit.MiB) + + # boot partition + return disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=start, + length=size, + mountpoint=Path('/boot'), + fs_type=disk.FilesystemType.Fat32, + flags=[disk.PartitionFlag.Boot] + ) + + +def ask_for_main_filesystem_format(advanced_options=False) -> disk.FilesystemType: + options = { + 'btrfs': disk.FilesystemType.Btrfs, + 'ext4': disk.FilesystemType.Ext4, + 'xfs': disk.FilesystemType.Xfs, + 'f2fs': disk.FilesystemType.F2fs + } + + if advanced_options: + options.update({'ntfs': disk.FilesystemType.Ntfs}) + + prompt = _('Select which filesystem your main partition should use') + choice = Menu(prompt, options, skip=False, sort=False).run() + return options[choice.single_value] + + +def suggest_single_disk_layout( + device: disk.BDevice, + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_options: bool = False, + separate_home: Optional[bool] = None +) -> disk.DeviceModification: + if not filesystem_type: + filesystem_type = ask_for_main_filesystem_format(advanced_options) + + min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB) + root_partition_size = disk.Size(20, disk.Unit.GiB) + using_subvolumes = False + using_home_partition = False + compression = False + device_size_gib = device.device_info.total_size + + if filesystem_type == disk.FilesystemType.Btrfs: + 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() + using_subvolumes = choice.value == Menu.yes() + + prompt = str(_('Would you like to use BTRFS compression?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + compression = choice.value == Menu.yes() + + device_modification = disk.DeviceModification(device, wipe=True) + + # Used for reference: https://wiki.archlinux.org/title/partitioning + # 2 MiB is unallocated for GRUB on BIOS. Potentially unneeded for other bootloaders? + + # TODO: On BIOS, /boot partition is only needed if the drive will + # be encrypted, otherwise it is not recommended. We should probably + # add a check for whether the drive will be encrypted or not. + + # Increase the UEFI partition if UEFI is detected. + # Also re-align the start to 1MiB since we don't need the first sectors + # like we do in MBR layouts where the boot loader is installed traditionally. + + boot_partition = _boot_partition() + device_modification.add_partition(boot_partition) + + if not using_subvolumes: + if device_size_gib >= min_size_to_allow_home_part: + if separate_home is None: + 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() + using_home_partition = choice.value == Menu.yes() + elif separate_home is True: + using_home_partition = True else: - return select_individual_blockdevice_usage(block_devices) + using_home_partition = False + # root partition + start = disk.Size(513, disk.Unit.MiB) if has_uefi() else disk.Size(206, disk.Unit.MiB) -def select_disk(dict_o_disks: Dict[str, BlockDevice]) -> Optional[BlockDevice]: - """ - Asks the user to select a harddrive from the `dict_o_disks` selection. - Usually this is combined with :ref:`archinstall.list_drives`. + # Set a size for / (/root) + if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition: + length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size) + else: + length = min(device.device_info.total_size, root_partition_size) - :param dict_o_disks: A `dict` where keys are the drive-name, value should be a dict containing drive information. - :type dict_o_disks: dict + root_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=start, + length=length, + mountpoint=Path('/') if not using_subvolumes else None, + fs_type=filesystem_type, + mount_options=['compress=zstd'] if compression else [], + ) + device_modification.add_partition(root_partition) - :return: The name/path (the dictionary key) of the selected drive - :rtype: str - """ - drives = sorted(list(dict_o_disks.keys())) - if len(drives) >= 1: - title = str(_('You can skip selecting a drive and partitioning and use whatever drive-setup is mounted at /mnt (experimental)')) + '\n' - title += str(_('Select one of the disks or skip and use /mnt as default')) + if using_subvolumes: + # https://btrfs.wiki.kernel.org/index.php/FAQ + # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash + # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh + subvolumes = [ + disk.SubvolumeModification(Path('@'), Path('/')), + disk.SubvolumeModification(Path('@home'), Path('/home')), + disk.SubvolumeModification(Path('@log'), Path('/var/log')), + disk.SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), + disk.SubvolumeModification(Path('@.snapshots'), Path('/.snapshots')) + ] + root_partition.btrfs_subvols = subvolumes + elif using_home_partition: + # If we don't want to use subvolumes, + # But we want to be able to re-use data between re-installs.. + # A second partition for /home would be nice if we have the space for it + home_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=root_partition.length, + length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size), + mountpoint=Path('/home'), + fs_type=filesystem_type, + mount_options=['compress=zstd'] if compression else [] + ) + device_modification.add_partition(home_partition) + + return device_modification + + +def suggest_multi_disk_layout( + devices: List[disk.BDevice], + filesystem_type: Optional[disk.FilesystemType] = None, + advanced_options: bool = False +) -> List[disk.DeviceModification]: + if not devices: + return [] + + # Not really a rock solid foundation of information to stand on, but it's a start: + # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ + # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ + min_home_partition_size = disk.Size(40, disk.Unit.GiB) + # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? + desired_root_partition_size = disk.Size(20, disk.Unit.GiB) + compression = False + + if not filesystem_type: + filesystem_type = ask_for_main_filesystem_format(advanced_options) + + # find proper disk for /home + possible_devices = list(filter(lambda x: x.device_info.total_size >= min_home_partition_size, devices)) + home_device = max(possible_devices, key=lambda d: d.device_info.total_size) if possible_devices else None + + # find proper device for /root + devices_delta = {} + for device in devices: + if device is not home_device: + delta = device.device_info.total_size - desired_root_partition_size + devices_delta[device] = delta + + sorted_delta: List[Tuple[disk.BDevice, Any]] = sorted(devices_delta.items(), key=lambda x: x[1]) + root_device: Optional[disk.BDevice] = sorted_delta[0][0] + + if home_device is None or root_device is None: + text = _('The selected drives do not have the minimum capacity required for an automatic suggestion\n') + text += _('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(disk.Unit.GiB)) + text += _('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(disk.Unit.GiB)) + Menu(str(text), [str(_('Continue'))], skip=False).run() + return [] + + if filesystem_type == disk.FilesystemType.Btrfs: + prompt = str(_('Would you like to use BTRFS compression?')) + choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run() + compression = choice.value == Menu.yes() + + device_paths = ', '.join([str(d.device_info.path) for d in devices]) + log(f"Suggesting multi-disk-layout for devices: {device_paths}", level=logging.DEBUG) + log(f"/root: {root_device.device_info.path}", level=logging.DEBUG) + log(f"/home: {home_device.device_info.path}", level=logging.DEBUG) + + root_device_modification = disk.DeviceModification(root_device, wipe=True) + home_device_modification = disk.DeviceModification(home_device, wipe=True) - choice = Menu(title, drives).run() + # add boot partition to the root device + boot_partition = _boot_partition() + root_device_modification.add_partition(boot_partition) - if choice.type_ == MenuSelectionType.Skip: - return None + # add root partition to the root device + root_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(513, disk.Unit.MiB) if has_uefi() else disk.Size(206, disk.Unit.MiB), + length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size), + mountpoint=Path('/'), + mount_options=['compress=zstd'] if compression else [], + fs_type=filesystem_type + ) + root_device_modification.add_partition(root_partition) - drive = dict_o_disks[choice.value] - return drive + # add home partition to home device + home_partition = disk.PartitionModification( + status=disk.ModificationStatus.Create, + type=disk.PartitionType.Primary, + start=disk.Size(1, disk.Unit.MiB), + length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size), + mountpoint=Path('/home'), + mount_options=['compress=zstd'] if compression else [], + fs_type=filesystem_type, + ) + home_device_modification.add_partition(home_partition) - raise DiskError('select_disk() requires a non-empty dictionary of disks to select from.') + return [root_device_modification, home_device_modification] diff --git a/archinstall/lib/user_interaction/general_conf.py b/archinstall/lib/user_interaction/general_conf.py index fc7ded45..7a6bb358 100644 --- a/archinstall/lib/user_interaction/general_conf.py +++ b/archinstall/lib/user_interaction/general_conf.py @@ -3,15 +3,13 @@ from __future__ import annotations import logging import pathlib from typing import List, Any, Optional, Dict, TYPE_CHECKING +from typing import Union from ..locale_helpers import list_keyboard_languages, list_timezones -from ..menu import Menu -from ..menu.menu import MenuSelectionType -from ..menu.text_input import TextInput +from ..menu import MenuSelectionType, Menu, TextInput from ..mirrors import list_mirrors from ..output import log from ..packages.packages import validate_package_list -from ..profiles import Profile, list_profiles from ..storage import storage from ..translationhandler import Language @@ -32,9 +30,10 @@ def ask_ntp(preset: bool = True) -> bool: def ask_hostname(preset: str = None) -> str: - hostname = TextInput(_('Desired hostname for the installation: '), preset).run().strip(' ') - return hostname - + while True: + hostname = TextInput(_('Desired hostname for the installation: '), preset).run().strip() + if hostname: + return hostname def ask_for_a_timezone(preset: str = None) -> str: timezones = list_timezones() @@ -52,7 +51,7 @@ def ask_for_a_timezone(preset: str = None) -> str: 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: Union[str, None] = None) -> Union[str, None]: no_audio = str(_('No audio server')) choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', no_audio] default = 'pipewire' if desktop else no_audio @@ -140,50 +139,6 @@ def select_archinstall_language(languages: List[Language], preset_value: Languag return options[choice.value] -def select_profile(preset) -> Optional[Profile]: - """ - # Asks the user to select a profile from the available profiles. - # - # :return: The name/dictionary key of the selected profile - # :rtype: str - # """ - top_level_profiles = sorted(list(list_profiles(filter_top_level_profiles=True))) - options = {} - - for profile in top_level_profiles: - profile = Profile(None, profile) - description = profile.get_profile_description() - - option = f'{profile.profile}: {description}' - options[option] = profile - - title = _('This is a list of pre-programmed profiles, they might make it easier to install things like desktop environments') - warning = str(_('Are you sure you want to reset this setting?')) - - selection = Menu( - title=title, - p_options=list(options.keys()), - allow_reset=True, - allow_reset_warning_msg=warning - ).run() - - match selection.type_: - case MenuSelectionType.Selection: - return options[selection.value] if selection.value is not None else None - case MenuSelectionType.Reset: - storage['profile_minimal'] = False - storage['_selected_servers'] = [] - storage['_desktop_profile'] = None - storage['sway_sys_priv_ctrl'] = None - storage['arguments']['sway_sys_priv_ctrl'] = None - storage['arguments']['desktop-environment'] = None - storage['arguments']['gfx_driver'] = None - storage['arguments']['gfx_driver_packages'] = None - return None - case MenuSelectionType.Skip: - return None - - def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: # Additional packages (with some light weight error handling for invalid package names) print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) diff --git a/archinstall/lib/user_interaction/locale_conf.py b/archinstall/lib/user_interaction/locale_conf.py index bbbe070b..88aec64e 100644 --- a/archinstall/lib/user_interaction/locale_conf.py +++ b/archinstall/lib/user_interaction/locale_conf.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import Any, TYPE_CHECKING from ..locale_helpers import list_locales -from ..menu import Menu -from ..menu.menu import MenuSelectionType +from ..menu import Menu, MenuSelectionType if TYPE_CHECKING: _: Any diff --git a/archinstall/lib/user_interaction/manage_users_conf.py b/archinstall/lib/user_interaction/manage_users_conf.py index 84ce3556..879578da 100644 --- a/archinstall/lib/user_interaction/manage_users_conf.py +++ b/archinstall/lib/user_interaction/manage_users_conf.py @@ -4,8 +4,7 @@ import re from typing import Any, Dict, TYPE_CHECKING, List, Optional from .utils import get_password -from ..menu import Menu -from ..menu.list_manager import ListManager +from ..menu import Menu, ListManager from ..models.users import User from ..output import FormattedOutput @@ -27,14 +26,14 @@ class UserList(ListManager): ] super().__init__(prompt, lusers, [self._actions[0]], self._actions[1:]) - def reformat(self, data: List[User]) -> Dict[str, User]: + def reformat(self, data: List[User]) -> Dict[str, Any]: table = FormattedOutput.as_table(data) rows = table.split('\n') # these are the header rows of the table and do not map to any User obviously # we're adding 2 spaces as prefix because the menu selector '> ' will be put before # the selectable rows so the header has to be aligned - display_data = {f' {rows[0]}': None, f' {rows[1]}': None} + display_data: Dict[str, Optional[User]] = {f' {rows[0]}': None, f' {rows[1]}': None} for row, user in zip(rows[2:], data): row = row.replace('|', '\\|') @@ -53,16 +52,16 @@ class UserList(ListManager): # was created we'll replace the existing one data = [d for d in data if d.username != new_user.username] data += [new_user] - elif action == self._actions[1]: # change password + elif action == self._actions[1] and entry: # change password prompt = str(_('Password for user "{}": ').format(entry.username)) new_password = get_password(prompt=prompt) if new_password: user = next(filter(lambda x: x == entry, data)) user.password = new_password - elif action == self._actions[2]: # promote/demote + elif action == self._actions[2] and entry: # promote/demote user = next(filter(lambda x: x == entry, data)) user.sudo = False if user.sudo else True - elif action == self._actions[3]: # delete + elif action == self._actions[3] and entry: # delete data = [d for d in data if d != entry] return data @@ -80,16 +79,20 @@ class UserList(ListManager): if not username: return None if not self._check_for_correct_username(username): - prompt = str(_("The username you entered is invalid. Try again")) + '\n' + prompt + error_prompt = str(_("The username you entered is invalid. Try again")) + print(error_prompt) else: break password = get_password(prompt=str(_('Password for user "{}": ').format(username))) + if not password: + return None + choice = Menu( str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(), skip=False, - default_option=Menu.no(), + default_option=Menu.yes(), clear_screen=False, show_search_hint=False ).run() diff --git a/archinstall/lib/user_interaction/network_conf.py b/archinstall/lib/user_interaction/network_conf.py index 5e637f23..b682c1d2 100644 --- a/archinstall/lib/user_interaction/network_conf.py +++ b/archinstall/lib/user_interaction/network_conf.py @@ -4,14 +4,12 @@ import ipaddress import logging from typing import Any, Optional, TYPE_CHECKING, List, Union, Dict -from ..menu.menu import MenuSelectionType -from ..menu.text_input import TextInput +from ..menu import MenuSelectionType, TextInput from ..models.network_configuration import NetworkConfiguration, NicType from ..networking import list_interfaces -from ..menu import Menu from ..output import log, FormattedOutput -from ..menu.list_manager import ListManager +from ..menu import ListManager, Menu if TYPE_CHECKING: _: Any diff --git a/archinstall/lib/user_interaction/partitioning_conf.py b/archinstall/lib/user_interaction/partitioning_conf.py deleted file mode 100644 index 0a5ede51..00000000 --- a/archinstall/lib/user_interaction/partitioning_conf.py +++ /dev/null @@ -1,362 +0,0 @@ -from __future__ import annotations - -import copy -from typing import List, Any, Dict, Union, TYPE_CHECKING, Callable, Optional - -from ..menu import Menu -from ..menu.menu import MenuSelectionType -from ..output import log, FormattedOutput - -from ..disk.validators import fs_types - -if TYPE_CHECKING: - from ..disk import BlockDevice - from ..disk.partition import Partition - _: Any - - -def partition_overlap(partitions: list, start: str, end: str) -> bool: - # TODO: Implement sanity check - return False - - -def current_partition_layout(partitions: List[Dict[str, Any]], with_idx: bool = False, with_title: bool = True) -> str: - - def do_padding(name: str, max_len: int): - spaces = abs(len(str(name)) - max_len) + 2 - pad_left = int(spaces / 2) - pad_right = spaces - pad_left - return f'{pad_right * " "}{name}{pad_left * " "}|' - - def flatten_data(data: Dict[str, Any]) -> Dict[str, Any]: - flattened = {} - for k, v in data.items(): - if k == 'filesystem': - flat = flatten_data(v) - flattened.update(flat) - elif k == 'btrfs': - # we're going to create a separate table for the btrfs subvolumes - pass - else: - flattened[k] = v - return flattened - - display_data: List[Dict[str, Any]] = [flatten_data(entry) for entry in partitions] - - column_names = {} - - # this will add an initial index to the table for each partition - if with_idx: - column_names['index'] = max([len(str(len(display_data))), len('index')]) - - # determine all attribute names and the max length - # of the value among all display_data to know the width - # of the table cells - for p in display_data: - for attribute, value in p.items(): - if attribute in column_names.keys(): - column_names[attribute] = max([column_names[attribute], len(str(value)), len(attribute)]) - else: - column_names[attribute] = max([len(str(value)), len(attribute)]) - - current_layout = '' - for name, max_len in column_names.items(): - current_layout += do_padding(name, max_len) - - current_layout = f'{current_layout[:-1]}\n{"-" * len(current_layout)}\n' - - for idx, p in enumerate(display_data): - row = '' - for name, max_len in column_names.items(): - if name == 'index': - row += do_padding(str(idx), max_len) - elif name in p: - row += do_padding(p[name], max_len) - else: - row += ' ' * (max_len + 2) + '|' - - current_layout += f'{row[:-1]}\n' - - # we'll create a separate table for the btrfs subvolumes - btrfs_subvolumes = [partition['btrfs']['subvolumes'] for partition in partitions if partition.get('btrfs', None)] - if len(btrfs_subvolumes) > 0: - for subvolumes in btrfs_subvolumes: - output = FormattedOutput.as_table(subvolumes) - current_layout += f'\n{output}' - - if with_title: - title = str(_('Current partition layout')) - return f'\n\n{title}:\n\n{current_layout}' - - return current_layout - - -def _get_partitions(partitions :List[Partition], filter_ :Callable = None) -> List[str]: - """ - filter allows to filter out the indexes once they are set. Should return True if element is to be included - """ - partition_indexes = [] - for i in range(len(partitions)): - if filter_: - if filter_(partitions[i]): - partition_indexes.append(str(i)) - else: - partition_indexes.append(str(i)) - - return partition_indexes - - -def select_partition( - title :str, - partitions :List[Partition], - multiple :bool = False, - filter_ :Callable = None -) -> Optional[int, List[int]]: - partition_indexes = _get_partitions(partitions, filter_) - - if len(partition_indexes) == 0: - return None - - choice = Menu(title, partition_indexes, multi=multiple).run() - - if choice.type_ == MenuSelectionType.Skip: - 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( - block_devices: Union['BlockDevice', List['BlockDevice']], - advanced_options: bool = False -) -> Optional[Dict[str, Any]]: - from ..disk import suggest_single_disk_layout, suggest_multi_disk_layout - - if len(block_devices) == 1: - return suggest_single_disk_layout(block_devices[0], advanced_options=advanced_options) - else: - return suggest_multi_disk_layout(block_devices, advanced_options=advanced_options) - - -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()]} - original_layout = copy.deepcopy(block_device_struct) - - new_partition = str(_('Create a new partition')) - suggest_partition_layout = str(_('Suggest partition layout')) - delete_partition = str(_('Delete a partition')) - delete_all_partitions = str(_('Clear/Delete all partitions')) - assign_mount_point = str(_('Assign mount-point for a partition')) - mark_formatted = str(_('Mark/Unmark a partition to be formatted (wipes data)')) - mark_compressed = str(_('Mark/Unmark a partition as compressed (btrfs only)')) - mark_bootable = str(_('Mark/Unmark a partition as bootable (automatic for /boot)')) - set_filesystem_partition = str(_('Set desired filesystem for a partition')) - set_btrfs_subvolumes = str(_('Set desired subvolumes on a btrfs partition')) - save_and_exit = str(_('Save and exit')) - cancel = str(_('Cancel')) - - while True: - modes = [new_partition, suggest_partition_layout] - - if len(block_device_struct['partitions']) > 0: - modes += [ - delete_partition, - delete_all_partitions, - assign_mount_point, - mark_formatted, - mark_bootable, - mark_compressed, - set_filesystem_partition, - ] - - indexes = _get_partitions( - block_device_struct["partitions"], - filter_=lambda x: True if x.get('filesystem', {}).get('format') == 'btrfs' else False - ) - - if len(indexes) > 0: - modes += [set_btrfs_subvolumes] - - title = _('Select what to do with\n{}').format(block_device) - - # show current partition layout: - if len(block_device_struct["partitions"]): - title += current_partition_layout(block_device_struct['partitions']) + '\n' - - modes += [save_and_exit, cancel] - - task = Menu(title, modes, sort=False, skip=False).run() - task = task.value - - if task == cancel: - return original_layout - elif task == save_and_exit: - break - - if task == new_partition: - from ..disk import valid_parted_position - - # if partition_type == 'gpt': - # # https://www.gnu.org/software/parted/manual/html_node/mkpart.html - # # https://www.gnu.org/software/parted/manual/html_node/mklabel.html - # name = input("Enter a desired name for the partition: ").strip() - - fs_choice = Menu(_('Enter a desired filesystem type for the partition'), fs_types()).run() - - if fs_choice.type_ == MenuSelectionType.Skip: - continue - - prompt = str(_('Enter the start location (in parted units: s, GB, %, etc. ; default: {}): ')).format( - block_device.first_free_sector - ) - start = input(prompt).strip() - - if not start.strip(): - start = block_device.first_free_sector - end_suggested = block_device.first_end_sector - else: - end_suggested = '100%' - - prompt = str(_('Enter the end location (in parted units: s, GB, %, etc. ; ex: {}): ')).format( - end_suggested - ) - end = input(prompt).strip() - - if not end.strip(): - end = end_suggested - - if valid_parted_position(start) and valid_parted_position(end): - if partition_overlap(block_device_struct["partitions"], start, end): - log(f"This partition overlaps with other partitions on the drive! Ignoring this partition creation.", - fg="red") - continue - - block_device_struct["partitions"].append({ - "type": "primary", # Strictly only allowed under MS-DOS, but GPT accepts it so it's "safe" to inject - "start": start, - "size": end, - "mountpoint": None, - "wipe": True, - "filesystem": { - "format": fs_choice.value - } - }) - else: - log(f"Invalid start ({valid_parted_position(start)}) or end ({valid_parted_position(end)}) for this partition. Ignoring this partition creation.", - fg="red") - continue - elif task == suggest_partition_layout: - from ..disk import suggest_single_disk_layout - - if len(block_device_struct["partitions"]): - prompt = _('{}\ncontains queued partitions, this will remove those, are you sure?').format(block_device) - choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), skip=False).run() - - if choice.value == Menu.no(): - continue - - block_device_struct.update(suggest_single_disk_layout(block_device)[block_device.path]) - else: - current_layout = current_partition_layout(block_device_struct['partitions'], with_idx=True) - - if task == delete_partition: - title = _('{}\n\nSelect by index which partitions to delete').format(current_layout) - to_delete = select_partition(title, block_device_struct["partitions"], multiple=True) - - if to_delete: - block_device_struct['partitions'] = [ - p for idx, p in enumerate(block_device_struct['partitions']) if idx not in to_delete - ] - elif task == mark_compressed: - title = _('{}\n\nSelect which partition to mark as bootable').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - if "filesystem" not in block_device_struct["partitions"][partition]: - block_device_struct["partitions"][partition]["filesystem"] = {} - if "mount_options" not in block_device_struct["partitions"][partition]["filesystem"]: - block_device_struct["partitions"][partition]["filesystem"]["mount_options"] = [] - - if "compress=zstd" not in block_device_struct["partitions"][partition]["filesystem"]["mount_options"]: - block_device_struct["partitions"][partition]["filesystem"]["mount_options"].append("compress=zstd") - elif task == delete_all_partitions: - block_device_struct["partitions"] = [] - block_device_struct["wipe"] = True - elif task == assign_mount_point: - title = _('{}\n\nSelect by index which partition to mount where').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - print(_(' * 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() - - if len(mountpoint): - block_device_struct["partitions"][partition]['mountpoint'] = mountpoint - if mountpoint == '/boot': - log(f"Marked partition as bootable because mountpoint was set to /boot.", fg="yellow") - block_device_struct["partitions"][partition]['boot'] = True - else: - del (block_device_struct["partitions"][partition]['mountpoint']) - - elif task == mark_formatted: - title = _('{}\n\nSelect which partition to mask for formatting').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - # If we mark a partition for formatting, but the format is CRYPTO LUKS, there's no point in formatting it really - # without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set, - # it's safe to change the filesystem for this partition. - if block_device_struct["partitions"][partition].get('filesystem',{}).get('format', 'crypto_LUKS') == 'crypto_LUKS': - if not block_device_struct["partitions"][partition].get('filesystem', None): - block_device_struct["partitions"][partition]['filesystem'] = {} - - fs_choice = Menu(_('Enter a desired filesystem type for the partition'), fs_types()).run() - - if fs_choice.type_ == MenuSelectionType.Selection: - block_device_struct["partitions"][partition]['filesystem']['format'] = fs_choice.value - - # Negate the current wipe marking - block_device_struct["partitions"][partition]['wipe'] = not block_device_struct["partitions"][partition].get('wipe', False) - - elif task == mark_bootable: - title = _('{}\n\nSelect which partition to mark as bootable').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - block_device_struct["partitions"][partition]['boot'] = \ - not block_device_struct["partitions"][partition].get('boot', False) - - elif task == set_filesystem_partition: - title = _('{}\n\nSelect which partition to set a filesystem on').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"]) - - if partition is not None: - if not block_device_struct["partitions"][partition].get('filesystem', None): - block_device_struct["partitions"][partition]['filesystem'] = {} - - fstype_title = _('Enter a desired filesystem type for the partition: ') - fs_choice = Menu(fstype_title, fs_types()).run() - - if fs_choice.type_ == MenuSelectionType.Selection: - block_device_struct["partitions"][partition]['filesystem']['format'] = fs_choice.value - - elif task == set_btrfs_subvolumes: - from .subvolume_config import SubvolumeList - - # TODO get preexisting partitions - title = _('{}\n\nSelect which partition to set subvolumes on').format(current_layout) - partition = select_partition(title, block_device_struct["partitions"],filter_=lambda x:True if x.get('filesystem',{}).get('format') == 'btrfs' else False) - - if partition is not None: - if not block_device_struct["partitions"][partition].get('btrfs', {}): - block_device_struct["partitions"][partition]['btrfs'] = {} - if not block_device_struct["partitions"][partition]['btrfs'].get('subvolumes', []): - block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = [] - - prev = block_device_struct["partitions"][partition]['btrfs']['subvolumes'] - result = SubvolumeList(_("Manage btrfs subvolumes for current partition"), prev).run() - block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = result - - return block_device_struct diff --git a/archinstall/lib/user_interaction/save_conf.py b/archinstall/lib/user_interaction/save_conf.py index 5b4ae2b3..e05b9afe 100644 --- a/archinstall/lib/user_interaction/save_conf.py +++ b/archinstall/lib/user_interaction/save_conf.py @@ -5,38 +5,30 @@ import logging from pathlib import Path from typing import Any, Dict, TYPE_CHECKING -from ..configuration import ConfigurationOutput from ..general import SysCommand from ..menu import Menu from ..menu.menu import MenuSelectionType from ..output import log +from ..configuration import ConfigurationOutput if TYPE_CHECKING: _: Any def save_config(config: Dict): - def preview(selection: str): if options['user_config'] == selection: - json_config = config_output.user_config_to_json() - return f'{config_output.user_configuration_file}\n{json_config}' + serialized = config_output.user_config_to_json() + return f'{config_output.user_configuration_file}\n{serialized}' elif options['user_creds'] == selection: - if json_config := config_output.user_credentials_to_json(): - return f'{config_output.user_credentials_file}\n{json_config}' - else: - return str(_('No configuration')) - elif options['disk_layout'] == selection: - if json_config := config_output.disk_layout_to_json(): - return f'{config_output.disk_layout_file}\n{json_config}' + if maybe_serial := config_output.user_credentials_to_json(): + return f'{config_output.user_credentials_file}\n{maybe_serial}' else: return str(_('No configuration')) elif options['all'] == selection: output = f'{config_output.user_configuration_file}\n' - if json_config := config_output.user_credentials_to_json(): + if config_output.user_credentials_to_json(): output += f'{config_output.user_credentials_file}\n' - if json_config := config_output.disk_layout_to_json(): - output += f'{config_output.disk_layout_file}\n' return output[:-1] return None @@ -61,6 +53,9 @@ def save_config(config: Dict): if choice.type_ == MenuSelectionType.Skip: return + save_config_value = choice.single_value + saving_key = [k for k, v in options.items() if v == save_config_value][0] + dirs_to_exclude = [ '/bin', '/dev', @@ -76,19 +71,19 @@ def save_config(config: Dict): '/usr', '/var', ] - log( - _('When picking a directory to save configuration files to,' - ' by default we will ignore the following folders: ') + ','.join(dirs_to_exclude), - level=logging.DEBUG - ) + log('Ignore configuration option folders: ' + ','.join(dirs_to_exclude), level=logging.DEBUG) log(_('Finding possible directories to save configuration files ...'), level=logging.INFO) - + find_exclude = '-path ' + ' -prune -o -path '.join(dirs_to_exclude) + ' -prune ' file_picker_command = f'find / {find_exclude} -o -type d -print0' - possible_save_dirs = list( - filter(None, SysCommand(file_picker_command).decode().split('\x00')) - ) + + directories = SysCommand(file_picker_command).decode() + + if directories is None: + raise ValueError('Failed to retrieve possible configuration directories') + + possible_save_dirs = list(filter(None, directories.split('\x00'))) selection = Menu( _('Select directory (or directories) for saving configuration files'), @@ -101,35 +96,18 @@ def save_config(config: Dict): match selection.type_: case MenuSelectionType.Skip: return - case _: - save_dirs = selection.value - - prompt = _('Do you want to save {} configuration file(s) in the following locations?\n\n{}').format( - list(options.keys())[list(options.values()).index(choice.value)], - save_dirs - ) - save_confirmation = Menu(prompt, Menu.yes_no(), default_option=Menu.yes()).run() - if save_confirmation == Menu.no(): - return - - log( - _('Saving {} configuration files to {}').format( - list(options.keys())[list(options.values()).index(choice.value)], - save_dirs - ), - level=logging.DEBUG - ) - + + save_dirs = selection.multi_value + + log(f'Saving {saving_key} configuration files to {save_dirs}', level=logging.DEBUG) + if save_dirs is not None: for save_dir_str in save_dirs: save_dir = Path(save_dir_str) - if options['user_config'] == choice.value: + if options['user_config'] == save_config_value: config_output.save_user_config(save_dir) - elif options['user_creds'] == choice.value: + elif options['user_creds'] == save_config_value: config_output.save_user_creds(save_dir) - elif options['disk_layout'] == choice.value: - config_output.save_disk_layout(save_dir) - elif options['all'] == choice.value: + elif options['all'] == save_config_value: config_output.save_user_config(save_dir) config_output.save_user_creds(save_dir) - config_output.save_disk_layout(save_dir) diff --git a/archinstall/lib/user_interaction/subvolume_config.py b/archinstall/lib/user_interaction/subvolume_config.py deleted file mode 100644 index 94150dee..00000000 --- a/archinstall/lib/user_interaction/subvolume_config.py +++ /dev/null @@ -1,98 +0,0 @@ -from typing import Dict, List, Optional, Any, TYPE_CHECKING - -from ..menu.list_manager import ListManager -from ..menu.menu import MenuSelectionType -from ..menu.text_input import TextInput -from ..menu import Menu -from ..models.subvolume import Subvolume -from ... import FormattedOutput - -if TYPE_CHECKING: - _: Any - - -class SubvolumeList(ListManager): - def __init__(self, prompt: str, subvolumes: List[Subvolume]): - self._actions = [ - str(_('Add subvolume')), - str(_('Edit subvolume')), - str(_('Delete subvolume')) - ] - super().__init__(prompt, subvolumes, [self._actions[0]], self._actions[1:]) - - def reformat(self, data: List[Subvolume]) -> Dict[str, Optional[Subvolume]]: - table = FormattedOutput.as_table(data) - rows = table.split('\n') - - # these are the header rows of the table and do not map to any User obviously - # we're adding 2 spaces as prefix because the menu selector '> ' will be put before - # the selectable rows so the header has to be aligned - display_data: Dict[str, Optional[Subvolume]] = {f' {rows[0]}': None, f' {rows[1]}': None} - - for row, subvol in zip(rows[2:], data): - row = row.replace('|', '\\|') - display_data[row] = subvol - - return display_data - - def selected_action_display(self, subvolume: Subvolume) -> str: - return subvolume.name - - def _prompt_options(self, editing: Optional[Subvolume] = None) -> List[str]: - preset_options = [] - if editing: - preset_options = editing.options - - choice = Menu( - str(_("Select the desired subvolume options ")), - ['nodatacow','compress'], - skip=True, - preset_values=preset_options, - multi=True - ).run() - - if choice.type_ == MenuSelectionType.Selection: - return choice.value # type: ignore - - return [] - - def _add_subvolume(self, editing: Optional[Subvolume] = None) -> Optional[Subvolume]: - name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run() - - if not name: - return None - - mountpoint = TextInput(f'\n{_("Subvolume mountpoint")}: ', editing.mountpoint if editing else '').run() - - if not mountpoint: - return None - - options = self._prompt_options(editing) - - subvolume = Subvolume(name, mountpoint) - subvolume.compress = 'compress' in options - subvolume.nodatacow = 'nodatacow' in options - - return subvolume - - def handle_action(self, action: str, entry: Optional[Subvolume], data: List[Subvolume]) -> List[Subvolume]: - if action == self._actions[0]: # add - new_subvolume = self._add_subvolume() - - if new_subvolume is not None: - # in case a user with the same username as an existing user - # was created we'll replace the existing one - data = [d for d in data if d.name != new_subvolume.name] - data += [new_subvolume] - elif entry is not None: - if action == self._actions[1]: # edit subvolume - new_subvolume = self._add_subvolume(entry) - - if new_subvolume is not None: - # we'll remove the original subvolume and add the modified version - data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name] - data += [new_subvolume] - elif action == self._actions[2]: # delete - data = [d for d in data if d != entry] - - return data diff --git a/archinstall/lib/user_interaction/system_conf.py b/archinstall/lib/user_interaction/system_conf.py index e1581677..3f57d0e7 100644 --- a/archinstall/lib/user_interaction/system_conf.py +++ b/archinstall/lib/user_interaction/system_conf.py @@ -1,19 +1,16 @@ from __future__ import annotations -from typing import List, Any, Dict, TYPE_CHECKING +from typing import List, Any, Dict, TYPE_CHECKING, Optional -from ..disk import all_blockdevices -from ..exceptions import RequirementError from ..hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics -from ..menu import Menu -from ..menu.menu import MenuSelectionType -from ..storage import storage +from ..menu import MenuSelectionType, Menu +from ..models.bootloader import Bootloader if TYPE_CHECKING: _: Any -def select_kernel(preset: List[str] = None) -> List[str]: +def select_kernel(preset: List[str] = []) -> List[str]: """ Asks the user to select a kernel for system. @@ -39,39 +36,36 @@ def select_kernel(preset: List[str] = None) -> List[str]: match choice.type_: case MenuSelectionType.Skip: return preset case MenuSelectionType.Reset: return [] - case MenuSelectionType.Selection: return choice.value + case MenuSelectionType.Selection: return choice.value # type: ignore -def select_harddrives(preset: List[str] = []) -> List[str]: - """ - Asks the user to select one or multiple hard drives - - :return: List of selected hard drives - :rtype: list - """ - hard_drives = all_blockdevices(partitions=False).values() - options = {f'{option}': option for option in hard_drives} - - title = str(_('Select one or more hard drives to use and configure\n')) - title += str(_('Any modifications to the existing setting will reset the disk layout!')) +def ask_for_bootloader(preset: Bootloader) -> Bootloader: + # when the system only supports grub + if not has_uefi(): + options = [Bootloader.Grub.value] + default = Bootloader.Grub.value + else: + options = Bootloader.values() + default = Bootloader.Systemd.value - warning = str(_('If you reset the harddrive selection this will also reset the current disk layout. Are you sure?')) + preset_value = preset.value if preset else None - selected_harddrive = Menu( - title, - list(options.keys()), - multi=True, - allow_reset=True, - allow_reset_warning_msg=warning + choice = Menu( + _('Choose a bootloader'), + options, + preset_values=preset_value, + sort=False, + default_option=default ).run() - match selected_harddrive.type_: - case MenuSelectionType.Reset: return [] + match choice.type_: case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: return [options[i] for i in selected_harddrive.value] + case MenuSelectionType.Selection: return Bootloader(choice.value) + + return preset -def select_driver(options: Dict[str, Any] = AVAILABLE_GFX_DRIVERS) -> str: +def select_driver(options: Dict[str, Any] = {}, current_value: Optional[str] = None) -> Optional[str]: """ Some what convoluted function, whose job is simple. Select a graphics driver from a pre-defined set of popular options. @@ -80,78 +74,31 @@ def select_driver(options: Dict[str, Any] = AVAILABLE_GFX_DRIVERS) -> str: there for appeal to the general public first and edge cases later) """ - drivers = sorted(list(options)) + if not options: + options = AVAILABLE_GFX_DRIVERS + + drivers = sorted(list(options.keys())) if drivers: - arguments = storage.get('arguments', {}) title = '' - if has_amd_graphics(): - title += str(_( - 'For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.' - )) + '\n' + title += str(_('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.')) + '\n' if has_intel_graphics(): - title += str(_( - 'For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n' - )) + title += str(_('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(): - title += str(_( - 'For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n' - )) + title += str(_('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')) - title += str(_('\n\nSelect a graphics driver or leave blank to install all open-source drivers')) - choice = Menu(title, drivers).run() + title += str(_('\nSelect a graphics driver or leave blank to install all open-source drivers')) - if choice.type_ != MenuSelectionType.Selection: - return arguments.get('gfx_driver') + preset = current_value if current_value else None + choice = Menu(title, drivers, preset_values=preset).run() - 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.") + if choice.type_ != MenuSelectionType.Selection: + return None + return choice.value # type: ignore -def ask_for_bootloader(advanced_options: bool = False, preset: str = None) -> str: - if preset == 'systemd-bootctl': - preset_val = 'systemd-boot' if advanced_options else Menu.no() - elif preset == 'grub-install': - preset_val = 'grub' if advanced_options else Menu.yes() - else: - preset_val = preset - - bootloader = "systemd-bootctl" if has_uefi() else "grub-install" - - if has_uefi(): - if not advanced_options: - selection = Menu( - _('Would you like to use GRUB as a bootloader instead of systemd-boot?'), - Menu.yes_no(), - preset_values=preset_val, - default_option=Menu.no() - ).run() - - match selection.type_: - case MenuSelectionType.Skip: return preset - case MenuSelectionType.Selection: bootloader = 'grub-install' if selection.value == Menu.yes() else bootloader - 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, preset_values=preset_val).run() - - value = '' - match selection.type_: - case MenuSelectionType.Skip: value = preset_val - case MenuSelectionType.Selection: value = selection.value - - if value != "": - if value == 'systemd-boot': - bootloader = 'systemd-bootctl' - elif value == 'grub': - bootloader = 'grub-install' - else: - bootloader = value - - return bootloader + return current_value def ask_for_swap(preset: bool = True) -> bool: @@ -166,3 +113,5 @@ def ask_for_swap(preset: bool = True) -> bool: match choice.type_: case MenuSelectionType.Skip: return preset case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True + + return preset diff --git a/archinstall/lib/user_interaction/utils.py b/archinstall/lib/user_interaction/utils.py index 7ee6fc07..918945c0 100644 --- a/archinstall/lib/user_interaction/utils.py +++ b/archinstall/lib/user_interaction/utils.py @@ -1,13 +1,9 @@ from __future__ import annotations import getpass -import signal -import sys -import time from typing import Any, Optional, TYPE_CHECKING -from ..menu import Menu -from ..models.password_strength import PasswordStrength +from ..models import PasswordStrength from ..output import log if TYPE_CHECKING: @@ -36,44 +32,3 @@ def get_password(prompt: str = '') -> Optional[str]: return password return None - - -def do_countdown() -> bool: - SIG_TRIGGER = False - - def kill_handler(sig: int, frame: Any) -> None: - print() - exit(0) - - def sig_handler(sig: int, frame: Any) -> None: - global SIG_TRIGGER - SIG_TRIGGER = True - signal.signal(signal.SIGINT, kill_handler) - - original_sigint_handler = signal.getsignal(signal.SIGINT) - signal.signal(signal.SIGINT, sig_handler) - - for i in range(5, 0, -1): - print(f"{i}", end='') - - for x in range(4): - sys.stdout.flush() - time.sleep(0.25) - print(".", end='') - - if SIG_TRIGGER: - prompt = _('Do you really want to abort?') - choice = Menu(prompt, Menu.yes_no(), skip=False).run() - if choice.value == Menu.yes(): - exit(0) - - if SIG_TRIGGER is False: - sys.stdin.read() - - SIG_TRIGGER = False - signal.signal(signal.SIGINT, sig_handler) - - print() - signal.signal(signal.SIGINT, original_sigint_handler) - - return True |