fix: reintegrate PasswordStrength into password prompt (#4111) (#4291)

This commit is contained in:
Piyush Singh 2026-03-18 13:48:05 +02:00 committed by GitHub
parent b186fb3f64
commit e74a72f482
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 3 deletions

View File

@ -4,7 +4,7 @@ from typing import Any, Literal, override
from textual.validation import ValidationResult, Validator from textual.validation import ValidationResult, Validator
from archinstall.lib.translationhandler import tr 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.menu_item import MenuItemGroup
from archinstall.tui.ui.result import Result, ResultType from archinstall.tui.ui.result import Result, ResultType
@ -138,6 +138,7 @@ class Input:
allow_skip: bool = True, allow_skip: bool = True,
allow_reset: bool = False, allow_reset: bool = False,
validator_callback: Callable[[str], str | None] | None = None, validator_callback: Callable[[str], str | None] | None = None,
info_callback: Callable[[str], InputInfo | None] | None = None,
): ):
self._header = header self._header = header
self._placeholder = placeholder self._placeholder = placeholder
@ -146,6 +147,7 @@ class Input:
self._allow_skip = allow_skip self._allow_skip = allow_skip
self._allow_reset = allow_reset self._allow_reset = allow_reset
self._validator_callback = validator_callback self._validator_callback = validator_callback
self._info_callback = info_callback
async def show(self) -> Result[str]: async def show(self) -> Result[str]:
validator = GenericValidator(self._validator_callback) if self._validator_callback else None validator = GenericValidator(self._validator_callback) if self._validator_callback else None
@ -158,6 +160,7 @@ class Input:
allow_skip=self._allow_skip, allow_skip=self._allow_skip,
allow_reset=self._allow_reset, allow_reset=self._allow_reset,
validator=validator, validator=validator,
info_callback=self._info_callback,
).run() ).run()
if result.type_ == ResultType.Reset: if result.type_ == ResultType.Reset:

View File

@ -3,9 +3,9 @@ import time
from pathlib import Path from pathlib import Path
from archinstall.lib.menu.helpers import Confirmation, Input 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.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 from archinstall.tui.ui.result import ResultType
@ -15,12 +15,25 @@ async def get_password(
preset: str | None = None, preset: str | None = None,
skip_confirmation: bool = False, skip_confirmation: bool = False,
) -> Password | None: ) -> 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: while True:
result = await Input( result = await Input(
header=header, header=header,
allow_skip=allow_skip, allow_skip=allow_skip,
default_value=preset, default_value=preset,
password=True, password=True,
info_callback=password_hint,
).show() ).show()
if result.type_ == ResultType.Skip: if result.type_ == ResultType.Skip:

View File

@ -1,6 +1,8 @@
import sys import sys
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable 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 typing import Any, ClassVar, Literal, TypeVar, cast, override
from textual import work from textual import work
@ -707,6 +709,18 @@ class NotifyScreen(ConfirmationScreen[ValueT]):
super().__init__(group, header) 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]): class InputScreen(BaseScreen[str]):
CSS = """ CSS = """
InputScreen { InputScreen {
@ -728,6 +742,22 @@ class InputScreen(BaseScreen[str]):
color: red; color: red;
text-align: center; 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__( def __init__(
@ -739,6 +769,7 @@ class InputScreen(BaseScreen[str]):
allow_reset: bool = False, allow_reset: bool = False,
allow_skip: bool = False, allow_skip: bool = False,
validator: Validator | None = None, validator: Validator | None = None,
info_callback: Callable[[str], InputInfo | None] | None = None,
): ):
super().__init__(allow_skip, allow_reset) super().__init__(allow_skip, allow_reset)
self._header = header or '' self._header = header or ''
@ -748,6 +779,7 @@ class InputScreen(BaseScreen[str]):
self._allow_reset = allow_reset self._allow_reset = allow_reset
self._allow_skip = allow_skip self._allow_skip = allow_skip
self._validator = validator self._validator = validator
self._info_callback = info_callback
async def run(self) -> Result[str]: async def run(self) -> Result[str]:
assert TApp.app assert TApp.app
@ -768,6 +800,7 @@ class InputScreen(BaseScreen[str]):
validate_on=['submitted'], validate_on=['submitted'],
) )
yield Label('', classes='input-failure', id='input-failure') yield Label('', classes='input-failure', id='input-failure')
yield Label('', id='input-info')
yield Footer() yield Footer()
@ -784,6 +817,24 @@ class InputScreen(BaseScreen[str]):
else: else:
_ = self.dismiss(Result(ResultType.Selection, _data=event.value)) _ = 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]): class _DataTable(DataTable[ValueT]):
BINDINGS: ClassVar = [ BINDINGS: ClassVar = [

View File

@ -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