Skip to content

Commit e74a72f

Browse files
fix: reintegrate PasswordStrength into password prompt (#4111) (#4291)
1 parent b186fb3 commit e74a72f

4 files changed

Lines changed: 89 additions & 3 deletions

File tree

archinstall/lib/menu/helpers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from textual.validation import ValidationResult, Validator
55

66
from archinstall.lib.translationhandler import tr
7-
from archinstall.tui.ui.components import InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen
7+
from archinstall.tui.ui.components import InputInfo, InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen
88
from archinstall.tui.ui.menu_item import MenuItemGroup
99
from archinstall.tui.ui.result import Result, ResultType
1010

@@ -138,6 +138,7 @@ def __init__(
138138
allow_skip: bool = True,
139139
allow_reset: bool = False,
140140
validator_callback: Callable[[str], str | None] | None = None,
141+
info_callback: Callable[[str], InputInfo | None] | None = None,
141142
):
142143
self._header = header
143144
self._placeholder = placeholder
@@ -146,6 +147,7 @@ def __init__(
146147
self._allow_skip = allow_skip
147148
self._allow_reset = allow_reset
148149
self._validator_callback = validator_callback
150+
self._info_callback = info_callback
149151

150152
async def show(self) -> Result[str]:
151153
validator = GenericValidator(self._validator_callback) if self._validator_callback else None
@@ -158,6 +160,7 @@ async def show(self) -> Result[str]:
158160
allow_skip=self._allow_skip,
159161
allow_reset=self._allow_reset,
160162
validator=validator,
163+
info_callback=self._info_callback,
161164
).run()
162165

163166
if result.type_ == ResultType.Reset:

archinstall/lib/menu/util.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
from pathlib import Path
44

55
from archinstall.lib.menu.helpers import Confirmation, Input
6-
from archinstall.lib.models.users import Password
6+
from archinstall.lib.models.users import Password, PasswordStrength
77
from archinstall.lib.translationhandler import tr
8-
from archinstall.tui.ui.components import tui
8+
from archinstall.tui.ui.components import InputInfo, InputInfoType, tui
99
from archinstall.tui.ui.result import ResultType
1010

1111

@@ -15,12 +15,25 @@ async def get_password(
1515
preset: str | None = None,
1616
skip_confirmation: bool = False,
1717
) -> Password | None:
18+
def password_hint(value: str) -> InputInfo | None:
19+
if not value:
20+
return None
21+
strength = PasswordStrength.strength(value)
22+
if strength in (PasswordStrength.VERY_WEAK, PasswordStrength.WEAK):
23+
return InputInfo(message=tr('Password strength: Weak'), info_type=InputInfoType.MsgError)
24+
elif strength == PasswordStrength.MODERATE:
25+
return InputInfo(message=tr('Password strength: Moderate'), info_type=InputInfoType.MsgWarning)
26+
elif strength == PasswordStrength.STRONG:
27+
return InputInfo(message=tr('Password strength: Strong'), info_type=InputInfoType.MsgInfo)
28+
return None
29+
1830
while True:
1931
result = await Input(
2032
header=header,
2133
allow_skip=allow_skip,
2234
default_value=preset,
2335
password=True,
36+
info_callback=password_hint,
2437
).show()
2538

2639
if result.type_ == ResultType.Skip:

archinstall/tui/ui/components.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import sys
22
from abc import ABC, abstractmethod
33
from collections.abc import Awaitable, Callable
4+
from dataclasses import dataclass
5+
from enum import Enum, auto
46
from typing import Any, ClassVar, Literal, TypeVar, cast, override
57

68
from textual import work
@@ -707,6 +709,18 @@ def __init__(self, header: str):
707709
super().__init__(group, header)
708710

709711

712+
class InputInfoType(Enum):
713+
MsgInfo = auto()
714+
MsgWarning = auto()
715+
MsgError = auto()
716+
717+
718+
@dataclass
719+
class InputInfo:
720+
message: str
721+
info_type: InputInfoType
722+
723+
710724
class InputScreen(BaseScreen[str]):
711725
CSS = """
712726
InputScreen {
@@ -728,6 +742,22 @@ class InputScreen(BaseScreen[str]):
728742
color: red;
729743
text-align: center;
730744
}
745+
746+
#input-info {
747+
text-align: center;
748+
}
749+
750+
.input-hint-msg-error {
751+
color: red;
752+
}
753+
754+
.input-hint-msg-warning {
755+
color: yellow;
756+
}
757+
758+
.input-hint-msg-info {
759+
color: green;
760+
}
731761
"""
732762

733763
def __init__(
@@ -739,6 +769,7 @@ def __init__(
739769
allow_reset: bool = False,
740770
allow_skip: bool = False,
741771
validator: Validator | None = None,
772+
info_callback: Callable[[str], InputInfo | None] | None = None,
742773
):
743774
super().__init__(allow_skip, allow_reset)
744775
self._header = header or ''
@@ -748,6 +779,7 @@ def __init__(
748779
self._allow_reset = allow_reset
749780
self._allow_skip = allow_skip
750781
self._validator = validator
782+
self._info_callback = info_callback
751783

752784
async def run(self) -> Result[str]:
753785
assert TApp.app
@@ -768,6 +800,7 @@ def compose(self) -> ComposeResult:
768800
validate_on=['submitted'],
769801
)
770802
yield Label('', classes='input-failure', id='input-failure')
803+
yield Label('', id='input-info')
771804

772805
yield Footer()
773806

@@ -784,6 +817,24 @@ def on_input_submitted(self, event: Input.Submitted) -> None:
784817
else:
785818
_ = self.dismiss(Result(ResultType.Selection, _data=event.value))
786819

820+
def on_input_changed(self, event: Input.Changed) -> None:
821+
info_label = self.query_one('#input-info', Label)
822+
if self._info_callback:
823+
result = self._info_callback(event.value)
824+
if result:
825+
css_class = ''
826+
if result.info_type == InputInfoType.MsgError:
827+
css_class = 'input-hint-msg-error'
828+
elif result.info_type == InputInfoType.MsgWarning:
829+
css_class = 'input-hint-msg-warning'
830+
elif result.info_type == InputInfoType.MsgInfo:
831+
css_class = 'input-hint-msg-info'
832+
info_label.update(result.message)
833+
info_label.set_classes(css_class)
834+
else:
835+
info_label.update('')
836+
info_label.set_classes('')
837+
787838

788839
class _DataTable(DataTable[ValueT]):
789840
BINDINGS: ClassVar = [

tests/test_password_strength.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
3+
from archinstall.lib.models.users import PasswordStrength
4+
5+
6+
@pytest.mark.parametrize(
7+
'password, expected',
8+
[
9+
('abc', PasswordStrength.VERY_WEAK),
10+
('Abcdef1!', PasswordStrength.WEAK),
11+
('Abcdef1234!', PasswordStrength.MODERATE),
12+
('Abcdef12345!@', PasswordStrength.STRONG),
13+
('', PasswordStrength.VERY_WEAK),
14+
('123456789', PasswordStrength.VERY_WEAK),
15+
('abcdefghijklmnopqr', PasswordStrength.STRONG),
16+
],
17+
)
18+
def test_password_strength(password: str, expected: PasswordStrength) -> None:
19+
assert PasswordStrength.strength(password) == expected

0 commit comments

Comments
 (0)