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>
94 lines
3.5 KiB
Python
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)
|