From 0b235cc8f89d28a25a3d587124a01d2fc8373405 Mon Sep 17 00:00:00 2001 From: Softer Date: Sat, 2 May 2026 01:21:55 +0300 Subject: [PATCH] Color-code install preview: red for errors, yellow for warnings, green for ready Add preview_markup opt-in field to MenuItem with automatic Rich markup escaping for all existing previews. Show missing configs and bootloader errors in red, network warning in yellow, "Ready to install" in green. Move network warning from confirmation dialog to install preview so it is visible earlier. --- archinstall/lib/configuration.py | 13 +------------ archinstall/lib/global_menu.py | 29 +++++++++++++++++++++-------- archinstall/scripts/guided.py | 2 +- archinstall/scripts/minimal.py | 2 +- archinstall/tui/ui/components.py | 17 +++++++++++++---- archinstall/tui/ui/menu_item.py | 1 + 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index 3e9135bf..6ed9dd7a 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -124,13 +124,10 @@ class ConfigurationOutput: 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()) @@ -156,14 +153,6 @@ class ConfigurationOutput: 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 cdd820a9..c68b717e 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -1,5 +1,7 @@ from typing import override +from rich.markup import escape as _escape_markup + from archinstall.default_profiles.profile import GreeterType from archinstall.lib.applications.application_menu import ApplicationMenu from archinstall.lib.args import ArchConfig @@ -183,6 +185,7 @@ class GlobalMenu(AbstractMenu[None]): MenuItem( text=tr('Install'), preview_action=self._prev_install_invalid_config, + preview_markup=True, key=SpecialMenuKey.INSTALL.value, ), MenuItem( @@ -495,20 +498,30 @@ class GlobalMenu(AbstractMenu[None]): return None def _prev_install_invalid_config(self, item: MenuItem) -> str | None: + self.sync_all_to_config() + config_output = ConfigurationOutput(self._arch_config) + + warnings = config_output.get_install_warnings() + warnings_text = '' + if warnings: + warnings_text = f'\n\n[yellow]{_escape_markup(tr("Warnings:"))}\n' + for w in warnings: + warnings_text += f'- {_escape_markup(w)}\n' + warnings_text = warnings_text.rstrip('\n') + '[/yellow]' + if missing := self._missing_configs(): - text = tr('Missing configurations:\n') + text = f'[red]{_escape_markup(tr("Missing configurations:"))}\n' for m in missing: - text += f'- {m}\n' - return text[:-1] # remove last new line + text += f'- {_escape_markup(m)}\n' + return text.rstrip('\n') + '[/red]' + warnings_text if error := self._validate_bootloader(): - return tr(f'Invalid configuration: {error}') + return f'[red]{_escape_markup(tr(f"Invalid configuration: {error}"))}[/red]' + warnings_text - self.sync_all_to_config() - summary = ConfigurationOutput(self._arch_config).as_summary() + summary = config_output.as_summary() if summary: - return f'{tr("Ready to install")}\n\n{summary}' - return tr('Ready to install') + return f'[green]{_escape_markup(tr("Ready to install"))}[/green]{warnings_text}\n\n{_escape_markup(summary)}' + return f'[green]{_escape_markup(tr("Ready to install"))}[/green]{warnings_text}' def _prev_profile(self, item: MenuItem) -> str | None: profile_config: ProfileConfiguration | None = item.value diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 21228480..5cdcdad9 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 681d4266..4e897142 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/ui/components.py b/archinstall/tui/ui/components.py index da6dc66b..405f59af 100644 --- a/archinstall/tui/ui/components.py +++ b/archinstall/tui/ui/components.py @@ -5,6 +5,7 @@ from dataclasses import dataclass, replace from enum import Enum, auto from typing import Any, ClassVar, Literal, TypeVar, cast, override +from rich.markup import escape as _escape_markup from textual import work from textual.app import App, ComposeResult from textual.binding import Binding, BindingsMap @@ -280,7 +281,7 @@ class OptionListScreen(BaseScreen[ValueT]): with Container(): yield option_list yield Rule(orientation=rule_orientation) - yield ScrollableContainer(Label('', id='preview_content', markup=False)) + yield ScrollableContainer(Label('', id='preview_content', markup=True)) if self._filter: yield Input(placeholder='/filter', id='filter-input') @@ -359,6 +360,8 @@ class OptionListScreen(BaseScreen[ValueT]): maybe_preview = item.preview_action(item) if maybe_preview is not None: + if not item.preview_markup: + maybe_preview = _escape_markup(maybe_preview) preview_widget.update(maybe_preview) return @@ -510,7 +513,7 @@ class SelectListScreen(BaseScreen[ValueT]): with Container(): yield selection_list yield Rule(orientation=rule_orientation) - yield ScrollableContainer(Label('', id='preview_content', markup=False)) + yield ScrollableContainer(Label('', id='preview_content', markup=True)) if self._filter: yield Input(placeholder='/filter', id='filter-input') @@ -601,6 +604,8 @@ class SelectListScreen(BaseScreen[ValueT]): if item.preview_action is not None: maybe_preview = item.preview_action(item) if maybe_preview is not None: + if not item.preview_markup: + maybe_preview = _escape_markup(maybe_preview) preview_widget.update(maybe_preview) return @@ -688,7 +693,7 @@ class ConfirmationScreen(BaseScreen[ValueT]): yield Rule(orientation='horizontal') if self._preview_header is not None: yield Label(self._preview_header, classes='preview-header', id='preview_header') - yield ScrollableContainer(Label('', id='preview_content', markup=False)) + yield ScrollableContainer(Label('', id='preview_content', markup=True)) yield Footer() @@ -726,6 +731,8 @@ class ConfirmationScreen(BaseScreen[ValueT]): else: text = focused.preview_action(focused) if text is not None: + if not focused.preview_markup: + text = _escape_markup(text) preview.update(text) else: button.remove_class('-active') @@ -1016,7 +1023,7 @@ class TableSelectionScreen(BaseScreen[ValueT]): yield Rule(orientation='horizontal') if self._preview_header is not None: yield Label(self._preview_header, classes='preview-header', id='preview-header') - yield ScrollableContainer(Label('', id='preview_content', markup=False)) + yield ScrollableContainer(Label('', id='preview_content', markup=True)) yield Footer() @@ -1126,6 +1133,8 @@ class TableSelectionScreen(BaseScreen[ValueT]): maybe_preview = item.preview_action(item) if maybe_preview is not None: + if not item.preview_markup: + maybe_preview = _escape_markup(maybe_preview) preview_widget.update(maybe_preview) return diff --git a/archinstall/tui/ui/menu_item.py b/archinstall/tui/ui/menu_item.py index 4c10e275..728289b0 100644 --- a/archinstall/tui/ui/menu_item.py +++ b/archinstall/tui/ui/menu_item.py @@ -19,6 +19,7 @@ class MenuItem: dependencies_not: list[str] = field(default_factory=list) display_action: Callable[[Any], str] | None = None preview_action: Callable[[Self], str | None] | None = None + preview_markup: bool = False key: str | None = None _id: str = ''