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 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-16 12:31:28 -07:00
parent c675f6b278
commit 6e46916dd9
3 changed files with 43 additions and 12 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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=)