From 13af836ca118f87bb0c59c079e103dad7df189b9 Mon Sep 17 00:00:00 2001 From: "Peter D. Gray" Date: Tue, 30 May 2023 12:21:34 -0400 Subject: [PATCH] factor-out ux for q1 vs mk4 --- shared/address_explorer.py | 12 +- shared/manifest_mk4.py | 1 + shared/manifest_q1.py | 1 + shared/ux.py | 335 +----------------------------------- shared/ux_mk4.py | 337 +++++++++++++++++++++++++++++++++++++ shared/ux_q1.py | 336 ++++++++++++++++++++++++++++++++++++ 6 files changed, 688 insertions(+), 334 deletions(-) create mode 100644 shared/ux_mk4.py create mode 100644 shared/ux_q1.py diff --git a/shared/address_explorer.py b/shared/address_explorer.py index 9fa80192..a951dd90 100644 --- a/shared/address_explorer.py +++ b/shared/address_explorer.py @@ -4,7 +4,7 @@ # # Address Explorer menu functionality # -import chains, stash +import chains, stash, version from ux import ux_show_story, the_ux, ux_enter_bip32_index from menu import MenuSystem, MenuItem from public_constants import AFC_BECH32, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH @@ -21,7 +21,14 @@ def truncate_address(addr): # - 16 chars screen width # - but 2 lost at left (menu arrow, corner arrow) # - want to show not truncated on right side - return addr[0:6] + '⋯' + addr[-6:] + if not version.has_qwerty: + # - 16 chars screen width + # - but 2 lost at left (menu arrow, corner arrow) + # - want to show not truncated on right side + return addr[0:6] + '⋯' + addr[-6:] + else: + # tons of space on Q1 + return addr[0:12] + '⋯' + addr[-12:] class KeypathMenu(MenuSystem): def __init__(self, path=None, nl=0): @@ -252,7 +259,6 @@ Press (3) if you really understand and accept these risks. # - also for other {account} numbers # - or multisig case from glob import dis, NFC, VD - import version def make_msg(change=0): export_msg = "Press (1) to save Address summary file to SD Card." diff --git a/shared/manifest_mk4.py b/shared/manifest_mk4.py index 594fd4fc..995b8f53 100644 --- a/shared/manifest_mk4.py +++ b/shared/manifest_mk4.py @@ -8,6 +8,7 @@ freeze_as_mpy('', [ 'nfc.py', 'ndef.py', 'trick_pins.py', + 'ux_mk4.py', ], opt=0) # Optimize data-like files, since no need to debug them. diff --git a/shared/manifest_q1.py b/shared/manifest_q1.py index 08b445dc..fbeb8305 100644 --- a/shared/manifest_q1.py +++ b/shared/manifest_q1.py @@ -11,6 +11,7 @@ freeze_as_mpy('', [ 'nfc.py', 'ndef.py', 'trick_pins.py', + 'ux_q1.py', ], opt=0) # Optimize data-like files, since no need to debug them. diff --git a/shared/ux.py b/shared/ux.py index cce1500f..c0ef5441 100644 --- a/shared/ux.py +++ b/shared/ux.py @@ -11,15 +11,19 @@ from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours +# see ux_mk or ux_q1 for these... + # How many characters can we fit on each line? How many lines? if version.has_qwerty: from lcd_display import CHARS_W, CHARS_H CH_PER_W = CHARS_W STORY_H = CHARS_H + from ux_q1 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text else: # (using FontSmall) CH_PER_W = 17 STORY_H = 5 + from ux_mk4 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text # This signals the need to switch from current # menu (or whatever) to show something new. The @@ -128,59 +132,6 @@ def ux_poll_key(): return ch -class PressRelease: - def __init__(self, need_release='xy'): - # Manage key-repeat: track last key, measure time it's held down, etc. - self.need_release = need_release - self.last_key = None - self.num_repeats = 0 - - async def wait(self): - from glob import numpad - - armed = None - - while 1: - # two values here: - # - (ms) time to wait before first key-repeat - # - (ms) time between 2nd and Nth repeated events - # - these values approved by @nvk - rep_delay = 200 if not self.num_repeats else 20 - so_far = 0 - - while numpad.empty(): - if self.last_key and numpad.key_pressed == self.last_key: - if so_far >= rep_delay: - self.num_repeats += 1 - return self.last_key - - await sleep_ms(1) - so_far += 1 - - ch = numpad.get_nowait() - - if ch == numpad.ABORT_KEY: - raise AbortInteraction() - - self.num_repeats = 0 - - if len(ch) > 1: - # multipress: cancel press/release cycle and be a keyup - # for other keys. - armed = None - continue - - if ch == '': - self.last_key = None - if armed: - return armed - elif ch in self.need_release: - # no key-repeat on these ones - armed = ch - else: - self.last_key = ch - return ch - async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_escape=False): # show a big long string, and wait for XY to continue # - returns character used to get out (X or Y) @@ -361,283 +312,5 @@ async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False): max_value = 9999 return await ux_enter_number(prompt=prompt, max_value=max_value, can_cancel=can_cancel) -async def ux_enter_number(prompt, max_value, can_cancel=False): - # return the decimal number which the user has entered - # - default/blank value assumed to be zero - # - clamps large values to the max - if has_qwerty: - return ux_enter_number_qwerty(prompt, max_value, can_cancel=can_cancel) - - from glob import dis - from display import FontTiny - from math import log - - # allow key repeat on X only - press = PressRelease('1234567890y') - - y = 26 - value = '' - max_w = int(log(max_value, 10) + 1) - - dis.clear() - dis.text(0, 0, prompt) - dis.text(None, -1, ("X to CANCEL, or OK when DONE." if can_cancel else - "X to DELETE, or OK when DONE."), FontTiny) - dis.save() - - while 1: - dis.restore() - - # text centered - if value: - bx = dis.text(None, y, value) - dis.icon(bx+1, y+11, 'space') - else: - dis.icon(64-7, y+11, 'space') - - dis.show() - - ch = await press.wait() - if ch == 'y' or ch == KEY_SELECT: - - if not value: return 0 - return min(max_value, int(value)) - - elif ch == 'x' or ch == KEY_CANCEL: - if value: - value = value[0:-1] - elif can_cancel: - # quit if they press X on empty screen - return None - else: - if len(value) == max_w: - value = value[0:-1] + ch - else: - value += ch - - # cleanup leading zeros and such - value = str(min(int(value), max_value)) - -async def ux_input_numbers(val, validate_func): - # collect a series of digits - from glob import dis - from display import FontTiny - - # allow key repeat on X only - press = PressRelease('1234567890y') - - footer = "X to DELETE, or OK when DONE." - lx = 6 - y = 16 - here = '' - - dis.clear() - dis.text(None, -1, footer, FontTiny) - dis.save() - - while 1: - dis.restore() - - # text centered - msg = here - by = y - bx = dis.text(lx, y, msg[0:16]) - dis.text(lx, y - 9, str(val, 'ascii').replace(' ', '_'), FontTiny) - - if len(msg) > 16: - # second line when needed (left just) - by += 15 - bx = dis.text(lx, by, msg[16:]) - - if len(here) < 32: - dis.icon(bx, by - 2, 'sm_box') - - dis.show() - - ch = await press.wait() - if ch == 'y': - val += here - validate_func() - return val - elif ch == 'x': - if here: - here = here[0:-1] - else: - # quit if they press X on empty screen - return - else: - if len(here) < 32: - here += ch - -async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100): - # Allow them to pick each digit using "D-pad" - from glob import dis - from display import FontTiny, FontSmall - - # Should allow full unicode, NKDN - # - but limited to what we can show in FontSmall - # - so really just ascii; not even latin-1 - # - 8-bit codepoints only - my_rng = range(32, 127) # FontSmall.code_range - if hex_only: - new_expand = "0" - symbols = b"0123456789abcdef" - else: - new_expand = " " - symbols = b' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' - letters = b'abcdefghijklmnopqrstuvwxyz' - Letters = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - numbers = b'1234567890' - # assert len(set(symbols+letters+Letters+numbers)) == len(my_rng) - - if hex_only: - footer1 = "Enter Hexidecimal Number" - footer2 = "58=Change 9=Next 7=Back" - else: - footer1 = "1=Letters 2=Numbers 3=Symbols" - footer2 = "4=SwapCase 0=HELP" - - y = 20 - pw = bytearray(pw or ('0' if hex_only else 'A')) - - pos = len(pw) - 1 # which part being changed - n_visible = const(9) - scroll_x = max(pos - n_visible, 0) - - def cycle_set(which, direction=1): - # pick next item in set of choices - for n, s in enumerate(which): - if pw[pos] == s: - try: - pw[pos] = which[n + direction] - except IndexError: - pw[pos] = which[0 if direction == 1 else -1] - return - pw[pos] = which[0] - - def change(dx): - # next/prev within the same subset of related chars - ch = pw[pos] - if hex_only: - return cycle_set(symbols, dx) - for subset in [symbols, letters, Letters, numbers]: - if ch in subset: - return cycle_set(subset, dx) - - # probably unreachable code: numeric up/down - ch = pw[pos] + dx - if ch not in my_rng: - ch = (my_rng.stop - 1) if dx < 0 else my_rng.start - assert ch in my_rng - pw[pos] = ch - - # pre-render the fixed stuff - dis.clear() - dis.text(None, -10, footer1, FontTiny) - dis.text(None, -1, footer2, FontTiny) - dis.save() - - # no key-repeat on certain keys - press = PressRelease('4xy') - while 1: - dis.restore() - - lr = pos - scroll_x # left/right distance of cursor - if lr < 4 and scroll_x: - scroll_x -= 1 - elif lr < 0: - scroll_x = pos - elif lr >= (n_visible - 1): - # past right edge - scroll_x += 1 - - for i in range(n_visible): - # calc abs position in string - ax = scroll_x + i - x = 4 + (13 * i) - try: - ch = pw[ax] - except IndexError: - continue - - if ax == pos: - # draw cursor - if not hex_only and (len(pw) < 2 * n_visible): - dis.text(x - 4, y - 19, '0x%02X' % ch, FontTiny) - dis.icon(x - 2, y - 10, 'spin') - - if ch == 0x20: - dis.icon(x, y + 11, 'space') - else: - dis.text(x, y, chr(ch) if ch in my_rng else chr(215), FontSmall) - - if scroll_x > 0: - dis.text(2, y - 14, str(pw, 'ascii')[0:scroll_x].replace(' ', '_'), FontTiny) - if scroll_x + n_visible < len(pw): - dis.text(-1, 1, "MORE>", FontTiny) - - dis.show() - - ch = await press.wait() - if ch == 'y': - return str(pw, 'ascii') - elif ch == 'x': - if len(pw) > 1: - # delete current char - pw = pw[0:pos] + pw[pos + 1:] - if pos >= len(pw): - pos = len(pw) - 1 - else: - if confirm_exit: - pp = await ux_show_story( - "OK to leave without any changes? Or X to cancel leaving.") - if pp == 'x': continue - return None - - elif ch == '7': # left - pos -= 1 - if pos < 0: pos = 0 - elif ch == '9': # right - pos += 1 - if pos >= len(pw): - if len(pw) < max_len and pw[-3:] != b' ': - # expands with space in normal mode - # expands with 0 in hex_only mode - pw += new_expand - else: - pos -= 1 # abort addition - - elif ch == '5': # up - change(1) - elif ch == '8': # down - change(-1) - elif hex_only: - # just got back at the beginning of the loop - # below branches are unreachable for hex_only mode - pass - elif ch == '1': # alpha - cycle_set(b'Aa') - elif ch == '4': # toggle case - if (pw[pos] & ~0x20) in range(65, 91): - pw[pos] ^= 0x20 - elif ch == '2': # numbers - cycle_set(numbers) - elif ch == '3': # symbols (all of them) - cycle_set(symbols) - elif ch == '0': # help - help_msg = '''\ -Use arrow keys (5789) to select letter and move around. - -1=Letters (Aa..) -2=Numbers (12..) -3=Symbols (!@#&*) -4=Swap Case (q/Q) -X=Delete char - -Add more characters by moving past end (right side).''' - - if confirm_exit: - help_msg += '\nTo quit without changes, delete everything.' - await ux_show_story(help_msg) # EOF diff --git a/shared/ux_mk4.py b/shared/ux_mk4.py new file mode 100644 index 00000000..13c898a9 --- /dev/null +++ b/shared/ux_mk4.py @@ -0,0 +1,337 @@ +# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# ux_mk4.py - UX/UI interactions that are Mk1-4 specific +# +from uasyncio import sleep_ms +import utime, gc +from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, + KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_SELECT, KEY_CANCEL) + +class PressRelease: + def __init__(self, need_release='xy'): + # Manage key-repeat: track last key, measure time it's held down, etc. + self.need_release = need_release + self.last_key = None + self.num_repeats = 0 + + async def wait(self): + from glob import numpad + + armed = None + + while 1: + # two values here: + # - (ms) time to wait before first key-repeat + # - (ms) time between 2nd and Nth repeated events + # - these values approved by @nvk + rep_delay = 200 if not self.num_repeats else 20 + so_far = 0 + + while numpad.empty(): + if self.last_key and numpad.key_pressed == self.last_key: + if so_far >= rep_delay: + self.num_repeats += 1 + return self.last_key + + await sleep_ms(1) + so_far += 1 + + ch = numpad.get_nowait() + + if ch == numpad.ABORT_KEY: + raise AbortInteraction() + + self.num_repeats = 0 + + if len(ch) > 1: + # multipress: cancel press/release cycle and be a keyup + # for other keys. + armed = None + continue + + if ch == '': + self.last_key = None + if armed: + return armed + elif ch in self.need_release: + # no key-repeat on these ones + armed = ch + else: + self.last_key = ch + return ch + +async def ux_enter_number(prompt, max_value, can_cancel=False): + # return the decimal number which the user has entered + # - default/blank value assumed to be zero + # - clamps large values to the max + from glob import dis + from display import FontTiny + from math import log + + # allow key repeat on X only + press = PressRelease('1234567890y') + + y = 26 + value = '' + max_w = int(log(max_value, 10) + 1) + + dis.clear() + dis.text(0, 0, prompt) + dis.text(None, -1, ("X to CANCEL, or OK when DONE." if can_cancel else + "X to DELETE, or OK when DONE."), FontTiny) + dis.save() + + while 1: + dis.restore() + + # text centered + if value: + bx = dis.text(None, y, value) + dis.icon(bx+1, y+11, 'space') + else: + dis.icon(64-7, y+11, 'space') + + dis.show() + + ch = await press.wait() + if ch == 'y' or ch == KEY_SELECT: + + if not value: return 0 + return min(max_value, int(value)) + + elif ch == 'x' or ch == KEY_CANCEL: + if value: + value = value[0:-1] + elif can_cancel: + # quit if they press X on empty screen + return None + else: + if len(value) == max_w: + value = value[0:-1] + ch + else: + value += ch + + # cleanup leading zeros and such + value = str(min(int(value), max_value)) + +async def ux_input_numbers(val, validate_func): + # collect a series of digits + from glob import dis + from display import FontTiny + + # allow key repeat on X only + press = PressRelease('1234567890y') + + footer = "X to DELETE, or OK when DONE." + lx = 6 + y = 16 + here = '' + + dis.clear() + dis.text(None, -1, footer, FontTiny) + dis.save() + + while 1: + dis.restore() + + # text centered + msg = here + by = y + bx = dis.text(lx, y, msg[0:16]) + dis.text(lx, y - 9, str(val, 'ascii').replace(' ', '_'), FontTiny) + + if len(msg) > 16: + # second line when needed (left just) + by += 15 + bx = dis.text(lx, by, msg[16:]) + + if len(here) < 32: + dis.icon(bx, by - 2, 'sm_box') + + dis.show() + + ch = await press.wait() + if ch == 'y': + val += here + validate_func() + return val + elif ch == 'x': + if here: + here = here[0:-1] + else: + # quit if they press X on empty screen + return + else: + if len(here) < 32: + here += ch + +async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100): + # Allow them to pick each digit using "D-pad" + from glob import dis + from display import FontTiny, FontSmall + + # Should allow full unicode, NKDN + # - but limited to what we can show in FontSmall + # - so really just ascii; not even latin-1 + # - 8-bit codepoints only + my_rng = range(32, 127) # FontSmall.code_range + if hex_only: + new_expand = "0" + symbols = b"0123456789abcdef" + else: + new_expand = " " + symbols = b' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' + letters = b'abcdefghijklmnopqrstuvwxyz' + Letters = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + numbers = b'1234567890' + # assert len(set(symbols+letters+Letters+numbers)) == len(my_rng) + + if hex_only: + footer1 = "Enter Hexidecimal Number" + footer2 = "58=Change 9=Next 7=Back" + else: + footer1 = "1=Letters 2=Numbers 3=Symbols" + footer2 = "4=SwapCase 0=HELP" + + y = 20 + pw = bytearray(pw or ('0' if hex_only else 'A')) + + pos = len(pw) - 1 # which part being changed + n_visible = const(9) + scroll_x = max(pos - n_visible, 0) + + def cycle_set(which, direction=1): + # pick next item in set of choices + for n, s in enumerate(which): + if pw[pos] == s: + try: + pw[pos] = which[n + direction] + except IndexError: + pw[pos] = which[0 if direction == 1 else -1] + return + pw[pos] = which[0] + + def change(dx): + # next/prev within the same subset of related chars + ch = pw[pos] + if hex_only: + return cycle_set(symbols, dx) + for subset in [symbols, letters, Letters, numbers]: + if ch in subset: + return cycle_set(subset, dx) + + # probably unreachable code: numeric up/down + ch = pw[pos] + dx + if ch not in my_rng: + ch = (my_rng.stop - 1) if dx < 0 else my_rng.start + assert ch in my_rng + pw[pos] = ch + + # pre-render the fixed stuff + dis.clear() + dis.text(None, -10, footer1, FontTiny) + dis.text(None, -1, footer2, FontTiny) + dis.save() + + # no key-repeat on certain keys + press = PressRelease('4xy') + while 1: + dis.restore() + + lr = pos - scroll_x # left/right distance of cursor + if lr < 4 and scroll_x: + scroll_x -= 1 + elif lr < 0: + scroll_x = pos + elif lr >= (n_visible - 1): + # past right edge + scroll_x += 1 + + for i in range(n_visible): + # calc abs position in string + ax = scroll_x + i + x = 4 + (13 * i) + try: + ch = pw[ax] + except IndexError: + continue + + if ax == pos: + # draw cursor + if not hex_only and (len(pw) < 2 * n_visible): + dis.text(x - 4, y - 19, '0x%02X' % ch, FontTiny) + dis.icon(x - 2, y - 10, 'spin') + + if ch == 0x20: + dis.icon(x, y + 11, 'space') + else: + dis.text(x, y, chr(ch) if ch in my_rng else chr(215), FontSmall) + + if scroll_x > 0: + dis.text(2, y - 14, str(pw, 'ascii')[0:scroll_x].replace(' ', '_'), FontTiny) + if scroll_x + n_visible < len(pw): + dis.text(-1, 1, "MORE>", FontTiny) + + dis.show() + + ch = await press.wait() + if ch == 'y': + return str(pw, 'ascii') + elif ch == 'x': + if len(pw) > 1: + # delete current char + pw = pw[0:pos] + pw[pos + 1:] + if pos >= len(pw): + pos = len(pw) - 1 + else: + if confirm_exit: + pp = await ux_show_story( + "OK to leave without any changes? Or X to cancel leaving.") + if pp == 'x': continue + return None + + elif ch == '7': # left + pos -= 1 + if pos < 0: pos = 0 + elif ch == '9': # right + pos += 1 + if pos >= len(pw): + if len(pw) < max_len and pw[-3:] != b' ': + # expands with space in normal mode + # expands with 0 in hex_only mode + pw += new_expand + else: + pos -= 1 # abort addition + + elif ch == '5': # up + change(1) + elif ch == '8': # down + change(-1) + elif hex_only: + # just got back at the beginning of the loop + # below branches are unreachable for hex_only mode + pass + elif ch == '1': # alpha + cycle_set(b'Aa') + elif ch == '4': # toggle case + if (pw[pos] & ~0x20) in range(65, 91): + pw[pos] ^= 0x20 + elif ch == '2': # numbers + cycle_set(numbers) + elif ch == '3': # symbols (all of them) + cycle_set(symbols) + elif ch == '0': # help + help_msg = '''\ +Use arrow keys (5789) to select letter and move around. + +1=Letters (Aa..) +2=Numbers (12..) +3=Symbols (!@#&*) +4=Swap Case (q/Q) +X=Delete char + +Add more characters by moving past end (right side).''' + + if confirm_exit: + help_msg += '\nTo quit without changes, delete everything.' + await ux_show_story(help_msg) diff --git a/shared/ux_q1.py b/shared/ux_q1.py new file mode 100644 index 00000000..b46b110e --- /dev/null +++ b/shared/ux_q1.py @@ -0,0 +1,336 @@ +# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# ux_q1.py - UX/UI interactions that are Q1 specific and use big screen, keyboard. +# +from uasyncio import sleep_ms +import utime, gc +from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, + KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_SELECT, KEY_CANCEL) + +class PressRelease: + def __init__(self, need_release='xy'): + # Manage key-repeat: track last key, measure time it's held down, etc. + self.need_release = need_release + self.last_key = None + self.num_repeats = 0 + + async def wait(self): + from glob import numpad + + armed = None + + while 1: + # two values here: + # - (ms) time to wait before first key-repeat + # - (ms) time between 2nd and Nth repeated events + # - these values approved by @nvk + rep_delay = 200 if not self.num_repeats else 20 + so_far = 0 + + while numpad.empty(): + if self.last_key and numpad.key_pressed == self.last_key: + if so_far >= rep_delay: + self.num_repeats += 1 + return self.last_key + + await sleep_ms(1) + so_far += 1 + + ch = numpad.get_nowait() + + if ch == numpad.ABORT_KEY: + raise AbortInteraction() + + self.num_repeats = 0 + + if len(ch) > 1: + # multipress: cancel press/release cycle and be a keyup + # for other keys. + armed = None + continue + + if ch == '': + self.last_key = None + if armed: + return armed + elif ch in self.need_release: + # no key-repeat on these ones + armed = ch + else: + self.last_key = ch + return ch + +async def ux_enter_number(prompt, max_value, can_cancel=False): + # return the decimal number which the user has entered + # - default/blank value assumed to be zero + # - clamps large values to the max + from glob import dis + from math import log + + # allow key repeat on X only + press = PressRelease('1234567890y') + + y = 26 + value = '' + max_w = int(log(max_value, 10) + 1) + + dis.clear() + dis.text(0, 0, prompt) + dis.text(None, -1, ("X to CANCEL, or OK when DONE." if can_cancel else + "X to DELETE, or OK when DONE."), FontTiny) + dis.save() + + while 1: + dis.restore() + + # text centered + if value: + bx = dis.text(None, y, value) + dis.icon(bx+1, y+11, 'space') + else: + dis.icon(64-7, y+11, 'space') + + dis.show() + + ch = await press.wait() + if ch == 'y' or ch == KEY_SELECT: + + if not value: return 0 + return min(max_value, int(value)) + + elif ch == 'x' or ch == KEY_CANCEL: + if value: + value = value[0:-1] + elif can_cancel: + # quit if they press X on empty screen + return None + else: + if len(value) == max_w: + value = value[0:-1] + ch + else: + value += ch + + # cleanup leading zeros and such + value = str(min(int(value), max_value)) + +async def ux_input_numbers(val, validate_func): + # collect a series of digits + from glob import dis + from display import FontTiny + + # allow key repeat on X only + press = PressRelease('1234567890y') + + footer = "X to DELETE, or OK when DONE." + lx = 6 + y = 16 + here = '' + + dis.clear() + dis.text(None, -1, footer, FontTiny) + dis.save() + + while 1: + dis.restore() + + # text centered + msg = here + by = y + bx = dis.text(lx, y, msg[0:16]) + dis.text(lx, y - 9, str(val, 'ascii').replace(' ', '_'), FontTiny) + + if len(msg) > 16: + # second line when needed (left just) + by += 15 + bx = dis.text(lx, by, msg[16:]) + + if len(here) < 32: + dis.icon(bx, by - 2, 'sm_box') + + dis.show() + + ch = await press.wait() + if ch == 'y': + val += here + validate_func() + return val + elif ch == 'x': + if here: + here = here[0:-1] + else: + # quit if they press X on empty screen + return + else: + if len(here) < 32: + here += ch + +async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100): + # Allow them to pick each digit using "D-pad" + from glob import dis + from display import FontTiny, FontSmall + + # Should allow full unicode, NKDN + # - but limited to what we can show in FontSmall + # - so really just ascii; not even latin-1 + # - 8-bit codepoints only + my_rng = range(32, 127) # FontSmall.code_range + if hex_only: + new_expand = "0" + symbols = b"0123456789abcdef" + else: + new_expand = " " + symbols = b' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' + letters = b'abcdefghijklmnopqrstuvwxyz' + Letters = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + numbers = b'1234567890' + # assert len(set(symbols+letters+Letters+numbers)) == len(my_rng) + + if hex_only: + footer1 = "Enter Hexidecimal Number" + footer2 = "58=Change 9=Next 7=Back" + else: + footer1 = "1=Letters 2=Numbers 3=Symbols" + footer2 = "4=SwapCase 0=HELP" + + y = 20 + pw = bytearray(pw or ('0' if hex_only else 'A')) + + pos = len(pw) - 1 # which part being changed + n_visible = const(9) + scroll_x = max(pos - n_visible, 0) + + def cycle_set(which, direction=1): + # pick next item in set of choices + for n, s in enumerate(which): + if pw[pos] == s: + try: + pw[pos] = which[n + direction] + except IndexError: + pw[pos] = which[0 if direction == 1 else -1] + return + pw[pos] = which[0] + + def change(dx): + # next/prev within the same subset of related chars + ch = pw[pos] + if hex_only: + return cycle_set(symbols, dx) + for subset in [symbols, letters, Letters, numbers]: + if ch in subset: + return cycle_set(subset, dx) + + # probably unreachable code: numeric up/down + ch = pw[pos] + dx + if ch not in my_rng: + ch = (my_rng.stop - 1) if dx < 0 else my_rng.start + assert ch in my_rng + pw[pos] = ch + + # pre-render the fixed stuff + dis.clear() + dis.text(None, -10, footer1, FontTiny) + dis.text(None, -1, footer2, FontTiny) + dis.save() + + # no key-repeat on certain keys + press = PressRelease('4xy') + while 1: + dis.restore() + + lr = pos - scroll_x # left/right distance of cursor + if lr < 4 and scroll_x: + scroll_x -= 1 + elif lr < 0: + scroll_x = pos + elif lr >= (n_visible - 1): + # past right edge + scroll_x += 1 + + for i in range(n_visible): + # calc abs position in string + ax = scroll_x + i + x = 4 + (13 * i) + try: + ch = pw[ax] + except IndexError: + continue + + if ax == pos: + # draw cursor + if not hex_only and (len(pw) < 2 * n_visible): + dis.text(x - 4, y - 19, '0x%02X' % ch, FontTiny) + dis.icon(x - 2, y - 10, 'spin') + + if ch == 0x20: + dis.icon(x, y + 11, 'space') + else: + dis.text(x, y, chr(ch) if ch in my_rng else chr(215), FontSmall) + + if scroll_x > 0: + dis.text(2, y - 14, str(pw, 'ascii')[0:scroll_x].replace(' ', '_'), FontTiny) + if scroll_x + n_visible < len(pw): + dis.text(-1, 1, "MORE>", FontTiny) + + dis.show() + + ch = await press.wait() + if ch == 'y': + return str(pw, 'ascii') + elif ch == 'x': + if len(pw) > 1: + # delete current char + pw = pw[0:pos] + pw[pos + 1:] + if pos >= len(pw): + pos = len(pw) - 1 + else: + if confirm_exit: + pp = await ux_show_story( + "OK to leave without any changes? Or X to cancel leaving.") + if pp == 'x': continue + return None + + elif ch == '7': # left + pos -= 1 + if pos < 0: pos = 0 + elif ch == '9': # right + pos += 1 + if pos >= len(pw): + if len(pw) < max_len and pw[-3:] != b' ': + # expands with space in normal mode + # expands with 0 in hex_only mode + pw += new_expand + else: + pos -= 1 # abort addition + + elif ch == '5': # up + change(1) + elif ch == '8': # down + change(-1) + elif hex_only: + # just got back at the beginning of the loop + # below branches are unreachable for hex_only mode + pass + elif ch == '1': # alpha + cycle_set(b'Aa') + elif ch == '4': # toggle case + if (pw[pos] & ~0x20) in range(65, 91): + pw[pos] ^= 0x20 + elif ch == '2': # numbers + cycle_set(numbers) + elif ch == '3': # symbols (all of them) + cycle_set(symbols) + elif ch == '0': # help + help_msg = '''\ +Use arrow keys (5789) to select letter and move around. + +1=Letters (Aa..) +2=Numbers (12..) +3=Symbols (!@#&*) +4=Swap Case (q/Q) +X=Delete char + +Add more characters by moving past end (right side).''' + + if confirm_exit: + help_msg += '\nTo quit without changes, delete everything.' + await ux_show_story(help_msg)