fancy text entry
This commit is contained in:
parent
f3527e1960
commit
f69157dcd9
@ -21,6 +21,7 @@ from glob import settings
|
||||
from pincodes import pa
|
||||
from menu import start_chooser
|
||||
from version import MAX_TXN_LEN
|
||||
from charcodes import KEY_NFC
|
||||
|
||||
|
||||
CLEAR_PIN = '999999-999999'
|
||||
@ -1021,7 +1022,7 @@ async def export_xpub(label, _2, item):
|
||||
if '{acct}' in path:
|
||||
msg += "Press (1) to select account other than zero. "
|
||||
if glob.NFC:
|
||||
msg += "Press (3) to share via NFC. "
|
||||
msg += "Press (" + ("nfc" if qwerty else "3") + ") to share via NFC. "
|
||||
|
||||
ch = await ux_show_story(msg, escape='13')
|
||||
if ch == 'x': return
|
||||
@ -1041,7 +1042,7 @@ async def export_xpub(label, _2, item):
|
||||
node = sv.derive_path(path) if path != 'm' else sv.node
|
||||
xpub = chain.serialize_public(node, addr_fmt)
|
||||
|
||||
if ch == '3' and glob.NFC:
|
||||
if glob.NFC and ch in '3'+KEY_NFC:
|
||||
await glob.NFC.share_text(xpub)
|
||||
else:
|
||||
from ux import show_qr_code
|
||||
@ -1316,7 +1317,7 @@ async def import_xprv(_1, _2, item):
|
||||
prompt, escape = import_prompt_builder("%s file" % label)
|
||||
if prompt:
|
||||
ch = await ux_show_story(prompt, escape=escape)
|
||||
if ch == "3":
|
||||
if ch in "3"+KEY_NFC:
|
||||
force_vdisk = None
|
||||
extended_key = await NFC.read_extended_private_key()
|
||||
if not extended_key:
|
||||
@ -1481,7 +1482,7 @@ async def import_tapsigner_backup_file(_1, _2, item):
|
||||
prompt, escape = import_prompt_builder(label)
|
||||
if prompt:
|
||||
ch = await ux_show_story(prompt, escape=escape)
|
||||
if ch == "3":
|
||||
if ch in "3"+KEY_NFC:
|
||||
force_vdisk = None
|
||||
data = await NFC.read_tapsigner_b64_backup()
|
||||
if not data:
|
||||
@ -1505,17 +1506,18 @@ async def import_tapsigner_backup_file(_1, _2, item):
|
||||
data = fp.read()
|
||||
|
||||
if await ux_show_story("Make sure to have your TAPSIGNER handy as you will need to provide "
|
||||
"'Backup Password' from the back of the card in the next step. "
|
||||
"'Backup Password' from the back of the card in the next step.\n\n"
|
||||
"Press OK to continue X to cancel.") != "y":
|
||||
return
|
||||
|
||||
while True:
|
||||
backup_key = await ux_input_text("", confirm_exit=False, hex_only=True, max_len=32)
|
||||
backup_key = await ux_input_text("", confirm_exit=False, hex_only=True,
|
||||
min_len=32, max_len=32, prompt='Backup Password (32 hex digits)')
|
||||
if backup_key is None:
|
||||
return
|
||||
if len(backup_key) != 32:
|
||||
await ux_show_story(title="FAILURE", msg="'Backup Key' length != 32")
|
||||
continue
|
||||
|
||||
assert len(backup_key) == 32
|
||||
|
||||
try:
|
||||
extended_key, derivation = decrypt_tapsigner_backup(backup_key, data)
|
||||
break
|
||||
|
||||
@ -82,13 +82,14 @@ DECODER_CAPS = (KEY_NFC + KEY_QR + KEY_TAB
|
||||
|
||||
# when SYMBOL pressed
|
||||
# - be nice and allow number+symbol == number + shift
|
||||
# - also nicity: symb+cancel=clear
|
||||
DECODER_SYMBOL = (KEY_NFC + KEY_QR + KEY_TAB
|
||||
+ KEY_HOME + KEY_PAGE_UP + KEY_PAGE_DOWN + KEY_END + KEY_CANCEL + KEY_SELECT + '\0'
|
||||
+ '!@#$%^&*()'
|
||||
+ '-_`\0\0\0[]{}'
|
||||
+ '+=\0\0:;~|\\"'
|
||||
+ KEY_F1 + KEY_F2 + KEY_F3 + KEY_F4 + KEY_F5 + KEY_F6 + '\0<>?'
|
||||
'\0\0\0\0\0\0\0\0\0\0' )
|
||||
'\0\0\0\0' + KEY_CLEAR + '\0\0\0\0\0' )
|
||||
|
||||
KEYNUM_LAMP = const(50)
|
||||
KEYNUM_SHIFT = const(51)
|
||||
|
||||
@ -341,6 +341,8 @@ class Display:
|
||||
|
||||
if cursor:
|
||||
# implement CursorSpec values
|
||||
assert 0 <= cursor.x < CHARS_W, 'cur x'
|
||||
assert 0 <= cursor.y < CHARS_H, 'cur y'
|
||||
self.gpu.cursor_at(*cursor)
|
||||
self.last_buf[cursor.y][cursor.x] = 0xfffd
|
||||
if cursor.dbl_wide:
|
||||
@ -589,6 +591,33 @@ class Display:
|
||||
|
||||
self.show()
|
||||
|
||||
def draw_box(self, x, y, w, h):
|
||||
# using line-drawing chars, draw a box
|
||||
# returns X pos of first inside char
|
||||
assert 0 <= h <= CHARS_H-2 # 8 max
|
||||
assert 0 <= w <= CHARS_W-2 # 32 max
|
||||
|
||||
if x is None:
|
||||
x = (CHARS_W - w - 2) // 2
|
||||
ln = '┏' + ('━'*w) + '┓'
|
||||
self.text(x, y, ln)
|
||||
for yy in range(y+1, y+h+1):
|
||||
self.text(x, yy, '┃')
|
||||
self.text(x+w+1, yy, '┃')
|
||||
|
||||
ln = '┗' + ln[1:-1] + '┛'
|
||||
self.text(x, y+h+1, ln)
|
||||
|
||||
return x+1
|
||||
|
||||
def clear_box(self, x, y, w, h):
|
||||
# clear (w/ spaces) a box on screen
|
||||
for Y in range(y, y+h):
|
||||
for X in range(x, x+w):
|
||||
assert 0 <= X < CHARS_W
|
||||
assert 0 <= Y < CHARS_H
|
||||
self.next_buf[Y][X] = 32
|
||||
|
||||
|
||||
# here for mpy reasons
|
||||
WIDTH = const(320)
|
||||
|
||||
@ -342,7 +342,7 @@ class MenuSystem:
|
||||
if not has_qwerty:
|
||||
key = numpad_remap(key)
|
||||
|
||||
if key == KEY_SELECT:
|
||||
if key == KEY_SELECT or key == ' ':
|
||||
# selected - done
|
||||
return self.cursor
|
||||
elif key == KEY_CANCEL:
|
||||
|
||||
@ -1167,7 +1167,8 @@ class PassphraseMenu(MenuSystem):
|
||||
async def view_edit_phrase(self, *a):
|
||||
# let them control each character
|
||||
global pp_sofar
|
||||
pw = await ux_input_text(pp_sofar)
|
||||
pw = await ux_input_text(pp_sofar, prompt="Your BIP-39 Passphrase",
|
||||
b39_complete=True, max_len=100)
|
||||
if pw is not None:
|
||||
pp_sofar = pw
|
||||
self.check_length()
|
||||
|
||||
@ -166,8 +166,9 @@ async def ux_input_numbers(val, validate_func):
|
||||
if len(here) < 32:
|
||||
here += ch
|
||||
|
||||
async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100):
|
||||
async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100, min_len=0, **_kws):
|
||||
# Allow them to pick each digit using "D-pad"
|
||||
# - Q1 version of this function can do much more w/ more keyword args
|
||||
from glob import dis
|
||||
from display import FontTiny, FontSmall
|
||||
|
||||
@ -277,6 +278,9 @@ async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100):
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y':
|
||||
if len(pw) < min_len:
|
||||
# enforce a minimum length, better: say so.
|
||||
continue
|
||||
return str(pw, 'ascii')
|
||||
elif ch == 'x':
|
||||
if len(pw) > 1:
|
||||
|
||||
157
shared/ux_q1.py
157
shared/ux_q1.py
@ -7,6 +7,7 @@ import utime, gc
|
||||
from charcodes import *
|
||||
from lcd_display import CHARS_W, CursorSpec
|
||||
from exceptions import AbortInteraction
|
||||
import bip39
|
||||
|
||||
class PressRelease:
|
||||
def __init__(self, need_release=KEY_SELECT+KEY_CANCEL):
|
||||
@ -115,19 +116,22 @@ async def ux_input_numbers(val, validate_func):
|
||||
# - not wanted on Q1; just get the digits w/ the text.
|
||||
pass
|
||||
|
||||
async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100):
|
||||
async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100,
|
||||
prompt='Enter value', min_len=0, b39_complete=False, scan_ok=False, num_words=0):
|
||||
# Get a text string.
|
||||
# - Should allow full unicode, NKDN
|
||||
# - but our font is mostly just ascii
|
||||
# - no control chars allowed either
|
||||
# - TODO: editing, line wrap, seed completion, etc
|
||||
# - TODO: press QR -> do scan and use that text
|
||||
# - TODO: regex validation for derviation paths
|
||||
# - TODO: assume phrase seed entry
|
||||
from glob import dis
|
||||
from ux import ux_show_story
|
||||
|
||||
dis.clear()
|
||||
dis.text(None, -2, "Use ↦ to auto-complete words.")
|
||||
if b39_complete or num_words:
|
||||
scan_ok = True
|
||||
dis.text(None, -2, "↦ to auto-complete. (QR) to scan.")
|
||||
dis.text(None, -1, "CANCEL or SELECT when done.")
|
||||
|
||||
# TODO:
|
||||
@ -135,32 +139,135 @@ async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100):
|
||||
# - multi line support
|
||||
# - add prompt text?
|
||||
|
||||
# map from what they entered, to allowed char. None if not allowed char
|
||||
# - can case fold if desired
|
||||
ch_remap = lambda ch: ch if ' ' <= ch < chr(127) else None
|
||||
if hex_only:
|
||||
ch_remap = lambda ch: ch.lower() if ch in '0123456789abcdefABCDEF' else None
|
||||
|
||||
|
||||
y = 2
|
||||
if max_len <= CHARS_W-2:
|
||||
# single-line or perhaps shorter value
|
||||
line_len = max_len
|
||||
num_lines = 1
|
||||
y = 4
|
||||
elif max_len == 100:
|
||||
# passphrase case, handle nicely
|
||||
line_len = 25
|
||||
num_lines = 4
|
||||
else:
|
||||
# multi-line mode: just do a box for most of screen
|
||||
num_lines = 6
|
||||
line_len = CHARS_W-2
|
||||
|
||||
dis.text(None, y-2, prompt)
|
||||
x = dis.draw_box(None, y-1, line_len, num_lines)
|
||||
|
||||
# NOTE:
|
||||
# - x,y here are top left of entry area
|
||||
# - not allow cursor movement, always appending to end
|
||||
|
||||
# no key-repeat on certain keys
|
||||
err_msg = last_err = None
|
||||
press = PressRelease()
|
||||
while 1:
|
||||
dis.clear_box(x, y, line_len, num_lines)
|
||||
|
||||
dis.text(1, 1, value+' ')
|
||||
bx = dis.width(value) + 1
|
||||
dis.show(cursor=CursorSpec(bx, 1, 0, 0))
|
||||
# show error msg, until they type anything to clear it
|
||||
if err_msg:
|
||||
dis.text(None, y+num_lines+1, err_msg)
|
||||
err_msg = None
|
||||
last_err = True
|
||||
elif last_err:
|
||||
dis.text(None, y+num_lines+1, '')
|
||||
last_err = False
|
||||
|
||||
if not value:
|
||||
bx = 0
|
||||
n = 0
|
||||
else:
|
||||
for n, ln_pos in enumerate(range(0, len(value), line_len)):
|
||||
ln = value[ln_pos:ln_pos+line_len]
|
||||
dis.text(x, y+n, ln)
|
||||
bx = len(ln)
|
||||
|
||||
# decide cursor appearance
|
||||
cur = CursorSpec(x+bx, y+n, 0, 0)
|
||||
if cur.x >= x+line_len:
|
||||
# outline mode if on final possible location
|
||||
cur = CursorSpec(x+line_len-1, y+n, 0, True)
|
||||
|
||||
dis.show(cursor=cur)
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == KEY_SELECT:
|
||||
return str(value, 'ascii')
|
||||
elif ch == KEY_DELETE:
|
||||
if len(value) >= min_len:
|
||||
return value
|
||||
else:
|
||||
err_msg = 'Need %d characters at least.' % min_len
|
||||
elif ch == KEY_DELETE or ch == KEY_LEFT:
|
||||
if len(value) > 0:
|
||||
# delete current char
|
||||
# delete last char
|
||||
value = value[:-1]
|
||||
elif ch == KEY_CLEAR:
|
||||
value = ''
|
||||
dis.text(0, 1, ' '*CHARS_W)
|
||||
elif ch == KEY_CANCEL:
|
||||
if confirm_exit:
|
||||
pp = await ux_show_story(
|
||||
"OK to leave without any changes? Or CANCEL to avoid leaving.")
|
||||
if pp == KEY_CANCEL: continue
|
||||
if pp == KEY_CANCEL:
|
||||
continue
|
||||
return None
|
||||
elif ' ' <= ch < chr(127):
|
||||
value += ch
|
||||
|
||||
elif b39_complete and ch == KEY_TAB:
|
||||
# match case and auto-complete BIP-39 word if we can
|
||||
# - search backwards for alpha chars, up to 5
|
||||
# - stop on first non-letter
|
||||
# - break if case changes, so "ZooAct" gives "Act"
|
||||
pref = []
|
||||
for b in reversed(value[-4:]):
|
||||
if not b: break
|
||||
if 'a' <= b.lower() <= 'z':
|
||||
pref.insert(0, b)
|
||||
if len(pref)>=1 and b.isupper() != pref[0].isupper():
|
||||
break
|
||||
else:
|
||||
break
|
||||
if not pref:
|
||||
#err_msg = 'Need some letters first.'
|
||||
continue
|
||||
|
||||
pref = ''.join(pref)
|
||||
exact, nextchars, is_word = bip39.next_char(pref.lower())
|
||||
|
||||
if is_word:
|
||||
# got a match; append it
|
||||
if pref.isupper():
|
||||
# all upper case, so append w/ same
|
||||
# - Titlecase will just happen w/o any code here
|
||||
is_word = is_word.upper()
|
||||
|
||||
value += is_word[len(pref):]
|
||||
elif not nextchars:
|
||||
err_msg = 'Not a prefix BIP-39 word: ' + pref
|
||||
elif len(nextchars) < 12:
|
||||
# 'sta' and other s-prefixes
|
||||
err_msg = 'Press next key: ' + nextchars
|
||||
else:
|
||||
err_msg = 'Need more letters.'
|
||||
print(pref)
|
||||
|
||||
else:
|
||||
ch = ch_remap(ch)
|
||||
if ch is not None:
|
||||
if len(value) < max_len:
|
||||
value += ch
|
||||
else:
|
||||
value = value[0:max_len-1] + ch
|
||||
|
||||
|
||||
|
||||
|
||||
def ux_show_pin(dis, pin, subtitle, is_first_part, is_confirmation, force_draw,
|
||||
footer=None, randomize=None):
|
||||
@ -178,16 +285,16 @@ def ux_show_pin(dis, pin, subtitle, is_first_part, is_confirmation, force_draw,
|
||||
# - only used at login, none of the other cases
|
||||
# - test w/ "simulator.py --q1 -g --eff --set rngk=1"
|
||||
|
||||
# remapped numbers along bottom
|
||||
dis.text(1, -5, ' 1 2 3 4 5 6 7 8 9 0 ', invert=1)
|
||||
dis.text(1, -4, '↳ ' + ' '.join(randomize[1:]) + ' ' + randomize[0])
|
||||
# show mapping of numbers vs. PIN digits
|
||||
dis.text(1, -5, ' ' + ' '.join(randomize[1:]) + ' ' + randomize[0] + ' ', invert=1)
|
||||
dis.text(1, -4, '↳ 1 2 3 4 5 6 7 8 9 0')
|
||||
|
||||
if force_draw:
|
||||
|
||||
if is_first_part:
|
||||
prompt="Enter FIRST part of PIN (XXX-)"
|
||||
prompt="Enter FIRST part of PIN (xxx-)"
|
||||
else:
|
||||
prompt="Enter SECOND part of PIN (-YYY)"
|
||||
prompt="Enter SECOND part of PIN (-yyy)"
|
||||
|
||||
if subtitle:
|
||||
# "New Main PIN" ... so not really a SUB title.
|
||||
@ -219,18 +326,22 @@ def ux_show_pin(dis, pin, subtitle, is_first_part, is_confirmation, force_draw,
|
||||
async def ux_login_countdown(sec):
|
||||
# Show a countdown, which may need to
|
||||
# run for multiple **days**
|
||||
# XXX untested TODO
|
||||
# - test with: ./simulator.py --q1 -g --eff --set lgto=60
|
||||
# - test with: ./simulator.py --q1 -g --eff --set lgto=3600
|
||||
# - test with: ./simulator.py --q1 -g --eff --set lgto=2419200
|
||||
from glob import dis
|
||||
from utime import ticks_ms, ticks_diff
|
||||
from utils import pretty_short_delay, pretty_delay
|
||||
|
||||
y = 4
|
||||
y = 1
|
||||
dis.clear()
|
||||
dis.text(None, y-2, "Login countdown in effect.", invert=1)
|
||||
dis.text(None, y-1, "Must wait:")
|
||||
dis.text(None, y, "Login countdown in effect.", invert=1)
|
||||
dis.text(None, y+2, "Must wait:")
|
||||
|
||||
st = ticks_ms()
|
||||
while sec > 0:
|
||||
dis.text(None, y, pretty_short_delay(sec))
|
||||
txt = pretty_delay(sec) if sec > 12*3600 else pretty_short_delay(sec)
|
||||
dis.text(None, y+4, txt)
|
||||
dis.busy_bar(1)
|
||||
|
||||
# this should be more accurate, errors were accumulating
|
||||
|
||||
Loading…
Reference in New Issue
Block a user