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:
@@ -148,13 +148,21 @@ def _handle_help(state: RawSession, args) -> None:
|
|||||||
|
|
||||||
def run(port=None) -> int:
|
def run(port=None) -> int:
|
||||||
from prompt_toolkit import PromptSession
|
from prompt_toolkit import PromptSession
|
||||||
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||||
from prompt_toolkit.key_binding import KeyBindings
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
from prompt_toolkit.patch_stdout import patch_stdout
|
from prompt_toolkit.patch_stdout import patch_stdout
|
||||||
|
|
||||||
|
from .completer import RawCompleter
|
||||||
|
|
||||||
state = RawSession(device=_connect(port))
|
state = RawSession(device=_connect(port))
|
||||||
kb = KeyBindings()
|
kb = KeyBindings()
|
||||||
install_key_bindings(kb, state)
|
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():
|
def message():
|
||||||
return state.breadcrumb() + " "
|
return state.breadcrumb() + " "
|
||||||
|
|||||||
54
pm3py/cli/rawcli/completer.py
Normal file
54
pm3py/cli/rawcli/completer.py
Normal 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)
|
||||||
@@ -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.identify import identify, clear, name_14a
|
||||||
from pm3py.cli.rawcli.catalog import TYPE2, ISO15, catalog_for, catalog_for as _cf
|
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.tlv import parse_tlv, format_tlv
|
||||||
|
from pm3py.cli.rawcli.completer import RawCompleter
|
||||||
from pm3py.cli.rawcli.app import dispatch
|
from pm3py.cli.rawcli.app import dispatch
|
||||||
|
|
||||||
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
||||||
@@ -296,3 +297,32 @@ class TestTLV:
|
|||||||
def test_tlv_command(self, capsys):
|
def test_tlv_command(self, capsys):
|
||||||
dispatch(RawSession(), "tlv " + self.BLOCK.hex())
|
dispatch(RawSession(), "tlv " + self.BLOCK.hex())
|
||||||
assert "NDEF MESSAGE" in capsys.readouterr().out
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user