From 0b235cc8f89d28a25a3d587124a01d2fc8373405 Mon Sep 17 00:00:00 2001 From: Softer Date: Sat, 2 May 2026 01:21:55 +0300 Subject: [PATCH 1/3] 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 3e9135bfb0..6ed9dd7ac7 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()) @@ -156,14 +153,6 @@ def get_install_warnings(self) -> list[str]: 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 cdd820a943..c68b717ede 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 @@ def _get_menu_options(self) -> list[MenuItem]: MenuItem( text=tr('Install'), preview_action=self._prev_install_invalid_config, + preview_markup=True, key=SpecialMenuKey.INSTALL.value, ), MenuItem( @@ -495,20 +498,30 @@ def _validate_bootloader(self) -> str | 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 2122848083..5cdcdad9d2 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 681d4266b0..4e89714260 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 da6dc66b77..405f59af26 100644 --- a/archinstall/tui/ui/components.py +++ b/archinstall/tui/ui/components.py @@ -5,6 +5,7 @@ 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 @@ def compose(self) -> ComposeResult: 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 @@ def _set_preview(self, item_id: str) -> 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 @@ -510,7 +513,7 @@ def compose(self) -> ComposeResult: 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 @@ def _set_preview(self, item: MenuItem) -> None: 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 @@ def compose(self) -> ComposeResult: 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 @@ def _update_selection(self) -> None: 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 @@ def compose(self) -> ComposeResult: 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 @@ def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> 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 diff --git a/archinstall/tui/ui/menu_item.py b/archinstall/tui/ui/menu_item.py index 4c10e275ef..728289b026 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 = '' From 885d1aeef924e5b891ba62b01129afb819a0758c Mon Sep 17 00:00:00 2001 From: Softer Date: Sat, 2 May 2026 13:28:22 +0300 Subject: [PATCH 2/3] Fix Rich markup parsing error on JSON preview strings Text.from_markup() replaces Label(markup=True) to avoid MarkupError on strings containing [" --- archinstall/lib/global_menu.py | 19 +++++++++++++++---- archinstall/tui/ui/components.py | 30 +++++++++++++----------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index c68b717ede..050e915bc5 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -509,14 +509,25 @@ def _prev_install_invalid_config(self, item: MenuItem) -> str | None: warnings_text += f'- {_escape_markup(w)}\n' warnings_text = warnings_text.rstrip('\n') + '[/yellow]' + blocks = '' + if missing := self._missing_configs(): text = f'[red]{_escape_markup(tr("Missing configurations:"))}\n' for m in missing: text += f'- {_escape_markup(m)}\n' - return text.rstrip('\n') + '[/red]' + warnings_text - - if error := self._validate_bootloader(): - return f'[red]{_escape_markup(tr(f"Invalid configuration: {error}"))}[/red]' + warnings_text + blocks += text.rstrip('\n') + '[/red]' + + disk_item = self._item_group.find_by_key('disk_config') + if disk_item.has_value(): + if error := self._validate_bootloader(): + if blocks: + blocks += '\n\n' + text = f'[red]{_escape_markup(tr("Invalid configuration:"))}\n' + text += f'- {_escape_markup(error)}' + blocks += text + '[/red]' + + if blocks: + return blocks + warnings_text summary = config_output.as_summary() if summary: diff --git a/archinstall/tui/ui/components.py b/archinstall/tui/ui/components.py index 405f59af26..07bf9d9042 100644 --- a/archinstall/tui/ui/components.py +++ b/archinstall/tui/ui/components.py @@ -5,7 +5,7 @@ from enum import Enum, auto from typing import Any, ClassVar, Literal, TypeVar, cast, override -from rich.markup import escape as _escape_markup +from rich.text import Text from textual import work from textual.app import App, ComposeResult from textual.binding import Binding, BindingsMap @@ -281,7 +281,7 @@ def compose(self) -> ComposeResult: with Container(): yield option_list yield Rule(orientation=rule_orientation) - yield ScrollableContainer(Label('', id='preview_content', markup=True)) + yield ScrollableContainer(Label('', id='preview_content', markup=False)) if self._filter: yield Input(placeholder='/filter', id='filter-input') @@ -360,9 +360,8 @@ def _set_preview(self, item_id: str) -> 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) + content = Text.from_markup(maybe_preview) if item.preview_markup else maybe_preview + preview_widget.update(content) return preview_widget.update('') @@ -513,7 +512,7 @@ def compose(self) -> ComposeResult: with Container(): yield selection_list yield Rule(orientation=rule_orientation) - yield ScrollableContainer(Label('', id='preview_content', markup=True)) + yield ScrollableContainer(Label('', id='preview_content', markup=False)) if self._filter: yield Input(placeholder='/filter', id='filter-input') @@ -604,9 +603,8 @@ def _set_preview(self, item: MenuItem) -> None: 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) + content = Text.from_markup(maybe_preview) if item.preview_markup else maybe_preview + preview_widget.update(content) return preview_widget.update('') @@ -693,7 +691,7 @@ def compose(self) -> ComposeResult: 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=True)) + yield ScrollableContainer(Label('', id='preview_content', markup=False)) yield Footer() @@ -731,9 +729,8 @@ def _update_selection(self) -> None: else: text = focused.preview_action(focused) if text is not None: - if not focused.preview_markup: - text = _escape_markup(text) - preview.update(text) + content = Text.from_markup(text) if focused.preview_markup else text + preview.update(content) else: button.remove_class('-active') @@ -1023,7 +1020,7 @@ def compose(self) -> ComposeResult: 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=True)) + yield ScrollableContainer(Label('', id='preview_content', markup=False)) yield Footer() @@ -1133,9 +1130,8 @@ def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> 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) + content = Text.from_markup(maybe_preview) if item.preview_markup else maybe_preview + preview_widget.update(content) return preview_widget.update('') From c5d5135c4a33e96d44a13e17300c8c62c492a78b Mon Sep 17 00:00:00 2001 From: Softer Date: Sun, 3 May 2026 04:40:50 +0300 Subject: [PATCH 3/3] Replace raw Rich markup with PreviewResult dataclass for typed preview levels --- archinstall/lib/configuration.py | 2 +- archinstall/lib/global_menu.py | 55 ++++++++++----------- archinstall/lib/menu/util.py | 9 ++-- archinstall/tui/ui/components.py | 85 ++++++++++++++++---------------- archinstall/tui/ui/menu_item.py | 20 ++++++-- 5 files changed, 90 insertions(+), 81 deletions(-) diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index 6ed9dd7ac7..7233a982bf 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -149,7 +149,7 @@ 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 diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 050e915bc5..cea8a2184c 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -1,7 +1,5 @@ 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 @@ -35,7 +33,7 @@ from archinstall.lib.pacman.pacman_menu import PacmanMenu from archinstall.lib.translationhandler import Language, tr, translation_handler from archinstall.tui.ui.components import tui -from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup +from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup, MsgLevelType, PreviewResult class GlobalMenu(AbstractMenu[None]): @@ -185,7 +183,6 @@ def _get_menu_options(self) -> list[MenuItem]: MenuItem( text=tr('Install'), preview_action=self._prev_install_invalid_config, - preview_markup=True, key=SpecialMenuKey.INSTALL.value, ), MenuItem( @@ -497,42 +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() - 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]' - - blocks = '' + sections: list[PreviewResult] = [] + errors = '' if missing := self._missing_configs(): - text = f'[red]{_escape_markup(tr("Missing configurations:"))}\n' - for m in missing: - text += f'- {_escape_markup(m)}\n' - blocks += text.rstrip('\n') + '[/red]' + 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 blocks: - blocks += '\n\n' - text = f'[red]{_escape_markup(tr("Invalid configuration:"))}\n' - text += f'- {_escape_markup(error)}' - blocks += text + '[/red]' - - if blocks: - return blocks + warnings_text - - summary = config_output.as_summary() - if summary: - 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}' + 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 warnings: + text = f'{tr("Warnings:")}\n' + '\n'.join(f'- {w}' for w in warnings) + sections.append(PreviewResult(text, MsgLevelType.MsgWarning)) + + 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 ff3dea4703..5348e049c1 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.ui.components import InputInfo, InputInfoType, tui +from archinstall.tui.ui.components import InputInfo, tui +from archinstall.tui.ui.menu_item import MsgLevelType from archinstall.tui.ui.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/tui/ui/components.py b/archinstall/tui/ui/components.py index 07bf9d9042..0d21c2edf4 100644 --- a/archinstall/tui/ui/components.py +++ b/archinstall/tui/ui/components.py @@ -2,7 +2,6 @@ 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 @@ -22,12 +21,39 @@ from archinstall.lib.output import debug from archinstall.lib.translationhandler import tr -from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup +from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup, MsgLevelType, PreviewResult from archinstall.tui.ui.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. @@ -357,14 +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: - content = Text.from_markup(maybe_preview) if item.preview_markup else maybe_preview - preview_widget.update(content) - return - - preview_widget.update('') + _update_preview(preview_widget, item.preview_action(item)) + else: + _update_preview(preview_widget, None) class _SelectionList(SelectionList[ValueT]): @@ -601,13 +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: - content = Text.from_markup(maybe_preview) if item.preview_markup else maybe_preview - preview_widget.update(content) - return - - preview_widget.update('') + _update_preview(preview_widget, item.preview_action(item)) + else: + _update_preview(preview_widget, None) # DEPRECATED: Removed when switching to async @@ -723,14 +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: - content = Text.from_markup(text) if focused.preview_markup else text - preview.update(content) + result = focused.preview_action(focused) if focused.preview_action else None + _update_preview(preview, result) else: button.remove_class('-active') @@ -757,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]): @@ -878,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) @@ -1127,14 +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: - content = Text.from_markup(maybe_preview) if item.preview_markup else maybe_preview - preview_widget.update(content) - 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) @@ -1271,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/ui/menu_item.py b/archinstall/tui/ui/menu_item.py index 728289b026..0e3552fecb 100644 --- a/archinstall/tui/ui/menu_item.py +++ b/archinstall/tui/ui/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,8 +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_markup: bool = False + preview_action: Callable[[Self], str | PreviewResult | list[PreviewResult] | None] | None = None key: str | None = None _id: str = ''