aboutsummaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-10 15:35:21 +0200
committerblasty <blasty@local>2026-07-10 15:35:21 +0200
commit0531f0e1787061aff06b01981d0e72d3718e3637 (patch)
treec8c14283ffb581be90260e5770cd7952c98fa9ff /idatui
parentrpc: more verbs — structured reads, modal select, in-view search, save (diff)
downloadida-tui-0531f0e1787061aff06b01981d0e72d3718e3637.tar.gz
ida-tui-0531f0e1787061aff06b01981d0e72d3718e3637.tar.xz
ida-tui-0531f0e1787061aff06b01981d0e72d3718e3637.zip
rpc: readiness, quit, methods discovery, not-ready guard
Spawn-and-drive needs to know when the freshly-launched TUI is usable: ping/state now carry {ready,functions,complete} (cheap; no health call) and ping adds the module name. Add a graceful 'quit' verb (acks, then exits on a short delay so the reply still flushes) and a self-documenting 'methods' table. Program-dependent verbs are refused with a clear 'not ready' instead of exploding in a worker while still loading. rpc_smoke covers readiness, methods and quit (21 green).
Diffstat (limited to 'idatui')
-rw-r--r--idatui/rpc.py73
1 files changed, 71 insertions, 2 deletions
diff --git a/idatui/rpc.py b/idatui/rpc.py
index b4b73c6..afe5be3 100644
--- a/idatui/rpc.py
+++ b/idatui/rpc.py
@@ -33,6 +33,48 @@ from .app import DecompView, DisasmView, HexView
PROTO_VERSION = 1
TYPE_DELAY_MS = 35 # default per-char delay for high-level typed ops (aesthetic)
+# Verbs that dereference app.program — refused with a clear error before load.
+_PROGRAM_METHODS = {
+ "goto", "open", "rename", "comment", "retype", "follow", "xrefs", "symbols",
+ "structs", "search", "select", "save", "hex", "toggle_view",
+ "pseudocode", "disassembly", "xrefs_to", "xrefs_from", "resolve",
+}
+
+# Self-documenting method table (returned by the 'methods' verb).
+METHODS = {
+ "ping": "-> {ok,proto,module,ready,functions,complete}",
+ "methods": "-> this table",
+ "quit": "gracefully exit the TUI",
+ "state": "-> full state snapshot",
+ "view": "{lines?} -> visible code-pane lines (disasm/decomp)",
+ "screen": "-> {width,height,text} full plain-text render",
+ "functions": "{filter?,limit?=50} -> [{ea,name,size}]",
+ "pseudocode": "{target?} -> full decompiled body",
+ "disassembly": "{target?,max?=2000} -> {total,lines:[{ea,text}]}",
+ "xrefs_to": "{target,limit?=200} -> [{frm,to,type,fn_addr,fn_name}]",
+ "xrefs_from": "{target,limit?=200} -> same shape",
+ "resolve": "{name} -> {ea}",
+ "keys": "{keys:[str],settle?,timeout?} raw key injection (supports 'wait:<ms>')",
+ "text": "{text,delay_ms?,settle?} type a literal string into the focused input",
+ "goto/open": "{target,delay_ms?} g-prompt to a name or 0xADDR",
+ "rename": "{name,delay_ms?} rename the token under the cursor",
+ "comment": "{text,delay_ms?} comment the current line",
+ "retype": "{proto,delay_ms?} set the prototype/type under the cursor",
+ "follow": "follow the reference under the cursor",
+ "back": "pop the nav stack",
+ "toggle_view": "disasm <-> pseudocode",
+ "hex": "hex view",
+ "xrefs": "open the xref picker",
+ "symbols": "{query?} open the symbol palette",
+ "structs": "open the struct editor",
+ "search": "{term,direction?=1} incremental search in the code view",
+ "select": "{index?} choose the highlighted/nth item in the open modal",
+ "save": "persist the .i64 (Ctrl+S)",
+ "close": "dismiss a modal (Escape)",
+ "move": "{dir,n?=1} fast movement (down/up/.../pagedown)",
+ "cursor": "{line?,col?} set the code-pane cursor directly",
+}
+
# Movement keys — driven fast (no typed delay) so the pane still visibly moves.
_MOVE_KEYS = {
"down": "j", "up": "k", "left": "h", "right": "l",
@@ -101,6 +143,18 @@ def _cursor_info(app, w) -> dict[str, Any]:
"scroll_y": round(w.scroll_offset.y)}
+def _readiness(app) -> dict[str, Any]:
+ """Whether the app is drivable yet, and how far function-loading has got.
+ (Cheap: no network — never call client.health() here.)"""
+ idx = app._func_index
+ n = len(idx.all_loaded()) if idx is not None else 0
+ return {
+ "ready": app.program is not None and idx is not None and n > 0,
+ "functions": n,
+ "complete": bool(idx is not None and idx.complete),
+ }
+
+
def snapshot(app) -> dict[str, Any]:
"""A structured view of everything an agent needs to decide the next move."""
cur = app._cur
@@ -120,7 +174,7 @@ def snapshot(app) -> dict[str, Any]:
"nav_depth": len(app._nav),
"dirty": bool(app._dirty),
"modal": _modal_snapshot(app),
- "loaded": app.program is not None and app._func_index is not None,
+ **_readiness(app),
}
@@ -347,7 +401,22 @@ class RpcServer:
async def _dispatch(self, method: str, params: dict[str, Any]) -> Any:
app = self.app
if method in (None, "ping"):
- return {"ok": True, "proto": PROTO_VERSION, "loaded": app.program is not None}
+ module = None
+ try:
+ module = app._module() if app.client else None
+ except Exception: # noqa: BLE001
+ pass
+ return {"ok": True, "proto": PROTO_VERSION, "module": module,
+ **_readiness(app)}
+ if method == "methods":
+ return METHODS
+ if method == "quit":
+ # answer first, then tear down (so this response still gets written)
+ asyncio.get_running_loop().call_later(0.2, app.exit)
+ return {"ok": True, "quitting": True}
+
+ if method in _PROGRAM_METHODS and app.program is None:
+ raise ValueError("not ready: still connecting / loading functions")
if method == "keys":
keys = params.get("keys") or []