diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index f009687296..5754dfcf12 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -11,7 +11,6 @@ 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 @@ def as_summary(self) -> str: 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 @@ async def confirm_config(self, show_install_warnings: bool = False) -> bool: 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: diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 45a0e3b473..548ed522de 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -33,7 +33,7 @@ from archinstall.lib.translationhandler import 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 class GlobalMenu(AbstractMenu[None]): @@ -494,21 +494,48 @@ def _validate_bootloader(self) -> str | 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 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.')) - if error := self._validate_bootloader(): - return tr('Invalid configuration: {}').format(error) + 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 diff --git a/archinstall/lib/menu/util.py b/archinstall/lib/menu/util.py index 10edfd490f..5efef09d12 100644 --- a/archinstall/lib/menu/util.py +++ b/archinstall/lib/menu/util.py @@ -5,7 +5,8 @@ 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 @@ def password_hint(value: str) -> InputInfo | None: 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: diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index 4db4554c08..8058e7ab8a 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -2463,3 +2463,17 @@ msgstr "" msgid "Enter a repository name" 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 "" diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 6f484910de..29573a5f1f 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -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') diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index c6eda03db8..a58963f215 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -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') diff --git a/archinstall/tui/components.py b/archinstall/tui/components.py index efbb64f92a..a2e6b1322e 100644 --- a/archinstall/tui/components.py +++ b/archinstall/tui/components.py @@ -2,9 +2,9 @@ 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 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 @@ def _set_preview(self, item_id: str) -> None: 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 @@ def _set_preview(self, item: MenuItem) -> None: 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 @@ def _update_selection(self) -> None: 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 @@ def __init__(self, header: str): 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 @@ def on_input_changed(self, event: Input.Changed) -> None: 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 @@ def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None 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: diff --git a/archinstall/tui/menu_item.py b/archinstall/tui/menu_item.py index 4c10e275ef..13f9f16516 100644 --- a/archinstall/tui/menu_item.py +++ b/archinstall/tui/menu_item.py @@ -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, ClassVar, 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 = ''