Async menu (#4308)

* Move to async TUI

* Update

* Update
This commit is contained in:
Daniel Girtler 2026-03-16 10:39:36 +11:00 committed by GitHub
parent ffa130fe99
commit b186fb3f64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 506 additions and 468 deletions

View File

@ -51,12 +51,12 @@ class DesktopProfile(Profile):
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 @@ class DesktopProfile(Profile):
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 @@ class DesktopProfile(Profile):
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

View File

@ -45,7 +45,7 @@ class HyprlandProfile(XorgProfile):
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 @@ class HyprlandProfile(XorgProfile):
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 @@ class HyprlandProfile(XorgProfile):
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()

View File

@ -42,7 +42,7 @@ class LabwcProfile(XorgProfile):
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 @@ class LabwcProfile(XorgProfile):
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 @@ class LabwcProfile(XorgProfile):
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()

View File

@ -50,7 +50,7 @@ class NiriProfile(XorgProfile):
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 @@ class NiriProfile(XorgProfile):
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 @@ class NiriProfile(XorgProfile):
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()

View File

@ -52,7 +52,7 @@ class SwayProfile(XorgProfile):
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 @@ class SwayProfile(XorgProfile):
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 @@ class SwayProfile(XorgProfile):
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()

View File

@ -117,7 +117,7 @@ class Profile:
"""
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
"""

View File

@ -23,7 +23,7 @@ class ServerProfile(Profile):
)
@override
def do_on_select(self) -> SelectResult:
async def do_on_select(self) -> SelectResult:
items = [
MenuItem(
p.name,
@ -36,7 +36,7 @@ class ServerProfile(Profile):
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,

View File

@ -39,8 +39,8 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
)
@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 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
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,

View File

@ -28,6 +28,7 @@ from archinstall.lib.output import debug, error, logger, warn
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 @@ class ArchConfigHandler:
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 @@ class ArchConfigHandler:
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

View File

@ -30,8 +30,8 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
)
@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 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
),
]
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 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
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,

View File

@ -86,12 +86,12 @@ class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
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 @@ class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
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 @@ class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
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 @@ class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
+ '\n'
)
result = Confirmation(
result = await Confirmation(
header=prompt,
allow_skip=True,
preset=preset,
@ -177,7 +177,7 @@ class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
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,

View File

@ -58,14 +58,14 @@ class ConfigurationOutput:
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 @@ class ConfigurationOutput:
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 save_config(config: ArchConfig) -> None:
]
group = MenuItemGroup(items)
result = Selection[str](
result = await Selection[str](
group,
allow_skip=True,
preview_location='right',
@ -178,7 +178,7 @@ def save_config(config: ArchConfig) -> 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 save_config(config: ArchConfig) -> 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 save_config(config: ArchConfig) -> 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 save_config(config: ArchConfig) -> 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,
)

View File

@ -93,8 +93,8 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
]
@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 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
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 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
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 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
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 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
preset=preset_type,
)
result = Selection[SnapshotType](
result = await Selection[SnapshotType](
group,
allow_reset=True,
allow_skip=True,

View File

@ -98,9 +98,9 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
),
]
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 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
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 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
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 select_iteration_time(preset: int | None = None) -> int | 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),

View File

@ -12,7 +12,6 @@ from archinstall.lib.disk.lvm import (
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 @@ from archinstall.lib.models.device import (
Unit,
)
from archinstall.lib.output import debug, info
from archinstall.lib.translationhandler import tr
class FilesystemHandler:
@ -37,7 +35,7 @@ class FilesystemHandler:
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 @@ class FilesystemHandler:
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 @@ class FilesystemHandler:
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

View File

@ -189,8 +189,8 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
# 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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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 @@ class PartitioningList(ListManager[DiskSegment]):
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

View File

@ -28,20 +28,20 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
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 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
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 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
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 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
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

View File

@ -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.hardware import SysInfo
from archinstall.lib.interactions.general_conf import add_number_of_parallel_downloads, select_hostname, select_ntp, select_timezone
from archinstall.lib.interactions.system_conf import select_kernel, select_swap
from archinstall.lib.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 @@ class GlobalMenu(AbstractMenu[None]):
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 @@ class GlobalMenu(AbstractMenu[None]):
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 @@ class GlobalMenu(AbstractMenu[None]):
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 @@ class GlobalMenu(AbstractMenu[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 @@ class GlobalMenu(AbstractMenu[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 @@ class GlobalMenu(AbstractMenu[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 @@ class GlobalMenu(AbstractMenu[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

View File

@ -32,7 +32,7 @@ from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
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 select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | 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 select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | 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()

View File

@ -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 add_number_of_parallel_downloads(preset: int = 1) -> int | 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 add_number_of_parallel_downloads(preset: int = 1) -> int | 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 add_number_of_parallel_downloads(preset: int = 1) -> int | 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)

View File

@ -8,7 +8,7 @@ from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
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,

View File

@ -50,22 +50,22 @@ class LocaleMenu(AbstractSubMenu[LocaleConfiguration]):
]
@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,

View File

@ -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 @@ class AbstractMenu[ValueT]:
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 @@ class AbstractMenu[ValueT]:
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 @@ class AbstractMenu[ValueT]:
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 @@ class AbstractMenu[ValueT]:
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 @@ class AbstractMenu[ValueT]:
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 _:

View File

@ -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 @@ class Selection[ValueT]:
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 @@ class Selection[ValueT]:
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 @@ class Confirmation:
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 @@ class Confirmation:
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 @@ class Input:
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 @@ class Input:
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 @@ class Loading[ValueT]:
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 @@ class Table[ValueT]:
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 @@ class Table[ValueT]:
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]:

View File

@ -53,7 +53,7 @@ class ListManager[ValueT]:
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 @@ class ListManager[ValueT]:
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 @@ class ListManager[ValueT]:
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 @@ class ListManager[ValueT]:
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 @@ class ListManager[ValueT]:
header = f'{self.selected_action_display(entry)}'
result = Selection[str](
result = await Selection[str](
group,
header=header,
enable_filter=False,
@ -119,7 +119,7 @@ class ListManager[ValueT]:
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 @@ class ListManager[ValueT]:
"""
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

View File

@ -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 get_password(
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 get_password(
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 prompt_dir(
else:
validate_func = None
result = Input(
result = await Input(
header=header,
allow_skip=allow_skip,
validator_callback=validate_func,
@ -96,3 +99,33 @@ def prompt_dir(
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

View File

@ -39,27 +39,27 @@ class CustomMirrorRepositoriesList(ListManager[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 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
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 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
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 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
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 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
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 @@ class CustomMirrorServersList(ListManager[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 @@ class CustomMirrorServersList(ListManager[CustomServer]):
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 @@ class MirrorMenu(AbstractSubMenu[MirrorConfiguration]):
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,

View File

@ -25,31 +25,32 @@ class ManualNetworkConfig(ListManager[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 @@ class ManualNetworkConfig(ListManager[Nic]):
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 @@ class ManualNetworkConfig(ListManager[Nic]):
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 @@ class ManualNetworkConfig(ListManager[Nic]):
except ValueError:
return failure
result = Input(
result = await Input(
header=header,
validator_callback=validator,
allow_skip=allow_skip,
@ -113,7 +114,7 @@ class ManualNetworkConfig(ListManager[Nic]):
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 @@ class ManualNetworkConfig(ListManager[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 @@ class ManualNetworkConfig(ListManager[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 @@ class ManualNetworkConfig(ListManager[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 @@ class ManualNetworkConfig(ListManager[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)

View File

@ -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.models.network import WifiConfiguredNetwork, WifiNetwork
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 @@ class WifiHandler:
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 @@ class WifiHandler:
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()

View File

@ -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,

View File

@ -60,11 +60,11 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
]
@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 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
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 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
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

View File

@ -26,17 +26,17 @@ class UserList(ListManager[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 @@ class UserList(ListManager[User]):
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 @@ class UserList(ListManager[User]):
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 @@ class UserList(ListManager[User]):
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 @@ class UserList(ListManager[User]):
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 @@ class UserList(ListManager[User]):
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

View File

@ -18,6 +18,7 @@ from archinstall.lib.packages.util import check_version_upgrade
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

View File

@ -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.disk.utils import disk_layouts
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.installer import Installer, accessibility_tools_in_use, run_custom_user_commands
from archinstall.lib.interactions.general_conf import PostInstallationAction, select_post_installation
from archinstall.lib.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.output import debug, error, info
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(

View File

@ -4,12 +4,15 @@ from archinstall.lib.configuration import ConfigurationOutput
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)

View File

@ -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)

View File

@ -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 @@ class BaseScreen(Screen[Result[ValueT]]):
_ = self.dismiss(Result(ResultType.Reset))
class LoadingScreen(BaseScreen[None]):
class LoadingScreen(BaseScreen[ValueT]):
CSS = """
LoadingScreen {
align: center middle;
@ -78,7 +79,7 @@ class LoadingScreen(BaseScreen[None]):
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 @@ class TableSelectionScreen(BaseScreen[ValueT]):
)
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 @@ class _AppInstance(App[ValueT]):
@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 @@ class _AppInstance(App[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 @@ class TApp:
return result
def exit(self, result: Result[ValueT]) -> None:
def exit(self, result: Any) -> None:
assert TApp.app
TApp.app.exit(result)

View File

@ -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 @@ from archinstall.lib.translationhandler import tr
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

View File

@ -25,6 +25,18 @@ class Result[ValueT]:
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

View File

@ -94,7 +94,7 @@ fs_handler = FilesystemHandler(disk_config)
# 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')