8000 Wheel forwarding sends one SGR report per terminal line, so one notch over-scrolls mouse-tracking apps by ~5x · Issue #245 · amirlehmam/wmux · GitHub
[go: up one dir, main page]

Skip to content

Wheel forwarding sends one SGR report per terminal line, so one notch over-scrolls mouse-tracking apps by ~5x #245

Closed

Description

@Mattheinrichs

What happens

In an alt-screen / mouse-tracking pane (opencode, Claude Code), one wheel detent scrolls far more than it does in a normal terminal. opencode moves its scroll_speed per wheel report (default 3 rows), so a single detent moves ~15-18 rows. At scroll_speed: 1 it moves 4-6 rows. A normal terminal moves ~scroll_speed rows per detent.

Why

writeWheelToPty loops one SGR report per line:

for (let i = 0; i < Math.abs(count); i++) window.wmux.pty.write(ptyId, seq);

where count = wheelDeltaToLines(...). A Windows detent is ~100px of deltaY, which at ~17-20px cell height is 5-6 lines, so one DOM wheel event becomes 5-6 app-level wheel reports.

xterm does the opposite: MouseService._sendEvent (case 'wheel') uses the line count only to decide whether this event crosses a line, then sends at most ONE report per wheel event. An app therefore sees one scroll step per detent, not one per line.

Impact

Users tune opencode's scroll_speed down to 0.25 to tame the wheel, which makes touch pan far too slow, since the touch stream (correctly, 1:1 per cell of finger travel) shares that multiplier. Measured on a ThinkPad Z16 Gen 2 (Windows 11):

  • default (scroll_speed 3): ~15-18 rows per detent
  • scroll_speed: 1: 4-6 rows per detent
  • scroll_speed: 0.25: 1-2 rows per detent (right for the wheel, touch unusably slow)

Suggested fix

In the mouse-tracking branch, emit one SGR report per DOM wheel event instead of one per line (keep the pixel/line accumulator and the remainder handling). The touch-synthesized stream should keep per-line reporting, since that is what makes pan track the finger; a source flag or ev.isTrusted can gate it. The arrow-key branch for non-mouse pagers (less/vim) should keep looping per line: 5-6 arrows per detent is correct there.

Not caused by the #243 touch work; the loop predates it.

Activity

  1. amirlehmam commented on Sep 19, 2026

    @amirlehmam
    Owner

    Confirmed, and fixed in 2.13.1 (9cc8fca). Your diagnosis was exactly right, including that #243 didn't cause it — the loop predates the touch work by a long way.

    What was wrong

    The two forwarding branches speak different units, and writeWheelToPty treated them as one:

    branch unit why
    SGR mouse report a step the app decides how far a step goes
    arrow keys a line an arrow is one line; nobody scales it

    Emitting Math.abs(count) reports is correct for the second and wrong for the first, because the app then multiplies on top. Your 5-6 lines per detent × opencode's scroll_speed 3 is the 15-18 rows you measured.

    So the mouse-report branch now sends one report per DOM wheel event, and the line count is demoted to a gate — "did this event cross a line at all?". That's what xterm does; I checked the pinned 6.0.0 bundle rather than going from the description, and in Terminal._bindMouse, case 'wheel', consumeWheelEvent() is only ever tested against 0 before a single triggerMouseEvent. The pixel/line accumulator and its sub-line remainder are untouched.

    One place I went the other way from your suggestion

    You said the arrow-key branch should keep looping per line. It does — but not for parity with xterm, because xterm doesn't do that either. That same grep turned up its non-mouse fallback, and it sends exactly one arrow per wheel event:

    const i = C0.ESC + (applicationCursorKeys ? "O" : "[") + (ev.deltaY < 0 ? "A" : "B");
    this.coreService.triggerDataEvent(i, true);

    Looping is therefore a deliberate divergence, and it needed an actual reason rather than "xterm does it". The reason is internal consistency: wmux's own scrollback branch moves lines rows per event, so one arrow per detent would make less in an alt-screen pane crawl at a fifth of the speed of the scrollback in the pane right beside it. #245 was the multiplier, and the arrow branch has none. Written down in the source so nobody "fixes" it back.

    Touch

    Kept per-line, as you asked — a finger is a position, not a detent, and deriving pan speed from a per-event report would tie it to the frame rate. Worth being honest about the limit: wmux can't know the app's rows-per-report multiplier, so one report per cell crossed is the closest it can get, and it's exactly 1:1 only where that multiplier is 1.

    The source rides on the synthetic event as a private Symbol rather than ev.isTrusted. Trust answers "who dispatched this", not "what gesture is this", so the next synthetic wheel from anywhere else would silently inherit touch semantics — and a real-Chromium harness confirmed both synthetic events read isTrusted: false, so trust isn't observable in a test either.

    What to expect

    You should be able to put scroll_speed back to 3. One detent → one report → ~3 rows, the same as any other terminal, and the pan keeps tracking your finger at that setting instead of needing 0.25.

    The decision is pure and pinned in tests/unit/wheel-forward.test.ts — the count is invisible in a screenshot and only reproduces on a touchscreen ThinkPad, so it isn't something I can eyeball here. Thanks for measuring it at three scroll_speed values; that's what made the multiplier obvious rather than the line count.

    2.13.1 is building now and will be on the releases page shortly. Please reopen if the wheel still overshoots on the Z16.

  2. Mattheinrichs commented on Sep 19, 2026

    @Mattheinrichs
    ContributorAuthor

    Confirmed on v2.13.1, ThinkPad Z16 Gen 2: no overshoot, one detent = scroll_speed rows (a consistent 3 at scroll_speed: 3). Wheel finally matches a normal terminal, and touch pan still works. Nice fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0