feat(rawcli): flexible entry + parser (Phase 2)

- parser.py: classify a line into control / function-call / raw payload;
  parse loose hex/binary with intermixed 0x/0b tokens (whitespace-
  insensitive, defaulting to the session entry mode)
- entry.py: byte_space() byte-group formatting + key bindings — Ctrl-/
  toggles hex/binary entry mode (breadcrumb updates), digits auto-space
  into byte groups as you type
- app.py: dispatch() drives the loop via parse_line (testable without a
  live prompt); identify/call are stubbed for Phases 3/4

14 new tests (parser tokens/intermix/errors, byte_space, dispatch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 12:07:21 -07:00
parent 028ce9c21e
commit 54e52f029f
4 changed files with 250 additions and 15 deletions

View File

@@ -10,6 +10,8 @@ from __future__ import annotations
import sys
from .session import RawSession
from .parser import parse_line
from .entry import install_key_bindings
from .trace_view import render_exchange
_HELP = """\
@@ -45,12 +47,48 @@ def _raw_exchange(device, payload: bytes) -> bytes | None:
return None
def dispatch(state: RawSession, line: str) -> bool:
"""Handle one input line. Returns False to exit the REPL, True to keep going. Testable
without a live prompt."""
try:
cmd = parse_line(line, state.entry_mode)
except ValueError as exc:
print(f"rawcli: {exc}")
return True
if cmd.kind == "control":
if cmd.name in ("quit", "exit", "q"):
return False
if cmd.name == "help":
print(_HELP)
else:
# identify / transponder / close land in Phase 3
print(f"rawcli: '{cmd.name}' is not wired yet (coming in a later phase)")
return True
if cmd.kind == "call":
# function-style transponder commands land in Phase 4 (the command catalog)
print(f"rawcli: {cmd.name}(...) needs an identified transponder + catalog (Phase 4)")
return True
# raw payload
if not cmd.payload:
return True
response = _raw_exchange(state.device, cmd.payload)
print(render_exchange(cmd.payload, response, protocol="hf14a",
is_tty=sys.stdout.isatty()))
return True
def run(port=None) -> int:
from prompt_toolkit import PromptSession
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.patch_stdout import patch_stdout
state = RawSession(device=_connect(port))
session = PromptSession()
kb = KeyBindings()
install_key_bindings(kb, state)
session = PromptSession(key_bindings=kb)
def message():
return state.breadcrumb() + " "
@@ -58,23 +96,12 @@ def run(port=None) -> int:
with patch_stdout():
while True:
try:
line = session.prompt(message).strip()
line = session.prompt(message)
except (EOFError, KeyboardInterrupt):
break
if not line:
if not line.strip():
continue
if line in ("quit", "exit", "q"):
if not dispatch(state, line):
break
if line == "help":
print(_HELP)
continue
try:
payload = bytes.fromhex(line.lower().replace("0x", "").replace(" ", ""))
except ValueError:
print(f"rawcli: could not parse {line!r} as hex")
continue
response = _raw_exchange(state.device, payload)
print(render_exchange(payload, response, protocol="hf14a",
is_tty=sys.stdout.isatty()))
print("bye")
return 0