Migrate UI to textual (#3997)
* Footer textual * Linting * Revert pre-commit for pylint * Add missing textual * Reinstate example * Linting
This commit is contained in:
parent
9a38b73baf
commit
0aca992ac5
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
pacman --noconfirm -Sy archlinux-keyring
|
||||
pacman --noconfirm -Syyu
|
||||
pacman --noconfirm -Sy python-uv python-setuptools python-pip
|
||||
pacman --noconfirm -Sy python-pyparted python-pydantic
|
||||
pacman --noconfirm -Sy python-pyparted python-pydantic python-textual
|
||||
- name: Remove existing archinstall (if any)
|
||||
run:
|
||||
uv pip uninstall archinstall --break-system-packages --system
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
pacman-key --init
|
||||
pacman --noconfirm -Sy archlinux-keyring
|
||||
pacman --noconfirm -Syyu
|
||||
pacman --noconfirm -Sy python python-uv python-setuptools python-pip python-pyparted python-pydantic
|
||||
pacman --noconfirm -Sy python python-uv python-setuptools python-pip python-pyparted python-pydantic python-textual
|
||||
- name: Build archinstall
|
||||
run: |
|
||||
uv build --no-build-isolation --wheel
|
||||
|
|
|
|||
|
|
@ -43,9 +43,12 @@ repos:
|
|||
- pytest
|
||||
- cryptography
|
||||
- textual
|
||||
- repo: https://github.com/pycqa/pylint
|
||||
rev: v4.0.4
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
fail_fast: true
|
||||
require_serial: true
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: pylint
|
||||
language: system
|
||||
types: [python]
|
||||
fail_fast: true
|
||||
require_serial: true
|
||||
|
|
|
|||
|
|
@ -8,18 +8,16 @@ import traceback
|
|||
|
||||
from archinstall.lib.args import arch_config_handler
|
||||
from archinstall.lib.disk.utils import disk_layouts
|
||||
from archinstall.lib.general import running_from_host
|
||||
from archinstall.lib.network.wifi_handler import wifi_handler
|
||||
from archinstall.lib.networking import ping
|
||||
from archinstall.lib.packages.packages import check_package_upgrade
|
||||
from archinstall.tui.ui.components import tui as ttui
|
||||
from archinstall.lib.packages.packages import check_version_upgrade
|
||||
|
||||
from .lib.general import running_from_host
|
||||
from .lib.hardware import SysInfo
|
||||
from .lib.output import FormattedOutput, debug, error, info, log, warn
|
||||
from .lib.pacman import Pacman
|
||||
from .lib.plugins import load_plugin, plugins
|
||||
from .lib.translationhandler import Language, tr, translation_handler
|
||||
from .tui.curses_menu import Tui
|
||||
|
||||
|
||||
# @archinstall.plugin decorator hook to programmatically add
|
||||
|
|
@ -66,21 +64,6 @@ def _fetch_arch_db() -> None:
|
|||
sys.exit(1)
|
||||
|
||||
|
||||
def check_version_upgrade() -> str | None:
|
||||
info('Checking version...')
|
||||
upgrade = None
|
||||
|
||||
upgrade = check_package_upgrade('archinstall')
|
||||
|
||||
if upgrade is None:
|
||||
debug('No archinstall upgrades found')
|
||||
return None
|
||||
|
||||
text = tr('New version available') + f': {upgrade}'
|
||||
info(text)
|
||||
return text
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
This can either be run as the compiled and installed application: python setup.py install
|
||||
|
|
@ -97,18 +80,16 @@ def main() -> int:
|
|||
|
||||
_log_sys_info()
|
||||
|
||||
ttui.global_header = 'Archinstall'
|
||||
|
||||
if not arch_config_handler.args.offline:
|
||||
_check_online()
|
||||
_fetch_arch_db()
|
||||
|
||||
if not arch_config_handler.args.skip_version_check:
|
||||
new_version = check_version_upgrade()
|
||||
upgrade = check_version_upgrade()
|
||||
|
||||
if new_version:
|
||||
ttui.global_header = f'{ttui.global_header} {new_version}'
|
||||
info(new_version)
|
||||
if upgrade:
|
||||
text = tr('New version available') + f': {upgrade}'
|
||||
info(text)
|
||||
time.sleep(3)
|
||||
|
||||
if running_from_host():
|
||||
|
|
@ -135,9 +116,6 @@ def run_as_a_module() -> None:
|
|||
except Exception as e:
|
||||
exc = e
|
||||
finally:
|
||||
# restore the terminal to the original state
|
||||
Tui.shutdown()
|
||||
|
||||
if exc:
|
||||
err = ''.join(traceback.format_exception(exc))
|
||||
error(err)
|
||||
|
|
@ -159,7 +137,6 @@ __all__ = [
|
|||
'Language',
|
||||
'Pacman',
|
||||
'SysInfo',
|
||||
'Tui',
|
||||
'arch_config_handler',
|
||||
'debug',
|
||||
'disk_layouts',
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING, Self, override
|
||||
|
||||
from archinstall.default_profiles.profile import GreeterType, Profile, ProfileType, SelectResult
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.output import info
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import FrameProperties, PreviewStyle
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
|
@ -62,7 +61,7 @@ class DesktopProfile(Profile):
|
|||
MenuItem(
|
||||
p.name,
|
||||
value=p,
|
||||
preview_action=lambda x: x.value.preview_text(),
|
||||
preview_action=lambda x: x.value.preview_text() if x.value else None,
|
||||
)
|
||||
for p in profile_handler.get_desktop_profiles()
|
||||
]
|
||||
|
|
@ -70,15 +69,13 @@ class DesktopProfile(Profile):
|
|||
group = MenuItemGroup(items, sort_items=True, sort_case_sensitive=False)
|
||||
group.set_selected_by_value(self.current_selection)
|
||||
|
||||
result = SelectMenu[Self](
|
||||
result = Selection[Self](
|
||||
group,
|
||||
multi=True,
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
preview_style=PreviewStyle.RIGHT,
|
||||
preview_size='auto',
|
||||
preview_frame=FrameProperties.max('Info'),
|
||||
).run()
|
||||
preview_location='right',
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ from typing import override
|
|||
from archinstall.default_profiles.desktops import SeatAccess
|
||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
||||
from archinstall.default_profiles.xorg import XorgProfile
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
|
||||
class HyprlandProfile(XorgProfile):
|
||||
|
|
@ -57,13 +56,11 @@ class HyprlandProfile(XorgProfile):
|
|||
default = self.custom_settings.get('seat_access', None)
|
||||
group.set_default_by_value(default)
|
||||
|
||||
result = SelectMenu[SeatAccess](
|
||||
result = Selection[SeatAccess](
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
frame=FrameProperties.min(tr('Seat access')),
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
if result.type_ == ResultType.Selection:
|
||||
self.custom_settings['seat_access'] = result.get_value().value
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ from typing import override
|
|||
from archinstall.default_profiles.desktops import SeatAccess
|
||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
||||
from archinstall.default_profiles.xorg import XorgProfile
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
|
||||
class LabwcProfile(XorgProfile):
|
||||
|
|
@ -54,13 +53,11 @@ class LabwcProfile(XorgProfile):
|
|||
default = self.custom_settings.get('seat_access', None)
|
||||
group.set_default_by_value(default)
|
||||
|
||||
result = SelectMenu[SeatAccess](
|
||||
result = Selection[SeatAccess](
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
frame=FrameProperties.min(tr('Seat access')),
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
if result.type_ == ResultType.Selection:
|
||||
self.custom_settings['seat_access'] = result.get_value().value
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ from typing import override
|
|||
from archinstall.default_profiles.desktops import SeatAccess
|
||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
||||
from archinstall.default_profiles.xorg import XorgProfile
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
|
||||
class NiriProfile(XorgProfile):
|
||||
|
|
@ -62,13 +61,11 @@ class NiriProfile(XorgProfile):
|
|||
default = self.custom_settings.get('seat_access', None)
|
||||
group.set_default_by_value(default)
|
||||
|
||||
result = SelectMenu[SeatAccess](
|
||||
result = Selection[SeatAccess](
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
frame=FrameProperties.min(tr('Seat access')),
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
if result.type_ == ResultType.Selection:
|
||||
self.custom_settings['seat_access'] = result.get_value().value
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ from typing import override
|
|||
from archinstall.default_profiles.desktops import SeatAccess
|
||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
||||
from archinstall.default_profiles.xorg import XorgProfile
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
|
||||
class SwayProfile(XorgProfile):
|
||||
|
|
@ -64,13 +63,11 @@ class SwayProfile(XorgProfile):
|
|||
default = self.custom_settings.get('seat_access', None)
|
||||
group.set_default_by_value(default)
|
||||
|
||||
result = SelectMenu[SeatAccess](
|
||||
result = Selection[SeatAccess](
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
frame=FrameProperties.min(tr('Seat access')),
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
if result.type_ == ResultType.Selection:
|
||||
self.custom_settings['seat_access'] = result.get_value().value
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING, Self, override
|
||||
|
||||
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.output import info
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import FrameProperties, PreviewStyle
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
|
@ -28,7 +27,7 @@ class ServerProfile(Profile):
|
|||
MenuItem(
|
||||
p.name,
|
||||
value=p,
|
||||
preview_action=lambda x: x.value.preview_text(),
|
||||
preview_action=lambda x: x.value.preview_text() if x.value else None,
|
||||
)
|
||||
for p in profile_handler.get_server_profiles()
|
||||
]
|
||||
|
|
@ -36,15 +35,13 @@ class ServerProfile(Profile):
|
|||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_selected_by_value(self.current_selection)
|
||||
|
||||
result = SelectMenu[Self](
|
||||
result = Selection[Self](
|
||||
group,
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
preview_style=PreviewStyle.RIGHT,
|
||||
preview_size='auto',
|
||||
preview_frame=FrameProperties.max('Info'),
|
||||
multi=True,
|
||||
).run()
|
||||
preview_location='right',
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from typing import override
|
|||
|
||||
from archinstall.lib.hardware import SysInfo
|
||||
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
|
||||
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||
from archinstall.lib.models.application import (
|
||||
ApplicationConfiguration,
|
||||
Audio,
|
||||
|
|
@ -14,10 +15,8 @@ from archinstall.lib.models.application import (
|
|||
PrintServiceConfiguration,
|
||||
)
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
|
||||
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
||||
|
|
@ -40,8 +39,8 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
|||
)
|
||||
|
||||
@override
|
||||
def run(self, additional_title: str | None = None) -> ApplicationConfiguration:
|
||||
super().run(additional_title=additional_title)
|
||||
def run(self) -> ApplicationConfiguration:
|
||||
super().run()
|
||||
return self._app_config
|
||||
|
||||
def _define_menu_options(self) -> list[MenuItem]:
|
||||
|
|
@ -123,13 +122,11 @@ def select_power_management(preset: PowerManagementConfiguration | None = None)
|
|||
if preset:
|
||||
group.set_focus_by_value(preset.power_management)
|
||||
|
||||
result = SelectMenu[PowerManagement](
|
||||
result = Selection[PowerManagement](
|
||||
group,
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_reset=True,
|
||||
frame=FrameProperties.min(tr('Power management')),
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -141,27 +138,18 @@ def select_power_management(preset: PowerManagementConfiguration | None = None)
|
|||
|
||||
|
||||
def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None:
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.no()
|
||||
|
||||
if preset is not None:
|
||||
group.set_selected_by_value(preset.enabled)
|
||||
|
||||
header = tr('Would you like to configure Bluetooth?') + '\n'
|
||||
preset_val = preset.enabled if preset else False
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
result = Confirmation(
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
preset=preset_val,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
enabled = result.item() == MenuItem.yes()
|
||||
return BluetoothConfiguration(enabled)
|
||||
return BluetoothConfiguration(result.get_value())
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
|
|
@ -169,27 +157,19 @@ def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfigur
|
|||
|
||||
|
||||
def select_print_service(preset: PrintServiceConfiguration | None) -> PrintServiceConfiguration | None:
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.no()
|
||||
|
||||
if preset is not None:
|
||||
group.set_selected_by_value(preset.enabled)
|
||||
|
||||
header = tr('Would you like to configure the print service?') + '\n'
|
||||
preset_val = preset.enabled if preset else False
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
result = Confirmation(
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
preset=preset_val,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
enabled = result.item() == MenuItem.yes()
|
||||
return PrintServiceConfiguration(enabled)
|
||||
result.get_value()
|
||||
return PrintServiceConfiguration(result.get_value())
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
|
|
@ -203,12 +183,11 @@ def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration
|
|||
if preset:
|
||||
group.set_focus_by_value(preset.audio)
|
||||
|
||||
result = SelectMenu[Audio](
|
||||
result = Selection[Audio](
|
||||
group,
|
||||
header=tr('Select audio configuration'),
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Audio')),
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -225,13 +204,11 @@ def select_firewall(preset: FirewallConfiguration | None = None) -> FirewallConf
|
|||
if preset:
|
||||
group.set_focus_by_value(preset.firewall)
|
||||
|
||||
result = SelectMenu[Firewall](
|
||||
result = Selection[Firewall](
|
||||
group,
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_reset=True,
|
||||
frame=FrameProperties.min(tr('Firewall')),
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ 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.utils.util import get_password
|
||||
from archinstall.tui.curses_menu import Tui
|
||||
|
||||
|
||||
@p_dataclass
|
||||
|
|
@ -499,31 +498,30 @@ class ArchConfigHandler:
|
|||
raise err from err
|
||||
else:
|
||||
incorrect_password = False
|
||||
header = tr('Enter credentials file decryption password')
|
||||
|
||||
with Tui():
|
||||
while True:
|
||||
header = tr('Incorrect password') if incorrect_password else None
|
||||
while True:
|
||||
prompt = f'{header}\n\n' + tr('Incorrect password') if incorrect_password else ''
|
||||
|
||||
decryption_pwd = get_password(
|
||||
text=tr('Credentials file decryption password'),
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
skip_confirmation=True,
|
||||
)
|
||||
decryption_pwd = get_password(
|
||||
header=prompt,
|
||||
allow_skip=False,
|
||||
skip_confirmation=True,
|
||||
)
|
||||
|
||||
if not decryption_pwd:
|
||||
return None
|
||||
if not decryption_pwd:
|
||||
return None
|
||||
|
||||
try:
|
||||
creds_data = decrypt(creds_data, decryption_pwd.plaintext)
|
||||
break
|
||||
except ValueError as err:
|
||||
if 'Invalid password' in str(err):
|
||||
debug('Incorrect credentials file decryption password')
|
||||
incorrect_password = True
|
||||
else:
|
||||
debug(f'Error decrypting credentials file: {err}')
|
||||
raise err from err
|
||||
try:
|
||||
creds_data = decrypt(creds_data, decryption_pwd.plaintext)
|
||||
break
|
||||
except ValueError as err:
|
||||
if 'Invalid password' in str(err):
|
||||
debug('Incorrect credentials file decryption password')
|
||||
incorrect_password = True
|
||||
else:
|
||||
debug(f'Error decrypting credentials file: {err}')
|
||||
raise err from err
|
||||
|
||||
return json.loads(creds_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ from typing import TYPE_CHECKING
|
|||
from archinstall.lib.general import SysCommandWorker
|
||||
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
|
||||
from archinstall.lib.models.users import User
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.output import debug, info
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import Tui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
|
@ -84,7 +83,7 @@ class AuthenticationHandler:
|
|||
|
||||
install_session.pacman.strap('pam-u2f')
|
||||
|
||||
Tui.print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}'))
|
||||
print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}'))
|
||||
|
||||
# https://developers.yubico.com/pam-u2f/
|
||||
u2f_auth_file = install_session.target / 'etc/u2f_mappings'
|
||||
|
|
@ -94,9 +93,9 @@ class AuthenticationHandler:
|
|||
registered_keys: list[str] = []
|
||||
|
||||
for user in users:
|
||||
Tui.print('')
|
||||
Tui.print(tr('Setting up U2F device for user: {}').format(user.username))
|
||||
Tui.print(tr('You may need to enter the PIN and then touch your U2F device to register it'))
|
||||
print('')
|
||||
info(tr('Setting up U2F device for user: {}').format(user.username))
|
||||
info(tr('You may need to enter the PIN and then touch your U2F device to register it'))
|
||||
|
||||
cmd = ' '.join(
|
||||
['arch-chroot', '-S', str(install_session.target), 'pamu2fcfg', '-u', user.username, '-o', f'pam://{hostname}', '-i', f'pam://{hostname}']
|
||||
|
|
|
|||
|
|
@ -3,15 +3,14 @@ from typing import override
|
|||
from archinstall.lib.disk.fido import Fido2
|
||||
from archinstall.lib.interactions.manage_users_conf import ask_for_additional_users
|
||||
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
|
||||
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
|
||||
from archinstall.lib.models.users import Password, User
|
||||
from archinstall.lib.output import FormattedOutput
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.lib.utils.util import get_password
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
|
||||
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
||||
|
|
@ -31,15 +30,14 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
|||
)
|
||||
|
||||
@override
|
||||
def run(self, additional_title: str | None = None) -> AuthenticationConfiguration:
|
||||
super().run(additional_title=additional_title)
|
||||
return self._auth_config
|
||||
def run(self) -> AuthenticationConfiguration | None:
|
||||
return super().run()
|
||||
|
||||
def _define_menu_options(self) -> list[MenuItem]:
|
||||
return [
|
||||
MenuItem(
|
||||
text=tr('Root password'),
|
||||
action=select_root_password,
|
||||
action=lambda x: select_root_password(),
|
||||
preview_action=self._prev_root_pwd,
|
||||
key='root_enc_password',
|
||||
),
|
||||
|
|
@ -101,12 +99,12 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
|||
return None
|
||||
|
||||
|
||||
def select_root_password(preset: str | None = None) -> Password | None:
|
||||
password = get_password(text=tr('Root password'), allow_skip=True)
|
||||
def select_root_password() -> Password | None:
|
||||
password = get_password(header=tr('Enter root password'), allow_skip=True)
|
||||
return password
|
||||
|
||||
|
||||
def select_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | None:
|
||||
def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConfiguration | None:
|
||||
devices = Fido2.get_fido2_devices()
|
||||
if not devices:
|
||||
return None
|
||||
|
|
@ -120,30 +118,22 @@ def select_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | N
|
|||
if preset is not None:
|
||||
group.set_selected_by_value(preset.u2f_login_method)
|
||||
|
||||
result = SelectMenu[U2FLoginMethod](
|
||||
result = Selection[U2FLoginMethod](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('U2F Login Method')),
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
u2f_method = result.get_value()
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.no()
|
||||
header = tr('Enable passwordless sudo?')
|
||||
|
||||
result_sudo = SelectMenu[bool](
|
||||
group,
|
||||
result_sudo = Confirmation(
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
preset=False,
|
||||
).show()
|
||||
|
||||
passwordless_sudo = result_sudo.item() == MenuItem.yes()
|
||||
|
||||
|
|
@ -155,5 +145,3 @@ def select_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | N
|
|||
return preset
|
||||
case ResultType.Reset:
|
||||
return None
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import textwrap
|
||||
from typing import override
|
||||
|
||||
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..args import arch_config_handler
|
||||
from ..hardware import SysInfo
|
||||
|
|
@ -87,11 +86,8 @@ class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
|
|||
return tr('Will install to custom location with NVRAM entry')
|
||||
|
||||
@override
|
||||
def run(
|
||||
self,
|
||||
additional_title: str | None = None,
|
||||
) -> BootloaderConfiguration:
|
||||
super().run(additional_title=additional_title)
|
||||
def run(self) -> BootloaderConfiguration:
|
||||
super().run()
|
||||
return self._bootloader_conf
|
||||
|
||||
def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None:
|
||||
|
|
@ -124,17 +120,7 @@ class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
|
|||
def _select_uki(self, preset: bool) -> bool:
|
||||
prompt = tr('Would you like to use unified kernel images?') + '\n'
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
header=prompt,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
result = Confirmation(header=prompt, allow_skip=True, preset=preset).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -151,7 +137,7 @@ class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
|
|||
+ tr('This installs the bootloader to /EFI/BOOT/BOOTX64.EFI (or similar) which is useful for:')
|
||||
+ '\n\n • '
|
||||
+ tr('Firmware that does not properly support NVRAM boot entries like most MSI motherboards,')
|
||||
+ '\n '
|
||||
+ '\n '
|
||||
+ tr('most Apple Macs, many laptops...')
|
||||
+ '\n • '
|
||||
+ tr('USB drives or other portable external media.')
|
||||
|
|
@ -176,23 +162,17 @@ class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
|
|||
+ '\n'
|
||||
)
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
result = Confirmation(
|
||||
header=prompt,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
preset=preset,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return result.item() == MenuItem.yes()
|
||||
return result.get_value()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
|
@ -201,7 +181,7 @@ def ask_for_bootloader(preset: Bootloader | None) -> Bootloader | None:
|
|||
options = []
|
||||
hidden_options = []
|
||||
default = None
|
||||
header = None
|
||||
header = tr('Select bootloader to install')
|
||||
|
||||
if arch_config_handler.args.skip_boot:
|
||||
default = Bootloader.NO_BOOTLOADER
|
||||
|
|
@ -212,7 +192,7 @@ def ask_for_bootloader(preset: Bootloader | None) -> Bootloader | None:
|
|||
options += [Bootloader.Grub, Bootloader.Limine]
|
||||
if not default:
|
||||
default = Bootloader.Grub
|
||||
header = tr('UEFI is not detected and some options are disabled')
|
||||
header += '\n' + tr('UEFI is not detected and some options are disabled')
|
||||
else:
|
||||
options += [b for b in Bootloader if b not in hidden_options]
|
||||
if not default:
|
||||
|
|
@ -223,13 +203,11 @@ def ask_for_bootloader(preset: Bootloader | None) -> Bootloader | None:
|
|||
group.set_default_by_value(default)
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
result = SelectMenu[Bootloader](
|
||||
result = Selection[Bootloader](
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Bootloader')),
|
||||
allow_skip=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ import readline
|
|||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu, Tui
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from .args import ArchConfig
|
||||
from .crypt import encrypt
|
||||
|
|
@ -56,25 +55,20 @@ class ConfigurationOutput:
|
|||
header = f'{tr("The specified configuration will be applied")}. '
|
||||
header += tr('Would you like to continue?') + '\n'
|
||||
|
||||
with Tui():
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.yes()
|
||||
group.set_preview_for_all(lambda x: self.user_config_to_json())
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_preview_for_all(lambda x: self.user_config_to_json())
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
allow_skip=False,
|
||||
preview_size='auto',
|
||||
preview_style=PreviewStyle.BOTTOM,
|
||||
preview_frame=FrameProperties.max(tr('Configuration')),
|
||||
).run()
|
||||
result = Confirmation(
|
||||
group=group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
preset=True,
|
||||
preview_location='bottom',
|
||||
preview_header=tr('Configuration preview'),
|
||||
).show()
|
||||
|
||||
if result.item() != MenuItem.yes():
|
||||
return False
|
||||
if not result.get_value():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -160,13 +154,11 @@ def save_config(config: ArchConfig) -> None:
|
|||
]
|
||||
|
||||
group = MenuItemGroup(items)
|
||||
result = SelectMenu[str](
|
||||
result = Selection[str](
|
||||
group,
|
||||
allow_skip=True,
|
||||
preview_frame=FrameProperties.max(tr('Configuration')),
|
||||
preview_size='auto',
|
||||
preview_style=PreviewStyle.RIGHT,
|
||||
).run()
|
||||
preview_location='right',
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -180,8 +172,7 @@ def save_config(config: ArchConfig) -> None:
|
|||
readline.parse_and_bind('tab: complete')
|
||||
|
||||
dest_path = prompt_dir(
|
||||
tr('Directory'),
|
||||
tr('Enter a directory for the configuration(s) to be saved (tab completion enabled)') + '\n',
|
||||
tr('Enter a directory for the configuration(s) to be saved') + '\n',
|
||||
allow_skip=True,
|
||||
)
|
||||
|
||||
|
|
@ -190,50 +181,39 @@ def save_config(config: ArchConfig) -> None:
|
|||
|
||||
header = tr('Do you want to save the configuration file(s) to {}?').format(dest_path)
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.yes()
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
save_result = Confirmation(
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
).run()
|
||||
preset=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
match save_result.type_:
|
||||
case ResultType.Selection:
|
||||
if result.item() == MenuItem.no():
|
||||
if not save_result.get_value():
|
||||
return
|
||||
case _:
|
||||
return
|
||||
|
||||
debug(f'Saving configuration files to {dest_path.absolute()}')
|
||||
|
||||
header = tr('Do you want to encrypt the user_credentials.json file?')
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.no()
|
||||
|
||||
result = SelectMenu(
|
||||
group,
|
||||
enc_result = Confirmation(
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
).run()
|
||||
preset=False,
|
||||
).show()
|
||||
|
||||
enc_password: str | None = None
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
if result.item() == MenuItem.yes():
|
||||
password = get_password(
|
||||
text=tr('Credentials file encryption password'),
|
||||
allow_skip=True,
|
||||
)
|
||||
if enc_result.type_ == ResultType.Selection:
|
||||
if enc_result.get_value():
|
||||
password = get_password(
|
||||
header=tr('Credentials file encryption password'),
|
||||
allow_skip=True,
|
||||
)
|
||||
|
||||
if password:
|
||||
enc_password = password.plaintext
|
||||
if password:
|
||||
enc_password = password.plaintext
|
||||
|
||||
match save_option:
|
||||
case 'user_config':
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|||
from typing import override
|
||||
|
||||
from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.models.device import (
|
||||
DEFAULT_ITER_TIME,
|
||||
BtrfsOptions,
|
||||
|
|
@ -14,10 +15,8 @@ from archinstall.lib.models.device import (
|
|||
SnapshotType,
|
||||
)
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..interactions.disk_conf import select_disk_config, select_lvm_config
|
||||
from ..menu.abstract_menu import AbstractSubMenu
|
||||
|
|
@ -32,7 +31,7 @@ class DiskMenuConfig:
|
|||
disk_encryption: DiskEncryption | None
|
||||
|
||||
|
||||
class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||
class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
|
||||
def __init__(self, disk_layout_config: DiskLayoutConfiguration | None):
|
||||
if not disk_layout_config:
|
||||
self._disk_menu_config = DiskMenuConfig(
|
||||
|
|
@ -95,14 +94,16 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
|||
]
|
||||
|
||||
@override
|
||||
def run(self, additional_title: str | None = None) -> DiskLayoutConfiguration | None:
|
||||
super().run(additional_title=additional_title)
|
||||
def run(self) -> DiskLayoutConfiguration | None: # type: ignore[override]
|
||||
config: DiskMenuConfig | None = super().run()
|
||||
if config is None:
|
||||
return None
|
||||
|
||||
if self._disk_menu_config.disk_config:
|
||||
self._disk_menu_config.disk_config.lvm_config = self._disk_menu_config.lvm_config
|
||||
self._disk_menu_config.disk_config.btrfs_options = BtrfsOptions(snapshot_config=self._disk_menu_config.btrfs_snapshot_config)
|
||||
self._disk_menu_config.disk_config.disk_encryption = self._disk_menu_config.disk_encryption
|
||||
return self._disk_menu_config.disk_config
|
||||
if config.disk_config:
|
||||
config.disk_config.lvm_config = self._disk_menu_config.lvm_config
|
||||
config.disk_config.btrfs_options = BtrfsOptions(snapshot_config=self._disk_menu_config.btrfs_snapshot_config)
|
||||
config.disk_config.disk_encryption = self._disk_menu_config.disk_encryption
|
||||
return config.disk_config
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -169,13 +170,11 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
|||
preset=preset_type,
|
||||
)
|
||||
|
||||
result = SelectMenu[SnapshotType](
|
||||
result = Selection[SnapshotType](
|
||||
group,
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
frame=FrameProperties.min(tr('Snapshot type')),
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -250,9 +249,10 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
|||
|
||||
def _prev_disk_encryption(self, item: MenuItem) -> str | 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
|
||||
enc_config: DiskEncryption | None = item.value
|
||||
|
||||
if disk_config and not DiskEncryption.validate_enc(disk_config.device_modifications, disk_config.lvm_config):
|
||||
if disk_config and not DiskEncryption.validate_enc(disk_config.device_modifications, lvm_config):
|
||||
return tr('LVM disk encryption with more than 2 partitions is currently not supported')
|
||||
|
||||
if enc_config:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
from archinstall.lib.menu.helpers import Input, Selection, Table
|
||||
from archinstall.lib.menu.menu_helper import MenuHelper
|
||||
from archinstall.lib.models.device import (
|
||||
DeviceModification,
|
||||
|
|
@ -11,10 +12,8 @@ from archinstall.lib.models.device import (
|
|||
PartitionModification,
|
||||
)
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import EditMenu, SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..menu.abstract_menu import AbstractSubMenu
|
||||
from ..models.device import DEFAULT_ITER_TIME, Fido2Device
|
||||
|
|
@ -52,9 +51,9 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
return [
|
||||
MenuItem(
|
||||
text=tr('Encryption type'),
|
||||
action=lambda x: select_encryption_type(self._device_modifications, self._lvm_config, x),
|
||||
action=lambda x: select_encryption_type(self._lvm_config, x),
|
||||
value=self._enc_config.encryption_type,
|
||||
preview_action=self._preview,
|
||||
preview_action=self._prev_type,
|
||||
key='encryption_type',
|
||||
),
|
||||
MenuItem(
|
||||
|
|
@ -62,7 +61,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
action=lambda x: select_encrypted_password(),
|
||||
value=self._enc_config.encryption_password,
|
||||
dependencies=[self._check_dep_enc_type],
|
||||
preview_action=self._preview,
|
||||
preview_action=self._prev_password,
|
||||
key='encryption_password',
|
||||
),
|
||||
MenuItem(
|
||||
|
|
@ -70,7 +69,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
action=select_iteration_time,
|
||||
value=self._enc_config.iter_time,
|
||||
dependencies=[self._check_dep_enc_type],
|
||||
preview_action=self._preview,
|
||||
preview_action=self._prev_iter_time,
|
||||
key='iter_time',
|
||||
),
|
||||
MenuItem(
|
||||
|
|
@ -78,7 +77,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
action=lambda x: select_partitions_to_encrypt(self._device_modifications, x),
|
||||
value=self._enc_config.partitions,
|
||||
dependencies=[self._check_dep_partitions],
|
||||
preview_action=self._preview,
|
||||
preview_action=self._prev_partitions,
|
||||
key='partitions',
|
||||
),
|
||||
MenuItem(
|
||||
|
|
@ -86,7 +85,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
action=self._select_lvm_vols,
|
||||
value=self._enc_config.lvm_volumes,
|
||||
dependencies=[self._check_dep_lvm_vols],
|
||||
preview_action=self._preview,
|
||||
preview_action=self._prev_lvm_vols,
|
||||
key='lvm_volumes',
|
||||
),
|
||||
MenuItem(
|
||||
|
|
@ -94,7 +93,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
action=select_hsm,
|
||||
value=self._enc_config.hsm_device,
|
||||
dependencies=[self._check_dep_enc_type],
|
||||
preview_action=self._preview,
|
||||
preview_action=self._prev_hsm,
|
||||
key='hsm_device',
|
||||
),
|
||||
]
|
||||
|
|
@ -123,8 +122,10 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
return False
|
||||
|
||||
@override
|
||||
def run(self, additional_title: str | None = None) -> DiskEncryption | None:
|
||||
super().run(additional_title=additional_title)
|
||||
def run(self) -> DiskEncryption | None:
|
||||
enc_config = super().run()
|
||||
if enc_config is None:
|
||||
return None
|
||||
|
||||
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
|
||||
enc_password: Password | None = self._item_group.find_by_key('encryption_password').value
|
||||
|
|
@ -148,7 +149,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
encryption_type=enc_type,
|
||||
partitions=enc_partitions,
|
||||
lvm_volumes=enc_lvm_vols,
|
||||
hsm_device=self._enc_config.hsm_device,
|
||||
hsm_device=enc_config.hsm_device,
|
||||
iter_time=iter_time or DEFAULT_ITER_TIME,
|
||||
)
|
||||
|
||||
|
|
@ -157,22 +158,22 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
def _preview(self, item: MenuItem) -> str | None:
|
||||
output = ''
|
||||
|
||||
if (enc_type := self._prev_type()) is not None:
|
||||
if (enc_type := self._prev_type(item)) is not None:
|
||||
output += enc_type
|
||||
|
||||
if (enc_pwd := self._prev_password()) is not None:
|
||||
if (enc_pwd := self._prev_password(item)) is not None:
|
||||
output += f'\n{enc_pwd}'
|
||||
|
||||
if (iter_time := self._prev_iter_time()) is not None:
|
||||
if (iter_time := self._prev_iter_time(item)) is not None:
|
||||
output += f'\n{iter_time}'
|
||||
|
||||
if (fido_device := self._prev_hsm()) is not None:
|
||||
if (fido_device := self._prev_hsm(item)) is not None:
|
||||
output += f'\n{fido_device}'
|
||||
|
||||
if (partitions := self._prev_partitions()) is not None:
|
||||
if (partitions := self._prev_partitions(item)) is not None:
|
||||
output += f'\n\n{partitions}'
|
||||
|
||||
if (lvm := self._prev_lvm_vols()) is not None:
|
||||
if (lvm := self._prev_lvm_vols(item)) is not None:
|
||||
output += f'\n\n{lvm}'
|
||||
|
||||
if not output:
|
||||
|
|
@ -180,7 +181,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
|
||||
return output
|
||||
|
||||
def _prev_type(self) -> str | None:
|
||||
def _prev_type(self, item: MenuItem) -> str | None:
|
||||
enc_type = self._item_group.find_by_key('encryption_type').value
|
||||
|
||||
if enc_type:
|
||||
|
|
@ -189,56 +190,50 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
|||
|
||||
return None
|
||||
|
||||
def _prev_password(self) -> str | None:
|
||||
enc_pwd = self._item_group.find_by_key('encryption_password').value
|
||||
|
||||
if enc_pwd:
|
||||
return f'{tr("Encryption password")}: {enc_pwd.hidden()}'
|
||||
def _prev_password(self, item: MenuItem) -> str | None:
|
||||
if item.value:
|
||||
return f'{tr("Encryption password")}: {item.value.hidden()}'
|
||||
|
||||
return None
|
||||
|
||||
def _prev_partitions(self) -> str | None:
|
||||
partitions: list[PartitionModification] | None = self._item_group.find_by_key('partitions').value
|
||||
|
||||
if partitions:
|
||||
def _prev_partitions(self, item: MenuItem) -> str | None:
|
||||
if item.value:
|
||||
output = tr('Partitions to be encrypted') + '\n'
|
||||
output += FormattedOutput.as_table(partitions)
|
||||
output += FormattedOutput.as_table(item.value)
|
||||
return output.rstrip()
|
||||
|
||||
return None
|
||||
|
||||
def _prev_lvm_vols(self) -> str | None:
|
||||
volumes: list[PartitionModification] | None = self._item_group.find_by_key('lvm_volumes').value
|
||||
|
||||
if volumes:
|
||||
def _prev_lvm_vols(self, item: MenuItem) -> str | None:
|
||||
if item.value:
|
||||
output = tr('LVM volumes to be encrypted') + '\n'
|
||||
output += FormattedOutput.as_table(volumes)
|
||||
output += FormattedOutput.as_table(item.value)
|
||||
return output.rstrip()
|
||||
|
||||
return None
|
||||
|
||||
def _prev_hsm(self) -> str | None:
|
||||
fido_device: Fido2Device | None = self._item_group.find_by_key('hsm_device').value
|
||||
|
||||
if not fido_device:
|
||||
def _prev_hsm(self, item: MenuItem) -> str | None:
|
||||
if not item.value:
|
||||
return None
|
||||
|
||||
fido_device: Fido2Device = item.value
|
||||
|
||||
output = str(fido_device.path)
|
||||
output += f' ({fido_device.manufacturer}, {fido_device.product})'
|
||||
return f'{tr("HSM device")}: {output}'
|
||||
|
||||
def _prev_iter_time(self) -> str | None:
|
||||
iter_time = self._item_group.find_by_key('iter_time').value
|
||||
enc_type = self._item_group.find_by_key('encryption_type').value
|
||||
def _prev_iter_time(self, item: MenuItem) -> str | None:
|
||||
if item.value:
|
||||
iter_time = item.value
|
||||
enc_type = self._item_group.find_by_key('encryption_type').value
|
||||
|
||||
if iter_time and enc_type != EncryptionType.NoEncryption:
|
||||
return f'{tr("Iteration time")}: {iter_time}ms'
|
||||
if iter_time and enc_type != EncryptionType.NoEncryption:
|
||||
return f'{tr("Iteration time")}: {iter_time}ms'
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def select_encryption_type(
|
||||
device_modifications: list[DeviceModification],
|
||||
lvm_config: LvmConfiguration | None = None,
|
||||
preset: EncryptionType | None = None,
|
||||
) -> EncryptionType | None:
|
||||
|
|
@ -258,13 +253,12 @@ def select_encryption_type(
|
|||
group = MenuItemGroup(items)
|
||||
group.set_focus_by_value(preset_value)
|
||||
|
||||
result = SelectMenu[EncryptionType](
|
||||
result = Selection[EncryptionType](
|
||||
group,
|
||||
header=tr('Select encryption type'),
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Encryption type')),
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Reset:
|
||||
|
|
@ -278,7 +272,6 @@ def select_encryption_type(
|
|||
def select_encrypted_password() -> Password | None:
|
||||
header = tr('Enter disk encryption password (leave blank for no encryption)') + '\n'
|
||||
password = get_password(
|
||||
text=tr('Disk encryption password'),
|
||||
header=header,
|
||||
allow_skip=True,
|
||||
)
|
||||
|
|
@ -297,12 +290,11 @@ def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
|
|||
if fido_devices:
|
||||
group = MenuHelper(data=fido_devices).create_menu_group()
|
||||
|
||||
result = SelectMenu[Fido2Device](
|
||||
result = Selection[Fido2Device](
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Reset:
|
||||
|
|
@ -329,15 +321,15 @@ def select_partitions_to_encrypt(
|
|||
avail_partitions = [p for p in partitions if not p.exists()]
|
||||
|
||||
if avail_partitions:
|
||||
group = MenuHelper(data=avail_partitions).create_menu_group()
|
||||
group = MenuItemGroup.from_objects(partitions)
|
||||
group.set_selected_by_value(preset)
|
||||
|
||||
result = SelectMenu[PartitionModification](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
multi=True,
|
||||
result = Table[PartitionModification](
|
||||
header=tr('Select disks for the installation'),
|
||||
group=group,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
multi=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Reset:
|
||||
|
|
@ -358,13 +350,15 @@ def select_lvm_vols_to_encrypt(
|
|||
volumes: list[LvmVolume] = lvm_config.get_all_volumes()
|
||||
|
||||
if volumes:
|
||||
group = MenuHelper(data=volumes).create_menu_group()
|
||||
group = MenuItemGroup.from_objects(volumes)
|
||||
group.set_selected_by_value(preset)
|
||||
|
||||
result = SelectMenu[LvmVolume](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
result = Table[LvmVolume](
|
||||
header=tr('Select disks for the installation'),
|
||||
group=group,
|
||||
allow_skip=True,
|
||||
multi=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Reset:
|
||||
|
|
@ -383,10 +377,7 @@ def select_iteration_time(preset: int | None = None) -> int | None:
|
|||
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'
|
||||
|
||||
def validate_iter_time(value: str | None) -> str | None:
|
||||
if not value:
|
||||
return None
|
||||
|
||||
def validate_iter_time(value: str) -> str | None:
|
||||
try:
|
||||
iter_time = int(value)
|
||||
if iter_time < 100:
|
||||
|
|
@ -397,21 +388,19 @@ def select_iteration_time(preset: int | None = None) -> int | None:
|
|||
except ValueError:
|
||||
return tr('Please enter a valid number')
|
||||
|
||||
result = EditMenu(
|
||||
tr('Iteration time'),
|
||||
result = Input(
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
default_text=str(preset) if preset else str(DEFAULT_ITER_TIME),
|
||||
validator=validate_iter_time,
|
||||
).input()
|
||||
default_value=str(preset) if preset else str(DEFAULT_ITER_TIME),
|
||||
validator_callback=validate_iter_time,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
if not result.text():
|
||||
if not result.get_value():
|
||||
return preset
|
||||
return int(result.text())
|
||||
return int(result.get_value())
|
||||
case ResultType.Reset:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import time
|
|||
from pathlib import Path
|
||||
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import Tui
|
||||
|
||||
from ..interactions.general_conf import ask_abort
|
||||
from ..luks import Luks2
|
||||
|
|
@ -328,15 +327,14 @@ class FilesystemHandler:
|
|||
# Issue a final warning before we continue with something un-revertable.
|
||||
# We count down from 5 to 0.
|
||||
out = tr('Starting device modifications in ')
|
||||
Tui.print(out, row=0, endl='', clear_screen=True)
|
||||
print(out, end='', flush=True)
|
||||
|
||||
try:
|
||||
countdown = '\n5...4...3...2...1\n'
|
||||
for c in countdown:
|
||||
Tui.print(c, row=0, endl='')
|
||||
print(c, end='', flush=True)
|
||||
time.sleep(0.25)
|
||||
except KeyboardInterrupt:
|
||||
with Tui():
|
||||
ask_abort()
|
||||
ask_abort()
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import re
|
|||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
from archinstall.lib.menu.helpers import Confirmation, Input, Selection
|
||||
from archinstall.lib.models.device import (
|
||||
BtrfsMountOption,
|
||||
DeviceModification,
|
||||
|
|
@ -16,10 +17,8 @@ from archinstall.lib.models.device import (
|
|||
Unit,
|
||||
)
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import EditMenu, SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..menu.list_manager import ListManager
|
||||
from ..output import FormattedOutput
|
||||
|
|
@ -236,7 +235,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
|||
# was marked as formatting, otherwise we run into issues where
|
||||
# 1. select a new fs -> potentially mark as wipe now
|
||||
# 2. Switch back to old filesystem -> should unmark wipe now, but
|
||||
# how do we know it was the original one?
|
||||
# how do we know it was the original one?
|
||||
not_filter += [
|
||||
self._actions['set_filesystem'],
|
||||
self._actions['mark_bootable'],
|
||||
|
|
@ -402,10 +401,10 @@ class PartitioningList(ListManager[DiskSegment]):
|
|||
partition.mountpoint = None
|
||||
|
||||
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'
|
||||
prompt = tr('Mountpoint')
|
||||
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(prompt, header, validate=False, allow_skip=False)
|
||||
mountpoint = prompt_dir(header, validate=False, allow_skip=False)
|
||||
assert mountpoint
|
||||
|
||||
return mountpoint
|
||||
|
|
@ -415,13 +414,11 @@ class PartitioningList(ListManager[DiskSegment]):
|
|||
items = [MenuItem(fs.value, value=fs) for fs in fs_types]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
|
||||
result = SelectMenu[FilesystemType](
|
||||
result = Selection[FilesystemType](
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Filesystem')),
|
||||
allow_skip=False,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
@ -483,18 +480,16 @@ class PartitioningList(ListManager[DiskSegment]):
|
|||
|
||||
prompt += tr('Size: {} / {}').format(max_sectors, max_bytes) + '\n\n'
|
||||
prompt += tr('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...') + '\n'
|
||||
prompt += tr('If no unit is provided, the value is interpreted as sectors') + '\n'
|
||||
prompt += tr('If no unit is provided, the value is interpreted as sectors') + '\n\n'
|
||||
|
||||
max_size = free_space.length
|
||||
prompt += tr('Enter a size (default: {}): ').format(max_size.format_highest())
|
||||
|
||||
title = tr('Size (default: {}): ').format(max_size.format_highest())
|
||||
|
||||
result = EditMenu(
|
||||
title,
|
||||
result = Input(
|
||||
header=f'{prompt}\b',
|
||||
allow_skip=True,
|
||||
validator=validate,
|
||||
).input()
|
||||
validator_callback=validate,
|
||||
).show()
|
||||
|
||||
size: Size | None = None
|
||||
|
||||
|
|
@ -502,12 +497,14 @@ class PartitioningList(ListManager[DiskSegment]):
|
|||
case ResultType.Skip:
|
||||
size = max_size
|
||||
case ResultType.Selection:
|
||||
value = result.text()
|
||||
value = result.get_value()
|
||||
|
||||
if value:
|
||||
size = self._validate_value(sector_size, max_size, value)
|
||||
else:
|
||||
size = max_size
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
assert size
|
||||
return size
|
||||
|
|
@ -544,15 +541,11 @@ class PartitioningList(ListManager[DiskSegment]):
|
|||
def _reset_confirmation(self) -> bool:
|
||||
prompt = tr('This will remove all newly added partitions, continue?') + '\n'
|
||||
|
||||
result = SelectMenu[bool](
|
||||
MenuItemGroup.yes_no(),
|
||||
result = Confirmation(
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
columns=2,
|
||||
reset_warning_msg=prompt,
|
||||
allow_skip=False,
|
||||
).run()
|
||||
allow_reset=False,
|
||||
).show()
|
||||
|
||||
return result.item() == MenuItem.yes()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from pathlib import Path
|
||||
from typing import assert_never, override
|
||||
|
||||
from archinstall.lib.menu.helpers import Input
|
||||
from archinstall.lib.models.device import SubvolumeModification
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import EditMenu
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..menu.list_manager import ListManager
|
||||
from ..utils.util import prompt_dir
|
||||
|
|
@ -40,28 +39,27 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
|
|||
return None
|
||||
return tr('Value cannot be empty')
|
||||
|
||||
result = EditMenu(
|
||||
tr('Subvolume name'),
|
||||
alignment=Alignment.CENTER,
|
||||
result = Input(
|
||||
header=tr('Enter subvolume name'),
|
||||
allow_skip=True,
|
||||
default_text=str(preset.name) if preset else None,
|
||||
validator=validate,
|
||||
).input()
|
||||
default_value=str(preset.name) if preset else None,
|
||||
validator_callback=validate,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
name = result.text()
|
||||
name = result.get_value()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
case _:
|
||||
assert_never(result.type_)
|
||||
|
||||
header = f'{tr("Subvolume name")}: {name}\n'
|
||||
header = f'{tr("Subvolume name")}: {name}\n\n'
|
||||
header += tr('Enter subvolume mountpoint')
|
||||
|
||||
path = prompt_dir(
|
||||
tr('Subvolume mountpoint'),
|
||||
header=header,
|
||||
allow_skip=True,
|
||||
validate=True,
|
||||
|
|
@ -80,7 +78,7 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
|
|||
entry: SubvolumeModification | None,
|
||||
data: list[SubvolumeModification],
|
||||
) -> list[SubvolumeModification]:
|
||||
if action == self._actions[0]: # add
|
||||
if action == self._actions[0]:
|
||||
new_subvolume = self._add_subvolume()
|
||||
|
||||
if new_subvolume is not None:
|
||||
|
|
@ -88,15 +86,15 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
|
|||
# was created we'll replace the existing one
|
||||
data = [d for d in data if d.name != new_subvolume.name]
|
||||
data += [new_subvolume]
|
||||
elif entry is not None: # edit
|
||||
if action == self._actions[1]: # edit subvolume
|
||||
elif entry is not None:
|
||||
if action == self._actions[1]:
|
||||
new_subvolume = self._add_subvolume(entry)
|
||||
|
||||
if new_subvolume is not None:
|
||||
# we'll remove the original subvolume and add the modified version
|
||||
data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name]
|
||||
data += [new_subvolume]
|
||||
elif action == self._actions[2]: # delete
|
||||
elif action == self._actions[2]:
|
||||
data = [d for d in data if d != entry]
|
||||
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -287,9 +287,9 @@ class SysCommandWorker:
|
|||
os.chdir(str(self.working_directory))
|
||||
|
||||
# Note: If for any reason, we get a Python exception between here
|
||||
# and until os.close(), the traceback will get locked inside
|
||||
# stdout of the child_fd object. `os.read(self.child_fd, 8192)` is the
|
||||
# only way to get the traceback without losing it.
|
||||
# and until os.close(), the traceback will get locked inside
|
||||
# stdout of the child_fd object. `os.read(self.child_fd, 8192)` is the
|
||||
# only way to get the traceback without losing it.
|
||||
|
||||
self.pid, self.child_fd = pty.fork()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from archinstall.lib.models.application import ApplicationConfiguration, ZramCon
|
|||
from archinstall.lib.models.authentication import AuthenticationConfiguration
|
||||
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, FilesystemType, PartitionModification
|
||||
from archinstall.lib.packages import list_available_packages
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
|
||||
from .applications.application_menu import ApplicationMenu
|
||||
from .args import ArchConfig
|
||||
|
|
@ -38,7 +38,7 @@ from .translationhandler import Language, tr, translation_handler
|
|||
|
||||
|
||||
class GlobalMenu(AbstractMenu[None]):
|
||||
def __init__(self, arch_config: ArchConfig) -> None:
|
||||
def __init__(self, arch_config: ArchConfig, title: str | None = None) -> None:
|
||||
self._arch_config = arch_config
|
||||
menu_options = self._get_menu_options()
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
checkmarks=True,
|
||||
)
|
||||
|
||||
super().__init__(self._item_group, config=arch_config)
|
||||
super().__init__(self._item_group, config=arch_config, title=title)
|
||||
|
||||
def _get_menu_options(self) -> list[MenuItem]:
|
||||
menu_options = [
|
||||
|
|
@ -60,6 +60,7 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
),
|
||||
MenuItem(
|
||||
text=tr('Locales'),
|
||||
value=LocaleConfiguration.default(),
|
||||
action=self._locale_selection,
|
||||
preview_action=self._prev_locale,
|
||||
key='locale_config',
|
||||
|
|
@ -135,7 +136,7 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
MenuItem(
|
||||
text=tr('Parallel Downloads'),
|
||||
action=add_number_of_parallel_downloads,
|
||||
value=0,
|
||||
value=1,
|
||||
preview_action=self._prev_parallel_dw,
|
||||
key='parallel_downloads',
|
||||
),
|
||||
|
|
@ -162,6 +163,7 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
),
|
||||
MenuItem(
|
||||
text='',
|
||||
read_only=True,
|
||||
),
|
||||
MenuItem(
|
||||
text=tr('Save configuration'),
|
||||
|
|
@ -185,8 +187,8 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
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
|
||||
# if item.key is not None:
|
||||
# data[item.key] = item.value
|
||||
|
||||
self.sync_all_to_config()
|
||||
save_config(self._arch_config)
|
||||
|
|
@ -547,10 +549,10 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
|
||||
return packages
|
||||
|
||||
def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration:
|
||||
def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration | None:
|
||||
mirror_configuration = MirrorMenu(preset=preset).run()
|
||||
|
||||
if mirror_configuration.optional_repositories:
|
||||
if mirror_configuration and mirror_configuration.optional_repositories:
|
||||
# reset the package list cache in case the repository selection has changed
|
||||
list_available_packages.cache_clear()
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ from archinstall.lib.models.device import (
|
|||
from archinstall.lib.models.packages import Repository
|
||||
from archinstall.lib.packages import installed_package
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import Tui
|
||||
|
||||
from .args import arch_config_handler
|
||||
from .boot import Boot
|
||||
|
|
@ -132,8 +131,8 @@ class Installer:
|
|||
|
||||
# We avoid printing /mnt/<log path> because that might confuse people if they note it down
|
||||
# and then reboot, and an identical log file will be found in the ISO medium anyway.
|
||||
Tui.print(str(tr('[!] A log file has been created here: {}').format(logger.path)))
|
||||
Tui.print(tr('Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues'))
|
||||
print(tr('[!] A log file has been created here: {}').format(logger.path))
|
||||
print(tr('Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues'))
|
||||
|
||||
# Return None to propagate the exception
|
||||
return None
|
||||
|
|
@ -206,7 +205,7 @@ class Installer:
|
|||
|
||||
# info('Waiting for pacman-init.service to complete.')
|
||||
# while self._service_state('pacman-init') not in ('dead', 'failed', 'exited'):
|
||||
# time.sleep(1)
|
||||
# time.sleep(1)
|
||||
|
||||
if not arch_config_handler.args.skip_wkd:
|
||||
info(tr('Waiting for Arch Linux keyring sync (archlinux-keyring-wkd-sync) to complete.'))
|
||||
|
|
@ -1100,7 +1099,7 @@ class Installer:
|
|||
|
||||
if root_partition in self._disk_encryption.partitions:
|
||||
# TODO: We need to detect if the encrypted device is a whole disk encryption,
|
||||
# or simply a partition encryption. Right now we assume it's a partition (and we always have)
|
||||
# or simply a partition encryption. Right now we assume it's a partition (and we always have)
|
||||
|
||||
if self._disk_encryption.hsm_device:
|
||||
debug(f'Root partition is an encrypted device, identifying by UUID: {root_partition.uuid}')
|
||||
|
|
@ -1216,9 +1215,9 @@ class Installer:
|
|||
f"""\
|
||||
# Created by: archinstall
|
||||
# Created on: {self.init_time}
|
||||
title Arch Linux ({{kernel}})
|
||||
linux /vmlinuz-{{kernel}}
|
||||
initrd /initramfs-{{kernel}}.img
|
||||
title Arch Linux ({{kernel}})
|
||||
linux /vmlinuz-{{kernel}}
|
||||
initrd /initramfs-{{kernel}}.img
|
||||
options {' '.join(self._get_kernel_params(root))}
|
||||
""",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from pathlib import Path
|
|||
from archinstall.lib.args import arch_config_handler
|
||||
from archinstall.lib.disk.device_handler import device_handler
|
||||
from archinstall.lib.disk.partitioning_menu import manual_partitioning
|
||||
from archinstall.lib.menu.menu_helper import MenuHelper
|
||||
from archinstall.lib.menu.helpers import Confirmation, Notify, Selection, Table
|
||||
from archinstall.lib.models.device import (
|
||||
BDevice,
|
||||
BtrfsMountOption,
|
||||
|
|
@ -28,10 +28,8 @@ from archinstall.lib.models.device import (
|
|||
)
|
||||
from archinstall.lib.output import debug
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..output import FormattedOutput
|
||||
from ..utils.util import prompt_dir
|
||||
|
|
@ -39,7 +37,7 @@ from ..utils.util import prompt_dir
|
|||
|
||||
def select_devices(preset: list[BDevice] | None = []) -> list[BDevice]:
|
||||
def _preview_device_selection(item: MenuItem) -> str | None:
|
||||
device = item.get_value()
|
||||
device: _DeviceInfo = item.value # type: ignore[assignment]
|
||||
dev = device_handler.get_device(device.path)
|
||||
|
||||
if dev and dev.partition_infos:
|
||||
|
|
@ -50,23 +48,32 @@ def select_devices(preset: list[BDevice] | None = []) -> list[BDevice]:
|
|||
preset = []
|
||||
|
||||
devices = device_handler.devices
|
||||
options = [d.device_info for d in devices]
|
||||
|
||||
items = [
|
||||
MenuItem(
|
||||
str(d.device_info.path),
|
||||
d.device_info,
|
||||
preview_action=_preview_device_selection,
|
||||
)
|
||||
for d in devices
|
||||
]
|
||||
|
||||
presets = [p.device_info for p in preset]
|
||||
|
||||
group = MenuHelper(options).create_menu_group()
|
||||
group = MenuItemGroup(items)
|
||||
group.set_selected_by_value(presets)
|
||||
group.set_preview_for_all(_preview_device_selection)
|
||||
|
||||
result = SelectMenu[_DeviceInfo](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
search_enabled=False,
|
||||
multi=True,
|
||||
preview_style=PreviewStyle.BOTTOM,
|
||||
preview_size='auto',
|
||||
preview_frame=FrameProperties.max('Partitions'),
|
||||
result = Table[_DeviceInfo](
|
||||
header=tr('Select disks for the installation'),
|
||||
group=group,
|
||||
presets=presets,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
multi=True,
|
||||
preview_location='bottom',
|
||||
preview_header=tr('Partitions'),
|
||||
).show()
|
||||
|
||||
debug(f'Result: {result}')
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Reset:
|
||||
|
|
@ -81,6 +88,7 @@ def select_devices(preset: list[BDevice] | None = []) -> list[BDevice]:
|
|||
if device.device_info in selected_device_info:
|
||||
selected_devices.append(device)
|
||||
|
||||
debug(f'Selected devices: {selected_device_info}')
|
||||
return selected_devices
|
||||
|
||||
|
||||
|
|
@ -132,13 +140,12 @@ def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLay
|
|||
if preset:
|
||||
group.set_selected_by_value(preset.config_type.display_msg())
|
||||
|
||||
result = SelectMenu[str](
|
||||
result = Selection[str](
|
||||
group,
|
||||
header=tr('Select a disk configuration'),
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Disk configuration type')),
|
||||
allow_reset=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -149,10 +156,11 @@ def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLay
|
|||
selection = result.get_value()
|
||||
|
||||
if selection == pre_mount_mode:
|
||||
output = 'You will use whatever drive-setup is mounted at the specified directory\n'
|
||||
output += "WARNING: Archinstall won't check the suitability of this setup\n"
|
||||
output = tr('Enter root mount directory') + '\n\n'
|
||||
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(tr('Root mount directory'), output, allow_skip=True)
|
||||
path = prompt_dir(output, allow_skip=True)
|
||||
|
||||
if path is None:
|
||||
return None
|
||||
|
|
@ -171,6 +179,9 @@ def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLay
|
|||
if not devices:
|
||||
return None
|
||||
|
||||
if devices == preset_devices:
|
||||
return preset
|
||||
|
||||
if result.get_value() == default_layout:
|
||||
modifications = get_default_partition_layout(devices)
|
||||
if modifications:
|
||||
|
|
@ -202,13 +213,11 @@ def select_lvm_config(
|
|||
group = MenuItemGroup(items)
|
||||
group.set_focus_by_value(preset_value)
|
||||
|
||||
result = SelectMenu[str](
|
||||
result = Selection[str](
|
||||
group,
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
frame=FrameProperties.min(tr('LVM configuration type')),
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -253,12 +262,11 @@ def select_main_filesystem_format() -> FilesystemType:
|
|||
items.append(MenuItem('ntfs', value=FilesystemType.Ntfs))
|
||||
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
result = SelectMenu[FilesystemType](
|
||||
result = Selection[FilesystemType](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min('Filesystem'),
|
||||
header=tr('Select main filesystem'),
|
||||
allow_skip=False,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
@ -277,15 +285,12 @@ def select_mount_options() -> list[str]:
|
|||
MenuItem(disable_cow, value=BtrfsMountOption.nodatacow.value),
|
||||
]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
result = SelectMenu[str](
|
||||
|
||||
result = Selection[str](
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
search_enabled=False,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -338,16 +343,12 @@ def suggest_single_disk_layout(
|
|||
|
||||
if filesystem_type == FilesystemType.Btrfs:
|
||||
prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n'
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(MenuItem.yes().value)
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
|
||||
result = Confirmation(
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
allow_skip=False,
|
||||
).run()
|
||||
preset=True,
|
||||
).show()
|
||||
|
||||
using_subvolumes = result.item() == MenuItem.yes()
|
||||
mount_options = select_mount_options()
|
||||
|
|
@ -375,16 +376,12 @@ def suggest_single_disk_layout(
|
|||
using_home_partition = True
|
||||
else:
|
||||
prompt = tr('Would you like to create a separate partition for /home?') + '\n'
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(MenuItem.yes().value)
|
||||
result = SelectMenu(
|
||||
group,
|
||||
|
||||
result = Confirmation(
|
||||
header=prompt,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
columns=2,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=False,
|
||||
).run()
|
||||
preset=True,
|
||||
).show()
|
||||
|
||||
using_home_partition = result.item() == MenuItem.yes()
|
||||
|
||||
|
|
@ -474,10 +471,7 @@ 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))
|
||||
|
||||
items = [MenuItem(tr('Continue'))]
|
||||
group = MenuItemGroup(items)
|
||||
SelectMenu(group).run()
|
||||
|
||||
Notify(text).show()
|
||||
return []
|
||||
|
||||
if filesystem_type == FilesystemType.Btrfs:
|
||||
|
|
@ -565,18 +559,7 @@ def suggest_lvm_layout(
|
|||
|
||||
if filesystem_type == FilesystemType.Btrfs:
|
||||
prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n'
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_focus_by_value(MenuItem.yes().value)
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
header=prompt,
|
||||
search_enabled=False,
|
||||
allow_skip=False,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
columns=2,
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
result = Confirmation(header=prompt, allow_skip=False, preset=True).show()
|
||||
|
||||
using_subvolumes = MenuItem.yes() == result.item()
|
||||
mount_options = select_mount_options()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import assert_never
|
||||
|
||||
from archinstall.lib.menu.helpers import Confirmation, Input, Loading, Notify, Selection
|
||||
from archinstall.lib.models.packages import Repository
|
||||
from archinstall.lib.packages.packages import list_available_packages
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import EditMenu, SelectMenu, Tui
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..locale.utils import list_timezones
|
||||
from ..models.packages import AvailablePackage, PackageGroup
|
||||
from ..output import warn
|
||||
from ..output import debug, warn
|
||||
from ..translationhandler import Language
|
||||
|
||||
|
||||
|
|
@ -32,18 +30,11 @@ def ask_ntp(preset: bool = True) -> bool:
|
|||
+ '\n'
|
||||
)
|
||||
|
||||
preset_val = MenuItem.yes() if preset else MenuItem.no()
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = preset_val
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
result = Confirmation(
|
||||
header=header,
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
).run()
|
||||
preset=preset,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -55,18 +46,17 @@ def ask_ntp(preset: bool = True) -> bool:
|
|||
|
||||
|
||||
def ask_hostname(preset: str | None = None) -> str | None:
|
||||
result = EditMenu(
|
||||
tr('Hostname'),
|
||||
alignment=Alignment.CENTER,
|
||||
result = Input(
|
||||
header=tr('Enter a hostname'),
|
||||
allow_skip=True,
|
||||
default_text=preset,
|
||||
).input()
|
||||
default_value=preset,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
hostname = result.text()
|
||||
hostname = result.get_value()
|
||||
if len(hostname) < 1:
|
||||
return None
|
||||
return hostname
|
||||
|
|
@ -83,13 +73,13 @@ def ask_for_a_timezone(preset: str | None = None) -> str | None:
|
|||
group.set_selected_by_value(preset)
|
||||
group.set_default_by_value(default)
|
||||
|
||||
result = SelectMenu[str](
|
||||
result = Selection[str](
|
||||
group,
|
||||
header=tr('Select timezone'),
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
frame=FrameProperties.min(tr('Timezone')),
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
enable_filter=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -126,14 +116,12 @@ 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 = SelectMenu[Language](
|
||||
group,
|
||||
result = Selection[Language](
|
||||
header=title,
|
||||
allow_skip=True,
|
||||
group=group,
|
||||
allow_reset=False,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(header=tr('Select language')),
|
||||
).run()
|
||||
allow_skip=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -152,11 +140,23 @@ def ask_additional_packages_to_install(
|
|||
|
||||
respos_text = ', '.join(r.value for r in repositories)
|
||||
output = tr('Repositories: {}').format(respos_text) + '\n'
|
||||
|
||||
output += tr('Loading packages...')
|
||||
Tui.print(output, clear_screen=True)
|
||||
|
||||
packages = list_available_packages(tuple(repositories))
|
||||
result = Loading[dict[str, AvailablePackage]](
|
||||
header=output,
|
||||
data_callback=lambda: list_available_packages(tuple(repositories)),
|
||||
).show()
|
||||
|
||||
if result.type_ != ResultType.Selection:
|
||||
debug('Error while loading packages')
|
||||
return preset
|
||||
|
||||
packages = result.get_value()
|
||||
|
||||
if not packages:
|
||||
Notify(tr('No packages found')).show()
|
||||
return []
|
||||
|
||||
package_groups = PackageGroup.from_available_packages(packages)
|
||||
|
||||
# Additional packages (with some light weight error handling for invalid package names)
|
||||
|
|
@ -176,7 +176,7 @@ def ask_additional_packages_to_install(
|
|||
MenuItem(
|
||||
name,
|
||||
value=pkg,
|
||||
preview_action=lambda x: x.value.info(),
|
||||
preview_action=lambda x: x.value.info() if x.value else None,
|
||||
)
|
||||
for name, pkg in packages.items()
|
||||
]
|
||||
|
|
@ -185,7 +185,7 @@ def ask_additional_packages_to_install(
|
|||
MenuItem(
|
||||
name,
|
||||
value=group,
|
||||
preview_action=lambda x: x.value.info(),
|
||||
preview_action=lambda x: x.value.info() if x.value else None,
|
||||
)
|
||||
for name, group in package_groups.items()
|
||||
]
|
||||
|
|
@ -193,65 +193,61 @@ def ask_additional_packages_to_install(
|
|||
menu_group = MenuItemGroup(items, sort_items=True)
|
||||
menu_group.set_selected_by_value(preset_packages)
|
||||
|
||||
result = SelectMenu[AvailablePackage | PackageGroup](
|
||||
pck_result = Selection[AvailablePackage | PackageGroup](
|
||||
menu_group,
|
||||
header=header,
|
||||
alignment=Alignment.LEFT,
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
multi=True,
|
||||
preview_frame=FrameProperties.max('Package info'),
|
||||
preview_style=PreviewStyle.RIGHT,
|
||||
preview_size='auto',
|
||||
).run()
|
||||
preview_location='right',
|
||||
enable_filter=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
match pck_result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return []
|
||||
case ResultType.Selection:
|
||||
selected_pacakges = result.get_values()
|
||||
selected_pacakges = pck_result.get_values()
|
||||
return [pkg.name for pkg in selected_pacakges]
|
||||
|
||||
|
||||
def add_number_of_parallel_downloads(preset: int | None = None) -> int | None:
|
||||
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'
|
||||
header += tr('Enter the number of parallel downloads to be enabled.\n\nNote:\n')
|
||||
header += tr(' - Maximum recommended value : {} ( Allows {} parallel downloads at a time )').format(max_recommended, max_recommended) + '\n'
|
||||
header += tr(' - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n')
|
||||
header += tr(' - Maximum recommended value : {} ( Allows {} parallel downloads at a time )').format(max_recommended, max_recommended) + '\n\n'
|
||||
header += tr('Enter the number of parallel downloads to be enabled')
|
||||
|
||||
def validator(s: str | None) -> str | None:
|
||||
if s is not None:
|
||||
try:
|
||||
value = int(s)
|
||||
if value >= 0:
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
def validator(s: str) -> str | None:
|
||||
try:
|
||||
value = int(s)
|
||||
|
||||
return tr('Invalid download number')
|
||||
if 1 <= value <= max_recommended:
|
||||
return None
|
||||
|
||||
result = EditMenu(
|
||||
tr('Number downloads'),
|
||||
return tr('Value must be between 1 and {}').format(max_recommended)
|
||||
except Exception:
|
||||
return tr('Please enter a valid number')
|
||||
|
||||
result = Input(
|
||||
header=header,
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
validator=validator,
|
||||
default_text=str(preset) if preset is not None else None,
|
||||
).input()
|
||||
validator_callback=validator,
|
||||
default_value=str(preset),
|
||||
).show()
|
||||
|
||||
downloads = 1
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Reset:
|
||||
return 0
|
||||
return downloads
|
||||
case ResultType.Selection:
|
||||
downloads: int = int(result.text())
|
||||
case _:
|
||||
assert_never(result.type_)
|
||||
downloads = int(result.get_value())
|
||||
|
||||
pacman_conf_path = Path('/etc/pacman.conf')
|
||||
with pacman_conf_path.open() as f:
|
||||
|
|
@ -278,12 +274,11 @@ def ask_post_installation(elapsed_time: float | None = None) -> PostInstallation
|
|||
items = [MenuItem(action.value, value=action) for action in PostInstallationAction]
|
||||
group = MenuItemGroup(items)
|
||||
|
||||
result = SelectMenu[PostInstallationAction](
|
||||
result = Selection[PostInstallationAction](
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
@ -294,16 +289,12 @@ def ask_post_installation(elapsed_time: float | None = None) -> PostInstallation
|
|||
|
||||
def ask_abort() -> None:
|
||||
prompt = tr('Do you really want to abort?') + '\n'
|
||||
group = MenuItemGroup.yes_no()
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
result = Confirmation(
|
||||
header=prompt,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
).run()
|
||||
preset=False,
|
||||
).show()
|
||||
|
||||
if result.item() == MenuItem.yes():
|
||||
if result.get_value():
|
||||
sys.exit(0)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import re
|
||||
from typing import override
|
||||
|
||||
from archinstall.lib.menu.helpers import Confirmation, Input
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import EditMenu, SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, Orientation
|
||||
from archinstall.tui.ui.menu_item import MenuItem
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..menu.list_manager import ListManager
|
||||
from ..models.users import User
|
||||
|
|
@ -43,7 +42,8 @@ class UserList(ListManager[User]):
|
|||
data += [new_user]
|
||||
elif action == self._actions[1] and entry: # change password
|
||||
header = f'{tr("User")}: {entry.username}\n'
|
||||
new_password = get_password(tr('Password'), header=header)
|
||||
header += tr('Enter new password')
|
||||
new_password = get_password(header=header)
|
||||
|
||||
if new_password:
|
||||
user = next(filter(lambda x: x == entry, data))
|
||||
|
|
@ -63,17 +63,17 @@ class UserList(ListManager[User]):
|
|||
return tr('The username you entered is invalid')
|
||||
|
||||
def _add_user(self) -> User | None:
|
||||
editResult = EditMenu(
|
||||
tr('Username'),
|
||||
editResult = Input(
|
||||
tr('Enter a username'),
|
||||
allow_skip=True,
|
||||
validator=self._check_for_correct_username,
|
||||
).input()
|
||||
validator_callback=self._check_for_correct_username,
|
||||
).show()
|
||||
|
||||
match editResult.type_:
|
||||
case ResultType.Skip:
|
||||
return None
|
||||
case ResultType.Selection:
|
||||
username = editResult.text()
|
||||
username = editResult.get_value()
|
||||
case _:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
|
@ -81,27 +81,21 @@ class UserList(ListManager[User]):
|
|||
return None
|
||||
|
||||
header = f'{tr("Username")}: {username}\n'
|
||||
prompt = f'{header}\n' + tr('Enter a password')
|
||||
|
||||
password = get_password(tr('Password'), header=header, allow_skip=True)
|
||||
password = get_password(header=prompt, allow_skip=True)
|
||||
|
||||
if not password:
|
||||
return None
|
||||
|
||||
header += f'{tr("Password")}: {password.hidden()}\n\n'
|
||||
header += str(tr('Should "{}" be a superuser (sudo)?\n')).format(username)
|
||||
header += f'{tr("Password")}: {password.hidden()}\n'
|
||||
prompt = f'{header}\n' + tr('Should "{}" be a superuser (sudo)?\n').format(username)
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.yes()
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
search_enabled=False,
|
||||
result = Confirmation(
|
||||
header=prompt,
|
||||
allow_skip=False,
|
||||
).run()
|
||||
preset=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import ipaddress
|
||||
from typing import assert_never, override
|
||||
|
||||
from archinstall.lib.menu.helpers import Input, Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import EditMenu, SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..menu.list_manager import ListManager
|
||||
from ..models.network import NetworkConfiguration, Nic, NicType
|
||||
|
|
@ -62,12 +61,11 @@ class ManualNetworkConfig(ListManager[Nic]):
|
|||
items = [MenuItem(i, value=i) for i in available]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
|
||||
result = SelectMenu[str](
|
||||
result = Selection[str](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Interfaces')),
|
||||
header=tr('Select an interface'),
|
||||
allow_skip=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -77,18 +75,13 @@ class ManualNetworkConfig(ListManager[Nic]):
|
|||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
def _get_ip_address(
|
||||
self,
|
||||
title: str,
|
||||
header: str,
|
||||
allow_skip: bool,
|
||||
multi: bool,
|
||||
preset: str | None = None,
|
||||
) -> str | None:
|
||||
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')
|
||||
|
||||
if not ip:
|
||||
if allow_empty:
|
||||
return None
|
||||
return failure
|
||||
|
||||
if multi:
|
||||
|
|
@ -103,19 +96,18 @@ class ManualNetworkConfig(ListManager[Nic]):
|
|||
except ValueError:
|
||||
return failure
|
||||
|
||||
result = EditMenu(
|
||||
title,
|
||||
result = Input(
|
||||
header=header,
|
||||
validator=validator,
|
||||
validator_callback=validator,
|
||||
allow_skip=allow_skip,
|
||||
default_text=preset,
|
||||
).input()
|
||||
default_value=preset,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return result.text()
|
||||
return result.get_value()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
|
@ -124,18 +116,17 @@ class ManualNetworkConfig(ListManager[Nic]):
|
|||
modes = ['DHCP (auto detect)', 'IP (static)']
|
||||
default_mode = 'DHCP (auto detect)'
|
||||
|
||||
header = tr('Select which mode to configure for "{}"').format(iface_name) + '\n'
|
||||
header = tr('Select which mode to configure for "{}"').format(iface_name)
|
||||
|
||||
items = [MenuItem(m, value=m) for m in modes]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_default_by_value(default_mode)
|
||||
|
||||
result = SelectMenu[str](
|
||||
result = Selection[str](
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Modes')),
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
@ -149,10 +140,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(tr('IP address'), header, False, False)
|
||||
ip = 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(tr('Gateway address'), header, True, False)
|
||||
gateway = self._get_ip_address(header, True, False, allow_empty=True)
|
||||
|
||||
if edit_nic.dns:
|
||||
display_dns = ' '.join(edit_nic.dns)
|
||||
|
|
@ -160,13 +151,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(
|
||||
tr('DNS servers'),
|
||||
header,
|
||||
True,
|
||||
True,
|
||||
display_dns,
|
||||
)
|
||||
dns_servers = self._get_ip_address(header, True, True, display_dns, allow_empty=True)
|
||||
|
||||
dns = []
|
||||
if dns_servers is not None:
|
||||
|
|
@ -189,13 +174,12 @@ def ask_to_configure_network(preset: NetworkConfiguration | None) -> NetworkConf
|
|||
if preset:
|
||||
group.set_selected_by_value(preset.type)
|
||||
|
||||
result = SelectMenu[NicType](
|
||||
result = Selection[NicType](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Network configuration')),
|
||||
header=tr('Choose network configuration'),
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from typing import assert_never
|
||||
|
||||
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||
from archinstall.lib.models.application import ZramAlgorithm, ZramConfiguration
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, FrameStyle, Orientation, PreviewStyle
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..hardware import GfxDriver, SysInfo
|
||||
|
||||
|
|
@ -27,14 +26,13 @@ def select_kernel(preset: list[str] = []) -> list[str]:
|
|||
group.set_focus_by_value(default_kernel)
|
||||
group.set_selected_by_value(preset)
|
||||
|
||||
result = SelectMenu[str](
|
||||
result = Selection[str](
|
||||
group,
|
||||
header=tr('Select which kernel(s) to install'),
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Kernel')),
|
||||
multi=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -45,6 +43,20 @@ def select_kernel(preset: list[str] = []) -> list[str]:
|
|||
return result.get_values()
|
||||
|
||||
|
||||
def ask_for_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()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return result.get_value()
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None:
|
||||
"""
|
||||
Somewhat convoluted function, whose job is simple.
|
||||
|
|
@ -56,7 +68,15 @@ def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None
|
|||
if not options:
|
||||
options = [driver for driver in GfxDriver]
|
||||
|
||||
items = [MenuItem(o.value, value=o, preview_action=lambda x: x.value.packages_text()) for o in options]
|
||||
items = [
|
||||
MenuItem(
|
||||
o.value,
|
||||
value=o,
|
||||
preview_action=lambda x: x.value.packages_text() if x.value else None,
|
||||
)
|
||||
for o in options
|
||||
]
|
||||
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_default_by_value(GfxDriver.AllOpenSource)
|
||||
|
||||
|
|
@ -71,15 +91,13 @@ 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 = SelectMenu[GfxDriver](
|
||||
result = Selection[GfxDriver](
|
||||
group,
|
||||
header=header,
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
preview_size='auto',
|
||||
preview_style=PreviewStyle.BOTTOM,
|
||||
preview_frame=FrameProperties(tr('Info'), h_frame_style=FrameStyle.MIN),
|
||||
).run()
|
||||
preview_location='right',
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -97,14 +115,11 @@ def ask_for_swap(preset: ZramConfiguration = ZramConfiguration(enabled=True)) ->
|
|||
group.set_default_by_value(True)
|
||||
group.set_focus_by_value(preset.enabled)
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
result = Confirmation(
|
||||
header=prompt,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
preset=preset.enabled,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -119,12 +134,11 @@ def ask_for_swap(preset: ZramConfiguration = ZramConfiguration(enabled=True)) ->
|
|||
algo_group.set_default_by_value(ZramAlgorithm.ZSTD)
|
||||
algo_group.set_focus_by_value(preset.algorithm)
|
||||
|
||||
algo_result = SelectMenu[ZramAlgorithm](
|
||||
algo_result = Selection[ZramAlgorithm](
|
||||
algo_group,
|
||||
header=tr('Select zram compression algorithm:') + '\n',
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match algo_result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
from typing import override
|
||||
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..menu.abstract_menu import AbstractSubMenu
|
||||
from ..models.locale import LocaleConfiguration
|
||||
|
|
@ -32,40 +31,33 @@ class LocaleMenu(AbstractSubMenu[LocaleConfiguration]):
|
|||
text=tr('Keyboard layout'),
|
||||
action=self._select_kb_layout,
|
||||
value=self._locale_conf.kb_layout,
|
||||
preview_action=self._prev_locale,
|
||||
preview_action=lambda item: item.get_value(),
|
||||
key='kb_layout',
|
||||
),
|
||||
MenuItem(
|
||||
text=tr('Locale language'),
|
||||
action=select_locale_lang,
|
||||
value=self._locale_conf.sys_lang,
|
||||
preview_action=self._prev_locale,
|
||||
preview_action=lambda item: item.get_value(),
|
||||
key='sys_lang',
|
||||
),
|
||||
MenuItem(
|
||||
text=tr('Locale encoding'),
|
||||
action=select_locale_enc,
|
||||
value=self._locale_conf.sys_enc,
|
||||
preview_action=self._prev_locale,
|
||||
preview_action=lambda item: item.get_value(),
|
||||
key='sys_enc',
|
||||
),
|
||||
]
|
||||
|
||||
def _prev_locale(self, item: MenuItem) -> str:
|
||||
temp_locale = LocaleConfiguration(
|
||||
self._menu_item_group.find_by_key('kb_layout').get_value(),
|
||||
self._menu_item_group.find_by_key('sys_lang').get_value(),
|
||||
self._menu_item_group.find_by_key('sys_enc').get_value(),
|
||||
)
|
||||
return temp_locale.preview()
|
||||
|
||||
@override
|
||||
def run(
|
||||
self,
|
||||
additional_title: str | None = None,
|
||||
) -> LocaleConfiguration:
|
||||
super().run(additional_title=additional_title)
|
||||
return self._locale_conf
|
||||
def run(self) -> LocaleConfiguration:
|
||||
config = super().run()
|
||||
|
||||
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)
|
||||
|
|
@ -82,12 +74,11 @@ def select_locale_lang(preset: str | None = None) -> str | None:
|
|||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
result = SelectMenu[str](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Locale language')),
|
||||
allow_skip=True,
|
||||
).run()
|
||||
result = Selection[str](
|
||||
header=tr('Locale language'),
|
||||
group=group,
|
||||
enable_filter=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
@ -106,12 +97,11 @@ def select_locale_enc(preset: str | None = None) -> str | None:
|
|||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
result = SelectMenu[str](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Locale encoding')),
|
||||
allow_skip=True,
|
||||
).run()
|
||||
result = Selection[str](
|
||||
header=tr('Locale encoding'),
|
||||
group=group,
|
||||
enable_filter=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
@ -138,12 +128,11 @@ def select_kb_layout(preset: str | None = None) -> str | None:
|
|||
group = MenuItemGroup(items, sort_items=False)
|
||||
group.set_focus_by_value(preset)
|
||||
|
||||
result = SelectMenu[str](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Keyboard layout')),
|
||||
allow_skip=True,
|
||||
).run()
|
||||
result = Selection[str](
|
||||
header=tr('Keyboard layout'),
|
||||
group=group,
|
||||
enable_filter=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
from types import TracebackType
|
||||
from typing import Any, Self
|
||||
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu, Tui
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Chars, FrameProperties, FrameStyle, PreviewStyle
|
||||
from archinstall.tui.types import Chars
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..output import error
|
||||
|
||||
|
|
@ -17,6 +17,7 @@ class AbstractMenu[ValueT]:
|
|||
self,
|
||||
item_group: MenuItemGroup,
|
||||
config: Any,
|
||||
title: str | None = None,
|
||||
auto_cursor: bool = True,
|
||||
allow_reset: bool = False,
|
||||
reset_warning: str | None = None,
|
||||
|
|
@ -26,6 +27,7 @@ class AbstractMenu[ValueT]:
|
|||
self.auto_cursor = auto_cursor
|
||||
self._allow_reset = allow_reset
|
||||
self._reset_warning = reset_warning
|
||||
self._title = title
|
||||
|
||||
self.is_context_mgr = False
|
||||
|
||||
|
|
@ -40,7 +42,7 @@ class AbstractMenu[ValueT]:
|
|||
# TODO: skip processing when it comes from a planified exit
|
||||
if exc_type is not None:
|
||||
error(str(exc_value))
|
||||
Tui.print('Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues')
|
||||
print('Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues')
|
||||
|
||||
# Return None to propagate the exception
|
||||
return None
|
||||
|
|
@ -92,37 +94,36 @@ class AbstractMenu[ValueT]:
|
|||
def _is_config_valid(self) -> bool:
|
||||
return True
|
||||
|
||||
def run(
|
||||
self,
|
||||
additional_title: str | None = None,
|
||||
) -> ValueT | None:
|
||||
def run(self) -> ValueT | None:
|
||||
self._sync_from_config()
|
||||
|
||||
while True:
|
||||
result = SelectMenu[ValueT](
|
||||
self._menu_item_group,
|
||||
result = Selection[ValueT](
|
||||
title=self._title,
|
||||
group=self._menu_item_group,
|
||||
allow_skip=False,
|
||||
allow_reset=self._allow_reset,
|
||||
reset_warning_msg=self._reset_warning,
|
||||
preview_style=PreviewStyle.RIGHT,
|
||||
preview_size='auto',
|
||||
preview_frame=FrameProperties('Info', FrameStyle.MAX),
|
||||
additional_title=additional_title,
|
||||
).run()
|
||||
preview_location='right',
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
item: MenuItem = result.item()
|
||||
self._menu_item_group.focus_item = item
|
||||
|
||||
if item.action is None:
|
||||
if not self._is_config_valid():
|
||||
continue
|
||||
break
|
||||
else:
|
||||
item.value = item.action(item.value)
|
||||
case ResultType.Reset:
|
||||
return None
|
||||
case _:
|
||||
pass
|
||||
|
||||
self.sync_all_to_config()
|
||||
return None
|
||||
return self._config
|
||||
|
||||
|
||||
class AbstractSubMenu[ValueT](AbstractMenu[ValueT]):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Literal, TypeVar, 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.menu_item import MenuItemGroup
|
||||
from archinstall.tui.ui.result import Result, ResultType
|
||||
|
||||
ValueT = TypeVar('ValueT')
|
||||
|
||||
|
||||
class Selection[ValueT]:
|
||||
def __init__(
|
||||
self,
|
||||
group: MenuItemGroup,
|
||||
header: str | None = None,
|
||||
title: str | None = None,
|
||||
allow_skip: bool = True,
|
||||
allow_reset: bool = False,
|
||||
preview_location: Literal['right', 'bottom'] | None = None,
|
||||
multi: bool = False,
|
||||
enable_filter: bool = False,
|
||||
):
|
||||
self._header = header
|
||||
self._title = title
|
||||
self._group: MenuItemGroup = group
|
||||
self._allow_skip = allow_skip
|
||||
self._allow_reset = allow_reset
|
||||
self._preview_location = preview_location
|
||||
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:
|
||||
if self._multi:
|
||||
result = await SelectListScreen[ValueT](
|
||||
self._group,
|
||||
header=self._header,
|
||||
allow_skip=self._allow_skip,
|
||||
allow_reset=self._allow_reset,
|
||||
preview_location=self._preview_location,
|
||||
enable_filter=self._enable_filter,
|
||||
).run()
|
||||
else:
|
||||
result = await OptionListScreen[ValueT](
|
||||
self._group,
|
||||
header=self._header,
|
||||
title=self._title,
|
||||
allow_skip=self._allow_skip,
|
||||
allow_reset=self._allow_reset,
|
||||
preview_location=self._preview_location,
|
||||
enable_filter=self._enable_filter,
|
||||
).run()
|
||||
|
||||
if result.type_ == ResultType.Reset:
|
||||
confirmed = await _confirm_reset()
|
||||
|
||||
if confirmed.get_value() is False:
|
||||
return await self._run()
|
||||
|
||||
tui.exit(result)
|
||||
|
||||
|
||||
class Confirmation:
|
||||
def __init__(
|
||||
self,
|
||||
header: str,
|
||||
group: MenuItemGroup | None = None,
|
||||
allow_skip: bool = True,
|
||||
allow_reset: bool = False,
|
||||
preset: bool = False,
|
||||
preview_location: Literal['bottom'] | None = None,
|
||||
preview_header: str | None = None,
|
||||
):
|
||||
self._header = header
|
||||
self._allow_skip = allow_skip
|
||||
self._allow_reset = allow_reset
|
||||
self._preset = preset
|
||||
self._preview_location = preview_location
|
||||
self._preview_header = preview_header
|
||||
|
||||
if not group:
|
||||
self._group = MenuItemGroup.yes_no()
|
||||
self._group.set_focus_by_value(preset)
|
||||
else:
|
||||
self._group = group
|
||||
|
||||
def show(self) -> Result[bool]:
|
||||
result: Result[bool] = tui.run(self)
|
||||
return result
|
||||
|
||||
async def _run(self) -> None:
|
||||
result = await OptionListScreen[bool](
|
||||
self._group,
|
||||
header=self._header,
|
||||
allow_skip=self._allow_skip,
|
||||
allow_reset=self._allow_reset,
|
||||
preview_location=self._preview_location,
|
||||
enable_filter=False,
|
||||
).run()
|
||||
|
||||
if result.type_ == ResultType.Reset:
|
||||
confirmed = await _confirm_reset()
|
||||
|
||||
if confirmed.get_value() is False:
|
||||
return await self._run()
|
||||
|
||||
tui.exit(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())
|
||||
|
||||
|
||||
class GenericValidator(Validator):
|
||||
def __init__(self, validator_callback: Callable[[str], str | None]) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._validator_callback = validator_callback
|
||||
|
||||
@override
|
||||
def validate(self, value: str) -> ValidationResult:
|
||||
result = self._validator_callback(value)
|
||||
|
||||
if result is not None:
|
||||
return self.failure(result)
|
||||
|
||||
return self.success()
|
||||
|
||||
|
||||
class Input:
|
||||
def __init__(
|
||||
self,
|
||||
header: str | None = None,
|
||||
placeholder: str | None = None,
|
||||
password: bool = False,
|
||||
default_value: str | None = None,
|
||||
allow_skip: bool = True,
|
||||
allow_reset: bool = False,
|
||||
validator_callback: Callable[[str], str | None] | None = None,
|
||||
):
|
||||
self._header = header
|
||||
self._placeholder = placeholder
|
||||
self._password = password
|
||||
self._default_value = default_value
|
||||
self._allow_skip = allow_skip
|
||||
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:
|
||||
validator = GenericValidator(self._validator_callback) if self._validator_callback else None
|
||||
|
||||
result = await InputScreen(
|
||||
header=self._header,
|
||||
placeholder=self._placeholder,
|
||||
password=self._password,
|
||||
default_value=self._default_value,
|
||||
allow_skip=self._allow_skip,
|
||||
allow_reset=self._allow_reset,
|
||||
validator=validator,
|
||||
).run()
|
||||
|
||||
if result.type_ == ResultType.Reset:
|
||||
confirmed = await _confirm_reset()
|
||||
|
||||
if confirmed.get_value() is False:
|
||||
return await self._run()
|
||||
|
||||
tui.exit(result)
|
||||
|
||||
|
||||
class Loading[ValueT]:
|
||||
def __init__(
|
||||
self,
|
||||
header: str | None = None,
|
||||
timer: int = 3,
|
||||
data_callback: Callable[[], Any] | None = None,
|
||||
):
|
||||
self._header = header
|
||||
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:
|
||||
if self._data_callback:
|
||||
result = await LoadingScreen(
|
||||
header=self._header,
|
||||
data_callback=self._data_callback,
|
||||
).run()
|
||||
tui.exit(result)
|
||||
else:
|
||||
await LoadingScreen(
|
||||
timer=self._timer,
|
||||
header=self._header,
|
||||
).run()
|
||||
tui.exit(Result.true())
|
||||
|
||||
|
||||
class Table[ValueT]:
|
||||
def __init__(
|
||||
self,
|
||||
header: str | None = None,
|
||||
group: MenuItemGroup | None = None,
|
||||
group_callback: Callable[[], Awaitable[MenuItemGroup]] | None = None,
|
||||
presets: list[ValueT] | None = None,
|
||||
allow_reset: bool = False,
|
||||
allow_skip: bool = False,
|
||||
loading_header: str | None = None,
|
||||
multi: bool = False,
|
||||
preview_location: Literal['bottom'] | None = None,
|
||||
preview_header: str | None = None,
|
||||
):
|
||||
self._header = header
|
||||
self._group = group
|
||||
self._data_callback = group_callback
|
||||
self._loading_header = loading_header
|
||||
self._allow_skip = allow_skip
|
||||
self._allow_reset = allow_reset
|
||||
self._multi = multi
|
||||
self._presets = presets
|
||||
self._preview_location = preview_location
|
||||
self._preview_header = preview_header
|
||||
|
||||
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:
|
||||
result = await TableSelectionScreen[ValueT](
|
||||
header=self._header,
|
||||
group=self._group,
|
||||
group_callback=self._data_callback,
|
||||
allow_skip=self._allow_skip,
|
||||
allow_reset=self._allow_reset,
|
||||
loading_header=self._loading_header,
|
||||
multi=self._multi,
|
||||
preview_location=self._preview_location,
|
||||
preview_header=self._preview_header,
|
||||
).run()
|
||||
|
||||
if result.type_ == ResultType.Reset:
|
||||
confirmed = await _confirm_reset()
|
||||
|
||||
if confirmed.get_value() is False:
|
||||
return await self._run()
|
||||
|
||||
tui.exit(result)
|
||||
|
||||
|
||||
async def _confirm_reset() -> Result[bool]:
|
||||
return await OptionListScreen[bool](
|
||||
MenuItemGroup.yes_no(),
|
||||
header=tr('Are you sure you want to reset this setting?'),
|
||||
allow_skip=False,
|
||||
allow_reset=False,
|
||||
).run()
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import copy
|
||||
from typing import cast
|
||||
|
||||
from archinstall.lib.menu.helpers import Selection
|
||||
from archinstall.lib.menu.menu_helper import MenuHelper
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
|
||||
class ListManager[ValueT]:
|
||||
|
|
@ -18,7 +17,7 @@ class ListManager[ValueT]:
|
|||
prompt: str | None = None,
|
||||
):
|
||||
"""
|
||||
:param prompt: Text which will appear at the header
|
||||
:param prompt: Text which will appear at the header
|
||||
type param: string
|
||||
|
||||
:param entries: list/dict of option to be shown / manipulated
|
||||
|
|
@ -68,13 +67,12 @@ class ListManager[ValueT]:
|
|||
if self._prompt is not None:
|
||||
prompt = f'{self._prompt}\n\n'
|
||||
|
||||
result = SelectMenu[ValueT | str](
|
||||
result = Selection[ValueT | str](
|
||||
group,
|
||||
header=prompt,
|
||||
search_enabled=False,
|
||||
enable_filter=False,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
@ -106,15 +104,14 @@ class ListManager[ValueT]:
|
|||
items = [MenuItem(o, value=o) for o in options]
|
||||
group = MenuItemGroup(items, sort_items=False)
|
||||
|
||||
header = f'{self.selected_action_display(entry)}\n'
|
||||
header = f'{self.selected_action_display(entry)}'
|
||||
|
||||
result = SelectMenu[str](
|
||||
result = Selection[str](
|
||||
group,
|
||||
header=header,
|
||||
search_enabled=False,
|
||||
enable_filter=False,
|
||||
allow_skip=False,
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from archinstall.lib.output import FormattedOutput
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
|
||||
|
||||
class MenuHelper[ValueT]:
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ import urllib.parse
|
|||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
from archinstall.lib.menu.helpers import Input, Loading, Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import EditMenu, SelectMenu, Tui
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from .menu.abstract_menu import AbstractSubMenu
|
||||
from .menu.list_manager import ListManager
|
||||
|
|
@ -68,40 +67,38 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
|
|||
return data
|
||||
|
||||
def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None:
|
||||
edit_result = EditMenu(
|
||||
tr('Repository name'),
|
||||
alignment=Alignment.CENTER,
|
||||
edit_result = Input(
|
||||
header=tr('Enter a respository name'),
|
||||
allow_skip=True,
|
||||
default_text=preset.name if preset else None,
|
||||
).input()
|
||||
default_value=preset.name if preset else None,
|
||||
).show()
|
||||
|
||||
match edit_result.type_:
|
||||
case ResultType.Selection:
|
||||
name = edit_result.text()
|
||||
name = edit_result.get_value()
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
header = f'{tr("Name")}: {name}'
|
||||
header = f'{tr("Name")}: {name}\n'
|
||||
prompt = f'{header}\n' + tr('Enter the repository url')
|
||||
|
||||
edit_result = EditMenu(
|
||||
tr('Url'),
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
edit_result = Input(
|
||||
header=prompt,
|
||||
allow_skip=True,
|
||||
default_text=preset.url if preset else None,
|
||||
).input()
|
||||
default_value=preset.url if preset else None,
|
||||
).show()
|
||||
|
||||
match edit_result.type_:
|
||||
case ResultType.Selection:
|
||||
url = edit_result.text()
|
||||
url = edit_result.get_value()
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case _:
|
||||
raise ValueError('Unhandled return type')
|
||||
|
||||
header += f'\n{tr("Url")}: {url}\n'
|
||||
header += f'{tr("Url")}: {url}\n'
|
||||
prompt = f'{header}\n' + tr('Select signature check')
|
||||
|
||||
sign_chk_items = [MenuItem(s.value, value=s.value) for s in SignCheck]
|
||||
|
|
@ -110,12 +107,11 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
|
|||
if preset is not None:
|
||||
group.set_selected_by_value(preset.sign_check.value)
|
||||
|
||||
result = SelectMenu[SignCheck](
|
||||
result = Selection[SignCheck](
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=False,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
@ -132,12 +128,11 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
|
|||
if preset is not None:
|
||||
group.set_selected_by_value(preset.sign_option.value)
|
||||
|
||||
result = SelectMenu(
|
||||
result = Selection(
|
||||
group,
|
||||
header=prompt,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=False,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
|
|
@ -190,21 +185,20 @@ class CustomMirrorServersList(ListManager[CustomServer]):
|
|||
return data
|
||||
|
||||
def _add_custom_server(self, preset: CustomServer | None = None) -> CustomServer | None:
|
||||
edit_result = EditMenu(
|
||||
tr('Server url'),
|
||||
alignment=Alignment.CENTER,
|
||||
edit_result = Input(
|
||||
header=tr('Enter server url'),
|
||||
allow_skip=True,
|
||||
default_text=preset.url if preset else None,
|
||||
).input()
|
||||
default_value=preset.url if preset else None,
|
||||
).show()
|
||||
|
||||
match edit_result.type_:
|
||||
case ResultType.Selection:
|
||||
uri = edit_result.text()
|
||||
uri = edit_result.get_value()
|
||||
return CustomServer(uri)
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
|
||||
return None
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
class MirrorMenu(AbstractSubMenu[MirrorConfiguration]):
|
||||
|
|
@ -296,15 +290,16 @@ class MirrorMenu(AbstractSubMenu[MirrorConfiguration]):
|
|||
return output.strip()
|
||||
|
||||
@override
|
||||
def run(self, additional_title: str | None = None) -> MirrorConfiguration:
|
||||
super().run(additional_title=additional_title)
|
||||
return self._mirror_config
|
||||
def run(self) -> MirrorConfiguration | None:
|
||||
return super().run()
|
||||
|
||||
|
||||
def select_mirror_regions(preset: list[MirrorRegion]) -> list[MirrorRegion]:
|
||||
Tui.print(tr('Loading mirror regions...'), clear_screen=True)
|
||||
Loading[None](
|
||||
header=tr('Loading mirror regions...'),
|
||||
data_callback=mirror_list_handler.load_mirrors,
|
||||
).show()
|
||||
|
||||
mirror_list_handler.load_mirrors()
|
||||
available_regions = mirror_list_handler.get_mirror_regions()
|
||||
|
||||
if not available_regions:
|
||||
|
|
@ -317,14 +312,14 @@ def select_mirror_regions(preset: list[MirrorRegion]) -> list[MirrorRegion]:
|
|||
|
||||
group.set_selected_by_value(preset_regions)
|
||||
|
||||
result = SelectMenu[MirrorRegion](
|
||||
result = Selection[MirrorRegion](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Mirror regions')),
|
||||
header=tr('Select mirror regions to be enabled'),
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
multi=True,
|
||||
).run()
|
||||
enable_filter=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -364,14 +359,13 @@ def select_optional_repositories(preset: list[Repository]) -> list[Repository]:
|
|||
group = MenuItemGroup(items, sort_items=False)
|
||||
group.set_selected_by_value(preset)
|
||||
|
||||
result = SelectMenu[Repository](
|
||||
result = Selection[Repository](
|
||||
group,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min('Additional repositories'),
|
||||
header=tr('Select optional repositories to be enabled'),
|
||||
allow_reset=True,
|
||||
allow_skip=True,
|
||||
multi=True,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
|
|||
|
|
@ -582,6 +582,10 @@ class _DeviceInfo:
|
|||
read_only: bool
|
||||
dirty: bool
|
||||
|
||||
@override
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.path)
|
||||
|
||||
def table_data(self) -> dict[str, str | int | bool]:
|
||||
total_free_space = sum([region.get_length(unit=Unit.MiB) for region in self.free_space_regions])
|
||||
return {
|
||||
|
|
@ -1054,10 +1058,6 @@ class LvmLayoutType(Enum):
|
|||
match self:
|
||||
case LvmLayoutType.Default:
|
||||
return tr('Default layout')
|
||||
# case LvmLayoutType.Manual:
|
||||
# return str(_('Manual configuration'))
|
||||
|
||||
raise ValueError(f'Unknown type: {self}')
|
||||
|
||||
|
||||
class _LvmVolumeGroupSerialization(TypedDict):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from asyncio import sleep
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, assert_never
|
||||
from typing import assert_never
|
||||
|
||||
from archinstall.lib.exceptions import SysCallError
|
||||
from archinstall.lib.general import SysCommand
|
||||
|
|
@ -9,9 +9,9 @@ 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.menu_item import MenuItemGroup
|
||||
from archinstall.tui.ui.components import ConfirmationScreen, InputScreen, LoadingScreen, NotifyScreen, TableSelectionScreen, tui
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import Result, ResultType
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -23,14 +23,13 @@ class WpaCliResult:
|
|||
|
||||
class WifiHandler:
|
||||
def __init__(self) -> None:
|
||||
tui.set_main(self)
|
||||
self._wpa_config = WpaSupplicantConfig()
|
||||
|
||||
def setup(self) -> Any:
|
||||
result = tui.run()
|
||||
return result
|
||||
def setup(self) -> bool:
|
||||
result: Result[bool] = tui.run(self)
|
||||
return result.get_value()
|
||||
|
||||
async def run(self) -> None:
|
||||
async def _run(self) -> None:
|
||||
"""
|
||||
This is the entry point that is called by components.TApp
|
||||
"""
|
||||
|
|
@ -38,7 +37,7 @@ class WifiHandler:
|
|||
|
||||
if not wifi_iface:
|
||||
debug('No wifi interface found')
|
||||
tui.exit(False)
|
||||
tui.exit(Result.false())
|
||||
return None
|
||||
|
||||
prompt = tr('No network connection found') + '\n\n'
|
||||
|
|
@ -53,17 +52,15 @@ class WifiHandler:
|
|||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
if result.value() is False:
|
||||
tui.exit(False)
|
||||
if result.get_value() is False:
|
||||
tui.exit(Result.false())
|
||||
return None
|
||||
case ResultType.Skip | ResultType.Reset:
|
||||
tui.exit(False)
|
||||
tui.exit(Result.false())
|
||||
return None
|
||||
case _:
|
||||
assert_never(result)
|
||||
|
||||
setup_result = await self._setup_wifi(wifi_iface)
|
||||
tui.exit(setup_result)
|
||||
tui.exit(Result(ResultType.Selection, _data=setup_result))
|
||||
|
||||
async def _enable_supplicant(self, wifi_iface: str) -> bool:
|
||||
self._wpa_config.load_config()
|
||||
|
|
@ -118,21 +115,24 @@ class WifiHandler:
|
|||
|
||||
debug(f'Found wifi interface: {wifi_iface}')
|
||||
|
||||
async def get_wifi_networks() -> list[WifiNetwork]:
|
||||
async def get_wifi_networks() -> MenuItemGroup:
|
||||
debug('Scanning Wifi networks')
|
||||
result = self._wpa_cli('scan', wifi_iface)
|
||||
|
||||
if not result.success:
|
||||
debug(f'Failed to scan wifi networks: {result.error}')
|
||||
return []
|
||||
return MenuItemGroup([])
|
||||
|
||||
await sleep(5)
|
||||
return self._get_scan_results(wifi_iface)
|
||||
wifi_networks = self._get_scan_results(wifi_iface)
|
||||
|
||||
items = [MenuItem(network.ssid, value=network) for network in wifi_networks]
|
||||
return MenuItemGroup(items)
|
||||
|
||||
result = await TableSelectionScreen[WifiNetwork](
|
||||
header=tr('Select wifi network to connect to'),
|
||||
loading_header=tr('Scanning wifi networks...'),
|
||||
data_callback=get_wifi_networks,
|
||||
group_callback=get_wifi_networks,
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
).run()
|
||||
|
|
@ -142,12 +142,12 @@ class WifiHandler:
|
|||
if not result.has_data():
|
||||
debug('No networks found')
|
||||
await NotifyScreen(header=tr('No wifi networks found')).run()
|
||||
tui.exit(False)
|
||||
tui.exit(Result.false())
|
||||
return False
|
||||
|
||||
network = result.value()
|
||||
network = result.get_value()
|
||||
case ResultType.Skip | ResultType.Reset:
|
||||
tui.exit(False)
|
||||
tui.exit(Result.false())
|
||||
return False
|
||||
case _:
|
||||
assert_never(result.type_)
|
||||
|
|
@ -170,7 +170,7 @@ class WifiHandler:
|
|||
await self._notify_failure()
|
||||
return False
|
||||
|
||||
await LoadingScreen(3, 'Setting up wifi...').run()
|
||||
await LoadingScreen(timer=3, header='Setting up wifi...').run()
|
||||
|
||||
network_id = self._find_network_id(network.ssid, wifi_iface)
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ class WifiHandler:
|
|||
await self._notify_failure()
|
||||
return False
|
||||
|
||||
await LoadingScreen(5, 'Connecting wifi...').run()
|
||||
await LoadingScreen(timer=5, header='Connecting wifi...').run()
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -253,7 +253,7 @@ class WifiHandler:
|
|||
debug('No password provided, aborting connection')
|
||||
return None
|
||||
|
||||
return result.value()
|
||||
return result.get_value()
|
||||
|
||||
def _get_scan_results(self, iface: str) -> list[WifiNetwork]:
|
||||
debug(f'Retrieving scan results: {iface}')
|
||||
|
|
|
|||
|
|
@ -332,6 +332,4 @@ def log(
|
|||
Journald.log(text, level=level)
|
||||
|
||||
if level != logging.DEBUG:
|
||||
from archinstall.tui.curses_menu import Tui
|
||||
|
||||
Tui.print(text)
|
||||
print(text)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,24 @@ BASE_URL_PKG_SEARCH = 'https://archlinux.org/packages/search/json/'
|
|||
BASE_GROUP_URL = 'https://archlinux.org/groups/search/json/'
|
||||
|
||||
|
||||
# TODO: This shouldn't be living in here but there are too many
|
||||
# circular dependecies so they will need to be cleanup up first
|
||||
@lru_cache(maxsize=128)
|
||||
def check_version_upgrade() -> str | None:
|
||||
debug('Checking version')
|
||||
upgrade = None
|
||||
|
||||
upgrade = check_package_upgrade('archinstall')
|
||||
|
||||
if upgrade is None:
|
||||
debug('No archinstall upgrades found')
|
||||
return None
|
||||
|
||||
debug(f'Archinstall latest: {upgrade}')
|
||||
|
||||
return upgrade
|
||||
|
||||
|
||||
def _make_request(url: str, params: dict[str, str]) -> addinfourl:
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from typing import override
|
||||
|
||||
from archinstall.default_profiles.profile import GreeterType, Profile
|
||||
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import SelectMenu
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation
|
||||
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..hardware import GfxDriver
|
||||
from ..interactions.system_conf import select_driver
|
||||
|
|
@ -62,9 +61,8 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
|
|||
]
|
||||
|
||||
@override
|
||||
def run(self, additional_title: str | None = None) -> ProfileConfiguration | None:
|
||||
super().run(additional_title=additional_title)
|
||||
return self._profile_config
|
||||
def run(self) -> ProfileConfiguration | None:
|
||||
return super().run()
|
||||
|
||||
def _select_profile(self, preset: Profile | None) -> Profile | None:
|
||||
profile = select_profile(preset)
|
||||
|
|
@ -102,20 +100,13 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
|
|||
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'
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.focus_item = MenuItem.no()
|
||||
group.default_item = MenuItem.no()
|
||||
|
||||
result = SelectMenu[bool](
|
||||
group,
|
||||
result = Confirmation(
|
||||
header=header,
|
||||
allow_skip=False,
|
||||
columns=2,
|
||||
orientation=Orientation.HORIZONTAL,
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
preset=False,
|
||||
).show()
|
||||
|
||||
if result.item() == MenuItem.no():
|
||||
if result.get_value():
|
||||
return preset
|
||||
|
||||
return driver
|
||||
|
|
@ -167,12 +158,11 @@ def select_greeter(
|
|||
|
||||
group.set_default_by_value(default)
|
||||
|
||||
result = SelectMenu[GreeterType](
|
||||
result = Selection[GreeterType](
|
||||
group,
|
||||
header=tr('Select which greeter to install'),
|
||||
allow_skip=True,
|
||||
frame=FrameProperties.min(tr('Greeter')),
|
||||
alignment=Alignment.CENTER,
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
|
|
@ -195,20 +185,18 @@ def select_profile(
|
|||
top_level_profiles = profile_handler.get_top_level_profiles()
|
||||
|
||||
if header is None:
|
||||
header = tr('This is a list of pre-programmed default_profiles') + '\n'
|
||||
header = tr('Select a profile type')
|
||||
|
||||
items = [MenuItem(p.name, value=p) for p in top_level_profiles]
|
||||
group = MenuItemGroup(items, sort_items=True)
|
||||
group.set_selected_by_value(current_profile)
|
||||
|
||||
result = SelectMenu[Profile](
|
||||
result = Selection[Profile](
|
||||
group,
|
||||
header=header,
|
||||
allow_reset=allow_reset,
|
||||
allow_skip=True,
|
||||
alignment=Alignment.CENTER,
|
||||
frame=FrameProperties.min(tr('Main profile')),
|
||||
).run()
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Reset:
|
||||
|
|
|
|||
|
|
@ -1,69 +1,64 @@
|
|||
from pathlib import Path
|
||||
|
||||
from archinstall.lib.menu.helpers import Input
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.curses_menu import EditMenu
|
||||
from archinstall.tui.result import ResultType
|
||||
from archinstall.tui.types import Alignment
|
||||
from archinstall.tui.ui.result import ResultType
|
||||
|
||||
from ..models.users import Password
|
||||
from ..output import FormattedOutput
|
||||
|
||||
|
||||
def get_password(
|
||||
text: str,
|
||||
header: str | None = None,
|
||||
allow_skip: bool = False,
|
||||
preset: str | None = None,
|
||||
skip_confirmation: bool = False,
|
||||
) -> Password | None:
|
||||
failure: str | None = None
|
||||
|
||||
while True:
|
||||
user_hdr = None
|
||||
if failure is not None:
|
||||
user_hdr = f'{header}\n{failure}\n'
|
||||
elif header is not None:
|
||||
user_hdr = header
|
||||
|
||||
result = EditMenu(
|
||||
text,
|
||||
header=user_hdr,
|
||||
alignment=Alignment.CENTER,
|
||||
result = Input(
|
||||
header=header,
|
||||
allow_skip=allow_skip,
|
||||
default_text=preset,
|
||||
hide_input=True,
|
||||
).input()
|
||||
default_value=preset,
|
||||
password=True,
|
||||
).show()
|
||||
|
||||
if allow_skip:
|
||||
if not result.has_item() or not result.text():
|
||||
if result.type_ == ResultType.Skip:
|
||||
if allow_skip:
|
||||
return None
|
||||
else:
|
||||
continue
|
||||
elif result.type_ == ResultType.Selection:
|
||||
if not result.get_value():
|
||||
if allow_skip:
|
||||
return None
|
||||
else:
|
||||
continue
|
||||
|
||||
password = Password(plaintext=result.text())
|
||||
password = Password(plaintext=result.get_value())
|
||||
break
|
||||
|
||||
if skip_confirmation:
|
||||
return password
|
||||
if skip_confirmation:
|
||||
return password
|
||||
|
||||
if header is not None:
|
||||
confirmation_header = f'{header}{tr("Password")}: {password.hidden()}\n'
|
||||
else:
|
||||
confirmation_header = f'{tr("Password")}: {password.hidden()}\n'
|
||||
confirmation_header = f'{tr("Password")}: {password.hidden()}\n\n'
|
||||
confirmation_header += tr('Confirm password')
|
||||
|
||||
result = EditMenu(
|
||||
tr('Confirm password'),
|
||||
header=confirmation_header,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=False,
|
||||
hide_input=True,
|
||||
).input()
|
||||
def _validate(value: str) -> str | None:
|
||||
if value != password._plaintext:
|
||||
return tr('The password did not match, please try again')
|
||||
return None
|
||||
|
||||
if password._plaintext == result.text():
|
||||
return password
|
||||
_ = Input(
|
||||
header=confirmation_header,
|
||||
allow_skip=False,
|
||||
password=True,
|
||||
validator_callback=_validate,
|
||||
).show()
|
||||
|
||||
failure = tr('The confirmation password did not match, please try again')
|
||||
return password
|
||||
|
||||
|
||||
def prompt_dir(
|
||||
text: str,
|
||||
header: str | None = None,
|
||||
validate: bool = True,
|
||||
must_exist: bool = True,
|
||||
|
|
@ -87,24 +82,22 @@ def prompt_dir(
|
|||
else:
|
||||
validate_func = None
|
||||
|
||||
result = EditMenu(
|
||||
text,
|
||||
result = Input(
|
||||
header=header,
|
||||
alignment=Alignment.CENTER,
|
||||
allow_skip=allow_skip,
|
||||
validator=validate_func,
|
||||
default_text=preset,
|
||||
).input()
|
||||
validator_callback=validate_func,
|
||||
default_value=preset,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return None
|
||||
case ResultType.Selection:
|
||||
if not result.text():
|
||||
if not result.get_value():
|
||||
return None
|
||||
return Path(result.text())
|
||||
|
||||
return None
|
||||
return Path(result.get_value())
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
def is_subpath(first: Path, second: Path) -> bool:
|
||||
|
|
|
|||
|
|
@ -20,10 +20,9 @@ from archinstall.lib.models.device import (
|
|||
)
|
||||
from archinstall.lib.models.users import User
|
||||
from archinstall.lib.output import debug, error, info
|
||||
from archinstall.lib.packages.packages import check_package_upgrade
|
||||
from archinstall.lib.packages.packages import check_version_upgrade
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui import Tui
|
||||
|
||||
|
||||
def ask_user_questions() -> None:
|
||||
|
|
@ -33,20 +32,19 @@ def ask_user_questions() -> None:
|
|||
will we continue with the actual installation steps.
|
||||
"""
|
||||
|
||||
title_text = None
|
||||
upgrade = check_version_upgrade()
|
||||
title_text = 'Archlinux'
|
||||
|
||||
upgrade = check_package_upgrade('archinstall')
|
||||
if upgrade:
|
||||
text = tr('New version available') + f': {upgrade}'
|
||||
title_text = f' ({text})'
|
||||
title_text += f' ({text})'
|
||||
|
||||
with Tui():
|
||||
global_menu = GlobalMenu(arch_config_handler.config)
|
||||
global_menu = GlobalMenu(arch_config_handler.config, title=title_text)
|
||||
|
||||
if not arch_config_handler.args.advanced:
|
||||
global_menu.set_enabled('parallel_downloads', False)
|
||||
if not arch_config_handler.args.advanced:
|
||||
global_menu.set_enabled('parallel_downloads', False)
|
||||
|
||||
global_menu.run(additional_title=title_text)
|
||||
global_menu.run()
|
||||
|
||||
|
||||
def perform_installation(mountpoint: Path) -> None:
|
||||
|
|
@ -170,9 +168,8 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
debug(f'Disk states after installing:\n{disk_layouts()}')
|
||||
|
||||
if not arch_config_handler.args.silent:
|
||||
with Tui():
|
||||
elapsed_time = time.time() - start_time
|
||||
action = ask_post_installation(elapsed_time)
|
||||
elapsed_time = time.time() - start_time
|
||||
action = ask_post_installation(elapsed_time)
|
||||
|
||||
match action:
|
||||
case PostInstallationAction.EXIT:
|
||||
|
|
@ -199,10 +196,9 @@ def guided() -> None:
|
|||
|
||||
if not arch_config_handler.args.silent:
|
||||
aborted = False
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
aborted = True
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
aborted = True
|
||||
|
||||
if aborted:
|
||||
return guided()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ from archinstall.lib.models.profile import ProfileConfiguration
|
|||
from archinstall.lib.models.users import Password, User
|
||||
from archinstall.lib.output import debug, error, info
|
||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||
from archinstall.tui import Tui
|
||||
|
||||
|
||||
def perform_installation(mountpoint: Path) -> None:
|
||||
|
|
@ -61,9 +60,8 @@ def perform_installation(mountpoint: Path) -> None:
|
|||
|
||||
|
||||
def _minimal() -> None:
|
||||
with Tui():
|
||||
disk_config = DiskLayoutConfigurationMenu(disk_layout_config=None).run()
|
||||
arch_config_handler.config.disk_config = disk_config
|
||||
disk_config = DiskLayoutConfigurationMenu(disk_layout_config=None).run()
|
||||
arch_config_handler.config.disk_config = disk_config
|
||||
|
||||
config = ConfigurationOutput(arch_config_handler.config)
|
||||
config.write_debug()
|
||||
|
|
@ -74,10 +72,9 @@ def _minimal() -> None:
|
|||
|
||||
if not arch_config_handler.args.silent:
|
||||
aborted = False
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
aborted = True
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
aborted = True
|
||||
|
||||
if aborted:
|
||||
return _minimal()
|
||||
|
|
|
|||
|
|
@ -8,20 +8,18 @@ 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.tui import Tui
|
||||
|
||||
|
||||
def ask_user_questions() -> None:
|
||||
with Tui():
|
||||
global_menu = GlobalMenu(arch_config_handler.config)
|
||||
global_menu.disable_all()
|
||||
global_menu = GlobalMenu(arch_config_handler.config)
|
||||
global_menu.disable_all()
|
||||
|
||||
global_menu.set_enabled('archinstall_language', True)
|
||||
global_menu.set_enabled('disk_config', True)
|
||||
global_menu.set_enabled('swap', True)
|
||||
global_menu.set_enabled('__config__', True)
|
||||
global_menu.set_enabled('archinstall_language', True)
|
||||
global_menu.set_enabled('disk_config', True)
|
||||
global_menu.set_enabled('swap', True)
|
||||
global_menu.set_enabled('__config__', True)
|
||||
|
||||
global_menu.run()
|
||||
global_menu.run()
|
||||
|
||||
|
||||
def perform_installation(mountpoint: Path) -> None:
|
||||
|
|
@ -70,10 +68,9 @@ def _only_hd() -> None:
|
|||
|
||||
if not arch_config_handler.args.silent:
|
||||
aborted = False
|
||||
with Tui():
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
aborted = True
|
||||
if not config.confirm_config():
|
||||
debug('Installation aborted')
|
||||
aborted = True
|
||||
|
||||
if aborted:
|
||||
return _only_hd()
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
from .curses_menu import EditMenu, SelectMenu, Tui
|
||||
from .menu_item import MenuItem, MenuItemGroup
|
||||
from .result import Result, ResultType
|
||||
from .types import Alignment, Chars, FrameProperties, FrameStyle, Orientation, PreviewStyle
|
||||
|
||||
__all__ = [
|
||||
'Alignment',
|
||||
'Chars',
|
||||
'EditMenu',
|
||||
'FrameProperties',
|
||||
'FrameStyle',
|
||||
'MenuItem',
|
||||
'MenuItemGroup',
|
||||
'Orientation',
|
||||
'PreviewStyle',
|
||||
'Result',
|
||||
'ResultType',
|
||||
'SelectMenu',
|
||||
'Tui',
|
||||
]
|
||||
|
|
@ -90,7 +90,7 @@ class AbstractCurses[ValueT](metaclass=ABCMeta):
|
|||
lines = help_text.split('\n')
|
||||
|
||||
entries = [ViewportEntry('', 0, 0, STYLE.NORMAL)]
|
||||
entries += [ViewportEntry(f' {e} ', idx + 1, 0, STYLE.NORMAL) for idx, e in enumerate(lines)]
|
||||
entries += [ViewportEntry(f' {e} ', idx + 1, 0, STYLE.NORMAL) for idx, e in enumerate(lines)]
|
||||
self._help_window.update(entries, 0)
|
||||
|
||||
def get_header_entries(self, header: str) -> list[ViewportEntry]:
|
||||
|
|
@ -227,7 +227,8 @@ class AbstractViewport:
|
|||
|
||||
# 2 for frames, 1 for extra space start away from frame
|
||||
# must align with def _adjust_entries
|
||||
frame_end += 3 # 2 for frame
|
||||
# 2 for frame
|
||||
frame_end += 3
|
||||
|
||||
frame_height = len(rows) + 1
|
||||
if frame_height > max_height:
|
||||
|
|
@ -1122,7 +1123,7 @@ class SelectMenu[ValueT](AbstractCurses[ValueT]):
|
|||
|
||||
def _multi_prefix(self, item: MenuItem) -> str:
|
||||
if item.read_only:
|
||||
return ' '
|
||||
return ' '
|
||||
elif self._item_group.is_item_selected(item):
|
||||
return '[x] '
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from collections.abc import Callable
|
|||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from functools import cached_property
|
||||
from typing import Any, ClassVar, Self
|
||||
from typing import Any, ClassVar, Self, override
|
||||
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
|
@ -20,12 +20,26 @@ class MenuItem:
|
|||
dependencies: list[str | Callable[[], bool]] = field(default_factory=list)
|
||||
dependencies_not: list[str] = field(default_factory=list)
|
||||
display_action: Callable[[Any], str] | None = None
|
||||
preview_action: Callable[[Any], str | None] | None = None
|
||||
preview_action: Callable[[Self], str | None] | None = None
|
||||
key: str | None = None
|
||||
|
||||
_id: str = ''
|
||||
_yes: ClassVar[Self | None] = None
|
||||
_no: ClassVar[Self | None] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.key is not None:
|
||||
self._id = self.key
|
||||
else:
|
||||
self._id = str(id(self))
|
||||
|
||||
@override
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._id)
|
||||
|
||||
def get_id(self) -> str:
|
||||
return self._id
|
||||
|
||||
def get_value(self) -> Any:
|
||||
assert self.value is not None
|
||||
return self.value
|
||||
|
|
@ -95,18 +109,25 @@ class MenuItemGroup:
|
|||
self.focus_first()
|
||||
|
||||
if self.focus_item not in self.items:
|
||||
raise ValueError(f'Selected item not in menu: {focus_item}')
|
||||
raise ValueError(f'Selected item not in menu: {self.focus_item}')
|
||||
|
||||
def add_item(self, item: MenuItem) -> None:
|
||||
self._menu_items.append(item)
|
||||
delattr(self, 'items') # resetting the cache
|
||||
|
||||
def find_by_id(self, item_id: str) -> MenuItem:
|
||||
for item in self._menu_items:
|
||||
if item.get_id() == item_id:
|
||||
return item
|
||||
|
||||
raise ValueError(f'No item found for id: {item_id}')
|
||||
|
||||
def find_by_key(self, key: str) -> MenuItem:
|
||||
for item in self._menu_items:
|
||||
if item.key == key:
|
||||
return item
|
||||
|
||||
raise ValueError(f'No key found for: {key}')
|
||||
raise ValueError(f'No item found for key: {key}')
|
||||
|
||||
def get_enabled_items(self) -> list[MenuItem]:
|
||||
return [it for it in self.items if self.is_enabled(it)]
|
||||
|
|
@ -163,6 +184,21 @@ class MenuItemGroup:
|
|||
if values:
|
||||
self.set_focus_by_value(values[0])
|
||||
|
||||
def get_focused_index(self) -> int | None:
|
||||
items = self.get_enabled_items()
|
||||
|
||||
if self.focus_item and items:
|
||||
try:
|
||||
return items.index(self.focus_item)
|
||||
except ValueError:
|
||||
# on large menus (15k+) when filtering very quickly
|
||||
# the index search is too slow while the items are reduced
|
||||
# by the filter and it will blow up as it cannot find the
|
||||
# focus item
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def index_focus(self) -> int | None:
|
||||
if self.focus_item and self.items:
|
||||
try:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,336 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from functools import cached_property
|
||||
from typing import Any, ClassVar, Self, override
|
||||
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
@dataclass
|
||||
class MenuItem:
|
||||
text: str
|
||||
value: Any | None = None
|
||||
action: Callable[[Any], Any] | None = None
|
||||
enabled: bool = True
|
||||
read_only: bool = False
|
||||
mandatory: bool = False
|
||||
dependencies: list[str | Callable[[], bool]] = field(default_factory=list)
|
||||
dependencies_not: list[str] = field(default_factory=list)
|
||||
display_action: Callable[[Any], str] | None = None
|
||||
preview_action: Callable[[Self], str | None] | None = None
|
||||
key: str | None = None
|
||||
|
||||
_id: str = ''
|
||||
|
||||
_yes: ClassVar[Self | None] = None
|
||||
_no: ClassVar[Self | None] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.key is not None:
|
||||
self._id = self.key
|
||||
else:
|
||||
self._id = str(id(self))
|
||||
|
||||
@override
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._id)
|
||||
|
||||
def get_id(self) -> str:
|
||||
return self._id
|
||||
|
||||
def get_value(self) -> Any:
|
||||
assert self.value is not None
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def yes(cls, action: Callable[[Any], Any] | None = None) -> Self:
|
||||
if cls._yes is None:
|
||||
cls._yes = cls(tr('Yes'), value=True, key='yes', action=action)
|
||||
|
||||
return cls._yes
|
||||
|
||||
@classmethod
|
||||
def no(cls, action: Callable[[Any], Any] | None = None) -> Self:
|
||||
if cls._no is None:
|
||||
cls._no = cls(tr('No'), value=False, key='no', action=action)
|
||||
|
||||
return cls._no
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return self.text == '' or self.text is None
|
||||
|
||||
def has_value(self) -> bool:
|
||||
if self.value is None:
|
||||
return False
|
||||
elif isinstance(self.value, list) and len(self.value) == 0:
|
||||
return False
|
||||
elif isinstance(self.value, dict) and len(self.value) == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def get_display_value(self) -> str | None:
|
||||
if self.display_action is not None:
|
||||
return self.display_action(self.value)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class MenuItemGroup:
|
||||
def __init__(
|
||||
self,
|
||||
menu_items: list[MenuItem],
|
||||
focus_item: MenuItem | None = None,
|
||||
default_item: MenuItem | None = None,
|
||||
sort_items: bool = False,
|
||||
sort_case_sensitive: bool = True,
|
||||
checkmarks: bool = False,
|
||||
) -> None:
|
||||
if len(menu_items) < 1:
|
||||
raise ValueError('Menu must have at least one item')
|
||||
|
||||
if sort_items:
|
||||
if sort_case_sensitive:
|
||||
menu_items = sorted(menu_items, key=lambda x: x.text)
|
||||
else:
|
||||
menu_items = sorted(menu_items, key=lambda x: x.text.lower())
|
||||
|
||||
self._filter_pattern: str = ''
|
||||
self._checkmarks: bool = checkmarks
|
||||
|
||||
self._menu_items: list[MenuItem] = menu_items
|
||||
self.focus_item: MenuItem | None = focus_item
|
||||
self.selected_items: list[MenuItem] = []
|
||||
self.default_item: MenuItem | None = default_item
|
||||
|
||||
if not focus_item:
|
||||
self.focus_first()
|
||||
|
||||
if self.focus_item not in self.items:
|
||||
raise ValueError(f'Selected item not in menu: {self.focus_item}')
|
||||
|
||||
@classmethod
|
||||
def from_objects(cls, items: list[Any]) -> Self:
|
||||
items = [MenuItem(str(id(item)), value=item) for item in items]
|
||||
return cls(items)
|
||||
|
||||
def add_item(self, item: MenuItem) -> None:
|
||||
self._menu_items.append(item)
|
||||
delattr(self, 'items') # resetting the cache
|
||||
|
||||
def find_by_id(self, item_id: str) -> MenuItem:
|
||||
for item in self._menu_items:
|
||||
if item.get_id() == item_id:
|
||||
return item
|
||||
|
||||
raise ValueError(f'No item found for id: {item_id}')
|
||||
|
||||
def find_by_key(self, key: str) -> MenuItem:
|
||||
for item in self._menu_items:
|
||||
if item.key == key:
|
||||
return item
|
||||
|
||||
raise ValueError(f'No item found for key: {key}')
|
||||
|
||||
def get_enabled_items(self) -> list[MenuItem]:
|
||||
return [it for it in self.items if self.is_enabled(it)]
|
||||
|
||||
@classmethod
|
||||
def yes_no(cls) -> Self:
|
||||
return cls(
|
||||
[MenuItem.yes(), MenuItem.no()],
|
||||
sort_items=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_enum(
|
||||
cls,
|
||||
enum_cls: type[Enum],
|
||||
sort_items: bool = False,
|
||||
preset: Enum | None = None,
|
||||
) -> Self:
|
||||
items = [MenuItem(elem.value, value=elem) for elem in enum_cls]
|
||||
group = cls(items, sort_items=sort_items)
|
||||
|
||||
if preset is not None:
|
||||
group.set_selected_by_value(preset)
|
||||
|
||||
return group
|
||||
|
||||
def set_preview_for_all(self, action: Callable[[Any], str | None]) -> None:
|
||||
for item in self.items:
|
||||
item.preview_action = action
|
||||
|
||||
def set_focus_by_value(self, value: Any) -> None:
|
||||
for item in self._menu_items:
|
||||
if item.value == value:
|
||||
self.focus_item = item
|
||||
break
|
||||
|
||||
def set_default_by_value(self, value: Any) -> None:
|
||||
for item in self._menu_items:
|
||||
if item.value == value:
|
||||
self.default_item = item
|
||||
break
|
||||
|
||||
def set_selected_by_value(self, values: Any | list[Any] | None) -> None:
|
||||
if values is None:
|
||||
return
|
||||
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
|
||||
for item in self._menu_items:
|
||||
if item.value in values:
|
||||
self.selected_items.append(item)
|
||||
|
||||
if values:
|
||||
self.set_focus_by_value(values[0])
|
||||
|
||||
def get_focused_index(self) -> int | None:
|
||||
items = self.get_enabled_items()
|
||||
|
||||
if self.focus_item and items:
|
||||
try:
|
||||
return items.index(self.focus_item)
|
||||
except ValueError:
|
||||
# on large menus (15k+) when filtering very quickly
|
||||
# the index search is too slow while the items are reduced
|
||||
# by the filter and it will blow up as it cannot find the
|
||||
# focus item
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def _max_items_text_width(self) -> int:
|
||||
return max([len(item.text) for item in self._menu_items])
|
||||
|
||||
def _default_suffix(self, item: MenuItem) -> str:
|
||||
if self.default_item == item:
|
||||
return tr(' (default)')
|
||||
return ''
|
||||
|
||||
def set_action_for_all(self, action: Callable[[Any], Any]) -> None:
|
||||
for item in self.items:
|
||||
item.action = action
|
||||
|
||||
@cached_property
|
||||
def items(self) -> list[MenuItem]:
|
||||
pattern = self._filter_pattern.lower()
|
||||
items = filter(lambda item: item.is_empty() or pattern in item.text.lower(), self._menu_items)
|
||||
l_items = sorted(items, key=self._items_score)
|
||||
return l_items
|
||||
|
||||
def _items_score(self, item: MenuItem) -> int:
|
||||
pattern = self._filter_pattern.lower()
|
||||
if item.text.lower().startswith(pattern):
|
||||
return 0
|
||||
return 1
|
||||
|
||||
def set_filter_pattern(self, pattern: str) -> None:
|
||||
self._filter_pattern = pattern
|
||||
delattr(self, 'items') # resetting the cache
|
||||
self.focus_first()
|
||||
|
||||
def focus_index(self, index: int) -> None:
|
||||
enabled = self.get_enabled_items()
|
||||
self.focus_item = enabled[index]
|
||||
|
||||
def focus_first(self) -> None:
|
||||
if len(self.items) == 0:
|
||||
return
|
||||
|
||||
first_item: MenuItem | None = self.items[0]
|
||||
|
||||
if first_item and not self._is_selectable(first_item):
|
||||
first_item = self._find_next_selectable_item(self.items, first_item, 1)
|
||||
|
||||
if first_item is not None:
|
||||
self.focus_item = first_item
|
||||
|
||||
def focus_last(self) -> None:
|
||||
if len(self.items) == 0:
|
||||
return
|
||||
|
||||
last_item: MenuItem | None = self.items[-1]
|
||||
|
||||
if last_item and not self._is_selectable(last_item):
|
||||
last_item = self._find_next_selectable_item(self.items, last_item, -1)
|
||||
|
||||
if last_item is not None:
|
||||
self.focus_item = last_item
|
||||
|
||||
def focus_prev(self, skip_empty: bool = True) -> None:
|
||||
# e.g. when filter shows no items
|
||||
if self.focus_item is None:
|
||||
return
|
||||
|
||||
item = self._find_next_selectable_item(self.items, self.focus_item, -1)
|
||||
|
||||
if item is not None:
|
||||
self.focus_item = item
|
||||
|
||||
def focus_next(self, skip_not_enabled: bool = True) -> None:
|
||||
# e.g. when filter shows no items
|
||||
if self.focus_item is None:
|
||||
return
|
||||
|
||||
item = self._find_next_selectable_item(self.items, self.focus_item, 1)
|
||||
|
||||
if item is not None:
|
||||
self.focus_item = item
|
||||
|
||||
def _find_next_selectable_item(
|
||||
self,
|
||||
items: list[MenuItem],
|
||||
start_item: MenuItem,
|
||||
direction: int,
|
||||
) -> MenuItem | None:
|
||||
start_index = self.items.index(start_item)
|
||||
n = len(items)
|
||||
|
||||
current_index = start_index
|
||||
for _ in range(n):
|
||||
current_index = (current_index + direction) % n
|
||||
|
||||
if self._is_selectable(items[current_index]):
|
||||
return items[current_index]
|
||||
|
||||
return None
|
||||
|
||||
def max_item_width(self) -> int:
|
||||
spaces = [len(str(it.text)) for it in self.items]
|
||||
if spaces:
|
||||
return max(spaces)
|
||||
return 0
|
||||
|
||||
def _is_selectable(self, item: MenuItem) -> bool:
|
||||
if item.is_empty():
|
||||
return False
|
||||
elif item.read_only:
|
||||
return False
|
||||
|
||||
return self.is_enabled(item)
|
||||
|
||||
def is_enabled(self, item: MenuItem) -> bool:
|
||||
if not item.enabled:
|
||||
return False
|
||||
|
||||
for dep in item.dependencies:
|
||||
if isinstance(dep, str):
|
||||
item = self.find_by_key(dep)
|
||||
if not item.value or not self.is_enabled(item):
|
||||
return False
|
||||
else:
|
||||
return dep()
|
||||
|
||||
for dep_not in item.dependencies_not:
|
||||
item = self.find_by_key(dep_not)
|
||||
if item.value is not None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from typing import cast
|
||||
from typing import Self, cast
|
||||
|
||||
from archinstall.tui.ui.menu_item import MenuItem
|
||||
|
||||
|
||||
class ResultType(Enum):
|
||||
|
|
@ -12,15 +14,46 @@ class ResultType(Enum):
|
|||
@dataclass
|
||||
class Result[ValueT]:
|
||||
type_: ResultType
|
||||
_data: ValueT | list[ValueT] | None
|
||||
_data: ValueT | list[ValueT] | None = None
|
||||
_item: MenuItem | list[MenuItem] | None = None
|
||||
|
||||
@classmethod
|
||||
def true(cls) -> Self:
|
||||
return cls(ResultType.Selection, _data=True) # type: ignore[arg-type]
|
||||
|
||||
@classmethod
|
||||
def false(cls) -> Self:
|
||||
return cls(ResultType.Selection, _data=False) # type: ignore[arg-type]
|
||||
|
||||
def has_data(self) -> bool:
|
||||
return self._data is not None
|
||||
|
||||
def value(self) -> ValueT:
|
||||
assert type(self._data) is not list and self._data is not None
|
||||
return cast(ValueT, self._data)
|
||||
def has_value(self) -> bool:
|
||||
return self._item is not None
|
||||
|
||||
def item(self) -> MenuItem:
|
||||
if isinstance(self._item, list) or self._item is None:
|
||||
raise ValueError('Invalid item type')
|
||||
return self._item
|
||||
|
||||
def items(self) -> list[MenuItem]:
|
||||
if isinstance(self._item, list):
|
||||
return self._item
|
||||
|
||||
raise ValueError('Invalid item type')
|
||||
|
||||
def get_value(self) -> ValueT:
|
||||
if self._item is not None:
|
||||
return self.item().get_value() # type: ignore[no-any-return]
|
||||
|
||||
if type(self._data) is not list and self._data is not None:
|
||||
return cast(ValueT, self._data)
|
||||
|
||||
raise ValueError('No value found')
|
||||
|
||||
def get_values(self) -> list[ValueT]:
|
||||
if self._item is not None:
|
||||
return [i.get_value() for i in self.items()]
|
||||
|
||||
def values(self) -> list[ValueT]:
|
||||
assert type(self._data) is list
|
||||
return cast(list[ValueT], self._data)
|
||||
|
|
|
|||
Loading…
Reference in New Issue