From c5bea04c05c50de07cde701f98693e97679ce965 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sat, 21 Mar 2026 14:55:01 +1100 Subject: [PATCH 1/3] Refactor interactive module --- archinstall/lib/disk/disk_menu.py | 605 ++++++++++++++++- archinstall/lib/disk/partitioning_menu.py | 2 +- archinstall/lib/global_menu.py | 6 +- archinstall/lib/interactions/__init__.py | 18 +- archinstall/lib/interactions/disk_conf.py | 611 ------------------ .../{general_conf.py => general_menu.py} | 0 .../{system_conf.py => system_menu.py} | 0 archinstall/lib/profile/profile_menu.py | 2 +- archinstall/scripts/guided.py | 2 +- 9 files changed, 610 insertions(+), 636 deletions(-) delete mode 100644 archinstall/lib/interactions/disk_conf.py rename archinstall/lib/interactions/{general_conf.py => general_menu.py} (100%) rename archinstall/lib/interactions/{system_conf.py => system_menu.py} (100%) diff --git a/archinstall/lib/disk/disk_menu.py b/archinstall/lib/disk/disk_menu.py index 2ca8925a7e..35f113319b 100644 --- a/archinstall/lib/disk/disk_menu.py +++ b/archinstall/lib/disk/disk_menu.py @@ -1,22 +1,42 @@ from dataclasses import dataclass +from pathlib import Path from typing import override +from archinstall.lib.disk.device_handler import device_handler from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu -from archinstall.lib.interactions.disk_conf import select_disk_config, select_lvm_config +from archinstall.lib.disk.partitioning_menu import manual_partitioning from archinstall.lib.menu.abstract_menu import AbstractSubMenu -from archinstall.lib.menu.helpers import Selection +from archinstall.lib.menu.helpers import Confirmation, Notify, Selection, Table +from archinstall.lib.menu.util import prompt_dir from archinstall.lib.models.device import ( DEFAULT_ITER_TIME, + BDevice, + BtrfsMountOption, BtrfsOptions, + DeviceModification, DiskEncryption, DiskLayoutConfiguration, DiskLayoutType, EncryptionType, + FilesystemType, LvmConfiguration, + LvmLayoutType, + LvmVolume, + LvmVolumeGroup, + LvmVolumeStatus, + ModificationStatus, + PartitionFlag, + PartitionModification, + PartitionType, + SectorSize, + Size, SnapshotConfig, SnapshotType, + SubvolumeModification, + Unit, + _DeviceInfo, ) -from archinstall.lib.output import FormattedOutput +from archinstall.lib.output import FormattedOutput, debug from archinstall.lib.translationhandler import tr from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType @@ -275,3 +295,582 @@ def _prev_disk_encryption(self, item: MenuItem) -> str | None: return output return None + + +async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | None: + def _preview_device_selection(item: MenuItem) -> str | None: + device: _DeviceInfo = item.value # type: ignore[assignment] + dev = device_handler.get_device(device.path) + + if dev and dev.partition_infos: + return FormattedOutput.as_table(dev.partition_infos) + return None + + if preset is None: + preset = [] + + devices = device_handler.devices + + items = [ + MenuItem( + str(d.device_info.path), + d.device_info, + preview_action=_preview_device_selection, + ) + for d in devices + ] + + presets = [p.device_info for p in preset] + + group = MenuItemGroup(items) + group.set_selected_by_value(presets) + + result = await Table[_DeviceInfo]( + header=tr('Select disks for the installation'), + group=group, + presets=presets, + allow_skip=True, + multi=True, + preview_location='bottom', + preview_header=tr('Partitions'), + ).show() + + match result.type_: + case ResultType.Reset: + return None + case ResultType.Skip: + return None + case ResultType.Selection: + selected_device_info = result.get_values() + selected_devices = [] + + for device in devices: + if device.device_info in selected_device_info: + selected_devices.append(device) + + return selected_devices + + +async def get_default_partition_layout( + devices: list[BDevice], + filesystem_type: FilesystemType | None = None, +) -> list[DeviceModification]: + if len(devices) == 1: + device_modification = await suggest_single_disk_layout( + devices[0], + filesystem_type=filesystem_type, + ) + return [device_modification] + else: + return await suggest_multi_disk_layout( + devices, + filesystem_type=filesystem_type, + ) + + +async def _manual_partitioning( + preset: list[DeviceModification], + devices: list[BDevice], +) -> list[DeviceModification] | None: + modifications: list[DeviceModification] = [] + + for device in devices: + mod = next(filter(lambda x: x.device == device, preset), None) + if not mod: + mod = DeviceModification(device, wipe=False) + + device_mod = await manual_partitioning(mod, device_handler.partition_table) + + if not device_mod: + return None + + modifications.append(device_mod) + + return modifications + + +async def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLayoutConfiguration | None: + default_layout = DiskLayoutType.Default.display_msg() + manual_mode = DiskLayoutType.Manual.display_msg() + pre_mount_mode = DiskLayoutType.Pre_mount.display_msg() + + items = [ + MenuItem(default_layout, value=default_layout), + MenuItem(manual_mode, value=manual_mode), + MenuItem(pre_mount_mode, value=pre_mount_mode), + ] + group = MenuItemGroup(items, sort_items=False) + + if preset: + group.set_selected_by_value(preset.config_type.display_msg()) + + result = await Selection[str]( + group, + header=tr('Select a disk configuration'), + allow_skip=True, + allow_reset=True, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Reset: + return None + case ResultType.Selection: + selection = result.get_value() + + if selection == pre_mount_mode: + output = tr('Enter root mount directory') + '\n\n' + output += tr('You will use whatever drive-setup is mounted at the specified directory') + '\n' + output += tr("WARNING: Archinstall won't check the suitability of this setup") + + path = await prompt_dir(output, allow_skip=True) + + if path is None: + return None + + mods = device_handler.detect_pre_mounted_mods(path) + + return DiskLayoutConfiguration( + config_type=DiskLayoutType.Pre_mount, + device_modifications=mods, + mountpoint=path, + ) + + preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] + devices = await select_devices(preset_devices) + + if devices is None: + return preset + + if result.get_value() == default_layout: + modifications = await get_default_partition_layout(devices) + if modifications: + return DiskLayoutConfiguration( + config_type=DiskLayoutType.Default, + device_modifications=modifications, + ) + elif result.get_value() == manual_mode: + preset_mods = preset.device_modifications if preset else [] + partitions = await _manual_partitioning(preset_mods, devices) + + if not partitions: + return preset + + return DiskLayoutConfiguration( + config_type=DiskLayoutType.Manual, + device_modifications=partitions, + ) + + return None + + +async def select_lvm_config( + disk_config: DiskLayoutConfiguration, + preset: LvmConfiguration | None = None, +) -> LvmConfiguration | None: + preset_value = preset.config_type.display_msg() if preset else None + default_mode = LvmLayoutType.Default.display_msg() + + items = [MenuItem(default_mode, value=default_mode)] + group = MenuItemGroup(items) + group.set_focus_by_value(preset_value) + + result = await Selection[str]( + group, + allow_reset=True, + allow_skip=True, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Reset: + return None + case ResultType.Selection: + if result.get_value() == default_mode: + return await suggest_lvm_layout(disk_config) + + return None + + +def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModification: + flags = [PartitionFlag.BOOT] + size = Size(1, Unit.GiB, sector_size) + start = Size(1, Unit.MiB, sector_size) + if using_gpt: + flags.append(PartitionFlag.ESP) + + # boot partition + return PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, + start=start, + length=size, + mountpoint=Path('/boot'), + fs_type=FilesystemType.Fat32, + flags=flags, + ) + + +async def select_main_filesystem_format() -> FilesystemType: + items = [ + MenuItem('btrfs', value=FilesystemType.Btrfs), + MenuItem('ext4', value=FilesystemType.Ext4), + MenuItem('xfs', value=FilesystemType.Xfs), + MenuItem('f2fs', value=FilesystemType.F2fs), + ] + + group = MenuItemGroup(items, sort_items=False) + result = await Selection[FilesystemType]( + group, + header=tr('Select main filesystem'), + allow_skip=False, + ).show() + + match result.type_: + case ResultType.Selection: + return result.get_value() + case _: + raise ValueError('Unhandled result type') + + +async def select_mount_options() -> list[str]: + prompt = tr('Would you like to use compression or disable CoW?') + '\n' + compression = tr('Use compression') + disable_cow = tr('Disable Copy-on-Write') + + items = [ + MenuItem(compression, value=BtrfsMountOption.compress.value), + MenuItem(disable_cow, value=BtrfsMountOption.nodatacow.value), + ] + group = MenuItemGroup(items, sort_items=False) + + result = await Selection[str]( + group, + header=prompt, + allow_skip=True, + ).show() + + match result.type_: + case ResultType.Skip: + return [] + case ResultType.Selection: + return [result.get_value()] + case _: + raise ValueError('Unhandled result type') + + +def process_root_partition_size(total_size: Size, sector_size: SectorSize) -> Size: + # root partition size processing + total_device_size = total_size.convert(Unit.GiB) + if total_device_size.value > 500: + # maximum size + return Size(value=50, unit=Unit.GiB, sector_size=sector_size) + elif total_device_size.value < 320: + # minimum size + return Size(value=32, unit=Unit.GiB, sector_size=sector_size) + else: + # 10% of total size + length = total_device_size.value // 10 + return Size(value=length, unit=Unit.GiB, sector_size=sector_size) + + +def get_default_btrfs_subvols() -> list[SubvolumeModification]: + # 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 + return [ + SubvolumeModification(Path('@'), Path('/')), + SubvolumeModification(Path('@home'), Path('/home')), + SubvolumeModification(Path('@log'), Path('/var/log')), + SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), + ] + + +async def suggest_single_disk_layout( + device: BDevice, + filesystem_type: FilesystemType | None = None, + separate_home: bool | None = None, +) -> DeviceModification: + if not filesystem_type: + filesystem_type = await select_main_filesystem_format() + + sector_size = device.device_info.sector_size + total_size = device.device_info.total_size + available_space = total_size + min_size_to_allow_home_part = Size(64, Unit.GiB, sector_size) + + if filesystem_type == FilesystemType.Btrfs: + prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n' + + result = await Confirmation( + header=prompt, + allow_skip=False, + preset=True, + ).show() + + using_subvolumes = result.item() == MenuItem.yes() + mount_options = await select_mount_options() + else: + using_subvolumes = False + mount_options = [] + + device_modification = DeviceModification(device, wipe=True) + + using_gpt = device_handler.partition_table.is_gpt() + + if using_gpt: + available_space = available_space.gpt_end() + + available_space = available_space.align() + + # Used for reference: https://wiki.archlinux.org/title/partitioning + + boot_partition = _boot_partition(sector_size, using_gpt) + device_modification.add_partition(boot_partition) + + if separate_home is False or using_subvolumes or total_size < min_size_to_allow_home_part: + using_home_partition = False + elif separate_home: + using_home_partition = True + else: + prompt = tr('Would you like to create a separate partition for /home?') + '\n' + + result = await Confirmation( + header=prompt, + allow_skip=False, + preset=True, + ).show() + + using_home_partition = result.item() == MenuItem.yes() + + # root partition + root_start = boot_partition.start + boot_partition.length + + # Set a size for / (/root) + if using_home_partition: + root_length = process_root_partition_size(total_size, sector_size) + else: + root_length = available_space - root_start + + root_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, + start=root_start, + length=root_length, + mountpoint=Path('/') if not using_subvolumes else None, + fs_type=filesystem_type, + mount_options=mount_options, + ) + + device_modification.add_partition(root_partition) + + if using_subvolumes: + root_partition.btrfs_subvols = get_default_btrfs_subvols() + elif using_home_partition: + # If we don't want to use subvolumes, + # But we want to be able to reuse data between re-installs.. + # A second partition for /home would be nice if we have the space for it + home_start = root_partition.start + root_partition.length + home_length = available_space - home_start + + flags = [] + if using_gpt: + flags.append(PartitionFlag.LINUX_HOME) + + home_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, + start=home_start, + length=home_length, + mountpoint=Path('/home'), + fs_type=filesystem_type, + mount_options=mount_options, + flags=flags, + ) + device_modification.add_partition(home_partition) + + return device_modification + + +async def suggest_multi_disk_layout( + devices: list[BDevice], + filesystem_type: FilesystemType | None = None, +) -> list[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 = Size(40, Unit.GiB, SectorSize.default()) + # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? + desired_root_partition_size = Size(32, Unit.GiB, SectorSize.default()) + mount_options = [] + + if not filesystem_type: + filesystem_type = await select_main_filesystem_format() + + # find proper disk for /home + possible_devices = [d for d in devices if d.device_info.total_size >= min_home_partition_size] + 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[BDevice, Size]] = sorted(devices_delta.items(), key=lambda x: x[1]) + root_device: BDevice | None = sorted_delta[0][0] + + if home_device is None or root_device is None: + text = tr('The selected drives do not have the minimum capacity required for an automatic suggestion\n') + text += tr('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(Unit.GiB)) + text += tr('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(Unit.GiB)) + + _ = await Notify(text).show() + return [] + + if filesystem_type == FilesystemType.Btrfs: + mount_options = await select_mount_options() + + device_paths = ', '.join(str(d.device_info.path) for d in devices) + + debug(f'Suggesting multi-disk-layout for devices: {device_paths}') + debug(f'/root: {root_device.device_info.path}') + debug(f'/home: {home_device.device_info.path}') + + root_device_modification = DeviceModification(root_device, wipe=True) + home_device_modification = DeviceModification(home_device, wipe=True) + + root_device_sector_size = root_device_modification.device.device_info.sector_size + home_device_sector_size = home_device_modification.device.device_info.sector_size + + using_gpt = device_handler.partition_table.is_gpt() + + # add boot partition to the root device + boot_partition = _boot_partition(root_device_sector_size, using_gpt) + root_device_modification.add_partition(boot_partition) + + root_start = boot_partition.start + boot_partition.length + root_length = root_device.device_info.total_size - root_start + + if using_gpt: + root_length = root_length.gpt_end() + + root_length = root_length.align() + + # add root partition to the root device + root_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, + start=root_start, + length=root_length, + mountpoint=Path('/'), + mount_options=mount_options, + fs_type=filesystem_type, + ) + root_device_modification.add_partition(root_partition) + + home_start = Size(1, Unit.MiB, home_device_sector_size) + home_length = home_device.device_info.total_size - home_start + + flags = [] + if using_gpt: + home_length = home_length.gpt_end() + flags.append(PartitionFlag.LINUX_HOME) + + home_length = home_length.align() + + # add home partition to home device + home_partition = PartitionModification( + status=ModificationStatus.Create, + type=PartitionType.Primary, + start=home_start, + length=home_length, + mountpoint=Path('/home'), + mount_options=mount_options, + fs_type=filesystem_type, + flags=flags, + ) + home_device_modification.add_partition(home_partition) + + return [root_device_modification, home_device_modification] + + +async def suggest_lvm_layout( + disk_config: DiskLayoutConfiguration, + filesystem_type: FilesystemType | None = None, + vg_grp_name: str = 'ArchinstallVg', +) -> LvmConfiguration: + if disk_config.config_type != DiskLayoutType.Default: + raise ValueError('LVM suggested volumes are only available for default partitioning') + + using_subvolumes = False + btrfs_subvols = [] + home_volume = True + mount_options = [] + + if not filesystem_type: + filesystem_type = await select_main_filesystem_format() + + if filesystem_type == FilesystemType.Btrfs: + prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n' + result = await Confirmation(header=prompt, allow_skip=False, preset=True).show() + + using_subvolumes = MenuItem.yes() == result.item() + mount_options = await select_mount_options() + + if using_subvolumes: + btrfs_subvols = get_default_btrfs_subvols() + home_volume = False + + boot_part: PartitionModification | None = None + other_part: list[PartitionModification] = [] + + for mod in disk_config.device_modifications: + for part in mod.partitions: + if part.is_boot(): + boot_part = part + else: + other_part.append(part) + + if not boot_part: + raise ValueError('Unable to find boot partition in partition modifications') + + total_vol_available = sum( + [p.length for p in other_part], + Size(0, Unit.B, SectorSize.default()), + ) + root_vol_size = process_root_partition_size(total_vol_available, SectorSize.default()) + home_vol_size = total_vol_available - root_vol_size + + lvm_vol_group = LvmVolumeGroup(vg_grp_name, pvs=other_part) + + root_vol = LvmVolume( + status=LvmVolumeStatus.Create, + name='root', + fs_type=filesystem_type, + length=root_vol_size, + mountpoint=Path('/'), + btrfs_subvols=btrfs_subvols, + mount_options=mount_options, + ) + + lvm_vol_group.volumes.append(root_vol) + + if home_volume: + home_vol = LvmVolume( + status=LvmVolumeStatus.Create, + name='home', + fs_type=filesystem_type, + length=home_vol_size, + mountpoint=Path('/home'), + ) + + lvm_vol_group.volumes.append(home_vol) + + return LvmConfiguration(LvmLayoutType.Default, [lvm_vol_group]) diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index 4839626dfc..feb97c6ceb 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -566,7 +566,7 @@ async def _suggest_partition_layout( if not await self._reset_confirmation(): return None - from archinstall.lib.interactions.disk_conf import suggest_single_disk_layout + from archinstall.lib.disk.disk_menu import suggest_single_disk_layout return await suggest_single_disk_layout(self._device) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index bb5a9e326b..393ec7f1cc 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -7,8 +7,8 @@ from archinstall.lib.configuration import save_config from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu from archinstall.lib.hardware import SysInfo -from archinstall.lib.interactions.general_conf import add_number_of_parallel_downloads, select_hostname, select_ntp, select_timezone -from archinstall.lib.interactions.system_conf import select_kernel, select_swap +from archinstall.lib.interactions.general_menu import add_number_of_parallel_downloads, select_hostname, select_ntp, select_timezone +from archinstall.lib.interactions.system_menu import select_kernel, select_swap from archinstall.lib.locale.locale_menu import LocaleMenu from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey from archinstall.lib.mirrors import MirrorListHandler, MirrorMenu @@ -231,7 +231,7 @@ def is_config_valid(self) -> bool: return self._validate_bootloader() is None async def _select_archinstall_language(self, preset: Language) -> Language: - from archinstall.lib.interactions.general_conf import select_archinstall_language + from archinstall.lib.interactions.general_menu import select_archinstall_language language = await select_archinstall_language(translation_handler.translated_languages, preset) translation_handler.activate(language) diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py index 52f308d76f..cdf398be17 100644 --- a/archinstall/lib/interactions/__init__.py +++ b/archinstall/lib/interactions/__init__.py @@ -1,33 +1,19 @@ -from archinstall.lib.interactions.disk_conf import ( - get_default_partition_layout, - select_devices, - select_disk_config, - select_main_filesystem_format, - suggest_multi_disk_layout, - suggest_single_disk_layout, -) -from archinstall.lib.interactions.general_conf import ( +from archinstall.lib.interactions.general_menu import ( add_number_of_parallel_downloads, select_archinstall_language, select_hostname, select_ntp, select_timezone, ) -from archinstall.lib.interactions.system_conf import select_driver, select_kernel, select_swap +from archinstall.lib.interactions.system_menu import select_driver, select_kernel, select_swap __all__ = [ 'add_number_of_parallel_downloads', - 'get_default_partition_layout', 'select_archinstall_language', - 'select_devices', - 'select_disk_config', 'select_driver', 'select_hostname', 'select_kernel', - 'select_main_filesystem_format', 'select_ntp', 'select_swap', 'select_timezone', - 'suggest_multi_disk_layout', - 'suggest_single_disk_layout', ] diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py deleted file mode 100644 index 19f0096a9a..0000000000 --- a/archinstall/lib/interactions/disk_conf.py +++ /dev/null @@ -1,611 +0,0 @@ -from pathlib import Path - -from archinstall.lib.disk.device_handler import device_handler -from archinstall.lib.disk.partitioning_menu import manual_partitioning -from archinstall.lib.menu.helpers import Confirmation, Notify, Selection, Table -from archinstall.lib.menu.util import prompt_dir -from archinstall.lib.models.device import ( - BDevice, - BtrfsMountOption, - DeviceModification, - DiskLayoutConfiguration, - DiskLayoutType, - FilesystemType, - LvmConfiguration, - LvmLayoutType, - LvmVolume, - LvmVolumeGroup, - LvmVolumeStatus, - ModificationStatus, - PartitionFlag, - PartitionModification, - PartitionType, - SectorSize, - Size, - SubvolumeModification, - Unit, - _DeviceInfo, -) -from archinstall.lib.output import FormattedOutput, debug -from archinstall.lib.translationhandler import tr -from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup -from archinstall.tui.ui.result import ResultType - - -async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | None: - def _preview_device_selection(item: MenuItem) -> str | None: - device: _DeviceInfo = item.value # type: ignore[assignment] - dev = device_handler.get_device(device.path) - - if dev and dev.partition_infos: - return FormattedOutput.as_table(dev.partition_infos) - return None - - if preset is None: - preset = [] - - devices = device_handler.devices - - items = [ - MenuItem( - str(d.device_info.path), - d.device_info, - preview_action=_preview_device_selection, - ) - for d in devices - ] - - presets = [p.device_info for p in preset] - - group = MenuItemGroup(items) - group.set_selected_by_value(presets) - - result = await Table[_DeviceInfo]( - header=tr('Select disks for the installation'), - group=group, - presets=presets, - allow_skip=True, - multi=True, - preview_location='bottom', - preview_header=tr('Partitions'), - ).show() - - match result.type_: - case ResultType.Reset: - return None - case ResultType.Skip: - return None - case ResultType.Selection: - selected_device_info = result.get_values() - selected_devices = [] - - for device in devices: - if device.device_info in selected_device_info: - selected_devices.append(device) - - return selected_devices - - -async def get_default_partition_layout( - devices: list[BDevice], - filesystem_type: FilesystemType | None = None, -) -> list[DeviceModification]: - if len(devices) == 1: - device_modification = await suggest_single_disk_layout( - devices[0], - filesystem_type=filesystem_type, - ) - return [device_modification] - else: - return await suggest_multi_disk_layout( - devices, - filesystem_type=filesystem_type, - ) - - -async def _manual_partitioning( - preset: list[DeviceModification], - devices: list[BDevice], -) -> list[DeviceModification] | None: - modifications: list[DeviceModification] = [] - - for device in devices: - mod = next(filter(lambda x: x.device == device, preset), None) - if not mod: - mod = DeviceModification(device, wipe=False) - - device_mod = await manual_partitioning(mod, device_handler.partition_table) - - if not device_mod: - return None - - modifications.append(device_mod) - - return modifications - - -async def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLayoutConfiguration | None: - default_layout = DiskLayoutType.Default.display_msg() - manual_mode = DiskLayoutType.Manual.display_msg() - pre_mount_mode = DiskLayoutType.Pre_mount.display_msg() - - items = [ - MenuItem(default_layout, value=default_layout), - MenuItem(manual_mode, value=manual_mode), - MenuItem(pre_mount_mode, value=pre_mount_mode), - ] - group = MenuItemGroup(items, sort_items=False) - - if preset: - group.set_selected_by_value(preset.config_type.display_msg()) - - result = await Selection[str]( - group, - header=tr('Select a disk configuration'), - allow_skip=True, - allow_reset=True, - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Reset: - return None - case ResultType.Selection: - selection = result.get_value() - - if selection == pre_mount_mode: - output = tr('Enter root mount directory') + '\n\n' - output += tr('You will use whatever drive-setup is mounted at the specified directory') + '\n' - output += tr("WARNING: Archinstall won't check the suitability of this setup") - - path = await prompt_dir(output, allow_skip=True) - - if path is None: - return None - - mods = device_handler.detect_pre_mounted_mods(path) - - return DiskLayoutConfiguration( - config_type=DiskLayoutType.Pre_mount, - device_modifications=mods, - mountpoint=path, - ) - - preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] - devices = await select_devices(preset_devices) - - if devices is None: - return preset - - if result.get_value() == default_layout: - modifications = await get_default_partition_layout(devices) - if modifications: - return DiskLayoutConfiguration( - config_type=DiskLayoutType.Default, - device_modifications=modifications, - ) - elif result.get_value() == manual_mode: - preset_mods = preset.device_modifications if preset else [] - partitions = await _manual_partitioning(preset_mods, devices) - - if not partitions: - return preset - - return DiskLayoutConfiguration( - config_type=DiskLayoutType.Manual, - device_modifications=partitions, - ) - - return None - - -async def select_lvm_config( - disk_config: DiskLayoutConfiguration, - preset: LvmConfiguration | None = None, -) -> LvmConfiguration | None: - preset_value = preset.config_type.display_msg() if preset else None - default_mode = LvmLayoutType.Default.display_msg() - - items = [MenuItem(default_mode, value=default_mode)] - group = MenuItemGroup(items) - group.set_focus_by_value(preset_value) - - result = await Selection[str]( - group, - allow_reset=True, - allow_skip=True, - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Reset: - return None - case ResultType.Selection: - if result.get_value() == default_mode: - return await suggest_lvm_layout(disk_config) - - return None - - -def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModification: - flags = [PartitionFlag.BOOT] - size = Size(1, Unit.GiB, sector_size) - start = Size(1, Unit.MiB, sector_size) - if using_gpt: - flags.append(PartitionFlag.ESP) - - # boot partition - return PartitionModification( - status=ModificationStatus.Create, - type=PartitionType.Primary, - start=start, - length=size, - mountpoint=Path('/boot'), - fs_type=FilesystemType.Fat32, - flags=flags, - ) - - -async def select_main_filesystem_format() -> FilesystemType: - items = [ - MenuItem('btrfs', value=FilesystemType.Btrfs), - MenuItem('ext4', value=FilesystemType.Ext4), - MenuItem('xfs', value=FilesystemType.Xfs), - MenuItem('f2fs', value=FilesystemType.F2fs), - ] - - group = MenuItemGroup(items, sort_items=False) - result = await Selection[FilesystemType]( - group, - header=tr('Select main filesystem'), - allow_skip=False, - ).show() - - match result.type_: - case ResultType.Selection: - return result.get_value() - case _: - raise ValueError('Unhandled result type') - - -async def select_mount_options() -> list[str]: - prompt = tr('Would you like to use compression or disable CoW?') + '\n' - compression = tr('Use compression') - disable_cow = tr('Disable Copy-on-Write') - - items = [ - MenuItem(compression, value=BtrfsMountOption.compress.value), - MenuItem(disable_cow, value=BtrfsMountOption.nodatacow.value), - ] - group = MenuItemGroup(items, sort_items=False) - - result = await Selection[str]( - group, - header=prompt, - allow_skip=True, - ).show() - - match result.type_: - case ResultType.Skip: - return [] - case ResultType.Selection: - return [result.get_value()] - case _: - raise ValueError('Unhandled result type') - - -def process_root_partition_size(total_size: Size, sector_size: SectorSize) -> Size: - # root partition size processing - total_device_size = total_size.convert(Unit.GiB) - if total_device_size.value > 500: - # maximum size - return Size(value=50, unit=Unit.GiB, sector_size=sector_size) - elif total_device_size.value < 320: - # minimum size - return Size(value=32, unit=Unit.GiB, sector_size=sector_size) - else: - # 10% of total size - length = total_device_size.value // 10 - return Size(value=length, unit=Unit.GiB, sector_size=sector_size) - - -def get_default_btrfs_subvols() -> list[SubvolumeModification]: - # 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 - return [ - SubvolumeModification(Path('@'), Path('/')), - SubvolumeModification(Path('@home'), Path('/home')), - SubvolumeModification(Path('@log'), Path('/var/log')), - SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')), - ] - - -async def suggest_single_disk_layout( - device: BDevice, - filesystem_type: FilesystemType | None = None, - separate_home: bool | None = None, -) -> DeviceModification: - if not filesystem_type: - filesystem_type = await select_main_filesystem_format() - - sector_size = device.device_info.sector_size - total_size = device.device_info.total_size - available_space = total_size - min_size_to_allow_home_part = Size(64, Unit.GiB, sector_size) - - if filesystem_type == FilesystemType.Btrfs: - prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n' - - result = await Confirmation( - header=prompt, - allow_skip=False, - preset=True, - ).show() - - using_subvolumes = result.item() == MenuItem.yes() - mount_options = await select_mount_options() - else: - using_subvolumes = False - mount_options = [] - - device_modification = DeviceModification(device, wipe=True) - - using_gpt = device_handler.partition_table.is_gpt() - - if using_gpt: - available_space = available_space.gpt_end() - - available_space = available_space.align() - - # Used for reference: https://wiki.archlinux.org/title/partitioning - - boot_partition = _boot_partition(sector_size, using_gpt) - device_modification.add_partition(boot_partition) - - if separate_home is False or using_subvolumes or total_size < min_size_to_allow_home_part: - using_home_partition = False - elif separate_home: - using_home_partition = True - else: - prompt = tr('Would you like to create a separate partition for /home?') + '\n' - - result = await Confirmation( - header=prompt, - allow_skip=False, - preset=True, - ).show() - - using_home_partition = result.item() == MenuItem.yes() - - # root partition - root_start = boot_partition.start + boot_partition.length - - # Set a size for / (/root) - if using_home_partition: - root_length = process_root_partition_size(total_size, sector_size) - else: - root_length = available_space - root_start - - root_partition = PartitionModification( - status=ModificationStatus.Create, - type=PartitionType.Primary, - start=root_start, - length=root_length, - mountpoint=Path('/') if not using_subvolumes else None, - fs_type=filesystem_type, - mount_options=mount_options, - ) - - device_modification.add_partition(root_partition) - - if using_subvolumes: - root_partition.btrfs_subvols = get_default_btrfs_subvols() - elif using_home_partition: - # If we don't want to use subvolumes, - # But we want to be able to reuse data between re-installs.. - # A second partition for /home would be nice if we have the space for it - home_start = root_partition.start + root_partition.length - home_length = available_space - home_start - - flags = [] - if using_gpt: - flags.append(PartitionFlag.LINUX_HOME) - - home_partition = PartitionModification( - status=ModificationStatus.Create, - type=PartitionType.Primary, - start=home_start, - length=home_length, - mountpoint=Path('/home'), - fs_type=filesystem_type, - mount_options=mount_options, - flags=flags, - ) - device_modification.add_partition(home_partition) - - return device_modification - - -async def suggest_multi_disk_layout( - devices: list[BDevice], - filesystem_type: FilesystemType | None = None, -) -> list[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 = Size(40, Unit.GiB, SectorSize.default()) - # rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - desired_root_partition_size = Size(32, Unit.GiB, SectorSize.default()) - mount_options = [] - - if not filesystem_type: - filesystem_type = await select_main_filesystem_format() - - # find proper disk for /home - possible_devices = [d for d in devices if d.device_info.total_size >= min_home_partition_size] - 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[BDevice, Size]] = sorted(devices_delta.items(), key=lambda x: x[1]) - root_device: BDevice | None = sorted_delta[0][0] - - if home_device is None or root_device is None: - text = tr('The selected drives do not have the minimum capacity required for an automatic suggestion\n') - text += tr('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(Unit.GiB)) - text += tr('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(Unit.GiB)) - - _ = await Notify(text).show() - return [] - - if filesystem_type == FilesystemType.Btrfs: - mount_options = await select_mount_options() - - device_paths = ', '.join(str(d.device_info.path) for d in devices) - - debug(f'Suggesting multi-disk-layout for devices: {device_paths}') - debug(f'/root: {root_device.device_info.path}') - debug(f'/home: {home_device.device_info.path}') - - root_device_modification = DeviceModification(root_device, wipe=True) - home_device_modification = DeviceModification(home_device, wipe=True) - - root_device_sector_size = root_device_modification.device.device_info.sector_size - home_device_sector_size = home_device_modification.device.device_info.sector_size - - using_gpt = device_handler.partition_table.is_gpt() - - # add boot partition to the root device - boot_partition = _boot_partition(root_device_sector_size, using_gpt) - root_device_modification.add_partition(boot_partition) - - root_start = boot_partition.start + boot_partition.length - root_length = root_device.device_info.total_size - root_start - - if using_gpt: - root_length = root_length.gpt_end() - - root_length = root_length.align() - - # add root partition to the root device - root_partition = PartitionModification( - status=ModificationStatus.Create, - type=PartitionType.Primary, - start=root_start, - length=root_length, - mountpoint=Path('/'), - mount_options=mount_options, - fs_type=filesystem_type, - ) - root_device_modification.add_partition(root_partition) - - home_start = Size(1, Unit.MiB, home_device_sector_size) - home_length = home_device.device_info.total_size - home_start - - flags = [] - if using_gpt: - home_length = home_length.gpt_end() - flags.append(PartitionFlag.LINUX_HOME) - - home_length = home_length.align() - - # add home partition to home device - home_partition = PartitionModification( - status=ModificationStatus.Create, - type=PartitionType.Primary, - start=home_start, - length=home_length, - mountpoint=Path('/home'), - mount_options=mount_options, - fs_type=filesystem_type, - flags=flags, - ) - home_device_modification.add_partition(home_partition) - - return [root_device_modification, home_device_modification] - - -async def suggest_lvm_layout( - disk_config: DiskLayoutConfiguration, - filesystem_type: FilesystemType | None = None, - vg_grp_name: str = 'ArchinstallVg', -) -> LvmConfiguration: - if disk_config.config_type != DiskLayoutType.Default: - raise ValueError('LVM suggested volumes are only available for default partitioning') - - using_subvolumes = False - btrfs_subvols = [] - home_volume = True - mount_options = [] - - if not filesystem_type: - filesystem_type = await select_main_filesystem_format() - - if filesystem_type == FilesystemType.Btrfs: - prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n' - result = await Confirmation(header=prompt, allow_skip=False, preset=True).show() - - using_subvolumes = MenuItem.yes() == result.item() - mount_options = await select_mount_options() - - if using_subvolumes: - btrfs_subvols = get_default_btrfs_subvols() - home_volume = False - - boot_part: PartitionModification | None = None - other_part: list[PartitionModification] = [] - - for mod in disk_config.device_modifications: - for part in mod.partitions: - if part.is_boot(): - boot_part = part - else: - other_part.append(part) - - if not boot_part: - raise ValueError('Unable to find boot partition in partition modifications') - - total_vol_available = sum( - [p.length for p in other_part], - Size(0, Unit.B, SectorSize.default()), - ) - root_vol_size = process_root_partition_size(total_vol_available, SectorSize.default()) - home_vol_size = total_vol_available - root_vol_size - - lvm_vol_group = LvmVolumeGroup(vg_grp_name, pvs=other_part) - - root_vol = LvmVolume( - status=LvmVolumeStatus.Create, - name='root', - fs_type=filesystem_type, - length=root_vol_size, - mountpoint=Path('/'), - btrfs_subvols=btrfs_subvols, - mount_options=mount_options, - ) - - lvm_vol_group.volumes.append(root_vol) - - if home_volume: - home_vol = LvmVolume( - status=LvmVolumeStatus.Create, - name='home', - fs_type=filesystem_type, - length=home_vol_size, - mountpoint=Path('/home'), - ) - - lvm_vol_group.volumes.append(home_vol) - - return LvmConfiguration(LvmLayoutType.Default, [lvm_vol_group]) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_menu.py similarity index 100% rename from archinstall/lib/interactions/general_conf.py rename to archinstall/lib/interactions/general_menu.py diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_menu.py similarity index 100% rename from archinstall/lib/interactions/system_conf.py rename to archinstall/lib/interactions/system_menu.py diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py index be620e158f..5194fff60e 100644 --- a/archinstall/lib/profile/profile_menu.py +++ b/archinstall/lib/profile/profile_menu.py @@ -2,7 +2,7 @@ from archinstall.default_profiles.profile import GreeterType, Profile from archinstall.lib.hardware import GfxDriver -from archinstall.lib.interactions.system_conf import select_driver +from archinstall.lib.interactions.system_menu import select_driver from archinstall.lib.menu.abstract_menu import AbstractSubMenu from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.models.profile import ProfileConfiguration diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 9c6df9885f..c977d1f192 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -10,7 +10,7 @@ from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer, accessibility_tools_in_use, run_custom_user_commands -from archinstall.lib.interactions.general_conf import PostInstallationAction, select_post_installation +from archinstall.lib.interactions.general_menu import PostInstallationAction, select_post_installation from archinstall.lib.menu.util import delayed_warning from archinstall.lib.mirrors import MirrorListHandler from archinstall.lib.models import Bootloader From c4d51558d9c4f1dd4ed8b451938bb47259b146f7 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sat, 21 Mar 2026 15:09:24 +1100 Subject: [PATCH 2/3] Refactor interactive module --- archinstall/lib/global_menu.py | 6 +- archinstall/lib/interactions/__init__.py | 19 -- archinstall/lib/interactions/general_menu.py | 200 ------------------- archinstall/lib/interactions/system_menu.py | 154 -------------- archinstall/lib/profile/profile_menu.py | 2 +- archinstall/scripts/guided.py | 2 +- 6 files changed, 5 insertions(+), 378 deletions(-) delete mode 100644 archinstall/lib/interactions/__init__.py delete mode 100644 archinstall/lib/interactions/general_menu.py delete mode 100644 archinstall/lib/interactions/system_menu.py diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 393ec7f1cc..b696e3259c 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -6,9 +6,9 @@ from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu from archinstall.lib.configuration import save_config from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu +from archinstall.lib.general.general_menu import add_number_of_parallel_downloads, select_hostname, select_ntp, select_timezone +from archinstall.lib.general.system_menu import select_kernel, select_swap from archinstall.lib.hardware import SysInfo -from archinstall.lib.interactions.general_menu import add_number_of_parallel_downloads, select_hostname, select_ntp, select_timezone -from archinstall.lib.interactions.system_menu import select_kernel, select_swap from archinstall.lib.locale.locale_menu import LocaleMenu from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey from archinstall.lib.mirrors import MirrorListHandler, MirrorMenu @@ -231,7 +231,7 @@ def is_config_valid(self) -> bool: return self._validate_bootloader() is None async def _select_archinstall_language(self, preset: Language) -> Language: - from archinstall.lib.interactions.general_menu import select_archinstall_language + from archinstall.lib.general.general_menu import select_archinstall_language language = await select_archinstall_language(translation_handler.translated_languages, preset) translation_handler.activate(language) diff --git a/archinstall/lib/interactions/__init__.py b/archinstall/lib/interactions/__init__.py deleted file mode 100644 index cdf398be17..0000000000 --- a/archinstall/lib/interactions/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from archinstall.lib.interactions.general_menu import ( - add_number_of_parallel_downloads, - select_archinstall_language, - select_hostname, - select_ntp, - select_timezone, -) -from archinstall.lib.interactions.system_menu import select_driver, select_kernel, select_swap - -__all__ = [ - 'add_number_of_parallel_downloads', - 'select_archinstall_language', - 'select_driver', - 'select_hostname', - 'select_kernel', - 'select_ntp', - 'select_swap', - 'select_timezone', -] diff --git a/archinstall/lib/interactions/general_menu.py b/archinstall/lib/interactions/general_menu.py deleted file mode 100644 index 5a95762ead..0000000000 --- a/archinstall/lib/interactions/general_menu.py +++ /dev/null @@ -1,200 +0,0 @@ -from enum import Enum -from pathlib import Path - -from archinstall.lib.locale.utils import list_timezones -from archinstall.lib.menu.helpers import Confirmation, Input, Selection -from archinstall.lib.output import warn -from archinstall.lib.translationhandler import Language, tr -from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup -from archinstall.tui.ui.result import ResultType - - -class PostInstallationAction(Enum): - EXIT = tr('Exit archinstall') - REBOOT = tr('Reboot system') - CHROOT = tr('chroot into installation for post-installation configurations') - - -async def select_ntp(preset: bool = True) -> bool: - header = tr('Would you like to use automatic time synchronization (NTP) with the default time servers?\n') + '\n' - header += ( - tr( - 'Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki', - ) - + '\n' - ) - - result = await Confirmation( - header=header, - allow_skip=True, - preset=preset, - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Selection: - return result.item() == MenuItem.yes() - case _: - raise ValueError('Unhandled return type') - - -async def select_hostname(preset: str | None = None) -> str | None: - result = await Input( - header=tr('Enter a hostname'), - allow_skip=True, - default_value=preset, - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Selection: - hostname = result.get_value() - if len(hostname) < 1: - return None - return hostname - case ResultType.Reset: - raise ValueError('Unhandled result type') - - -async def select_timezone(preset: str | None = None) -> str | None: - default = 'UTC' - timezones = list_timezones() - - items = [MenuItem(tz, value=tz) for tz in timezones] - group = MenuItemGroup(items, sort_items=True) - group.set_selected_by_value(preset) - group.set_default_by_value(default) - - result = await Selection[str]( - group, - header=tr('Select timezone'), - allow_reset=True, - allow_skip=True, - enable_filter=True, - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Reset: - return default - case ResultType.Selection: - return result.get_value() - - -async def select_language(preset: str | None = None) -> str | None: - from archinstall.lib.locale.locale_menu import select_kb_layout - - # We'll raise an exception in an upcoming version. - # from ..exceptions import Deprecated - # raise Deprecated("select_language() has been deprecated, use select_kb_layout() instead.") - - # No need to translate this i feel, as it's a short lived message. - warn('select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version') - - return await select_kb_layout(preset) - - -async def select_archinstall_language(languages: list[Language], preset: Language) -> Language: - # these are the displayed language names which can either be - # the english name of a language or, if present, the - # name of the language in its own language - - items = [MenuItem(lang.display_name, lang) for lang in languages] - group = MenuItemGroup(items, sort_items=True) - group.set_focus_by_value(preset) - - title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n' - title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n' - title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n' - - result = await Selection[Language]( - header=title, - group=group, - allow_reset=False, - allow_skip=True, - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Selection: - return result.get_value() - case ResultType.Reset: - raise ValueError('Language selection not handled') - - -async def add_number_of_parallel_downloads(preset: int = 1) -> int | None: - max_recommended = 5 - - header = tr('This option enables the number of parallel downloads that can occur during package downloads') + '\n' - header += tr(' - Maximum recommended value : {} ( Allows {} parallel downloads at a time )').format(max_recommended, max_recommended) + '\n\n' - header += tr('Enter the number of parallel downloads to be enabled') - - def validator(s: str) -> str | None: - try: - value = int(s) - - if 1 <= value <= max_recommended: - return None - - return tr('Value must be between 1 and {}').format(max_recommended) - except Exception: - return tr('Please enter a valid number') - - result = await Input( - header=header, - allow_skip=True, - allow_reset=True, - validator_callback=validator, - default_value=str(preset), - ).show() - - downloads = 1 - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Reset: - return downloads - case ResultType.Selection: - downloads = int(result.get_value()) - - pacman_conf_path = Path('/etc/pacman.conf') - with pacman_conf_path.open() as f: # noqa: ASYNC230 - pacman_conf = f.read().split('\n') - - with pacman_conf_path.open('w') as fwrite: # noqa: ASYNC230 - for line in pacman_conf: - if 'ParallelDownloads' in line: - fwrite.write(f'ParallelDownloads = {downloads}\n') - else: - fwrite.write(f'{line}\n') - - return downloads - - -async def select_post_installation(elapsed_time: float | None = None) -> PostInstallationAction: - header = 'Installation completed' - if elapsed_time is not None: - minutes = int(elapsed_time // 60) - seconds = int(elapsed_time % 60) - header += f' in {minutes}m{seconds}s' + '\n' - header += tr('What would you like to do next?') + '\n' - - items = [MenuItem(action.value, value=action) for action in PostInstallationAction] - group = MenuItemGroup(items) - - result = await Selection[PostInstallationAction]( - group, - header=header, - allow_skip=False, - ).show() - - match result.type_: - case ResultType.Selection: - return result.get_value() - case _: - raise ValueError('Post installation action not handled') diff --git a/archinstall/lib/interactions/system_menu.py b/archinstall/lib/interactions/system_menu.py deleted file mode 100644 index 5c4d4634d2..0000000000 --- a/archinstall/lib/interactions/system_menu.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import assert_never - -from archinstall.lib.hardware import GfxDriver, SysInfo -from archinstall.lib.menu.helpers import Confirmation, Selection -from archinstall.lib.models.application import ZramAlgorithm, ZramConfiguration -from archinstall.lib.translationhandler import tr -from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup -from archinstall.tui.ui.result import ResultType - - -async def select_kernel(preset: list[str] = []) -> list[str]: - """ - Asks the user to select a kernel for system. - - :return: The string as a selected kernel - :rtype: string - """ - kernels = ['linux', 'linux-lts', 'linux-zen', 'linux-hardened'] - default_kernel = 'linux' - - items = [MenuItem(k, value=k) for k in kernels] - - group = MenuItemGroup(items, sort_items=True) - group.set_default_by_value(default_kernel) - group.set_focus_by_value(default_kernel) - group.set_selected_by_value(preset) - - result = await Selection[str]( - group, - header=tr('Select which kernel(s) to install'), - allow_skip=True, - allow_reset=True, - multi=True, - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Reset: - return [] - case ResultType.Selection: - return result.get_values() - - -async def select_uki(preset: bool = True) -> bool: - prompt = tr('Would you like to use unified kernel images?') + '\n' - - result = await Confirmation(header=prompt, allow_skip=True, preset=preset).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Selection: - return result.get_value() - case ResultType.Reset: - raise ValueError('Unhandled result type') - - -async def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None: - """ - Somewhat convoluted function, whose job is simple. - Select a graphics driver from a pre-defined set of popular options. - - (The template xorg is for beginner users, not advanced, and should - there for appeal to the general public first and edge cases later) - """ - if not options: - options = [driver for driver in GfxDriver] - - items = [ - MenuItem( - o.value, - value=o, - preview_action=lambda x: x.value.packages_text() if x.value else None, - ) - for o in options - ] - - group = MenuItemGroup(items, sort_items=True) - group.set_default_by_value(GfxDriver.AllOpenSource) - - if preset is not None: - group.set_focus_by_value(preset) - - header = '' - if SysInfo.has_amd_graphics(): - header += tr('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.') + '\n' - if SysInfo.has_intel_graphics(): - header += tr('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n') - if SysInfo.has_nvidia_graphics(): - header += tr('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n') - - result = await Selection[GfxDriver]( - group, - header=header, - allow_skip=True, - allow_reset=True, - preview_location='right', - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Reset: - return None - case ResultType.Selection: - return result.get_value() - - -async def select_swap(preset: ZramConfiguration = ZramConfiguration(enabled=True)) -> ZramConfiguration: - prompt = tr('Would you like to use swap on zram?') + '\n' - - group = MenuItemGroup.yes_no() - group.set_default_by_value(True) - group.set_focus_by_value(preset.enabled) - - result = await Confirmation( - header=prompt, - allow_skip=True, - preset=preset.enabled, - ).show() - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Selection: - enabled = result.item() == MenuItem.yes() - if not enabled: - return ZramConfiguration(enabled=False) - - # Ask for compression algorithm - algo_group = MenuItemGroup.from_enum(ZramAlgorithm, sort_items=False) - algo_group.set_default_by_value(ZramAlgorithm.ZSTD) - algo_group.set_focus_by_value(preset.algorithm) - - algo_result = await Selection[ZramAlgorithm]( - algo_group, - header=tr('Select zram compression algorithm:') + '\n', - allow_skip=True, - ).show() - - match algo_result.type_: - case ResultType.Skip: - algo = preset.algorithm - case ResultType.Selection: - algo = algo_result.get_value() - case ResultType.Reset: - raise ValueError('Unhandled result type') - case _: - assert_never(algo_result.type_) - - return ZramConfiguration(enabled=True, algorithm=algo) - case ResultType.Reset: - raise ValueError('Unhandled result type') diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py index 5194fff60e..cb560c27f0 100644 --- a/archinstall/lib/profile/profile_menu.py +++ b/archinstall/lib/profile/profile_menu.py @@ -1,8 +1,8 @@ from typing import override from archinstall.default_profiles.profile import GreeterType, Profile +from archinstall.lib.general.system_menu import select_driver from archinstall.lib.hardware import GfxDriver -from archinstall.lib.interactions.system_menu import select_driver from archinstall.lib.menu.abstract_menu import AbstractSubMenu from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.models.profile import ProfileConfiguration diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index c977d1f192..70437222f3 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -8,9 +8,9 @@ from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.disk.utils import disk_layouts +from archinstall.lib.general.general_menu import PostInstallationAction, select_post_installation from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer, accessibility_tools_in_use, run_custom_user_commands -from archinstall.lib.interactions.general_menu import PostInstallationAction, select_post_installation from archinstall.lib.menu.util import delayed_warning from archinstall.lib.mirrors import MirrorListHandler from archinstall.lib.models import Bootloader From 5452e78737f5e5b3a732451ed099ccb61c9fc97d Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sat, 21 Mar 2026 17:59:21 +1100 Subject: [PATCH 3/3] Update --- archinstall/lib/general/__init__.py | 19 +++ archinstall/lib/general/general_menu.py | 200 ++++++++++++++++++++++++ archinstall/lib/general/system_menu.py | 154 ++++++++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 archinstall/lib/general/__init__.py create mode 100644 archinstall/lib/general/general_menu.py create mode 100644 archinstall/lib/general/system_menu.py diff --git a/archinstall/lib/general/__init__.py b/archinstall/lib/general/__init__.py new file mode 100644 index 0000000000..21add83479 --- /dev/null +++ b/archinstall/lib/general/__init__.py @@ -0,0 +1,19 @@ +from archinstall.lib.general.general_menu import ( + add_number_of_parallel_downloads, + select_archinstall_language, + select_hostname, + select_ntp, + select_timezone, +) +from archinstall.lib.general.system_menu import select_driver, select_kernel, select_swap + +__all__ = [ + 'add_number_of_parallel_downloads', + 'select_archinstall_language', + 'select_driver', + 'select_hostname', + 'select_kernel', + 'select_ntp', + 'select_swap', + 'select_timezone', +] diff --git a/archinstall/lib/general/general_menu.py b/archinstall/lib/general/general_menu.py new file mode 100644 index 0000000000..5a95762ead --- /dev/null +++ b/archinstall/lib/general/general_menu.py @@ -0,0 +1,200 @@ +from enum import Enum +from pathlib import Path + +from archinstall.lib.locale.utils import list_timezones +from archinstall.lib.menu.helpers import Confirmation, Input, Selection +from archinstall.lib.output import warn +from archinstall.lib.translationhandler import Language, tr +from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup +from archinstall.tui.ui.result import ResultType + + +class PostInstallationAction(Enum): + EXIT = tr('Exit archinstall') + REBOOT = tr('Reboot system') + CHROOT = tr('chroot into installation for post-installation configurations') + + +async def select_ntp(preset: bool = True) -> bool: + header = tr('Would you like to use automatic time synchronization (NTP) with the default time servers?\n') + '\n' + header += ( + tr( + 'Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki', + ) + + '\n' + ) + + result = await Confirmation( + header=header, + allow_skip=True, + preset=preset, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + return result.item() == MenuItem.yes() + case _: + raise ValueError('Unhandled return type') + + +async def select_hostname(preset: str | None = None) -> str | None: + result = await Input( + header=tr('Enter a hostname'), + allow_skip=True, + default_value=preset, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + hostname = result.get_value() + if len(hostname) < 1: + return None + return hostname + case ResultType.Reset: + raise ValueError('Unhandled result type') + + +async def select_timezone(preset: str | None = None) -> str | None: + default = 'UTC' + timezones = list_timezones() + + items = [MenuItem(tz, value=tz) for tz in timezones] + group = MenuItemGroup(items, sort_items=True) + group.set_selected_by_value(preset) + group.set_default_by_value(default) + + result = await Selection[str]( + group, + header=tr('Select timezone'), + allow_reset=True, + allow_skip=True, + enable_filter=True, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Reset: + return default + case ResultType.Selection: + return result.get_value() + + +async def select_language(preset: str | None = None) -> str | None: + from archinstall.lib.locale.locale_menu import select_kb_layout + + # We'll raise an exception in an upcoming version. + # from ..exceptions import Deprecated + # raise Deprecated("select_language() has been deprecated, use select_kb_layout() instead.") + + # No need to translate this i feel, as it's a short lived message. + warn('select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version') + + return await select_kb_layout(preset) + + +async def select_archinstall_language(languages: list[Language], preset: Language) -> Language: + # these are the displayed language names which can either be + # the english name of a language or, if present, the + # name of the language in its own language + + items = [MenuItem(lang.display_name, lang) for lang in languages] + group = MenuItemGroup(items, sort_items=True) + group.set_focus_by_value(preset) + + title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n' + title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n' + title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n' + + result = await Selection[Language]( + header=title, + group=group, + allow_reset=False, + allow_skip=True, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + return result.get_value() + case ResultType.Reset: + raise ValueError('Language selection not handled') + + +async def add_number_of_parallel_downloads(preset: int = 1) -> int | None: + max_recommended = 5 + + header = tr('This option enables the number of parallel downloads that can occur during package downloads') + '\n' + header += tr(' - Maximum recommended value : {} ( Allows {} parallel downloads at a time )').format(max_recommended, max_recommended) + '\n\n' + header += tr('Enter the number of parallel downloads to be enabled') + + def validator(s: str) -> str | None: + try: + value = int(s) + + if 1 <= value <= max_recommended: + return None + + return tr('Value must be between 1 and {}').format(max_recommended) + except Exception: + return tr('Please enter a valid number') + + result = await Input( + header=header, + allow_skip=True, + allow_reset=True, + validator_callback=validator, + default_value=str(preset), + ).show() + + downloads = 1 + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Reset: + return downloads + case ResultType.Selection: + downloads = int(result.get_value()) + + pacman_conf_path = Path('/etc/pacman.conf') + with pacman_conf_path.open() as f: # noqa: ASYNC230 + pacman_conf = f.read().split('\n') + + with pacman_conf_path.open('w') as fwrite: # noqa: ASYNC230 + for line in pacman_conf: + if 'ParallelDownloads' in line: + fwrite.write(f'ParallelDownloads = {downloads}\n') + else: + fwrite.write(f'{line}\n') + + return downloads + + +async def select_post_installation(elapsed_time: float | None = None) -> PostInstallationAction: + header = 'Installation completed' + if elapsed_time is not None: + minutes = int(elapsed_time // 60) + seconds = int(elapsed_time % 60) + header += f' in {minutes}m{seconds}s' + '\n' + header += tr('What would you like to do next?') + '\n' + + items = [MenuItem(action.value, value=action) for action in PostInstallationAction] + group = MenuItemGroup(items) + + result = await Selection[PostInstallationAction]( + group, + header=header, + allow_skip=False, + ).show() + + match result.type_: + case ResultType.Selection: + return result.get_value() + case _: + raise ValueError('Post installation action not handled') diff --git a/archinstall/lib/general/system_menu.py b/archinstall/lib/general/system_menu.py new file mode 100644 index 0000000000..5c4d4634d2 --- /dev/null +++ b/archinstall/lib/general/system_menu.py @@ -0,0 +1,154 @@ +from typing import assert_never + +from archinstall.lib.hardware import GfxDriver, SysInfo +from archinstall.lib.menu.helpers import Confirmation, Selection +from archinstall.lib.models.application import ZramAlgorithm, ZramConfiguration +from archinstall.lib.translationhandler import tr +from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup +from archinstall.tui.ui.result import ResultType + + +async def select_kernel(preset: list[str] = []) -> list[str]: + """ + Asks the user to select a kernel for system. + + :return: The string as a selected kernel + :rtype: string + """ + kernels = ['linux', 'linux-lts', 'linux-zen', 'linux-hardened'] + default_kernel = 'linux' + + items = [MenuItem(k, value=k) for k in kernels] + + group = MenuItemGroup(items, sort_items=True) + group.set_default_by_value(default_kernel) + group.set_focus_by_value(default_kernel) + group.set_selected_by_value(preset) + + result = await Selection[str]( + group, + header=tr('Select which kernel(s) to install'), + allow_skip=True, + allow_reset=True, + multi=True, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Reset: + return [] + case ResultType.Selection: + return result.get_values() + + +async def select_uki(preset: bool = True) -> bool: + prompt = tr('Would you like to use unified kernel images?') + '\n' + + result = await Confirmation(header=prompt, allow_skip=True, preset=preset).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + return result.get_value() + case ResultType.Reset: + raise ValueError('Unhandled result type') + + +async def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None: + """ + Somewhat convoluted function, whose job is simple. + Select a graphics driver from a pre-defined set of popular options. + + (The template xorg is for beginner users, not advanced, and should + there for appeal to the general public first and edge cases later) + """ + if not options: + options = [driver for driver in GfxDriver] + + items = [ + MenuItem( + o.value, + value=o, + preview_action=lambda x: x.value.packages_text() if x.value else None, + ) + for o in options + ] + + group = MenuItemGroup(items, sort_items=True) + group.set_default_by_value(GfxDriver.AllOpenSource) + + if preset is not None: + group.set_focus_by_value(preset) + + header = '' + if SysInfo.has_amd_graphics(): + header += tr('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.') + '\n' + if SysInfo.has_intel_graphics(): + header += tr('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n') + if SysInfo.has_nvidia_graphics(): + header += tr('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n') + + result = await Selection[GfxDriver]( + group, + header=header, + allow_skip=True, + allow_reset=True, + preview_location='right', + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Reset: + return None + case ResultType.Selection: + return result.get_value() + + +async def select_swap(preset: ZramConfiguration = ZramConfiguration(enabled=True)) -> ZramConfiguration: + prompt = tr('Would you like to use swap on zram?') + '\n' + + group = MenuItemGroup.yes_no() + group.set_default_by_value(True) + group.set_focus_by_value(preset.enabled) + + result = await Confirmation( + header=prompt, + allow_skip=True, + preset=preset.enabled, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + enabled = result.item() == MenuItem.yes() + if not enabled: + return ZramConfiguration(enabled=False) + + # Ask for compression algorithm + algo_group = MenuItemGroup.from_enum(ZramAlgorithm, sort_items=False) + algo_group.set_default_by_value(ZramAlgorithm.ZSTD) + algo_group.set_focus_by_value(preset.algorithm) + + algo_result = await Selection[ZramAlgorithm]( + algo_group, + header=tr('Select zram compression algorithm:') + '\n', + allow_skip=True, + ).show() + + match algo_result.type_: + case ResultType.Skip: + algo = preset.algorithm + case ResultType.Selection: + algo = algo_result.get_value() + case ResultType.Reset: + raise ValueError('Unhandled result type') + case _: + assert_never(algo_result.type_) + + return ZramConfiguration(enabled=True, algorithm=algo) + case ResultType.Reset: + raise ValueError('Unhandled result type')