Merge upstream/master into lang-for-target-locale
Resolve import conflict in global_menu.py: keep both ResultType (locale/timezone confirmation dialog) and MsgLevelType/PreviewResult (color preview from #4512).
This commit is contained in:
commit
6b3b6e3f7d
|
|
@ -11,7 +11,6 @@ from archinstall.lib.crypt import encrypt
|
|||
from archinstall.lib.log import debug, logger, warn
|
||||
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||
from archinstall.lib.menu.util import get_password, prompt_dir
|
||||
from archinstall.lib.models.network import NetworkConfiguration
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.lib.utils.format import as_key_value_pair
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
|
|
@ -83,13 +82,10 @@ class ConfigurationOutput:
|
|||
|
||||
return simple_summary
|
||||
|
||||
async def confirm_config(self, show_install_warnings: bool = False) -> bool:
|
||||
async def confirm_config(self) -> bool:
|
||||
header = f'{tr("The specified configuration will be applied")}. '
|
||||
header += tr('Would you like to continue?') + '\n'
|
||||
|
||||
if show_install_warnings:
|
||||
header += self._render_install_warnings()
|
||||
|
||||
group = MenuItemGroup.yes_no()
|
||||
group.set_preview_for_all(lambda x: self.user_config_to_json())
|
||||
|
||||
|
|
@ -107,22 +103,6 @@ class ConfigurationOutput:
|
|||
|
||||
return True
|
||||
|
||||
def get_install_warnings(self) -> list[str]:
|
||||
warnings: list[str] = []
|
||||
|
||||
if not isinstance(self._config.network_config, NetworkConfiguration):
|
||||
warnings.append(tr('Warning: no network configuration selected. Network will need to be set up manually on the installed system.'))
|
||||
|
||||
return warnings
|
||||
|
||||
def _render_install_warnings(self) -> str:
|
||||
warnings = self.get_install_warnings()
|
||||
|
||||
if not warnings:
|
||||
return ''
|
||||
|
||||
return '\n' + '\n'.join(f'[yellow]{w}[/]' for w in warnings) + '\n'
|
||||
|
||||
def _is_valid_path(self, dest_path: Path) -> bool:
|
||||
dest_path_ok = dest_path.exists() and dest_path.is_dir()
|
||||
if not dest_path_ok:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ from archinstall.lib.pacman.pacman_menu import PacmanMenu
|
|||
from archinstall.lib.translationhandler import DEFAULT_TIMEZONE, Language, tr, translation_handler
|
||||
from archinstall.lib.utils.format import as_table
|
||||
from archinstall.tui.components import tui
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup, MsgLevelType, PreviewResult
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
|
|
@ -571,21 +571,48 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
|
||||
return None
|
||||
|
||||
def _prev_install_invalid_config(self, item: MenuItem) -> str | None:
|
||||
if missing := self._missing_configs():
|
||||
text = tr('Missing configurations:\n')
|
||||
for m in missing:
|
||||
text += f'- {m}\n'
|
||||
return text[:-1] # remove last new line
|
||||
def _get_install_warnings(self) -> list[str]:
|
||||
warnings: list[str] = []
|
||||
|
||||
if error := self._validate_bootloader():
|
||||
return tr('Invalid configuration: {}').format(error)
|
||||
if not isinstance(self._arch_config.network_config, NetworkConfiguration):
|
||||
warnings.append(tr('No network configuration selected. Network will need to be set up manually on the installed system.'))
|
||||
|
||||
return warnings
|
||||
|
||||
def _prev_install_invalid_config(self, item: MenuItem) -> PreviewResult | None:
|
||||
self.sync_all_to_config()
|
||||
summary = ConfigurationOutput(self._arch_config).as_summary()
|
||||
if summary:
|
||||
return f'{tr("Ready to install")}\n\n{summary}'
|
||||
return tr('Ready to install')
|
||||
config_output = ConfigurationOutput(self._arch_config)
|
||||
|
||||
warnings = self._get_install_warnings()
|
||||
messages: list[tuple[str, MsgLevelType]] = []
|
||||
|
||||
errors = ''
|
||||
if missing := self._missing_configs():
|
||||
errors += f'{tr("Missing configurations:")}\n'
|
||||
errors += '\n'.join(f'- {m}' for m in missing)
|
||||
|
||||
disk_item = self._item_group.find_by_key('disk_config')
|
||||
if disk_item.has_value():
|
||||
if error := self._validate_bootloader():
|
||||
if errors:
|
||||
errors += '\n\n'
|
||||
errors += f'{tr("Invalid configuration:")}\n- {error}'
|
||||
|
||||
if errors:
|
||||
messages.append((errors, MsgLevelType.MsgError))
|
||||
else:
|
||||
messages.append((tr('Ready to install'), MsgLevelType.MsgInfo))
|
||||
|
||||
if warnings:
|
||||
text = f'{tr("Warnings:")}\n' + '\n'.join(f'- {w}' for w in warnings)
|
||||
messages.append((text, MsgLevelType.MsgWarning))
|
||||
|
||||
if not errors:
|
||||
summary = config_output.as_summary()
|
||||
if summary:
|
||||
messages.append((summary, MsgLevelType.MsgNone))
|
||||
|
||||
return PreviewResult(messages)
|
||||
|
||||
def _prev_profile(self, item: MenuItem) -> str | None:
|
||||
profile_config: ProfileConfiguration | None = item.value
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ from pathlib import Path
|
|||
from archinstall.lib.menu.helpers import Confirmation, Input
|
||||
from archinstall.lib.models.users import Password, PasswordStrength
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.components import InputInfo, InputInfoType, tui
|
||||
from archinstall.tui.components import InputInfo, tui
|
||||
from archinstall.tui.menu_item import MsgLevelType
|
||||
from archinstall.tui.result import ResultType
|
||||
|
||||
|
||||
|
|
@ -20,11 +21,11 @@ async def get_password(
|
|||
return None
|
||||
strength = PasswordStrength.strength(value)
|
||||
if strength in (PasswordStrength.VERY_WEAK, PasswordStrength.WEAK):
|
||||
return InputInfo(message=tr('Password strength: Weak'), info_type=InputInfoType.MsgError)
|
||||
return InputInfo(message=tr('Password strength: Weak'), msg_level=MsgLevelType.MsgError)
|
||||
elif strength == PasswordStrength.MODERATE:
|
||||
return InputInfo(message=tr('Password strength: Moderate'), info_type=InputInfoType.MsgWarning)
|
||||
return InputInfo(message=tr('Password strength: Moderate'), msg_level=MsgLevelType.MsgWarning)
|
||||
elif strength == PasswordStrength.STRONG:
|
||||
return InputInfo(message=tr('Password strength: Strong'), info_type=InputInfoType.MsgInfo)
|
||||
return InputInfo(message=tr('Password strength: Strong'), msg_level=MsgLevelType.MsgInfo)
|
||||
return None
|
||||
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from asyncio import sleep
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import assert_never, override
|
||||
|
|
@ -109,36 +108,25 @@ class WifiHandler(InstanceRunnable[bool]):
|
|||
|
||||
debug(f'Found wifi interface: {wifi_iface}')
|
||||
|
||||
async def get_wifi_networks() -> MenuItemGroup:
|
||||
debug('Scanning Wifi networks')
|
||||
result = self._wpa_cli('scan', wifi_iface)
|
||||
wifi_networks = await self._scan_wifi(wifi_iface)
|
||||
|
||||
if not result.success:
|
||||
debug(f'Failed to scan wifi networks: {result.error}')
|
||||
return MenuItemGroup([])
|
||||
if not wifi_networks:
|
||||
debug('No networks found')
|
||||
await NotifyScreen(header=tr('No wifi networks found')).run()
|
||||
tui.exit(Result.false())
|
||||
return False
|
||||
|
||||
await sleep(5)
|
||||
wifi_networks = self._get_scan_results(wifi_iface)
|
||||
|
||||
items = [MenuItem(network.ssid, value=network) for network in wifi_networks]
|
||||
return MenuItemGroup(items)
|
||||
items = [MenuItem(network.ssid, value=network) for network in wifi_networks]
|
||||
|
||||
result = await TableSelectionScreen[WifiNetwork](
|
||||
header=tr('Select wifi network to connect to'),
|
||||
loading_header=tr('Scanning wifi networks...'),
|
||||
group_callback=get_wifi_networks,
|
||||
group=MenuItemGroup(items),
|
||||
allow_skip=True,
|
||||
allow_reset=True,
|
||||
).run()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Selection:
|
||||
if not result.has_data():
|
||||
debug('No networks found')
|
||||
await NotifyScreen(header=tr('No wifi networks found')).run()
|
||||
tui.exit(Result.false())
|
||||
return False
|
||||
|
||||
network = result.get_value()
|
||||
case ResultType.Skip | ResultType.Reset:
|
||||
tui.exit(Result.false())
|
||||
|
|
@ -184,6 +172,18 @@ class WifiHandler(InstanceRunnable[bool]):
|
|||
|
||||
return True
|
||||
|
||||
async def _scan_wifi(self, wifi_iface: str) -> list[WifiNetwork]:
|
||||
debug('Scanning Wifi networks')
|
||||
scan_result = self._wpa_cli('scan', wifi_iface)
|
||||
|
||||
if not scan_result.success:
|
||||
debug(f'Failed to scan wifi networks: {scan_result.error}')
|
||||
return []
|
||||
|
||||
await LoadingScreen(timer=5, header=tr('Scanning wifi networks...')).run()
|
||||
|
||||
return self._get_scan_results(wifi_iface)
|
||||
|
||||
async def _notify_failure(self) -> None:
|
||||
await NotifyScreen(header=tr('Failed setting up wifi')).run()
|
||||
|
||||
|
|
|
|||
|
|
@ -1214,3 +1214,17 @@ msgstr ""
|
|||
|
||||
msgid " (default)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid configuration:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing configurations:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Warnings:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"No network configuration selected. Network will need to be set up manually "
|
||||
"on the installed system."
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None:
|
|||
|
||||
if not arch_config_handler.args.silent:
|
||||
aborted = False
|
||||
res: bool = tui.run(lambda: config.confirm_config(show_install_warnings=True))
|
||||
res: bool = tui.run(config.confirm_config)
|
||||
|
||||
if not res:
|
||||
debug('Installation aborted')
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ async def main(arch_config_handler: ArchConfigHandler | None = None) -> None:
|
|||
|
||||
if not arch_config_handler.args.silent:
|
||||
aborted = False
|
||||
res: bool = tui.run(lambda: config.confirm_config(show_install_warnings=True))
|
||||
res: bool = tui.run(config.confirm_config)
|
||||
|
||||
if not res:
|
||||
debug('Installation aborted')
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import sys
|
|||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import Enum, auto
|
||||
from typing import Any, ClassVar, Literal, TypeVar, cast, override
|
||||
|
||||
from rich.text import Text
|
||||
from textual import work
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding, BindingsMap
|
||||
|
|
@ -21,12 +21,28 @@ from textual.worker import WorkerCancelled
|
|||
|
||||
from archinstall.lib.log import debug
|
||||
from archinstall.lib.translationhandler import tr
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup, MsgLevelType, PreviewResult
|
||||
from archinstall.tui.result import Result, ResultType
|
||||
|
||||
ValueT = TypeVar('ValueT')
|
||||
|
||||
|
||||
def _update_preview(widget: Label, result: str | PreviewResult | None) -> None:
|
||||
if result is None:
|
||||
widget.update('')
|
||||
return
|
||||
|
||||
if isinstance(result, str):
|
||||
widget.update(result)
|
||||
else:
|
||||
text = Text()
|
||||
for i, (message, level) in enumerate(result.messages):
|
||||
if i > 0:
|
||||
text.append('\n\n')
|
||||
text.append(message, style=level.style())
|
||||
widget.update(text)
|
||||
|
||||
|
||||
def _translate_bindings(source: BindingsMap | None, target: BindingsMap) -> None:
|
||||
"""Translate binding descriptions from source to target.
|
||||
|
||||
|
|
@ -361,13 +377,9 @@ class OptionListScreen(BaseScreen[ValueT]):
|
|||
item = self._group.find_by_id(item_id)
|
||||
|
||||
if item.preview_action is not None:
|
||||
maybe_preview = item.preview_action(item)
|
||||
|
||||
if maybe_preview is not None:
|
||||
preview_widget.update(maybe_preview)
|
||||
return
|
||||
|
||||
preview_widget.update('')
|
||||
_update_preview(preview_widget, item.preview_action(item))
|
||||
else:
|
||||
_update_preview(preview_widget, None)
|
||||
|
||||
|
||||
class _SelectionList(SelectionList[ValueT]):
|
||||
|
|
@ -614,12 +626,9 @@ class SelectListScreen(BaseScreen[ValueT]):
|
|||
preview_widget = self.query_one('#preview_content', Label)
|
||||
|
||||
if item.preview_action is not None:
|
||||
maybe_preview = item.preview_action(item)
|
||||
if maybe_preview is not None:
|
||||
preview_widget.update(maybe_preview)
|
||||
return
|
||||
|
||||
preview_widget.update('')
|
||||
_update_preview(preview_widget, item.preview_action(item))
|
||||
else:
|
||||
_update_preview(preview_widget, None)
|
||||
|
||||
|
||||
# DEPRECATED: Removed when switching to async
|
||||
|
|
@ -735,13 +744,8 @@ class ConfirmationScreen(BaseScreen[ValueT]):
|
|||
|
||||
if self._preview_header is not None:
|
||||
preview = self.query_one('#preview_content', Label)
|
||||
|
||||
if focused.preview_action is None:
|
||||
preview.update('')
|
||||
else:
|
||||
text = focused.preview_action(focused)
|
||||
if text is not None:
|
||||
preview.update(text)
|
||||
result = focused.preview_action(focused) if focused.preview_action else None
|
||||
_update_preview(preview, result)
|
||||
else:
|
||||
button.remove_class('-active')
|
||||
|
||||
|
|
@ -768,16 +772,10 @@ class NotifyScreen(ConfirmationScreen[ValueT]):
|
|||
super().__init__(group, header)
|
||||
|
||||
|
||||
class InputInfoType(Enum):
|
||||
MsgInfo = auto()
|
||||
MsgWarning = auto()
|
||||
MsgError = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class InputInfo:
|
||||
message: str
|
||||
info_type: InputInfoType
|
||||
msg_level: MsgLevelType
|
||||
|
||||
|
||||
class InputScreen(BaseScreen[str]):
|
||||
|
|
@ -889,11 +887,11 @@ class InputScreen(BaseScreen[str]):
|
|||
result = self._info_callback(event.value)
|
||||
if result:
|
||||
css_class = ''
|
||||
if result.info_type == InputInfoType.MsgError:
|
||||
if result.msg_level == MsgLevelType.MsgError:
|
||||
css_class = 'input-hint-msg-error'
|
||||
elif result.info_type == InputInfoType.MsgWarning:
|
||||
elif result.msg_level == MsgLevelType.MsgWarning:
|
||||
css_class = 'input-hint-msg-warning'
|
||||
elif result.info_type == InputInfoType.MsgInfo:
|
||||
elif result.msg_level == MsgLevelType.MsgInfo:
|
||||
css_class = 'input-hint-msg-info'
|
||||
info_label.update(result.message)
|
||||
info_label.set_classes(css_class)
|
||||
|
|
@ -1138,13 +1136,7 @@ class TableSelectionScreen(BaseScreen[ValueT]):
|
|||
return
|
||||
|
||||
preview_widget = self.query_one('#preview_content', Label)
|
||||
|
||||
maybe_preview = item.preview_action(item)
|
||||
if maybe_preview is not None:
|
||||
preview_widget.update(maybe_preview)
|
||||
return
|
||||
|
||||
preview_widget.update('')
|
||||
_update_preview(preview_widget, item.preview_action(item))
|
||||
|
||||
def _set_cursor(self, row_index: int) -> None:
|
||||
data_table = self.query_one(DataTable)
|
||||
|
|
@ -1279,6 +1271,7 @@ class _AppInstance(App[ValueT]):
|
|||
background: black;
|
||||
border-left: vkey white 20%;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, main: InstanceRunnable[ValueT] | Callable[[], Awaitable[ValueT]]) -> None:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,44 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from enum import Enum, StrEnum, auto
|
||||
from functools import cached_property
|
||||
from typing import Any, Self, override
|
||||
|
||||
from archinstall.lib.translationhandler import tr
|
||||
|
||||
|
||||
class MsgLevelStyle(StrEnum):
|
||||
White = 'white'
|
||||
Green = 'green'
|
||||
Yellow = 'bright_yellow'
|
||||
Red = 'red'
|
||||
|
||||
|
||||
class MsgLevelType(Enum):
|
||||
MsgNone = auto()
|
||||
MsgInfo = auto()
|
||||
MsgWarning = auto()
|
||||
MsgError = auto()
|
||||
|
||||
def style(self) -> MsgLevelStyle:
|
||||
match self:
|
||||
case MsgLevelType.MsgNone:
|
||||
return MsgLevelStyle.White
|
||||
case MsgLevelType.MsgInfo:
|
||||
return MsgLevelStyle.Green
|
||||
case MsgLevelType.MsgWarning:
|
||||
return MsgLevelStyle.Yellow
|
||||
case MsgLevelType.MsgError:
|
||||
return MsgLevelStyle.Red
|
||||
|
||||
|
||||
@dataclass
|
||||
class PreviewResult:
|
||||
messages: list[tuple[str, MsgLevelType]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MenuItem:
|
||||
text: str
|
||||
|
|
@ -18,7 +50,7 @@ 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[[Self], str | None] | None = None
|
||||
preview_action: Callable[[Self], str | PreviewResult | None] | None = None
|
||||
key: str | None = None
|
||||
|
||||
_id: str = ''
|
||||
|
|
|
|||
Loading…
Reference in New Issue