archinstall/archinstall/lib/global_menu.py

615 lines
20 KiB
Python

from typing import override
from archinstall.default_profiles.profile import GreeterType
from archinstall.lib.applications.application_menu import ApplicationMenu
from archinstall.lib.args import ArchConfig
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
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.locale.locale_menu import LocaleMenu
from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.mirror.mirror_menu import MirrorMenu
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, FilesystemType, PartitionModification
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration
from archinstall.lib.models.network import NetworkConfiguration, NicType
from archinstall.lib.models.packages import Repository
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.network.network_menu import select_network
from archinstall.lib.output import FormattedOutput
from archinstall.lib.packages.packages import list_available_packages, select_additional_packages
from archinstall.lib.pacman.config import PacmanConfig
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
class GlobalMenu(AbstractMenu[None]):
def __init__(
self,
arch_config: ArchConfig,
mirror_list_handler: MirrorListHandler | None = None,
skip_boot: bool = False,
title: str | None = None,
) -> None:
self._arch_config = arch_config
self._mirror_list_handler = mirror_list_handler
self._skip_boot = skip_boot
self._uefi = SysInfo.has_uefi()
menu_options = self._get_menu_options()
self._item_group = MenuItemGroup(
menu_options,
sort_items=False,
checkmarks=True,
)
super().__init__(self._item_group, config=arch_config, title=title)
def _get_menu_options(self) -> list[MenuItem]:
menu_options = [
MenuItem(
text=tr('Archinstall language'),
action=self._select_archinstall_language,
preview_action=self._prev_archinstall_language,
key='archinstall_language',
),
MenuItem(
text=tr('Locales'),
value=LocaleConfiguration.default(),
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=ZramConfiguration(enabled=True),
action=select_swap,
preview_action=self._prev_swap,
key='swap',
),
MenuItem(
text=tr('Bootloader'),
value=BootloaderConfiguration.get_default(self._uefi, self._skip_boot),
action=self._select_bootloader_config,
preview_action=self._prev_bootloader_config,
key='bootloader_config',
),
MenuItem(
text=tr('Kernels'),
value=['linux'],
action=select_kernel,
preview_action=self._prev_kernel,
mandatory=True,
key='kernels',
),
MenuItem(
text=tr('Hostname'),
value='archlinux',
action=select_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('Network configuration'),
action=select_network,
value={},
preview_action=self._prev_network_config,
key='network_config',
),
MenuItem(
text=tr('Parallel Downloads'),
action=add_number_of_parallel_downloads,
value=1,
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=select_timezone,
value='UTC',
preview_action=self._prev_tz,
key='timezone',
),
MenuItem(
text=tr('Automatic time sync (NTP)'),
action=select_ntp,
value=True,
preview_action=self._prev_ntp,
key='ntp',
),
MenuItem(
text='',
read_only=True,
),
MenuItem(
text=tr('Save configuration'),
action=lambda x: self._safe_config(),
key=SpecialMenuKey.SAVE.value,
),
MenuItem(
text=tr('Install'),
preview_action=self._prev_install_invalid_config,
key=SpecialMenuKey.INSTALL.value,
),
MenuItem(
text=tr('Abort'),
key=SpecialMenuKey.ABORT.value,
),
]
return menu_options
async 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()
await 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
def has_regular_user() -> bool:
if auth_config and auth_config.users:
return len(auth_config.users) > 0
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'),
)
# These greeters only show users with UID >= 1000 and have no manual login by default
if not has_regular_user():
profile_item: MenuItem = self._item_group.find_by_key('profile_config')
profile_config: ProfileConfiguration | None = profile_item.value
if profile_config and profile_config.profile and profile_config.profile.is_desktop_profile():
problematic_greeters = {GreeterType.Sddm}
if any(p.default_greeter_type in problematic_greeters for p in profile_config.profile.current_selection):
missing.add(
tr('The selected desktop profile requires a regular user to log in via the greeter'),
)
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
async def _select_archinstall_language(self, preset: Language) -> 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)
self._update_lang_text()
return language
def _prev_archinstall_language(self, item: MenuItem) -> str | None:
if not item.value:
return None
lang: Language = item.value
return f'{tr("Language")}: {lang.display_name}'
async def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None:
app_config = await ApplicationMenu(preset).show()
return app_config
async def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
auth_config = await AuthenticationMenu(preset).show()
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
async def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration | None:
locale_config = await LocaleMenu(preset).show()
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'
if app_config.print_service_config:
output += f'{tr("Print service")}: '
output += tr('Enabled') if app_config.print_service_config.enabled else tr('Disabled')
output += '\n'
if app_config.power_management_config:
power_management_config = app_config.power_management_config
output += f'{tr("Power management")}: {power_management_config.power_management.value}'
output += '\n'
if app_config.firewall_config:
firewall_config = app_config.firewall_config
output += f'{tr("Firewall")}: {firewall_config.firewall.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') + ': ' + disk_layout_conf.disk_encryption.encryption_type.type_to_text() + '\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.enabled else tr('Disabled')
if item.value.enabled:
output += f'\n{tr("Compression algorithm")}: {item.value.algorithm.value}'
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_config(self, item: MenuItem) -> str | None:
bootloader_config: BootloaderConfiguration | None = item.value
if bootloader_config:
return bootloader_config.preview(self._uefi)
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.
"""
bootloader_config: BootloaderConfiguration | None = None
root_partition: PartitionModification | None = None
boot_partition: PartitionModification | None = None
efi_partition: PartitionModification | None = None
bootloader_config = self._item_group.find_by_key('bootloader_config').value
if not bootloader_config or bootloader_config.bootloader == Bootloader.NO_BOOTLOADER:
return None
bootloader = bootloader_config.bootloader
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 self._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 self._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'
elif bootloader == Bootloader.Refind:
if not self._uefi:
return 'rEFInd can only be used on UEFI systems'
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
async def _select_disk_config(
self,
preset: DiskLayoutConfiguration | None = None,
) -> DiskLayoutConfiguration | None:
disk_config = await DiskLayoutConfigurationMenu(preset).show()
return disk_config
async def _select_bootloader_config(
self,
preset: BootloaderConfiguration | None = None,
) -> BootloaderConfiguration | None:
if preset is None:
preset = BootloaderConfiguration.get_default(self._uefi, self._skip_boot)
bootloader_config = await BootloaderMenu(preset, self._uefi, self._skip_boot).show()
return bootloader_config
async def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None:
from archinstall.lib.profile.profile_menu import ProfileMenu
profile_config = await ProfileMenu(preset=current_profile).show()
return profile_config
async 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 = await select_additional_packages(
preset,
repositories=repositories,
)
return packages
async def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration | None:
if self._mirror_list_handler is None:
self._mirror_list_handler = MirrorListHandler()
mirror_configuration = await MirrorMenu(self._mirror_list_handler, preset=preset).run()
if mirror_configuration and 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()