aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/app.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-24 23:38:30 +0200
committerblasty <blasty@local>2026-07-24 23:38:30 +0200
commitde2fa194278bb132e23edddd65bdad84f32ec37d (patch)
tree67d18789676b3377892d3edf4a6fbe4d497967af /idatui/app.py
parentsplit-view phase 2: cursor sync + linked-row highlight (diff)
downloadida-tui-de2fa194278bb132e23edddd65bdad84f32ec37d.tar.gz
ida-tui-de2fa194278bb132e23edddd65bdad84f32ec37d.tar.xz
ida-tui-de2fa194278bb132e23edddd65bdad84f32ec37d.zip
split-view phase 3: rich per-line instruction region highlight
The Ghidra "region band": moving the pseudocode cursor now lights up EVERY instruction that C line owns, not just one. * server/patch_server.py: new decomp_map tool — sweeps cfunc.get_line_item across each pseudocode line's columns and collects the ea from each item's dstr() ('EA: desc', matching the /*ea*/ marker source so it aligns with the display lines). Returns {addr, lines:[{ea, eas:[...]}]}. (First tried item.get_ea(), which reports a different ea and didn't align — dstr() is the right source.) * domain: Program.decomp_map(ea) -> per-line ea lists, cached by name-gen. * app: _load_split_map fetches it off-thread into _split_eamap/_split_ea2line; _sync_split bands the full instruction region for a C line (decomp drives) and uses the exact ea->line inverse (listing drives), falling back to the single marker until the map lands. Maps cleared on leaving split. Pilot split_view gains: decomp_map returns/aligns with the markers, and a multi-instruction C line bands >1 listing row (13/13). Full suite 154/2-flake. The idalib spike ran on the pilot's own worker (the standalone worker kept getting reaped in this sandbox).
Diffstat (limited to 'idatui/app.py')
-rw-r--r--idatui/app.py57
1 files changed, 45 insertions, 12 deletions
diff --git a/idatui/app.py b/idatui/app.py
index a057412..dbd5025 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -2606,6 +2606,8 @@ class IdaTui(App):
self._pref = "listing" # unified: the linear listing is the code view
self._active = "listing" # currently shown view (in split: the focused pane)
self._split = False # side-by-side listing + pseudocode
+ self._split_eamap: list[list[int]] = [] # split: decomp line -> instr EAs
+ self._split_ea2line: dict[int, int] = {} # split: instr EA -> decomp line
self._hex_pending_ea: int | None = None
self._cur: NavEntry | None = None
self._pending_focus_name: str | None = None # token to land the cursor on
@@ -3165,7 +3167,8 @@ class IdaTui(App):
if lm is not None:
lst.load(lm, name, cursor=idx, scroll_y=max(idx - _JUMP_CONTEXT, 0))
self._show_active() # split branch shows both + loads the decomp
- self._sync_split(self._active) # link now if the decomp was already loaded
+ self._sync_split(self._active) # crude link now
+ self._load_split_map(ea) # region map (async) if decomp is loaded
def action_hex(self) -> None:
"""Backslash: show the raw bytes of the loaded image, synced to the code
@@ -4448,6 +4451,8 @@ class IdaTui(App):
self.query_one("#panes").set_class(False, "split")
lst.set_link(set())
dec.set_link(None)
+ self._split_eamap = []
+ self._split_ea2line = {}
dec.display = lst.display = hx.display = False
if self._active in ("listing", "disasm"):
lst.display = True
@@ -4561,36 +4566,64 @@ class IdaTui(App):
view.show(ea, dec.code or "", cursor=c, cursor_x=cx, scroll_y=sy, scroll_x=sx)
self._status(f"{name} @ {ea:#x} [pseudocode {len(dec.code or '')} chars]{note}")
if self._split:
- self._sync_split(self._active) # decomp just loaded: establish the link
+ self._sync_split(self._active) # crude link now
+ self._load_split_map(ea) # then upgrade to the region map
def _sync_split(self, source: str) -> None:
"""Split view: highlight (+ scroll into view) the companion pane's
location for the focused pane's cursor. The companion only gets a band +
- scroll (its cursor never moves), so there is no echo/ping-pong. Crude
- single-ea mapping for now; phase 3 upgrades to full ea sets."""
+ scroll (its cursor never moves), so there is no echo/ping-pong. Uses the
+ rich per-line ea map (decomp_map) when loaded, else the single marker."""
if not self._split or self.program is None:
return
lst = self.query_one(ListingView)
dec = self.query_one(DecompView)
if source == "decomp":
dec.set_link(None) # the driver shows its own cursor, no band
- ea = dec._line_ea(dec.cursor)
- row = lst.model.ensure_ea(ea) if (ea is not None and lst.model) else None
- if row is not None and row >= 0:
- lst.set_link({row})
- lst.reveal(row)
- else:
- lst.set_link(set())
+ eas = (self._split_eamap[dec.cursor]
+ if 0 <= dec.cursor < len(self._split_eamap) else [])
+ if not eas: # fallback: the single /*ea*/ marker for the line
+ one = dec._line_ea(dec.cursor)
+ eas = [one] if one is not None else []
+ rows: set[int] = set()
+ if lst.model is not None:
+ for e in eas:
+ r = lst.model.ensure_ea(e)
+ if r is not None and r >= 0:
+ rows.add(r)
+ lst.set_link(rows)
+ if rows:
+ lst.reveal(min(rows))
else: # the listing drives
lst.set_link(set())
ea = lst._cursor_ea()
- line = dec.line_for_ea(ea) if ea is not None else None
+ line = self._split_ea2line.get(ea) if ea is not None else None
+ if line is None and ea is not None: # fallback: nearest marker
+ line = dec.line_for_ea(ea)
if line is not None:
dec.set_link(line)
dec.reveal(line)
else:
dec.set_link(None)
+ @work(thread=True, group="split-map")
+ def _load_split_map(self, ea: int) -> None:
+ # Fetch the rich per-line instruction map off the UI thread; sync stays
+ # on the crude single-ea fallback until it lands.
+ assert self.program is not None
+ m = self.program.decomp_map(ea)
+ self.app.call_from_thread(self._apply_split_map, ea, m)
+
+ def _apply_split_map(self, ea: int, m: list) -> None:
+ if not self._split or self._cur is None or self._cur.ea != ea:
+ return # left split / navigated away
+ self._split_eamap = m
+ self._split_ea2line = {}
+ for line, eas in enumerate(m):
+ for e in eas:
+ self._split_ea2line.setdefault(e, line)
+ self._sync_split(self._active) # re-link with the region map
+
def on_decomp_view_cursor_moved(self, msg: DecompView.CursorMoved) -> None:
if self._nav:
self._nav[-1].dec_cursor = msg.index