diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index aba835d983..23075eafc6 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -124,13 +124,10 @@ def as_summary(self) -> str: label_width = max(len(label) for label, _ in rows) + 2 return '\n'.join(f'{label:<{label_width}}{value}' for label, value in rows) - 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()) @@ -152,18 +149,10 @@ 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.')) + warnings.append(tr('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 eac936bdd0..9e544ffd43 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -33,7 +33,7 @@ from archinstall.lib.pacman.pacman_menu import PacmanMenu from archinstall.lib.translationhandler import Language, tr, translation_handler 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,40 @@ def _validate_bootloader(self) -> str | None: return None - def _prev_install_invalid_config(self, item: MenuItem) -> str | None: + def _prev_install_invalid_config(self, item: MenuItem) -> str | PreviewResult | list[PreviewResult] | None: + self.sync_all_to_config() + config_output = ConfigurationOutput(self._arch_config) + + warnings = config_output.get_install_warnings() + sections: list[PreviewResult] = [] + + errors = '' 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 + 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: + sections.append(PreviewResult(errors, MsgLevelType.MsgError)) + else: + sections.append(PreviewResult(tr('Ready to install'), MsgLevelType.MsgInfo)) - if error := self._validate_bootloader(): - return tr(f'Invalid configuration: {error}') + if warnings: + text = f'{tr("Warnings:")}\n' + '\n'.join(f'- {w}' for w in warnings) + sections.append(PreviewResult(text, MsgLevelType.MsgWarning)) - 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') + if not errors: + summary = config_output.as_summary() + if summary: + sections.append(PreviewResult(summary, MsgLevelType.MsgNone)) + + return sections 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/scripts/guided.py b/archinstall/scripts/guided.py index 899683f26b..57ff87b31a 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -226,7 +226,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 ee4b49c70d..3d41386bd3 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 f92364bbdd..1a4ad60e55 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,39 @@ from archinstall.lib.output 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') +_LEVEL_STYLE: dict[MsgLevelType, str] = { + MsgLevelType.MsgNone: '', + MsgLevelType.MsgError: 'red', + MsgLevelType.MsgWarning: 'bright_yellow', + MsgLevelType.MsgInfo: 'green', +} + + +def _update_preview(widget: Label, result: str | PreviewResult | list[PreviewResult] | None) -> None: + if result is None: + widget.update('') + return + + if isinstance(result, str): + widget.update(result) + elif isinstance(result, PreviewResult): + text = Text(result.message, style=_LEVEL_STYLE[result.msg_level]) + widget.update(text) + else: + text = Text() + for i, section in enumerate(result): + if i > 0: + text.append('\n\n') + text.append(section.message, style=_LEVEL_STYLE[section.msg_level]) + widget.update(text) + + def _translate_bindings(source: BindingsMap | None, target: BindingsMap) -> None: """Translate binding descriptions from source to target. @@ -356,13 +383,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]): @@ -599,12 +622,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 @@ -720,13 +740,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') @@ -753,16 +768,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]): @@ -874,11 +883,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) @@ -1123,13 +1132,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) @@ -1266,6 +1269,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..0e3552fecb 100644 --- a/archinstall/tui/menu_item.py +++ b/archinstall/tui/menu_item.py @@ -1,12 +1,27 @@ +from __future__ import annotations + from collections.abc import Awaitable, Callable, Iterable from dataclasses import dataclass, field -from enum import Enum +from enum import Enum, auto from functools import cached_property from typing import Any, ClassVar, Self, override from archinstall.lib.translationhandler import tr +class MsgLevelType(Enum): + MsgNone = auto() + MsgInfo = auto() + MsgWarning = auto() + MsgError = auto() + + +@dataclass +class PreviewResult: + message: str + msg_level: MsgLevelType + + @dataclass class MenuItem: text: str @@ -18,7 +33,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 | list[PreviewResult] | None] | None = None key: str | None = None _id: str = ''