aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-23 02:43:30 +0200
committerblasty <blasty@local>2026-07-23 02:43:30 +0200
commitfc6ad44b4a462560c57c97bf00b25b770c3b6204 (patch)
treec4a95bc00a4bf4325a385024e1a45f271aee58db
parentlisting: expand struct-typed globals into member rows (M4 item 3) (diff)
downloadida-tui-fc6ad44b4a462560c57c97bf00b25b770c3b6204.tar.gz
ida-tui-fc6ad44b4a462560c57c97bf00b25b770c3b6204.tar.xz
ida-tui-fc6ad44b4a462560c57c97bf00b25b770c3b6204.zip
unify: back the function disasm view on the heads listing walker (M4 item 4)
The function disasm view is now a filtered listing: DisasmModel sources its lines from the heads walker bounded to [func.start, func.end) instead of the disasm tool, so both code views render from the one listing mechanism. DisasmView UI (opcode bytes, o-toggle, Tab, rename) preserved -> no test churn. total() stays on disasm include_total: paging heads for the count hit the MCP response-size limit (count=1000 truncated -> wrong count) and corrupted prime. include_total is one fast exact call; for a code function it equals the heads row count backing the lines. _fetch_block uses count=257 (safe). Verified: disasm-heavy scenarios 56/0; full suite 127/1 (1 = flaky filter). M4 complete.
-rw-r--r--TODO15
-rw-r--r--idatui/domain.py27
2 files changed, 29 insertions, 13 deletions
diff --git a/TODO b/TODO
index 3080d25..2311d7d 100644
--- a/TODO
+++ b/TODO
@@ -33,12 +33,15 @@ hard:
[x] back-paging / streaming: listing primes the viewport instantly and
streams the rest in the background (was load_all -> blank pane for
seconds on a big .text). concurrency-safe page loads.
- [ ] struct-typed data expansion (render a struct global as its fields).
- deferred: model change (one head -> many lines); the data is already
- functional via `d my_struct`, expansion is display polish.
- [ ] unify the function disasm view as a filtered listing. deferred: a
- maintainer refactor (no new capability) with real regression risk on
- the battle-tested function-view path; not worth doing blind.
+ [x] struct-typed data expansion: a struct global expands into indented
+ member rows (+off name type) in the listing.
+ [x] unify the function disasm view as a filtered listing: DisasmModel now
+ sources its lines from the `heads` walker bounded to [func.start,
+ func.end) (total via disasm include_total to avoid response-size
+ truncation). Both code views render from the one listing mechanism;
+ the DisasmView UI (opcode bytes/Tab/rename) is preserved. Full suite
+ 127/1 (1 = pre-existing flaky filter).
+ -> M4 COMPLETE.
crazy:
[ ] multi/split view ala ghidra?
diff --git a/idatui/domain.py b/idatui/domain.py
index e70182c..3fe0c51 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -306,8 +306,14 @@ class DisasmModel:
self._lock = threading.Lock()
self._inflight: set[int] = set()
+ def _end_kw(self) -> dict:
+ end = self._function_end()
+ return {"end": hex(end)} if end is not None else {}
+
def total(self) -> int:
- """Total instruction count (fetched once; ~200ms on huge funcs)."""
+ """Instruction/row count of the function (fetched once). Uses disasm's
+ ``include_total`` — one fast call, no response-size truncation. For a
+ code function this equals the heads row count that backs the lines."""
if self._total is not None:
return self._total
payload = self._prog.client.call(
@@ -361,15 +367,22 @@ class DisasmModel:
self._max_raw = biggest
return out
+ @staticmethod
+ def _line_from_head(r: dict) -> Line:
+ """Adapt a ``heads`` row to a disasm Line (label = the head's name)."""
+ return Line(ea=_as_int(r["ea"]), text=r.get("text", ""),
+ label=r.get("name"))
+
def _fetch_block(self, b: int) -> list[Line]:
- # Over-fetch one instruction so the block knows where its last
- # instruction ends (variable-length archs give no size field).
+ # The function disasm view is a listing filtered to the function: fetch a
+ # block of heads (one per instruction for code). Over-fetch one row so
+ # the block knows where its last instruction ends (opcode-byte sizing).
payload = self._prog.client.call(
- "disasm", addr=hex(self.ea), offset=b * self.BLOCK,
- max_instructions=self.BLOCK + 1,
+ "heads", addr=hex(self.ea), offset=b * self.BLOCK,
+ count=self.BLOCK + 1, **self._end_kw(),
)
- raw = payload.get("asm", {}).get("lines", []) if isinstance(payload, dict) else []
- fetched = [Line.from_raw(r) for r in raw]
+ rows = payload.get("heads", []) if isinstance(payload, dict) else []
+ fetched = [self._line_from_head(r) for r in rows]
lines = fetched[:self.BLOCK]
if len(fetched) > self.BLOCK:
end_ea: int | None = fetched[self.BLOCK].ea