feat(mockui): shared on-screen keyboard via KeyboardManager#21
feat(mockui): shared on-screen keyboard via KeyboardManager#21maggo83 merged 7 commits intok9ert:mainfrom
Conversation
…nt framework - Remove generic .bmad/bmm, .bmad/bmb, .bmad/core upstream scaffolding - Add custom agent team: orchestrator, pm, architect, ux-designer, developer, tester, i18n-specialist, micropython-specialist, lvgl-mockui-specialist, hw-bootloader-specialist, security, scrum-master, doc-writer - Add workflows: feature-development (9 stages incl. device validation), bug-fix, refactor, release - Add adapters: copilot-instructions, cursor-rules, continue-dev - Add team-config.md and BMAD.md with full project context - Propagate nix-develop prefix to all make targets across all agent files - Document correct manifest system (freeze tree, no per-file listing) - Document i18n pipeline direction: specter_ui_en.json → sync-i18n → translation_keys.py (auto-generated); keys are UPPERCASE_UNDERSCORES - Document boot chain: main.py shared, boot.py hardware-only, platform detection via sys.platform, /flash mount differences - Add auto-generated files rule: path-independent .gitignore patterns - Add device validation stage to feature-development workflow with test-first triage loop (fix test before blaming implementation) - Update .gitignore: lang_*.bin, language_config.json, translation_keys.py - Update CLAUDE.md with corrected pipeline and key locations
Replaces three independent ad-hoc keyboard implementations (wallet_menu, passphrase_menu, generate_seedphrase_menu) with a single shared KeyboardManager owned by NavigationController. Architecture - KeyboardManager holds one reusable lv.keyboard instance (lazy-created). - Menus activate it by calling bind() from a CLICKED event lambda on their textarea; no permanent owner reference needed. - Exactly one binding is active at a time. A new bind() silently replaces the previous one (restoring the old textarea text, suppressing on_cancel). - Lifecycle cleanup is driven by lv.EVENT.DELETE on the textarea, so menus never need to manually unbind on navigation. - Layout constants live in Layout.ALNUM / Layout.FULL (enum-style class). - Menus own their own accepted-char restrictions and sanitizer callbacks. The passphrase menu trims leading/trailing spaces via sanitize=str.strip. Bug fixes - Fix use-after-free crash in _commit(): textarea reference captured before _unbind() clears self.textarea. - Fix set_textarea() never being called (keyboard was shown but not linked). - Fix remove_event_cb() being called with extra args (MicroPython ABI). - Fix lv.EVENT.DELETE handler calling set_text() on a dying object. - Fix re-entrancy when replacing a binding: on_cancel suppressed via call_cb=False to prevent navigation callbacks mid-bind(). - Remove stray 'from portpicker import bind' that caused ImportError on boot. Removed - keyboard_text_rules.py and its unit test (rules logic folded into menus). Tests - 19 new unit tests in test_keyboard_manager.py covering bind/unbind lifecycle, commit, cancel, sanitizer, DELETE path, rebind replacement, rebind on_cancel suppression, and no-crash guards. - New device test test_passphrase_keyboard_repeated_commits_no_reset: regression for the crash observed after multiple passphrase edit cycles. - Extended test_keyboard_device.py with open/commit/cancel flow coverage. - Keyboard helper utilities extracted to tests_device/conftest.py. All tests pass: 119 unit tests, 9 device tests (build+flash default).
Replaces three independent ad-hoc keyboard implementations (wallet_menu, passphrase_menu, generate_seedphrase_menu) with a single shared KeyboardManager owned by NavigationController. Architecture - KeyboardManager holds one reusable lv.keyboard instance (lazy-created). - Menus activate it by calling bind() from a CLICKED event lambda on their textarea; no permanent owner reference needed. - Exactly one binding is active at a time. A new bind() silently replaces the previous one (restoring the old textarea text, suppressing on_cancel). - Lifecycle cleanup is driven by lv.EVENT.DELETE on the textarea, so menus never need to manually unbind on navigation. - Layout constants live in Layout.ALNUM / Layout.FULL (enum-style class). - Menus own their own accepted-char restrictions and sanitizer callbacks. The passphrase menu trims leading/trailing spaces via sanitize=str.strip. Bug fixes - Fix use-after-free crash in _commit(): textarea reference captured before _unbind() clears self.textarea. - Fix set_textarea() never being called (keyboard was shown but not linked). - Fix remove_event_cb() being called with extra args (MicroPython ABI). - Fix lv.EVENT.DELETE handler calling set_text() on a dying object. - Fix re-entrancy when replacing a binding: on_cancel suppressed via call_cb=False to prevent navigation callbacks mid-bind(). - Remove stray 'from portpicker import bind' that caused ImportError on boot. Removed - keyboard_text_rules.py and its unit test (rules logic folded into menus). Tests - 19 new unit tests in test_keyboard_manager.py covering bind/unbind lifecycle, commit, cancel, sanitizer, DELETE path, rebind replacement, rebind on_cancel suppression, and no-crash guards. - New device test test_passphrase_keyboard_repeated_commits_no_reset: regression for the crash observed after multiple passphrase edit cycles. - Extended test_keyboard_device.py with open/commit/cancel flow coverage. - Keyboard helper utilities extracted to tests_device/conftest.py. All tests pass: 119 unit tests, 9 device tests (build+flash default).
al-munazzim
left a comment
There was a problem hiding this comment.
Review: Clean, well-structured refactor 👍
+725 / -219 across 8 files, but net complexity drops significantly — three independent keyboard implementations replaced by one shared KeyboardManager. Test coverage is solid (19 unit + 3 device tests).
Strengths:
- Single ownership model with
bind()/_unbind()— one keyboard instance, one active binding, automatic cleanup vialv.EVENT.DELETE - Silent rebind with
call_cb=Falseavoids spurious cancel callbacks during screen transitions - Sanitizer as a callback (
sanitize=str.strip) keeps the manager generic - Commit/cancel ordering (unbind first, then callbacks) prevents reentrancy bugs
- Device regression test (
repeated_commits_no_reset) catches real crash scenarios
Nits / questions:
-
_unbinddoubleremove_event_cb(lines 80-81) — called twice. Intentional for DEFOCUSED + DELETE? Deserves a comment, looks like copy-paste at first glance. -
_original_nameingenerate_seedphrase_menu(line 37) — set but never read. Leftover? -
set_accepted_charsremoved fromwallet_menu— the old code restricted wallet name characters, now deleted. Passphrase menu still has it. Intentional? Without it, wallet names accept anything including newlines. -
set_wallet_bar_visibleremoved — oldwallet_menuhid the wallet bar while keyboard was open. NewKeyboardManagerdoesn't. Does the keyboard overlap the wallet bar now, or does LVGL layering handle it? -
Layout map duplication —
_build_full_layoutand_build_alnum_layoutshare identicalmap_lower/map_upperrows; onlymap_specialdiffers. Minor, but could share the text rows. -
on_cancelcalled on DELETE — if the textarea is being destroyed during navigation, callingon_cancelcould trigger logic on a dying screen. Safe today (passphrase cancel is unset), but worth documenting the contract.
Verdict: Merge-ready after addressing the _original_name leftover and set_accepted_chars question. Double remove_event_cb deserves a comment at minimum.
…o83/specter-playground into feat/mockui-shared-keyboard
- _unbind: switch from remove_event_cb() to remove_event_dsc() using stored descriptors (defocus_cb / delete_cb); verified on device via REPL — add_event_cb returns lv_event_dsc_t, remove_event_dsc returns True - wallet_menu: restore set_accepted_chars() to prevent newlines and other control characters in wallet names (passphrase menu already had it) - keyboard_manager docstring: clarify on_cancel / text-restore contract — neither is triggered during lv.EVENT.DELETE because the textarea object is being destroyed by LVGL at that point (calling set_text or navigation callbacks on a dying object would be unsafe) - test_keyboard_manager: fix MockTextarea to use descriptor-based remove_event_dsc() matching the real LVGL API; update test_cancel_on_delete_still_calls_on_cancel to test_cancel_on_delete_does_not_call_on_cancel to match the now-documented contract All tests pass: 119 unit tests, 9 device tests.
|
Addressed below:
|
Summary
Replaces three independent ad-hoc keyboard implementations in
wallet_menu,passphrase_menu, andgenerate_seedphrase_menuwith a single sharedKeyboardManagerowned byNavigationController.Architecture
KeyboardManagerholds one reusablelv.keyboardinstance (lazy-created).bind()from aCLICKEDevent lambda on theirtextarea — no permanent owner reference is held.
bind()silently replaces theprevious one (restoring the old textarea text, suppressing
on_cancel).lv.EVENT.DELETEon the textarea, so menusnever need to manually unbind on navigation.
Layout.ALNUM/Layout.FULL.leading/trailing spaces via
sanitize=str.strip).Tests
test_keyboard_manager.pycovering: bind/unbindlifecycle, commit, cancel, sanitizer, DELETE path, rebind replacement,
rebind
on_cancelsuppression, and no-crash guards.test_passphrase_keyboard_repeated_commits_no_reset:regression for crash observed after multiple passphrase edit cycles on device.
test_keyboard_device.pywith open/commit/cancel flow coverage.tests_device/conftest.py.All tests pass: 119 unit tests, 9 device tests.