feat(rawcli): ipython-style completion (Phase 6)

- completer.py: RawCompleter completes the leading command token —
  control verbs + the identified transponder's catalog commands — with
  each command's help as the completion meta (tooltip). `help <cmd>`
  completes catalog command names.
- app: wire the completer with complete_while_typing (live menu) +
  AutoSuggestFromHistory (inline hints) for the ipython feel.

Completes the 6-phase rawcli plan. 4 new tests (verb/catalog completion,
tooltips, help-arg completion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 12:19:42 -07:00
parent 4e7181d289
commit f76663f798
3 changed files with 93 additions and 1 deletions

View File

@@ -148,13 +148,21 @@ def _handle_help(state: RawSession, args) -> None:
def run(port=None) -> int:
from prompt_toolkit import PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.patch_stdout import patch_stdout
from .completer import RawCompleter
state = RawSession(device=_connect(port))
kb = KeyBindings()
install_key_bindings(kb, state)
session = PromptSession(key_bindings=kb)
session = PromptSession(
key_bindings=kb,
completer=RawCompleter(state),
complete_while_typing=True, # ipython-style live menu
auto_suggest=AutoSuggestFromHistory(), # inline suggestion hints
)
def message():
return state.breadcrumb() + " "

View File

@@ -0,0 +1,54 @@
"""ipython-style completion for rawcli.
Completes the leading command token — control verbs plus the identified transponder's catalog
commands — with each command's help as the completion **meta** (the tooltip). ``help <cmd>``
completes catalog command names too. Paired in the app with menu completion while typing and
history auto-suggestion for the ipython feel.
"""
from __future__ import annotations
from prompt_toolkit.completion import Completer, Completion
from .parser import CONTROL_VERBS
from .catalog import catalog_for
class RawCompleter(Completer):
def __init__(self, session):
self._session = session
def get_completions(self, document, complete_event):
text = document.text_before_cursor
stripped = text.lstrip()
tokens = stripped.split()
at_word = bool(tokens) and not stripped.endswith(" ")
word = tokens[-1] if at_word else ""
completing_first = (" " not in stripped) or (len(tokens) == 1 and at_word)
if completing_first:
yield from self._commands(word)
elif tokens and tokens[0].lower() == "help":
yield from self._catalog_names(word)
def _commands(self, word: str):
low = word.lower()
for verb in sorted(CONTROL_VERBS):
if verb.startswith(low):
yield Completion(verb, start_position=-len(word), display_meta="command")
catalog = catalog_for(self._session)
if catalog is not None:
for name in catalog.names():
if name.lower().startswith(low):
tc = catalog.get(name)
yield Completion(f"{name}(", start_position=-len(word),
display=tc.usage(), display_meta=tc.help)
def _catalog_names(self, word: str):
catalog = catalog_for(self._session)
if catalog is None:
return
low = word.lower()
for name in catalog.names():
if name.lower().startswith(low):
yield Completion(name, start_position=-len(word),
display_meta=catalog.get(name).help)

View File

@@ -13,6 +13,7 @@ from pm3py.cli.rawcli.entry import byte_space
from pm3py.cli.rawcli.identify import identify, clear, name_14a
from pm3py.cli.rawcli.catalog import TYPE2, ISO15, catalog_for, catalog_for as _cf
from pm3py.cli.rawcli.tlv import parse_tlv, format_tlv
from pm3py.cli.rawcli.completer import RawCompleter
from pm3py.cli.rawcli.app import dispatch
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
@@ -296,3 +297,32 @@ class TestTLV:
def test_tlv_command(self, capsys):
dispatch(RawSession(), "tlv " + self.BLOCK.hex())
assert "NDEF MESSAGE" in capsys.readouterr().out
class TestCompleter:
def _complete(self, session, text):
from prompt_toolkit.document import Document
return list(RawCompleter(session).get_completions(Document(text), None))
def test_completes_control_verbs(self):
cs = self._complete(RawSession(), "ide")
assert any(c.text == "identify" for c in cs)
def test_completes_catalog_commands_with_meta(self):
s = RawSession()
s.protocol, s.transponder = "hf14a", "NTAG215" # -> Type 2 catalog
cs = self._complete(s, "REA")
texts = [c.text for c in cs]
assert "READ(" in texts
read = next(c for c in cs if c.text == "READ(")
assert "0x30" in read.display_meta_text # tooltip = help
def test_no_catalog_before_identify(self):
cs = self._complete(RawSession(), "REA")
assert all(c.text != "READ(" for c in cs) # no tag => no catalog commands
def test_help_argument_completes_command_names(self):
s = RawSession()
s.protocol, s.transponder = "hf14a", "NTAG215"
cs = self._complete(s, "help GET")
assert any(c.text == "GET_VERSION" for c in cs)