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:
Softer 2026-06-08 15:53:30 +03:00
commit 6b3b6e3f7d
9 changed files with 148 additions and 101 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = ''