Skip to content
Merged
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
22 changes: 1 addition & 21 deletions archinstall/lib/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from archinstall.lib.log import debug, logger, warn
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password, prompt_dir
from archinstall.lib.models.network import NetworkConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_key_value_pair
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
Expand Down Expand Up @@ -83,13 +82,10 @@ def as_summary(self) -> str:

return simple_summary

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 @@ -107,22 +103,6 @@ async def confirm_config(self, show_install_warnings: bool = False) -> bool:

return True

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.'))

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
53 changes: 40 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.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_table
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,48 @@ def _validate_bootloader(self) -> str | None:

return None

def _prev_install_invalid_config(self, item: MenuItem) -> str | None:
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
def _get_install_warnings(self) -> list[str]:
warnings: list[str] = []

if not isinstance(self._arch_config.network_config, NetworkConfiguration):
warnings.append(tr('No network configuration selected. Network will need to be set up manually on the installed system.'))

if error := self._validate_bootloader():
return tr('Invalid configuration: {}').format(error)
return warnings

def _prev_install_invalid_config(self, item: MenuItem) -> PreviewResult | None:
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')
config_output = ConfigurationOutput(self._arch_config)

warnings = self._get_install_warnings()
messages: list[tuple[str, MsgLevelType]] = []

errors = ''
if missing := self._missing_configs():
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():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the same logic as the existing code, if there is a bug it'd be good to make these changes incremental to understand why they have changed

if error := self._validate_bootloader():
if errors:
errors += '\n\n'
errors += f'{tr("Invalid configuration:")}\n- {error}'

if errors:
messages.append((errors, MsgLevelType.MsgError))
else:
messages.append((tr('Ready to install'), MsgLevelType.MsgInfo))

if warnings:
text = f'{tr("Warnings:")}\n' + '\n'.join(f'- {w}' for w in warnings)
messages.append((text, MsgLevelType.MsgWarning))

if not errors:
summary = config_output.as_summary()
if summary:
messages.append((summary, MsgLevelType.MsgNone))

return PreviewResult(messages)

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
14 changes: 14 additions & 0 deletions archinstall/locales/base.pot
Original file line number Diff line number Diff line change
Expand Up @@ -2463,3 +2463,17 @@ msgstr ""

msgid "Enter a repository name"
msgstr ""

msgid "Invalid configuration:"
msgstr ""

msgid "Missing configurations:"
msgstr ""

msgid "Warnings:"
msgstr ""

msgid ""
"No network configuration selected. Network will need to be set up manually "
"on the installed system."
msgstr ""
2 changes: 1 addition & 1 deletion archinstall/scripts/guided.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,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
71 changes: 32 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,28 @@

from archinstall.lib.log 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')


def _update_preview(widget: Label, result: str | PreviewResult | None) -> None:
if result is None:
widget.update('')
return

if isinstance(result, str):
widget.update(result)
else:
text = Text()
for i, (message, level) in enumerate(result.messages):
if i > 0:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we throwing away the first entry?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing is dropped. The if i > 0 only skips the separator before the first block - text.append(message, ...) runs on every iteration, including i == 0. The first message has no leading blank line; the rest get one as a separator between blocks.

text.append('\n\n')
text.append(message, style=level.style())
widget.update(text)


def _translate_bindings(source: BindingsMap | None, target: BindingsMap) -> None:
"""Translate binding descriptions from source to target.

Expand Down Expand Up @@ -361,13 +377,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 @@ -614,12 +626,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 @@ -735,13 +744,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 @@ -768,16 +772,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 @@ -889,11 +887,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 @@ -1138,13 +1136,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 @@ -1279,6 +1271,7 @@ class _AppInstance(App[ValueT]):
background: black;
border-left: vkey white 20%;
}

"""

def __init__(self, main: InstanceRunnable[ValueT] | Callable[[], Awaitable[ValueT]]) -> None:
Expand Down
36 changes: 34 additions & 2 deletions archinstall/tui/menu_item.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
from __future__ import annotations

from collections.abc import Awaitable, Callable, Iterable
from dataclasses import dataclass, field
from enum import Enum
from enum import Enum, StrEnum, auto
from functools import cached_property
from typing import Any, ClassVar, Self, override

from archinstall.lib.translationhandler import tr


class MsgLevelStyle(StrEnum):
White = 'white'
Green = 'green'
Yellow = 'bright_yellow'
Red = 'red'


class MsgLevelType(Enum):
MsgNone = auto()
MsgInfo = auto()
MsgWarning = auto()
MsgError = auto()

def style(self) -> MsgLevelStyle:
match self:
case MsgLevelType.MsgNone:
return MsgLevelStyle.White
case MsgLevelType.MsgInfo:
return MsgLevelStyle.Green
case MsgLevelType.MsgWarning:
return MsgLevelStyle.Yellow
case MsgLevelType.MsgError:
return MsgLevelStyle.Red


@dataclass
class PreviewResult:
messages: list[tuple[str, MsgLevelType]]


@dataclass
class MenuItem:
text: str
Expand All @@ -18,7 +50,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 | None] | None = None
key: str | None = None

_id: str = ''
Expand Down