aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-09 15:25:02 +0200
committerblasty <blasty@local>2026-07-09 15:25:02 +0200
commite3636217535a5ff93f9b448cb1998ed010bfbbba (patch)
treef0654dc2d657711ac0f2cbfe36eae1cb4ed6f82b
parentsearch: land the cursor on the match's starting column (diff)
downloadida-tui-e3636217535a5ff93f9b448cb1998ed010bfbbba.tar.gz
ida-tui-e3636217535a5ff93f9b448cb1998ed010bfbbba.tar.xz
ida-tui-e3636217535a5ff93f9b448cb1998ed010bfbbba.zip
paging: preserve the cursor's viewport-relative row (IDA-style)
PageUp/Down and Ctrl+U/D now move the scroll top and the cursor by the same (clamped) delta, so the cursor keeps its relative screen row instead of snapping to an edge; at the very top/bottom the cursor jumps to the extreme. Shared _page_scroll in ColumnCursor, used by both views. pilot suite 49/49.
-rw-r--r--idatui/app.py37
-rw-r--r--tests/test_tui.py21
2 files changed, 46 insertions, 12 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 497f0ac..87420c2 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -301,6 +301,31 @@ class ColumnCursor:
if event.chain >= 2: # double-click == place cursor + follow
self.post_message(FollowRequested(self))
+ # -- paging (keeps the cursor at the same viewport-relative row) ------- #
+ def _page_scroll(self, lines: int) -> None:
+ total = self.total
+ if total <= 0:
+ return
+ height = self._visible_height()
+ top = round(self.scroll_offset.y)
+ max_top = max(total - height, 0)
+ new_top = max(0, min(max_top, top + lines))
+ delta = new_top - top
+ if delta == 0: # already at an edge: jump the cursor to the extreme
+ self.cursor = (total - 1) if lines > 0 else 0
+ else: # move cursor by the same delta -> relative row preserved
+ self.cursor = max(0, min(total - 1, self.cursor + delta))
+ self._clamp_x()
+ self.scroll_to(y=new_top, animate=False)
+ self.refresh()
+ self._after_cursor_move()
+
+ def action_page(self, direction: int) -> None:
+ self._page_scroll(direction * self._visible_height())
+
+ def action_half_page(self, direction: int) -> None:
+ self._page_scroll(direction * (self._visible_height() // 2))
+
class SearchMixin:
"""Vim-style in-view search shared by the disasm and pseudocode views.
@@ -698,12 +723,6 @@ class DisasmView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True
def action_cursor_up(self) -> None:
self._move(-1)
- def action_half_page(self, direction: int) -> None:
- self._move(direction * (self._visible_height() // 2))
-
- def action_page(self, direction: int) -> None:
- self._move(direction * self._visible_height())
-
def action_goto_top(self) -> None:
self._move(-self.total)
@@ -869,12 +888,6 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True
def action_cursor_up(self) -> None:
self._move(-1)
- def action_half_page(self, direction: int) -> None:
- self._move(direction * (self._visible_height() // 2))
-
- def action_page(self, direction: int) -> None:
- self._move(direction * self._visible_height())
-
def action_goto_top(self) -> None:
self._move(-len(self._strips))
diff --git a/tests/test_tui.py b/tests/test_tui.py
index 688df57..47c5a67 100644
--- a/tests/test_tui.py
+++ b/tests/test_tui.py
@@ -501,6 +501,27 @@ async def run(db):
round(dis.scroll_offset.y) == want_sy and dis.cursor == want_cur,
f"scroll={round(dis.scroll_offset.y)} (want {want_sy}) cursor={dis.cursor}")
+ # PageUp/Down keep the cursor at the same viewport-relative row.
+ await goto_name(fa.name)
+ await wait_until(pilot, lambda: dis.total > 100 and app._cur.ea == fa.addr, timeout=20)
+ if app._active != "disasm":
+ await pilot.press("tab")
+ await pilot.pause(0.2)
+ for _ in range(6):
+ await pilot.press("j")
+ await pilot.pause(0.1)
+ rel = dis.cursor - round(dis.scroll_offset.y)
+ await pilot.press("pagedown")
+ await pilot.pause(0.1)
+ check("PageDown preserves the viewport-relative row",
+ dis.cursor - round(dis.scroll_offset.y) == rel,
+ f"rel={dis.cursor - round(dis.scroll_offset.y)} want={rel}")
+ await pilot.press("pageup")
+ await pilot.pause(0.1)
+ check("PageUp preserves the viewport-relative row",
+ dis.cursor - round(dis.scroll_offset.y) == rel,
+ f"rel={dis.cursor - round(dis.scroll_offset.y)} want={rel}")
+
def main(argv):
db = None