Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions archinstall/lib/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand All @@ -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:
Expand Down
45 changes: 32 additions & 13 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions archinstall/lib/menu/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion archinstall/scripts/guided.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion archinstall/scripts/minimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
82 changes: 43 additions & 39 deletions archinstall/tui/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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]):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')

Expand All @@ -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]):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
19 changes: 17 additions & 2 deletions archinstall/tui/menu_item.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 = ''
Expand Down