Skip to content

Forward the terminal wheel to programs that own the mouse or the alternate screen - #363

Open
webmatze wants to merge 2 commits into
zeronsh:mainfrom
webmatze:fix/terminal-wheel-forwarding
Open

webmatze wants to merge 2 commits into
zeronsh:mainfrom
webmatze:fix/terminal-wheel-forwarding

Conversation

@webmatze

@webmatze webmatze commented Sep 14, 2026

Copy link
Copy Markdown

Summary

  • route wheel events in the terminal panel by the modes the running program set, in xterm's precedence: mouse reporting (DECSET 1000/1002/1003, encoded per 1006 SGR / 1005 UTF-8 / X10) → alternate screen with alternate-scroll (DECSET 1007, on by default) as cursor keys honoring DECCKM → the emulator's own scrollback as before
  • add Emulator::wheel_route, wheel_report_bytes, wheel_arrow_bytes, and a WheelAccumulator so trackpad sub-line pixel deltas add up to whole lines instead of rounding to nothing
  • keep Shift+wheel on the local scrollback while a program owns the mouse (reading the delta back from the X axis, where AppKit puts a physical mouse's Shift+wheel), route a tab whose program has exited to the scrollback regardless of the modes it left behind, and walk a mouse-mode program on the primary screen back to the live bottom before reporting (viewport coordinates mean nothing while scrolled into history)

Closes #361.

Root cause

on_scroll_wheel always called Term::scroll_display. A full-screen TUI in the alternate screen has no history to scroll, and one with mouse tracking on expects the wheel as input — Claude Code does both (?1049h, ?1000h, ?1006h), so the wheel was dead inside it while a native terminal scrolls fine.

Out of scope

  • Click/drag reporting (MOUSE_REPORT_CLICK, MOUSE_DRAG, MOUSE_MOTION): the same encoder applies, but clicks currently drive text selection and need their own design.

Testing

  • cargo test -p zeron-ui terminal:: (67 passed, 9 new: mode routing over real escape sequences, the Shift/exited/history overrides, pointer → placement → SGR/X10 report, Shift axis fallback, SGR/X10/UTF-8 encoding incl. the 223/2015 caps, arrow bytes, accumulator)
  • cargo test -p zeron-ui (967 passed)
  • rustfmt --edition 2024 --check crates/ui/src/terminal/view.rs crates/ui/src/terminal/emulator.rs; panel.rs carries one pre-existing on_terminal_hover hunk from main that this change leaves untouched
  • cargo clippy -p zeron-ui --all-targets: no findings under crates/ui/src/terminal/
  • macOS, scripts/run-macos-dev.sh: wheel and trackpad scroll the conversation inside claude in the right-sidebar terminal; a plain shell scrolls its scrollback as before

…rnate screen

The panel's wheel handler always scrolled the emulator's own history, so a
full-screen TUI got nothing: Claude Code runs in the alternate screen with
SGR mouse tracking on, and the wheel was dead inside it (zeronsh#361). A native
terminal reports the wheel to such a program instead.

`Emulator::wheel_route` reads the modes the program set and picks one of
three destinations, in xterm's precedence: mouse reporting (DECSET
1000/1002/1003, encoded per 1006/1005/X10), alternate screen with
alternate-scroll (1007, on by default) as cursor keys honoring DECCKM, or
the scrollback as before. The panel encodes with `wheel_report_bytes` /
`wheel_arrow_bytes` and writes through the existing input coalescer.
Shift+wheel always reaches the scrollback, and a mouse-mode program on the
primary screen gets no report while the view is scrolled into history —
the wheel walks back to the live bottom first, where the reported
coordinates mean something.

Wheel lines now pass through a `WheelAccumulator` on every route: a
trackpad's sub-line pixel deltas used to round to nothing, and a partial
line must not leak from the scrollback into a stray key press when the
route changes.
…scrollback

Review follow-ups on the wheel forwarding:

- `wheel_lines` reads a Shift+wheel back from the X axis when Y is empty:
  AppKit moves a physical mouse's Shift+wheel there (that is how horizontal
  scrolling works for mice) and gpui passes the raw deltas through, so the
  "Shift always reaches the scrollback" rule was a silent no-op on macOS.
- A tab whose program has exited routes to the scrollback regardless of
  the modes it left behind; `queue_input` already dropped the bytes, which
  left the wheel doing nothing.
- `effective_wheel_route` gathers the three overrides (Shift, exited,
  scrolled into history under mouse mode) into one pure function with its
  own tests, instead of a closure inside the gpui listener.
- `GridGeometry::cell_at` replaces the six-argument `cell_at` call that
  `grid_point_at` and the wheel handler both spelled out, and gets a test
  that follows a pointer through the placement into an SGR/X10 report.
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.

Terminal panel does not forward mouse wheel to the running program (no scrolling inside Claude Code)

1 participant