1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-12 09:38:08 +00:00
This commit is contained in:
matejcik 2024-08-06 16:20:13 +02:00 committed by M1nd3r
parent 695daab14e
commit b02c7c4895
13 changed files with 603 additions and 1078 deletions

View File

@ -169,8 +169,6 @@ trezor.ui.layouts.mercury
import trezor.ui.layouts.mercury import trezor.ui.layouts.mercury
trezor.ui.layouts.mercury.fido trezor.ui.layouts.mercury.fido
import trezor.ui.layouts.mercury.fido import trezor.ui.layouts.mercury.fido
trezor.ui.layouts.mercury.homescreen
import trezor.ui.layouts.mercury.homescreen
trezor.ui.layouts.mercury.recovery trezor.ui.layouts.mercury.recovery
import trezor.ui.layouts.mercury.recovery import trezor.ui.layouts.mercury.recovery
trezor.ui.layouts.mercury.reset trezor.ui.layouts.mercury.reset

View File

@ -58,6 +58,9 @@ if __debug__:
def wait_until_layout_is_running(tries: int | None = _DEADLOCK_WAIT_TRIES) -> Awaitable[None]: # type: ignore [awaitable-is-generator] def wait_until_layout_is_running(tries: int | None = _DEADLOCK_WAIT_TRIES) -> Awaitable[None]: # type: ignore [awaitable-is-generator]
counter = 0 counter = 0
while ui.CURRENT_LAYOUT is None: while ui.CURRENT_LAYOUT is None:
# TODO modify this so that we can detect a Rust layout in transition:
# ui.CURRENT_LAYOUT is not None
# but something like layout.is_rust_ready_for_events() is False
counter += 1 counter += 1
if tries is not None and counter > tries: if tries is not None and counter > tries:
raise wire.FirmwareError( raise wire.FirmwareError(
@ -104,9 +107,9 @@ if __debug__:
async def _layout_click(x: int, y: int, hold_ms: int = 0) -> None: async def _layout_click(x: int, y: int, hold_ms: int = 0) -> None:
assert isinstance(ui.CURRENT_LAYOUT, ui.Layout) assert isinstance(ui.CURRENT_LAYOUT, ui.Layout)
msg = ui.CURRENT_LAYOUT.layout.touch_event(io.TOUCH_START, x, y) ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT._emit_message(msg) ui.CURRENT_LAYOUT.layout.touch_event, io.TOUCH_START, x, y
ui.CURRENT_LAYOUT._paint() )
if hold_ms: if hold_ms:
await loop.sleep(hold_ms) await loop.sleep(hold_ms)
@ -114,9 +117,9 @@ if __debug__:
if not isinstance(ui.CURRENT_LAYOUT, ui.Layout): if not isinstance(ui.CURRENT_LAYOUT, ui.Layout):
return return
msg = ui.CURRENT_LAYOUT.layout.touch_event(io.TOUCH_END, x, y) ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT._emit_message(msg) ui.CURRENT_LAYOUT.layout.touch_event, io.TOUCH_END, x, y
ui.CURRENT_LAYOUT._paint() )
async def _layout_press_button( async def _layout_press_button(
debug_btn: DebugPhysicalButton, hold_ms: int = 0 debug_btn: DebugPhysicalButton, hold_ms: int = 0
@ -135,9 +138,9 @@ if __debug__:
assert isinstance(ui.CURRENT_LAYOUT, ui.Layout) assert isinstance(ui.CURRENT_LAYOUT, ui.Layout)
for btn in buttons: for btn in buttons:
msg = ui.CURRENT_LAYOUT.layout.button_event(io.BUTTON_PRESSED, btn) ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT._emit_message(msg) ui.CURRENT_LAYOUT.layout.button_event, io.BUTTON_PRESSED, btn
ui.CURRENT_LAYOUT._paint() )
if hold_ms: if hold_ms:
await loop.sleep(hold_ms) await loop.sleep(hold_ms)
@ -146,9 +149,9 @@ if __debug__:
if not isinstance(ui.CURRENT_LAYOUT, ui.Layout): if not isinstance(ui.CURRENT_LAYOUT, ui.Layout):
return return
for btn in buttons: for btn in buttons:
msg = ui.CURRENT_LAYOUT.layout.button_event(io.BUTTON_RELEASED, btn) ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT._emit_message(msg) ui.CURRENT_LAYOUT.layout.button_event, io.BUTTON_RELEASED, btn
ui.CURRENT_LAYOUT._paint() )
if utils.USE_TOUCH: if utils.USE_TOUCH:
@ -169,9 +172,9 @@ if __debug__:
(io.TOUCH_MOVE, orig_x + 1 * off_x, orig_y + 1 * off_y), (io.TOUCH_MOVE, orig_x + 1 * off_x, orig_y + 1 * off_y),
(io.TOUCH_END, orig_x + 2 * off_x, orig_y + 2 * off_y), (io.TOUCH_END, orig_x + 2 * off_x, orig_y + 2 * off_y),
): ):
msg = ui.CURRENT_LAYOUT.layout.touch_event(event, x, y) ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT._emit_message(msg) ui.CURRENT_LAYOUT.layout.touch_event, event, x, y
ui.CURRENT_LAYOUT._paint() )
elif utils.USE_BUTTON: elif utils.USE_BUTTON:
@ -213,6 +216,10 @@ if __debug__:
x = msg.x # local_cache_attribute x = msg.x # local_cache_attribute
y = msg.y # local_cache_attribute y = msg.y # local_cache_attribute
# TODO
# if ui.CURRENT_LAYOUT is None:
# await layout_change_chan
await wait_until_layout_is_running() await wait_until_layout_is_running()
assert isinstance(ui.CURRENT_LAYOUT, ui.Layout) assert isinstance(ui.CURRENT_LAYOUT, ui.Layout)
layout_change_chan.clear() layout_change_chan.clear()
@ -278,6 +285,11 @@ if __debug__:
# default behavior: msg.wait_layout == DebugWaitType.CURRENT_LAYOUT # default behavior: msg.wait_layout == DebugWaitType.CURRENT_LAYOUT
if not isinstance(ui.CURRENT_LAYOUT, ui.Layout): if not isinstance(ui.CURRENT_LAYOUT, ui.Layout):
# TODO
# if Rust layout is animating, we _could_ get an animation step here
# because the event which triggered the animation was "already processed"
# but the layout is still in the process of updating itself.
# We don't have a clear information that the layout is ready to "be read".
return await return_layout_change(DEBUG_CONTEXT, detect_deadlock=True) return await return_layout_change(DEBUG_CONTEXT, detect_deadlock=True)
else: else:
return _state() return _state()
@ -429,4 +441,4 @@ if __debug__:
def boot() -> None: def boot() -> None:
import usb import usb
loop.schedule(handle_session(usb.iface_debug)) loop.schedule(handle_session(usb.iface_debug))#

View File

@ -1,16 +1,19 @@
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
import utime import utime
import trezorui2
from micropython import const from micropython import const
from trezorui import Display from trezorui import Display
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from trezor import io, loop, utils, workflow from trezor import io, log, loop, utils, workflow
from trezorui2 import AttachType, BacklightLevels from trezor.messages import ButtonAck, ButtonRequest
from trezor.wire import context
from trezorui2 import BacklightLevels
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable, Generator, Generic, Iterator, TypeVar from typing import Any, Callable, Generator, Generic, Iterator, TypeVar
from trezorui2 import LayoutObj, UiResult # noqa: F401 from trezorui2 import AttachType, LayoutObj, UiResult # noqa: F401
T = TypeVar("T", covariant=True) T = TypeVar("T", covariant=True)
@ -19,6 +22,10 @@ else:
Generic = {T: object} Generic = {T: object}
if __debug__:
trezorui2.disable_animation(bool(utils.DISABLE_ANIMATION))
# all rendering is done through a singleton of `Display` # all rendering is done through a singleton of `Display`
display = Display() display = Display()
@ -133,8 +140,6 @@ class Layout(Generic[T]):
[docs/core/misc/layout-lifecycle.md] for details. [docs/core/misc/layout-lifecycle.md] for details.
""" """
BACKLIGHT_LEVEL = BacklightLevels.NORMAL
if __debug__: if __debug__:
@staticmethod @staticmethod
@ -156,7 +161,10 @@ class Layout(Generic[T]):
self.tasks: set[loop.Task] = set() self.tasks: set[loop.Task] = set()
self.timers: dict[int, loop.Task] = {} self.timers: dict[int, loop.Task] = {}
self.result_box = loop.mailbox() self.result_box = loop.mailbox()
self.button_request_box = loop.mailbox()
self.transition_out: AttachType | None = None self.transition_out: AttachType | None = None
self.backlight_level = BacklightLevels.NORMAL
self.context: context.Context | None = None
def is_ready(self) -> bool: def is_ready(self) -> bool:
"""True if the layout is in READY state.""" """True if the layout is in READY state."""
@ -197,14 +205,17 @@ class Layout(Generic[T]):
assert CURRENT_LAYOUT is None assert CURRENT_LAYOUT is None
set_current_layout(self) set_current_layout(self)
# save context
self.context = context.CURRENT_CONTEXT
# attach a timer callback and paint self # attach a timer callback and paint self
self.layout.attach_timer_fn(self._set_timer, transition_in) self._event(self.layout.attach_timer_fn, self._set_timer, transition_in)
# self.layout.attach_timer_fn(self._set_timer, transition_in)
self._first_paint() self._first_paint()
# spawn all tasks # spawn all tasks
for task in self.create_tasks(): for task in self.create_tasks():
self.tasks.add(task) self._start_task(task)
loop.schedule(task)
def stop(self, _kill_taker: bool = True) -> None: def stop(self, _kill_taker: bool = True) -> None:
"""Stop the layout, moving out of RUNNING state and unsetting self as the """Stop the layout, moving out of RUNNING state and unsetting self as the
@ -247,6 +258,8 @@ class Layout(Generic[T]):
self.start() self.start()
# else we are (a) still running or (b) already stopped # else we are (a) still running or (b) already stopped
try: try:
if self.context is not None and self.result_box.is_empty():
self._start_task(self._handle_usb_iface())
return await self.result_box return await self.result_box
finally: finally:
self.stop() self.stop()
@ -256,12 +269,37 @@ class Layout(Generic[T]):
msg = self.layout.request_complete_repaint() msg = self.layout.request_complete_repaint()
assert msg is None assert msg is None
def _event(self, event_call: Callable[..., object], *args: Any) -> None:
"""Process an event coming out of the Rust layout. Set is as a result and shut
down the layout if appropriate, do nothing otherwise."""
msg = event_call(*args)
self._emit_message(msg)
self._button_request()
if self.layout.paint():
refresh()
def _button_request(self) -> None:
"""Process a button request coming out of the Rust layout."""
if __debug__ and not self.button_request_box.is_empty():
raise RuntimeError # button request already pending
if self.context is None:
return
res = self.layout.button_request()
if res is None:
return
# in production, we don't want this to fail, hence replace=True
self.button_request_box.put(res, replace=True)
def _paint(self) -> None: def _paint(self) -> None:
"""Paint the layout and ensure that homescreen cache is properly invalidated.""" """Paint the layout and ensure that homescreen cache is properly invalidated."""
import storage.cache as storage_cache import storage.cache as storage_cache
painted = self.layout.paint() painted = self.layout.paint()
refresh() if painted:
refresh()
if storage_cache.homescreen_shown is not None and painted: if storage_cache.homescreen_shown is not None and painted:
storage_cache.homescreen_shown = None storage_cache.homescreen_shown = None
@ -276,7 +314,7 @@ class Layout(Generic[T]):
self._paint() self._paint()
# Turn the brightness on. # Turn the brightness on.
backlight_fade(self.BACKLIGHT_LEVEL) backlight_fade(self.backlight_level)
def _set_timer(self, token: int, duration: int) -> None: def _set_timer(self, token: int, duration: int) -> None:
"""Timer callback for Rust layouts.""" """Timer callback for Rust layouts."""
@ -305,6 +343,13 @@ class Layout(Generic[T]):
down the layout if appropriate, do nothing otherwise.""" down the layout if appropriate, do nothing otherwise."""
if msg is None: if msg is None:
return return
# XXX DOES NOT WORK YET
if isinstance(msg, ButtonRequest):
# FIXME: special return value for "layout has changed"
# and the buttonrequest is an attribute on that value
from apps.debug import notify_layout_change
notify_layout_change(self)
# when emitting a message, there should not be another one already waiting # when emitting a message, there should not be another one already waiting
assert self.result_box.is_empty() assert self.result_box.is_empty()
@ -336,15 +381,61 @@ class Layout(Generic[T]):
# Using `yield` instead of `await` to avoid allocations. # Using `yield` instead of `await` to avoid allocations.
event = yield touch event = yield touch
workflow.idle_timer.touch() workflow.idle_timer.touch()
msg = event_call(*event) self._event(event_call, *event)
self._emit_message(msg)
self.layout.paint()
refresh()
except Shutdown: except Shutdown:
return return
finally: finally:
touch.close() touch.close()
async def _handle_usb_iface(self) -> None:
if self.context is None:
return
read_and_raise = self.context.read(())
while True:
try:
br_code, br_name = await loop.race(
read_and_raise, self.button_request_box
)
await self.context.call(
ButtonRequest(
code=br_code, pages=self.layout.page_count(), name=br_name
),
ButtonAck,
)
except Exception:
print(self._trace(self.layout))
raise
def _task_finalizer(self, task: loop.Task, value: Any) -> None:
if value is None:
# all is good
if __debug__:
log.debug(__name__, "UI task exited by itself: %s", task)
return
if isinstance(value, GeneratorExit):
if __debug__:
log.debug(__name__, "UI task was stopped: %s", task)
return
if isinstance(value, BaseException):
if __debug__ and value.__class__.__name__ != "UnexpectedMessage":
log.error(
__name__, "UI task died: %s (%s)", task, value.__class__.__name__
)
try:
self._emit_message(value)
except Shutdown:
pass
if __debug__:
log.error(__name__, "UI task returned non-None: %s (%s)", task, value)
def _start_task(self, task: loop.Task) -> None:
self.tasks.add(task)
loop.schedule(task, finalizer=self._task_finalizer)
def __del__(self) -> None: def __del__(self) -> None:
self.layout.__del__() self.layout.__del__()
@ -379,8 +470,8 @@ class ProgressLayout:
msg = self.layout.progress_event(value, description or "") msg = self.layout.progress_event(value, description or "")
assert msg is None assert msg is None
self.layout.paint() if self.layout.paint():
refresh() refresh()
def start(self) -> None: def start(self) -> None:
global CURRENT_LAYOUT global CURRENT_LAYOUT
@ -392,9 +483,10 @@ class ProgressLayout:
CURRENT_LAYOUT = self CURRENT_LAYOUT = self
self.layout.request_complete_repaint() self.layout.request_complete_repaint()
self.layout.paint() painted = self.layout.paint()
backlight_fade(BacklightLevels.NONE) backlight_fade(BacklightLevels.NONE)
refresh() if painted:
refresh()
def stop(self) -> None: def stop(self) -> None:
global CURRENT_LAYOUT global CURRENT_LAYOUT

View File

@ -1,13 +1,13 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import trezorui2 import trezorui2
from trezor import ui, workflow from trezor import ui, utils, workflow
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezor.messages import ButtonAck, ButtonRequest from trezor.messages import ButtonAck, ButtonRequest
from trezor.wire import ActionCancelled, context from trezor.wire import ActionCancelled, context
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Awaitable, Callable, TypeVar from typing import Any, Awaitable, Callable, TypeVar
PropertyType = tuple[str | None, str | bytes | None] PropertyType = tuple[str | None, str | bytes | None]
ExceptionType = BaseException | type[BaseException] ExceptionType = BaseException | type[BaseException]
@ -43,7 +43,7 @@ async def interact(
if br_name is not None: if br_name is not None:
await _button_request(br_name, br_code, layout_obj.page_count()) await _button_request(br_name, br_code, layout_obj.page_count())
# wait for the layout result # wait for the layout result
result = await context.wait(layout.get_result()) result = await layout.get_result()
# raise an exception if the user cancelled the action # raise an exception if the user cancelled the action
if raise_on_cancel is not None and result is trezorui2.CANCELLED: if raise_on_cancel is not None and result is trezorui2.CANCELLED:
raise raise_on_cancel raise raise_on_cancel
@ -58,3 +58,41 @@ def raise_if_not_confirmed(
) -> Awaitable[None]: ) -> Awaitable[None]:
action = interact(layout_obj, br_name, br_code, exc) action = interact(layout_obj, br_name, br_code, exc)
return action # type: ignore [Type cannot be assigned to type "None"] return action # type: ignore [Type cannot be assigned to type "None"]
async def with_info(
main_layout: ui.LayoutObj[ui.UiResult],
info_layout: ui.LayoutObj[Any],
br_name: str,
br_code: ButtonRequestType,
) -> None:
send_button_request = True
while True:
result = await interact(
main_layout, br_name if send_button_request else None, br_code
)
# raises on cancel
send_button_request = False
if result is trezorui2.CONFIRMED:
return
elif result is trezorui2.INFO:
await interact(info_layout, None, raise_on_cancel=None)
continue
else:
raise RuntimeError # unexpected result
def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None:
# Simple drawing not supported for layouts that set timers.
def dummy_set_timer(token: int, duration: int) -> None:
raise RuntimeError
layout.attach_timer_fn(dummy_set_timer, None)
if utils.USE_BACKLIGHT:
ui.backlight_fade(ui.BacklightLevels.DIM)
if layout.paint():
ui.refresh()
if utils.USE_BACKLIGHT:
ui.backlight_fade(ui.BacklightLevels.NORMAL)

View File

@ -15,10 +15,11 @@ class HomescreenBase(ui.Layout):
def __init__(self, layout: Any) -> None: def __init__(self, layout: Any) -> None:
super().__init__(layout=layout) super().__init__(layout=layout)
self.context = None
def _paint(self) -> None: def _paint(self) -> None:
self.layout.paint() if self.layout.paint():
ui.refresh() ui.refresh()
def _first_paint(self) -> None: def _first_paint(self) -> None:
if storage_cache.homescreen_shown is not self.RENDER_INDICATOR: if storage_cache.homescreen_shown is not self.RENDER_INDICATOR:
@ -40,7 +41,7 @@ class Homescreen(HomescreenBase):
) -> None: ) -> None:
level = 1 level = 1
if notification is not None: if notification is not None:
notification = notification.rstrip("!") notification = notification.rstrip("!") # TODO handle TS5 that doesn't have it
if notification == TR.homescreen__title_coinjoin_authorized: if notification == TR.homescreen__title_coinjoin_authorized:
level = 3 level = 3
elif notification == TR.homescreen__title_experimental_mode: elif notification == TR.homescreen__title_experimental_mode:
@ -65,9 +66,7 @@ class Homescreen(HomescreenBase):
usbcheck = loop.wait(io.USB_CHECK) usbcheck = loop.wait(io.USB_CHECK)
while True: while True:
is_connected = await usbcheck is_connected = await usbcheck
self.layout.usb_event(is_connected) self._event(self.layout.usb_event, is_connected)
self.layout.paint()
ui.refresh()
def create_tasks(self) -> Iterator[loop.Task]: def create_tasks(self) -> Iterator[loop.Task]:
yield from super().create_tasks() yield from super().create_tasks()
@ -76,7 +75,6 @@ class Homescreen(HomescreenBase):
class Lockscreen(HomescreenBase): class Lockscreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON
BACKLIGHT_LEVEL = ui.BacklightLevels.LOW
def __init__( def __init__(
self, self,
@ -85,8 +83,9 @@ class Lockscreen(HomescreenBase):
coinjoin_authorized: bool = False, coinjoin_authorized: bool = False,
) -> None: ) -> None:
self.bootscreen = bootscreen self.bootscreen = bootscreen
self.backlight_level = ui.BacklightLevels.LOW
if bootscreen: if bootscreen:
self.BACKLIGHT_LEVEL = ui.BacklightLevels.NORMAL self.backlight_level = ui.BacklightLevels.NORMAL
skip = ( skip = (
not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +1,15 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import trezorui2 import trezorui2
from trezor import ui
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from ..common import interact from ..common import interact
from . import RustLayout
if TYPE_CHECKING: if TYPE_CHECKING:
from trezor.loop import AwaitableTask from trezor.loop import AwaitableTask
if __debug__:
from trezor import io, ui
from ... import Result
class _RustFidoLayoutImpl(RustLayout):
def create_tasks(self) -> tuple[AwaitableTask, ...]:
return (
self.handle_input_and_rendering(),
self.handle_timers(),
self.handle_swipe(),
self.handle_debug_confirm(),
)
async def handle_debug_confirm(self) -> None:
from apps.debug import result_signal
_event_id, result = await result_signal()
if result is not trezorui2.CONFIRMED:
raise Result(result)
for event, x, y in (
(io.TOUCH_START, 220, 220),
(io.TOUCH_END, 220, 220),
):
msg = self.layout.touch_event(event, x, y)
if self.layout.paint():
ui.refresh()
if msg is not None:
raise Result(msg)
_RustFidoLayout = _RustFidoLayoutImpl
else:
_RustFidoLayout = RustLayout
async def confirm_fido( async def confirm_fido(
header: str, header: str,
app_name: str, app_name: str,
@ -54,16 +17,30 @@ async def confirm_fido(
accounts: list[str | None], accounts: list[str | None],
) -> int: ) -> int:
"""Webauthn confirmation for one or more credentials.""" """Webauthn confirmation for one or more credentials."""
confirm = _RustFidoLayout( confirm = trezorui2.confirm_fido(
trezorui2.confirm_fido( title=header,
title=header, app_name=app_name,
app_name=app_name, icon_name=icon_name,
icon_name=icon_name, accounts=accounts,
accounts=accounts,
)
) )
result = await interact(confirm, "confirm_fido", ButtonRequestType.Other) result = await interact(confirm, "confirm_fido", ButtonRequestType.Other)
if __debug__ and result is trezorui2.CONFIRMED:
# debuglink will directly inject a CONFIRMED message which we need to handle
# by playing back a click to the Rust layout and getting out the selected number
# that way
from trezor import io
msg = confirm.touch_event(io.TOUCH_START, 220, 220)
assert msg is None
if confirm.paint():
ui.refresh()
msg = confirm.touch_event(io.TOUCH_END, 220, 220)
if confirm.paint():
ui.refresh()
assert isinstance(msg, int)
return msg
# The Rust side returns either an int or `CANCELLED`. We detect the int situation # The Rust side returns either an int or `CANCELLED`. We detect the int situation
# and assume cancellation otherwise. # and assume cancellation otherwise.
if isinstance(result, int): if isinstance(result, int):
@ -78,7 +55,7 @@ async def confirm_fido(
async def confirm_fido_reset() -> bool: async def confirm_fido_reset() -> bool:
from trezor import TR from trezor import TR
confirm = RustLayout( confirm = ui.Layout(
trezorui2.confirm_action( trezorui2.confirm_action(
title=TR.fido__title_reset, title=TR.fido__title_reset,
action=TR.fido__erase_credentials, action=TR.fido__erase_credentials,
@ -87,4 +64,4 @@ async def confirm_fido_reset() -> bool:
prompt_screen=True, prompt_screen=True,
) )
) )
return (await confirm) is trezorui2.CONFIRMED return (await confirm.get_result()) is trezorui2.CONFIRMED

View File

@ -1,149 +0,0 @@
from typing import TYPE_CHECKING
import storage.cache as storage_cache
import trezorui2
from trezor import TR, ui
from . import RustLayout
if TYPE_CHECKING:
from typing import Any, Tuple
from trezor import loop
class HomescreenBase(RustLayout):
RENDER_INDICATOR: object | None = None
def __init__(self, layout: Any) -> None:
super().__init__(layout=layout)
def _attach(self) -> None:
if storage_cache.homescreen_shown is self.RENDER_INDICATOR:
self.layout.attach_timer_fn(self.set_timer, ui.AttachType.RESUME)
else:
self.layout.attach_timer_fn(self.set_timer, ui.LAST_TRANSITION_OUT)
def _paint(self) -> None:
if self.layout.paint():
ui.refresh()
def _first_paint(self) -> None:
if storage_cache.homescreen_shown is not self.RENDER_INDICATOR:
super()._first_paint()
storage_cache.homescreen_shown = self.RENDER_INDICATOR
else:
self._paint()
if __debug__:
# In __debug__ mode, ignore {confirm,swipe,input}_signal.
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
return (
self.handle_input_and_rendering(),
self.handle_timers(),
self.handle_click_signal(), # so we can receive debug events
)
class Homescreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.HOMESCREEN_ON
def __init__(
self,
label: str | None,
notification: str | None,
notification_is_error: bool,
hold_to_lock: bool,
) -> None:
level = 1
if notification is not None:
if notification == TR.homescreen__title_coinjoin_authorized:
level = 3
elif notification == TR.homescreen__title_experimental_mode:
level = 2
elif notification_is_error:
level = 0
skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR
super().__init__(
layout=trezorui2.show_homescreen(
label=label,
notification=notification,
notification_level=level,
hold=hold_to_lock,
skip_first_paint=skip,
),
)
async def usb_checker_task(self) -> None:
from trezor import io, loop
usbcheck = loop.wait(io.USB_CHECK)
while True:
is_connected = await usbcheck
self.layout.usb_event(is_connected)
if self.layout.paint():
ui.refresh()
def create_tasks(self) -> Tuple[loop.AwaitableTask, ...]:
return super().create_tasks() + (self.usb_checker_task(),)
class Lockscreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON
def __init__(
self,
label: str | None,
bootscreen: bool = False,
coinjoin_authorized: bool = False,
) -> None:
self.bootscreen = bootscreen
self.backlight_level = ui.BacklightLevels.LOW
if bootscreen:
self.backlight_level = ui.BacklightLevels.NORMAL
skip = (
not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR
)
super().__init__(
layout=trezorui2.show_lockscreen(
label=label,
bootscreen=bootscreen,
skip_first_paint=skip,
coinjoin_authorized=coinjoin_authorized,
),
)
async def __iter__(self) -> Any:
result = await super().__iter__()
if self.bootscreen:
self.request_complete_repaint()
return result
class Busyscreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.BUSYSCREEN_ON
def __init__(self, delay_ms: int) -> None:
from trezor import TR
skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR
super().__init__(
layout=trezorui2.show_progress_coinjoin(
title=TR.coinjoin__waiting_for_others,
indeterminate=True,
time_ms=delay_ms,
skip_first_paint=skip,
)
)
async def __iter__(self) -> Any:
from apps.base import set_homescreen
# Handle timeout.
result = await super().__iter__()
assert result == trezorui2.CANCELLED
storage_cache.delete(storage_cache.APP_COMMON_BUSY_DEADLINE_MS)
set_homescreen()
return result

View File

@ -1,42 +1,64 @@
from typing import Callable, Iterable from typing import Callable, Iterable
import trezorui2 import trezorui2
from trezor import TR from trezor import TR, ui
from trezor.enums import ButtonRequestType, RecoveryType from trezor.enums import ButtonRequestType, RecoveryType
from ..common import interact from ..common import interact
from . import RustLayout, raise_if_not_confirmed from . import raise_if_not_confirmed
CONFIRMED = trezorui2.CONFIRMED # global_import_cache CONFIRMED = trezorui2.CONFIRMED # global_import_cache
CANCELLED = trezorui2.CANCELLED # global_import_cache CANCELLED = trezorui2.CANCELLED # global_import_cache
INFO = trezorui2.INFO # global_import_cache INFO = trezorui2.INFO # global_import_cache
async def _is_confirmed_info(
dialog: ui.LayoutObj[ui.UiResult],
info_func: Callable,
br_name: str,
br_code: ButtonRequestType,
) -> bool:
send_button_request = True
while True:
result = await interact(
dialog, br_name if send_button_request else None, br_code
)
if result is trezorui2.INFO:
await info_func()
else:
return result is CONFIRMED
async def request_word_count(recovery_type: RecoveryType) -> int: async def request_word_count(recovery_type: RecoveryType) -> int:
selector = RustLayout(trezorui2.select_word_count(recovery_type=recovery_type)) selector = trezorui2.select_word_count(recovery_type=recovery_type)
count = await interact(selector, "word_count", ButtonRequestType.MnemonicWordCount) count = await interact(selector, "word_count", ButtonRequestType.MnemonicWordCount)
return int(count) return int(count)
async def request_word( async def request_word(
word_index: int, word_count: int, is_slip39: bool, prefill_word: str = "" word_index: int,
word_count: int,
is_slip39: bool,
send_button_request: bool,
prefill_word: str = "",
) -> str: ) -> str:
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count) prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
can_go_back = word_index > 0 can_go_back = word_index > 0
if is_slip39: if is_slip39:
keyboard = RustLayout( keyboard = trezorui2.request_slip39(
trezorui2.request_slip39( prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
) )
else: else:
keyboard = RustLayout( keyboard = trezorui2.request_bip39(
trezorui2.request_bip39( prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
) )
word: str = await keyboard word: str = await interact(
keyboard,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
return word return word
@ -100,23 +122,29 @@ async def continue_recovery(
recovery_type: RecoveryType, recovery_type: RecoveryType,
show_info: bool = False, show_info: bool = False,
) -> bool: ) -> bool:
# TODO: info_func should be changed to return data to be shown (and not show if show_info:
# them) so that individual models can implement showing logic on their own. # Show this just one-time
# T3T1 should move the data to `flow_continue_recovery` and hide them description = TR.recovery__enter_each_word
# in the context menu else:
description = subtext or ""
# NOTE: show_info can be understood as first screen before any shares homepage = trezorui2.confirm_recovery(
# NOTE: button request sent from the flow title=text,
homepage = RustLayout( description=description,
trezorui2.flow_continue_recovery( button=button_label,
first_screen=show_info, info_button=info_func is not None,
recovery_type=recovery_type, recovery_type=recovery_type,
text=text,
subtext=subtext,
)
) )
result = await homepage
return result is CONFIRMED if info_func is not None:
return await _is_confirmed_info(
homepage, info_func, "recovery", ButtonRequestType.RecoveryHomepage
)
else:
result = await interact(
homepage, "recovery", ButtonRequestType.RecoveryHomepage
)
return result is CONFIRMED
async def show_recovery_warning( async def show_recovery_warning(
@ -128,16 +156,12 @@ async def show_recovery_warning(
) -> None: ) -> None:
button = button or TR.buttons__try_again # def_arg button = button or TR.buttons__try_again # def_arg
await raise_if_not_confirmed( await raise_if_not_confirmed(
interact( trezorui2.show_warning(
RustLayout( title=content or TR.words__warning,
trezorui2.show_warning( value=subheader or "",
title=content or TR.words__warning, button=button,
value=subheader or "", description="",
button=button, ),
description="", br_name,
) br_code,
),
br_name,
br_code,
)
) )

View File

@ -1,27 +1,22 @@
from typing import TYPE_CHECKING from typing import Awaitable, Callable, Sequence
import trezorui2 import trezorui2
from trezor import TR from trezor import TR, ui
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled from trezor.wire import ActionCancelled
from ..common import interact from ..common import interact
from . import RustLayout, raise_if_not_confirmed, show_success from . import raise_if_not_confirmed, show_success
if TYPE_CHECKING:
pass
from typing import Callable, Sequence
CONFIRMED = trezorui2.CONFIRMED # global_import_cache CONFIRMED = trezorui2.CONFIRMED # global_import_cache
async def show_share_words( def show_share_words(
share_words: Sequence[str], share_words: Sequence[str],
share_index: int | None = None, share_index: int | None = None,
group_index: int | None = None, group_index: int | None = None,
) -> None: ) -> Awaitable[None]:
title = TR.reset__recovery_wallet_backup_title title = TR.reset__recovery_wallet_backup_title
highlight_repeated = True highlight_repeated = True
if share_index is None: if share_index is None:
@ -44,7 +39,7 @@ async def show_share_words(
text_info.append(TR.reset__repeat_for_all_shares) text_info.append(TR.reset__repeat_for_all_shares)
text_confirm = TR.reset__words_written_down_template.format(words_count) text_confirm = TR.reset__words_written_down_template.format(words_count)
result = await RustLayout( return raise_if_not_confirmed(
trezorui2.flow_show_share_words( trezorui2.flow_show_share_words(
title=title, title=title,
subtitle=subtitle, subtitle=subtitle,
@ -53,12 +48,10 @@ async def show_share_words(
text_info=text_info, text_info=text_info,
text_confirm=text_confirm, text_confirm=text_confirm,
highlight_repeated=highlight_repeated, highlight_repeated=highlight_repeated,
) ),
None,
) )
if result != CONFIRMED:
raise ActionCancelled
async def select_word( async def select_word(
words: Sequence[str], words: Sequence[str],
@ -83,14 +76,15 @@ async def select_word(
while len(words) < 3: while len(words) < 3:
words.append(words[-1]) words.append(words[-1])
result = await RustLayout( result = await interact(
trezorui2.select_word( trezorui2.select_word(
title=title, title=title,
description=TR.reset__select_word_x_of_y_template.format( description=TR.reset__select_word_x_of_y_template.format(
checked_index + 1, count checked_index + 1, count
), ),
words=(words[0], words[1], words[2]), words=(words[0], words[1], words[2]),
) ),
None,
) )
if __debug__ and isinstance(result, str): if __debug__ and isinstance(result, str):
return result return result
@ -162,8 +156,7 @@ async def _prompt_number(
max_count: int, max_count: int,
br_name: str, br_name: str,
) -> int: ) -> int:
result = await interact(
result = await RustLayout(
trezorui2.flow_request_number( trezorui2.flow_request_number(
title=title, title=title,
description=description, description=description,
@ -173,7 +166,8 @@ async def _prompt_number(
info=info, info=info,
br_code=ButtonRequestType.ResetDevice, br_code=ButtonRequestType.ResetDevice,
br_name=br_name, br_name=br_name,
) ),
None,
) )
if __debug__: if __debug__:
@ -189,9 +183,9 @@ async def _prompt_number(
raise ActionCancelled # user cancelled request number prompt raise ActionCancelled # user cancelled request number prompt
async def slip39_prompt_threshold( def slip39_prompt_threshold(
num_of_shares: int, group_id: int | None = None num_of_shares: int, group_id: int | None = None
) -> int: ) -> Awaitable[int]:
count = num_of_shares // 2 + 1 count = num_of_shares // 2 + 1
# min value of share threshold is 2 unless the number of shares is 1 # min value of share threshold is 2 unless the number of shares is 1
# number of shares 1 is possible in advanced slip39 # number of shares 1 is possible in advanced slip39
@ -213,7 +207,7 @@ async def slip39_prompt_threshold(
) )
) )
return await _prompt_number( return _prompt_number(
TR.reset__title_set_threshold, TR.reset__title_set_threshold,
description, description,
info, info,
@ -307,8 +301,8 @@ async def show_intro_backup(single_share: bool, num_of_words: int | None) -> Non
) )
async def show_warning_backup() -> None: def show_warning_backup() -> Awaitable[ui.UiResult]:
result = await interact( return interact(
trezorui2.show_warning( trezorui2.show_warning(
title=TR.words__important, title=TR.words__important,
value=TR.reset__never_make_digital_copy, value=TR.reset__never_make_digital_copy,
@ -318,25 +312,23 @@ async def show_warning_backup() -> None:
"backup_warning", "backup_warning",
ButtonRequestType.ResetDevice, ButtonRequestType.ResetDevice,
) )
if result != CONFIRMED:
raise ActionCancelled
async def show_success_backup() -> None: def show_success_backup() -> Awaitable[None]:
await show_success( return show_success(
"success_backup", "success_backup",
TR.backup__title_backup_completed, TR.backup__title_backup_completed,
) )
async def show_reset_warning( def show_reset_warning(
br_name: str, br_name: str,
content: str, content: str,
subheader: str | None = None, subheader: str | None = None,
button: str | None = None, button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning, br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None: ) -> Awaitable[None]:
await interact( return raise_if_not_confirmed(
trezorui2.show_warning( trezorui2.show_warning(
title=subheader or "", title=subheader or "",
description=content, description=content,
@ -382,11 +374,11 @@ async def show_share_confirmation_success(
) )
) )
return await show_success("success_recovery", title, subheader=footer_description) await show_success("success_recovery", title, subheader=footer_description)
async def show_share_confirmation_failure() -> None: def show_share_confirmation_failure() -> Awaitable[None]:
await show_reset_warning( return show_reset_warning(
"warning_backup_check", "warning_backup_check",
TR.words__try_again, TR.words__try_again,
TR.reset__incorrect_word_selected, TR.reset__incorrect_word_selected,

View File

@ -5,7 +5,7 @@ from trezor import TR, ui, utils
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled from trezor.wire import ActionCancelled
from ..common import interact, raise_if_not_confirmed from ..common import interact, raise_if_not_confirmed, draw_simple
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Awaitable, Iterable, NoReturn, Sequence from typing import Any, Awaitable, Iterable, NoReturn, Sequence
@ -20,22 +20,6 @@ INFO = trezorui2.INFO
BR_CODE_OTHER = ButtonRequestType.Other # global_import_cache BR_CODE_OTHER = ButtonRequestType.Other # global_import_cache
if __debug__:
from trezor.utils import DISABLE_ANIMATION
trezorui2.disable_animation(bool(DISABLE_ANIMATION))
def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None:
# Simple drawing not supported for layouts that set timers.
def dummy_set_timer(token: int, duration: int) -> None:
raise RuntimeError
layout.attach_timer_fn(dummy_set_timer, None)
layout.paint()
ui.refresh()
# Temporary function, so we know where it is used # Temporary function, so we know where it is used
# Should be gradually replaced by custom designs/layouts # Should be gradually replaced by custom designs/layouts
def _placeholder_confirm( def _placeholder_confirm(

View File

@ -5,10 +5,10 @@ from trezor import TR, ui, utils
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled from trezor.wire import ActionCancelled
from ..common import interact, raise_if_not_confirmed from ..common import interact, raise_if_not_confirmed, with_info, draw_simple
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Awaitable, Iterable, NoReturn, Sequence from typing import Awaitable, Iterable, NoReturn, Sequence
from ..common import ExceptionType, PropertyType from ..common import ExceptionType, PropertyType
@ -20,24 +20,6 @@ CANCELLED = trezorui2.CANCELLED
INFO = trezorui2.INFO INFO = trezorui2.INFO
if __debug__:
from trezor.utils import DISABLE_ANIMATION
trezorui2.disable_animation(bool(DISABLE_ANIMATION))
def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None:
# Simple drawing not supported for layouts that set timers.
def dummy_set_timer(token: int, duration: int) -> None:
raise RuntimeError
layout.attach_timer_fn(dummy_set_timer, None)
ui.backlight_fade(ui.BacklightLevels.DIM)
layout.paint()
ui.refresh()
ui.backlight_fade(ui.BacklightLevels.NORMAL)
def confirm_action( def confirm_action(
br_name: str, br_name: str,
title: str, title: str,
@ -927,30 +909,6 @@ async def confirm_modify_output(
return return
async def with_info(
main_layout: ui.LayoutObj[ui.UiResult],
info_layout: ui.LayoutObj[Any],
br_name: str,
br_code: ButtonRequestType,
) -> None:
send_button_request = True
while True:
result = await interact(
main_layout, br_name if send_button_request else None, br_code
)
# raises on cancel
send_button_request = False
if result is CONFIRMED:
return
elif result is INFO:
await interact(info_layout, None, raise_on_cancel=None)
continue
else:
raise RuntimeError # unexpected result
def confirm_modify_fee( def confirm_modify_fee(
title: str, title: str,
sign: int, sign: int,

View File

@ -28,11 +28,11 @@ async def confirm_fido(
msg = confirm.touch_event(io.TOUCH_START, 220, 220) msg = confirm.touch_event(io.TOUCH_START, 220, 220)
assert msg is None assert msg is None
confirm.paint() if confirm.paint():
ui.refresh() ui.refresh()
msg = confirm.touch_event(io.TOUCH_END, 220, 220) msg = confirm.touch_event(io.TOUCH_END, 220, 220)
confirm.paint() if confirm.paint():
ui.refresh() ui.refresh()
assert isinstance(msg, int) assert isinstance(msg, int)
return msg return msg