feat(core/ui): delete old UI code

pull/2643/head
grdddj 2 years ago committed by Martin Milata
parent 1a9f3c18dd
commit d045e0089d

@ -251,21 +251,6 @@ core unix frozen debug build:
untracked: true
expire_in: 1 week
core unix frozen ui2 debug build:
stage: build
<<: *gitlab_caching
needs: []
variables:
PYOPT: "0"
UI2: "1"
script:
- nix-shell --run "poetry run make -C core build_unix_frozen"
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
untracked: true
expire_in: 1 week
core unix frozen debug asan build:
stage: build
<<: *gitlab_caching

@ -246,25 +246,6 @@ ui tests fixtures deploy:
tags:
- deploy
ui tests ui2 fixtures deploy:
stage: deploy
variables:
DEPLOY_PATH: "${DEPLOY_BASE_DIR}/ui_tests/"
BUCKET: "data.trezor.io"
GIT_SUBMODULE_STRATEGY: "none"
before_script: [] # no poetry
needs:
- core device ui2 test
script:
- echo "Deploying to $DEPLOY_PATH"
- rsync --delete -va ci/ui_test_records/* "$DEPLOY_PATH"
- source ${AWS_DEPLOY_DATA}
- aws s3 sync $DEPLOY_PATH s3://$BUCKET/dev/firmware/ui_tests
# This "hack" is needed because aws does not have an easy option to generate autoindex. We fetch the one autogenerated by nginx on local server.
- wget https://firmware.corp.sldev.cz/ui_tests/ -O index.html && aws s3 cp index.html s3://$BUCKET/dev/firmware/ui_tests/
tags:
- deploy
# sync to aws
sync emulators to aws:

@ -1,4 +1,3 @@
import os
import shutil
import sys
from pathlib import Path
@ -18,8 +17,6 @@ if len(sys.argv) > 1 and sys.argv[1].upper() == "T1":
model = "T1"
else:
model = "TT"
if os.getenv("UI2") == "1":
model += "ui2"
model_file_hashes = {k: v for k, v in FILE_HASHES.items() if k.startswith(f"{model}_")}
for test_case, expected_hash in model_file_hashes.items():

@ -11,8 +11,8 @@ image: registry.gitlab.com/satoshilabs/trezor/trezor-firmware/trezor-firmware-en
# Core
# Python and rust unit tests, checking TT functionality.
core unit test:
# Python unit tests, checking core functionality.
core unit python test:
stage: test
<<: *gitlab_caching
needs:
@ -20,14 +20,15 @@ core unit test:
script:
- nix-shell --run "poetry run make -C core test | ts -s"
core unit ui2 test:
# Rust unit tests.
core unit rust test:
stage: test
<<: *gitlab_caching
needs:
- core unix frozen ui2 debug build
- core unix frozen debug build
script:
- nix-shell --run "poetry run make -C core clippy | ts -s"
- nix-shell --run "poetry run make -C core test_rust UI2=1 | ts -s"
- nix-shell --run "poetry run make -C core test_rust | ts -s"
core unit asan test:
stage: test
@ -43,8 +44,8 @@ core unit asan test:
LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt"
script:
- nix-shell --run "poetry run make -C core test | ts -s"
- nix-shell --run "poetry run make -C core clean build_unix UI2=1 | ts -s"
- nix-shell --run "poetry run make -C core test_rust UI2=1 | ts -s"
- nix-shell --run "poetry run make -C core clean build_unix | ts -s"
- nix-shell --run "poetry run make -C core test_rust | ts -s"
core unit t1 test:
stage: test
@ -90,40 +91,6 @@ core device test:
reports:
junit: tests/junit.xml
core device ui2 test:
stage: test
<<: *gitlab_caching
when: manual
needs:
- core unix frozen ui2 debug build
variables:
TREZOR_PROFILING: "1"
UI2: "1"
script:
- nix-shell --run "poetry run make -C core test_emu_ui | ts -s"
# - mv core/src/.coverage core/.coverage.test_emu
after_script:
- mv tests/ui_tests/reporting/reports/test/ test_ui_report
- nix-shell --run "poetry run python ci/prepare_ui_artifacts.py TTui2 | ts -s"
- diff tests/ui_tests/fixtures.json tests/ui_tests/fixtures.suggestion.json
- nix-shell --run "cd tests/ui_tests ; poetry run python reporting/report_master_diff.py TTui2_"
- mv tests/ui_tests/reporting/reports/master_diff/ .
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- ci/ui_test_records/
- test_ui_report
- tests/ui_tests/screens/
- tests/ui_tests/fixtures.suggestion.json
- tests/junit.xml
- tests/trezor.log
# - core/.coverage.*
- master_diff
when: always
expire_in: 1 week
reports:
junit: tests/junit.xml
core device asan test:
stage: test
<<: *gitlab_caching

@ -24,7 +24,6 @@ BITCOIN_ONLY ?= 0
TREZOR_MODEL ?= T
TREZOR_MEMPERF ?= 0
ADDRESS_SANITIZER ?= 0
UI2 ?= 0
# OpenOCD interface default. Alternative: ftdi/olimex-arm-usb-tiny-h
OPENOCD_INTERFACE ?= stlink
@ -107,16 +106,16 @@ test_emu_click: ## run click tests
$(EMU_TEST) $(PYTEST) $(TESTPATH)/click_tests $(TESTOPTS)
test_emu_ui: ## run ui integration tests
UI2="$(UI2)" $(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests --ui=test --ui-check-missing $(TESTOPTS)
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests --ui=test --ui-check-missing $(TESTOPTS)
test_emu_ui_multicore: ## run ui integration tests using multiple cores
UI2="$(UI2)" $(PYTEST) -n auto $(TESTPATH)/device_tests $(TESTOPTS) --ui=test --ui-check-missing --control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM)
$(PYTEST) -n auto $(TESTPATH)/device_tests $(TESTOPTS) --ui=test --ui-check-missing --control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM)
test_emu_ui_record: ## record and hash screens for ui integration tests
UI2="$(UI2)" $(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests --ui=record --ui-check-missing $(TESTOPTS)
$(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests --ui=record --ui-check-missing $(TESTOPTS)
test_emu_ui_record_multicore: ## record and hash screens for ui integration tests using multiple cores
UI2="$(UI2)" $(PYTEST) -n auto $(TESTPATH)/device_tests $(TESTOPTS) --ui=record --ui-check-missing --control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM)
$(PYTEST) -n auto $(TESTPATH)/device_tests $(TESTOPTS) --ui=record --ui-check-missing --control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM)
pylint: ## run pylint on application sources and tests
pylint -E $(shell find src tests -name *.py)
@ -165,16 +164,16 @@ build_reflash: ## build reflash firmware + reflash image
dd if=build/bootloader/bootloader.bin of=$(REFLASH_BUILD_DIR)/sdimage.bin bs=1 seek=49152
build_firmware: templates build_cross ## build firmware with frozen modules
$(SCONS) CFLAGS="$(CFLAGS)" PRODUCTION="$(PRODUCTION)" TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" UI2="$(UI2)" $(FIRMWARE_BUILD_DIR)/firmware.bin
$(SCONS) CFLAGS="$(CFLAGS)" PRODUCTION="$(PRODUCTION)" TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" $(FIRMWARE_BUILD_DIR)/firmware.bin
build_unix: templates ## build unix port
$(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="0" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" UI2="$(UI2)"
$(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="0" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)"
build_unix_frozen: templates build_cross ## build unix port with frozen modules
$(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" UI2="$(UI2)" TREZOR_MEMPERF="$(TREZOR_MEMPERF)" TREZOR_EMULATOR_FROZEN=1
$(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" TREZOR_MEMPERF="$(TREZOR_MEMPERF)" TREZOR_EMULATOR_FROZEN=1
build_unix_debug: templates ## build unix port
$(SCONS) --max-drift=1 CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN=1 UI2="$(UI2)" TREZOR_EMULATOR_DEBUGGABLE=1
$(SCONS) --max-drift=1 CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN=1 TREZOR_EMULATOR_DEBUGGABLE=1
build_cross: ## build mpy-cross port
$(MAKE) -C vendor/micropython/mpy-cross $(CROSS_PORT_OPTS)

@ -7,7 +7,6 @@ import tools
BITCOIN_ONLY = ARGUMENTS.get('BITCOIN_ONLY', '0')
EVERYTHING = BITCOIN_ONLY != '1'
TREZOR_MODEL = ARGUMENTS.get('TREZOR_MODEL', 'T')
UI2 = ARGUMENTS.get('UI2', '0') == '1' or TREZOR_MODEL in ('1', 'R')
DMA2D = TREZOR_MODEL in ('T', )
FEATURE_FLAGS = {
@ -30,16 +29,10 @@ if TREZOR_MODEL in ('1', 'R'):
FONT_BOLD='Font_PixelOperator_Bold_8'
FONT_MONO='Font_PixelOperatorMono_Regular_8'
if TREZOR_MODEL in ('T', ):
if UI2:
FONT_NORMAL='Font_TTHoves_Regular_18'
FONT_DEMIBOLD='Font_TTHoves_DemiBold_18'
FONT_BOLD='Font_TTHoves_Bold_16'
FONT_MONO='Font_RobotoMono_Regular_20'
else:
FONT_NORMAL='Font_Roboto_Regular_20'
FONT_DEMIBOLD=None
FONT_BOLD='Font_Roboto_Bold_20'
FONT_MONO='Font_RobotoMono_Regular_20'
FONT_NORMAL='Font_TTHoves_Regular_18'
FONT_DEMIBOLD='Font_TTHoves_DemiBold_18'
FONT_BOLD='Font_TTHoves_Bold_16'
FONT_MONO='Font_RobotoMono_Regular_20'
# modtrezorconfig
CPPPATH_MOD += [
@ -191,11 +184,10 @@ SOURCE_MOD += [
'vendor/micropython/lib/uzlib/crc32.c',
'vendor/micropython/lib/uzlib/tinflate.c',
]
if UI2:
CPPDEFINES_MOD += [
'TREZOR_UI2',
'USE_RUST_LOADER'
]
CPPDEFINES_MOD += [
'TREZOR_UI2',
'USE_RUST_LOADER'
]
# modtrezorutils
SOURCE_MOD += [
@ -205,11 +197,8 @@ SOURCE_MOD += [
# rust mods
SOURCE_MOD += [
'embed/extmod/rustmods/modtrezorproto.c',
'embed/extmod/rustmods/modtrezorui2.c',
]
if UI2:
SOURCE_MOD += [
'embed/extmod/rustmods/modtrezorui2.c',
]
# modutime
SOURCE_MOD += [
@ -584,7 +573,6 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/common.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/reset.py'))
@ -592,28 +580,16 @@ if FROZEN:
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/altcoin.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/webauthn.py'))
if TREZOR_MODEL in ('T',) and UI2:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
if TREZOR_MODEL in ('T',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py'))
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/altcoin.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/webauthn.py'))
elif TREZOR_MODEL in ('T',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/recovery.py'))
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/altcoin.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/webauthn.py'))
elif TREZOR_MODEL in ('1',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py'))
elif TREZOR_MODEL in ('R',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tr.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
else:
raise ValueError('Unknown Trezor model')
@ -742,10 +718,9 @@ def cargo_build():
features = ['micropython', 'protobuf', f'model_t{TREZOR_MODEL.lower()}']
if BITCOIN_ONLY == '1':
features.append('bitcoin_only')
if UI2:
features.append('ui')
if PYOPT == '0':
features.append('ui_debug')
features.append('ui')
if PYOPT == '0':
features.append('ui_debug')
if DMA2D:
features.append('dma2d')

@ -7,7 +7,6 @@ import tools
BITCOIN_ONLY = ARGUMENTS.get('BITCOIN_ONLY', '0')
EVERYTHING = BITCOIN_ONLY != '1'
TREZOR_MODEL = ARGUMENTS.get('TREZOR_MODEL', 'T')
UI2 = ARGUMENTS.get('UI2', '0') == '1' or TREZOR_MODEL in ('1', 'R')
DMA2D = TREZOR_MODEL in ('T', )
FEATURE_FLAGS = {
@ -31,16 +30,10 @@ if TREZOR_MODEL in ('1', 'R'):
FONT_BOLD='Font_PixelOperator_Bold_8'
FONT_MONO='Font_PixelOperatorMono_Regular_8'
if TREZOR_MODEL in ('T', ):
if UI2:
FONT_NORMAL='Font_TTHoves_Regular_18'
FONT_DEMIBOLD='Font_TTHoves_DemiBold_18'
FONT_BOLD='Font_TTHoves_Bold_16'
FONT_MONO='Font_RobotoMono_Regular_20'
else:
FONT_NORMAL='Font_Roboto_Regular_20'
FONT_DEMIBOLD=None
FONT_BOLD='Font_Roboto_Bold_20'
FONT_MONO='Font_RobotoMono_Regular_20'
FONT_NORMAL='Font_TTHoves_Regular_18'
FONT_DEMIBOLD='Font_TTHoves_DemiBold_18'
FONT_BOLD='Font_TTHoves_Bold_16'
FONT_MONO='Font_RobotoMono_Regular_20'
# modtrezorconfig
CPPPATH_MOD += [
@ -187,11 +180,10 @@ SOURCE_MOD += [
'vendor/micropython/lib/uzlib/crc32.c',
'vendor/micropython/lib/uzlib/tinflate.c',
]
if UI2:
CPPDEFINES_MOD += [
'TREZOR_UI2',
'USE_RUST_LOADER'
]
CPPDEFINES_MOD += [
'TREZOR_UI2',
'USE_RUST_LOADER'
]
if FROZEN:
CPPDEFINES_MOD += ['TREZOR_EMULATOR_FROZEN']
if RASPI:
@ -205,11 +197,8 @@ SOURCE_MOD += [
# rust mods
SOURCE_MOD += [
'embed/extmod/rustmods/modtrezorproto.c',
'embed/extmod/rustmods/modtrezorui2.c',
]
if UI2:
SOURCE_MOD += [
'embed/extmod/rustmods/modtrezorui2.c',
]
# modutime
SOURCE_MOD += [
@ -538,7 +527,6 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/common.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/reset.py'))
@ -546,28 +534,16 @@ if FROZEN:
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/altcoin.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/webauthn.py'))
if TREZOR_MODEL in ('T',) and UI2:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
if TREZOR_MODEL in ('T',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py'))
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/altcoin.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/webauthn.py'))
elif TREZOR_MODEL in ('T',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/recovery.py'))
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/altcoin.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/webauthn.py'))
elif TREZOR_MODEL in ('1',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py'))
elif TREZOR_MODEL in ('R',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tr.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py'))
else:
raise ValueError('Unknown Trezor model')
@ -696,10 +672,9 @@ def cargo_build():
features = ['micropython', 'protobuf', f'model_t{TREZOR_MODEL.lower()}']
if BITCOIN_ONLY == '1':
features.append('bitcoin_only')
if UI2:
features.append('ui')
if PYOPT == '0':
features.append('debug')
features.append('ui')
if PYOPT == '0':
features.append('debug')
if DMA2D:
features.append('dma2d')

@ -151,48 +151,6 @@ trezor.ui.components.common
import trezor.ui.components.common
trezor.ui.components.common.confirm
import trezor.ui.components.common.confirm
trezor.ui.components.common.text
import trezor.ui.components.common.text
trezor.ui.components.tt.button
import trezor.ui.components.tt.button
trezor.ui.components.tt.checklist
import trezor.ui.components.tt.checklist
trezor.ui.components.tt.confirm
import trezor.ui.components.tt.confirm
trezor.ui.components.tt.info
import trezor.ui.components.tt.info
trezor.ui.components.tt.keyboard_bip39
import trezor.ui.components.tt.keyboard_bip39
trezor.ui.components.tt.keyboard_slip39
import trezor.ui.components.tt.keyboard_slip39
trezor.ui.components.tt.num_input
import trezor.ui.components.tt.num_input
trezor.ui.components.tt.passphrase
import trezor.ui.components.tt.passphrase
trezor.ui.components.tt.pin
import trezor.ui.components.tt.pin
trezor.ui.components.tt.recovery
import trezor.ui.components.tt.recovery
trezor.ui.components.tt.reset
import trezor.ui.components.tt.reset
trezor.ui.components.tt.scroll
import trezor.ui.components.tt.scroll
trezor.ui.components.tt.swipe
import trezor.ui.components.tt.swipe
trezor.ui.components.tt.text
import trezor.ui.components.tt.text
trezor.ui.components.tt.word_select
import trezor.ui.components.tt.word_select
trezor.ui.constants
import trezor.ui.constants
trezor.ui.constants.t1
import trezor.ui.constants.t1
trezor.ui.constants.tr
import trezor.ui.constants.tr
trezor.ui.constants.tt
import trezor.ui.constants.tt
trezor.ui.container
import trezor.ui.container
trezor.ui.layouts
import trezor.ui.layouts
trezor.ui.layouts.altcoin
@ -207,14 +165,6 @@ trezor.ui.layouts.t1
import trezor.ui.layouts.t1
trezor.ui.layouts.tr
import trezor.ui.layouts.tr
trezor.ui.layouts.tt
import trezor.ui.layouts.tt
trezor.ui.layouts.tt.altcoin
import trezor.ui.layouts.tt.altcoin
trezor.ui.layouts.tt.recovery
import trezor.ui.layouts.tt.recovery
trezor.ui.layouts.tt.reset
import trezor.ui.layouts.tt.reset
trezor.ui.layouts.tt_v2
import trezor.ui.layouts.tt_v2
trezor.ui.layouts.tt_v2.altcoin
@ -227,8 +177,6 @@ trezor.ui.loader
import trezor.ui.loader
trezor.ui.popup
import trezor.ui.popup
trezor.ui.qr
import trezor.ui.qr
trezor.ui.style
import trezor.ui.style
trezor.utils
@ -463,10 +411,6 @@ if not utils.BITCOIN_ONLY:
import trezor.enums.TezosContractType
trezor.ui.components.common.webauthn
import trezor.ui.components.common.webauthn
trezor.ui.components.tt.webauthn
import trezor.ui.components.tt.webauthn
trezor.ui.layouts.tt.webauthn
import trezor.ui.layouts.tt.webauthn
trezor.ui.layouts.tt_v2.webauthn
import trezor.ui.layouts.tt_v2.webauthn
trezor.ui.layouts.webauthn

File diff suppressed because one or more lines are too long

@ -1,26 +0,0 @@
# generated from resources.py.mako
# (by running `make templates` in `core`)
# do not edit manually!
# fmt: off
<%
from pathlib import Path
from itertools import chain
THIS = Path(local.filename).resolve()
SRCDIR = THIS.parent.parent.parent
PATTERNS = (
"trezor/res/**/*.toif",
"apps/*/res/**/*.toif",
)
resfiles = chain.from_iterable(sorted(SRCDIR.glob(p)) for p in PATTERNS)
%>\
def load_resource(name: str) -> bytes:
% for resfile in resfiles:
if name == "${resfile.relative_to(SRCDIR)}":
return ${repr(resfile.read_bytes())}
% endfor
return bytes()

@ -1,18 +1,11 @@
# pylint: disable=wrong-import-position
import math
import utime
from micropython import const
from trezorui import Display
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Awaitable, Generator
from trezor import io, loop, res, utils, workflow
if TYPE_CHECKING:
from typing import Any, Awaitable, Generator
Pos = tuple[int, int]
Area = tuple[int, int, int, int]
# all rendering is done through a singleton of `Display`
display = Display()
@ -23,10 +16,6 @@ MONO: int = Display.FONT_MONO
WIDTH: int = Display.WIDTH
HEIGHT: int = Display.HEIGHT
# viewport margins
VIEWX = const(6)
_VIEWY = const(9)
# channel used to cancel layouts, see `Cancelled` exception
layout_chan = loop.chan()
@ -53,22 +42,10 @@ if utils.EMULATOR or utils.MODEL in ("1", "R"):
loop.after_step_hook = refresh
def lerpi(a: int, b: int, t: float) -> int:
return int(a + t * (b - a))
def rgb(r: int, g: int, b: int) -> int:
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)
def blend(ca: int, cb: int, t: float) -> int:
return rgb(
lerpi((ca >> 8) & 0xF8, (cb >> 8) & 0xF8, t),
lerpi((ca >> 3) & 0xFC, (cb >> 3) & 0xFC, t),
lerpi((ca << 3) & 0xF8, (cb << 3) & 0xF8, t),
)
# import style later to avoid circular dep
from trezor.ui import style # isort:skip
@ -76,11 +53,6 @@ from trezor.ui import style # isort:skip
from trezor.ui.style import * # isort:skip # noqa: F401,F403
def pulse(period: int, offset: int = 0) -> float:
# normalize sin from interval -1:1 to 0:1
return 0.5 + 0.5 * math.sin(2 * math.pi * (utime.ticks_us() + offset) / period)
async def _alert(count: int) -> None:
short_sleep = loop.sleep(20)
long_sleep = loop.sleep(80)
@ -157,62 +129,6 @@ def get_header_height() -> int:
return MODEL_HEADER_HEIGHTS[utils.MODEL]
def draw_simple(t: "Component") -> None:
"""Render a component synchronously.
Useful when you need to put something on screen and go on to do other things.
This function bypasses the UI workflow engine, so other layouts will not know
that something was drawn over them. In particular, if no other Layout is shown
in a workflow, the homescreen will not redraw when the workflow is finished.
Make sure you use `workflow.close_others()` before invoking this function
(note that `workflow.close_others()` is implicitly called with `button_request()`).
"""
backlight_fade(style.BACKLIGHT_DIM)
display.clear()
t.on_render()
refresh()
backlight_fade(style.BACKLIGHT_NORMAL)
def grid(
i: int, # i-th cell of the table of which we wish to return Area (snake-like starting with 0)
n_x: int = 3, # number of rows in the table
n_y: int = 5, # number of columns in the table
start_x: int = VIEWX, # where the table starts on x-axis
start_y: int = _VIEWY, # where the table starts on y-axis
end_x: int = (WIDTH - VIEWX), # where the table ends on x-axis
end_y: int = (HEIGHT - _VIEWY), # where the table ends on y-axis
cells_x: int = 1, # number of cells to be merged into one in the direction of x-axis
cells_y: int = 1, # number of cells to be merged into one in the direction of y-axis
spacing: int = 0, # spacing size between cells
) -> Area:
"""
Returns area (tuple of four integers, in pixels) of a cell on i-th position
in a table you define yourself. Example:
>>> ui.grid(4, n_x=2, n_y=3, start_x=20, start_y=20)
(20, 160, 107, 70)
Returns 5th cell from the following table. It has two columns, three rows
and starts on coordinates 20-20.
|____|____|
|____|____|
|XXXX|____|
"""
w = (end_x - start_x) // n_x
h = (end_y - start_y) // n_y
x = (i % n_x) * w
y = (i // n_x) * h
return (x + start_x, y + start_y, (w - spacing) * cells_x, (h - spacing) * cells_y)
def in_area(area: Area, x: int, y: int) -> bool:
ax, ay, aw, ah = area
return ax <= x < ax + aw and ay <= y < ay + ah
# Component events. Should be different from `io.TOUCH_*` events.
# Event dispatched when components should draw to the display, if they are
# marked for re-paint.

@ -8,14 +8,3 @@ SWIPE_UP = const(0x01)
SWIPE_DOWN = const(0x02)
SWIPE_LEFT = const(0x04)
SWIPE_RIGHT = const(0x08)
def break_path_to_lines(path_str: str, per_line: int) -> list[str]:
lines = []
while len(path_str) > per_line:
i = path_str[:per_line].rfind("/")
lines.append(path_str[:i])
path_str = path_str[i:]
lines.append(path_str)
return lines

@ -1,17 +1,17 @@
from typing import TYPE_CHECKING
from trezor import loop, ui, wire
from trezor import ui, wire
import trezorui2
if TYPE_CHECKING:
from typing import Callable, Any, Awaitable, TypeVar
T = TypeVar("T")
CONFIRMED = object()
CANCELLED = object()
INFO = object()
GO_BACK = object()
SHOW_PAGINATED = object()
CONFIRMED = trezorui2.CONFIRMED
CANCELLED = trezorui2.CANCELLED
INFO = trezorui2.INFO
def is_confirmed(x: Any) -> bool:
@ -39,43 +39,6 @@ async def is_confirmed_info(
return is_confirmed(result)
class ConfirmBase(ui.Layout):
def __init__(
self,
content: ui.Component,
confirm: ui.Component | None = None,
cancel: ui.Component | None = None,
) -> None:
super().__init__()
self.content = content
self.confirm = confirm
self.cancel = cancel
def dispatch(self, event: int, x: int, y: int) -> None:
super().dispatch(event, x, y)
self.content.dispatch(event, x, y)
if self.confirm is not None:
self.confirm.dispatch(event, x, y)
if self.cancel is not None:
self.cancel.dispatch(event, x, y)
def on_confirm(self) -> None:
raise ui.Result(CONFIRMED)
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
if __debug__:
def read_content(self) -> list[str]:
return self.content.read_content()
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import confirm_signal
return super().create_tasks() + (confirm_signal(),)
class Pageable:
def __init__(self) -> None:
self._page = 0

@ -1,536 +0,0 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor import ui
from ...constants import (
PAGINATION_MARGIN_RIGHT,
TEXT_HEADER_HEIGHT,
TEXT_LINE_HEIGHT,
TEXT_LINE_HEIGHT_HALF,
TEXT_MARGIN_LEFT,
TEXT_MAX_LINES,
TEXT_MAX_LINES_NO_HEADER,
)
LINE_WIDTH = ui.WIDTH - TEXT_MARGIN_LEFT
LINE_WIDTH_PAGINATED = LINE_WIDTH - PAGINATION_MARGIN_RIGHT
if TYPE_CHECKING:
from typing import Any, Sequence
TextContent = str | int
# needs to be different from all colors and font ids
BR = const(-256)
BR_HALF = const(-257)
_FONTS = (ui.NORMAL, ui.BOLD, ui.MONO)
DASH_WIDTH = ui.display.text_width("-", ui.BOLD)
class Span:
def __init__(
self,
string: str = "",
start: int = 0,
font: int = ui.NORMAL,
line_width: int = LINE_WIDTH,
offset_x: int = 0,
break_words: bool = False,
) -> None:
self.reset(string, start, font, line_width, offset_x, break_words)
def reset(
self,
string: str,
start: int,
font: int,
line_width: int = LINE_WIDTH,
offset_x: int = 0,
break_words: bool = False,
) -> None:
self.string = string
self.start = start
self.font = font
self.line_width = line_width
self.offset_x = offset_x
self.break_words = break_words
self.length = 0
self.width = 0
self.word_break = False
self.advance_whitespace = False
def count_lines(self) -> int:
"""Get a number of lines in the specified string.
Should be used with a cleanly reset span. Leaves the span in the final position.
"""
n_lines = 0
while self.next_line():
n_lines += 1
# deal with trailing newlines: if the final span does not have any content,
# do not count it
if self.length > 0:
n_lines += 1
return n_lines
def has_more_content(self) -> bool:
"""Look ahead to check if there is more content after the current span is
consumed.
"""
start = self.start + self.length
if self.advance_whitespace:
start += 1
return start < len(self.string)
def next_line(self) -> bool:
"""Advance the span to point to contents of the next line.
Returns True if the renderer should make newline afterwards, False if this is
the end of the text.
Within the renderer, we use this as:
>>> while span.next_line():
>>> render_the_line(span)
>>> go_to_next_line()
>>> render_the_line(span) # final line without linebreak
This is unsuitable for other uses however. To count lines (as in
`apps.common.layout.paginate_text`), use instead:
>>> while span.has_more_content():
>>> span.next_line()
"""
# We are making copies of most class variables so that the lookup is faster.
# This also allows us to pick defaults independently of the current status
string = self.string
start = self.start + self.length
line_width = self.line_width - self.offset_x
break_words = self.break_words
font = self.font
self.offset_x = 0
width = 0
result_width = 0
length = 0
if start >= len(string):
return False
# advance over the left-over whitespace character from last time
if self.advance_whitespace:
start += 1
word_break = True
advance_whitespace = False
for i in range(len(string) - start):
nextchar_width = ui.display.text_width(string[start + i], font)
if string[start + i] in " \n":
word_break = False
length = i # break is _before_ the whitespace
advance_whitespace = True
result_width = width
if string[start + i] == "\n":
# do not continue over newline
break
elif width + nextchar_width > line_width:
# this char would overflow the line. end loop, use last result
break
elif (
break_words or word_break
) and width + nextchar_width + DASH_WIDTH <= line_width:
# Trying a possible break in the middle of a word.
# We can do this if:
# - we haven't found a space yet (word_break is still True) -- if a word
# doesn't fit on a single line, this will place a break in it
# - we are allowed to break words (break_words is True)
# AND the current character and a word-break dash will fit on the line.
result_width = width + nextchar_width
length = i + 1 # break is _after_ current character
advance_whitespace = False
word_break = True
width += nextchar_width
else:
# whole string (from offset) fits
word_break = False
advance_whitespace = False
result_width = width
length = len(string) - start
self.start = start
self.length = length
self.width = result_width
self.word_break = word_break
self.advance_whitespace = advance_whitespace
return start + length < len(string)
_WORKING_SPAN = Span()
def render_text(
items: Sequence[TextContent],
new_lines: bool,
max_lines: int,
font: int = ui.NORMAL,
fg: int = ui.FG,
bg: int = ui.BG,
offset_x: int = TEXT_MARGIN_LEFT,
offset_y: int = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT,
line_width: int = ui.WIDTH - TEXT_MARGIN_LEFT,
item_offset: int = 0,
char_offset: int = 0,
break_words: bool = False,
render_page_overflow: bool = True,
) -> None:
"""Render a sequence of items on screen.
The items can either be strings, or rendering instructions specified as ints.
They can change font, insert an explicit linebreak, or change color of the following
text.
If `new_lines` is true, a linebreak is rendered after every string. In effect, the
following calls are equivalent:
>>> render_text(["hello", "world"], new_lines=True)
>>> render_text(["hello\nworld"], new_lines=False)
TODO, we should get rid of all cases that use `new_lines=True`
If the rendered text ends up longer than `max_lines`, a trailing "..." is rendered
at end. This indicates to the user that the full contents have not been shown.
It is possible to override this behavior via `render_page_overflow` argument --
if false, the trailing "..." is not shown. This is useful when the rendered text is
in fact paginated.
`font` specifies the default font, but that can be overridden by font instructions
in `items`.
`fg` specifies default foreground color, which can also be overridden by instructions
in `items`.
`bg` specifies background color. This cannot be overridden.
`offset_x` and `offset_y` specify starting XY position of the text bounding box.
`line_width` specifies width of the bounding box. Height of the bounding box is
calculated as `max_lines * TEXT_LINE_HEIGHT`.
`item_offset` and `char_offset` must be specified together. Item offset specifies
the first element of `items` which should be considered, and char offset specifies
the first character of the indicated item which should be considered.
The purpose is to allow rendering different "pages" of text, using the same `items`
argument (slicing the list could be expensive in terms of memory).
If `break_words` is false (default), linebreaks will only be rendered (a) at
whitespace, or (b) in case a word does not fit on a single line. If true, whitespace
is ignored and linebreaks are inserted after the last character that fits.
"""
# initial rendering state
INITIAL_OFFSET_X = offset_x
offset_y_max = offset_y + (TEXT_LINE_HEIGHT * (max_lines - 1))
span = _WORKING_SPAN
# scan through up to item_offset so that the current font & color is up to date
for item_index in range(item_offset):
item = items[item_index]
if isinstance(item, int):
if item is BR or item is BR_HALF:
# do nothing
pass
elif item in _FONTS:
font = item
else:
fg = item
for item_index in range(item_offset, len(items)):
# load current item
item = items[item_index]
if isinstance(item, int):
if item is BR or item is BR_HALF:
# line break or half-line break
if offset_y > offset_y_max:
if render_page_overflow:
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
return
offset_x = INITIAL_OFFSET_X
offset_y += TEXT_LINE_HEIGHT if item is BR else TEXT_LINE_HEIGHT_HALF
elif item in _FONTS:
# change of font style
font = item
else:
# change of foreground color
fg = item
continue
# XXX hack:
# if the upcoming word does not fit on this line but fits on the following,
# render it after a linebreak
item_width = ui.display.text_width(item, font)
if (
item_width <= line_width # pylint: disable=chained-comparison
and item_width + offset_x - INITIAL_OFFSET_X > line_width
and "\n" not in item
):
offset_y += TEXT_LINE_HEIGHT
ui.display.text(INITIAL_OFFSET_X, offset_y, item, font, fg, bg)
offset_x = INITIAL_OFFSET_X + item_width
continue
span.reset(
item,
char_offset,
font,
line_width=line_width,
offset_x=offset_x - INITIAL_OFFSET_X,
break_words=break_words,
)
char_offset = 0
while span.next_line():
ui.display.text(
offset_x, offset_y, item, font, fg, bg, span.start, span.length
)
end_of_page = offset_y >= offset_y_max
have_more_content = span.has_more_content() or item_index < len(items) - 1
if end_of_page and have_more_content and render_page_overflow:
ui.display.text(
offset_x + span.width, offset_y, "...", ui.BOLD, ui.GREY, bg
)
elif span.word_break:
ui.display.text(
offset_x + span.width, offset_y, "-", ui.BOLD, ui.GREY, bg
)
if end_of_page:
return
offset_x = INITIAL_OFFSET_X
offset_y += TEXT_LINE_HEIGHT
# render last chunk
ui.display.text(offset_x, offset_y, item, font, fg, bg, span.start, span.length)
if new_lines:
offset_x = INITIAL_OFFSET_X
offset_y += TEXT_LINE_HEIGHT
elif span.width > 0:
# only advance cursor if we actually rendered anything
offset_x += span.width
if __debug__:
class DisplayMock:
"""Mock Display class that stores rendered text in an array.
Used to extract data for unit tests.
"""
def __init__(self) -> None:
self.screen_contents: list[str] = []
self.orig_display = ui.display
def __getattr__(self, key: str) -> Any:
return getattr(self.orig_display, key)
def __enter__(self) -> None:
ui.display = self
def __exit__(self, exc: Any, exc_type: Any, tb: Any) -> None:
ui.display = self.orig_display
def text(
self,
offset_x: int,
offset_y: int,
string: str,
font: int,
fg: int,
bg: int,
start: int = 0,
length: int | None = None,
) -> None:
if length is None:
length = len(string) - start
self.screen_contents.append(string[start : start + length])
class TextBase(ui.Component):
def __init__(
self,
header_text: str | None,
header_icon: str = ui.ICON_DEFAULT,
icon_color: int = ui.ORANGE_ICON,
max_lines: int | None = None,
new_lines: bool = True,
break_words: bool = False,
render_page_overflow: bool = True,
content_offset: int = 0,
char_offset: int = 0,
line_width: int = ui.WIDTH - TEXT_MARGIN_LEFT,
):
super().__init__()
self.header_text = header_text
self.header_icon = header_icon
self.icon_color = icon_color
if max_lines is None:
self.max_lines = (
TEXT_MAX_LINES_NO_HEADER if self.header_text is None else TEXT_MAX_LINES
)
else:
self.max_lines = max_lines
self.new_lines = new_lines
self.break_words = break_words
self.render_page_overflow = render_page_overflow
self.content: list[TextContent] = []
self.content_offset = content_offset
self.char_offset = char_offset
self.line_width = line_width
def normal(self, *content: TextContent) -> None:
self.content.append(ui.NORMAL)
self.content.extend(content)
def bold(self, *content: TextContent) -> None:
self.content.append(ui.BOLD)
self.content.extend(content)
def mono(self, *content: TextContent) -> None:
self.content.append(ui.MONO)
self.content.extend(content)
def br(self) -> None:
self.content.append(BR)
def br_half(self) -> None:
self.content.append(BR_HALF)
def format_parametrized(
self,
format_string: str,
*params: str,
font: int = ui.NORMAL,
param_font: int = ui.BOLD,
) -> None:
parts = format_string.split("{}", len(params))
for i in range(len(parts)): # pylint: disable=consider-using-enumerate
self.content.append(font)
self.content.append(parts[i])
if i < len(parts) - 1 and i < len(params):
param = params[i]
self.content.append(param_font)
self.content.append(param)
def on_render(self) -> None:
pass
if __debug__:
def read_content(self) -> list[str]:
display_mock = DisplayMock()
should_repaint = self.repaint
try:
with display_mock:
self.repaint = True
self.on_render()
finally:
self.repaint = should_repaint
return [self.header_text or ""] + display_mock.screen_contents
LABEL_LEFT = const(0)
LABEL_CENTER = const(1)
LABEL_RIGHT = const(2)
class Label(ui.Component):
def __init__(
self,
area: ui.Area,
content: str,
align: int = LABEL_LEFT,
style: int = ui.NORMAL,
) -> None:
super().__init__()
self.area = area
self.content = content
self.align = align
self.style = style
def on_render(self) -> None:
if self.repaint:
align = self.align
ax, ay, aw, ah = self.area
ui.display.bar(ax, ay, aw, ah, ui.BG)
tx = ax + aw // 2
ty = ay + ah // 2 + 8
if align is LABEL_LEFT:
ui.display.text(tx, ty, self.content, self.style, ui.FG, ui.BG)
elif align is LABEL_CENTER:
ui.display.text_center(tx, ty, self.content, self.style, ui.FG, ui.BG)
elif align is LABEL_RIGHT:
ui.display.text_right(tx, ty, self.content, self.style, ui.FG, ui.BG)
self.repaint = False
if __debug__:
def read_content(self) -> list[str]:
return [self.content]
def text_center_trim_left(
x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16
) -> None:
if ui.display.text_width(text, font) <= width:
ui.display.text_center(x, y, text, font, ui.FG, ui.BG)
return
ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD)
if width < ELLIPSIS_WIDTH:
return
text_length = 0
for i in range(1, len(text)):
if ui.display.text_width(text[-i:], font) + ELLIPSIS_WIDTH > width:
text_length = i - 1
break
text_width = ui.display.text_width(text[-text_length:], font)
x -= (text_width + ELLIPSIS_WIDTH) // 2
ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG)
x += ELLIPSIS_WIDTH
ui.display.text(x, y, text[-text_length:], font, ui.FG, ui.BG)
def text_center_trim_right(
x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16
) -> None:
if ui.display.text_width(text, font) <= width:
ui.display.text_center(x, y, text, font, ui.FG, ui.BG)
return
ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD)
if width < ELLIPSIS_WIDTH:
return
text_length = 0
for i in range(1, len(text)):
if ui.display.text_width(text[:i], font) + ELLIPSIS_WIDTH > width:
text_length = i - 1
break
text_width = ui.display.text_width(text[:text_length], font)
x -= (text_width + ELLIPSIS_WIDTH) // 2
ui.display.text(x, y, text[:text_length], font, ui.FG, ui.BG)
x += text_width
ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG)

@ -1,253 +0,0 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor import ui
from trezor.ui import display, in_area
if TYPE_CHECKING:
ButtonContent = str | bytes
ButtonStyleType = type["ButtonDefault"]
ButtonStyleStateType = type["ButtonDefault.normal"]
class ButtonDefault:
class normal:
bg_color = ui.BLACKISH
fg_color = ui.FG
text_style = ui.BOLD
border_color = ui.BG
radius = ui.RADIUS
class active(normal):
bg_color = ui.FG
fg_color = ui.BLACKISH
text_style = ui.BOLD
border_color = ui.FG
radius = ui.RADIUS
class disabled(normal):
bg_color = ui.BG
fg_color = ui.GREY
text_style = ui.NORMAL
border_color = ui.BG
radius = ui.RADIUS
class ButtonMono(ButtonDefault):
class normal(ButtonDefault.normal):
text_style = ui.MONO
class active(ButtonDefault.active):
text_style = ui.MONO
class disabled(ButtonDefault.disabled):
text_style = ui.MONO
class ButtonMonoDark(ButtonDefault):
class normal:
bg_color = ui.DARK_BLACK
fg_color = ui.DARK_WHITE
text_style = ui.MONO
border_color = ui.BG
radius = ui.RADIUS
class active(normal):
bg_color = ui.FG
fg_color = ui.DARK_BLACK
text_style = ui.MONO
border_color = ui.FG
radius = ui.RADIUS
class disabled(normal):
bg_color = ui.DARK_BLACK
fg_color = ui.GREY
text_style = ui.MONO
border_color = ui.BG
radius = ui.RADIUS
class ButtonConfirm(ButtonDefault):
class normal(ButtonDefault.normal):
bg_color = ui.GREEN
class active(ButtonDefault.active):
fg_color = ui.GREEN
class ButtonCancel(ButtonDefault):
class normal(ButtonDefault.normal):
bg_color = ui.RED
class active(ButtonDefault.active):
fg_color = ui.RED
class ButtonAbort(ButtonDefault):
class normal(ButtonDefault.normal):
bg_color = ui.DARK_GREY
class active(ButtonDefault.active):
fg_color = ui.DARK_GREY
class ButtonClear(ButtonDefault):
class normal(ButtonDefault.normal):
bg_color = ui.ORANGE
class active(ButtonDefault.active):
fg_color = ui.ORANGE
class ButtonMonoConfirm(ButtonDefault):
class normal(ButtonDefault.normal):
text_style = ui.MONO
bg_color = ui.GREEN
class active(ButtonDefault.active):
text_style = ui.MONO
fg_color = ui.GREEN
class disabled(ButtonDefault.disabled):
text_style = ui.MONO
# button states
_INITIAL = const(0)
_PRESSED = const(1)
_RELEASED = const(2)
_DISABLED = const(3)
# button constants
_ICON = const(16) # icon size in pixels
_BORDER = const(4) # border size in pixels
class Button(ui.Component):
def __init__(
self,
area: ui.Area,
content: ButtonContent,
style: ButtonStyleType = ButtonDefault,
) -> None:
super().__init__()
if isinstance(content, str):
self.text = content
self.icon = b""
elif isinstance(content, bytes):
self.icon = content
self.text = ""
else:
raise TypeError
self.area = area
self.normal_style = style.normal
self.active_style = style.active
self.disabled_style = style.disabled
self.state = _INITIAL
def enable(self) -> None:
if self.state is not _INITIAL:
self.state = _INITIAL
self.repaint = True
def disable(self) -> None:
if self.state is not _DISABLED:
self.state = _DISABLED
self.repaint = True
def on_render(self) -> None:
if self.repaint:
if self.state is _INITIAL or self.state is _RELEASED:
s = self.normal_style
elif self.state is _DISABLED:
s = self.disabled_style
elif self.state is _PRESSED:
s = self.active_style
else:
raise RuntimeError # invalid state
ax, ay, aw, ah = self.area
self.render_background(s, ax, ay, aw, ah)
self.render_content(s, ax, ay, aw, ah)
self.repaint = False
def render_background(
self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int
) -> None:
radius = s.radius
bg_color = s.bg_color
border_color = s.border_color
if border_color == bg_color:
# we don't need to render the border
display.bar_radius(ax, ay, aw, ah, bg_color, ui.BG, radius)
else:
# render border and background on top of it
display.bar_radius(ax, ay, aw, ah, border_color, ui.BG, radius)
display.bar_radius(
ax + _BORDER,
ay + _BORDER,
aw - _BORDER * 2,
ah - _BORDER * 2,
bg_color,
border_color,
radius,
)
def render_content(
self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int
) -> None:
tx = ax + aw // 2
ty = ay + ah // 2 + 8
t = self.text
if t:
display.text_center(tx, ty, t, s.text_style, s.fg_color, s.bg_color)
return
i = self.icon
if i:
display.icon(tx - _ICON // 2, ty - _ICON, i, s.fg_color, s.bg_color)
return
def on_touch_start(self, x: int, y: int) -> None:
if self.state is _DISABLED:
return
if in_area(self.area, x, y):
self.state = _PRESSED
self.repaint = True
self.on_press_start()
def on_touch_move(self, x: int, y: int) -> None:
if self.state is _DISABLED:
return
if in_area(self.area, x, y):
if self.state is _RELEASED:
self.state = _PRESSED
self.repaint = True
self.on_press_start()
else:
if self.state is _PRESSED:
self.state = _RELEASED
self.repaint = True
self.on_press_end()
def on_touch_end(self, x: int, y: int) -> None:
state = self.state
if state is not _INITIAL and state is not _DISABLED:
self.state = _INITIAL
self.repaint = True
if in_area(self.area, x, y):
if state is _PRESSED:
self.on_press_end()
self.on_click()
def on_press_start(self) -> None:
pass
def on_press_end(self) -> None:
pass
def on_click(self) -> None:
pass
if __debug__:
def read_content(self) -> list[str]:
return [f"<Button: {self.text}>"]

@ -1,67 +0,0 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor import res, ui
from ...constants import TEXT_HEADER_HEIGHT, TEXT_LINE_HEIGHT
if TYPE_CHECKING:
from typing import Iterable
ChecklistItem = str | Iterable[str]
_CHECKLIST_MAX_LINES = const(5)
_CHECKLIST_OFFSET_X = const(24)
_CHECKLIST_OFFSET_X_ICON = const(0)
class Checklist(ui.Component):
def __init__(self, title: str, icon: str) -> None:
super().__init__()
self.title = title
self.icon = icon
self.items: list[ChecklistItem] = []
self.active = 0
def add(self, item: ChecklistItem) -> None:
self.items.append(item)
def select(self, active: int) -> None:
self.active = active
def on_render(self) -> None:
if self.repaint:
ui.header(self.title, self.icon)
self.render_items()
self.repaint = False
def render_items(self) -> None:
offset_x = _CHECKLIST_OFFSET_X
offset_y = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT
bg = ui.BG
for index, item in enumerate(self.items):
# compute font and color
if index < self.active:
font = ui.BOLD
fg = ui.GREEN
elif index == self.active:
font = ui.BOLD
fg = ui.FG
else: # index > self.active
font = ui.NORMAL
fg = ui.GREY
# render item icon in past items
if index < self.active:
icon = res.load(ui.ICON_CONFIRM)
ui.display.icon(0, offset_y - 14, icon, fg, bg)
# render item text
if isinstance(item, str):
ui.display.text(offset_x, offset_y, item, font, fg, bg)
offset_y += TEXT_LINE_HEIGHT
else:
for line in item:
ui.display.text(offset_x, offset_y, line, font, fg, bg)
offset_y += TEXT_LINE_HEIGHT

@ -1,257 +0,0 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor import loop, res, ui, utils
from trezor.ui.loader import Loader, LoaderDefault
from ..common.confirm import CANCELLED, CONFIRMED, INFO, ConfirmBase, Pageable
from .button import Button, ButtonAbort, ButtonCancel, ButtonConfirm, ButtonDefault
if TYPE_CHECKING:
from typing import Any
from .button import ButtonContent, ButtonStyleType
from trezor.ui.loader import LoaderStyleType
class Confirm(ConfirmBase):
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
DEFAULT_CONFIRM_STYLE = ButtonConfirm
DEFAULT_CANCEL = res.load(ui.ICON_CANCEL)
DEFAULT_CANCEL_STYLE = ButtonCancel
def __init__(
self,
content: ui.Component,
confirm: ButtonContent | None = DEFAULT_CONFIRM,
confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE,
cancel: ButtonContent | None = DEFAULT_CANCEL,
cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE,
major_confirm: bool = False,
) -> None:
self.content = content
button_confirm: Button | None = None
button_cancel: Button | None = None
if confirm is not None:
if cancel is None:
area = ui.grid(4, n_x=1)
elif major_confirm:
area = ui.grid(13, cells_x=2)
else:
area = ui.grid(9, n_x=2)
button_confirm = Button(area, confirm, confirm_style)
button_confirm.on_click = self.on_confirm
if cancel is not None:
if confirm is None:
area = ui.grid(4, n_x=1)
elif major_confirm:
area = ui.grid(12, cells_x=1)
else:
area = ui.grid(8, n_x=2)
button_cancel = Button(area, cancel, cancel_style)
button_cancel.on_click = self.on_cancel
super().__init__(content, button_confirm, button_cancel)
class ConfirmPageable(Confirm):
def __init__(self, pageable: Pageable, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.pageable = pageable
async def handle_paging(self) -> None:
from .swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, SWIPE_RIGHT, Swipe
if self.pageable.is_first():
directions = SWIPE_LEFT
elif self.pageable.is_last():
directions = SWIPE_RIGHT
else:
directions = SWIPE_HORIZONTAL
if __debug__:
from apps.debug import swipe_signal
swipe = await loop.race(Swipe(directions), swipe_signal())
else:
swipe = await Swipe(directions)
if swipe == SWIPE_LEFT:
self.pageable.next()
else:
self.pageable.prev()
self.content.repaint = True
if self.confirm is not None:
self.confirm.repaint = True
if self.cancel is not None:
self.cancel.repaint = True
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
tasks = super().create_tasks()
if self.pageable.page_count() > 1:
return tasks + (self.handle_paging(),)
else:
return tasks
def on_render(self) -> None:
PULSE_PERIOD = const(1_200_000)
super().on_render()
if not self.pageable.is_first():
t = ui.pulse(PULSE_PERIOD)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE_RIGHT)
if utils.DISABLE_ANIMATION:
ui.display.icon(18, 68, icon, ui.GREY, ui.BG)
else:
ui.display.icon(18, 68, icon, c, ui.BG)
if not self.pageable.is_last():
t = ui.pulse(PULSE_PERIOD, PULSE_PERIOD // 2)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE_LEFT)
if utils.DISABLE_ANIMATION:
ui.display.icon(205, 68, icon, ui.GREY, ui.BG)
else:
ui.display.icon(205, 68, icon, c, ui.BG)
class InfoConfirm(ui.Layout):
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
DEFAULT_CONFIRM_STYLE = ButtonConfirm
DEFAULT_CANCEL = res.load(ui.ICON_CANCEL)
DEFAULT_CANCEL_STYLE = ButtonCancel
DEFAULT_INFO = res.load(ui.ICON_CLICK) # TODO: this should be (i) icon, not click
DEFAULT_INFO_STYLE = ButtonDefault
def __init__(
self,
content: ui.Component,
confirm: ButtonContent = DEFAULT_CONFIRM,
confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE,
cancel: ButtonContent = DEFAULT_CANCEL,
cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE,
info: ButtonContent = DEFAULT_INFO,
info_style: ButtonStyleType = DEFAULT_INFO_STYLE,
) -> None:
super().__init__()
self.content = content
self.confirm = Button(ui.grid(14), confirm, confirm_style)
self.confirm.on_click = self.on_confirm
self.info = Button(ui.grid(13), info, info_style)
self.info.on_click = self.on_info
self.cancel = Button(ui.grid(12), cancel, cancel_style)
self.cancel.on_click = self.on_cancel
def dispatch(self, event: int, x: int, y: int) -> None:
self.content.dispatch(event, x, y)
if self.confirm is not None:
self.confirm.dispatch(event, x, y)
if self.cancel is not None:
self.cancel.dispatch(event, x, y)
if self.info is not None:
self.info.dispatch(event, x, y)
def on_confirm(self) -> None:
raise ui.Result(CONFIRMED)
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
def on_info(self) -> None:
raise ui.Result(INFO)
if __debug__:
def read_content(self) -> list[str]:
return self.content.read_content()
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import confirm_signal
return super().create_tasks() + (confirm_signal(),)
class HoldToConfirm(ui.Layout):
DEFAULT_CONFIRM = "Hold to confirm"
DEFAULT_CONFIRM_STYLE = ButtonConfirm
DEFAULT_LOADER_STYLE = LoaderDefault
def __init__(
self,
content: ui.Component,
confirm: str = DEFAULT_CONFIRM,
confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE,
loader_style: LoaderStyleType = DEFAULT_LOADER_STYLE,
cancel: bool = True,
):
super().__init__()
self.content = content
self.loader = Loader(loader_style)
self.loader.on_start = self._on_loader_start
if cancel:
self.confirm = Button(ui.grid(17, n_x=4, cells_x=3), confirm, confirm_style)
else:
self.confirm = Button(ui.grid(4, n_x=1), confirm, confirm_style)
self.confirm.on_press_start = self._on_press_start
self.confirm.on_press_end = self._on_press_end
self.confirm.on_click = self._on_click
self.cancel = None
if cancel:
self.cancel = Button(
ui.grid(16, n_x=4), res.load(ui.ICON_CANCEL), ButtonAbort
)
self.cancel.on_click = self.on_cancel
def _on_press_start(self) -> None:
self.loader.start()
def _on_press_end(self) -> None:
self.loader.stop()
def _on_loader_start(self) -> None:
# Loader has either started growing, or returned to the 0-position.
# In the first case we need to clear the content leftovers, in the latter
# we need to render the content again.
ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT - 58, ui.BG)
self.content.dispatch(ui.REPAINT, 0, 0)
def _on_click(self) -> None:
if self.loader.elapsed_ms() >= self.loader.target_ms:
self.on_confirm()
def dispatch(self, event: int, x: int, y: int) -> None:
if self.loader.start_ms is not None:
if utils.DISABLE_ANIMATION:
self.on_confirm()
self.loader.dispatch(event, x, y)
else:
self.content.dispatch(event, x, y)
self.confirm.dispatch(event, x, y)
if self.cancel:
self.cancel.dispatch(event, x, y)
def on_confirm(self) -> None:
raise ui.Result(CONFIRMED)
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
if __debug__:
def read_content(self) -> list[str]:
return self.content.read_content()
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import confirm_signal
return super().create_tasks() + (confirm_signal(),)

@ -1,87 +0,0 @@
from typing import TYPE_CHECKING
from trezor import loop, res, ui
from ...constants import TEXT_LINE_HEIGHT, TEXT_MARGIN_LEFT
from .button import Button, ButtonConfirm
from .confirm import CONFIRMED
from .text import render_text
if TYPE_CHECKING:
from .button import ButtonContent
InfoConfirmStyleType = type["DefaultInfoConfirm"]
class DefaultInfoConfirm:
fg_color = ui.LIGHT_GREY
bg_color = ui.BLACKISH
class button(ButtonConfirm):
class normal(ButtonConfirm.normal):
border_color = ui.BLACKISH
class disabled(ButtonConfirm.disabled):
border_color = ui.BLACKISH
class InfoConfirm(ui.Layout):
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
DEFAULT_STYLE = DefaultInfoConfirm
def __init__(
self,
text: str,
confirm: ButtonContent = DEFAULT_CONFIRM,
style: InfoConfirmStyleType = DEFAULT_STYLE,
) -> None:
super().__init__()
self.text = [text]
self.style = style
panel_area = ui.grid(0, n_x=1, n_y=1)
self.panel_area = panel_area
confirm_area = ui.grid(4, n_x=1)
self.confirm = Button(confirm_area, confirm, style.button)
self.confirm.on_click = self.on_confirm
def dispatch(self, event: int, x: int, y: int) -> None:
if event == ui.RENDER:
self.on_render()
self.confirm.dispatch(event, x, y)
def on_render(self) -> None:
if self.repaint:
x, y, w, h = self.panel_area
fg_color = self.style.fg_color
bg_color = self.style.bg_color
# render the background panel
ui.display.bar_radius(x, y, w, h, bg_color, ui.BG, ui.RADIUS)
# render the info text
render_text(
self.text,
new_lines=False,
max_lines=6,
offset_y=y + TEXT_LINE_HEIGHT,
offset_x=x + TEXT_MARGIN_LEFT - ui.VIEWX,
line_width=w - TEXT_MARGIN_LEFT,
fg=fg_color,
bg=bg_color,
)
self.repaint = False
def on_confirm(self) -> None:
raise ui.Result(CONFIRMED)
if __debug__:
def read_content(self) -> list[str]:
return self.text
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import confirm_signal
return super().create_tasks() + (confirm_signal(),)

@ -1,220 +0,0 @@
from typing import TYPE_CHECKING
from trezor import io, loop, res, ui, workflow
from trezor.crypto import bip39
from trezor.ui import display
from .button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
if TYPE_CHECKING:
from .button import ButtonContent, ButtonStyleStateType
def compute_mask(text: str) -> int:
mask = 0
for c in text:
shift = ord(c) - 97 # ord('a') == 97
if shift < 0:
continue
mask |= 1 << shift
return mask
class KeyButton(Button):
def __init__(
self, area: ui.Area, content: ButtonContent, keyboard: "Bip39Keyboard"
):
self.keyboard = keyboard
super().__init__(area, content)
def on_click(self) -> None:
self.keyboard.on_key_click(self)
class InputButton(Button):
def __init__(self, area: ui.Area, text: str, word: str) -> None:
super().__init__(area, text)
self.word = word
self.pending = False
self.disable()
def edit(self, text: str, word: str, pending: bool) -> None:
self.word = word
self.text = text
self.pending = pending
self.repaint = True
if word:
if text == word: # confirm button
self.enable()
self.normal_style = ButtonMonoConfirm.normal
self.active_style = ButtonMonoConfirm.active
self.icon = res.load(ui.ICON_CONFIRM)
else: # auto-complete button
self.enable()
self.normal_style = ButtonMono.normal
self.active_style = ButtonMono.active
self.icon = res.load(ui.ICON_CLICK)
else: # disabled button
self.disabled_style = ButtonMono.disabled
self.disable()
self.icon = b""
def render_content(
self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int
) -> None:
text_style = s.text_style
fg_color = s.fg_color
bg_color = s.bg_color
tx = ax + 16 # x-offset of the content
ty = ay + ah // 2 + 8 # y-offset of the content
# entered content
display.text(tx, ty, self.text, text_style, fg_color, bg_color)
# word suggestion
suggested_word = self.word[len(self.text) :]
width = display.text_width(self.text, text_style)
display.text(tx + width, ty, suggested_word, text_style, ui.GREY, bg_color)
if self.pending:
pw = display.text_width(self.text[-1:], text_style)
px = tx + width - pw
display.bar(px, ty + 2, pw + 1, 3, fg_color)
if self.icon:
ix = ax + aw - 16 * 2
iy = ty - 16
display.icon(ix, iy, self.icon, fg_color, bg_color)
class Prompt(ui.Component):
def __init__(self, prompt: str) -> None:
super().__init__()
self.prompt = prompt
def on_render(self) -> None:
if self.repaint:
display.bar(0, 8, ui.WIDTH, 60, ui.BG)
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
self.repaint = False
class Bip39Keyboard(ui.Layout):
def __init__(self, prompt: str) -> None:
super().__init__()
self.prompt = Prompt(prompt)
icon_back = res.load(ui.ICON_BACK)
self.back = Button(ui.grid(0, n_x=3, n_y=4), icon_back, ButtonClear)
self.back.on_click = self.on_back_click
self.input = InputButton(ui.grid(1, n_x=3, n_y=4, cells_x=2), "", "")
self.input.on_click = self.on_input_click
self.keys = [
KeyButton(ui.grid(i + 3, n_y=4), k, self)
for i, k in enumerate(
("abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz")
)
]
self.pending_button: Button | None = None
self.pending_index = 0
def dispatch(self, event: int, x: int, y: int) -> None:
for btn in self.keys:
btn.dispatch(event, x, y)
if self.input.text:
self.input.dispatch(event, x, y)
self.back.dispatch(event, x, y)
else:
self.prompt.dispatch(event, x, y)
def on_back_click(self) -> None:
# Backspace was clicked, let's delete the last character of input.
self.edit(self.input.text[:-1])
def on_input_click(self) -> None:
# Input button was clicked. If the content matches the suggested word,
# let's confirm it, otherwise just auto-complete.
text = self.input.text
word = self.input.word
if word and word == text:
self.edit("")
self.on_confirm(word)
else:
self.edit(word)
def on_key_click(self, btn: Button) -> None:
# Key button was clicked. If this button is pending, let's cycle the
# pending character in input. If not, let's just append the first
# character.
if self.pending_button is btn:
index = (self.pending_index + 1) % len(btn.text)
text = self.input.text[:-1] + btn.text[index]
else:
index = 0
text = self.input.text + btn.text[0]
self.edit(text, btn, index)
def on_timeout(self) -> None:
# Timeout occurred. If we can auto-complete current input, let's just
# reset the pending marker. If not, input is invalid, let's backspace
# the last character.
if self.input.word:
self.edit(self.input.text)
else:
self.edit(self.input.text[:-1])
def on_confirm(self, word: str) -> None:
# Word was confirmed by the user.
raise ui.Result(word)
def edit(self, text: str, button: Button | None = None, index: int = 0) -> None:
self.pending_button = button
self.pending_index = index
# find the completions
pending = button is not None
word = bip39.complete_word(text) or ""
mask = bip39.word_completion_mask(text)
# modify the input state
self.input.edit(text, word, pending)
# enable or disable key buttons
for btn in self.keys:
if btn is button or compute_mask(btn.text) & mask:
btn.enable()
else:
btn.disable()
# invalidate the prompt if we display it next frame
if not self.input.text:
self.prompt.repaint = True
async def handle_input(self) -> None:
touch = loop.wait(io.TOUCH)
timeout = loop.sleep(1000)
race_touch = loop.race(touch)
race_timeout = loop.race(touch, timeout)
while True:
if self.pending_button is not None:
race = race_timeout
else:
race = race_touch
result = await race
if touch in race.finished:
event, x, y = result
workflow.idle_timer.touch()
self.dispatch(event, x, y)
else:
self.on_timeout()
if __debug__:
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import input_signal
return super().create_tasks() + (input_signal(),)

@ -1,230 +0,0 @@
from typing import TYPE_CHECKING
from trezor import io, loop, res, ui, workflow
from trezor.crypto import slip39
from trezor.ui import display
from .button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
if TYPE_CHECKING:
from .button import ButtonContent, ButtonStyleStateType
class KeyButton(Button):
def __init__(
self,
area: ui.Area,
content: ButtonContent,
keyboard: "Slip39Keyboard",
index: int,
):
self.keyboard = keyboard
self.index = index
super().__init__(area, content)
def on_click(self) -> None:
self.keyboard.on_key_click(self)
class InputButton(Button):
def __init__(self, area: ui.Area, keyboard: "Slip39Keyboard") -> None:
super().__init__(area, "")
self.word = ""
self.pending_button: Button | None = None
self.pending_index: int | None = None
self.keyboard = keyboard
self.disable()
def edit(
self,
text: str,
word: str,
pending_button: Button | None,
pending_index: int | None,
) -> None:
self.word = word
self.text = text
self.pending_button = pending_button
self.pending_index = pending_index
self.repaint = True
if word: # confirm button
self.enable()
self.normal_style = ButtonMonoConfirm.normal
self.active_style = ButtonMonoConfirm.active
self.icon = res.load(ui.ICON_CONFIRM)
else: # disabled button
self.disabled_style = ButtonMono.disabled
self.disable()
self.icon = b""
def render_content(
self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int
) -> None:
text_style = s.text_style
fg_color = s.fg_color
bg_color = s.bg_color
tx = ax + 16 # x-offset of the content
ty = ay + ah // 2 + 8 # y-offset of the content
if not self.keyboard.is_input_final():
pending_button = self.pending_button
pending_index = self.pending_index
to_display = len(self.text) * "*"
if pending_button and pending_index is not None:
to_display = to_display[:-1] + pending_button.text[pending_index]
else:
to_display = self.word
display.text(tx, ty, to_display, text_style, fg_color, bg_color)
if self.pending_button and not self.keyboard.is_input_final():
width = display.text_width(to_display, text_style)
pw = display.text_width(self.text[-1:], text_style)
px = tx + width - pw
display.bar(px, ty + 2, pw + 1, 3, fg_color)
if self.icon:
ix = ax + aw - 16 * 2
iy = ty - 16
display.icon(ix, iy, self.icon, fg_color, bg_color)
class Prompt(ui.Component):
def __init__(self, prompt: str) -> None:
super().__init__()
self.prompt = prompt
def on_render(self) -> None:
if self.repaint:
display.bar(0, 8, ui.WIDTH, 60, ui.BG)
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
self.repaint = False
class Slip39Keyboard(ui.Layout):
def __init__(self, prompt: str) -> None:
super().__init__()
self.prompt = Prompt(prompt)
icon_back = res.load(ui.ICON_BACK)
self.back = Button(ui.grid(0, n_x=3, n_y=4), icon_back, ButtonClear)
self.back.on_click = self.on_back_click
self.input = InputButton(ui.grid(1, n_x=3, n_y=4, cells_x=2), self)
self.input.on_click = self.on_input_click
self.keys = [
KeyButton(ui.grid(i + 3, n_y=4), k, self, i + 1)
for i, k in enumerate(
("ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "wxyz")
)
]
self.pending_button: Button | None = None
self.pending_index = 0
self.button_sequence = ""
self.mask = slip39.KEYBOARD_FULL_MASK
def dispatch(self, event: int, x: int, y: int) -> None:
for btn in self.keys:
btn.dispatch(event, x, y)
if self.input.text:
self.input.dispatch(event, x, y)
self.back.dispatch(event, x, y)
else:
self.prompt.dispatch(event, x, y)
def on_back_click(self) -> None:
# Backspace was clicked, let's delete the last character of input.
self.button_sequence = self.button_sequence[:-1]
self.edit()
def on_input_click(self) -> None:
# Input button was clicked. If the content matches the suggested word,
# let's confirm it, otherwise just auto-complete.
result = self.input.word
if self.is_input_final():
self.button_sequence = ""
self.edit()
self.on_confirm(result)
def on_key_click(self, btn: KeyButton) -> None:
# Key button was clicked. If this button is pending, let's cycle the
# pending character in input. If not, let's just append the first
# character.
if self.pending_button is btn:
index = (self.pending_index + 1) % len(btn.text)
else:
index = 0
self.button_sequence += str(btn.index)
self.edit(btn, index)
def on_timeout(self) -> None:
# Timeout occurred. Let's redraw to draw asterisks.
self.edit()
def on_confirm(self, word: str) -> None:
# Word was confirmed by the user.
raise ui.Result(word)
def edit(self, button: Button | None = None, index: int = 0) -> None:
self.pending_button = button
self.pending_index = index
# find the completions
word = ""
self.mask = slip39.word_completion_mask(self.button_sequence)
if self.is_input_final():
word = slip39.button_sequence_to_word(self.button_sequence)
# modify the input state
self.input.edit(
self.button_sequence, word, self.pending_button, self.pending_index
)
# enable or disable key buttons
for btn in self.keys:
if self.is_input_final():
btn.disable()
elif btn is button or self.check_mask(btn.index):
btn.enable()
else:
btn.disable()
# invalidate the prompt if we display it next frame
if not self.input.text:
self.prompt.repaint = True
def is_input_final(self) -> bool:
# returns True if mask has exactly one bit set to 1 or is 0
return not self.mask & (self.mask - 1)
def check_mask(self, index: int) -> bool:
return bool((1 << (index - 1)) & self.mask)
async def handle_input(self) -> None:
touch = loop.wait(io.TOUCH)
timeout = loop.sleep(1000)
race_touch = loop.race(touch)
race_timeout = loop.race(touch, timeout)
while True:
if self.pending_button is not None:
race = race_timeout
else:
race = race_touch
result = await race
if touch in race.finished:
event, x, y = result
workflow.idle_timer.touch()
self.dispatch(event, x, y)
else:
self.on_timeout()
if __debug__:
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import input_signal
return super().create_tasks() + (input_signal(),)

@ -1,51 +0,0 @@
from trezor import ui
from .button import Button
from .text import LABEL_CENTER, Label
class NumInput(ui.Component):
def __init__(self, count: int = 5, max_count: int = 16, min_count: int = 1) -> None:
super().__init__()
self.count = count
self.max_count = max_count
self.min_count = min_count
self.minus = Button(ui.grid(3), "-")
self.minus.on_click = self.on_minus
self.plus = Button(ui.grid(5), "+")
self.plus.on_click = self.on_plus
self.text = Label(ui.grid(4), "", LABEL_CENTER, ui.BOLD)
self.edit(count)
def dispatch(self, event: int, x: int, y: int) -> None:
self.minus.dispatch(event, x, y)
self.plus.dispatch(event, x, y)
self.text.dispatch(event, x, y)
def on_minus(self) -> None:
self.edit(self.count - 1)
def on_plus(self) -> None:
self.edit(self.count + 1)
def edit(self, count: int) -> None:
count = max(count, self.min_count)
count = min(count, self.max_count)
if self.count != count:
self.on_change(count)
self.count = count
self.text.content = str(count)
self.text.repaint = True
if self.count == self.min_count:
self.minus.disable()
else:
self.minus.enable()
if self.count == self.max_count:
self.plus.disable()
else:
self.plus.enable()
def on_change(self, count: int) -> None:
pass

@ -1,262 +0,0 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor import io, loop, res, ui, workflow
from trezor.ui import display
from .button import Button, ButtonClear, ButtonConfirm
from .swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
if TYPE_CHECKING:
from typing import Iterable
from .button import ButtonContent, ButtonStyleStateType
SPACE = res.load(ui.ICON_SPACE)
KEYBOARD_KEYS = (
("1", "2", "3", "4", "5", "6", "7", "8", "9", "0"),
(SPACE, "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz", "*#"),
(SPACE, "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ", "*#"),
("_<>", ".:@", "/|\\", "!()", "+%&", "-[]", "?{}", ",'`", ';"~', "$^="),
)
def digit_area(i: int) -> ui.Area:
if i == 9: # 0-position
i = 10 # display it in the middle
return ui.grid(i + 3) # skip the first line
def render_scrollbar(page: int) -> None:
BBOX = const(240)
SIZE = const(8)
pages = len(KEYBOARD_KEYS)
padding = 12
if pages * padding > BBOX:
padding = BBOX // pages
x = (BBOX // 2) - (pages // 2) * padding
Y = const(44)
for i in range(0, pages):
if i == page:
fg = ui.FG
else:
fg = ui.DARK_GREY
ui.display.bar_radius(x + i * padding, Y, SIZE, SIZE, fg, ui.BG, SIZE // 2)
class KeyButton(Button):
def __init__(
self, area: ui.Area, content: ButtonContent, keyboard: "PassphraseKeyboard"
) -> None:
self.keyboard = keyboard
super().__init__(area, content)
def on_click(self) -> None:
self.keyboard.on_key_click(self)
def get_text_content(self) -> str:
if self.text:
return self.text
elif self.icon is SPACE:
return " "
else:
raise TypeError
def key_buttons(
keys: Iterable[ButtonContent], keyboard: "PassphraseKeyboard"
) -> list[KeyButton]:
return [KeyButton(digit_area(i), k, keyboard) for i, k in enumerate(keys)]
class Input(Button):
def __init__(self, area: ui.Area, text: str) -> None:
super().__init__(area, text)
self.pending = False
self.disable()
def edit(self, text: str, pending: bool) -> None:
self.text = text
self.pending = pending
self.repaint = True
def render_content(
self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int
) -> None:
text_style = s.text_style
fg_color = s.fg_color
bg_color = s.bg_color
p = self.pending # should we draw the pending marker?
t = self.text # input content
tx = ax + 24 # x-offset of the content
ty = ay + ah // 2 + 8 # y-offset of the content
maxlen = const(14) # maximum text length
# input content
if len(t) > maxlen:
t = "<" + t[-maxlen:] # too long, align to the right
width = display.text_width(t, text_style)
display.text(tx, ty, t, text_style, fg_color, bg_color)
if p: # pending marker
pw = display.text_width(t[-1:], text_style)
px = tx + width - pw
display.bar(px, ty + 2, pw + 1, 3, fg_color)
else: # cursor
cx = tx + width + 1
display.bar(cx, ty - 18, 2, 22, fg_color)
def on_click(self) -> None:
pass
class Prompt(ui.Component):
def __init__(self, text: str) -> None:
super().__init__()
self.text = text
def on_render(self) -> None:
if self.repaint:
display.bar(0, 0, ui.WIDTH, 48, ui.BG)
display.text_center(ui.WIDTH // 2, 32, self.text, ui.BOLD, ui.GREY, ui.BG)
self.repaint = False
CANCELLED = object()
class PassphraseKeyboard(ui.Layout):
def __init__(self, prompt: str, max_length: int, page: int = 1) -> None:
super().__init__()
self.prompt = Prompt(prompt)
self.max_length = max_length
self.page = page
self.input = Input(ui.grid(0, n_x=1, n_y=6), "")
self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), ButtonClear)
self.back.on_click = self.on_back_click
self.back.disable()
self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), ButtonConfirm)
self.done.on_click = self.on_confirm
self.keys = key_buttons(KEYBOARD_KEYS[self.page], self)
self.pending_button: KeyButton | None = None
self.pending_index = 0
def dispatch(self, event: int, x: int, y: int) -> None:
if self.input.text:
self.input.dispatch(event, x, y)
else:
self.prompt.dispatch(event, x, y)
self.back.dispatch(event, x, y)
self.done.dispatch(event, x, y)
for btn in self.keys:
btn.dispatch(event, x, y)
if event == ui.RENDER:
render_scrollbar(self.page)
def on_back_click(self) -> None:
# Backspace was clicked. If we have any content in the input, let's delete
# the last character. Otherwise cancel.
text = self.input.text
if text:
self.edit(text[:-1])
else:
self.on_cancel()
def on_key_click(self, button: KeyButton) -> None:
# Key button was clicked. If this button is pending, let's cycle the
# pending character in input. If not, let's just append the first
# character.
button_text = button.get_text_content()
if self.pending_button is button:
index = (self.pending_index + 1) % len(button_text)
prefix = self.input.text[:-1]
else:
index = 0
prefix = self.input.text
if len(button_text) > 1:
self.edit(prefix + button_text[index], button, index)
else:
self.edit(prefix + button_text[index])
def on_timeout(self) -> None:
# Timeout occurred, let's just reset the pending marker.
self.edit(self.input.text)
def edit(self, text: str, button: KeyButton | None = None, index: int = 0) -> None:
if len(text) > self.max_length:
return
self.pending_button = button
self.pending_index = index
# modify the input state
pending = button is not None
self.input.edit(text, pending)
if text:
self.back.enable()
else:
self.back.disable()
self.prompt.repaint = True
async def handle_input(self) -> None:
touch = loop.wait(io.TOUCH)
timeout = loop.sleep(1000)
race_touch = loop.race(touch)
race_timeout = loop.race(touch, timeout)
while True:
if self.pending_button is not None:
race = race_timeout
else:
race = race_touch
result = await race
if touch in race.finished:
event, x, y = result
workflow.idle_timer.touch()
self.dispatch(event, x, y)
else:
self.on_timeout()
async def handle_paging(self) -> None:
swipe = await Swipe(SWIPE_HORIZONTAL)
if swipe == SWIPE_LEFT:
self.page = (self.page + 1) % len(KEYBOARD_KEYS)
else:
self.page = (self.page - 1) % len(KEYBOARD_KEYS)
self.keys = key_buttons(KEYBOARD_KEYS[self.page], self)
self.back.repaint = True
self.done.repaint = True
self.input.repaint = True
self.prompt.repaint = True
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
def on_confirm(self) -> None:
raise ui.Result(self.input.text)
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
tasks: tuple[loop.Task, ...] = (
self.handle_input(),
self.handle_rendering(),
self.handle_paging(),
)
if __debug__:
from apps.debug import input_signal
return tasks + (input_signal(),)
else:
return tasks

@ -1,183 +0,0 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor import config, res, ui
from trezor.crypto import random
from trezor.ui import display
from .button import Button, ButtonCancel, ButtonClear, ButtonConfirm, ButtonMono
if TYPE_CHECKING:
from trezor import loop
from typing import Iterable
def digit_area(i: int) -> ui.Area:
if i == 9: # 0-position
i = 10 # display it in the middle
return ui.grid(i + 3) # skip the first line
def generate_digits() -> Iterable[int]:
digits = list(range(0, 10)) # 0-9
random.shuffle(digits)
# We lay out the buttons top-left to bottom-right, but the order
# of the digits is defined as bottom-left to top-right (on numpad).
return digits[6:] + digits[3:6] + digits[:3]
class PinInput(ui.Component):
def __init__(self, prompt: str, subprompt: str | None, pin: str) -> None:
super().__init__()
self.prompt = prompt
self.subprompt = subprompt
self.pin = pin
def on_render(self) -> None:
if self.repaint:
if self.pin:
self.render_pin()
else:
self.render_prompt()
self.repaint = False
def render_pin(self) -> None:
MAX_LENGTH = const(14) # maximum length of displayed PIN
CONTD_MARK = "<"
BOX_WIDTH = const(240)
DOT_SIZE = const(10)
PADDING = const(4)
RENDER_Y = const(20)
TWITCH = const(3)
display.bar(0, 0, ui.WIDTH, 50, ui.BG)
if len(self.pin) > MAX_LENGTH:
contd_width = display.text_width(CONTD_MARK, ui.BOLD) + PADDING
twitch = TWITCH * (len(self.pin) % 2)
else:
contd_width = 0
twitch = 0
count = min(len(self.pin), MAX_LENGTH)
render_x = (BOX_WIDTH - count * (DOT_SIZE + PADDING) - contd_width) // 2
if contd_width:
display.text(
render_x, RENDER_Y + DOT_SIZE, CONTD_MARK, ui.BOLD, ui.GREY, ui.BG
)
for i in range(0, count):
display.bar_radius(
render_x + contd_width + twitch + i * (DOT_SIZE + PADDING),
RENDER_Y,
DOT_SIZE,
DOT_SIZE,
ui.GREY,
ui.BG,
4,
)
def render_prompt(self) -> None:
display.bar(0, 0, ui.WIDTH, 50, ui.BG)
if self.subprompt:
display.text_center(ui.WIDTH // 2, 20, self.prompt, ui.BOLD, ui.GREY, ui.BG)
display.text_center(
ui.WIDTH // 2, 46, self.subprompt, ui.NORMAL, ui.GREY, ui.BG
)
else:
display.text_center(ui.WIDTH // 2, 36, self.prompt, ui.BOLD, ui.GREY, ui.BG)
class PinButton(Button):
def __init__(self, index: int, digit: int, dialog: "PinDialog"):
self.dialog = dialog
super().__init__(digit_area(index), str(digit), ButtonMono)
def on_click(self) -> None:
self.dialog.assign(self.dialog.input.pin + self.text)
CANCELLED = object()
class PinDialog(ui.Layout):
def __init__(
self,
prompt: str,
subprompt: str | None,
allow_cancel: bool = True,
maxlength: int = 50,
) -> None:
super().__init__()
self.maxlength = maxlength
self.input = PinInput(prompt, subprompt, "")
icon_confirm = res.load(ui.ICON_CONFIRM)
self.confirm_button = Button(ui.grid(14), icon_confirm, ButtonConfirm)
self.confirm_button.on_click = self.on_confirm
self.confirm_button.disable()
icon_back = res.load(ui.ICON_BACK)
self.reset_button = Button(ui.grid(12), icon_back, ButtonClear)
self.reset_button.on_click = self.on_reset
if allow_cancel:
icon_lock = res.load(
ui.ICON_CANCEL if config.is_unlocked() else ui.ICON_LOCK
)
self.cancel_button = Button(ui.grid(12), icon_lock, ButtonCancel)
self.cancel_button.on_click = self.on_cancel
else:
self.cancel_button = Button(ui.grid(12), "")
self.cancel_button.disable()
self.pin_buttons = [
PinButton(i, d, self) for i, d in enumerate(generate_digits())
]
def dispatch(self, event: int, x: int, y: int) -> None:
self.input.dispatch(event, x, y)
if self.input.pin:
self.reset_button.dispatch(event, x, y)
else:
self.cancel_button.dispatch(event, x, y)
self.confirm_button.dispatch(event, x, y)
for btn in self.pin_buttons:
btn.dispatch(event, x, y)
def assign(self, pin: str) -> None:
if len(pin) > self.maxlength:
return
for btn in self.pin_buttons:
if len(pin) < self.maxlength:
btn.enable()
else:
btn.disable()
if pin:
self.confirm_button.enable()
self.reset_button.enable()
self.cancel_button.disable()
else:
self.confirm_button.disable()
self.reset_button.disable()
self.cancel_button.enable()
self.input.pin = pin
self.input.repaint = True
def on_reset(self) -> None:
self.assign("")
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
def on_confirm(self) -> None:
if self.input.pin:
raise ui.Result(self.input.pin)
if __debug__:
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import input_signal
return super().create_tasks() + (input_signal(),)

@ -1,41 +0,0 @@
from trezor import ui
class RecoveryHomescreen(ui.Component):
def __init__(self, dry_run: bool, text: str, subtext: str | None = None):
super().__init__()
self.text = text
self.subtext = subtext
self.dry_run = dry_run
def on_render(self) -> None:
if not self.repaint:
return
if self.dry_run:
heading = "SEED CHECK"
else:
heading = "RECOVERY MODE"
ui.header_warning(heading)
if not self.subtext:
ui.display.text_center(ui.WIDTH // 2, 80, self.text, ui.BOLD, ui.FG, ui.BG)
else:
ui.display.text_center(ui.WIDTH // 2, 65, self.text, ui.BOLD, ui.FG, ui.BG)
ui.display.text_center(
ui.WIDTH // 2, 92, self.subtext, ui.NORMAL, ui.FG, ui.BG
)
ui.display.text_center(
ui.WIDTH // 2, 130, "It is safe to eject Trezor", ui.NORMAL, ui.GREY, ui.BG
)
ui.display.text_center(
ui.WIDTH // 2, 155, "and continue later", ui.NORMAL, ui.GREY, ui.BG
)
self.repaint = False
if __debug__:
def read_content(self) -> list[str]:
return [self.__class__.__name__, self.text, self.subtext or ""]

@ -1,156 +0,0 @@
from typing import TYPE_CHECKING
from trezor import ui
from .button import Button
from .num_input import NumInput
from .text import Text
if TYPE_CHECKING:
from trezor import loop
from typing import Callable, NoReturn, Sequence
if __debug__:
from apps import debug
class Slip39NumInput(ui.Component):
SET_SHARES = object()
SET_THRESHOLD = object()
SET_GROUPS = object()
SET_GROUP_THRESHOLD = object()
def __init__(
self,
step: object,
count: int,
min_count: int,
max_count: int,
group_id: int | None = None,
) -> None:
super().__init__()
self.step = step
self.input = NumInput(count, min_count=min_count, max_count=max_count)
self.input.on_change = self.on_change
self.group_id = group_id
def dispatch(self, event: int, x: int, y: int) -> None:
self.input.dispatch(event, x, y)
if event is ui.RENDER:
self.on_render()
def on_render(self) -> None:
if self.repaint:
count = self.input.count
# render the headline
if self.step is Slip39NumInput.SET_SHARES:
header = "Set num. of shares"
elif self.step is Slip39NumInput.SET_THRESHOLD:
header = "Set threshold"
elif self.step is Slip39NumInput.SET_GROUPS:
header = "Set num. of groups"
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
header = "Set group threshold"
else:
raise RuntimeError # invalid step
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
# render the counter
if self.step is Slip39NumInput.SET_SHARES:
if self.group_id is None:
if count == 1:
first_line_text = "Only one share will"
second_line_text = "be created."
else:
first_line_text = f"{count} people or locations"
second_line_text = "will each hold one share."
else:
first_line_text = "Set the total number of"
second_line_text = f"shares in Group {self.group_id + 1}."
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
elif self.step is Slip39NumInput.SET_THRESHOLD:
if self.group_id is None:
first_line_text = "For recovery you need"
if count == 1:
second_line_text = "1 share."
elif count == self.input.max_count:
second_line_text = f"all {count} of the shares."
else:
second_line_text = f"any {count} of the shares."
else:
first_line_text = "The required number of "
second_line_text = f"shares to form Group {self.group_id + 1}."
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
elif self.step is Slip39NumInput.SET_GROUPS:
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(
12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG
)
ui.display.text(12, 156, "recovery shares.", ui.NORMAL, ui.FG, ui.BG)
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
ui.display.text(
12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG
)
ui.display.text(
12, 156, "groups for recovery.", ui.NORMAL, ui.FG, ui.BG
)
self.repaint = False
def on_change(self, count: int) -> None:
self.repaint = True
class MnemonicWordSelect(ui.Layout):
NUM_OF_CHOICES = 3
def __init__(
self,
words: Sequence[str],
share_index: int | None,
word_index: int,
count: int,
group_index: int | None = None,
) -> None:
super().__init__()
self.words = words
self.share_index = share_index
self.word_index = word_index
self.buttons = []
for i, word in enumerate(words):
area = ui.grid(i + 2, n_x=1)
btn = Button(area, word)
btn.on_click = self.select(word)
self.buttons.append(btn)
if share_index is None:
self.text: ui.Component = Text("Check seed")
elif group_index is None:
self.text = Text(f"Check share #{share_index + 1}")
else:
self.text = Text(f"Check G{group_index + 1} - Share {share_index + 1}")
self.text.normal(f"Select word {word_index + 1} of {count}:")
def dispatch(self, event: int, x: int, y: int) -> None:
for btn in self.buttons:
btn.dispatch(event, x, y)
self.text.dispatch(event, x, y)
def select(self, word: str) -> Callable:
def fn() -> NoReturn:
raise ui.Result(word)
return fn
if __debug__:
def read_content(self) -> list[str]:
return self.text.read_content() + [b.text for b in self.buttons]
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
return super().create_tasks() + (debug.input_signal(),)

@ -1,430 +0,0 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor import loop, res, ui, utils, wire, workflow
from trezor.enums import ButtonRequestType
from trezor.messages import ButtonAck, ButtonRequest
from ..common.confirm import CANCELLED, CONFIRMED, GO_BACK, SHOW_PAGINATED
from .button import Button, ButtonCancel, ButtonConfirm, ButtonDefault
from .confirm import Confirm
from .swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe
from .text import (
LINE_WIDTH_PAGINATED,
TEXT_MAX_LINES,
TEXT_MAX_LINES_NO_HEADER,
Span,
Text,
)
if TYPE_CHECKING:
from typing import Any, Callable, Iterable
from ..common.text import TextContent
WAS_PAGED = object()
def render_scrollbar(pages: int, page: int) -> None:
BBOX = const(220)
SIZE = const(8)
padding = 14
if pages * padding > BBOX:
padding = BBOX // pages
X = const(220)
Y = (BBOX // 2) - (pages // 2) * padding
for i in range(0, pages):
if i == page:
fg = ui.FG
else:
fg = ui.GREY
ui.display.bar_radius(X, Y + i * padding, SIZE, SIZE, fg, ui.BG, 4)
def render_swipe_icon(x_offset: int = 0) -> None:
if utils.DISABLE_ANIMATION:
c = ui.GREY
else:
PULSE_PERIOD = const(1_200_000)
t = ui.pulse(PULSE_PERIOD)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE)
ui.display.icon(70 + x_offset, 205, icon, c, ui.BG)
def render_swipe_text(x_offset: int = 0) -> None:
ui.display.text_center(130 + x_offset, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG)
class Paginated(ui.Layout):
def __init__(
self, pages: list[ui.Component], page: int = 0, back_button: bool = False
):
super().__init__()
self.pages = pages
self.page = page
self.back_button = None
if back_button:
area = ui.grid(16, n_x=4)
icon = res.load(ui.ICON_BACK)
self.back_button = Button(area, icon, ButtonDefault)
self.back_button.on_click = self.on_back_click
def dispatch(self, event: int, x: int, y: int) -> None:
pages = self.pages
page = self.page
length = len(pages)
last_page = page >= length - 1
x_offset = 0
pages[page].dispatch(event, x, y)
if self.back_button is not None and not last_page:
self.back_button.dispatch(event, x, y)
x_offset = 30
if event is ui.REPAINT:
self.repaint = True
elif event is ui.RENDER:
if not last_page:
render_swipe_icon(x_offset=x_offset)
if self.repaint:
render_swipe_text(x_offset=x_offset)
if self.repaint:
render_scrollbar(length, page)
self.repaint = False
async def handle_paging(self) -> None:
if self.page == 0:
directions = SWIPE_UP
elif self.page == len(self.pages) - 1:
directions = SWIPE_DOWN
else:
directions = SWIPE_VERTICAL
if __debug__:
from apps.debug import swipe_signal
swipe = await loop.race(Swipe(directions), swipe_signal())
else:
swipe = await Swipe(directions)
if swipe is SWIPE_UP:
self.page = min(self.page + 1, len(self.pages) - 1)
elif swipe is SWIPE_DOWN:
self.page = max(self.page - 1, 0)
self.on_change()
raise ui.Result(WAS_PAGED)
async def interact(
self,
ctx: wire.GenericContext,
code: ButtonRequestType = ButtonRequestType.Other,
) -> Any:
workflow.close_others()
await ctx.call(ButtonRequest(code=code, pages=len(self.pages)), ButtonAck)
result = WAS_PAGED
while result is WAS_PAGED:
result = await ctx.wait(self)
return result
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
tasks: tuple[loop.AwaitableTask, ...] = (
self.handle_input(),
self.handle_rendering(),
self.handle_paging(),
)
if __debug__:
# XXX This isn't strictly correct, as it allows *any* Paginated layout to be
# shut down by a DebugLink confirm, even if used outside of a confirm() call
# But we don't have any such usages in the codebase, and it doesn't actually
# make much sense to use a Paginated without a way to confirm it.
from apps.debug import confirm_signal
return tasks + (confirm_signal(),)
else:
return tasks
def on_change(self) -> None:
pass
def on_back_click(self) -> None:
raise ui.Result(GO_BACK)
if __debug__:
def read_content(self) -> list[str]:
return self.pages[self.page].read_content()
class AskPaginated(ui.Component):
def __init__(self, content: ui.Component, button_text: str = "Show all") -> None:
super().__init__()
self.content = content
self.button = Button(ui.grid(3, n_x=1), button_text, ButtonDefault)
self.button.on_click = self.on_show_paginated_click
def dispatch(self, event: int, x: int, y: int) -> None:
self.content.dispatch(event, x, y)
self.button.dispatch(event, x, y)
def on_show_paginated_click(self) -> None:
raise ui.Result(SHOW_PAGINATED)
if __debug__:
def read_content(self) -> list[str]:
return self.content.read_content()
class PageWithButtons(ui.Component):
def __init__(
self,
content: ui.Component,
paginated: "PaginatedWithButtons",
index: int,
count: int,
) -> None:
super().__init__()
self.content = content
self.paginated = paginated
self.index = index
self.count = count
# somewhere in the middle, we can go up or down
left = res.load(ui.ICON_BACK)
left_style = ButtonDefault
right = res.load(ui.ICON_CLICK)
right_style = ButtonDefault
if self.index == 0:
# first page, we can cancel or go down
left = res.load(ui.ICON_CANCEL)
left_style = ButtonCancel
right = res.load(ui.ICON_CLICK)
right_style = ButtonDefault
elif self.index == count - 1:
# last page, we can go up or confirm
left = res.load(ui.ICON_BACK)
left_style = ButtonDefault
right = res.load(ui.ICON_CONFIRM)
right_style = ButtonConfirm
self.left = Button(ui.grid(8, n_x=2), left, left_style)
self.left.on_click = self.on_left
self.right = Button(ui.grid(9, n_x=2), right, right_style)
self.right.on_click = self.on_right
def dispatch(self, event: int, x: int, y: int) -> None:
self.content.dispatch(event, x, y)
self.left.dispatch(event, x, y)
self.right.dispatch(event, x, y)
def on_left(self) -> None:
if self.index == 0:
self.paginated.on_cancel()
else:
self.paginated.on_up()
def on_right(self) -> None:
if self.index == self.count - 1:
self.paginated.on_confirm()
else:
self.paginated.on_down()
if __debug__:
def read_content(self) -> list[str]:
return self.content.read_content()
class PaginatedWithButtons(ui.Layout):
def __init__(
self, pages: list[ui.Component], page: int = 0, one_by_one: bool = False
) -> None:
super().__init__()
self.pages = [
PageWithButtons(p, self, i, len(pages)) for i, p in enumerate(pages)
]
self.page = page
self.one_by_one = one_by_one
def dispatch(self, event: int, x: int, y: int) -> None:
pages = self.pages
page = self.page
pages[page].dispatch(event, x, y)
if event is ui.RENDER:
render_scrollbar(len(pages), page)
def on_up(self) -> None:
self.page = max(self.page - 1, 0)
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
self.on_change()
def on_down(self) -> None:
self.page = min(self.page + 1, len(self.pages) - 1)
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
self.on_change()
def on_confirm(self) -> None:
raise ui.Result(CONFIRMED)
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
def on_change(self) -> None:
if self.one_by_one:
raise ui.Result(self.page)
if __debug__:
def read_content(self) -> list[str]:
return self.pages[self.page].read_content()
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import confirm_signal
return super().create_tasks() + (confirm_signal(),)
def paginate_text(
text: str,
header: str,
font: int = ui.NORMAL,
header_icon: str = ui.ICON_DEFAULT,
icon_color: int = ui.ORANGE_ICON,
break_words: bool = False,
confirm: Callable[[ui.Component], ui.Layout] = Confirm,
) -> ui.Layout:
span = Span(text, 0, font, break_words=break_words)
if span.count_lines() <= TEXT_MAX_LINES:
result = Text(
header,
header_icon=header_icon,
icon_color=icon_color,
new_lines=False,
break_words=break_words,
)
result.content = [font, text]
return confirm(result)
else:
pages: list[ui.Component] = []
span.reset(
text, 0, font, break_words=break_words, line_width=LINE_WIDTH_PAGINATED
)
while span.has_more_content():
# advance to first line of the page
span.next_line()
page = Text(
header,
header_icon=header_icon,
icon_color=icon_color,
new_lines=False,
content_offset=0,
char_offset=span.start,
line_width=LINE_WIDTH_PAGINATED,
break_words=break_words,
render_page_overflow=False,
)
page.content = [font, text]
pages.append(page)
# roll over the remaining lines on the page
for _ in range(TEXT_MAX_LINES - 1):
span.next_line()
pages[-1] = confirm(pages[-1])
return Paginated(pages)
PAGEBREAK = 0, ""
def paginate_paragraphs(
para: Iterable[tuple[int, str]],
header: str | None,
header_icon: str = ui.ICON_DEFAULT,
icon_color: int = ui.ORANGE_ICON,
break_words: bool = False,
confirm: Callable[[ui.Component], ui.Layout] = Confirm,
back_button: bool = False,
) -> ui.Layout:
span = Span("", 0, ui.NORMAL, break_words=break_words)
lines = 0
content: list[TextContent] = []
max_lines = TEXT_MAX_LINES_NO_HEADER if header is None else TEXT_MAX_LINES
for item in para:
if item is PAGEBREAK:
continue
span.reset(item[1], 0, item[0], break_words=break_words)
lines += span.count_lines()
# we'll need this for multipage too
if content:
content.append("\n")
content.extend(item)
if lines <= max_lines:
result = Text(
header,
header_icon=header_icon,
icon_color=icon_color,
new_lines=False,
break_words=break_words,
)
result.content = content
return confirm(result)
else:
pages: list[ui.Component] = []
lines_left = 0
content_ctr = 0
page: Text | None = None
for item in para:
if item is PAGEBREAK:
if page is not None:
page.max_lines -= lines_left
lines_left = 0
continue
span.reset(
item[1],
0,
item[0],
break_words=break_words,
line_width=LINE_WIDTH_PAGINATED,
)
while span.has_more_content():
span.next_line()
if lines_left <= 0:
page = Text(
header,
header_icon=header_icon,
icon_color=icon_color,
new_lines=False,
content_offset=content_ctr * 3 + 1, # font, _text_, newline
char_offset=span.start,
line_width=LINE_WIDTH_PAGINATED,
render_page_overflow=False,
break_words=break_words,
)
page.content = content
pages.append(page)
lines_left = max_lines - 1
else:
lines_left -= 1
content_ctr += 1
pages[-1] = confirm(pages[-1])
return Paginated(pages, back_button=back_button)

@ -1,114 +0,0 @@
from micropython import const
from typing import Generator
from trezor import io, loop, ui
from trezor.ui.components.common import SWIPE_DOWN, SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP
SWIPE_VERTICAL = SWIPE_UP | SWIPE_DOWN
SWIPE_HORIZONTAL = SWIPE_LEFT | SWIPE_RIGHT
SWIPE_ALL = SWIPE_VERTICAL | SWIPE_HORIZONTAL
_SWIPE_DISTANCE = const(120)
_SWIPE_TRESHOLD = const(30)
class Swipe(ui.Component):
def __init__(
self, directions: int = SWIPE_ALL, area: ui.Area | None = None
) -> None:
super().__init__()
if area is None:
area = (0, 0, ui.WIDTH, ui.HEIGHT)
self.area = area
self.directions = directions
self.started = False
self.start_x = 0
self.start_y = 0
self.light_origin = ui.BACKLIGHT_NORMAL
self.light_target = ui.BACKLIGHT_NONE
def on_touch_start(self, x: int, y: int) -> None:
if ui.in_area(self.area, x, y):
self.start_x = x
self.start_y = y
self.light_origin = ui.BACKLIGHT_NORMAL
self.started = True
def on_touch_move(self, x: int, y: int) -> None:
if not self.started:
return # not started in our area
dirs = self.directions
pdx = x - self.start_x
pdy = y - self.start_y
pdxa = abs(pdx)
pdya = abs(pdy)
if pdxa > pdya and dirs & SWIPE_HORIZONTAL:
# horizontal direction
if (pdx > 0 and dirs & SWIPE_RIGHT) or (pdx < 0 and dirs & SWIPE_LEFT):
ui.display.backlight(
ui.lerpi(
self.light_origin,
self.light_target,
min(pdxa / _SWIPE_DISTANCE, 1),
)
)
elif pdxa < pdya and dirs & SWIPE_VERTICAL:
# vertical direction
if (pdy > 0 and dirs & SWIPE_DOWN) or (pdy < 0 and dirs & SWIPE_UP):
ui.display.backlight(
ui.lerpi(
self.light_origin,
self.light_target,
min(pdya / _SWIPE_DISTANCE, 1),
)
)
def on_touch_end(self, x: int, y: int) -> None:
if not self.started:
return # not started in our area
dirs = self.directions
pdx = x - self.start_x
pdy = y - self.start_y
pdxa = abs(pdx)
pdya = abs(pdy)
if pdxa > pdya and dirs & SWIPE_HORIZONTAL:
# horizontal direction
ratio = min(pdxa / _SWIPE_DISTANCE, 1)
if ratio * 100 >= _SWIPE_TRESHOLD:
if pdx > 0 and dirs & SWIPE_RIGHT:
self.on_swipe(SWIPE_RIGHT)
return
elif pdx < 0 and dirs & SWIPE_LEFT:
self.on_swipe(SWIPE_LEFT)
return
elif pdxa < pdya and dirs & SWIPE_VERTICAL:
# vertical direction
ratio = min(pdya / _SWIPE_DISTANCE, 1)
if ratio * 100 >= _SWIPE_TRESHOLD:
if pdy > 0 and dirs & SWIPE_DOWN:
self.on_swipe(SWIPE_DOWN)
return
elif pdy < 0 and dirs & SWIPE_UP:
self.on_swipe(SWIPE_UP)
return
# no swipe detected, reset the state
ui.display.backlight(self.light_origin)
self.started = False
def on_swipe(self, swipe: int) -> None:
raise ui.Result(swipe)
def __await__(self) -> Generator:
return self.__iter__() # type: ignore [Expression of type "Task" cannot be assigned to return type "Generator[Unknown, Unknown, Unknown]"]
def __iter__(self) -> loop.Task: # type: ignore [awaitable-is-generator]
try:
touch = loop.wait(io.TOUCH)
while True:
event, x, y = yield touch
self.dispatch(event, x, y)
except ui.Result as result:
return result.value

@ -1,145 +0,0 @@
from micropython import const
from trezor import res, ui
from trezor.ui import display, style
from ..common.text import ( # noqa: F401
BR,
BR_HALF,
LINE_WIDTH,
LINE_WIDTH_PAGINATED,
TEXT_HEADER_HEIGHT,
TEXT_LINE_HEIGHT,
TEXT_MAX_LINES,
TEXT_MAX_LINES_NO_HEADER,
Span,
TextBase,
render_text,
)
def header(
title: str,
icon: str = style.ICON_DEFAULT,
fg: int = style.FG,
bg: int = style.BG,
ifg: int = style.GREEN,
) -> None:
if icon is not None:
display.icon(14, 15, res.load(icon), ifg, bg)
display.text(44, 35, title, ui.BOLD, fg, bg)
class Text(TextBase):
def on_render(self) -> None:
if self.repaint:
offset_y = TEXT_LINE_HEIGHT
if self.header_text is not None:
header(
self.header_text,
self.header_icon,
ui.TITLE_GREY,
ui.BG,
self.icon_color,
)
offset_y = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT
render_text(
self.content,
self.new_lines,
self.max_lines,
item_offset=self.content_offset,
char_offset=self.char_offset,
break_words=self.break_words,
line_width=self.line_width,
render_page_overflow=self.render_page_overflow,
offset_y=offset_y,
)
self.repaint = False
LABEL_LEFT = const(0)
LABEL_CENTER = const(1)
LABEL_RIGHT = const(2)
class Label(ui.Component):
def __init__(
self,
area: ui.Area,
content: str,
align: int = LABEL_LEFT,
style: int = ui.NORMAL,
) -> None:
super().__init__()
self.area = area
self.content = content
self.align = align
self.style = style
def on_render(self) -> None:
if self.repaint:
align = self.align
ax, ay, aw, ah = self.area
ui.display.bar(ax, ay, aw, ah, ui.BG)
tx = ax + aw // 2
ty = ay + ah // 2 + 8
if align is LABEL_LEFT:
ui.display.text(tx, ty, self.content, self.style, ui.FG, ui.BG)
elif align is LABEL_CENTER:
ui.display.text_center(tx, ty, self.content, self.style, ui.FG, ui.BG)
elif align is LABEL_RIGHT:
ui.display.text_right(tx, ty, self.content, self.style, ui.FG, ui.BG)
self.repaint = False
if __debug__:
def read_content(self) -> list[str]:
return [self.content]
def text_center_trim_left(
x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16
) -> None:
if ui.display.text_width(text, font) <= width:
ui.display.text_center(x, y, text, font, ui.FG, ui.BG)
return
ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD)
if width < ELLIPSIS_WIDTH:
return
text_length = 0
for i in range(1, len(text)):
if ui.display.text_width(text[-i:], font) + ELLIPSIS_WIDTH > width:
text_length = i - 1
break
text_width = ui.display.text_width(text[-text_length:], font)
x -= (text_width + ELLIPSIS_WIDTH) // 2
ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG)
x += ELLIPSIS_WIDTH
ui.display.text(x, y, text[-text_length:], font, ui.FG, ui.BG)
def text_center_trim_right(
x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16
) -> None:
if ui.display.text_width(text, font) <= width:
ui.display.text_center(x, y, text, font, ui.FG, ui.BG)
return
ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD)
if width < ELLIPSIS_WIDTH:
return
text_length = 0
for i in range(1, len(text)):
if ui.display.text_width(text[:i], font) + ELLIPSIS_WIDTH > width:
text_length = i - 1
break
text_width = ui.display.text_width(text[:text_length], font)
x -= (text_width + ELLIPSIS_WIDTH) // 2
ui.display.text(x, y, text[:text_length], font, ui.FG, ui.BG)
x += text_width
ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG)

@ -1,34 +0,0 @@
from trezor import ui
from ..common.webauthn import ConfirmInfo
from .text import text_center_trim_left, text_center_trim_right
class ConfirmContent(ui.Component):
def __init__(self, info: ConfirmInfo) -> None:
super().__init__()
self.info = info
def on_render(self) -> None:
if self.repaint:
header = self.info.get_header()
ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN)
if self.info.app_icon is not None:
ui.display.image((ui.WIDTH - 64) // 2, 48, self.info.app_icon)
app_name = self.info.app_name()
account_name = self.info.account_name()
# Dummy requests usually have some text as both app_name and account_name,
# in that case show the text only once.
if account_name is not None:
if app_name != account_name:
text_center_trim_left(ui.WIDTH // 2, 140, app_name)
text_center_trim_right(ui.WIDTH // 2, 172, account_name)
else:
text_center_trim_right(ui.WIDTH // 2, 156, account_name)
else:
text_center_trim_left(ui.WIDTH // 2, 156, app_name)
self.repaint = False

@ -1,56 +0,0 @@
from typing import TYPE_CHECKING
from trezor import ui
from .button import Button
if TYPE_CHECKING:
from trezor import loop
# todo improve?
class WordSelector(ui.Layout):
def __init__(self, content: ui.Component) -> None:
super().__init__()
self.content = content
self.w12 = Button(ui.grid(6, n_y=4), "12")
self.w12.on_click = self.on_w12
self.w18 = Button(ui.grid(7, n_y=4), "18")
self.w18.on_click = self.on_w18
self.w20 = Button(ui.grid(8, n_y=4), "20")
self.w20.on_click = self.on_w20
self.w24 = Button(ui.grid(9, n_y=4), "24")
self.w24.on_click = self.on_w24
self.w33 = Button(ui.grid(10, n_y=4), "33")
self.w33.on_click = self.on_w33
def dispatch(self, event: int, x: int, y: int) -> None:
self.content.dispatch(event, x, y)
self.w12.dispatch(event, x, y)
self.w18.dispatch(event, x, y)
self.w20.dispatch(event, x, y)
self.w24.dispatch(event, x, y)
self.w33.dispatch(event, x, y)
def on_w12(self) -> None:
raise ui.Result(12)
def on_w18(self) -> None:
raise ui.Result(18)
def on_w20(self) -> None:
raise ui.Result(20)
def on_w24(self) -> None:
raise ui.Result(24)
def on_w33(self) -> None:
raise ui.Result(33)
if __debug__:
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import input_signal
return super().create_tasks() + (input_signal(),)

@ -1,10 +0,0 @@
from trezor import utils
if utils.MODEL in ("1",):
from .t1 import * # noqa: F401,F403
elif utils.MODEL in ("R",):
from .tr import * # noqa: F401,F403
elif utils.MODEL in ("T",):
from .tt import * # noqa: F401,F403
else:
raise ValueError("Unknown Trezor model")

@ -1,9 +0,0 @@
from micropython import const
TEXT_HEADER_HEIGHT = const(13)
TEXT_LINE_HEIGHT = const(9)
TEXT_LINE_HEIGHT_HALF = const(4)
TEXT_MARGIN_LEFT = const(0)
TEXT_MAX_LINES = const(4)
TEXT_MAX_LINES_NO_HEADER = const(5)
PAGINATION_MARGIN_RIGHT = const(4)

@ -1,9 +0,0 @@
from micropython import const
TEXT_HEADER_HEIGHT = const(13)
TEXT_LINE_HEIGHT = const(9)
TEXT_LINE_HEIGHT_HALF = const(4)
TEXT_MARGIN_LEFT = const(0)
TEXT_MAX_LINES = const(4)
TEXT_MAX_LINES_NO_HEADER = const(5)
PAGINATION_MARGIN_RIGHT = const(4)

@ -1,16 +0,0 @@
from micropython import const
TEXT_HEADER_HEIGHT = const(48)
TEXT_LINE_HEIGHT = const(26)
TEXT_LINE_HEIGHT_HALF = const(13)
TEXT_MARGIN_LEFT = const(14)
TEXT_MAX_LINES = const(5)
TEXT_MAX_LINES_NO_HEADER = const(7)
PAGINATION_MARGIN_RIGHT = const(22)
MONO_ADDR_PER_LINE = const(17)
MONO_HEX_PER_LINE = const(18)
QR_X = const(120)
QR_Y = const(112)
QR_SIDE_MAX = const(140)

@ -1,16 +0,0 @@
from trezor import ui
class Container(ui.Component):
def __init__(self, *children: ui.Component):
super().__init__()
self.children = children
def dispatch(self, event: int, x: int, y: int) -> None:
for child in self.children:
child.dispatch(event, x, y)
if __debug__:
def read_content(self) -> list[str]:
return sum((c.read_content() for c in self.children), [])

@ -2,12 +2,6 @@ from trezor import utils
from .common import * # noqa: F401,F403
try:
UI2 = True
import trezorui2 # noqa: F401
except ImportError:
UI2 = False
# NOTE: using any import magic probably causes mypy not to check equivalence of
# layout type signatures across models
if utils.MODEL in ("1",):
@ -15,9 +9,6 @@ if utils.MODEL in ("1",):
elif utils.MODEL in ("R",):
from .tr import * # noqa: F401,F403
elif utils.MODEL in ("T",):
if not UI2:
from .tt import * # noqa: F401,F403
else:
from .tt_v2 import * # noqa: F401,F403
from .tt_v2 import * # noqa: F401,F403
else:
raise ValueError("Unknown Trezor model")

@ -1,6 +1 @@
from . import UI2
if UI2:
from .tt_v2.altcoin import * # noqa: F401,F403
else:
from .tt.altcoin import * # noqa: F401,F403
from .tt_v2.altcoin import * # noqa: F401,F403

@ -33,12 +33,7 @@ async def interact(
br_type: str,
br_code: ButtonRequestType = ButtonRequestType.Other,
) -> Any:
if layout.__class__.__name__ == "Paginated":
from ..components.tt.scroll import Paginated
assert isinstance(layout, Paginated)
return await layout.interact(ctx, code=br_code)
elif hasattr(layout, "page_count") and layout.page_count() > 1: # type: ignore [Cannot access member "page_count" for type "LayoutType"]
if hasattr(layout, "page_count") and layout.page_count() > 1: # type: ignore [Cannot access member "page_count" for type "LayoutType"]
await button_request(ctx, br_type, br_code, pages=layout.page_count()) # type: ignore [Cannot access member "page_count" for type "LayoutType"]
return await ctx.wait(layout)
else:

@ -1,6 +1 @@
from . import UI2
if UI2:
from .tt_v2.recovery import * # noqa: F401,F403
else:
from .tt.recovery import * # noqa: F401,F403
from .tt_v2.recovery import * # noqa: F401,F403

@ -1,6 +1 @@
from . import UI2
if UI2:
from .tt_v2.reset import * # noqa: F401,F403
else:
from .tt.reset import * # noqa: F401,F403
from .tt_v2.reset import * # noqa: F401,F403

@ -5,6 +5,7 @@ from trezor.enums import ButtonRequestType
import trezorui2
from ..components.common.confirm import is_confirmed
from .common import interact
if TYPE_CHECKING:
@ -62,11 +63,7 @@ async def confirm_action(
verb: str | bytes | None = "OK",
verb_cancel: str | bytes | None = "cancel",
hold: bool = False,
hold_danger: bool = False,
icon: str | None = None,
icon_color: int | None = None,
reverse: bool = False,
larger_vspace: bool = False,
exc: ExceptionType = wire.ActionCancelled,
br_code: ButtonRequestType = ButtonRequestType.Other,
) -> None:
@ -97,7 +94,7 @@ async def confirm_action(
br_type,
br_code,
)
if result is not trezorui2.CONFIRMED:
if not is_confirmed(result):
raise exc
@ -108,8 +105,6 @@ async def confirm_text(
data: str,
description: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Other,
icon: str = ui.ICON_SEND, # TODO cleanup @ redesign
icon_color: int = ui.GREEN, # TODO cleanup @ redesign
) -> None:
result = await interact(
ctx,
@ -123,7 +118,7 @@ async def confirm_text(
br_type,
br_code,
)
if result is not trezorui2.CONFIRMED:
if not is_confirmed(result):
raise wire.ActionCancelled
@ -131,10 +126,8 @@ async def show_error_and_raise(
ctx: wire.GenericContext,
br_type: str,
content: str,
header: str = "Error",
subheader: str | None = None,
button: str = "Close",
red: bool = False,
exc: ExceptionType = wire.ActionCancelled,
) -> NoReturn:
raise NotImplementedError

File diff suppressed because it is too large Load Diff

@ -1,81 +0,0 @@
from typing import Sequence
from trezor import ui, wire
from trezor.enums import ButtonRequestType
from trezor.utils import chunks_intersperse
from ...components.common.confirm import raise_if_cancelled
from ...components.tt.confirm import Confirm, HoldToConfirm
from ...components.tt.scroll import Paginated
from ...components.tt.text import Text
from ...constants.tt import MONO_ADDR_PER_LINE
from ..common import interact
async def confirm_total_ethereum(
ctx: wire.GenericContext, total_amount: str, gas_price: str, fee_max: str
) -> None:
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN, new_lines=False)
text.bold(total_amount)
text.normal(" ", ui.GREY, "Gas price:", ui.FG)
text.bold(gas_price)
text.normal(" ", ui.GREY, "Maximum fee:", ui.FG)
text.bold(fee_max)
await raise_if_cancelled(
interact(ctx, HoldToConfirm(text), "confirm_total", ButtonRequestType.SignTx)
)
async def confirm_total_ripple(
ctx: wire.GenericContext,
address: str,
amount: str,
) -> None:
title = "Confirm sending"
text = Text(title, ui.ICON_SEND, ui.GREEN, new_lines=False)
text.bold(f"{amount} XRP\n")
text.normal("to\n")
text.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE))
await raise_if_cancelled(
interact(ctx, HoldToConfirm(text), "confirm_output", ButtonRequestType.SignTx)
)
async def confirm_transfer_binance(
ctx: wire.GenericContext, inputs_outputs: Sequence[tuple[str, str, str]]
) -> None:
pages: list[ui.Component] = []
for title, amount, address in inputs_outputs:
coin_page = Text(title, ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False)
coin_page.bold(amount)
coin_page.normal("\nto\n")
coin_page.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE))
pages.append(coin_page)
pages[-1] = HoldToConfirm(pages[-1])
await raise_if_cancelled(
interact(
ctx, Paginated(pages), "confirm_transfer", ButtonRequestType.ConfirmOutput
)
)
async def confirm_decred_sstx_submission(
ctx: wire.GenericContext,
address: str,
amount: str,
) -> None:
text = Text("Purchase ticket", ui.ICON_SEND, ui.GREEN, new_lines=False)
text.normal(amount)
text.normal("\nwith voting rights to\n")
text.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE))
await raise_if_cancelled(
interact(
ctx,
Confirm(text),
"confirm_decred_sstx_submission",
ButtonRequestType.ConfirmOutput,
)
)

@ -1,135 +0,0 @@
from typing import Callable, Iterable
from trezor import strings, ui, wire
from trezor.crypto.slip39 import MAX_SHARE_COUNT
from trezor.enums import ButtonRequestType
from ...components.common.confirm import (
is_confirmed,
is_confirmed_info,
raise_if_cancelled,
)
from ...components.tt.confirm import Confirm, InfoConfirm
from ...components.tt.keyboard_bip39 import Bip39Keyboard
from ...components.tt.keyboard_slip39 import Slip39Keyboard
from ...components.tt.recovery import RecoveryHomescreen
from ...components.tt.scroll import Paginated
from ...components.tt.text import Text
from ...components.tt.word_select import WordSelector
from ..common import button_request, interact
async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int:
await button_request(ctx, "word_count", code=ButtonRequestType.MnemonicWordCount)
if dry_run:
text = Text("Seed check", ui.ICON_RECOVERY)
else:
text = Text("Recovery mode", ui.ICON_RECOVERY)
text.normal("Number of words?")
count = await ctx.wait(WordSelector(text))
# WordSelector can return int, or string if the value came from debuglink
# ctx.wait has a return type Any
# Hence, it is easier to convert the returned value to int explicitly
return int(count)
async def request_word(
ctx: wire.GenericContext, word_index: int, word_count: int, is_slip39: bool
) -> str:
if is_slip39:
keyboard: Slip39Keyboard | Bip39Keyboard = Slip39Keyboard(
f"Type word {word_index + 1} of {word_count}:"
)
else:
keyboard = Bip39Keyboard(f"Type word {word_index + 1} of {word_count}:")
word: str = await ctx.wait(keyboard)
return word
async def show_remaining_shares(
ctx: wire.GenericContext,
groups: Iterable[tuple[int, tuple[str, ...]]], # remaining + list 3 words
shares_remaining: list[int],
group_threshold: int,
) -> None:
pages: list[ui.Component] = []
for remaining, group in groups:
if 0 < remaining < MAX_SHARE_COUNT:
text = Text("Remaining Shares")
text.bold(
strings.format_plural(
"{count} more {plural} starting", remaining, "share"
)
)
for word in group:
text.normal(word)
pages.append(text)
elif (
remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold
):
text = Text("Remaining Shares")
groups_remaining = group_threshold - shares_remaining.count(0)
text.bold(
strings.format_plural(
"{count} more {plural} starting", groups_remaining, "group"
)
)
for word in group:
text.normal(word)
pages.append(text)
pages[-1] = Confirm(pages[-1], cancel=None)
await raise_if_cancelled(
interact(ctx, Paginated(pages), "show_shares", ButtonRequestType.Other)
)
async def show_group_share_success(
ctx: wire.GenericContext, share_index: int, group_index: int
) -> None:
text = Text("Success", ui.ICON_CONFIRM)
text.bold("You have entered")
text.bold(f"Share {share_index + 1}")
text.normal("from")
text.bold(f"Group {group_index + 1}")
await raise_if_cancelled(
interact(
ctx,
Confirm(text, confirm="Continue", cancel=None),
"share_success",
ButtonRequestType.Other,
)
)
async def continue_recovery(
ctx: wire.GenericContext,
button_label: str,
text: str,
subtext: str | None,
info_func: Callable | None,
dry_run: bool,
) -> bool:
homepage = RecoveryHomescreen(dry_run, text, subtext)
if info_func is not None:
content = InfoConfirm(
homepage,
confirm=button_label,
info="Info",
cancel="Abort",
)
await button_request(ctx, "recovery", ButtonRequestType.RecoveryHomepage)
return await is_confirmed_info(ctx, content, info_func)
else:
return is_confirmed(
await interact(
ctx,
Confirm(homepage, confirm=button_label, major_confirm=True),
"recovery",
ButtonRequestType.RecoveryHomepage,
)
)

@ -1,363 +0,0 @@
from typing import TYPE_CHECKING
from trezor import ui, utils, wire
from trezor.enums import BackupType, ButtonRequestType
from ...components.common.confirm import is_confirmed, raise_if_cancelled
from ...components.tt.button import ButtonDefault
from ...components.tt.checklist import Checklist
from ...components.tt.confirm import Confirm, HoldToConfirm
from ...components.tt.info import InfoConfirm
from ...components.tt.reset import MnemonicWordSelect, Slip39NumInput
from ...components.tt.scroll import Paginated
from ...components.tt.text import Text
from ..common import interact
from . import confirm_action
if TYPE_CHECKING:
from typing import Sequence
NumberedWords = Sequence[tuple[int, str]]
if __debug__:
from apps import debug
async def show_share_words(
ctx: wire.GenericContext,
share_words: Sequence[str],
share_index: int | None = None,
group_index: int | None = None,
) -> None:
first, chunks, last = _split_share_into_pages(share_words)
if share_index is None:
header_title = "Recovery seed"
elif group_index is None:
header_title = f"Recovery share #{share_index + 1}"
else:
header_title = f"Group {group_index + 1} - Share {share_index + 1}"
header_icon = ui.ICON_RESET
pages: list[ui.Component] = [] # ui page components
shares_words_check = [] # check we display correct data
# first page
text = Text(header_title, header_icon)
text.bold("Write down these")
text.bold(f"{len(share_words)} words:")
text.br_half()
for index, word in first:
text.mono(f"{index + 1}. {word}")
shares_words_check.append(word)
pages.append(text)
# middle pages
for chunk in chunks:
text = Text(header_title, header_icon)
for index, word in chunk:
text.mono(f"{index + 1}. {word}")
shares_words_check.append(word)
pages.append(text)
# last page
text = Text(header_title, header_icon)
for index, word in last:
text.mono(f"{index + 1}. {word}")
shares_words_check.append(word)
text.br_half()
text.bold(f"I wrote down all {len(share_words)}")
text.bold("words in order.")
pages.append(text)
pages[-1] = HoldToConfirm(pages[-1], cancel=False)
# pagination
paginated = Paginated(pages)
if __debug__:
word_pages = [first] + chunks + [last]
def export_displayed_words() -> None:
# export currently displayed mnemonic words into debuglink
words = [w for _, w in word_pages[paginated.page]]
debug.reset_current_words.publish(words)
paginated.on_change = export_displayed_words
export_displayed_words()
# make sure we display correct data
utils.ensure(share_words == shares_words_check)
# confirm the share
await raise_if_cancelled(
interact(
ctx,
paginated,
"backup_words",
ButtonRequestType.ResetDevice,
)
)
async def select_word(
ctx: wire.GenericContext,
words: Sequence[str],
share_index: int | None,
checked_index: int,
count: int,
group_index: int | None = None,
) -> str:
# let the user pick a word
select = MnemonicWordSelect(words, share_index, checked_index, count, group_index)
selected_word: str = await ctx.wait(select)
return selected_word
def _split_share_into_pages(
share_words: Sequence[str],
) -> tuple[NumberedWords, list[NumberedWords], NumberedWords]:
share = list(enumerate(share_words)) # we need to keep track of the word indices
first = share[:2] # two words on the first page
length = len(share_words)
if length in (12, 20, 24):
middle = share[2:-2]
last = share[-2:] # two words on the last page
elif length in (18, 33):
middle = share[2:]
last = [] # no words at the last page, because it does not add up
else:
# Invalid number of shares. SLIP-39 allows 20 or 33 words, BIP-39 12 or 24
raise RuntimeError
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
return first, list(chunks), last
async def slip39_show_checklist(
ctx: wire.GenericContext, step: int, backup_type: BackupType
) -> None:
checklist = Checklist("Backup checklist", ui.ICON_RESET)
if backup_type is BackupType.Slip39_Basic:
checklist.add("Set number of shares")
checklist.add("Set threshold")
checklist.add(("Write down and check", "all recovery shares"))
elif backup_type is BackupType.Slip39_Advanced:
checklist.add("Set number of groups")
checklist.add("Set group threshold")
checklist.add(("Set size and threshold", "for each group"))
checklist.select(step)
await raise_if_cancelled(
interact(
ctx,
Confirm(checklist, confirm="Continue", cancel=None),
"slip39_checklist",
ButtonRequestType.ResetDevice,
)
)
async def slip39_prompt_threshold(
ctx: wire.GenericContext, num_of_shares: int, group_id: int | None = None
) -> int:
count = num_of_shares // 2 + 1
# min value of share threshold is 2 unless the number of shares is 1
# number of shares 1 is possible in advnaced slip39
min_count = min(2, num_of_shares)
max_count = num_of_shares
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id
)
confirmed = is_confirmed(
await interact(
ctx,
Confirm(
shares,
confirm="Continue",
cancel="Info",
major_confirm=True,
cancel_style=ButtonDefault,
),
"slip39_threshold",
ButtonRequestType.ResetDevice,
)
)
count = shares.input.count
if confirmed:
break
text = "The threshold sets the number of shares "
if group_id is None:
text += "needed to recover your wallet. "
text += f"Set it to {count} and you will need "
if num_of_shares == 1:
text += "1 share."
elif num_of_shares == count:
text += f"all {count} of your {num_of_shares} shares."
else:
text += f"any {count} of your {num_of_shares} shares."
else:
text += "needed to form a group. "
text += f"Set it to {count} and you will "
if num_of_shares == 1:
text += "need 1 share "
elif num_of_shares == count:
text += f"need all {count} of {num_of_shares} shares "
else:
text += f"need any {count} of {num_of_shares} shares "
text += f"to form Group {group_id + 1}."
info = InfoConfirm(text)
await info
return count
async def slip39_prompt_number_of_shares(
ctx: wire.GenericContext, group_id: int | None = None
) -> int:
count = 5
min_count = 1
max_count = 16
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_SHARES, count, min_count, max_count, group_id
)
confirmed = is_confirmed(
await interact(
ctx,
Confirm(
shares,
confirm="Continue",
cancel="Info",
major_confirm=True,
cancel_style=ButtonDefault,
),
"slip39_shares",
ButtonRequestType.ResetDevice,
)
)
count = shares.input.count
if confirmed:
break
if group_id is None:
info = InfoConfirm(
"Each recovery share is a "
"sequence of 20 words. "
"Next you will choose "
"how many shares you "
"need to recover your "
"wallet."
)
else:
info = InfoConfirm(
"Each recovery share is a "
"sequence of 20 words. "
"Next you will choose "
"the threshold number of "
"shares needed to form "
f"Group {group_id + 1}."
)
await info
return count
async def slip39_advanced_prompt_number_of_groups(ctx: wire.GenericContext) -> int:
count = 5
min_count = 2
max_count = 16
while True:
shares = Slip39NumInput(Slip39NumInput.SET_GROUPS, count, min_count, max_count)
confirmed = is_confirmed(
await interact(
ctx,
Confirm(
shares,
confirm="Continue",
cancel="Info",
major_confirm=True,
cancel_style=ButtonDefault,
),
"slip39_groups",
ButtonRequestType.ResetDevice,
)
)
count = shares.input.count
if confirmed:
break
info = InfoConfirm(
"Each group has a set "
"number of shares and "
"its own threshold. In the "
"next steps you will set "
"the numbers of shares "
"and the thresholds."
)
await info
return count
async def slip39_advanced_prompt_group_threshold(
ctx: wire.GenericContext, num_of_groups: int
) -> int:
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
while True:
shares = Slip39NumInput(
Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
)
confirmed = is_confirmed(
await interact(
ctx,
Confirm(
shares,
confirm="Continue",
cancel="Info",
major_confirm=True,
cancel_style=ButtonDefault,
),
"slip39_group_threshold",
ButtonRequestType.ResetDevice,
)
)
count = shares.input.count
if confirmed:
break
else:
info = InfoConfirm(
"The group threshold "
"specifies the number of "
"groups required to "
"recover your wallet. "
)
await info
return count
async def show_warning_backup(ctx: wire.GenericContext, slip39: bool) -> None:
if slip39:
description = "Never make a digital copy of your recovery shares and never upload them online!"
else:
description = "Never make a digital copy of your recovery seed and never upload\nit online!"
await confirm_action(
ctx,
"backup_warning",
"Caution",
description=description,
verb="I understand",
verb_cancel=None,
icon=ui.ICON_NOCOPY,
br_code=ButtonRequestType.ResetDevice,
)

@ -1,34 +0,0 @@
from trezor import ui, wire
from trezor.enums import ButtonRequestType
from ...components.common.confirm import is_confirmed
from ...components.common.webauthn import ConfirmInfo
from ...components.tt.confirm import Confirm, ConfirmPageable, Pageable
from ...components.tt.text import Text
from ...components.tt.webauthn import ConfirmContent
from ..common import interact
async def confirm_webauthn(
ctx: wire.GenericContext | None,
info: ConfirmInfo,
pageable: Pageable | None = None,
) -> bool:
if pageable is not None:
confirm: ui.Layout = ConfirmPageable(pageable, ConfirmContent(info))
else:
confirm = Confirm(ConfirmContent(info))
if ctx is None:
return is_confirmed(await confirm)
else:
return is_confirmed(
await interact(ctx, confirm, "confirm_webauthn", ButtonRequestType.Other)
)
async def confirm_webauthn_reset() -> bool:
text = Text("FIDO2 Reset", ui.ICON_CONFIG)
text.normal("Do you really want to")
text.bold("erase all credentials?")
return is_confirmed(await Confirm(text))

@ -1,6 +1 @@
from . import UI2
if UI2:
from .tt_v2.webauthn import * # noqa: F401,F403
else:
from .tt.webauthn import * # noqa: F401,F403
from .tt_v2.webauthn import * # noqa: F401,F403

@ -20,14 +20,6 @@ class LoaderDefault:
icon_fg_color: int | None = ui.WHITE
class LoaderDanger(LoaderDefault):
class normal(LoaderDefault.normal):
fg_color = ui.RED
class active(LoaderDefault.active):
fg_color = ui.RED
class LoaderNeutral(LoaderDefault):
class normal(LoaderDefault.normal):
fg_color = ui.FG

@ -1,44 +0,0 @@
from typing import Sequence
from trezor import ui
from trezor.ui.constants import QR_SIDE_MAX
_QR_WIDTHS = (21, 25, 29, 33, 37, 41, 45, 49, 53, 57)
_THRESHOLDS_BINARY = (14, 26, 42, 62, 84, 106, 122, 152, 180, 213)
_THRESHOLDS_ALPHANUM = (20, 38, 61, 90, 122, 154, 178, 221, 262, 311)
def _qr_version_index(data: str, thresholds: Sequence[int]) -> int:
for i, threshold in enumerate(thresholds):
if len(data) <= threshold:
return i
raise ValueError # data too long
def is_alphanum_only(data: str) -> bool:
return all(c in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $*+-./:" for c in data)
class Qr(ui.Component):
def __init__(self, data: str, case_sensitive: bool, x: int, y: int) -> None:
super().__init__()
if case_sensitive and not is_alphanum_only(data):
# must encode in BINARY mode
version_idx = _qr_version_index(data, _THRESHOLDS_BINARY)
else:
# can make the QR code more readable by using the best version
version_idx = _qr_version_index(data, _THRESHOLDS_ALPHANUM)
if len(data) > _THRESHOLDS_BINARY[version_idx]:
data = data.upper()
size = _QR_WIDTHS[version_idx]
self.data = data
self.x = x
self.y = y
self.scale = QR_SIDE_MAX // size
def on_render(self) -> None:
if self.repaint:
ui.display.qrcode(self.x, self.y, self.data, self.scale)
self.repaint = False

@ -2,9 +2,6 @@ from micropython import const
from trezor.ui import rgb
# radius for buttons and other elements
RADIUS = const(2)
# backlight brightness
BACKLIGHT_NORMAL = const(150)
BACKLIGHT_LOW = const(45)
@ -14,58 +11,21 @@ BACKLIGHT_MAX = const(255)
# color palette
RED = rgb(0xFF, 0x00, 0x00)
PINK = rgb(0xE9, 0x1E, 0x63)
PURPLE = rgb(0x9C, 0x27, 0xB0)
DEEP_PURPLE = rgb(0x67, 0x3A, 0xB7)
INDIGO = rgb(0x3F, 0x51, 0xB5)
BLUE = rgb(0x21, 0x96, 0xF3)
LIGHT_BLUE = rgb(0x03, 0xA9, 0xF4)
CYAN = rgb(0x00, 0xBC, 0xD4)
TEAL = rgb(0x00, 0x96, 0x88)
GREEN = rgb(0x00, 0xAE, 0x0B)
LIGHT_GREEN = rgb(0x87, 0xCE, 0x26)
LIME = rgb(0xCD, 0xDC, 0x39)
YELLOW = rgb(0xFF, 0xEB, 0x3B)
AMBER = rgb(0xFF, 0xC1, 0x07)
ORANGE = rgb(0xFF, 0x98, 0x00)
DEEP_ORANGE = rgb(0xFF, 0x57, 0x22)
BROWN = rgb(0x79, 0x55, 0x48)
LIGHT_GREY = rgb(0xDA, 0xDD, 0xD8)
GREY = rgb(0x9E, 0x9E, 0x9E)
DARK_GREY = rgb(0x3E, 0x3E, 0x3E)
BLUE_GRAY = rgb(0x60, 0x7D, 0x8B)
BLACK = rgb(0x00, 0x00, 0x00)
WHITE = rgb(0xFA, 0xFA, 0xFA)
BLACKISH = rgb(0x30, 0x30, 0x30)
DARK_BLACK = rgb(0x10, 0x10, 0x10)
DARK_WHITE = rgb(0xE8, 0xE8, 0xE8)
TITLE_GREY = rgb(0x9B, 0x9B, 0x9B)
ORANGE_ICON = rgb(0xF5, 0xA6, 0x23)
# common color styles
BG = BLACK
FG = WHITE
# icons
ICON_RESET = "trezor/res/header_icons/reset.toif"
ICON_WIPE = "trezor/res/header_icons/wipe.toif"
ICON_RECOVERY = "trezor/res/header_icons/recovery.toif"
ICON_NOCOPY = "trezor/res/header_icons/nocopy.toif"
ICON_WRONG = "trezor/res/header_icons/wrong.toif"
ICON_CONFIG = "trezor/res/header_icons/cog.toif"
ICON_RECEIVE = "trezor/res/header_icons/receive.toif"
ICON_SEND = "trezor/res/header_icons/send.toif"
ICON_DEFAULT = ICON_CONFIG
ICON_CANCEL = "trezor/res/cancel.toif"
ICON_CONFIRM = "trezor/res/confirm.toif"
ICON_LOCK = "trezor/res/lock.toif"
ICON_CLICK = "trezor/res/click.toif"
ICON_BACK = "trezor/res/left.toif"
ICON_SWIPE = "trezor/res/swipe.toif"
ICON_SWIPE_LEFT = "trezor/res/swipe_left.toif"
ICON_SWIPE_RIGHT = "trezor/res/swipe_right.toif"
ICON_CHECK = "trezor/res/check.toif"
ICON_SPACE = "trezor/res/space.toif"
ICON_DEFAULT = ICON_CONFIG

@ -1,150 +0,0 @@
import mock
from common import *
from trezor import ui
from trezor.ui import display
from trezor.ui.components.common import text
if False:
from typing import List, Tuple
class TestTextSpan(unittest.TestCase):
def lines(self, span: text.Span) -> List[Tuple[str, int, bool]]:
result = []
while True:
should_continue = span.next_line()
substr = span.string[span.start : span.start + span.length]
result.append((substr, span.width, span.word_break))
if not should_continue:
break
return result
def checkSpanWithoutWidths(
self, span: text.Span, expected: List[Tuple[str, bool]]
) -> None:
expected_with_calculated_widths = [
(string, ui.display.text_width(string, span.font), word_break)
for string, word_break in expected
]
self.assertListEqual(self.lines(span), expected_with_calculated_widths)
def test_basic(self):
span = text.Span("hello")
self.checkSpanWithoutWidths(
span,
[("hello", False)],
)
span.reset("world", start=0, font=ui.NORMAL)
self.checkSpanWithoutWidths(
span,
[("world", False)],
)
span.reset("", start=0, font=ui.NORMAL)
self.checkSpanWithoutWidths(
span,
[("", False)],
)
def test_two_lines(self):
line_width = display.text_width("hello world", ui.NORMAL) - 1
span = text.Span("hello world", line_width=line_width)
self.checkSpanWithoutWidths(
span,
[
("hello", False),
("world", False),
],
)
def test_newlines(self):
span = text.Span("hello\nworld")
self.checkSpanWithoutWidths(
span,
[
("hello", False),
("world", False),
],
)
span = text.Span("\nhello\nworld\n")
self.checkSpanWithoutWidths(
span,
[
("", False),
("hello", False),
("world", False),
("", False),
],
)
def test_break_words(self):
line_width = display.text_width("hello w", ui.NORMAL) + text.DASH_WIDTH
span = text.Span("hello world", line_width=line_width, break_words=True)
self.checkSpanWithoutWidths(
span,
[
("hello w", True),
("orld", False),
],
)
def test_long_word(self):
line_width = display.text_width("establishme", ui.NORMAL) + text.DASH_WIDTH
span = text.Span(
"Down with the establishment!", line_width=line_width, break_words=False
)
self.checkSpanWithoutWidths(
span,
[
("Down with", False),
("the", False),
("establishme", True),
("nt!", False),
],
)
def test_has_more_content(self):
line_width = display.text_width("hello world", ui.NORMAL) - 1
span = text.Span("hello world", line_width=line_width)
self.assertTrue(span.has_more_content())
self.assertTrue(span.next_line())
self.assertEqual("hello", span.string[span.start : span.start + span.length])
# has_more_content is True because there's text remaining on the line
self.assertTrue(span.has_more_content())
# next_line is False because we should not continue iterating
self.assertFalse(span.next_line())
self.assertEqual("world", span.string[span.start : span.start + span.length])
self.assertFalse(span.has_more_content())
self.assertFalse(span.next_line())
self.assertEqual("world", span.string[span.start : span.start + span.length])
def test_has_more_content_trailing_newline(self):
span = text.Span("1\n2\n3\n")
self.assertTrue(span.has_more_content())
self.assertTrue(span.next_line())
self.assertEqual("1", span.string[span.start : span.start + span.length])
self.assertTrue(span.has_more_content())
self.assertTrue(span.next_line())
self.assertEqual("2", span.string[span.start : span.start + span.length])
self.assertTrue(span.has_more_content())
self.assertTrue(span.next_line())
self.assertEqual("3", span.string[span.start : span.start + span.length])
# has_more_content is False because the "remaining" text is empty
self.assertFalse(span.has_more_content())
# next_line is False because we should not continue iterating
self.assertFalse(span.next_line())
self.assertEqual("", span.string[span.start : span.start + span.length])
if __name__ == "__main__":
unittest.main()

@ -54,7 +54,7 @@ or contain `[no changelog]` in the commit message.
## BUILD stage - [build.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml)
All builds are published as artifacts so they can be downloaded and used.
Consists of **28 jobs** below:
Consists of **27 jobs** below:
### [core fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L20)
Build of Core into firmware. Regular version.
@ -102,130 +102,127 @@ Frozen version. That means you do not need any other files to run it,
it is just a single binary file that you can execute directly.
**Are you looking for a Trezor T emulator? This is most likely it.**
### [core unix frozen ui2 debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L255)
### [core unix frozen debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L254)
### [core unix frozen debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L269)
### [core unix frozen debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L270)
### [core unix frozen debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L285)
### [core unix frozen btconly debug t1 build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L292)
### [core unix frozen btconly debug t1 build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L307)
### [core macos frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L308)
### [core macos frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L323)
### [crypto build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L348)
### [crypto build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L333)
Build of our cryptographic library, which is then incorporated into the other builds.
### [legacy fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L377)
### [legacy fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L362)
### [legacy fw regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L393)
### [legacy fw regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L378)
### [legacy fw btconly build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L410)
### [legacy fw btconly build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L395)
### [legacy fw btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L429)
### [legacy fw btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L414)
### [legacy emu regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L450)
### [legacy emu regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L435)
Regular version (not only Bitcoin) of above.
**Are you looking for a Trezor One emulator? This is most likely it.**
### [legacy emu regular debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L465)
### [legacy emu regular debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L450)
### [legacy emu regular debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L483)
### [legacy emu regular debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L468)
### [legacy emu btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L509)
### [legacy emu btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L494)
Build of Legacy into UNIX emulator. Use keyboard arrows to emulate button presses.
Bitcoin-only version.
### [legacy emu btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L526)
### [legacy emu btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L511)
---
## TEST stage - [test.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml)
All the tests run test cases on the freshly built emulators from the previous `BUILD` stage.
Consists of **35 jobs** below:
Consists of **34 jobs** below:
### [core unit test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L15)
Python and rust unit tests, checking TT functionality.
### [core unit python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L15)
Python unit tests, checking core functionality.
### [core unit ui2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L23)
### [core unit rust test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L24)
Rust unit tests.
### [core unit asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L32)
### [core unit asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L33)
### [core unit t1 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L49)
### [core unit t1 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L50)
### [core device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L64)
### [core device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L65)
Device tests for Core. Running device tests and also comparing screens
with the expected UI result.
See artifacts for a comprehensive report of UI.
See [docs/tests/ui-tests](../tests/ui-tests.md) for more info.
### [core device ui2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L93)
### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L127)
### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L94)
### [core btconly device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L146)
### [core btconly device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L113)
Device tests excluding altcoins, only for BTC.
### [core btconly device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L166)
### [core btconly device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L133)
### [core monero test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L187)
### [core monero test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L154)
Monero tests.
### [core monero asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L206)
### [core monero asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L173)
### [core u2f test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L228)
### [core u2f test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L195)
Tests for U2F and HID.
### [core u2f asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L247)
### [core u2f asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L214)
### [core fido2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L265)
### [core fido2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L232)
FIDO2 device tests.
### [core fido2 asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L288)
### [core fido2 asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L255)
### [core click test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L308)
### [core click test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L275)
Click tests.
See [docs/tests/click-tests](../tests/click-tests.md) for more info.
### [core click asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L325)
### [core click asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L292)
### [core upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L346)
### [core upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L313)
Upgrade tests.
See [docs/tests/upgrade-tests](../tests/upgrade-tests.md) for more info.
### [core upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L365)
### [core upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L332)
### [core persistence test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L387)
### [core persistence test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L354)
Persistence tests.
### [core persistence asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L403)
### [core persistence asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L370)
### [core hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L421)
### [core hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L388)
### [crypto test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L439)
### [crypto test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L406)
### [legacy device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L470)
### [legacy device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L437)
### [legacy asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L497)
### [legacy asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L464)
### [legacy btconly test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L509)
### [legacy btconly test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L476)
### [legacy btconly asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L529)
### [legacy btconly asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L496)
### [legacy upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L544)
### [legacy upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L511)
### [legacy upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L563)
### [legacy upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L530)
### [legacy hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L584)
### [legacy hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L551)
### [python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L603)
### [python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L570)
### [python support test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L622)
### [python support test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L589)
### [storage test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L632)
### [storage test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L599)
### [core unix memory profiler](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L656)
### [core unix memory profiler](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L623)
### [connect test core](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L680)
### [connect test core](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L647)
---
## TEST-HW stage - [test-hw.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/test-hw.yml)
@ -272,7 +269,7 @@ Consists of **2 jobs** below:
---
## DEPLOY stage - [deploy.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml)
Consists of **14 jobs** below:
Consists of **13 jobs** below:
### [release core fw regular deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L5)
@ -296,10 +293,8 @@ Consists of **14 jobs** below:
### [ui tests fixtures deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L229)
### [ui tests ui2 fixtures deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L249)
### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L270)
### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L251)
### [common sync](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L295)
### [common sync](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L276)
---

@ -1,6 +1,5 @@
import hashlib
import json
import os
import re
import shutil
from contextlib import contextmanager
@ -123,8 +122,6 @@ def screen_recording(
# Making the model global for other functions
global MODEL
MODEL = f"T{client.features.model}"
if os.getenv("UI2") == "1":
MODEL += "ui2"
test_name = f"{MODEL}_{test_name}"

Loading…
Cancel
Save