diff --git a/archinstall/default_profiles/desktop.py b/archinstall/default_profiles/desktop.py index b40eb54e81..afeab0b6cf 100644 --- a/archinstall/default_profiles/desktop.py +++ b/archinstall/default_profiles/desktop.py @@ -51,12 +51,12 @@ def default_greeter_type(self) -> GreeterType | None: return None - def _do_on_select_profiles(self) -> None: + async def _do_on_select_profiles(self) -> None: for profile in self.current_selection: - profile.do_on_select() + await profile.do_on_select() @override - def do_on_select(self) -> SelectResult: + async def do_on_select(self) -> SelectResult: items = [ MenuItem( p.name, @@ -69,7 +69,7 @@ def do_on_select(self) -> SelectResult: group = MenuItemGroup(items, sort_items=True, sort_case_sensitive=False) group.set_selected_by_value(self.current_selection) - result = Selection[Self]( + result = await Selection[Self]( group, multi=True, allow_reset=True, @@ -80,7 +80,7 @@ def do_on_select(self) -> SelectResult: match result.type_: case ResultType.Selection: self.current_selection = result.get_values() - self._do_on_select_profiles() + await self._do_on_select_profiles() return SelectResult.NewSelection case ResultType.Skip: return SelectResult.SameSelection diff --git a/archinstall/default_profiles/desktops/hyprland.py b/archinstall/default_profiles/desktops/hyprland.py index a4c0ae593a..9593421140 100644 --- a/archinstall/default_profiles/desktops/hyprland.py +++ b/archinstall/default_profiles/desktops/hyprland.py @@ -45,7 +45,7 @@ def services(self) -> list[str]: return [pref] return [] - def _select_seat_access(self) -> None: + async def _select_seat_access(self) -> None: # need to activate seat service and add to seat group header = tr('Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)') header += '\n' + tr('Choose an option to give Hyprland access to your hardware') + '\n' @@ -56,7 +56,7 @@ def _select_seat_access(self) -> None: default = self.custom_settings.get('seat_access', None) group.set_default_by_value(default) - result = Selection[SeatAccess]( + result = await Selection[SeatAccess]( group, header=header, allow_skip=False, @@ -66,5 +66,5 @@ def _select_seat_access(self) -> None: self.custom_settings['seat_access'] = result.get_value().value @override - def do_on_select(self) -> None: - self._select_seat_access() + async def do_on_select(self) -> None: + await self._select_seat_access() diff --git a/archinstall/default_profiles/desktops/labwc.py b/archinstall/default_profiles/desktops/labwc.py index fd9558fcb6..b4ebd95539 100644 --- a/archinstall/default_profiles/desktops/labwc.py +++ b/archinstall/default_profiles/desktops/labwc.py @@ -42,7 +42,7 @@ def services(self) -> list[str]: return [pref] return [] - def _select_seat_access(self) -> None: + async def _select_seat_access(self) -> None: # need to activate seat service and add to seat group header = tr('labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)') header += '\n' + tr('Choose an option to give labwc access to your hardware') + '\n' @@ -53,7 +53,7 @@ def _select_seat_access(self) -> None: default = self.custom_settings.get('seat_access', None) group.set_default_by_value(default) - result = Selection[SeatAccess]( + result = await Selection[SeatAccess]( group, header=header, allow_skip=False, @@ -63,5 +63,5 @@ def _select_seat_access(self) -> None: self.custom_settings['seat_access'] = result.get_value().value @override - def do_on_select(self) -> None: - self._select_seat_access() + async def do_on_select(self) -> None: + await self._select_seat_access() diff --git a/archinstall/default_profiles/desktops/niri.py b/archinstall/default_profiles/desktops/niri.py index 4416b8c85d..4ba613c212 100644 --- a/archinstall/default_profiles/desktops/niri.py +++ b/archinstall/default_profiles/desktops/niri.py @@ -50,7 +50,7 @@ def services(self) -> list[str]: return [pref] return [] - def _select_seat_access(self) -> None: + async def _select_seat_access(self) -> None: # need to activate seat service and add to seat group header = tr('niri needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)') header += '\n' + tr('Choose an option to give niri access to your hardware') + '\n' @@ -61,7 +61,7 @@ def _select_seat_access(self) -> None: default = self.custom_settings.get('seat_access', None) group.set_default_by_value(default) - result = Selection[SeatAccess]( + result = await Selection[SeatAccess]( group, header=header, allow_skip=False, @@ -71,5 +71,5 @@ def _select_seat_access(self) -> None: self.custom_settings['seat_access'] = result.get_value().value @override - def do_on_select(self) -> None: - self._select_seat_access() + async def do_on_select(self) -> None: + await self._select_seat_access() diff --git a/archinstall/default_profiles/desktops/sway.py b/archinstall/default_profiles/desktops/sway.py index f9c9ecb3d0..7418ad49db 100644 --- a/archinstall/default_profiles/desktops/sway.py +++ b/archinstall/default_profiles/desktops/sway.py @@ -52,7 +52,7 @@ def services(self) -> list[str]: return [pref] return [] - def _select_seat_access(self) -> None: + async def _select_seat_access(self) -> None: # need to activate seat service and add to seat group header = tr('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)') header += '\n' + tr('Choose an option to give Sway access to your hardware') + '\n' @@ -63,7 +63,7 @@ def _select_seat_access(self) -> None: default = self.custom_settings.get('seat_access', None) group.set_default_by_value(default) - result = Selection[SeatAccess]( + result = await Selection[SeatAccess]( group, header=header, allow_skip=False, @@ -73,5 +73,5 @@ def _select_seat_access(self) -> None: self.custom_settings['seat_access'] = result.get_value().value @override - def do_on_select(self) -> None: - self._select_seat_access() + async def do_on_select(self) -> None: + await self._select_seat_access() diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index d977cd9aa7..df55f03fdb 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -117,7 +117,7 @@ def json(self) -> dict[str, str]: """ return {} - def do_on_select(self) -> SelectResult | None: + async def do_on_select(self) -> SelectResult | None: """ Hook that will be called when a profile is selected """ diff --git a/archinstall/default_profiles/server.py b/archinstall/default_profiles/server.py index 27164e2d05..6b1f5e7267 100644 --- a/archinstall/default_profiles/server.py +++ b/archinstall/default_profiles/server.py @@ -23,7 +23,7 @@ def __init__(self, current_value: list[Self] = []): ) @override - def do_on_select(self) -> SelectResult: + async def do_on_select(self) -> SelectResult: items = [ MenuItem( p.name, @@ -36,7 +36,7 @@ def do_on_select(self) -> SelectResult: group = MenuItemGroup(items, sort_items=True) group.set_selected_by_value(self.current_selection) - result = Selection[Self]( + result = await Selection[Self]( group, allow_reset=True, allow_skip=True, diff --git a/archinstall/lib/applications/application_menu.py b/archinstall/lib/applications/application_menu.py index c7af6c5eb2..5ef50441f3 100644 --- a/archinstall/lib/applications/application_menu.py +++ b/archinstall/lib/applications/application_menu.py @@ -39,8 +39,8 @@ def __init__( ) @override - def run(self) -> ApplicationConfiguration: - super().run() + async def show(self) -> ApplicationConfiguration | None: + _ = await super().show() return self._app_config def _define_menu_options(self) -> list[MenuItem]: @@ -116,13 +116,13 @@ def _prev_firewall(self, item: MenuItem) -> str | None: return None -def select_power_management(preset: PowerManagementConfiguration | None = None) -> PowerManagementConfiguration | None: +async def select_power_management(preset: PowerManagementConfiguration | None = None) -> PowerManagementConfiguration | None: group = MenuItemGroup.from_enum(PowerManagement) if preset: group.set_focus_by_value(preset.power_management) - result = Selection[PowerManagement]( + result = await Selection[PowerManagement]( group, allow_skip=True, allow_reset=True, @@ -137,11 +137,11 @@ def select_power_management(preset: PowerManagementConfiguration | None = None) return None -def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None: +async def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None: header = tr('Would you like to configure Bluetooth?') + '\n' preset_val = preset.enabled if preset else False - result = Confirmation( + result = await Confirmation( header=header, allow_skip=True, preset=preset_val, @@ -156,11 +156,11 @@ def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfigur raise ValueError('Unhandled result type') -def select_print_service(preset: PrintServiceConfiguration | None) -> PrintServiceConfiguration | None: +async def select_print_service(preset: PrintServiceConfiguration | None) -> PrintServiceConfiguration | None: header = tr('Would you like to configure the print service?') + '\n' preset_val = preset.enabled if preset else False - result = Confirmation( + result = await Confirmation( header=header, allow_skip=True, preset=preset_val, @@ -176,14 +176,14 @@ def select_print_service(preset: PrintServiceConfiguration | None) -> PrintServi raise ValueError('Unhandled result type') -def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None: +async def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None: items = [MenuItem(a.value, value=a) for a in Audio] group = MenuItemGroup(items) if preset: group.set_focus_by_value(preset.audio) - result = Selection[Audio]( + result = await Selection[Audio]( group, header=tr('Select audio configuration'), allow_skip=True, @@ -198,13 +198,13 @@ def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration raise ValueError('Unhandled result type') -def select_firewall(preset: FirewallConfiguration | None = None) -> FirewallConfiguration | None: +async def select_firewall(preset: FirewallConfiguration | None = None) -> FirewallConfiguration | None: group = MenuItemGroup.from_enum(Firewall) if preset: group.set_focus_by_value(preset.firewall) - result = Selection[Firewall]( + result = await Selection[Firewall]( group, allow_skip=True, allow_reset=True, diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index 7382dfda8f..12a9dbbbf1 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -28,6 +28,7 @@ from archinstall.lib.plugins import load_plugin from archinstall.lib.translationhandler import Language, tr, translation_handler from archinstall.lib.version import get_version +from archinstall.tui.ui.components import tui @p_dataclass @@ -491,16 +492,17 @@ def _process_creds_data(self, creds_data: str) -> dict[str, Any] | None: debug(f'Error decrypting credentials file: {err}') raise err from err else: - incorrect_password = False header = tr('Enter credentials file decryption password') + wrong_pwd_text = tr('Incorrect password') + prompt = header while True: - prompt = f'{header}\n\n' + tr('Incorrect password') if incorrect_password else '' - - decryption_pwd = get_password( - header=prompt, - allow_skip=False, - skip_confirmation=True, + decryption_pwd: Password | None = tui.run( + lambda p=prompt: get_password( # type: ignore[misc] + header=p, + allow_skip=False, + skip_confirmation=True, + ) ) if not decryption_pwd: @@ -512,7 +514,7 @@ def _process_creds_data(self, creds_data: str) -> dict[str, Any] | None: except ValueError as err: if 'Invalid password' in str(err): debug('Incorrect credentials file decryption password') - incorrect_password = True + prompt = f'{header}' + f'\n\n{wrong_pwd_text}' else: debug(f'Error decrypting credentials file: {err}') raise err from err diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py index 94404201ff..6bead8ca06 100644 --- a/archinstall/lib/authentication/authentication_menu.py +++ b/archinstall/lib/authentication/authentication_menu.py @@ -30,8 +30,8 @@ def __init__(self, preset: AuthenticationConfiguration | None = None): ) @override - def run(self) -> AuthenticationConfiguration | None: - return super().run() + async def show(self) -> AuthenticationConfiguration | None: + return await super().show() def _define_menu_options(self) -> list[MenuItem]: return [ @@ -56,9 +56,9 @@ def _define_menu_options(self) -> list[MenuItem]: ), ] - def _create_user_account(self, preset: list[User] | None = None) -> list[User]: + async def _create_user_account(self, preset: list[User] | None = None) -> list[User]: preset = [] if preset is None else preset - users = select_users(preset=preset) + users = await select_users(preset=preset) return users def _prev_users(self, item: MenuItem) -> str | None: @@ -99,12 +99,12 @@ def _prev_u2f_login(self, item: MenuItem) -> str | None: return None -def select_root_password() -> Password | None: - password = get_password(header=tr('Enter root password'), allow_skip=True) +async def select_root_password() -> Password | None: + password = await get_password(header=tr('Enter root password'), allow_skip=True) return password -def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConfiguration | None: +async def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConfiguration | None: devices = Fido2.get_fido2_devices() if not devices: return None @@ -118,7 +118,7 @@ def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConfigurat if preset is not None: group.set_selected_by_value(preset.u2f_login_method) - result = Selection[U2FLoginMethod]( + result = await Selection[U2FLoginMethod]( group, allow_skip=True, allow_reset=True, @@ -129,7 +129,7 @@ def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConfigurat u2f_method = result.get_value() header = tr('Enable passwordless sudo?') - result_sudo = Confirmation( + result_sudo = await Confirmation( header=header, allow_skip=True, preset=False, diff --git a/archinstall/lib/bootloader/bootloader_menu.py b/archinstall/lib/bootloader/bootloader_menu.py index 14fff91c5b..37eaeb8d21 100644 --- a/archinstall/lib/bootloader/bootloader_menu.py +++ b/archinstall/lib/bootloader/bootloader_menu.py @@ -86,12 +86,12 @@ def _prev_removable(self, item: MenuItem) -> str | None: return tr('Will install to custom location with NVRAM entry') @override - def run(self) -> BootloaderConfiguration: - super().run() + async def show(self) -> BootloaderConfiguration: + _ = await super().show() return self._bootloader_conf - def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None: - bootloader = select_bootloader(preset, self._uefi, self._skip_boot) + async def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None: + bootloader = await select_bootloader(preset, self._uefi, self._skip_boot) if bootloader: # Update UKI option based on bootloader @@ -117,10 +117,10 @@ def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None: return bootloader - def _select_uki(self, preset: bool) -> bool: + async def _select_uki(self, preset: bool) -> bool: prompt = tr('Would you like to use unified kernel images?') + '\n' - result = Confirmation(header=prompt, allow_skip=True, preset=preset).show() + result = await Confirmation(header=prompt, allow_skip=True, preset=preset).show() match result.type_: case ResultType.Skip: @@ -130,7 +130,7 @@ def _select_uki(self, preset: bool) -> bool: case ResultType.Reset: raise ValueError('Unhandled result type') - def _select_removable(self, preset: bool) -> bool: + async def _select_removable(self, preset: bool) -> bool: prompt = ( tr('Would you like to install the bootloader to the default removable media search location?') + '\n\n' @@ -162,7 +162,7 @@ def _select_removable(self, preset: bool) -> bool: + '\n' ) - result = Confirmation( + result = await Confirmation( header=prompt, allow_skip=True, preset=preset, @@ -177,7 +177,7 @@ def _select_removable(self, preset: bool) -> bool: raise ValueError('Unhandled result type') -def select_bootloader( +async def select_bootloader( preset: Bootloader | None, uefi: bool, skip_boot: bool = False, @@ -202,7 +202,7 @@ def select_bootloader( group.set_default_by_value(default) group.set_focus_by_value(preset) - result = Selection[Bootloader]( + result = await Selection[Bootloader]( group, header=header, allow_skip=True, diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index 9398a083bb..39d39511a8 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -58,14 +58,14 @@ def write_debug(self) -> None: debug(' -- Chosen configuration --') debug(self.user_config_to_json()) - def confirm_config(self) -> bool: + async def confirm_config(self) -> bool: header = f'{tr("The specified configuration will be applied")}. ' header += tr('Would you like to continue?') + '\n' group = MenuItemGroup.yes_no() group.set_preview_for_all(lambda x: self.user_config_to_json()) - result = Confirmation( + result = await Confirmation( group=group, header=header, allow_skip=False, @@ -123,7 +123,7 @@ def save( self.save_user_creds(save_path, password=password) -def save_config(config: ArchConfig) -> None: +async def save_config(config: ArchConfig) -> None: def preview(item: MenuItem) -> str | None: match item.value: case 'user_config': @@ -161,7 +161,7 @@ def preview(item: MenuItem) -> str | None: ] group = MenuItemGroup(items) - result = Selection[str]( + result = await Selection[str]( group, allow_skip=True, preview_location='right', @@ -178,7 +178,7 @@ def preview(item: MenuItem) -> str | None: readline.set_completer_delims('\t\n=') readline.parse_and_bind('tab: complete') - dest_path = prompt_dir( + dest_path = await prompt_dir( tr('Enter a directory for the configuration(s) to be saved') + '\n', allow_skip=True, ) @@ -188,7 +188,7 @@ def preview(item: MenuItem) -> str | None: header = tr('Do you want to save the configuration file(s) to {}?').format(dest_path) - save_result = Confirmation( + save_result = await Confirmation( header=header, allow_skip=False, preset=True, @@ -205,7 +205,7 @@ def preview(item: MenuItem) -> str | None: header = tr('Do you want to encrypt the user_credentials.json file?') - enc_result = Confirmation( + enc_result = await Confirmation( header=header, allow_skip=False, preset=False, @@ -214,7 +214,7 @@ def preview(item: MenuItem) -> str | None: enc_password: str | None = None if enc_result.type_ == ResultType.Selection: if enc_result.get_value(): - password = get_password( + password = await get_password( header=tr('Credentials file encryption password'), allow_skip=True, ) diff --git a/archinstall/lib/disk/disk_menu.py b/archinstall/lib/disk/disk_menu.py index 5d91197f7d..2ca8925a7e 100644 --- a/archinstall/lib/disk/disk_menu.py +++ b/archinstall/lib/disk/disk_menu.py @@ -93,8 +93,8 @@ def _define_menu_options(self) -> list[MenuItem]: ] @override - def run(self) -> DiskLayoutConfiguration | None: # type: ignore[override] - config: DiskMenuConfig | None = super().run() + async def show(self) -> DiskLayoutConfiguration | None: # type: ignore[override] + config: DiskMenuConfig | None = await super().show() if config is None: return None @@ -122,7 +122,7 @@ def _check_dep_btrfs(self) -> bool: return False - def _select_disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None: + async def _select_disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None: disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value lvm_config: LvmConfiguration | None = self._item_group.find_by_key('lvm_config').value @@ -134,12 +134,12 @@ def _select_disk_encryption(self, preset: DiskEncryption | None) -> DiskEncrypti if not DiskEncryption.validate_enc(modifications, lvm_config): return None - disk_encryption = DiskEncryptionMenu(modifications, lvm_config=lvm_config, preset=preset).run() + disk_encryption = await DiskEncryptionMenu(modifications, lvm_config=lvm_config, preset=preset).show() return disk_encryption - def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> DiskLayoutConfiguration | None: - disk_config = select_disk_config(preset) + async def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> DiskLayoutConfiguration | None: + disk_config = await select_disk_config(preset) if disk_config != preset: self._menu_item_group.find_by_key('lvm_config').value = None @@ -147,20 +147,20 @@ def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> return disk_config - def _select_lvm_config(self, preset: LvmConfiguration | None) -> LvmConfiguration | None: + async def _select_lvm_config(self, preset: LvmConfiguration | None) -> LvmConfiguration | None: disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value if not disk_config: return preset - lvm_config = select_lvm_config(disk_config, preset=preset) + lvm_config = await select_lvm_config(disk_config, preset=preset) if lvm_config != preset: self._menu_item_group.find_by_key('disk_encryption').value = None return lvm_config - def _select_btrfs_snapshots(self, preset: SnapshotConfig | None) -> SnapshotConfig | None: + async def _select_btrfs_snapshots(self, preset: SnapshotConfig | None) -> SnapshotConfig | None: preset_type = preset.snapshot_type if preset else None group = MenuItemGroup.from_enum( @@ -169,7 +169,7 @@ def _select_btrfs_snapshots(self, preset: SnapshotConfig | None) -> SnapshotConf preset=preset_type, ) - result = Selection[SnapshotType]( + result = await Selection[SnapshotType]( group, allow_reset=True, allow_skip=True, diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 10babd9195..b3950f05d9 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -98,9 +98,9 @@ def _define_menu_options(self) -> list[MenuItem]: ), ] - def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]: + async def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]: if self._lvm_config: - return select_lvm_vols_to_encrypt(self._lvm_config, preset=preset) + return await select_lvm_vols_to_encrypt(self._lvm_config, preset=preset) return [] def _check_dep_enc_type(self) -> bool: @@ -122,8 +122,8 @@ def _check_dep_lvm_vols(self) -> bool: return False @override - def run(self) -> DiskEncryption | None: - enc_config = super().run() + async def show(self) -> DiskEncryption | None: + enc_config = await super().show() if enc_config is None: return None @@ -233,7 +233,7 @@ def _prev_iter_time(self, item: MenuItem) -> str | None: return None -def select_encryption_type( +async def select_encryption_type( lvm_config: LvmConfiguration | None = None, preset: EncryptionType | None = None, ) -> EncryptionType | None: @@ -253,7 +253,7 @@ def select_encryption_type( group = MenuItemGroup(items) group.set_focus_by_value(preset_value) - result = Selection[EncryptionType]( + result = await Selection[EncryptionType]( group, header=tr('Select encryption type'), allow_skip=True, @@ -269,9 +269,9 @@ def select_encryption_type( return result.get_value() -def select_encrypted_password() -> Password | None: +async def select_encrypted_password() -> Password | None: header = tr('Enter disk encryption password (leave blank for no encryption)') + '\n' - password = get_password( + password = await get_password( header=header, allow_skip=True, ) @@ -279,7 +279,7 @@ def select_encrypted_password() -> Password | None: return password -def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None: +async def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None: header = tr('Select a FIDO2 device to use for HSM') + '\n' try: @@ -290,7 +290,7 @@ def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None: if fido_devices: group = MenuHelper(data=fido_devices).create_menu_group() - result = Selection[Fido2Device]( + result = await Selection[Fido2Device]( group, header=header, allow_skip=True, @@ -307,7 +307,7 @@ def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None: return None -def select_partitions_to_encrypt( +async def select_partitions_to_encrypt( modification: list[DeviceModification], preset: list[PartitionModification], ) -> list[PartitionModification]: @@ -324,7 +324,7 @@ def select_partitions_to_encrypt( group = MenuItemGroup.from_objects(partitions) group.set_selected_by_value(preset) - result = Table[PartitionModification]( + result = await Table[PartitionModification]( header=tr('Select disks for the installation'), group=group, allow_skip=True, @@ -343,7 +343,7 @@ def select_partitions_to_encrypt( return [] -def select_lvm_vols_to_encrypt( +async def select_lvm_vols_to_encrypt( lvm_config: LvmConfiguration, preset: list[LvmVolume], ) -> list[LvmVolume]: @@ -353,7 +353,7 @@ def select_lvm_vols_to_encrypt( group = MenuItemGroup.from_objects(volumes) group.set_selected_by_value(preset) - result = Table[LvmVolume]( + result = await Table[LvmVolume]( header=tr('Select disks for the installation'), group=group, allow_skip=True, @@ -372,7 +372,7 @@ def select_lvm_vols_to_encrypt( return [] -def select_iteration_time(preset: int | None = None) -> int | None: +async def select_iteration_time(preset: int | None = None) -> int | None: header = tr('Enter iteration time for LUKS encryption (in milliseconds)') + '\n' header += tr('Higher values increase security but slow down boot time') + '\n' header += tr(f'Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000') + '\n' @@ -388,7 +388,7 @@ def validate_iter_time(value: str) -> str | None: except ValueError: return tr('Please enter a valid number') - result = Input( + result = await Input( header=header, allow_skip=True, default_value=str(preset) if preset else str(DEFAULT_ITER_TIME), diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 61e85beb1d..f15ac52aa5 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -12,7 +12,6 @@ lvm_vol_reduce, ) from archinstall.lib.disk.utils import udev_sync -from archinstall.lib.interactions.general_conf import confirm_abort from archinstall.lib.luks import Luks2 from archinstall.lib.models.device import ( DiskEncryption, @@ -29,7 +28,6 @@ Unit, ) from archinstall.lib.output import debug, info -from archinstall.lib.translationhandler import tr class FilesystemHandler: @@ -37,7 +35,7 @@ def __init__(self, disk_config: DiskLayoutConfiguration): self._disk_config = disk_config self._enc_config = disk_config.disk_encryption - def perform_filesystem_operations(self, show_countdown: bool = True) -> None: + def perform_filesystem_operations(self) -> None: if self._disk_config.config_type == DiskLayoutType.Pre_mount: debug('Disk layout configuration is set to pre-mount, not performing any operations') return @@ -48,9 +46,6 @@ def perform_filesystem_operations(self, show_countdown: bool = True) -> None: debug('No modifications required') return - if show_countdown: - self._final_warning() - # Setup the blockdevice, filesystem (and optionally encryption). # Once that's done, we'll hand over to perform_installation() @@ -330,19 +325,3 @@ def _lvm_vol_handle_e2scrub(self, vol_gp: LvmVolumeGroup) -> None: largest_vol.safe_dev_path, Size(256, Unit.MiB, SectorSize.default()), ) - - def _final_warning(self) -> bool: - # Issue a final warning before we continue with something un-revertable. - # We count down from 5 to 0. - out = tr('Starting device modifications in ') - print(out, end='', flush=True) - - try: - countdown = '\n5...4...3...2...1\n' - for c in countdown: - print(c, end='', flush=True) - time.sleep(0.25) - except KeyboardInterrupt: - confirm_abort() - - return True diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index f2e45f809e..4839626dfc 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -189,8 +189,8 @@ def as_segments(self, device_partitions: list[PartitionModification]) -> list[Di def get_part_mods(disk_segments: list[DiskSegment]) -> list[PartitionModification]: return [s.segment for s in disk_segments if isinstance(s.segment, PartitionModification)] - def show(self) -> DeviceModification | None: - disk_segments = super()._run() + async def show(self) -> DeviceModification | None: + disk_segments = await super()._run() if not disk_segments: return None @@ -199,12 +199,12 @@ def show(self) -> DeviceModification | None: return DeviceModification(self._device, self._wipe, partitions) @override - def _run_actions_on_entry(self, entry: DiskSegment) -> None: + async def _run_actions_on_entry(self, entry: DiskSegment) -> None: # Do not create a menu when the segment is free space if isinstance(entry.segment, FreeSpace): - self._data = self.handle_action('', entry, self._data) + self._data = await self.handle_action('', entry, self._data) else: - super()._run_actions_on_entry(entry) + await super()._run_actions_on_entry(entry) @override def selected_action_display(self, selection: DiskSegment) -> str: @@ -267,7 +267,7 @@ def filter_options(self, selection: DiskSegment, options: list[str]) -> list[str return [o for o in options if o not in not_filter] @override - def handle_action( + async def handle_action( self, action: str, entry: DiskSegment | None, @@ -278,20 +278,20 @@ def handle_action( match action_key: case 'suggest_partition_layout': part_mods = self.get_part_mods(data) - device_mod = self._suggest_partition_layout(part_mods) + device_mod = await self._suggest_partition_layout(part_mods) if device_mod and device_mod.partitions: data = self.as_segments(device_mod.partitions) self._wipe = device_mod.wipe self._prompt = self._info + self.wipe_str() case 'remove_added_partitions': - if self._reset_confirmation(): + if await self._reset_confirmation(): data = [s for s in data if isinstance(s.segment, PartitionModification) and s.segment.is_exists_or_modify()] elif isinstance(entry.segment, PartitionModification): partition = entry.segment action_key = [k for k, v in self._actions.items() if v == action][0] match action_key: case 'assign_mountpoint': - new_mountpoint = self._prompt_mountpoint() + new_mountpoint = await self._prompt_mountpoint() if not partition.is_swap(): if partition.is_home(): partition.invert_flag(PartitionFlag.LINUX_HOME) @@ -307,7 +307,7 @@ def handle_action( partition.flags = [] partition.set_flag(PartitionFlag.LINUX_HOME) case 'mark_formatting': - self._prompt_formatting(partition) + await self._prompt_formatting(partition) case 'mark_bootable': if not partition.is_swap(): partition.invert_flag(PartitionFlag.BOOT) @@ -322,7 +322,7 @@ def handle_action( partition.invert_flag(PartitionFlag.ESP) partition.invert_flag(PartitionFlag.XBOOTLDR) case 'set_filesystem': - fs_type = self._prompt_partition_fs_type() + fs_type = await self._prompt_partition_fs_type() if partition.is_swap(): partition.invert_flag(PartitionFlag.SWAP) @@ -339,13 +339,14 @@ def handle_action( case 'btrfs_mark_nodatacow': self._toggle_mount_option(partition, BtrfsMountOption.nodatacow) case 'btrfs_set_subvolumes': - self._set_btrfs_subvolumes(partition) + await self._set_btrfs_subvolumes(partition) case 'delete_partition': data = self._delete_partition(partition, data) else: part_mods = self.get_part_mods(data) index = data.index(entry) - part_mods.insert(index, self._create_new_partition(entry.segment)) + part = await self._create_new_partition(entry.segment) + part_mods.insert(index, part) data = self.as_segments(part_mods) return data @@ -378,8 +379,8 @@ def _toggle_mount_option( else: partition.mount_options = [o for o in partition.mount_options if o != option.value] - def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None: - subvols = SubvolumeMenu( + async def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None: + subvols = await SubvolumeMenu( partition.btrfs_subvols, None, ).show() @@ -387,7 +388,7 @@ def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None: if subvols is not None: partition.btrfs_subvols = subvols - def _prompt_formatting(self, partition: PartitionModification) -> None: + async def _prompt_formatting(self, partition: PartitionModification) -> None: # an existing partition can toggle between Exist or Modify if partition.is_modify(): partition.status = ModificationStatus.Exist @@ -400,27 +401,27 @@ def _prompt_formatting(self, partition: PartitionModification) -> None: # it's safe to change the filesystem for this partition. if partition.fs_type == FilesystemType.Crypto_luks: prompt = tr('This partition is currently encrypted, to format it a filesystem has to be specified') + '\n' - fs_type = self._prompt_partition_fs_type(prompt) + fs_type = await self._prompt_partition_fs_type(prompt) partition.fs_type = fs_type if fs_type == FilesystemType.Btrfs: partition.mountpoint = None - def _prompt_mountpoint(self) -> Path: + async def _prompt_mountpoint(self) -> Path: header = tr('Partition mount-points are relative to inside the installation, the boot would be /boot as an example.') + '\n\n' header += tr('Enter a mountpoint') - mountpoint = prompt_dir(header, validate=False, allow_skip=False) + mountpoint = await prompt_dir(header, validate=False, allow_skip=False) assert mountpoint return mountpoint - def _prompt_partition_fs_type(self, prompt: str | None = None) -> FilesystemType: + async def _prompt_partition_fs_type(self, prompt: str | None = None) -> FilesystemType: fs_types = filter(lambda fs: fs != FilesystemType.Crypto_luks, FilesystemType) items = [MenuItem(fs.value, value=fs) for fs in fs_types] group = MenuItemGroup(items, sort_items=False) - result = Selection[FilesystemType]( + result = await Selection[FilesystemType]( group, header=prompt, allow_skip=False, @@ -464,7 +465,7 @@ def _validate_value( return size - def _prompt_size(self, free_space: FreeSpace) -> Size: + async def _prompt_size(self, free_space: FreeSpace) -> Size: def validate(value: str | None) -> str | None: if not value: return None @@ -491,7 +492,7 @@ def validate(value: str | None) -> str | None: max_size = free_space.length prompt += tr('Enter a size (default: {}): ').format(max_size.format_highest()) - result = Input( + result = await Input( header=f'{prompt}\b', allow_skip=True, validator_callback=validate, @@ -515,14 +516,14 @@ def validate(value: str | None) -> str | None: assert size return size - def _create_new_partition(self, free_space: FreeSpace) -> PartitionModification: - length = self._prompt_size(free_space) + async def _create_new_partition(self, free_space: FreeSpace) -> PartitionModification: + length = await self._prompt_size(free_space) - fs_type = self._prompt_partition_fs_type() + fs_type = await self._prompt_partition_fs_type() mountpoint = None if fs_type not in (FilesystemType.Btrfs, FilesystemType.LinuxSwap): - mountpoint = self._prompt_mountpoint() + mountpoint = await self._prompt_mountpoint() partition = PartitionModification( status=ModificationStatus.Create, @@ -544,10 +545,10 @@ def _create_new_partition(self, free_space: FreeSpace) -> PartitionModification: return partition - def _reset_confirmation(self) -> bool: + async def _reset_confirmation(self) -> bool: prompt = tr('This will remove all newly added partitions, continue?') + '\n' - result = Confirmation( + result = await Confirmation( header=prompt, allow_skip=False, allow_reset=False, @@ -555,27 +556,27 @@ def _reset_confirmation(self) -> bool: return result.item() == MenuItem.yes() - def _suggest_partition_layout( + async def _suggest_partition_layout( self, data: list[PartitionModification], ) -> DeviceModification | None: # if modifications have been done already, inform the user # that this operation will erase those modifications if any([not entry.exists() for entry in data]): - if not self._reset_confirmation(): + if not await self._reset_confirmation(): return None from archinstall.lib.interactions.disk_conf import suggest_single_disk_layout - return suggest_single_disk_layout(self._device) + return await suggest_single_disk_layout(self._device) -def manual_partitioning( +async def manual_partitioning( device_mod: DeviceModification, partition_table: PartitionTable, ) -> DeviceModification | None: menu_list = PartitioningList(device_mod, partition_table) - mod = menu_list.show() + mod = await menu_list.show() if not mod: return None diff --git a/archinstall/lib/disk/subvolume_menu.py b/archinstall/lib/disk/subvolume_menu.py index f84da1e93a..0f8da9e798 100644 --- a/archinstall/lib/disk/subvolume_menu.py +++ b/archinstall/lib/disk/subvolume_menu.py @@ -28,20 +28,20 @@ def __init__( prompt, ) - def show(self) -> list[SubvolumeModification] | None: - return super()._run() + async def show(self) -> list[SubvolumeModification] | None: + return await super()._run() @override def selected_action_display(self, selection: SubvolumeModification) -> str: return str(selection.name) - def _add_subvolume(self, preset: SubvolumeModification | None = None) -> SubvolumeModification | None: + async def _add_subvolume(self, preset: SubvolumeModification | None = None) -> SubvolumeModification | None: def validate(value: str | None) -> str | None: if value: return None return tr('Value cannot be empty') - result = Input( + result = await Input( header=tr('Enter subvolume name'), allow_skip=True, default_value=str(preset.name) if preset else None, @@ -61,7 +61,7 @@ def validate(value: str | None) -> str | None: header = f'{tr("Subvolume name")}: {name}\n\n' header += tr('Enter subvolume mountpoint') - path = prompt_dir( + path = await prompt_dir( header=header, allow_skip=True, validate=True, @@ -74,14 +74,14 @@ def validate(value: str | None) -> str | None: return SubvolumeModification(Path(name), path) @override - def handle_action( + async def handle_action( self, action: str, entry: SubvolumeModification | None, data: list[SubvolumeModification], ) -> list[SubvolumeModification]: if action == self._actions[0]: - new_subvolume = self._add_subvolume() + new_subvolume = await self._add_subvolume() if new_subvolume is not None: # in case a user with the same username as an existing user @@ -90,7 +90,7 @@ def handle_action( data += [new_subvolume] elif entry is not None: if action == self._actions[1]: - new_subvolume = self._add_subvolume(entry) + new_subvolume = await self._add_subvolume(entry) if new_subvolume is not None: # we'll remove the original subvolume and add the modified version diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index b64661708f..bb5a9e326b 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -1,4 +1,3 @@ -import sys from typing import override from archinstall.lib.applications.application_menu import ApplicationMenu @@ -11,7 +10,7 @@ from archinstall.lib.interactions.general_conf import add_number_of_parallel_downloads, select_hostname, select_ntp, select_timezone from archinstall.lib.interactions.system_conf import select_kernel, select_swap from archinstall.lib.locale.locale_menu import LocaleMenu -from archinstall.lib.menu.abstract_menu import CONFIG_KEY, AbstractMenu +from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey from archinstall.lib.mirrors import MirrorListHandler, MirrorMenu from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration @@ -170,30 +169,29 @@ def _get_menu_options(self) -> list[MenuItem]: MenuItem( text=tr('Save configuration'), action=lambda x: self._safe_config(), - key=f'{CONFIG_KEY}_save', + key=SpecialMenuKey.SAVE.value, ), MenuItem( text=tr('Install'), preview_action=self._prev_install_invalid_config, - key=f'{CONFIG_KEY}_install', + key=SpecialMenuKey.INSTALL.value, ), MenuItem( text=tr('Abort'), - action=lambda x: sys.exit(1), - key=f'{CONFIG_KEY}_abort', + key=SpecialMenuKey.ABORT.value, ), ] return menu_options - def _safe_config(self) -> None: + 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() - save_config(self._arch_config) + await save_config(self._arch_config) def _missing_configs(self) -> list[str]: item: MenuItem = self._item_group.find_by_key('auth_config') @@ -224,7 +222,7 @@ def has_superuser() -> bool: return list(missing) @override - def _is_config_valid(self) -> bool: + def is_config_valid(self) -> bool: """ Checks the validity of the current configuration. """ @@ -232,10 +230,10 @@ def _is_config_valid(self) -> bool: return False return self._validate_bootloader() is None - def _select_archinstall_language(self, preset: Language) -> Language: + async def _select_archinstall_language(self, preset: Language) -> Language: from archinstall.lib.interactions.general_conf import select_archinstall_language - language = select_archinstall_language(translation_handler.translated_languages, preset) + language = await select_archinstall_language(translation_handler.translated_languages, preset) translation_handler.activate(language) self._update_lang_text() @@ -249,12 +247,12 @@ def _prev_archinstall_language(self, item: MenuItem) -> str | None: lang: Language = item.value return f'{tr("Language")}: {lang.display_name}' - def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None: - app_config = ApplicationMenu(preset).run() + async def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None: + app_config = await ApplicationMenu(preset).show() return app_config - def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None: - auth_config = AuthenticationMenu(preset).run() + 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: @@ -268,8 +266,8 @@ def _update_lang_text(self) -> None: 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() + 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: @@ -427,9 +425,6 @@ def _validate_bootloader(self) -> str | None: 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_config: BootloaderConfiguration | None = None root_partition: PartitionModification | None = None @@ -512,50 +507,49 @@ def _prev_profile(self, item: MenuItem) -> str | None: return None - def _select_disk_config( + async def _select_disk_config( self, preset: DiskLayoutConfiguration | None = None, ) -> DiskLayoutConfiguration | None: - disk_config = DiskLayoutConfigurationMenu(preset).run() - + disk_config = await DiskLayoutConfigurationMenu(preset).show() return disk_config - def _select_bootloader_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 = BootloaderMenu(preset, self._uefi, self._skip_boot).run() + bootloader_config = await BootloaderMenu(preset, self._uefi, self._skip_boot).show() return bootloader_config - def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None: + async def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None: from archinstall.lib.profile.profile_menu import ProfileMenu - profile_config = ProfileMenu(preset=current_profile).run() + profile_config = await ProfileMenu(preset=current_profile).show() return profile_config - def _select_additional_packages(self, preset: list[str]) -> list[str]: + 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 = select_additional_packages( + packages = await select_additional_packages( preset, repositories=repositories, ) return packages - def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration | None: + 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 = MirrorMenu(self._mirror_list_handler, preset=preset).run() + 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 diff --git a/archinstall/lib/interactions/disk_conf.py b/archinstall/lib/interactions/disk_conf.py index b9a5767ee9..19f0096a9a 100644 --- a/archinstall/lib/interactions/disk_conf.py +++ b/archinstall/lib/interactions/disk_conf.py @@ -32,7 +32,7 @@ from archinstall.tui.ui.result import ResultType -def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | None: +async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | None: def _preview_device_selection(item: MenuItem) -> str | None: device: _DeviceInfo = item.value # type: ignore[assignment] dev = device_handler.get_device(device.path) @@ -60,7 +60,7 @@ def _preview_device_selection(item: MenuItem) -> str | None: group = MenuItemGroup(items) group.set_selected_by_value(presets) - result = Table[_DeviceInfo]( + result = await Table[_DeviceInfo]( header=tr('Select disks for the installation'), group=group, presets=presets, @@ -86,24 +86,24 @@ def _preview_device_selection(item: MenuItem) -> str | None: return selected_devices -def get_default_partition_layout( +async def get_default_partition_layout( devices: list[BDevice], filesystem_type: FilesystemType | None = None, ) -> list[DeviceModification]: if len(devices) == 1: - device_modification = suggest_single_disk_layout( + device_modification = await suggest_single_disk_layout( devices[0], filesystem_type=filesystem_type, ) return [device_modification] else: - return suggest_multi_disk_layout( + return await suggest_multi_disk_layout( devices, filesystem_type=filesystem_type, ) -def _manual_partitioning( +async def _manual_partitioning( preset: list[DeviceModification], devices: list[BDevice], ) -> list[DeviceModification] | None: @@ -114,7 +114,7 @@ def _manual_partitioning( if not mod: mod = DeviceModification(device, wipe=False) - device_mod = manual_partitioning(mod, device_handler.partition_table) + device_mod = await manual_partitioning(mod, device_handler.partition_table) if not device_mod: return None @@ -124,7 +124,7 @@ def _manual_partitioning( return modifications -def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLayoutConfiguration | None: +async def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLayoutConfiguration | None: default_layout = DiskLayoutType.Default.display_msg() manual_mode = DiskLayoutType.Manual.display_msg() pre_mount_mode = DiskLayoutType.Pre_mount.display_msg() @@ -139,7 +139,7 @@ def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLay if preset: group.set_selected_by_value(preset.config_type.display_msg()) - result = Selection[str]( + result = await Selection[str]( group, header=tr('Select a disk configuration'), allow_skip=True, @@ -159,7 +159,7 @@ def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLay output += tr('You will use whatever drive-setup is mounted at the specified directory') + '\n' output += tr("WARNING: Archinstall won't check the suitability of this setup") - path = prompt_dir(output, allow_skip=True) + path = await prompt_dir(output, allow_skip=True) if path is None: return None @@ -173,13 +173,13 @@ def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLay ) preset_devices = [mod.device for mod in preset.device_modifications] if preset else [] - devices = select_devices(preset_devices) + devices = await select_devices(preset_devices) if devices is None: return preset if result.get_value() == default_layout: - modifications = get_default_partition_layout(devices) + modifications = await get_default_partition_layout(devices) if modifications: return DiskLayoutConfiguration( config_type=DiskLayoutType.Default, @@ -187,7 +187,7 @@ def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLay ) elif result.get_value() == manual_mode: preset_mods = preset.device_modifications if preset else [] - partitions = _manual_partitioning(preset_mods, devices) + partitions = await _manual_partitioning(preset_mods, devices) if not partitions: return preset @@ -200,7 +200,7 @@ def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLay return None -def select_lvm_config( +async def select_lvm_config( disk_config: DiskLayoutConfiguration, preset: LvmConfiguration | None = None, ) -> LvmConfiguration | None: @@ -211,7 +211,7 @@ def select_lvm_config( group = MenuItemGroup(items) group.set_focus_by_value(preset_value) - result = Selection[str]( + result = await Selection[str]( group, allow_reset=True, allow_skip=True, @@ -224,7 +224,7 @@ def select_lvm_config( return None case ResultType.Selection: if result.get_value() == default_mode: - return suggest_lvm_layout(disk_config) + return await suggest_lvm_layout(disk_config) return None @@ -248,7 +248,7 @@ def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModifi ) -def select_main_filesystem_format() -> FilesystemType: +async def select_main_filesystem_format() -> FilesystemType: items = [ MenuItem('btrfs', value=FilesystemType.Btrfs), MenuItem('ext4', value=FilesystemType.Ext4), @@ -257,7 +257,7 @@ def select_main_filesystem_format() -> FilesystemType: ] group = MenuItemGroup(items, sort_items=False) - result = Selection[FilesystemType]( + result = await Selection[FilesystemType]( group, header=tr('Select main filesystem'), allow_skip=False, @@ -270,7 +270,7 @@ def select_main_filesystem_format() -> FilesystemType: raise ValueError('Unhandled result type') -def select_mount_options() -> list[str]: +async def select_mount_options() -> list[str]: prompt = tr('Would you like to use compression or disable CoW?') + '\n' compression = tr('Use compression') disable_cow = tr('Disable Copy-on-Write') @@ -281,7 +281,7 @@ def select_mount_options() -> list[str]: ] group = MenuItemGroup(items, sort_items=False) - result = Selection[str]( + result = await Selection[str]( group, header=prompt, allow_skip=True, @@ -323,13 +323,13 @@ def get_default_btrfs_subvols() -> list[SubvolumeModification]: ] -def suggest_single_disk_layout( +async def suggest_single_disk_layout( device: BDevice, filesystem_type: FilesystemType | None = None, separate_home: bool | None = None, ) -> DeviceModification: if not filesystem_type: - filesystem_type = select_main_filesystem_format() + filesystem_type = await select_main_filesystem_format() sector_size = device.device_info.sector_size total_size = device.device_info.total_size @@ -339,14 +339,14 @@ def suggest_single_disk_layout( if filesystem_type == FilesystemType.Btrfs: prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n' - result = Confirmation( + result = await Confirmation( header=prompt, allow_skip=False, preset=True, ).show() using_subvolumes = result.item() == MenuItem.yes() - mount_options = select_mount_options() + mount_options = await select_mount_options() else: using_subvolumes = False mount_options = [] @@ -372,7 +372,7 @@ def suggest_single_disk_layout( else: prompt = tr('Would you like to create a separate partition for /home?') + '\n' - result = Confirmation( + result = await Confirmation( header=prompt, allow_skip=False, preset=True, @@ -429,7 +429,7 @@ def suggest_single_disk_layout( return device_modification -def suggest_multi_disk_layout( +async def suggest_multi_disk_layout( devices: list[BDevice], filesystem_type: FilesystemType | None = None, ) -> list[DeviceModification]: @@ -445,7 +445,7 @@ def suggest_multi_disk_layout( mount_options = [] if not filesystem_type: - filesystem_type = select_main_filesystem_format() + filesystem_type = await select_main_filesystem_format() # find proper disk for /home possible_devices = [d for d in devices if d.device_info.total_size >= min_home_partition_size] @@ -466,11 +466,11 @@ def suggest_multi_disk_layout( text += tr('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(Unit.GiB)) text += tr('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(Unit.GiB)) - Notify(text).show() + _ = await Notify(text).show() return [] if filesystem_type == FilesystemType.Btrfs: - mount_options = select_mount_options() + mount_options = await select_mount_options() device_paths = ', '.join(str(d.device_info.path) for d in devices) @@ -536,7 +536,7 @@ def suggest_multi_disk_layout( return [root_device_modification, home_device_modification] -def suggest_lvm_layout( +async def suggest_lvm_layout( disk_config: DiskLayoutConfiguration, filesystem_type: FilesystemType | None = None, vg_grp_name: str = 'ArchinstallVg', @@ -550,14 +550,14 @@ def suggest_lvm_layout( mount_options = [] if not filesystem_type: - filesystem_type = select_main_filesystem_format() + filesystem_type = await select_main_filesystem_format() if filesystem_type == FilesystemType.Btrfs: prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n' - result = Confirmation(header=prompt, allow_skip=False, preset=True).show() + result = await Confirmation(header=prompt, allow_skip=False, preset=True).show() using_subvolumes = MenuItem.yes() == result.item() - mount_options = select_mount_options() + mount_options = await select_mount_options() if using_subvolumes: btrfs_subvols = get_default_btrfs_subvols() diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 750e687238..5a95762ead 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -1,4 +1,3 @@ -import sys from enum import Enum from pathlib import Path @@ -16,7 +15,7 @@ class PostInstallationAction(Enum): CHROOT = tr('chroot into installation for post-installation configurations') -def select_ntp(preset: bool = True) -> bool: +async def select_ntp(preset: bool = True) -> bool: header = tr('Would you like to use automatic time synchronization (NTP) with the default time servers?\n') + '\n' header += ( tr( @@ -25,7 +24,7 @@ def select_ntp(preset: bool = True) -> bool: + '\n' ) - result = Confirmation( + result = await Confirmation( header=header, allow_skip=True, preset=preset, @@ -40,8 +39,8 @@ def select_ntp(preset: bool = True) -> bool: raise ValueError('Unhandled return type') -def select_hostname(preset: str | None = None) -> str | None: - result = Input( +async def select_hostname(preset: str | None = None) -> str | None: + result = await Input( header=tr('Enter a hostname'), allow_skip=True, default_value=preset, @@ -59,7 +58,7 @@ def select_hostname(preset: str | None = None) -> str | None: raise ValueError('Unhandled result type') -def select_timezone(preset: str | None = None) -> str | None: +async def select_timezone(preset: str | None = None) -> str | None: default = 'UTC' timezones = list_timezones() @@ -68,7 +67,7 @@ def select_timezone(preset: str | None = None) -> str | None: group.set_selected_by_value(preset) group.set_default_by_value(default) - result = Selection[str]( + result = await Selection[str]( group, header=tr('Select timezone'), allow_reset=True, @@ -85,7 +84,7 @@ def select_timezone(preset: str | None = None) -> str | None: return result.get_value() -def select_language(preset: str | None = None) -> str | None: +async def select_language(preset: str | None = None) -> str | None: from archinstall.lib.locale.locale_menu import select_kb_layout # We'll raise an exception in an upcoming version. @@ -95,10 +94,10 @@ def select_language(preset: str | None = None) -> str | None: # No need to translate this i feel, as it's a short lived message. warn('select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version') - return select_kb_layout(preset) + return await select_kb_layout(preset) -def select_archinstall_language(languages: list[Language], preset: Language) -> Language: +async def select_archinstall_language(languages: list[Language], preset: Language) -> Language: # these are the displayed language names which can either be # the english name of a language or, if present, the # name of the language in its own language @@ -111,7 +110,7 @@ def select_archinstall_language(languages: list[Language], preset: Language) -> title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n' title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n' - result = Selection[Language]( + result = await Selection[Language]( header=title, group=group, allow_reset=False, @@ -127,7 +126,7 @@ def select_archinstall_language(languages: list[Language], preset: Language) -> raise ValueError('Language selection not handled') -def add_number_of_parallel_downloads(preset: int = 1) -> int | None: +async def add_number_of_parallel_downloads(preset: int = 1) -> int | None: max_recommended = 5 header = tr('This option enables the number of parallel downloads that can occur during package downloads') + '\n' @@ -145,7 +144,7 @@ def validator(s: str) -> str | None: except Exception: return tr('Please enter a valid number') - result = Input( + result = await Input( header=header, allow_skip=True, allow_reset=True, @@ -164,10 +163,10 @@ def validator(s: str) -> str | None: downloads = int(result.get_value()) pacman_conf_path = Path('/etc/pacman.conf') - with pacman_conf_path.open() as f: + with pacman_conf_path.open() as f: # noqa: ASYNC230 pacman_conf = f.read().split('\n') - with pacman_conf_path.open('w') as fwrite: + with pacman_conf_path.open('w') as fwrite: # noqa: ASYNC230 for line in pacman_conf: if 'ParallelDownloads' in line: fwrite.write(f'ParallelDownloads = {downloads}\n') @@ -177,7 +176,7 @@ def validator(s: str) -> str | None: return downloads -def select_post_installation(elapsed_time: float | None = None) -> PostInstallationAction: +async def select_post_installation(elapsed_time: float | None = None) -> PostInstallationAction: header = 'Installation completed' if elapsed_time is not None: minutes = int(elapsed_time // 60) @@ -188,7 +187,7 @@ def select_post_installation(elapsed_time: float | None = None) -> PostInstallat items = [MenuItem(action.value, value=action) for action in PostInstallationAction] group = MenuItemGroup(items) - result = Selection[PostInstallationAction]( + result = await Selection[PostInstallationAction]( group, header=header, allow_skip=False, @@ -199,16 +198,3 @@ def select_post_installation(elapsed_time: float | None = None) -> PostInstallat return result.get_value() case _: raise ValueError('Post installation action not handled') - - -def confirm_abort() -> None: - prompt = tr('Do you really want to abort?') + '\n' - - result = Confirmation( - header=prompt, - allow_skip=False, - preset=False, - ).show() - - if result.get_value(): - sys.exit(0) diff --git a/archinstall/lib/interactions/system_conf.py b/archinstall/lib/interactions/system_conf.py index 0551a0af59..5c4d4634d2 100644 --- a/archinstall/lib/interactions/system_conf.py +++ b/archinstall/lib/interactions/system_conf.py @@ -8,7 +8,7 @@ from archinstall.tui.ui.result import ResultType -def select_kernel(preset: list[str] = []) -> list[str]: +async def select_kernel(preset: list[str] = []) -> list[str]: """ Asks the user to select a kernel for system. @@ -25,7 +25,7 @@ def select_kernel(preset: list[str] = []) -> list[str]: group.set_focus_by_value(default_kernel) group.set_selected_by_value(preset) - result = Selection[str]( + result = await Selection[str]( group, header=tr('Select which kernel(s) to install'), allow_skip=True, @@ -42,10 +42,10 @@ def select_kernel(preset: list[str] = []) -> list[str]: return result.get_values() -def select_uki(preset: bool = True) -> bool: +async def select_uki(preset: bool = True) -> bool: prompt = tr('Would you like to use unified kernel images?') + '\n' - result = Confirmation(header=prompt, allow_skip=True, preset=preset).show() + result = await Confirmation(header=prompt, allow_skip=True, preset=preset).show() match result.type_: case ResultType.Skip: @@ -56,7 +56,7 @@ def select_uki(preset: bool = True) -> bool: raise ValueError('Unhandled result type') -def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None: +async def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None: """ Somewhat convoluted function, whose job is simple. Select a graphics driver from a pre-defined set of popular options. @@ -90,7 +90,7 @@ def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None if SysInfo.has_nvidia_graphics(): header += tr('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n') - result = Selection[GfxDriver]( + result = await Selection[GfxDriver]( group, header=header, allow_skip=True, @@ -107,14 +107,14 @@ def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None return result.get_value() -def select_swap(preset: ZramConfiguration = ZramConfiguration(enabled=True)) -> ZramConfiguration: +async def select_swap(preset: ZramConfiguration = ZramConfiguration(enabled=True)) -> ZramConfiguration: prompt = tr('Would you like to use swap on zram?') + '\n' group = MenuItemGroup.yes_no() group.set_default_by_value(True) group.set_focus_by_value(preset.enabled) - result = Confirmation( + result = await Confirmation( header=prompt, allow_skip=True, preset=preset.enabled, @@ -133,7 +133,7 @@ def select_swap(preset: ZramConfiguration = ZramConfiguration(enabled=True)) -> algo_group.set_default_by_value(ZramAlgorithm.ZSTD) algo_group.set_focus_by_value(preset.algorithm) - algo_result = Selection[ZramAlgorithm]( + algo_result = await Selection[ZramAlgorithm]( algo_group, header=tr('Select zram compression algorithm:') + '\n', allow_skip=True, diff --git a/archinstall/lib/locale/locale_menu.py b/archinstall/lib/locale/locale_menu.py index 4a72e6c1fb..c03824a145 100644 --- a/archinstall/lib/locale/locale_menu.py +++ b/archinstall/lib/locale/locale_menu.py @@ -50,22 +50,22 @@ def _define_menu_options(self) -> list[MenuItem]: ] @override - def run(self) -> LocaleConfiguration: - config = super().run() + async def show(self) -> LocaleConfiguration | None: + config = await super().show() if config is None: config = LocaleConfiguration.default() return config - def _select_kb_layout(self, preset: str | None) -> str | None: - kb_lang = select_kb_layout(preset) + async def _select_kb_layout(self, preset: str | None) -> str | None: + kb_lang = await select_kb_layout(preset) if kb_lang: set_kb_layout(kb_lang) return kb_lang -def select_locale_lang(preset: str | None = None) -> str | None: +async def select_locale_lang(preset: str | None = None) -> str | None: locales = list_locales() locale_lang = set([locale.split()[0] for locale in locales]) @@ -73,7 +73,7 @@ def select_locale_lang(preset: str | None = None) -> str | None: group = MenuItemGroup(items, sort_items=True) group.set_focus_by_value(preset) - result = Selection[str]( + result = await Selection[str]( header=tr('Locale language'), group=group, enable_filter=True, @@ -88,7 +88,7 @@ def select_locale_lang(preset: str | None = None) -> str | None: raise ValueError('Unhandled return type') -def select_locale_enc(preset: str | None = None) -> str | None: +async def select_locale_enc(preset: str | None = None) -> str | None: locales = list_locales() locale_enc = set([locale.split()[1] for locale in locales]) @@ -96,7 +96,7 @@ def select_locale_enc(preset: str | None = None) -> str | None: group = MenuItemGroup(items, sort_items=True) group.set_focus_by_value(preset) - result = Selection[str]( + result = await Selection[str]( header=tr('Locale encoding'), group=group, enable_filter=True, @@ -111,7 +111,7 @@ def select_locale_enc(preset: str | None = None) -> str | None: raise ValueError('Unhandled return type') -def select_kb_layout(preset: str | None = None) -> str | None: +async def select_kb_layout(preset: str | None = None) -> str | None: """ Select keyboard layout @@ -127,7 +127,7 @@ def select_kb_layout(preset: str | None = None) -> str | None: group = MenuItemGroup(items, sort_items=False) group.set_focus_by_value(preset) - result = Selection[str]( + result = await Selection[str]( header=tr('Keyboard layout'), group=group, enable_filter=True, diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 06b2acf3d2..084c6e4f16 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -1,17 +1,29 @@ +from enum import Enum from types import TracebackType -from typing import Any, Self +from typing import Any, Self, override from archinstall.lib.menu.helpers import Selection from archinstall.lib.output import error from archinstall.lib.translationhandler import tr from archinstall.tui.types import Chars +from archinstall.tui.ui.components import InstanceRunnable from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType CONFIG_KEY = '__config__' -class AbstractMenu[ValueT]: +class SpecialMenuKey(Enum): + SAVE = f'{CONFIG_KEY}_save' + INSTALL = f'{CONFIG_KEY}_install' + ABORT = f'{CONFIG_KEY}_abort' + + @staticmethod + def matches(key: str) -> bool: + return any(key == item.value for item in SpecialMenuKey) + + +class AbstractMenu[ValueT](InstanceRunnable[ValueT]): def __init__( self, item_group: MenuItemGroup, @@ -50,7 +62,7 @@ def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseExceptio def _sync_from_config(self) -> None: for item in self._menu_item_group._menu_items: - if item.key is not None and not item.key.startswith(CONFIG_KEY): + if item.key is not None and not SpecialMenuKey.matches(item.key): config_value = getattr(self._config, item.key) if config_value is not None: item.value = config_value @@ -61,7 +73,7 @@ def sync_all_to_config(self) -> None: setattr(self._config, item.key, item.value) def _sync(self, item: MenuItem) -> None: - if not item.key or item.key.startswith(CONFIG_KEY): + if not item.key or SpecialMenuKey.matches(item.key): return config_value = getattr(self._config, item.key) @@ -79,7 +91,7 @@ def set_enabled(self, key: str, enabled: bool) -> None: for item in self._menu_item_group.items: if item.key: - if item.key == key or (is_config_key and item.key.startswith(CONFIG_KEY)): + if item.key == key or (is_config_key and SpecialMenuKey.matches(item.key)): item.enabled = enabled found = True @@ -90,14 +102,18 @@ def disable_all(self) -> None: for item in self._menu_item_group.items: item.enabled = False - def _is_config_valid(self) -> bool: + def is_config_valid(self) -> bool: return True - def run(self) -> ValueT | None: + @override + async def run(self) -> ValueT | None: + return await self.show() + + async def show(self) -> ValueT | None: self._sync_from_config() while True: - result = Selection[ValueT]( + result = await Selection[ValueT]( title=self._title, group=self._menu_item_group, allow_skip=False, @@ -111,11 +127,16 @@ def run(self) -> ValueT | None: self._menu_item_group.focus_item = item if item.action is None: - if not self._is_config_valid(): - continue - break + if item.key == SpecialMenuKey.INSTALL.value: + if not self.is_config_valid(): + continue + break + elif item.key == SpecialMenuKey.ABORT.value: + return None + else: + break else: - item.value = item.action(item.value) + item.value = await item.action(item.value) case ResultType.Reset: return None case _: diff --git a/archinstall/lib/menu/helpers.py b/archinstall/lib/menu/helpers.py index 3b91c6218c..9b8b0bfb26 100644 --- a/archinstall/lib/menu/helpers.py +++ b/archinstall/lib/menu/helpers.py @@ -1,15 +1,13 @@ from collections.abc import Awaitable, Callable -from typing import Any, Literal, TypeVar, override +from typing import Any, Literal, override from textual.validation import ValidationResult, Validator from archinstall.lib.translationhandler import tr -from archinstall.tui.ui.components import InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen, tui +from archinstall.tui.ui.components import InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen from archinstall.tui.ui.menu_item import MenuItemGroup from archinstall.tui.ui.result import Result, ResultType -ValueT = TypeVar('ValueT') - class Selection[ValueT]: def __init__( @@ -32,11 +30,7 @@ def __init__( self._multi = multi self._enable_filter = enable_filter - def show(self) -> Result[ValueT]: - result: Result[ValueT] = tui.run(self) - return result - - async def _run(self) -> None: + async def show(self) -> Result[ValueT]: if self._multi: result = await SelectListScreen[ValueT]( self._group, @@ -61,9 +55,9 @@ async def _run(self) -> None: confirmed = await _confirm_reset() if confirmed.get_value() is False: - return await self._run() + return await self.show() - tui.exit(result) + return result class Confirmation: @@ -90,11 +84,7 @@ def __init__( else: self._group = group - def show(self) -> Result[bool]: - result: Result[bool] = tui.run(self) - return result - - async def _run(self) -> None: + async def show(self) -> Result[bool]: result = await OptionListScreen[bool]( self._group, header=self._header, @@ -108,22 +98,18 @@ async def _run(self) -> None: confirmed = await _confirm_reset() if confirmed.get_value() is False: - return await self._run() + return await self.show() - tui.exit(result) + return result class Notify: def __init__(self, header: str): self._header = header - def show(self) -> Result[bool]: - result: Result[bool] = tui.run(self) - return result - - async def _run(self) -> None: - await NotifyScreen(header=self._header).run() - tui.exit(Result.true()) + async def show(self) -> Result[bool]: + _ = await NotifyScreen(header=self._header).run() + return Result.true() class GenericValidator(Validator): @@ -161,11 +147,7 @@ def __init__( self._allow_reset = allow_reset self._validator_callback = validator_callback - def show(self) -> Result[str]: - result: Result[str] = tui.run(self) - return result - - async def _run(self) -> None: + async def show(self) -> Result[str]: validator = GenericValidator(self._validator_callback) if self._validator_callback else None result = await InputScreen( @@ -182,9 +164,9 @@ async def _run(self) -> None: confirmed = await _confirm_reset() if confirmed.get_value() is False: - return await self._run() + return await self.show() - tui.exit(result) + return result class Loading[ValueT]: @@ -198,23 +180,19 @@ def __init__( self._timer = timer self._data_callback = data_callback - def show(self) -> Result[ValueT]: - result: Result[ValueT] = tui.run(self) - return result - - async def _run(self) -> None: + async def show(self) -> Result[ValueT]: if self._data_callback: - result = await LoadingScreen( + result = await LoadingScreen[ValueT]( header=self._header, data_callback=self._data_callback, ).run() - tui.exit(result) + return result else: - await LoadingScreen( + _ = await LoadingScreen( timer=self._timer, header=self._header, ).run() - tui.exit(Result.true()) + return Result.true() class Table[ValueT]: @@ -245,11 +223,7 @@ def __init__( if self._group is None and self._data_callback is None: raise ValueError('Either data or data_callback must be provided') - def show(self) -> Result[ValueT]: - result: Result[ValueT] = tui.run(self) - return result - - async def _run(self) -> None: + async def show(self) -> Result[ValueT]: result = await TableSelectionScreen[ValueT]( header=self._header, group=self._group, @@ -266,9 +240,9 @@ async def _run(self) -> None: confirmed = await _confirm_reset() if confirmed.get_value() is False: - return await self._run() + return await self.show() - tui.exit(result) + return result async def _confirm_reset() -> Result[bool]: diff --git a/archinstall/lib/menu/list_manager.py b/archinstall/lib/menu/list_manager.py index 8a07db9782..8061eeeec2 100644 --- a/archinstall/lib/menu/list_manager.py +++ b/archinstall/lib/menu/list_manager.py @@ -53,7 +53,7 @@ def is_last_choice_cancel(self) -> bool: return self._last_choice == self._cancel_action return False - def _run(self) -> list[ValueT] | None: + async def _run(self) -> list[ValueT] | None: additional_options = self._base_actions + self._terminate_actions while True: @@ -66,7 +66,7 @@ def _run(self) -> list[ValueT] | None: if self._prompt is not None: prompt = f'{self._prompt}\n\n' - result = Selection[ValueT | str]( + result = await Selection[ValueT | str]( group, header=prompt, enable_filter=False, @@ -81,14 +81,14 @@ def _run(self) -> list[ValueT] | None: if value in self._base_actions: value = cast(str, value) - self._data = self.handle_action(value, None, self._data) + self._data = await self.handle_action(value, None, self._data) elif value in self._terminate_actions: break else: # an entry of the existing selection was chosen selected_entry = result.get_value() selected_entry = cast(ValueT, selected_entry) - self._run_actions_on_entry(selected_entry) + await self._run_actions_on_entry(selected_entry) self._last_choice = value @@ -97,7 +97,7 @@ def _run(self) -> list[ValueT] | None: else: return self._data - def _run_actions_on_entry(self, entry: ValueT) -> None: + async def _run_actions_on_entry(self, entry: ValueT) -> None: options = self.filter_options(entry, self._sub_menu_actions) + [self._cancel_action] items = [MenuItem(o, value=o) for o in options] @@ -105,7 +105,7 @@ def _run_actions_on_entry(self, entry: ValueT) -> None: header = f'{self.selected_action_display(entry)}' - result = Selection[str]( + result = await Selection[str]( group, header=header, enable_filter=False, @@ -119,7 +119,7 @@ def _run_actions_on_entry(self, entry: ValueT) -> None: raise ValueError('Unhandled return type') if value != self._cancel_action: - self._data = self.handle_action(value, entry, self._data) + self._data = await self.handle_action(value, entry, self._data) def selected_action_display(self, selection: ValueT) -> str: """ @@ -128,7 +128,7 @@ def selected_action_display(self, selection: ValueT) -> str: """ raise NotImplementedError('Please implement me in the child class') - def handle_action(self, action: str, entry: ValueT | None, data: list[ValueT]) -> list[ValueT]: + async def handle_action(self, action: str, entry: ValueT | None, data: list[ValueT]) -> list[ValueT]: """ this function is called when a base action or a specific action for an entry is triggered diff --git a/archinstall/lib/menu/util.py b/archinstall/lib/menu/util.py index 78434ab592..9daa3a6d77 100644 --- a/archinstall/lib/menu/util.py +++ b/archinstall/lib/menu/util.py @@ -1,19 +1,22 @@ +import sys +import time from pathlib import Path -from archinstall.lib.menu.helpers import Input +from archinstall.lib.menu.helpers import Confirmation, Input from archinstall.lib.models.users import Password from archinstall.lib.translationhandler import tr +from archinstall.tui.ui.components import tui from archinstall.tui.ui.result import ResultType -def get_password( +async def get_password( header: str | None = None, allow_skip: bool = False, preset: str | None = None, skip_confirmation: bool = False, ) -> Password | None: while True: - result = Input( + result = await Input( header=header, allow_skip=allow_skip, default_value=preset, @@ -46,7 +49,7 @@ def _validate(value: str) -> str | None: return tr('The password did not match, please try again') return None - _ = Input( + _ = await Input( header=confirmation_header, allow_skip=False, password=True, @@ -56,7 +59,7 @@ def _validate(value: str) -> str | None: return password -def prompt_dir( +async def prompt_dir( header: str | None = None, validate: bool = True, must_exist: bool = True, @@ -80,7 +83,7 @@ def validate_path(path: str | None) -> str | None: else: validate_func = None - result = Input( + result = await Input( header=header, allow_skip=allow_skip, validator_callback=validate_func, @@ -96,3 +99,33 @@ def validate_path(path: str | None) -> str | None: return Path(result.get_value()) case _: return None + + +async def confirm_abort() -> bool: + prompt = tr('Do you really want to abort?') + '\n' + + result = await Confirmation( + header=prompt, + allow_skip=False, + preset=False, + ).show() + + return result.get_value() + + +def delayed_warning(message: str) -> bool: + # Issue a final warning before we continue with something un-revertable. + # We count down from 5 to 0. + print(message, end='', flush=True) + + try: + countdown = '\n5...4...3...2...1\n' + for c in countdown: + print(c, end='', flush=True) + time.sleep(0.25) + except KeyboardInterrupt: + ret: bool = tui.run(confirm_abort) + if ret: + sys.exit(1) + + return True diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 3f2c643168..0602e9678e 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -39,27 +39,27 @@ def __init__(self, custom_repositories: list[CustomRepository]): '', ) - def show(self) -> list[CustomRepository] | None: - return super()._run() + async def show(self) -> list[CustomRepository] | None: + return await super()._run() @override def selected_action_display(self, selection: CustomRepository) -> str: return selection.name @override - def handle_action( + async def handle_action( self, action: str, entry: CustomRepository | None, data: list[CustomRepository], ) -> list[CustomRepository]: if action == self._actions[0]: # add - new_repo = self._add_custom_repository() + new_repo = await self._add_custom_repository() if new_repo is not None: data = [d for d in data if d.name != new_repo.name] data += [new_repo] elif action == self._actions[1] and entry: # modify repo - new_repo = self._add_custom_repository(entry) + new_repo = await self._add_custom_repository(entry) if new_repo is not None: data = [d for d in data if d.name != entry.name] data += [new_repo] @@ -68,8 +68,8 @@ def handle_action( return data - def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None: - edit_result = Input( + async def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None: + edit_result = await Input( header=tr('Enter a respository name'), allow_skip=True, default_value=preset.name if preset else None, @@ -86,7 +86,7 @@ def _add_custom_repository(self, preset: CustomRepository | None = None) -> Cust header = f'{tr("Name")}: {name}\n' prompt = f'{header}\n' + tr('Enter the repository url') - edit_result = Input( + edit_result = await Input( header=prompt, allow_skip=True, default_value=preset.url if preset else None, @@ -109,7 +109,7 @@ def _add_custom_repository(self, preset: CustomRepository | None = None) -> Cust if preset is not None: group.set_selected_by_value(preset.sign_check.value) - result = Selection[SignCheck]( + result = await Selection[SignCheck]( group, header=prompt, allow_skip=False, @@ -130,7 +130,7 @@ def _add_custom_repository(self, preset: CustomRepository | None = None) -> Cust if preset is not None: group.set_selected_by_value(preset.sign_option.value) - result = Selection( + result = await Selection( group, header=prompt, allow_skip=False, @@ -160,27 +160,27 @@ def __init__(self, custom_servers: list[CustomServer]): '', ) - def show(self) -> list[CustomServer] | None: - return super()._run() + async def show(self) -> list[CustomServer] | None: + return await super()._run() @override def selected_action_display(self, selection: CustomServer) -> str: return selection.url @override - def handle_action( + async def handle_action( self, action: str, entry: CustomServer | None, data: list[CustomServer], ) -> list[CustomServer]: if action == self._actions[0]: # add - new_server = self._add_custom_server() + new_server = await self._add_custom_server() if new_server is not None: data = [d for d in data if d.url != new_server.url] data += [new_server] elif action == self._actions[1] and entry: # modify repo - new_server = self._add_custom_server(entry) + new_server = await self._add_custom_server(entry) if new_server is not None: data = [d for d in data if d.url != entry.url] data += [new_server] @@ -189,8 +189,8 @@ def handle_action( return data - def _add_custom_server(self, preset: CustomServer | None = None) -> CustomServer | None: - edit_result = Input( + async def _add_custom_server(self, preset: CustomServer | None = None) -> CustomServer | None: + edit_result = await Input( header=tr('Enter server url'), allow_skip=True, default_value=preset.url if preset else None, @@ -453,15 +453,15 @@ def _prev_custom_servers(self, item: MenuItem) -> str | None: return output.strip() @override - def run(self) -> MirrorConfiguration | None: - return super().run() + async def show(self) -> MirrorConfiguration | None: + return await super().show() -def select_mirror_regions( +async def select_mirror_regions( mirror_list_handler: MirrorListHandler, preset: list[MirrorRegion], ) -> list[MirrorRegion]: - Loading[None]( + await Loading[None]( header=tr('Loading mirror regions...'), data_callback=mirror_list_handler.load_mirrors, ).show() @@ -478,7 +478,7 @@ def select_mirror_regions( group.set_selected_by_value(preset_regions) - result = Selection[MirrorRegion]( + result = await Selection[MirrorRegion]( group, header=tr('Select mirror regions to be enabled'), allow_reset=True, @@ -497,8 +497,8 @@ def select_mirror_regions( return selected_mirrors -def add_custom_mirror_servers(preset: list[CustomServer] = []) -> list[CustomServer]: - custom_mirrors = CustomMirrorServersList(preset).show() +async def add_custom_mirror_servers(preset: list[CustomServer] = []) -> list[CustomServer]: + custom_mirrors = await CustomMirrorServersList(preset).show() if not custom_mirrors: return preset @@ -506,8 +506,8 @@ def add_custom_mirror_servers(preset: list[CustomServer] = []) -> list[CustomSer return custom_mirrors -def select_custom_mirror(preset: list[CustomRepository] = []) -> list[CustomRepository]: - custom_mirrors = CustomMirrorRepositoriesList(preset).show() +async def select_custom_mirror(preset: list[CustomRepository] = []) -> list[CustomRepository]: + custom_mirrors = await CustomMirrorRepositoriesList(preset).show() if not custom_mirrors: return preset @@ -515,7 +515,7 @@ def select_custom_mirror(preset: list[CustomRepository] = []) -> list[CustomRepo return custom_mirrors -def select_optional_repositories(preset: list[Repository]) -> list[Repository]: +async def select_optional_repositories(preset: list[Repository]) -> list[Repository]: """ Allows the user to select additional repositories (multilib, and testing) if desired. @@ -533,7 +533,7 @@ def select_optional_repositories(preset: list[Repository]) -> list[Repository]: group = MenuItemGroup(items, sort_items=False) group.set_selected_by_value(preset) - result = Selection[Repository]( + result = await Selection[Repository]( group, header=tr('Select optional repositories to be enabled'), allow_reset=True, diff --git a/archinstall/lib/network/network_menu.py b/archinstall/lib/network/network_menu.py index 3eff828a34..bcf546981a 100644 --- a/archinstall/lib/network/network_menu.py +++ b/archinstall/lib/network/network_menu.py @@ -25,31 +25,32 @@ def __init__(self, prompt: str, preset: list[Nic]): prompt, ) - def show(self) -> list[Nic] | None: - return super()._run() + async def show(self) -> list[Nic] | None: + return await super()._run() @override def selected_action_display(self, selection: Nic) -> str: return selection.iface if selection.iface else '' @override - def handle_action(self, action: str, entry: Nic | None, data: list[Nic]) -> list[Nic]: + async def handle_action(self, action: str, entry: Nic | None, data: list[Nic]) -> list[Nic]: if action == self._actions[0]: # add - iface = self._select_iface(data) + iface = await self._select_iface(data) if iface: nic = Nic(iface=iface) - nic = self._edit_iface(nic) + nic = await self._edit_iface(nic) data += [nic] elif entry: if action == self._actions[1]: # edit interface data = [d for d in data if d.iface != entry.iface] - data.append(self._edit_iface(entry)) + nic = await self._edit_iface(entry) + data.append(nic) elif action == self._actions[2]: # delete data = [d for d in data if d != entry] return data - def _select_iface(self, data: list[Nic]) -> str | None: + async def _select_iface(self, data: list[Nic]) -> str | None: all_ifaces = list_interfaces().values() existing_ifaces = [d.iface for d in data] available = set(all_ifaces) - set(existing_ifaces) @@ -63,7 +64,7 @@ def _select_iface(self, data: list[Nic]) -> str | None: items = [MenuItem(i, value=i) for i in available] group = MenuItemGroup(items, sort_items=True) - result = Selection[str]( + result = await Selection[str]( group, header=tr('Select an interface'), allow_skip=True, @@ -77,7 +78,7 @@ def _select_iface(self, data: list[Nic]) -> str | None: case ResultType.Reset: raise ValueError('Unhandled result type') - def _get_ip_address(self, header: str, allow_skip: bool, multi: bool, preset: str | None = None, allow_empty: bool = False) -> str | None: + async def _get_ip_address(self, header: str, allow_skip: bool, multi: bool, preset: str | None = None, allow_empty: bool = False) -> str | None: def validator(ip: str | None) -> str | None: failure = tr('You need to enter a valid IP in IP-config mode') @@ -98,7 +99,7 @@ def validator(ip: str | None) -> str | None: except ValueError: return failure - result = Input( + result = await Input( header=header, validator_callback=validator, allow_skip=allow_skip, @@ -113,7 +114,7 @@ def validator(ip: str | None) -> str | None: case ResultType.Reset: raise ValueError('Unhandled result type') - def _edit_iface(self, edit_nic: Nic) -> Nic: + async def _edit_iface(self, edit_nic: Nic) -> Nic: iface_name = edit_nic.iface modes = ['DHCP (auto detect)', 'IP (static)'] default_mode = 'DHCP (auto detect)' @@ -124,7 +125,7 @@ def _edit_iface(self, edit_nic: Nic) -> Nic: group = MenuItemGroup(items, sort_items=True) group.set_default_by_value(default_mode) - result = Selection[str]( + result = await Selection[str]( group, header=header, allow_skip=False, @@ -142,10 +143,10 @@ def _edit_iface(self, edit_nic: Nic) -> Nic: if mode == 'IP (static)': header = tr('Enter the IP and subnet for {} (example: 192.168.0.5/24): ').format(iface_name) + '\n' - ip = self._get_ip_address(header, False, False) + ip = await self._get_ip_address(header, False, False) header = tr('Enter your gateway (router) IP address (leave blank for none)') + '\n' - gateway = self._get_ip_address(header, True, False, allow_empty=True) + gateway = await self._get_ip_address(header, True, False, allow_empty=True) if edit_nic.dns: display_dns = ' '.join(edit_nic.dns) @@ -153,7 +154,7 @@ def _edit_iface(self, edit_nic: Nic) -> Nic: display_dns = None header = tr('Enter your DNS servers with space separated (leave blank for none)') + '\n' - dns_servers = self._get_ip_address(header, True, True, display_dns, allow_empty=True) + dns_servers = await self._get_ip_address(header, True, True, display_dns, allow_empty=True) dns = [] if dns_servers is not None: @@ -165,7 +166,7 @@ def _edit_iface(self, edit_nic: Nic) -> Nic: return Nic(iface=iface_name) -def select_network(preset: NetworkConfiguration | None) -> NetworkConfiguration | None: +async def select_network(preset: NetworkConfiguration | None) -> NetworkConfiguration | None: """ Configure the network on the newly installed system """ @@ -176,7 +177,7 @@ def select_network(preset: NetworkConfiguration | None) -> NetworkConfiguration if preset: group.set_selected_by_value(preset.type) - result = Selection[NicType]( + result = await Selection[NicType]( group, header=tr('Choose network configuration'), allow_reset=True, @@ -200,7 +201,7 @@ def select_network(preset: NetworkConfiguration | None) -> NetworkConfiguration return NetworkConfiguration(NicType.NM_IWD) case NicType.MANUAL: preset_nics = preset.nics if preset else [] - nics = ManualNetworkConfig(tr('Configure interfaces'), preset_nics).show() + nics = await ManualNetworkConfig(tr('Configure interfaces'), preset_nics).show() if nics: return NetworkConfiguration(NicType.MANUAL, nics) diff --git a/archinstall/lib/network/wifi_handler.py b/archinstall/lib/network/wifi_handler.py index 45e96c43e2..0546310b67 100644 --- a/archinstall/lib/network/wifi_handler.py +++ b/archinstall/lib/network/wifi_handler.py @@ -1,7 +1,7 @@ from asyncio import sleep from dataclasses import dataclass from pathlib import Path -from typing import assert_never +from typing import assert_never, override from archinstall.lib.command import SysCommand from archinstall.lib.exceptions import SysCallError @@ -9,7 +9,7 @@ from archinstall.lib.network.wpa_supplicant import WpaSupplicantConfig from archinstall.lib.output import debug from archinstall.lib.translationhandler import tr -from archinstall.tui.ui.components import ConfirmationScreen, InputScreen, LoadingScreen, NotifyScreen, TableSelectionScreen, tui +from archinstall.tui.ui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import Result, ResultType @@ -21,15 +21,12 @@ class WpaCliResult: error: str | None = None -class WifiHandler: +class WifiHandler(InstanceRunnable[bool]): def __init__(self) -> None: - self._wpa_config = WpaSupplicantConfig() + self._wpa_config: WpaSupplicantConfig = WpaSupplicantConfig() - def setup(self) -> bool: - result: Result[bool] = tui.run(self) - return result.get_value() - - async def _run(self) -> None: + @override + async def run(self) -> bool | None: """ This is the entry point that is called by components.TApp """ @@ -37,8 +34,7 @@ async def _run(self) -> None: if not wifi_iface: debug('No wifi interface found') - tui.exit(Result.false()) - return None + return False prompt = tr('No network connection found') + '\n\n' prompt += tr('Would you like to connect to a Wifi?') + '\n' @@ -53,14 +49,12 @@ async def _run(self) -> None: match result.type_: case ResultType.Selection: if result.get_value() is False: - tui.exit(Result.false()) - return None + return False case ResultType.Skip | ResultType.Reset: - tui.exit(Result.false()) - return None + return False setup_result = await self._setup_wifi(wifi_iface) - tui.exit(Result(ResultType.Selection, _data=setup_result)) + return setup_result async def _enable_supplicant(self, wifi_iface: str) -> bool: self._wpa_config.load_config() diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index fa3493a051..28e90bc02c 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -84,7 +84,7 @@ def _parse_package_output[PackageType: (AvailablePackage, LocalPackage)]( return cls.model_validate(package) -def select_additional_packages( +async def select_additional_packages( preset: list[str] = [], repositories: set[Repository] = set(), ) -> list[str]: @@ -94,7 +94,7 @@ def select_additional_packages( output = tr('Repositories: {}').format(respos_text) + '\n' output += tr('Loading packages...') - result = Loading[dict[str, AvailablePackage]]( + result = await Loading[dict[str, AvailablePackage]]( header=output, data_callback=lambda: list_available_packages(tuple(repositories)), ).show() @@ -106,7 +106,7 @@ def select_additional_packages( packages = result.get_value() if not packages: - Notify(tr('No packages found')).show() + await Notify(tr('No packages found')).show() return [] package_groups = PackageGroup.from_available_packages(packages) @@ -145,7 +145,7 @@ def select_additional_packages( menu_group = MenuItemGroup(items, sort_items=True) menu_group.set_selected_by_value(preset_packages) - pck_result = Selection[AvailablePackage | PackageGroup]( + pck_result = await Selection[AvailablePackage | PackageGroup]( menu_group, header=header, allow_reset=True, diff --git a/archinstall/lib/profile/profile_menu.py b/archinstall/lib/profile/profile_menu.py index 45fb3f9cf4..be620e158f 100644 --- a/archinstall/lib/profile/profile_menu.py +++ b/archinstall/lib/profile/profile_menu.py @@ -60,11 +60,11 @@ def _define_menu_options(self) -> list[MenuItem]: ] @override - def run(self) -> ProfileConfiguration | None: - return super().run() + async def show(self) -> ProfileConfiguration | None: + return await super().show() - def _select_profile(self, preset: Profile | None) -> Profile | None: - profile = select_profile(preset) + async def _select_profile(self, preset: Profile | None) -> Profile | None: + profile = await select_profile(preset) if profile is not None: if not profile.is_graphic_driver_supported(): @@ -86,20 +86,20 @@ def _select_profile(self, preset: Profile | None) -> Profile | None: return profile - def _select_gfx_driver(self, preset: GfxDriver | None = None) -> GfxDriver | None: + async def _select_gfx_driver(self, preset: GfxDriver | None = None) -> GfxDriver | None: driver = preset profile: Profile | None = self._item_group.find_by_key('profile').value if profile: if profile.is_graphic_driver_supported(): - driver = select_driver(preset=preset) + driver = await select_driver(preset=preset) if driver and 'Sway' in profile.current_selection_names(): if driver.is_nvidia(): header = tr('The proprietary Nvidia driver is not supported by Sway.') + '\n' header += tr('It is likely that you will run into issues, are you okay with that?') + '\n' - result = Confirmation( + result = await Confirmation( header=header, allow_skip=False, preset=False, @@ -140,7 +140,7 @@ def _preview_profile(self, item: MenuItem) -> str | None: return None -def select_greeter( +async def select_greeter( profile: Profile | None = None, preset: GreeterType | None = None, ) -> GreeterType | None: @@ -157,7 +157,7 @@ def select_greeter( group.set_default_by_value(default) - result = Selection[GreeterType]( + result = await Selection[GreeterType]( group, header=tr('Select which greeter to install'), allow_skip=True, @@ -174,7 +174,7 @@ def select_greeter( return None -def select_profile( +async def select_profile( current_profile: Profile | None = None, header: str | None = None, allow_reset: bool = True, @@ -190,7 +190,7 @@ def select_profile( group = MenuItemGroup(items, sort_items=True) group.set_selected_by_value(current_profile) - result = Selection[Profile]( + result = await Selection[Profile]( group, header=header, allow_reset=allow_reset, @@ -204,7 +204,7 @@ def select_profile( return current_profile case ResultType.Selection: profile_selection = result.get_value() - select_result = profile_selection.do_on_select() + select_result = await profile_selection.do_on_select() if not select_result: return None diff --git a/archinstall/lib/user/user_menu.py b/archinstall/lib/user/user_menu.py index 3f50f517f7..28ef7c3b42 100644 --- a/archinstall/lib/user/user_menu.py +++ b/archinstall/lib/user/user_menu.py @@ -26,17 +26,17 @@ def __init__(self, prompt: str, lusers: list[User]): prompt, ) - def show(self) -> list[User] | None: - return super()._run() + async def show(self) -> list[User] | None: + return await super()._run() @override def selected_action_display(self, selection: User) -> str: return selection.username @override - def handle_action(self, action: str, entry: User | None, data: list[User]) -> list[User]: + async def handle_action(self, action: str, entry: User | None, data: list[User]) -> list[User]: if action == self._actions[0]: # add - new_user = self._add_user() + new_user = await self._add_user() if new_user is not None: # in case a user with the same username as an existing user # was created we'll replace the existing one @@ -45,7 +45,7 @@ def handle_action(self, action: str, entry: User | None, data: list[User]) -> li elif action == self._actions[1] and entry: # change password header = f'{tr("User")}: {entry.username}\n' header += tr('Enter new password') - new_password = get_password(header=header) + new_password = await get_password(header=header) if new_password: user = next(filter(lambda x: x == entry, data)) @@ -64,8 +64,8 @@ def _check_for_correct_username(self, username: str | None) -> str | None: return None return tr('The username you entered is invalid') - def _add_user(self) -> User | None: - editResult = Input( + async def _add_user(self) -> User | None: + editResult = await Input( tr('Enter a username'), allow_skip=True, validator_callback=self._check_for_correct_username, @@ -85,7 +85,7 @@ def _add_user(self) -> User | None: header = f'{tr("Username")}: {username}\n' prompt = f'{header}\n' + tr('Enter a password') - password = get_password(header=prompt, allow_skip=True) + password = await get_password(header=prompt, allow_skip=True) if not password: return None @@ -93,7 +93,7 @@ def _add_user(self) -> User | None: header += f'{tr("Password")}: {password.hidden()}\n' prompt = f'{header}\n' + tr('Should "{}" be a superuser (sudo)?\n').format(username) - result = Confirmation( + result = await Confirmation( header=prompt, allow_skip=False, preset=True, @@ -108,8 +108,8 @@ def _add_user(self) -> User | None: return User(username, password, sudo) -def select_users(prompt: str = '', preset: list[User] = []) -> list[User]: - users = UserList(prompt, preset).show() +async def select_users(prompt: str = '', preset: list[User] = []) -> list[User]: + users = await UserList(prompt, preset).show() if users is None: return preset diff --git a/archinstall/main.py b/archinstall/main.py index 756d822309..93596c605b 100644 --- a/archinstall/main.py +++ b/archinstall/main.py @@ -18,6 +18,7 @@ from archinstall.lib.pacman.pacman import Pacman from archinstall.lib.translationhandler import tr from archinstall.lib.utils.util import running_from_iso +from archinstall.tui.ui.components import tui def _log_sys_info() -> None: @@ -38,8 +39,8 @@ def _check_online(wifi_handler: WifiHandler | None = None) -> bool: except OSError as ex: if 'Network is unreachable' in str(ex): if wifi_handler is not None: - success = not wifi_handler.setup() - if not success: + result: bool = tui.run(wifi_handler) + if not result: return False return True diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 33ebad4970..9c6df9885f 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -1,8 +1,9 @@ import os +import sys import time from archinstall.lib.applications.application_handler import ApplicationHandler -from archinstall.lib.args import ArchConfigHandler +from archinstall.lib.args import ArchConfig, ArchConfigHandler from archinstall.lib.authentication.authentication_handler import AuthenticationHandler from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.disk.filesystem import FilesystemHandler @@ -10,6 +11,7 @@ from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer, accessibility_tools_in_use, run_custom_user_commands from archinstall.lib.interactions.general_conf import PostInstallationAction, select_post_installation +from archinstall.lib.menu.util import delayed_warning from archinstall.lib.mirrors import MirrorListHandler from archinstall.lib.models import Bootloader from archinstall.lib.models.device import DiskLayoutType, EncryptionType @@ -19,6 +21,7 @@ from archinstall.lib.packages.util import check_version_upgrade from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.translationhandler import tr +from archinstall.tui.ui.components import tui def show_menu( @@ -42,7 +45,9 @@ def show_menu( if not arch_config_handler.args.advanced: global_menu.set_enabled('parallel_downloads', False) - global_menu.run() + result: ArchConfig | None = tui.run(global_menu) + if result is None: + sys.exit(0) def perform_installation( @@ -177,13 +182,13 @@ def perform_installation( if not arch_config_handler.args.silent: elapsed_time = time.monotonic() - start_time - action = select_post_installation(elapsed_time) + action: PostInstallationAction = tui.run(lambda: select_post_installation(elapsed_time)) match action: case PostInstallationAction.EXIT: pass case PostInstallationAction.REBOOT: - os.system('reboot') + _ = os.system('reboot') case PostInstallationAction.CHROOT: try: installation.drop_to_shell() @@ -212,7 +217,9 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None: if not arch_config_handler.args.silent: aborted = False - if not config.confirm_config(): + res: bool = tui.run(config.confirm_config) + + if not res: debug('Installation aborted') aborted = True @@ -221,6 +228,10 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None: if arch_config_handler.config.disk_config: fs_handler = FilesystemHandler(arch_config_handler.config.disk_config) + + if not delayed_warning(tr('Starting device modifications in ')): + return main() + fs_handler.perform_filesystem_operations() perform_installation( diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index b7ed6cb7af..77c51626f9 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -4,12 +4,15 @@ from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.installer import Installer +from archinstall.lib.menu.util import delayed_warning from archinstall.lib.models import Bootloader from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.users import Password, User from archinstall.lib.network.network_handler import NetworkHandler from archinstall.lib.output import debug, error, info from archinstall.lib.profile.profiles_handler import profile_handler +from archinstall.lib.translationhandler import tr +from archinstall.tui.ui.components import tui def perform_installation(arch_config_handler: ArchConfigHandler) -> None: @@ -58,11 +61,11 @@ def perform_installation(arch_config_handler: ArchConfigHandler) -> None: info(' * devel (password: devel)') -def main(arch_config_handler: ArchConfigHandler | None = None) -> None: +async def main(arch_config_handler: ArchConfigHandler | None = None) -> None: if arch_config_handler is None: arch_config_handler = ArchConfigHandler() - disk_config = DiskLayoutConfigurationMenu(disk_layout_config=None).run() + disk_config = await DiskLayoutConfigurationMenu(disk_layout_config=None).show() arch_config_handler.config.disk_config = disk_config config = ConfigurationOutput(arch_config_handler.config) @@ -74,19 +77,25 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None: if not arch_config_handler.args.silent: aborted = False - if not config.confirm_config(): + res: bool = tui.run(config.confirm_config) + + if not res: debug('Installation aborted') aborted = True if aborted: - return main(arch_config_handler) + return await main(arch_config_handler) if arch_config_handler.config.disk_config: fs_handler = FilesystemHandler(arch_config_handler.config.disk_config) + + if not delayed_warning(tr('Starting device modifications in ')): + return await main() + fs_handler.perform_filesystem_operations() perform_installation(arch_config_handler) if __name__ == '__main__': - main() + tui.run(main) diff --git a/archinstall/scripts/only_hd.py b/archinstall/scripts/only_hd.py index 15adad7878..afb1f9190b 100644 --- a/archinstall/scripts/only_hd.py +++ b/archinstall/scripts/only_hd.py @@ -1,12 +1,16 @@ +import sys from pathlib import Path -from archinstall.lib.args import ArchConfigHandler +from archinstall.lib.args import ArchConfig, ArchConfigHandler from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer +from archinstall.lib.menu.util import delayed_warning from archinstall.lib.output import debug, error +from archinstall.lib.translationhandler import tr +from archinstall.tui.ui.components import tui def show_menu(arch_config_handler: ArchConfigHandler) -> None: @@ -18,7 +22,9 @@ def show_menu(arch_config_handler: ArchConfigHandler) -> None: global_menu.set_enabled('swap', True) global_menu.set_enabled('__config__', True) - global_menu.run() + result: ArchConfig | None = tui.run(global_menu) + if result is None: + sys.exit(0) def perform_installation(arch_config_handler: ArchConfigHandler) -> None: @@ -72,7 +78,9 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None: if not arch_config_handler.args.silent: aborted = False - if not config.confirm_config(): + res: bool = tui.run(config.confirm_config) + + if not res: debug('Installation aborted') aborted = True @@ -81,6 +89,10 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None: if arch_config_handler.config.disk_config: fs_handler = FilesystemHandler(arch_config_handler.config.disk_config) + + if not delayed_warning(tr('Starting device modifications in ')): + return main() + fs_handler.perform_filesystem_operations() perform_installation(arch_config_handler) diff --git a/archinstall/tui/ui/components.py b/archinstall/tui/ui/components.py index 5c9cb5bcee..7e453dcb12 100644 --- a/archinstall/tui/ui/components.py +++ b/archinstall/tui/ui/components.py @@ -1,6 +1,7 @@ import sys +from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable -from typing import Any, ClassVar, Literal, TypeVar, override +from typing import Any, ClassVar, Literal, TypeVar, cast, override from textual import work from textual.app import App, ComposeResult @@ -44,7 +45,7 @@ async def action_reset_operation(self) -> None: _ = self.dismiss(Result(ResultType.Reset)) -class LoadingScreen(BaseScreen[None]): +class LoadingScreen(BaseScreen[ValueT]): CSS = """ LoadingScreen { align: center middle; @@ -78,7 +79,7 @@ def __init__( self._header = header self._data_callback = data_callback - async def run(self) -> Result[None]: + async def run(self) -> Result[ValueT]: assert TApp.app return await TApp.app.show(self) @@ -1051,6 +1052,12 @@ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: ) +class InstanceRunnable[ValueT](ABC): + @abstractmethod + async def run(self) -> ValueT | None: + pass + + class _AppInstance(App[ValueT]): ENABLE_COMMAND_PALETTE = False @@ -1148,7 +1155,7 @@ class _AppInstance(App[ValueT]): } """ - def __init__(self, main: Any) -> None: + def __init__(self, main: InstanceRunnable[ValueT] | Callable[[], Awaitable[ValueT]]) -> None: super().__init__(ansi_color=True) self._main = main @@ -1166,13 +1173,18 @@ def on_mount(self) -> None: @work async def _run_worker(self) -> None: try: - await self._main._run() + if isinstance(self._main, InstanceRunnable): + result: ValueT | None = await self._main.run() + else: + result = await self._main() + + tui.exit(result) except WorkerCancelled: debug('Worker was cancelled') except Exception as err: debug(f'Error while running main app: {err}') # this will terminate the textual app and return the exception - self.exit(err) # type: ignore[arg-type] + self.exit(cast(ValueT, err)) @work async def _show_async(self, screen: Screen[Result[ValueT]]) -> Result[ValueT]: @@ -1185,13 +1197,9 @@ async def show(self, screen: Screen[Result[ValueT]]) -> Result[ValueT]: class TApp: app: _AppInstance[Any] | None = None - def __init__(self) -> None: - self._main = None - self._global_header: str | None = None - - def run(self, main: Any) -> Result[ValueT]: + def run(self, main: InstanceRunnable[ValueT] | Callable[[], Awaitable[ValueT]]) -> ValueT: TApp.app = _AppInstance(main) - result: Result[ValueT] | Exception | None = TApp.app.run() + result: ValueT | Exception | None = TApp.app.run() if isinstance(result, Exception): raise result @@ -1202,7 +1210,7 @@ def run(self, main: Any) -> Result[ValueT]: return result - def exit(self, result: Result[ValueT]) -> None: + def exit(self, result: Any) -> None: assert TApp.app TApp.app.exit(result) diff --git a/archinstall/tui/ui/menu_item.py b/archinstall/tui/ui/menu_item.py index ac17296db4..b55c9fd036 100644 --- a/archinstall/tui/ui/menu_item.py +++ b/archinstall/tui/ui/menu_item.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Awaitable, Callable from dataclasses import dataclass, field from enum import Enum from functools import cached_property @@ -13,7 +13,7 @@ class MenuItem: text: str value: Any | None = None - action: Callable[[Any], Any] | None = None + action: Callable[[Any], Awaitable[Any]] | None = None enabled: bool = True read_only: bool = False mandatory: bool = False diff --git a/archinstall/tui/ui/result.py b/archinstall/tui/ui/result.py index 0d89f23c64..6258bccf1e 100644 --- a/archinstall/tui/ui/result.py +++ b/archinstall/tui/ui/result.py @@ -25,6 +25,18 @@ def true(cls) -> Self: def false(cls) -> Self: return cls(ResultType.Selection, _data=False) # type: ignore[arg-type] + @classmethod + def reset(cls) -> Self: + return cls(ResultType.Reset) + + @classmethod + def selection(cls, value: ValueT | list[ValueT] | None) -> Self: + return cls(ResultType.Selection, _data=value) + + @classmethod + def skip(cls) -> Self: + return cls(ResultType.Skip) + def has_data(self) -> bool: return self._data is not None diff --git a/examples/full_automated_installation.py b/examples/full_automated_installation.py index 760d492689..84920605da 100644 --- a/examples/full_automated_installation.py +++ b/examples/full_automated_installation.py @@ -94,7 +94,7 @@ # perform all file operations # WARNING: this will potentially format the filesystem and delete all data -fs_handler.perform_filesystem_operations(show_countdown=False) +fs_handler.perform_filesystem_operations() mountpoint = Path('/tmp')