Skip to content

feat(client): pixel-smooth scrollback scrolling - #47

Merged
jesse23 merged 4 commits into
mainfrom
feat/client-smooth-scroll
Oct 13, 2026
Merged

jesse23 merged 4 commits into
mainfrom
feat/client-smooth-scroll

Conversation

@jesse23

@jesse23 jesse23 commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Description

Trackpad scrolling stepped a whole line at a time behind a 100 ms easing animation, and in mouse-tracking apps it ran far ahead of the fingers.

Scrollback (normal screen, no mouse tracking). ghostty-web's viewport is whole lines only. src/client/smooth-scroll.ts keeps the scroll position as fractional lines and takes over wheel events:

  • The whole part goes to ghostty-web as viewportY (scrollToLine), rendered as usual.
  • The fraction becomes a device-pixel-snapped CSS offset: the canvas is translated down and its bottom edge clipped, and a one-row strip above it shows the scrollback line sliding in. The strip is drawn with ghostty-web's own renderLine (renderer context swapped for the strip's), so it matches the canvas.
  • At a whole-line position there is no transform and the strip is hidden, so a resting terminal renders as before.
  • ghostty-web's own viewport moves (output snapping to the bottom, keyboard scrolling, scrollbar drag, resize) reset the position.

Mouse-tracking apps (herdr, Claude Code fullscreen, vim mouse=a). These scroll their own view in whole lines, so no pixel offset is possible. The SGR wheel handler sent at least one tick per wheel event, and a trackpad fires many small events plus momentum. Pixel deltas now count by distance: one tick per cell height scrolled, 1:1 with the OS like native macOS scrolling. mouseScrollSpeed no longer applies to pixel deltas (Ghostty likewise skips its multiplier for precision scrolls); it still scales line/page deltas.

Builds on #46 (uses its whole-ratio renderer DPR for the strip).

Verification

  • bun run build
  • bun test (441 passed, including the new smooth-scroll.test.ts)
  • bun run lint
  • In Chrome (DPR 2), synthetic pixel wheel events: a 6px offset renders crisply with the incoming line joined seamlessly at the top; scrolling to the bottom, new output while partly scrolled, and a viewport resize all reset the offset; less on the alternate screen is unaffected.
  • Mouse-tracking app (cat -v with modes 1002/1006): 20 × 2px wheel events sent 4 SGR ticks with distance counting and mouseScrollSpeed: 1.5 (14px cells), instead of 20. With the multiplier dropped for pixel deltas, the same input gives 2 (not re-measured in the browser).
  • Manually checked on a MacBook trackpad, in plain scrollback and in herdr: scrolling is smooth, and select-to-copy still works. Feedback that herdr felt slightly faster than native led to dropping mouseScrollSpeed for pixel deltas.
  • Known limits while at a fractional position: the overlay scrollbar shifts by under a line, and Kitty graphics placements are not offset.

ADRs

Base automatically changed from fix/client-dpr-change to main October 13, 2026 00:52
jesse23 added 4 commits October 12, 2026 20:53
Keep the scroll position as fractional lines: ghostty-web renders the
whole lines, the fraction becomes a device-pixel CSS offset on the
canvas plus a one-row strip for the line sliding in above it. Normal
screen only. See ADR 040.
Pixel wheel deltas send one SGR scroll tick per cell height scrolled
(times mouseScrollSpeed) instead of one per event, so trackpad scrolling
in herdr, Claude Code and vim tracks the fingers. See ADR 040.
mouseScrollSpeed no longer multiplies pixel wheel deltas, so trackpad
scrolling in herdr and Claude Code moves one line per cell height of
travel, like native macOS scrolling and Ghostty's precision scrolling.
It still scales line/page wheel deltas. See ADR 040.
@jesse23
jesse23 force-pushed the feat/client-smooth-scroll branch from f5f484f to eeaad03 Compare October 13, 2026 00:54
@jesse23
jesse23 merged commit 157650c into main Oct 13, 2026
3 checks passed
@jesse23
jesse23 deleted the feat/client-smooth-scroll branch October 13, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant