Repository navigation
Description
Summary
A wheel event that arrives while a mouse button is held is decoded as drag (SGR) or move (legacy), with scroll: undefined. Any gesture of the form "hold the button and scroll" — extending a selection, dragging a scrollbar, drag-scrolling a viewport — therefore produces no scroll event at all, so the host never scrolls. The wheel is not ignored: it is reported as pointer motion.
Where
packages/core/src/lib/parse.mouse.ts, decodeSgrEvent:
const button = rawButtonCode & 3
const isScroll = (rawButtonCode & 64) !== 0
const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button]
const isMotion = (rawButtonCode & 32) !== 0
…
let type: MouseEventType
let scrollInfo: ScrollInfo | undefined
if (isMotion) {
const isDragging = this.mouseButtonsPressed.size > 0
if (button === 3) {
type = "move"
} else if (isDragging) {
type = "drag"
} else {
type = "move"
}
} else if (isScroll && pressRelease === "M") {
type = "scroll"
scrollInfo = { direction: scrollDirection!, delta: 1 }
}The motion branch is tested first, so the scroll branch is only reachable when the motion bit is clear — i.e. only when no button is held.
SGR encodes the wheel in bits 6 (wheel) and the held button plus bit 5 (motion) in the same Cb byte:
| Gesture | Cb |
isScroll |
isMotion |
decoded |
|---|---|---|---|---|
| wheel up, nothing held | 64 | true | false | scroll, direction up ✔ |
| wheel up, left button held | 96 | true | true | drag, scroll: undefined ✘ |
decodeBasicEvent has the same ordering (if (isMotion) → "move" before else if (isScroll) → "scroll"), so the legacy path drops the wheel as well.
Expected Behavior
A wheel event reports type: "scroll" with a direction whether or not a button is held. Held buttons are already tracked separately in mouseButtonsPressed, and the scroll branch deliberately does not add to it, so reordering does not disturb drag tracking.
Reproduction
Unit level, no terminal needed — mirror the table above through MouseParser:
const parser = new MouseParser()
// wheel up, no button held
parser.parse("\x1b[<64;10;5M") // → { type: "scroll", scroll: { direction: "up" } }
// wheel up, left button held (Cb = 64 | 32)
parser.parse("\x1b[<96;10;5M") // → { type: "drag", scroll: undefined } ← expected "scroll"
// wheel down, left button held
parser.parse("\x1b[<97;10;5M") // → { type: "drag", scroll: undefined } ← expected "scroll", downEnd to end, in a host with a scrollable embedded terminal: hold the left button on the pane's text and scroll the wheel. The viewport does not move, where a plain wheel with no button held scrolls it (opencode TUI terminal pane reproduces this; reported there as anomalyco/opencode#61053, where the visible effect is a selection that can never extend past one screen).
Fix
Test the scroll bit before motion in both decoders:
if (isScroll && pressRelease === "M") {
type = "scroll"
scrollInfo = { direction: scrollDirection!, delta: 1 }
} else if (isMotion) {
…
}That restores wheel events during a drag. Whether a drag should also autoscroll at the viewport edges is a separate, host-level concern; this issue is only the dropped event.
Impact
Any OpenTUI host that scrolls on wheel — embedded terminals, scrollboxes, anything with onMouseScroll — cannot be scrolled while the pointer is dragging, which is exactly when a user tries to extend a selection past the visible area. Not specific to one app.
Activity
mleivaDesarrollos commentedon Nov 2, 2026
Correction to my own proposal, before anyone spends time on it: "test
isScrollbeforeisMotion" would regress #830 / #661.git logonparse.mouse.ts:That change deliberately prefers motion when both bits are set, because URxvt sends packets with the motion and wheel bits together while the pointer moves during a scroll — treating them as wheel input turned small movements into extra scroll and made the screen jump (#661). The ordering is intentional, so a plain swap is not the fix.
One signal separates the two cases: whether a button is currently held.
draggingfalse,isMotiontrue → motion wins, Mouse moves translate to mouse scrolling under URxvt #661 stays fixed.Cb = 64 | 32 | held button) →draggingtrue → scroll, so a host can move its viewport mid-drag.The same shape applies to
decodeBasicEvent, which drops the wheel for the same reason (if (isMotion) → "move"beforeelse if (isScroll)); it has no drag/move distinction, so there the test is simply whether a button bit is set on the packet.Trade-off that is yours to make: URxvt scrolling while a button is held would then report scroll instead of motion. I would defend that — the user is dragging and scrolling at once — but it is a deliberate narrowing of the #830 heuristic rather than a pure reorder, and it is fair to want the wheel to win only when a button is held.
The repro table in the body still stands; only the proposed fix changes.
mleivaDesarrollos commentedon Nov 2, 2026
Confirmed against a released build rather than a source reading: opencode 2.2.5 (upgraded from 2.1.4) reproduces both ways — wheel during a drag produces no scroll, and dragging to the pane's top edge does not scroll the viewport. The packaged binary still carries this exact branch (
mouseButtonsPressed.size > 0appears in the shipped bundle), so the code path described above is the one in use.The edge-autoscroll half is host-level and tracked separately (anomalyco/opencode#61053); this issue is only the dropped wheel event.
native: drag the held button in URxvt motion after a wheel tick (#1917)
mleivaDesarrollos commentedon Nov 4, 2026
Retracting the diagnosis in this issue, on evidence rather than argument. I captured the raw stdin of the failing gesture (
OTUI_STDIN_LOG/script -I) in WezTerm on opencode 2.2.5, and the TypeScript parser is not dropping the wheel.The gesture was: press at the bottom row of the pane, drag upward past the top edge, then wheel while still holding. On the wire:
120 mouse packets, 18 with the wheel bit, zero with both the wheel and motion bits set. So
isMotionis false on every wheel tick,decodeSgrEventtakes theisScroll && pressRelease === "M"branch and returnsscroll— the branch order from #830 is not implicated here, and no change toparse.mouse.tswould alter the outcome for this terminal.What the capture does show is that the gesture leaves the renderable: the drag's y is clamped at 1 (over the tab strip, outside the pane) and the wheel ticks arrive at y=6–8, at the pane's top edge. That points at two host-side defects rather than a decoder one:
renderer.tsdispatches a scroll tohitTest(x, y), not to the renderable that ownscurrentSelection. During a selection drag the pointer is at or outside the pane's edge, so the wheel does not reach the pane that owns the selection.Both are tracked in anomalyco/opencode#61053. #1917's native-parser fix still stands on its own for the URxvt case (motion-with-button
96being decoded as a buttonless move); it just is not the cause of my report, and I would rather it not stay attributed to it. Happy to close this once that's agreed.simonklee commentedon Nov 9, 2026
Terminals send wheel events during a button drag as SGR codes 64 through 67 without the motion bit (32). Both
MouseParserand the native input parser decode codes 64 through 67 asscrollevents during a drag. URxvt sends SGR codes 96 and 97 for pointer motion after a wheel event. OpenTUI tests the motion bit before the wheel bit so URxvt pointer motion does not produce extra scroll events. #1917 updates the native input parser to reportdragfor codes 96 and 97 during a drag. Issue #1930 tracks selection scroll behavior when a drag leaves a renderable. Close this because the input parsers do not drop wheel events during a drag.