Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall/lib/user_interaction
diff options
context:
space:
mode:
authorDaniel Girtler <blackrabbit256@gmail.com>2023-04-19 20:55:42 +1000
committerGitHub <noreply@github.com>2023-04-19 12:55:42 +0200
commit00b0ae7ba439a5a420095175b3bedd52c569db51 (patch)
treef02d081e361d5e65603f74dea3873dcc6606cf7c /archinstall/lib/user_interaction
parent5253e57e9f26cf3e59cb2460544af13f56e485bb (diff)
PyParted and a large rewrite of the underlying partitioning (#1604)
* Invert mypy files * Add optional pre-commit hooks * New profile structure * Serialize profiles * Use profile instead of classmethod * Custom profile setup * Separator between back * Support profile import via url * Move profiles module * Refactor files * Remove symlink * Add user to docker group * Update schema description * Handle list services * mypy fixes * mypy fixes * Rename profilesv2 to profiles * flake8 * mypy again * Support selecting DM * Fix mypy * Cleanup * Update greeter setting * Update schema * Revert toml changes * Poc external dependencies * Dependency support * New encryption menu * flake8 * Mypy and flake8 * Unify lsblk command * Update bootloader configuration * Git hooks * Fix import * Pyparted * Remove custom font setting * flake8 * Remove default preview * Manual partitioning menu * Update structure * Disk configuration * Update filesystem * luks2 encryption * Everything works until installation * Btrfsutil * Btrfs handling * Update btrfs * Save encryption config * Fix pipewire issue * Update mypy version * Update all pre-commit * Update package versions * Revert audio/pipewire * Merge master PRs * Add master changes * Merge master changes * Small renaming * Pull master changes * Reset disk enc after disk config change * Generate locals * Update naming * Fix imports * Fix broken sync * Fix pre selection on table menu * Profile menu * Update profile * Fix post_install * Added python-pyparted to PKGBUILD, this requires [testing] to be enabled in order to run makepkg. Package still works via python -m build etc. * Swaped around some setuptools logic in pyproject Since we define `package-data` and `packages` there should be no need for: ``` [tool.setuptools.packages.find] where = ["archinstall", "archinstall.*"] ``` * Removed pyproject collisions. Duplicate definitions. * Made sure pyproject.toml includes languages * Add example and update README * Fix pyproject issues * Generate locale * Refactor imports * Simplify imports * Add profile description and package examples * Align code * Fix mypy * Simplify imports * Fix saving config * Fix wrong luks merge * Refactor installation * Fix cdrom device loading * Fix wrongly merged code * Fix imports and greeter * Don't terminate on partprobe error * Use specific path on partprobe from luks * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update archinstall/lib/disk/device_model.py Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com> * Update github workflow to test archinstall installation * Update sway merge * Generate locales * Update workflow --------- Co-authored-by: Daniel Girtler <girtler.daniel@gmail.com> Co-authored-by: Anton Hvornum <anton@hvornum.se> Co-authored-by: Anton Hvornum <anton.feeds+github@gmail.com> Co-authored-by: codefiles <11915375+codefiles@users.noreply.github.com>
Diffstat (limited to 'archinstall/lib/user_interaction')
-rw-r--r--archinstall/lib/user_interaction/__init__.py16
-rw-r--r--archinstall/lib/user_interaction/backwards_compatible_conf.py95
-rw-r--r--archinstall/lib/user_interaction/disk_conf.py403
-rw-r--r--archinstall/lib/user_interaction/general_conf.py59
-rw-r--r--archinstall/lib/user_interaction/locale_conf.py3
-rw-r--r--archinstall/lib/user_interaction/manage_users_conf.py21
-rw-r--r--archinstall/lib/user_interaction/network_conf.py6
-rw-r--r--archinstall/lib/user_interaction/partitioning_conf.py362
-rw-r--r--archinstall/lib/user_interaction/save_conf.py74
-rw-r--r--archinstall/lib/user_interaction/subvolume_config.py98
-rw-r--r--archinstall/lib/user_interaction/system_conf.py133
-rw-r--r--archinstall/lib/user_interaction/utils.py47
12 files changed, 451 insertions, 866 deletions
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