aboutsummaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-10 13:30:48 +0200
committerblasty <blasty@local>2026-07-10 13:30:48 +0200
commit6d83015906f850b872261655f014ef14612a8ba5 (patch)
tree40278c66ac828e7073b74d9d0728c1473293779d /idatui
parentstruct editor: auto-format the definition on save (diff)
downloadida-tui-6d83015906f850b872261655f014ef14612a8ba5.tar.gz
ida-tui-6d83015906f850b872261655f014ef14612a8ba5.tar.xz
ida-tui-6d83015906f850b872261655f014ef14612a8ba5.zip
retype: set variable/function types with 'y' (IDA-style), via structured tools
Instead of parsing pseudocode text, add structured server tools (server/ patch_server.py, alongside del_type): - func_types(addr): prototype + local variables (name/type/is_arg) - set_lvar_type(addr,var,type): retype a decompiler local, working on auto/ register vars too (stock set_type only updates already-user-modified lvars) 'y' in a code view retypes what's under the cursor: a local variable (prompt prefilled with its current type) or a function (prompt prefilled with its full prototype). Applies via set_lvar_type / set_type, then recompiles + refreshes. Copy-line moved off 'y' to Ctrl+Y. domain: Program.func_types / set_function_type / set_lvar_type (LVar/FuncTypes). Pilot: function-prototype retype (prefill + apply). full suite 94/94.
Diffstat (limited to 'idatui')
-rw-r--r--idatui/app.py119
-rw-r--r--idatui/domain.py51
2 files changed, 169 insertions, 1 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 32a0698..3ea0f3e 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -121,6 +121,15 @@ class CommentRequested(Message):
self.view = view
+class RetypeRequested(Message):
+ """A code view asks to retype the function/variable under the cursor."""
+
+ def __init__(self, view, name: str | None) -> None: # type: ignore[no-untyped-def]
+ super().__init__()
+ self.view = view
+ self.name = name
+
+
class NavMixin:
"""Follow / xrefs actions shared by the code views (bindings live on each
view since Textual only merges BINDINGS from DOMNode subclasses)."""
@@ -129,8 +138,9 @@ class NavMixin:
Binding("enter", "follow", "Follow"),
Binding("x", "xrefs", "Xrefs"),
Binding("n", "rename", "Rename"),
+ Binding("y", "retype", "Retype"),
Binding("semicolon", "comment", "Comment"),
- Binding("y", "copy_line", "Copy line"),
+ Binding("ctrl+y", "copy_line", "Copy line"),
]
def action_follow(self) -> None:
@@ -145,6 +155,9 @@ class NavMixin:
def action_comment(self) -> None:
self.post_message(CommentRequested(self))
+ def action_retype(self) -> None:
+ self.post_message(RetypeRequested(self, self.word_under_cursor()))
+
def action_copy_line(self) -> None:
plain = self._line_plain(self.cursor)
if not plain:
@@ -1459,6 +1472,10 @@ class IdaTui(App):
height: 1; border: none; padding: 0 1;
background: $success-darken-2; color: $text;
}
+ #retype {
+ height: 1; border: none; padding: 0 1;
+ background: $accent-darken-2; color: $text;
+ }
#status { height: 1; background: $panel; color: $text; padding: 0 1; }
.decomp-loading {
width: 100%; height: 100%; content-align: center middle;
@@ -1525,6 +1542,7 @@ class IdaTui(App):
self._search_ctx: tuple[object | None, int] = (None, 1)
self._rename_ctx: tuple[object | None, str] = (None, "")
self._comment_ctx: tuple[object | None, int, str] = (None, 0, "")
+ self._retype_ctx: tuple[object | None, str, int, str] = (None, "", 0, "")
self._xref_focus_name: str | None = None
self._dirty = False
@@ -1551,6 +1569,10 @@ class IdaTui(App):
ci.display = False
ci.can_focus = False
yield ci
+ ti = Input(id="retype")
+ ti.display = False
+ ti.can_focus = False
+ yield ti
yield Static("connecting…", id="status")
yield Footer()
@@ -2141,6 +2163,90 @@ class IdaTui(App):
verb = "cleared comment" if not text else "commented"
self._status(f"{verb} @ {ea:#x} (Ctrl+S to save)")
+ # -- retype (set type, IDA 'y') --------------------------------------- #
+ def on_retype_requested(self, msg: RetypeRequested) -> None:
+ if self._cur is None or self.program is None:
+ return
+ self._prepare_retype(msg.view, msg.name)
+
+ @work(thread=True, exclusive=True, group="retype")
+ def _prepare_retype(self, view, word: str | None) -> None: # type: ignore[no-untyped-def]
+ """Work out whether the cursor is on a local variable or a function, and
+ fetch the current type/prototype to prefill the prompt."""
+ assert self.program is not None and self._cur is not None
+ ft = self.program.func_types(self._cur.ea)
+ kind: str | None = None
+ subject: int = self._cur.ea
+ prefill = ""
+ # 1) a local variable (or arg) of the current function
+ if word and ft is not None:
+ lv = next((v for v in ft.lvars if v.name == word), None)
+ if lv is not None:
+ kind, prefill = "lvar", lv.type
+ # 2) a function under the cursor (self or a referenced one)
+ if kind is None and self._looks_like_symbol(word):
+ try:
+ tgt = self.program.resolve(word)
+ except Exception: # noqa: BLE001
+ tgt = None
+ if tgt is not None:
+ tft = self.program.func_types(tgt)
+ if tft is not None:
+ kind, subject, prefill = "func", tgt, tft.prototype
+ # 3) fall back to the current function itself
+ if kind is None and ft is not None:
+ kind, subject, prefill = "func", self._cur.ea, ft.prototype
+ if kind is None:
+ self.app.call_from_thread(self._status, "nothing to retype under the cursor")
+ return
+ self.app.call_from_thread(self._open_retype, view, kind, subject, word or "", prefill)
+
+ def _open_retype(self, view, kind: str, subject: int, word: str, # type: ignore[no-untyped-def]
+ prefill: str) -> None:
+ self._retype_ctx = (view, kind, subject, word)
+ self.query_one("#status", Static).display = False
+ inp = self.query_one("#retype", Input)
+ label = "prototype" if kind == "func" else f"type for '{word}'"
+ inp.placeholder = f"{label} — Enter=apply Esc=cancel"
+ inp.can_focus = True
+ inp.display = True
+ inp.value = prefill
+ inp.focus()
+
+ def _end_retype(self) -> None:
+ inp = self.query_one("#retype", Input)
+ inp.display = False
+ inp.can_focus = False
+ self.query_one("#status", Static).display = True
+ view = self._retype_ctx[0]
+ if view is not None:
+ view.focus()
+
+ @work(thread=True, exclusive=True, group="retype-apply")
+ def _do_retype(self, kind: str, subject: int, word: str, new: str) -> None:
+ assert self.program is not None
+ if kind == "func":
+ err = self.program.set_function_type(subject, new)
+ else: # lvar of the current function
+ err = self.program.set_lvar_type(self._cur.ea, word, new)
+ if err:
+ self.app.call_from_thread(self._status, f"retype failed: {err}")
+ return
+ self.app.call_from_thread(self._after_retype, kind, word)
+
+ def _after_retype(self, kind: str, word: str) -> None:
+ # A type change alters the pseudocode (and disasm operand types), so
+ # recompile via the name-generation invalidation and reopen in place.
+ self.program.bump_names()
+ cur = self._cur
+ if cur is not None:
+ self._save_current_pos()
+ self.query_one(DecompView).loaded_ea = None
+ self._open_entry(cur, push=False)
+ self._dirty = True
+ what = "prototype" if kind == "func" else f"'{word}'"
+ self._status(f"retyped {what} (Ctrl+S to save)")
+
@work(thread=True, exclusive=True, group="rename")
def _do_rename(self, view, old: str, new: str) -> None: # type: ignore[no-untyped-def]
assert self.program is not None
@@ -2353,6 +2459,11 @@ class IdaTui(App):
event.prevent_default()
self._end_comment()
return
+ if self.query_one("#retype", Input).display:
+ event.stop()
+ event.prevent_default()
+ self._end_retype()
+ return
fi = self.query_one("#func-filter", Input)
if fi.display:
event.stop()
@@ -2391,6 +2502,12 @@ class IdaTui(App):
if view is not None and value != existing: # empty value clears it
self._do_comment(ea, value)
return
+ if inp.id == "retype":
+ view, kind, subject, word = self._retype_ctx
+ self._end_retype()
+ if view is not None and value:
+ self._do_retype(kind, subject, word, value)
+ return
if getattr(self, "_goto_mode", False):
self._goto_mode = False
self.query_one("#func-table", DataTable).focus()
diff --git a/idatui/domain.py b/idatui/domain.py
index f52535c..279ef49 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -93,6 +93,21 @@ class Xref:
@dataclass(frozen=True)
+class LVar:
+ name: str
+ type: str
+ is_arg: bool
+
+
+@dataclass
+class FuncTypes:
+ addr: int
+ name: str
+ prototype: str # e.g. 'int __fastcall foo(int a, char *b)'
+ lvars: list[LVar]
+
+
+@dataclass(frozen=True)
class Struct:
name: str
size: int
@@ -495,6 +510,42 @@ class Program:
return res[0].get("error")
return None
+ # -- function / variable types ---------------------------------------- #
+ def func_types(self, ea: int) -> FuncTypes | None:
+ """Structured decompiler types for the function at ``ea`` (prototype +
+ local variables). None if ``ea`` isn't a decompilable function. Requires
+ the injected ``func_types`` server tool."""
+ try:
+ r = self.client.call("func_types", addr=hex(ea))
+ except IDAToolError:
+ return None
+ if not isinstance(r, dict) or r.get("error"):
+ return None
+ lvars = [LVar(name=lv.get("name", ""), type=lv.get("type", ""),
+ is_arg=bool(lv.get("is_arg")))
+ for lv in r.get("lvars", []) if isinstance(lv, dict)]
+ return FuncTypes(addr=_as_int(r.get("addr", hex(ea))), name=r.get("name", ""),
+ prototype=r.get("prototype", ""), lvars=lvars)
+
+ def set_function_type(self, ea: int, signature: str) -> str | None:
+ """Set a function's prototype. None on success, else an error string."""
+ r = self.client.call("set_type", edits=[{"addr": hex(ea), "signature": signature}])
+ res = r.get("result", []) if isinstance(r, dict) else []
+ row = res[0] if res and isinstance(res[0], dict) else {}
+ if row.get("ok"):
+ return None
+ return row.get("error") or "failed to set the prototype"
+
+ def set_lvar_type(self, fn_ea: int, var: str, ty: str) -> str | None:
+ """Set a decompiler local variable's type (via the injected server tool).
+ None on success, else an error string."""
+ r = self.client.call("set_lvar_type", addr=hex(fn_ea), variable=var, type=ty)
+ if isinstance(r, dict) and r.get("error"):
+ return r["error"]
+ if isinstance(r, dict) and not r.get("ok"):
+ return "failed to set the variable type"
+ return None
+
def delete_type(self, name: str) -> str | None:
"""Delete a named type. Returns None on success, else an error string.
Requires a server-side ``del_type`` tool; if absent, a clear message is