From 6e46916dd934df1c6f0b14044230379cb6bddc6e Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 16 Jul 2026 12:31:28 -0700 Subject: [PATCH] fix(rawcli): raw byte-line autocomplete fires in hex and binary The digit key handler applied each keystroke as `buf.document = Document(...)`. Assigning the buffer document resets prompt_toolkit's complete_state and never re-fires completion, so the live autocomplete menu never appeared during raw byte entry -- in BOTH hex and binary (only command-name completion worked, since that path already used insert_text). type_digit only ever appends the digit (optionally after an auto-space) or drops it, so the change is always a pure suffix. Apply it with buf.insert_text, which triggers completion exactly like an ordinary keystroke. Verified by driving the real key binding: the completion menu now populates on every digit in hex and binary (before: complete_state None, start_completion never called). Adds a regression test asserting the handler routes through insert_text; updates the rawcli roadmap. Co-Authored-By: Claude Opus 4.8 --- docs/rawcli-todo.md | 23 +++++++++++++---------- pm3py/cli/rawcli/entry.py | 9 +++++++-- tests/test_rawcli.py | 23 +++++++++++++++++++++++ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/docs/rawcli-todo.md b/docs/rawcli-todo.md index fb226e5..979670f 100644 --- a/docs/rawcli-todo.md +++ b/docs/rawcli-todo.md @@ -11,17 +11,20 @@ dropdown** — never on the bottom bar, never in command output. - **Must work in both hex AND binary entry modes**, rendering in the active base. - Function-call args are base-10 (`READ(4)`); raw bytes are hex, `0b` to intermix binary. -## BUG — binary-mode autocomplete does not fire in the live prompt +## FIXED — raw byte-line autocomplete now fires in hex AND binary -The completer logic is correct when fed the text directly (hex and binary), but in the running -REPL the completion menu doesn't reliably appear while entering **binary**. -- Prime suspect: `entry._regroup_after` sets `buf.document = Document(...)` directly on - auto-spacing, which resets prompt_toolkit's `complete_state` and cancels the menu. Binary - regroups every 8 digits, so it bites almost immediately. -- Fix direction: regroup via a completion-preserving path (don't replace `buf.document` - wholesale); re-arm `complete_while_typing` after regroup. Also confirm the Ctrl-t / Ctrl-_ - toggle actually flips the breadcrumb to `0b`. -- Verify on hardware in binary mode (needs a tag on the antenna). +Root cause: `entry._digit` applied each keystroke as `buf.document = Document(...)`, which resets +prompt_toolkit's `complete_state` and never re-fires completion — so the live menu never appeared +during raw byte entry. This hit **both** hex and binary (the earlier "hex works, binary doesn't" +was inaccurate; only command-name completion worked, because that path already used `insert_text`). +Fixed by appending the per-keystroke delta via `buf.insert_text` instead — `type_digit` only ever +appends (optionally after an auto-space) or drops, so the change is always a pure suffix, and +`insert_text` triggers completion exactly like an ordinary keystroke. Verified by driving the real +key handler: the menu now populates on every digit in both modes. Regression: +`TestKeyBindings::test_digit_binding_fires_completion_trigger_for_raw_bytes`. + +Still worth a quick live-REPL eyeball with a tag on the antenna, and confirming the Ctrl-t / Ctrl-_ +toggle flips the breadcrumb to `0b` (separate from the completion trigger, not yet re-verified). ## Priority 1 — MIFARE Classic (do this first) diff --git a/pm3py/cli/rawcli/entry.py b/pm3py/cli/rawcli/entry.py index cf6079a..3bd7f8c 100644 --- a/pm3py/cli/rawcli/entry.py +++ b/pm3py/cli/rawcli/entry.py @@ -75,7 +75,6 @@ def install_key_bindings(kb, session) -> None: """Wire the entry-mode toggle (Ctrl-/ via Ctrl-_, plus Ctrl-t) and per-byte base-aware digit entry onto ``kb``. ``session`` is the :class:`RawSession` whose ``entry_mode`` is toggled/read and whose ``byte_bases`` records each raw byte's base. Never raises on an unknown key name.""" - from prompt_toolkit.document import Document def _toggle(event): session.toggle_entry_mode() # only flips the mode — the next byte you type uses it @@ -93,7 +92,13 @@ def install_key_bindings(kb, session) -> None: buf.insert_text(d) return new_text, session.byte_bases = type_digit(text, session.byte_bases, session.entry_mode, d) - buf.document = Document(new_text, len(new_text)) + # Apply the change as an append via insert_text, NOT `buf.document = …`: assigning the + # document resets prompt_toolkit's complete_state and never re-fires completion, which + # killed the live autocomplete menu during raw byte entry. type_digit only ever appends + # (optionally after an auto-space) or drops, so the delta is always a pure suffix. + suffix = new_text[len(text):] + if suffix: + buf.insert_text(suffix) for ch in sorted(_DIGITS[HEX] | _DIGITS[BIN]): _bind(kb, ch, _digit) diff --git a/tests/test_rawcli.py b/tests/test_rawcli.py index c1b8017..edcbed2 100644 --- a/tests/test_rawcli.py +++ b/tests/test_rawcli.py @@ -908,3 +908,26 @@ class TestKeyBindings: for ch in "00000100": press(ch) assert buf.text == "30 00000100" and s.byte_bases == ["hex", "bin"] + + def test_digit_binding_fires_completion_trigger_for_raw_bytes(self): + # regression: raw byte entry must append via insert_text (which fires completion), not + # replace buf.document (which resets complete_state and silently killed the live + # autocomplete menu during hex/binary byte entry). on_text_insert fires from insert_text, + # never from a document= swap, so it is a faithful proxy for "the menu would refresh". + from types import SimpleNamespace + from prompt_toolkit.key_binding import KeyBindings + from prompt_toolkit.buffer import Buffer + from pm3py.cli.rawcli.entry import install_key_bindings + s = RawSession() + s.toggle_entry_mode() # binary: regroups every 8 digits (worst case) + kb = KeyBindings() + install_key_bindings(kb, s) + digit = {getattr(b.keys[0], "value", b.keys[0]): b for b in kb.bindings} + buf = Buffer() + fired = [] + buf.on_text_insert += lambda _b: fired.append(1) + app = SimpleNamespace(invalidate=lambda: None) + for ch in "00000100": + digit[ch].handler(SimpleNamespace(current_buffer=buf, app=app, data=ch)) + assert buf.text == "00000100" + assert len(fired) == 8 # one insert_text per digit (0 with document=)