Files
pm3py/pm3py/cli/clientcli/hints.py
michael 4f66a93de9 feat(cli): add pm3py client — autocompleting console over the stock proxmark3 client
Wraps the standard Proxmark3 C client (so it works with any Proxmark/firmware) in a prompt_toolkit REPL: full command-tree completion sourced from the client's commands.json, option-flag completion, Tab-to-descend, a press-right help toggle, and the client's own coloured output.

Two swappable backends behind ClientDriver: in-process SWIG (_pm3) by default, a pexpect-around-the-binary fallback otherwise. Both auto-detect /dev/ttyACM* and discover the checkout via its doc/commands.json marker (walk-up + side-by-side), so no path is hardcoded.

`pm3py client --pack OUT` spins off a self-contained drop-in — a readable .py, or a .pyz with prompt_toolkit/pexpect bundled — amalgamated from the package (the tested source of truth) so it can't drift. Adds pexpect to deps; hardware-free tests in tests/test_clientcli.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 21:05:53 -07:00

94 lines
3.5 KiB
Python

"""Bottom-toolbar hints for clientcli.
Two channels, both driven by the command tree (models :func:`pm3py.cli.rawcli.memory.input_hint`):
- :func:`one_line_hint` — a one-line relevance hint for the current input (always on).
- :func:`help_panel` / :func:`panel_for` — the multi-line help shown when the help toggle is open;
:func:`resolve_target` maps the *highlighted* completion back to the node whose help to show.
"""
from __future__ import annotations
from .tree import parse_option
# ANSI (same palette as the trace / breadcrumb)
_DIM = "\033[2m"
_BOLD = "\033[1m"
_CYAN = "\033[36m"
_RESET = "\033[0m"
def _skip(flags) -> bool:
return "--help" in flags or "-h" in flags
def one_line_hint(tree, text):
"""A one-line hint for ``text``: a leaf's description + usage, or an interior node's subcommand
count. Resolves on the completed prefix when the buffer ends mid-word. ``None`` if unknown."""
tokens = (text or "").split()
if not tokens:
return None
node, used = tree.node_at(tokens), tokens
if node is None:
node, used = tree.node_at(tokens[:-1]), tokens[:-1]
if node is None or node is tree.root:
return None
if node.is_leaf:
e = node.entry
return f"{e['command']}{e['description']} · {e['usage']}"
return f"{' '.join(used)}{len(node.children)} subcommands"
def help_panel(entry) -> str:
"""The full multi-line help for a leaf command: description, usage, options, and notes."""
if not entry:
return ""
out = [f"{_BOLD}{entry['command']}{_RESET}{entry.get('description', '')}",
f"{_DIM}usage:{_RESET} {entry.get('usage', '')}"]
rows = []
for opt in entry.get("options", []):
flags, meta, desc = parse_option(opt)
if not flags or _skip(flags):
continue
rows.append((", ".join(flags) + (f" {meta}" if meta else ""), desc))
if rows:
w = min(max(len(h) for h, _ in rows), 28)
out.append(f"{_DIM}options:{_RESET}")
out += [f" {_CYAN}{h:<{w}}{_RESET} {d}" for h, d in rows]
notes = entry.get("notes", [])
if notes:
out.append(f"{_DIM}notes:{_RESET}")
out += [f" {_DIM}{n}{_RESET}" for n in notes]
return "\n".join(out)
def panel_for(node) -> str | None:
"""The help panel for a highlighted node — a leaf's full help, or an interior node's child
list. ``None`` when there is nothing to show."""
if node is None:
return None
if node.is_leaf:
return help_panel(node.entry)
kids = sorted(node.children)
if not kids:
return None
head = f"{_BOLD}{node.token or '(commands)'}{_RESET}{len(kids)} subcommands"
return head + "\n" + f"{_DIM}{', '.join(kids)}{_RESET}"
def resolve_target(tree, buffer_text, current_completion):
"""The tree node whose help to show for the highlighted completion. A subcommand completion
extends the current path; a flag completion targets the leaf under the cursor. ``None`` if
nothing resolves."""
tokens = (buffer_text or "").split()
at_word = bool(tokens) and not (buffer_text or "").endswith(" ")
path = tokens[:-1] if at_word else tokens
ctext = getattr(current_completion, "text", "") if current_completion else ""
if ctext.startswith("-"):
for i in range(len(path), -1, -1):
node = tree.node_at(path[:i])
if node is not None and node.is_leaf:
return node
return None
full = path + [ctext] if ctext else path
return tree.node_at(full)