aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-24 15:48:19 +0200
committerblasty <blasty@local>2026-07-24 15:48:19 +0200
commit4975a92c338819b43cb6647d7bbb9197c5772f32 (patch)
treed36a08551940cc25643bab5867d741e1862e8007
parenthex: click to place the byte cursor (hex + ascii panes) (diff)
downloadida-tui-4975a92c338819b43cb6647d7bbb9197c5772f32.tar.gz
ida-tui-4975a92c338819b43cb6647d7bbb9197c5772f32.tar.xz
ida-tui-4975a92c338819b43cb6647d7bbb9197c5772f32.zip
hex: cursor follows the scroll (stays visible on wheel/scrollbar)
Override watch_scroll_y so a viewport scroll drags the byte cursor along: when the rounded scroll changes, clamp the cursor's row into the visible range [top, top+height) keeping its column, so it rides the nearest edge instead of being left off-screen. No feedback loop -- key-nav's _scroll_to_cursor already puts the cursor in view (clamp is then a no-op), and HexView.Moved only updates the status line. Pairs with the click-to-place support. Pilot `hex` scenario updated: after a scroll the cursor must now be within the visible rows (was: asserted it stayed put). 9/9 hex checks pass.
-rw-r--r--idatui/app.py20
-rw-r--r--tests/test_scenarios.py9
2 files changed, 25 insertions, 4 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 5314f66..3b84217 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -1680,6 +1680,26 @@ class HexView(ScrollView, can_focus=True):
top = max(0, min(top, max(self.total - 1, 0)))
self._apply_scroll(top)
+ def watch_scroll_y(self, old_value: float, new_value: float) -> None:
+ super().watch_scroll_y(old_value, new_value)
+ # Follow the scroll: a wheel-scroll / scrollbar drag drags the byte cursor
+ # along so it stays visible (pinned to the nearest edge row, same column).
+ if round(old_value) != round(new_value):
+ self._follow_scroll(round(new_value))
+
+ def _follow_scroll(self, top: int) -> None:
+ if self.model is None or self.model.size == 0:
+ return
+ height = self._visible_height()
+ row, col = self.cursor // 16, self.cursor % 16
+ new_row = min(max(row, top), top + max(height - 1, 0))
+ new_row = max(0, min(new_row, max(self.total - 1, 0)))
+ new_cursor = max(0, min(new_row * 16 + col, self.model.size - 1))
+ if new_cursor != self.cursor:
+ self.cursor = new_cursor
+ self.refresh() # cursor is reactive(repaint=False)
+ self.post_message(HexView.Moved(self.cursor_va()))
+
# -- navigation -------------------------------------------------------- #
def _move(self, delta: int) -> None:
if self.model is None or self.model.size == 0:
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index 92c6315..4731a16 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -511,12 +511,13 @@ async def s_hex(c: Ctx):
await c.type(hex(rng[0]))
await c.press("enter")
await c.wait(lambda: hx.cursor_va() == rng[0], 10)
- hx.scroll_to(y=40, animate=False) # wheel-style scroll: viewport only
+ hx.scroll_to(y=40, animate=False) # wheel-style scroll
await c.pause(0.15)
top = round(hx.scroll_offset.y)
- c.check("hex viewport scrolls without dragging the cursor along",
- top >= 20 and (hx.cursor // 16) < top,
- f"top={top} cursor_row={hx.cursor // 16}")
+ height = hx._visible_height()
+ c.check("hex cursor follows the scroll (stays visible)",
+ top >= 20 and top <= (hx.cursor // 16) < top + height,
+ f"top={top} cursor_row={hx.cursor // 16} height={height}")
PAD = 1 # HexView { padding: 0 1 } -> content is inset one col
await c.pilot.click(HexView, offset=(PAD + 19 + 3 * 3, 5)) # hex byte 3, row 5
await c.pause(0.1)