From 8e3099ec0b4236d40265c8e72823fc2eefa6c9fd Mon Sep 17 00:00:00 2001 From: Piyush Singh <195040717+singhh-piyush@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:01:00 +0200 Subject: [PATCH] fix: reintegrate PasswordStrength into password prompt (#4111) --- archinstall/lib/menu/helpers.py | 5 +++- archinstall/lib/menu/util.py | 17 +++++++++-- archinstall/tui/ui/components.py | 51 ++++++++++++++++++++++++++++++++ tests/test_password_strength.py | 19 ++++++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/test_password_strength.py diff --git a/archinstall/lib/menu/helpers.py b/archinstall/lib/menu/helpers.py index 9b8b0bfb26..9636176598 100644 --- a/archinstall/lib/menu/helpers.py +++ b/archinstall/lib/menu/helpers.py @@ -4,7 +4,7 @@ from textual.validation import ValidationResult, Validator from archinstall.lib.translationhandler import tr -from archinstall.tui.ui.components import InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen +from archinstall.tui.ui.components import InputInfo, InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen from archinstall.tui.ui.menu_item import MenuItemGroup from archinstall.tui.ui.result import Result, ResultType @@ -138,6 +138,7 @@ def __init__( allow_skip: bool = True, allow_reset: bool = False, validator_callback: Callable[[str], str | None] | None = None, + info_callback: Callable[[str], InputInfo | None] | None = None, ): self._header = header self._placeholder = placeholder @@ -146,6 +147,7 @@ def __init__( self._allow_skip = allow_skip self._allow_reset = allow_reset self._validator_callback = validator_callback + self._info_callback = info_callback async def show(self) -> Result[str]: validator = GenericValidator(self._validator_callback) if self._validator_callback else None @@ -158,6 +160,7 @@ async def show(self) -> Result[str]: allow_skip=self._allow_skip, allow_reset=self._allow_reset, validator=validator, + info_callback=self._info_callback, ).run() if result.type_ == ResultType.Reset: diff --git a/archinstall/lib/menu/util.py b/archinstall/lib/menu/util.py index 9daa3a6d77..2fe7c9d352 100644 --- a/archinstall/lib/menu/util.py +++ b/archinstall/lib/menu/util.py @@ -3,9 +3,9 @@ from pathlib import Path from archinstall.lib.menu.helpers import Confirmation, Input -from archinstall.lib.models.users import Password +from archinstall.lib.models.users import Password, PasswordStrength from archinstall.lib.translationhandler import tr -from archinstall.tui.ui.components import tui +from archinstall.tui.ui.components import InputInfo, InputInfoType, tui from archinstall.tui.ui.result import ResultType @@ -15,12 +15,25 @@ async def get_password( preset: str | None = None, skip_confirmation: bool = False, ) -> Password | None: + def password_hint(value: str) -> InputInfo | None: + if not value: + 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) + elif strength == PasswordStrength.MODERATE: + return InputInfo(message=tr('Password strength: Moderate'), info_type=InputInfoType.MsgWarning) + elif strength == PasswordStrength.STRONG: + return InputInfo(message=tr('Password strength: Strong'), info_type=InputInfoType.MsgInfo) + return None + while True: result = await Input( header=header, allow_skip=allow_skip, default_value=preset, password=True, + info_callback=password_hint, ).show() if result.type_ == ResultType.Skip: diff --git a/archinstall/tui/ui/components.py b/archinstall/tui/ui/components.py index 7e453dcb12..6c78b7547c 100644 --- a/archinstall/tui/ui/components.py +++ b/archinstall/tui/ui/components.py @@ -1,6 +1,8 @@ import sys from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from enum import Enum, auto from typing import Any, ClassVar, Literal, TypeVar, cast, override from textual import work @@ -707,6 +709,18 @@ 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 + + class InputScreen(BaseScreen[str]): CSS = """ InputScreen { @@ -728,6 +742,22 @@ class InputScreen(BaseScreen[str]): color: red; text-align: center; } + + #input-info { + text-align: center; + } + + .input-hint-msg-error { + color: red; + } + + .input-hint-msg-warning { + color: yellow; + } + + .input-hint-msg-info { + color: green; + } """ def __init__( @@ -739,6 +769,7 @@ def __init__( allow_reset: bool = False, allow_skip: bool = False, validator: Validator | None = None, + info_callback: Callable[[str], InputInfo | None] | None = None, ): super().__init__(allow_skip, allow_reset) self._header = header or '' @@ -748,6 +779,7 @@ def __init__( self._allow_reset = allow_reset self._allow_skip = allow_skip self._validator = validator + self._info_callback = info_callback async def run(self) -> Result[str]: assert TApp.app @@ -768,6 +800,7 @@ def compose(self) -> ComposeResult: validate_on=['submitted'], ) yield Label('', classes='input-failure', id='input-failure') + yield Label('', id='input-info') yield Footer() @@ -784,6 +817,24 @@ def on_input_submitted(self, event: Input.Submitted) -> None: else: _ = self.dismiss(Result(ResultType.Selection, _data=event.value)) + def on_input_changed(self, event: Input.Changed) -> None: + info_label = self.query_one('#input-info', Label) + if self._info_callback: + result = self._info_callback(event.value) + if result: + css_class = '' + if result.info_type == InputInfoType.MsgError: + css_class = 'input-hint-msg-error' + elif result.info_type == InputInfoType.MsgWarning: + css_class = 'input-hint-msg-warning' + elif result.info_type == InputInfoType.MsgInfo: + css_class = 'input-hint-msg-info' + info_label.update(result.message) + info_label.set_classes(css_class) + else: + info_label.update('') + info_label.set_classes('') + class _DataTable(DataTable[ValueT]): BINDINGS: ClassVar = [ diff --git a/tests/test_password_strength.py b/tests/test_password_strength.py new file mode 100644 index 0000000000..f3f76caa5a --- /dev/null +++ b/tests/test_password_strength.py @@ -0,0 +1,19 @@ +import pytest + +from archinstall.lib.models.users import PasswordStrength + + +@pytest.mark.parametrize( + 'password, expected', + [ + ('abc', PasswordStrength.VERY_WEAK), + ('Abcdef1!', PasswordStrength.WEAK), + ('Abcdef1234!', PasswordStrength.MODERATE), + ('Abcdef12345!@', PasswordStrength.STRONG), + ('', PasswordStrength.VERY_WEAK), + ('123456789', PasswordStrength.VERY_WEAK), + ('abcdefghijklmnopqr', PasswordStrength.STRONG), + ], +) +def test_password_strength(password: str, expected: PasswordStrength) -> None: + assert PasswordStrength.strength(password) == expected