diff --git a/archinstall/lib/menu/helpers.py b/archinstall/lib/menu/helpers.py index 3b91c6218c..c1bdec62ef 100644 --- a/archinstall/lib/menu/helpers.py +++ b/archinstall/lib/menu/helpers.py @@ -152,6 +152,7 @@ def __init__( allow_skip: bool = True, allow_reset: bool = False, validator_callback: Callable[[str], str | None] | None = None, + info_callback: Callable[[str], tuple[str, str] | None] | None = None, ): self._header = header self._placeholder = placeholder @@ -160,6 +161,7 @@ def __init__( self._allow_skip = allow_skip self._allow_reset = allow_reset self._validator_callback = validator_callback + self._info_callback = info_callback def show(self) -> Result[str]: result: Result[str] = tui.run(self) @@ -176,6 +178,7 @@ async def _run(self) -> None: 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 78434ab592..cf85d37d72 100644 --- a/archinstall/lib/menu/util.py +++ b/archinstall/lib/menu/util.py @@ -1,7 +1,7 @@ from pathlib import Path from archinstall.lib.menu.helpers import 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.result import ResultType @@ -12,12 +12,26 @@ def get_password( preset: str | None = None, skip_confirmation: bool = False, ) -> Password | None: + def password_hint(value: str) -> tuple[str, str] | None: + if not value: + return None + strength = PasswordStrength.strength(value) + if strength == PasswordStrength.VERY_WEAK: + return (tr('Too weak - too short'), 'input-hint-weak') + elif strength == PasswordStrength.WEAK: + return (tr('Weak - add uppercase, lowercase, numbers and symbols'), 'input-hint-weak') + elif strength == PasswordStrength.MODERATE: + return (tr('Moderate - increase length'), 'input-hint-moderate') + elif strength == PasswordStrength.STRONG: + return (tr('Strong'), 'input-hint-strong') + while True: result = Input( header=header, allow_skip=allow_skip, default_value=preset, password=True, + info_callback=password_hint, ).show() if result.type_ == ResultType.Skip: @@ -33,6 +47,7 @@ def get_password( continue password = Password(plaintext=result.get_value()) + break if skip_confirmation: diff --git a/archinstall/tui/ui/components.py b/archinstall/tui/ui/components.py index 5c9cb5bcee..76af647b15 100644 --- a/archinstall/tui/ui/components.py +++ b/archinstall/tui/ui/components.py @@ -727,6 +727,22 @@ class InputScreen(BaseScreen[str]): color: red; text-align: center; } + + #input-info { + text-align: center; + } + + .input-hint-weak { + color: red; + } + + .input-hint-moderate { + color: yellow; + } + + .input-hint-strong { + color: green; + } """ def __init__( @@ -738,6 +754,7 @@ def __init__( allow_reset: bool = False, allow_skip: bool = False, validator: Validator | None = None, + info_callback: Callable[[str], tuple[str, str] | None] | None = None, ): super().__init__(allow_skip, allow_reset) self._header = header or '' @@ -747,6 +764,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 @@ -767,6 +785,7 @@ def compose(self) -> ComposeResult: validate_on=['submitted'], ) yield Label('', classes='input-failure', id='input-failure') + yield Label('', id='input-info') yield Footer() @@ -783,6 +802,18 @@ 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: + info_msg, info_class = result + info_label.update(info_msg) + info_label.set_classes(info_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