diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 844135cc..d27640b9 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -180,6 +180,9 @@ class Fonts(Singleton): if font_name not in cls.fonts: cls.fonts[font_name] = {} + if font_name in [GUIConstants.ICON_FONT_NAME__FONT_AWESOME, GUIConstants.ICON_FONT_NAME__SEEDSIGNER]: + file_extension = "otf" + if size not in cls.fonts[font_name]: try: cls.fonts[font_name][size] = ImageFont.truetype(os.path.join(cls.font_path, f"{font_name}.{file_extension}"), size) diff --git a/src/seedsigner/gui/keyboard.py b/src/seedsigner/gui/keyboard.py index 8da4e006..09169903 100644 --- a/src/seedsigner/gui/keyboard.py +++ b/src/seedsigner/gui/keyboard.py @@ -162,6 +162,8 @@ class Keyboard: def __init__(self, draw: ImageDraw, charset="1234567890abcdefghijklmnopqrstuvwxyz", + font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, + font_size=24, selected_char="a", rows=4, cols=10, @@ -179,7 +181,7 @@ class Keyboard: self.rows = rows self.cols = cols self.rect = rect - self.font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, 24) + self.font = Fonts.get_font(font_name, font_size) self.auto_wrap = auto_wrap self.background_color = GUIConstants.BUTTON_BACKGROUND_COLOR diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 27cef4fe..2e8b2276 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -1,11 +1,10 @@ -import time - from dataclasses import dataclass from PIL.Image import Image +from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.camera import Camera -from seedsigner.gui.components import Fonts, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea +from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea -from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, ButtonListScreen +from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen from seedsigner.hardware.buttons import HardwareButtonsConstants @@ -82,6 +81,7 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): return preview_images + @dataclass class ToolsImageEntropyFinalImageScreen(BaseScreen): final_image: Image = None @@ -110,6 +110,157 @@ class ToolsImageEntropyFinalImageScreen(BaseScreen): +@dataclass +class ToolsDiceEntropyEntryScreen(BaseTopNavScreen): + total_rolls: int = None + + + def __post_init__(self): + self.title = f"Dice Roll 1/{self.total_rolls}" + super().__post_init__() + + self.dice_rolls = "" + + # Set up the keyboard params + keyboard_width = self.canvas_width - 2*GUIConstants.EDGE_PADDING + text_entry_display_y = self.top_nav.height + text_entry_display_height = 30 + + keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING + rows = 3 + button_height = int((self.canvas_height - GUIConstants.EDGE_PADDING - text_entry_display_y - text_entry_display_height - GUIConstants.COMPONENT_PADDING - (rows - 1) * 2) / rows) + self.keyboard_digits = Keyboard( + draw=self.renderer.draw, + charset="".join([ + FontAwesomeIconConstants.DICE_ONE, + FontAwesomeIconConstants.DICE_TWO, + FontAwesomeIconConstants.DICE_THREE, + FontAwesomeIconConstants.DICE_FOUR, + FontAwesomeIconConstants.DICE_FIVE, + FontAwesomeIconConstants.DICE_SIX, + ]), + font_name=GUIConstants.ICON_FONT_NAME__FONT_AWESOME, + font_size=button_height - GUIConstants.COMPONENT_PADDING, + rows=rows, + cols=3, + rect=( + GUIConstants.EDGE_PADDING, + keyboard_start_y, + GUIConstants.EDGE_PADDING + keyboard_width, + keyboard_start_y + rows * button_height + (rows - 1) * 2 + ), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], + render_now=False + ) + self.keyboard_digits.set_selected_key(selected_letter=FontAwesomeIconConstants.DICE_ONE) + + self.text_entry_display = TextEntryDisplay( + canvas=self.renderer.canvas, + rect=( + GUIConstants.EDGE_PADDING, + text_entry_display_y, + self.canvas_width - GUIConstants.EDGE_PADDING, + text_entry_display_y + text_entry_display_height + ), + cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR, + is_centered=False, + ) + + + def _render(self): + super()._render() + + self.keyboard_digits.render_keys() + self.text_entry_display.render() + + self.renderer.show_image() + + + def _run(self): + cursor_position = len(self.dice_rolls) + + # Start the interactive update loop + while True: + input = self.hw_inputs.wait_for( + HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3], + check_release=True, + release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3] + ) + + # Check possible exit condition + if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS: + return RET_CODE__BACK_BUTTON + + # Process normal input + if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected: + # We're navigating off the previous button + self.top_nav.is_selected = False + self.top_nav.render_buttons() + + # Override the actual input w/an ENTER signal for the Keyboard + if input == HardwareButtonsConstants.KEY_DOWN: + input = Keyboard.ENTER_TOP + else: + input = Keyboard.ENTER_BOTTOM + elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected: + # ignore + continue + + ret_val = self.keyboard_digits.update_from_input(input) + + # Now process the result from the keyboard + if ret_val in Keyboard.EXIT_DIRECTIONS: + self.top_nav.is_selected = True + self.top_nav.render_buttons() + + elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS: + if ret_val == Keyboard.KEY_BACKSPACE["code"]: + if len(self.dice_rolls) > 0: + self.dice_rolls = self.dice_rolls[:-1] + cursor_position -= 1 + + elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS: + # User has locked in the current letter + if ret_val == FontAwesomeIconConstants.DICE_ONE: + ret_val = "1" + elif ret_val == FontAwesomeIconConstants.DICE_TWO: + ret_val = "2" + elif ret_val == FontAwesomeIconConstants.DICE_THREE: + ret_val = "3" + elif ret_val == FontAwesomeIconConstants.DICE_FOUR: + ret_val = "4" + elif ret_val == FontAwesomeIconConstants.DICE_FIVE: + ret_val = "5" + elif ret_val == FontAwesomeIconConstants.DICE_SIX: + ret_val = "6" + self.dice_rolls += ret_val + cursor_position += 1 + + if cursor_position == self.total_rolls: + return self.dice_rolls + + # Render a new TextArea over the TopNav title bar + TextArea( + text=f"Dice Roll {cursor_position + 1}/{self.total_rolls}", + font_name=GUIConstants.TOP_NAV_TITLE_FONT_NAME, + font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE, + height=self.top_nav.height, + ).render() + self.top_nav.render_buttons() + + elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN: + # Live joystick movement; haven't locked this new letter in yet. + # Leave current spot blank for now. Only update the active keyboard keys + # when a selection has been locked in (KEY_PRESS) or removed ("del"). + pass + + # Render the text entry display and cursor block + self.text_entry_display.render(self.dice_rolls) + + self.renderer.show_image() + + + @dataclass class ToolsCalcFinalWordShowFinalWordScreen(ButtonListScreen): final_word: str = None diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index a5482c39..e8fb44aa 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -43,6 +43,10 @@ def generate_mnemonic_from_bytes(entropy_bytes) -> List[str]: def generate_mnemonic_from_dice(roll_data: str) -> List[str]: entropy_bytes = hashlib.sha256(roll_data.encode()).digest() + if len(roll_data) == 50: + # 12-word mnemonic; only use 128bits / 16 bytes + entropy_bytes = entropy_bytes[:16] + # Return as a list return bip39.mnemonic_from_bytes(entropy_bytes).split() diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index b2fa3bf8..bcc8c2f8 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -9,7 +9,7 @@ from seedsigner.gui.screens.screen import LargeButtonScreen from seedsigner.hardware.camera import Camera from seedsigner.gui.components import FontAwesomeIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) -from seedsigner.gui.screens.tools_screens import ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordShowFinalWordScreen +from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordShowFinalWordScreen from seedsigner.helpers import mnemonic_generation from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants @@ -39,7 +39,7 @@ class ToolsMenuView(View): return Destination(ToolsImageEntropyLivePreviewView) elif button_data[selected_menu_num] == DICE: - return Destination(NotYetImplementedView) + return Destination(ToolsDiceEntropyMnemonicLengthView) elif button_data[selected_menu_num] == KEYBOARD: return Destination(ToolsCalcFinalWordNumWordsView) @@ -166,6 +166,60 @@ class ToolsImageEntropyMnemonicLengthView(View): +"""**************************************************************************** + Dice rolls Views +****************************************************************************""" +class ToolsDiceEntropyMnemonicLengthView(View): + def run(self): + TWELVE = "12 words (50 rolls)" + TWENTY_FOUR = "24 words (99 rolls)" + + button_data = [TWELVE, TWENTY_FOUR] + selected_menu_num = ButtonListScreen( + title="Mnemonic Length", + is_bottom_list=True, + is_button_text_centered=True, + button_data=button_data, + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_num] == TWELVE: + return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=50)) + + elif button_data[selected_menu_num] == TWENTY_FOUR: + return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=99)) + + + +class ToolsDiceEntropyEntryView(View): + def __init__(self, total_rolls: int): + super().__init__() + self.total_rolls = total_rolls + + + def run(self): + ret = ToolsDiceEntropyEntryScreen( + total_rolls=self.total_rolls, + ).display() + + if ret == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + print(ret) + dice_seed_phrase = mnemonic_generation.generate_mnemonic_from_dice(ret) + print(dice_seed_phrase) + + # Add the mnemonic as an in-memory Seed + seed = Seed(dice_seed_phrase, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + self.controller.storage.set_pending_seed(seed) + + # Cannot return BACK to this View + return Destination(SeedWordsWarningView, view_args={"seed_num": None}, clear_history=True) + + + """**************************************************************************** Calc final word Views ****************************************************************************"""