Replace raw Rich markup with PreviewResult dataclass for typed preview levels

This commit is contained in:
Softer 2026-05-03 04:40:50 +03:00
parent 885d1aeef9
commit c5d5135c4a
5 changed files with 88 additions and 79 deletions

View File

@ -149,7 +149,7 @@ class ConfigurationOutput:
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

View File

@ -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.config import PacmanConfig
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 @@ class GlobalMenu(AbstractMenu[None]):
MenuItem(
text=tr('Install'),
preview_action=self._prev_install_invalid_config,
preview_markup=True,
key=SpecialMenuKey.INSTALL.value,
),
MenuItem(
@ -497,42 +494,40 @@ class GlobalMenu(AbstractMenu[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 errors:
errors += '\n\n'
errors += f'{tr("Invalid configuration:")}\n- {error}'
if blocks:
return blocks + warnings_text
if errors:
sections.append(PreviewResult(errors, MsgLevelType.MsgError))
else:
sections.append(PreviewResult(tr('Ready to install'), MsgLevelType.MsgInfo))
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 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

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

@ -2,7 +2,6 @@ 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
@ -22,12 +21,39 @@ from textual.worker import WorkerCancelled
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 @@ 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:
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 @@ 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:
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 @@ 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:
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 @@ 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]):
@ -878,11 +883,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)
@ -1127,14 +1132,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:
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:

View File

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