from __future__ import annotations from typing import override from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu from archinstall.lib.models.application import ApplicationConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, EncryptionType, FilesystemType, PartitionModification from archinstall.lib.packages import list_available_packages from archinstall.tui.menu_item import MenuItem, MenuItemGroup from .applications.application_menu import ApplicationMenu from .args import ArchConfig from .authentication.authentication_menu import AuthenticationMenu from .configuration import save_config from .hardware import SysInfo from .interactions.general_conf import ( add_number_of_parallel_downloads, ask_additional_packages_to_install, ask_for_a_timezone, ask_hostname, ask_ntp, ) from .interactions.network_menu import ask_to_configure_network from .interactions.system_conf import ask_for_bootloader, ask_for_swap, ask_for_uki, select_kernel from .locale.locale_menu import LocaleMenu from .menu.abstract_menu import CONFIG_KEY, AbstractMenu from .mirrors import MirrorMenu from .models.bootloader import Bootloader from .models.locale import LocaleConfiguration from .models.mirrors import MirrorConfiguration from .models.network import NetworkConfiguration, NicType from .models.packages import Repository from .models.profile import ProfileConfiguration from .output import FormattedOutput from .pacman.config import PacmanConfig from .translationhandler import Language, tr, translation_handler class GlobalMenu(AbstractMenu[None]): def __init__(self, arch_config: ArchConfig) -> None: self._arch_config = arch_config menu_optioons = self._get_menu_options() self._item_group = MenuItemGroup( menu_optioons, sort_items=False, checkmarks=True, ) super().__init__(self._item_group, config=arch_config) def _get_menu_options(self) -> list[MenuItem]: menu_options = [ MenuItem( text=tr('Archinstall language'), action=self._select_archinstall_language, display_action=lambda x: x.display_name if x else '', key='archinstall_language', ), MenuItem( text=tr('Locales'), action=self._locale_selection, preview_action=self._prev_locale, key='locale_config', ), MenuItem( text=tr('Mirrors and repositories'), action=self._mirror_configuration, preview_action=self._prev_mirror_config, key='mirror_config', ), MenuItem( text=tr('Disk configuration'), action=self._select_disk_config, preview_action=self._prev_disk_config, mandatory=True, key='disk_config', ), MenuItem( text=tr('Swap'), value=True, action=ask_for_swap, preview_action=self._prev_swap, key='swap', ), MenuItem( text=tr('Bootloader'), value=Bootloader.get_default(), action=self._select_bootloader, preview_action=self._prev_bootloader, mandatory=True, key='bootloader', ), MenuItem( text=tr('Unified kernel images'), value=False, enabled=SysInfo.has_uefi(), action=ask_for_uki, preview_action=self._prev_uki, key='uki', ), MenuItem( text=tr('Hostname'), value='archlinux', action=ask_hostname, preview_action=self._prev_hostname, key='hostname', ), MenuItem( text=tr('Authentication'), action=self._select_authentication, preview_action=self._prev_authentication, key='auth_config', ), MenuItem( text=tr('Profile'), action=self._select_profile, preview_action=self._prev_profile, key='profile_config', ), MenuItem( text=tr('Applications'), action=self._select_applications, value=[], preview_action=self._prev_applications, key='app_config', ), MenuItem( text=tr('Kernels'), value=['linux'], action=select_kernel, preview_action=self._prev_kernel, mandatory=True, key='kernels', ), MenuItem( text=tr('Network configuration'), action=ask_to_configure_network, value={}, preview_action=self._prev_network_config, key='network_config', ), MenuItem( text=tr('Parallel Downloads'), action=add_number_of_parallel_downloads, value=0, preview_action=self._prev_parallel_dw, key='parallel_downloads', ), MenuItem( text=tr('Additional packages'), action=self._select_additional_packages, value=[], preview_action=self._prev_additional_pkgs, key='packages', ), MenuItem( text=tr('Timezone'), action=ask_for_a_timezone, value='UTC', preview_action=self._prev_tz, key='timezone', ), MenuItem( text=tr('Automatic time sync (NTP)'), action=ask_ntp, value=True, preview_action=self._prev_ntp, key='ntp', ), MenuItem( text='', ), MenuItem( text=tr('Save configuration'), action=lambda x: self._safe_config(), key=f'{CONFIG_KEY}_save', ), MenuItem( text=tr('Install'), preview_action=self._prev_install_invalid_config, key=f'{CONFIG_KEY}_install', ), MenuItem( text=tr('Abort'), action=lambda x: exit(1), key=f'{CONFIG_KEY}_abort', ), ] return menu_options def _safe_config(self) -> None: # data: dict[str, Any] = {} # for item in self._item_group.items: # if item.key is not None: # data[item.key] = item.value self.sync_all_to_config() save_config(self._arch_config) def _missing_configs(self) -> list[str]: item: MenuItem = self._item_group.find_by_key('auth_config') auth_config: AuthenticationConfiguration | None = item.value def check(s: str) -> bool: item = self._item_group.find_by_key(s) return item.has_value() def has_superuser() -> bool: if auth_config and auth_config.users: return any([u.sudo for u in auth_config.users]) return False missing = set() if (auth_config is None or auth_config.root_enc_password is None) and not has_superuser(): missing.add( tr('Either root-password or at least 1 user with sudo privileges must be specified'), ) for item in self._item_group.items: if item.mandatory: assert item.key is not None if not check(item.key): missing.add(item.text) return list(missing) @override def _is_config_valid(self) -> bool: """ Checks the validity of the current configuration. """ if len(self._missing_configs()) != 0: return False return self._validate_bootloader() is None def _select_archinstall_language(self, preset: Language) -> Language: from .interactions.general_conf import select_archinstall_language language = select_archinstall_language(translation_handler.translated_languages, preset) translation_handler.activate(language) self._update_lang_text() return language def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None: app_config = ApplicationMenu(preset).run() return app_config def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None: auth_config = AuthenticationMenu(preset).run() return auth_config def _update_lang_text(self) -> None: """ The options for the global menu are generated with a static text; each entry of the menu needs to be updated with the new translation """ new_options = self._get_menu_options() for o in new_options: if o.key is not None: self._item_group.find_by_key(o.key).text = o.text def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration: locale_config = LocaleMenu(preset).run() return locale_config def _prev_locale(self, item: MenuItem) -> str | None: if not item.value: return None config: LocaleConfiguration = item.value return config.preview() def _prev_network_config(self, item: MenuItem) -> str | None: if item.value: network_config: NetworkConfiguration = item.value if network_config.type == NicType.MANUAL: output = FormattedOutput.as_table(network_config.nics) else: output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}' return output return None def _prev_additional_pkgs(self, item: MenuItem) -> str | None: if item.value: output = '\n'.join(sorted(item.value)) return output return None def _prev_authentication(self, item: MenuItem) -> str | None: if item.value: auth_config: AuthenticationConfiguration = item.value output = '' if auth_config.root_enc_password: output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n' if auth_config.users: output += FormattedOutput.as_table(auth_config.users) + '\n' if auth_config.u2f_config: u2f_config = auth_config.u2f_config login_method = u2f_config.u2f_login_method.display_value() output = tr('U2F login method: ') + login_method output += '\n' output += tr('Passwordless sudo: ') + (tr('Enabled') if u2f_config.passwordless_sudo else tr('Disabled')) return output return None def _prev_applications(self, item: MenuItem) -> str | None: if item.value: app_config: ApplicationConfiguration = item.value output = '' if app_config.bluetooth_config: output += f'{tr("Bluetooth")}: ' output += tr('Enabled') if app_config.bluetooth_config.enabled else tr('Disabled') output += '\n' if app_config.audio_config: audio_config = app_config.audio_config output += f'{tr("Audio")}: {audio_config.audio.value}' output += '\n' return output return None def _prev_tz(self, item: MenuItem) -> str | None: if item.value: return f'{tr("Timezone")}: {item.value}' return None def _prev_ntp(self, item: MenuItem) -> str | None: if item.value is not None: output = f'{tr("NTP")}: ' output += tr('Enabled') if item.value else tr('Disabled') return output return None def _prev_disk_config(self, item: MenuItem) -> str | None: disk_layout_conf: DiskLayoutConfiguration | None = item.value if disk_layout_conf: output = tr('Configuration type: {}').format(disk_layout_conf.config_type.display_msg()) + '\n' if disk_layout_conf.config_type == DiskLayoutType.Pre_mount: output += tr('Mountpoint') + ': ' + str(disk_layout_conf.mountpoint) if disk_layout_conf.lvm_config: output += '{}: {}'.format(tr('LVM configuration type'), disk_layout_conf.lvm_config.config_type.display_msg()) + '\n' if disk_layout_conf.disk_encryption: output += tr('Disk encryption') + ': ' + EncryptionType.type_to_text(disk_layout_conf.disk_encryption.encryption_type) + '\n' if disk_layout_conf.btrfs_options: btrfs_options = disk_layout_conf.btrfs_options if btrfs_options.snapshot_config: output += tr('Btrfs snapshot type: {}').format(btrfs_options.snapshot_config.snapshot_type.value) + '\n' return output return None def _prev_swap(self, item: MenuItem) -> str | None: if item.value is not None: output = f'{tr("Swap on zram")}: ' output += tr('Enabled') if item.value else tr('Disabled') return output return None def _prev_uki(self, item: MenuItem) -> str | None: if item.value is not None: output = f'{tr("Unified kernel images")}: ' output += tr('Enabled') if item.value else tr('Disabled') return output return None def _prev_hostname(self, item: MenuItem) -> str | None: if item.value is not None: return f'{tr("Hostname")}: {item.value}' return None def _prev_parallel_dw(self, item: MenuItem) -> str | None: if item.value is not None: return f'{tr("Parallel Downloads")}: {item.value}' return None def _prev_kernel(self, item: MenuItem) -> str | None: if item.value: kernel = ', '.join(item.value) return f'{tr("Kernel")}: {kernel}' return None def _prev_bootloader(self, item: MenuItem) -> str | None: if item.value is not None: return f'{tr("Bootloader")}: {item.value.value}' return None def _validate_bootloader(self) -> str | None: """ Checks the selected bootloader is valid for the selected filesystem type of the boot partition. Returns [`None`] if the bootloader is valid, otherwise returns a string with the error message. XXX: The caller is responsible for wrapping the string with the translation shim if necessary. """ bootloader: Bootloader | None = None root_partition: PartitionModification | None = None boot_partition: PartitionModification | None = None efi_partition: PartitionModification | None = None bootloader = self._item_group.find_by_key('bootloader').value if bootloader == Bootloader.NO_BOOTLOADER: return None if disk_config := self._item_group.find_by_key('disk_config').value: for layout in disk_config.device_modifications: if root_partition := layout.get_root_partition(): break for layout in disk_config.device_modifications: if boot_partition := layout.get_boot_partition(): break if SysInfo.has_uefi(): for layout in disk_config.device_modifications: if efi_partition := layout.get_efi_partition(): break else: return 'No disk layout selected' if root_partition is None: return 'Root partition not found' if boot_partition is None: return 'Boot partition not found' if SysInfo.has_uefi(): if efi_partition is None: return 'EFI system partition (ESP) not found' if efi_partition.fs_type not in [FilesystemType.Fat12, FilesystemType.Fat16, FilesystemType.Fat32]: return 'ESP must be formatted as a FAT filesystem' if bootloader == Bootloader.Limine: if boot_partition.fs_type not in [FilesystemType.Fat12, FilesystemType.Fat16, FilesystemType.Fat32]: return 'Limine does not support booting with a non-FAT boot partition' return None def _prev_install_invalid_config(self, item: MenuItem) -> str | None: if missing := self._missing_configs(): text = tr('Missing configurations:\n') for m in missing: text += f'- {m}\n' return text[:-1] # remove last new line if error := self._validate_bootloader(): return tr(f'Invalid configuration: {error}') return None def _prev_profile(self, item: MenuItem) -> str | None: profile_config: ProfileConfiguration | None = item.value if profile_config and profile_config.profile: output = tr('Profiles') + ': ' if profile_names := profile_config.profile.current_selection_names(): output += ', '.join(profile_names) + '\n' else: output += profile_config.profile.name + '\n' if profile_config.gfx_driver: output += tr('Graphics driver') + ': ' + profile_config.gfx_driver.value + '\n' if profile_config.greeter: output += tr('Greeter') + ': ' + profile_config.greeter.value + '\n' return output return None def _select_disk_config( self, preset: DiskLayoutConfiguration | None = None, ) -> DiskLayoutConfiguration | None: disk_config = DiskLayoutConfigurationMenu(preset).run() return disk_config def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None: bootloader = ask_for_bootloader(preset) if bootloader: uki = self._item_group.find_by_key('uki') if not SysInfo.has_uefi() or not bootloader.has_uki_support(): uki.value = False uki.enabled = False else: uki.enabled = True return bootloader def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None: from .profile.profile_menu import ProfileMenu profile_config = ProfileMenu(preset=current_profile).run() return profile_config def _select_additional_packages(self, preset: list[str]) -> list[str]: config: MirrorConfiguration | None = self._item_group.find_by_key('mirror_config').value repositories: set[Repository] = set() if config: repositories = set(config.optional_repositories) packages = ask_additional_packages_to_install( preset, repositories=repositories, ) return packages def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration: mirror_configuration = MirrorMenu(preset=preset).run() if mirror_configuration.optional_repositories: # reset the package list cache in case the repository selection has changed list_available_packages.cache_clear() # enable the repositories in the config pacman_config = PacmanConfig(None) pacman_config.enable(mirror_configuration.optional_repositories) pacman_config.apply() return mirror_configuration def _prev_mirror_config(self, item: MenuItem) -> str | None: if not item.value: return None mirror_config: MirrorConfiguration = item.value output = '' if mirror_config.mirror_regions: title = tr('Selected mirror regions') divider = '-' * len(title) regions = mirror_config.region_names output += f'{title}\n{divider}\n{regions}\n\n' if mirror_config.custom_servers: title = tr('Custom servers') divider = '-' * len(title) servers = mirror_config.custom_server_urls output += f'{title}\n{divider}\n{servers}\n\n' if mirror_config.optional_repositories: title = tr('Optional repositories') divider = '-' * len(title) repos = ', '.join([r.value for r in mirror_config.optional_repositories]) output += f'{title}\n{divider}\n{repos}\n\n' if mirror_config.custom_repositories: title = tr('Custom repositories') table = FormattedOutput.as_table(mirror_config.custom_repositories) output += f'{title}:\n\n{table}' return output.strip()