aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 10:47:41 +0200
committerblasty <blasty@local>2026-07-25 10:47:41 +0200
commit82f8d3d800d47c444b8b2f5a08918ede38954e46 (patch)
treed0a1d055fcb390ed23d010d4d2288382511ab958
parentdocs: README covers the split view, strings browser and command palette (diff)
downloadida-tui-82f8d3d800d47c444b8b2f5a08918ede38954e46.tar.gz
ida-tui-82f8d3d800d47c444b8b2f5a08918ede38954e46.tar.xz
ida-tui-82f8d3d800d47c444b8b2f5a08918ede38954e46.zip
split: keep the companion pane level with the driver's cursor
Scrolling either pane left the other one wherever it happened to be: the companion only scrolled when the linked row went off-screen (reveal()), so the link could sit at the bottom edge while the driver's cursor was mid-viewport — visually incoherent, your eye had to hunt for it. Add ListingView/DecompView.align(row, screen_row): scroll so the linked row lands at the SAME viewport offset as the driver's cursor, and use it in _sync_split for both directions. The two panes now track each other line-for-line, so the eye reads straight across. Best-effort at the ends (can't scroll above line 0, nor past the end when the pseudocode is shorter than the viewport). Pilot split_view: deterministic alignment check — park the listing cursor at a known viewport offset deep in the function, sync, and assert the decomp's scroll top is exactly the aligned value (accounting for both clamps). 19/19; full suite 166/2-flake.
-rw-r--r--idatui/app.py21
-rw-r--r--tests/test_scenarios.py19
2 files changed, 38 insertions, 2 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 2bfda88..30e9b9b 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -1284,6 +1284,14 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
if row < top or row >= top + height:
self.scroll_to(y=max(row - height // 3, 0), animate=False)
+ def align(self, row: int, screen_row: int) -> None:
+ """Scroll so ``row`` sits at viewport offset ``screen_row`` — keeps this
+ (companion) pane visually level with the driver's cursor in split view.
+ Clamps at the ends, so alignment is best-effort near the edges."""
+ top = max(0, min(row - max(screen_row, 0), max(self.total - 1, 0)))
+ if top != round(self.scroll_offset.y):
+ self.scroll_to(y=top, animate=False)
+
# -- navigation -------------------------------------------------------- #
def _visible_height(self) -> int:
return max(self.size.height, 1)
@@ -1597,6 +1605,13 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True
if line < top or line >= top + height:
self.scroll_to(y=max(line - height // 3, 0), animate=False)
+ def align(self, line: int, screen_row: int) -> None:
+ """Scroll so ``line`` sits at viewport offset ``screen_row`` — keeps this
+ (companion) pane visually level with the driver's cursor in split view."""
+ top = max(0, min(line - max(screen_row, 0), max(len(self._strips) - 1, 0)))
+ if top != round(self.scroll_offset.y):
+ self.scroll_to(y=top, animate=False)
+
def line_for_ea(self, ea: int) -> int | None:
"""The pseudocode line whose marker ea is the largest <= ``ea`` (the C
line that best covers an instruction address)."""
@@ -4741,7 +4756,9 @@ class IdaTui(App):
rows.add(r)
lst.set_link(rows)
if rows:
- lst.reveal(min(rows))
+ # keep the linked region level with the driver's cursor row so
+ # the eye tracks straight across the two panes
+ lst.align(min(rows), dec.cursor - round(dec.scroll_offset.y))
else: # the listing drives
lst.set_link(set())
ea = lst._cursor_ea()
@@ -4759,7 +4776,7 @@ class IdaTui(App):
line = dec.line_for_ea(ea)
if line is not None:
dec.set_link(line)
- dec.reveal(line)
+ dec.align(line, lst.cursor - round(lst.scroll_offset.y))
else:
dec.set_link(None)
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index 2476b21..d5f710e 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -428,6 +428,25 @@ async def s_split_view(c: Ctx):
dl is not None and lea is not None and dec._line_eas[dl] is not None
and dec._line_eas[dl] <= lea,
f"link_line={dl} lea={hex(lea) if lea else None}")
+ # the companion pane sits LEVEL with the driver's cursor (visual coherence):
+ # the linked row lands at the same viewport offset, not merely on-screen.
+ deep = [i for i, eas in enumerate(m) if eas][10:]
+ row = lst.model.ensure_ea(m[deep[0]][0]) if (deep and lst.model) else None
+ if row is not None and row > 12:
+ lst.scroll_to(y=row - 10, animate=False)
+ await c.pause(0.2) # let the deferred scroll land
+ lst.cursor = row # driver cursor now at viewport offset 10
+ app._sync_split("listing")
+ await c.pause(0.2) # let the companion's scroll land
+ drv = lst.cursor - round(lst.scroll_offset.y)
+ link, top = dec._link_line, round(dec.scroll_offset.y)
+ # exact, modulo the unavoidable clamps (can't scroll above line 0, nor
+ # past the end when the pseudocode is shorter than the viewport)
+ want = min(max(0, (link or 0) - drv),
+ max(0, dec.total - dec._visible_height()))
+ c.check("the companion pane sits level with the driver's cursor",
+ link is not None and top == want,
+ f"driver_row={drv} link={link} dec_top={top} want={want}")
await c.press("tab")
await c.pause(0.1)
c.check("Tab in split focuses the pseudocode pane", app._active == "decomp",