aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-09 21:05:07 +0200
committerblasty <blasty@local>2026-07-09 21:05:07 +0200
commit143ec78053313c95770b02f385ec0c3ee5cf48ac (patch)
tree3b0afb5127b27b89db226152f0a84cb3888c08a8
parentcomments: ';' sets a comment on the current line (disasm + pseudocode) (diff)
downloadida-tui-143ec78053313c95770b02f385ec0c3ee5cf48ac.tar.gz
ida-tui-143ec78053313c95770b02f385ec0c3ee5cf48ac.tar.xz
ida-tui-143ec78053313c95770b02f385ec0c3ee5cf48ac.zip
decompiler: hide the per-line /*0xEA*/ address markers (clutter)
Hex-Rays appends a /*0xEA*/ VMA marker to every pseudocode line (we fetch with include_addresses). It's the only per-line address anchor the app has, so we can't turn it off server-side without breaking follow/comments/xref-jumps. Instead, strip it at display time: DecompView.show pulls each line's marker into a parallel _line_eas list and highlights the cleaned text. _line_ea now reads that list; _follow_decomp takes the line ea explicitly (was re-parsing the marker). Stripping only edits within lines, so indices still align with the domain's raw code (used by _decomp_line_for). Var-location comments (// [rsp+..]) are Hex-Rays output, not VMAs -> kept. full suite 63/63.
-rw-r--r--idatui/app.py45
-rw-r--r--tests/test_tui.py11
2 files changed, 37 insertions, 19 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 5e4061c..0e68f47 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -55,6 +55,12 @@ _S_CELL = Style(reverse=True) # the block cursor cell
_S_LINENO = Style(color="grey37") # pseudocode line-number gutter
_S_LINENO_CUR = Style(color="grey66", bold=True) # gutter on the cursor line
+# Hex-Rays appends a `/*0xEA*/` address marker to each pseudocode line (we fetch
+# with include_addresses so we have a per-line anchor). Matched here to extract
+# the address and strip the marker from the display.
+_ADDR_MARK_RE = re.compile(r"/\*\s*0x([0-9A-Fa-f]+)\s*\*/")
+_ADDR_MARK_STRIP_RE = re.compile(r"\s*/\*\s*0x[0-9A-Fa-f]+\s*\*/")
+
@dataclass
class NavEntry:
@@ -818,6 +824,7 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True
self._strips: list[Strip] = []
self._texts: list[str] = []
self._gutter = 0 # line-number gutter width (cells)
+ self._line_eas: list[int | None] = [] # per-line address (marker stripped)
self._term = ""
self._matches: list[int] = []
self._ranges: dict[int, list[tuple[int, int]]] = {}
@@ -838,11 +845,22 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True
def show(self, ea: int, text: str, cursor: int = 0, cursor_x: int = 0,
scroll_y: int = -1, scroll_x: int = 0) -> None:
- seglists = highlight_c(text)
+ # Pull each line's `/*0xEA*/` marker into _line_eas, then strip it from
+ # the displayed text (clutter) before highlighting. Stripping only edits
+ # within lines, so line indices still align with the domain's raw code.
+ line_eas: list[int | None] = []
+ clean: list[str] = []
+ for ln in text.split("\n"):
+ eas = _ADDR_MARK_RE.findall(ln)
+ line_eas.append(int(eas[-1], 16) if eas else None)
+ clean.append(_ADDR_MARK_STRIP_RE.sub("", ln).rstrip())
+ seglists = highlight_c("\n".join(clean))
self._strips = [Strip(segs) for segs in seglists]
self._texts = ["".join(seg.text for seg in segs) for segs in seglists]
- self.loaded_ea = ea
total = len(self._strips)
+ # highlight_c may drop a lone trailing blank line; keep _line_eas aligned.
+ self._line_eas = (line_eas[:total] + [None] * total)[:total]
+ self.loaded_ea = ea
self.cursor = max(0, min(max(total - 1, 0), cursor))
self.cursor_x = cursor_x
self._matches = []
@@ -865,10 +883,7 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True
return Static("― decompiling… ―", classes="decomp-loading")
def _line_ea(self, idx: int) -> int | None:
- if not (0 <= idx < len(self._texts)):
- return None
- ms = re.findall(r"/\*\s*0x([0-9A-Fa-f]+)\s*\*/", self._texts[idx])
- return int(ms[-1], 16) if ms else None
+ return self._line_eas[idx] if 0 <= idx < len(self._line_eas) else None
def _after_cursor_move(self) -> None:
self.post_message(DecompView.CursorMoved(self.cursor, self._line_ea(self.cursor)))
@@ -1316,7 +1331,8 @@ class IdaTui(App):
if ea is not None:
self._follow_disasm(ea, word, nxt.ea if nxt else None)
elif isinstance(view, DecompView) and view._texts:
- self._follow_decomp(view._texts[view.cursor], word)
+ self._follow_decomp(view._texts[view.cursor], word,
+ view._line_ea(view.cursor))
def on_xrefs_requested(self, msg: XrefsRequested) -> None:
view = msg.view
@@ -1360,7 +1376,8 @@ class IdaTui(App):
self._do_navigate(tgt.to, push=True)
@work(thread=True, group="nav")
- def _follow_decomp(self, line: str, word: str | None) -> None:
+ def _follow_decomp(self, line: str, word: str | None,
+ line_ea: int | None = None) -> None:
if self._cur is None:
return
dec = self.program.decompile(self._cur.ea)
@@ -1376,14 +1393,12 @@ class IdaTui(App):
addr = None
if addr is None:
addr = self._ref_on_line(line)
- if addr is None:
- # Address-based fallback: use the line's /*0xEA*/ marker and follow a
- # code xref from that statement. Immune to a stale name (e.g. right
+ if addr is None and line_ea is not None:
+ # Address-based fallback: follow a code xref from this statement's ea
+ # (the stripped /*0xEA*/ anchor). Immune to a stale name (e.g. right
# after a rename, before the pseudocode text catches up).
- m = re.search(r"/\*\s*0x([0-9A-Fa-f]+)\s*\*/", line or "")
- if m:
- xr = self.program.xrefs_from(int(m.group(1), 16))
- addr = next((x.to for x in xr if x.type == "code" and x.to), None)
+ xr = self.program.xrefs_from(line_ea)
+ addr = next((x.to for x in xr if x.type == "code" and x.to), None)
if addr is None:
self.app.call_from_thread(self._status, "nothing to follow on this line")
return
diff --git a/tests/test_tui.py b/tests/test_tui.py
index b8eba79..2f80270 100644
--- a/tests/test_tui.py
+++ b/tests/test_tui.py
@@ -517,13 +517,15 @@ async def run(db):
# fallback follows by address regardless of the (old) token.
dstale = app.program.resolve(dsym)
old_line = dec._texts[drow] if drow < len(dec._texts) else ""
- if "/*0x" in old_line and dsym in old_line:
+ old_ea = dec._line_ea(drow)
+ if old_ea is not None and dsym in old_line:
tmpname = f"stale_{os.getpid()}"
app.program.client.call(
"rename", batch={"func": {"addr": hex(dstale), "name": tmpname}})
app.program.bump_names()
d2 = len(app._nav)
- app._follow_decomp(old_line, dsym) # dsym is now the OLD name
+ # dsym is now the OLD name; follow must fall back to the line's ea
+ app._follow_decomp(old_line, dsym, old_ea)
await wait_until(pilot, lambda: len(app._nav) > d2, timeout=25)
check("decomp follow works with a stale name (ea-marker fallback)",
app._cur.ea == dstale, f"cur={app._cur.ea:#x} want={dstale:#x}")
@@ -631,8 +633,9 @@ async def run(db):
# Add a comment on the current pseudocode line via ';' and confirm it
# renders after the decompiler refreshes.
- cline = next((i for i, t in enumerate(dec._texts)
- if i > 5 and "/*0x" in t and t.strip() and "//" not in t), None)
+ cline = next((i for i in range(len(dec._texts))
+ if i > 5 and dec._line_ea(i) is not None
+ and dec._texts[i].strip() and "//" not in dec._texts[i]), None)
if cline is not None:
cea = dec._line_ea(cline)
dec.focus()