diff --git a/CHANGELOG.md b/CHANGELOG.md index aac6864..4f7c026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,10 @@ - chore: remove what has remained from poetry in the codebase - refactor(core): avoid truncating or coloring logs in log files - feat(web-ui): add web-ui service -- fix(users): avoid setting user as sudoer when it performs a password reset - feat(web-ui): process input demands, dispatched on the bus - feat(keypad): ability to use key-combinations, set key combinations for screenshot, snapshot and quit +- feat(web-ui): add `fields` in `InputDescription` with `InputFieldDescription` data structures to describe the fields of an input demand in detail +- fix(users): avoid setting user as sudoer when it performs a password reset ## Version 1.0.0 diff --git a/ubo_app/services/010-voice/setup.py b/ubo_app/services/010-voice/setup.py index ee69bf0..5c894b2 100644 --- a/ubo_app/services/010-voice/setup.py +++ b/ubo_app/services/010-voice/setup.py @@ -19,6 +19,7 @@ from ubo_app.store.core import RegisterSettingAppAction, SettingsCategory from ubo_app.store.main import store from ubo_app.store.services.audio import AudioPlayAudioAction, AudioPlaybackDoneEvent +from ubo_app.store.services.notifications import NotificationExtraInformation from ubo_app.store.services.voice import ( VoiceEngine, VoiceReadTextAction, @@ -74,10 +75,13 @@ async def act() -> None: try: access_key = ( await ubo_input( - '.*', - prompt='Convert the Picovoice access key to a QR code and ' - 'scan it.', title='Picovoice Access Key', + extra_information=NotificationExtraInformation( + text='Convert the Picovoice access key to a QR code and scan ' + 'it', + ), + prompt='Enter Picovoice Access Key', + fields=[], ) )[0] secrets.write_secret(key=PICOVOICE_ACCESS_KEY, value=access_key) diff --git a/ubo_app/services/030-wifi/pages/create_wireless_connection.py b/ubo_app/services/030-wifi/pages/create_wireless_connection.py index af6a1ae..44c0527 100644 --- a/ubo_app/services/030-wifi/pages/create_wireless_connection.py +++ b/ubo_app/services/030-wifi/pages/create_wireless_connection.py @@ -15,6 +15,7 @@ from ubo_app.logging import logger from ubo_app.store.core import CloseApplicationEvent from ubo_app.store.main import store +from ubo_app.store.operations import InputFieldDescription, InputFieldType from ubo_app.store.services.notifications import ( Chime, Notification, @@ -55,8 +56,7 @@ def __init__( async def create_wireless_connection(self: CreateWirelessConnectionPage) -> None: try: - _, match = await ubo_input( - BARCODE_PATTERN, + _, data = await ubo_input( prompt='Enter WiFi connection', extra_information=NotificationExtraInformation( text='Go to your phone settings, choose QR code and hold it in ' @@ -64,27 +64,59 @@ async def create_wireless_connection(self: CreateWirelessConnectionPage) -> None picovoice_text='Go to your phone settings, choose {QR|K Y UW AA R} ' 'code and hold it in front of the camera to scan it.', ), + pattern=BARCODE_PATTERN, + fields=[ + InputFieldDescription( + name='SSID', + label='SSID', + type=InputFieldType.TEXT, + description='The name of the WiFi network', + required=True, + ), + InputFieldDescription( + name='Password', + label='Password', + type=InputFieldType.PASSWORD, + description='The password of the WiFi network', + required=False, + ), + InputFieldDescription( + name='Type', + label='Type', + type=InputFieldType.SELECT, + description='The type of the WiFi network', + default='WPA2', + options=['WEP', 'WPA', 'WPA2', 'nopass'], + required=False, + ), + InputFieldDescription( + name='Hidden', + label='Hidden', + type=InputFieldType.CHECKBOX, + description='Is the WiFi network hidden?', + default='false', + required=False, + ), + ], ) except asyncio.CancelledError: store.dispatch(CloseApplicationEvent(application=self)) return - if not match: + if not data: store.dispatch(CloseApplicationEvent(application=self)) return - ssid = match.get('SSID') or match.get('SSID_') + ssid = data.get('SSID') or data.get('SSID_') if ssid is None: store.dispatch(CloseApplicationEvent(application=self)) return - password = match.get('Password') or match.get('Password_') - type = match.get('Type') or match.get('Type_') + password = data.get('Password') or data.get('Password_') + type = data.get('Type') or data.get('Type_') if type: type = type.upper() type = cast(WiFiType, type) - hidden = ( - str_to_bool(match.get('Hidden') or match.get('Hidden_') or 'false') == 1 - ) + hidden = str_to_bool(data.get('Hidden') or data.get('Hidden_') or 'false') == 1 if not password: logger.warning('Password is required') diff --git a/ubo_app/services/040-camera/reducer.py b/ubo_app/services/040-camera/reducer.py index 00abf08..bec1109 100644 --- a/ubo_app/services/040-camera/reducer.py +++ b/ubo_app/services/040-camera/reducer.py @@ -144,7 +144,10 @@ def reducer( InputProvideAction( id=state.current.id, value=code, - data=match.groupdict(), + data={ + key.rstrip('_'): value + for key, value in match.groupdict().items() + }, ), ], events=[ diff --git a/ubo_app/services/080-docker/images_.py b/ubo_app/services/080-docker/images_.py index 93de7ff..30da52f 100644 --- a/ubo_app/services/080-docker/images_.py +++ b/ubo_app/services/080-docker/images_.py @@ -116,7 +116,6 @@ class ImageEntry(Immutable): registry='docker.io', environment_vairables={ 'NGROK_AUTHTOKEN': lambda: ubo_input( - r'^[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{20,30}$', resolver=lambda code, _: code, prompt='Enter the Ngrok Auth Token', extra_information=NotificationExtraInformation( @@ -135,10 +134,10 @@ class ImageEntry(Immutable): 3. Convert it to {QR|K Y UW AA R} code 4. Scan QR code to input the token""", ), + pattern=rf'^[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{20,30}$', ), }, command=lambda: ubo_input( - '', resolver=lambda code, _: code, prompt='Enter the command, for example: `http 80` or `tcp 22`', extra_information=NotificationExtraInformation( @@ -148,6 +147,7 @@ class ImageEntry(Immutable): This is the command you would enter when running {ngrok|EH N G EH R AA K}. Refer to {ngrok|EH N G EH R AA K} documentation for further information""", ), + fields=[], ), ), *( diff --git a/ubo_app/services/080-docker/setup.py b/ubo_app/services/080-docker/setup.py index ae6bd20..9fb7640 100644 --- a/ubo_app/services/080-docker/setup.py +++ b/ubo_app/services/080-docker/setup.py @@ -23,6 +23,7 @@ SettingsCategory, ) from ubo_app.store.main import store +from ubo_app.store.operations import InputFieldDescription, InputFieldType from ubo_app.store.services.docker import ( DockerImageRegisterAppEvent, DockerRemoveUsernameAction, @@ -210,10 +211,7 @@ async def act() -> None: try: credentials = ( await ubo_input( - r'^(?P[^|]*)\|(?P[^|]*)\|(?P[^|]*)$|' - r'(?P^[^|]*)|(?P[^|]*)$', - prompt='Format: [i]SERVICE|USERNAME|PASSWORD[/i]', - title='Enter Docker Credentials', + prompt='Enter Docker Credentials', extra_information=NotificationExtraInformation( text="""To generate your QR code for login, format your \ details by separating your service, username, and password with the pipe symbol. For \ @@ -232,6 +230,30 @@ async def act() -> None: omit the service name, "docker {.|D AA T} io" will automatically be used as the \ default.""", ), + pattern=r'^(?P[^|]*)\|(?P[^|]*)\|(?P[^|]*)$|' + r'(?P^[^|]*)|(?P[^|]*)$', + fields=[ + InputFieldDescription( + name='Service', + label='Service', + type=InputFieldType.TEXT, + description='The service name', + default='docker.io', + required=False, + ), + InputFieldDescription( + name='Username', + label='Username', + type=InputFieldType.TEXT, + required=True, + ), + InputFieldDescription( + name='Password', + label='Password', + type=InputFieldType.PASSWORD, + required=True, + ), + ], ) )[1] if not credentials: diff --git a/ubo_app/services/090-web-ui/templates/index.jinja2 b/ubo_app/services/090-web-ui/templates/index.jinja2 index b472f2a..247766b 100644 --- a/ubo_app/services/090-web-ui/templates/index.jinja2 +++ b/ubo_app/services/090-web-ui/templates/index.jinja2 @@ -3,6 +3,38 @@ ubo - web-ui + @@ -11,11 +43,43 @@

+ +

+ {% endfor %} + {% elif input.pattern and re.compile(input.pattern).groupindex.keys() | length > 0 %} {% for group_name in - re.compile(input.pattern).groupindex.keys() | map('replace', '_', '')|list|unique + re.compile(input.pattern).groupindex.keys() | map('regex_replace', '_+$', '') | list | unique %}

- - + + {% if not loop.last %}
{% endif %} diff --git a/ubo_app/store/operations.py b/ubo_app/store/operations.py index ecea0ca..302ce99 100644 --- a/ubo_app/store/operations.py +++ b/ubo_app/store/operations.py @@ -2,6 +2,7 @@ from __future__ import annotations +from enum import StrEnum from typing import TYPE_CHECKING from immutable import Immutable @@ -19,6 +20,35 @@ class SnapshotEvent(BaseEvent): """Event for taking a snapshot of the store.""" +class InputFieldType(StrEnum): + """Enumeration of input field types.""" + + LONG = 'long' + TEXT = 'text' + PASSWORD = 'password' # noqa: S105 + NUMBER = 'number' + CHECKBOX = 'checkbox' + COLOR = 'color' + SELECT = 'select' + FILE = 'file' + DATE = 'date' + TIME = 'time' + + +class InputFieldDescription(Immutable): + """Description of an input field in an input demand.""" + + name: str + label: str + type: InputFieldType + description: str | None = None + title: str | None = None + pattern: str | None = None + default: str | None = None + options: list[str] | None = None + required: bool = False + + class InputDescription(Immutable): """Description of an input demand.""" @@ -27,6 +57,7 @@ class InputDescription(Immutable): extra_information: NotificationExtraInformation | None = None id: str pattern: str | None + fields: list[InputFieldDescription] | None = None class InputAction(BaseAction): diff --git a/ubo_app/utils/input.py b/ubo_app/utils/input.py index 68bffd0..2bf00ad 100644 --- a/ubo_app/utils/input.py +++ b/ubo_app/utils/input.py @@ -14,6 +14,7 @@ InputCancelEvent, InputDemandAction, InputDescription, + InputFieldDescription, InputProvideEvent, ) from ubo_app.store.services.camera import CameraStopViewfinderEvent @@ -32,27 +33,47 @@ @overload async def ubo_input( + *, + prompt: str | None = None, + extra_information: NotificationExtraInformation | None = None, + title: str | None = None, pattern: str, + fields: list[InputFieldDescription] | None = None, +) -> tuple[str, InputResultGroupDict]: ... +@overload +async def ubo_input( *, prompt: str | None = None, extra_information: NotificationExtraInformation | None = None, title: str | None = None, + fields: list[InputFieldDescription], ) -> tuple[str, InputResultGroupDict]: ... @overload async def ubo_input( - pattern: str, *, prompt: str | None = None, extra_information: NotificationExtraInformation | None = None, title: str | None = None, + pattern: str, + fields: list[InputFieldDescription] | None = None, resolver: Callable[[str, InputResultGroupDict], ReturnType], ) -> ReturnType: ... +@overload async def ubo_input( - pattern: str, *, prompt: str | None = None, extra_information: NotificationExtraInformation | None = None, title: str | None = None, + fields: list[InputFieldDescription], + resolver: Callable[[str, InputResultGroupDict], ReturnType], +) -> ReturnType: ... +async def ubo_input( # noqa: PLR0913 + *, + prompt: str | None = None, + extra_information: NotificationExtraInformation | None = None, + title: str | None = None, + pattern: str | None = None, + fields: list[InputFieldDescription] | None = None, resolver: Callable[[str, InputResultGroupDict], ReturnType] | None = None, ) -> tuple[str, InputResultGroupDict] | ReturnType: """Input the user in an imperative way.""" @@ -131,6 +152,7 @@ def handle_cancel(event: CameraStopViewfinderEvent) -> None: extra_information=extra_information, id=prompt_id, pattern=pattern, + fields=fields, ), ), )