mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-21 23:18:13 +00:00
feat(core/ui): delete old UI code
This commit is contained in:
parent
1a9f3c18dd
commit
d045e0089d
15
ci/build.yml
15
ci/build.yml
@ -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():
|
||||
|
49
ci/test.yml
49
ci/test.yml
@ -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()
|
115
docs/ci/jobs.md
115
docs/ci/jobs.md
@ -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#L94)
|
||||
|
||||
### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L127)
|
||||
|
||||
### [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#L251)
|
||||
|
||||
### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L270)
|
||||
|
||||
### [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…
Reference in New Issue
Block a user