Fix mouse wheel-down not scrolling on legacy ncurses (fixes #722) - #883
Open
SamyakJ05 wants to merge 1 commit into
Open
Fix mouse wheel-down not scrolling on legacy ncurses (fixes #722)#883SamyakJ05 wants to merge 1 commit into
SamyakJ05 wants to merge 1 commit into
Conversation
On ncurses builds where NCURSES_MOUSE_VERSION < 2 (a 5th mouse button isn't supported), including the system ncurses 5.x that macOS ships, libncurses cannot report BUTTON5_PRESSED for a wheel-down scroll at all. Its own lib_mouse.c deliberately downgrades that event to a bare REPORT_MOUSE_POSITION bstate (see handle_wheel() and the `button > MAX_BUTTONS` check in decode_X10_bstate()), which is otherwise used for idle mouse-move/drag reports. cursive's parse_mouse_event() already had logic meant to disambiguate this case: it fell back to last_mouse_button.map(Hold), then tried to special-case bstate == BUTTON5_DOUBLE_CLICKED as WheelDown. But that special case was dead code - it lived inside the branch where bstate == REPORT_MOUSE_POSITION was already known true, and BUTTON5_DOUBLE_CLICKED and REPORT_MOUSE_POSITION are different bits, so the comparison could never succeed. In practice this meant a legacy-mode wheel-down event fell through to Hold(<last button>) (matching the "middle button press/hold" misparse reported in the issue) or Event::Unknown, and never WheelDown. Replace that dead-code branch with disambiguate_position_report(): when no button is currently held, treat a bare REPORT_MOUSE_POSITION as WheelDown (the only interpretation of that value that isn't already handled by the Hold(...) case for a held button). Applied identically to the ncurses and pancurses backends, which share this exact logic. Extracted the check into a small pure function and added unit tests, since the surrounding code all requires a live ncurses session.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #722
Root cause
I traced this through ncurses' own
lib_mouse.c(from the ncurses mirror):ncurses only defines
BUTTON5_PRESSED(wheel down) when it's built withNCURSES_MOUSE_VERSION >= 2(configure.inties this to ABI 6+). On builds where the ABI is older — notably the ncurses 5.x that macOS ships as its system library (the reporter's environment, macOS Monterey) — a 5th mouse button doesn't exist in that ABI at all.On those builds,
handle_wheel()inlib_mouse.cexplicitly downgrades a wheel-down event to a bareREPORT_MOUSE_POSITIONbstate instead ofBUTTON5_PRESSED(see the#if NCURSES_MOUSE_VERSION >= 2 ... #else /* Ignore this event as it is not a true press of the button */ eventp->bstate = REPORT_MOUSE_POSITION; #endifinhandle_wheel, and the earlierbutton > MAX_BUTTONSshort-circuit indecode_X10_bstate()for the legacy X10 path). This makes a real wheel-down indistinguishable, at thebstatelevel, from an idle "mouse moved, no button held" report.cursive'sparse_mouse_event()(in bothn.rsandpan.rs, which share this logic) already had code that looks like an attempt to handle exactly this ambiguity:But this
.or_elsebranch is dead code: it's nested inside the outerif mevent.bstate == REPORT_MOUSE_POSITION, somevent.bstateis already known to equalREPORT_MOUSE_POSITIONat that point. Comparing it againstBUTTON5_DOUBLE_CLICKEDcan never be true — computing the actual bit values from ncurses'NCURSES_MOUSE_MASK(b,m) = m << ((b-1)*5)macro,BUTTON5_DOUBLE_CLICKED = 0x800000whileREPORT_MOUSE_POSITION = 0x10000000— different bits entirely. So this branch never fires, and the code falls through toHold(<last pressed button>)(orUnknownif nothing was pressed yet).This matches the maintainer's own diagnosis in the issue thread exactly: "Running the key_codes event shows it is parsed as middle button press/hold events." If the user's last real button press happened to register as
Middle, a subsequent legacy-mode wheel-down report falls intolast_mouse_button.map(Hold)and comes out asHold(Middle)— neverWheelDown.What I changed
In both
cursive/src/backends/curses/n.rsandcursive/src/backends/curses/pan.rs, replaced the dead disambiguation branch with a small pure function,disambiguate_position_report(last_button: Option<MouseButton>) -> Option<MouseEvent>:last_buttonisSome), it's a drag/hold event for that button, same as before.REPORT_MOUSE_POSITIONasWheelDown— the only interpretation left once "held-button drag" is ruled out, and consistent with what the original (broken) check was clearly trying to express.I applied the same fix to both backends since they contain effectively identical logic (and identical dead code).
Tests
I extracted the check into a standalone pure function specifically so it could be unit tested without a live terminal —
parse_mouse_eventitself needs a realncurses::MEVENT/&mut selfand can't be tested in isolation, and there were no pre-existing tests for the ncurses/pancurses mouse parsing at all (the only existing backend tests are for thepuppetbackend). Added two#[cfg(test)]tests per file covering the no-held-button (WheelDown) and held-button (Hold) cases.Verification — what I could and couldn't check
This sandbox has no system ncurses headers, so
ncurses-backendcan't build here for reasons unrelated to this patch: thencursescrate v6.0.1 itself fails to compile against the current stable Rust toolchain (cannot find value TRUE/OK/ERR in this scopeinside the crate's ownlib.rs/constantsmodule) — I confirmed this is pre-existing by reproducing the identical failure on a clean checkout ofmainwith the same feature flag, before any of my changes.pancurses-backend, however, does build in this environment (it pulls inncursesv5.101.0 instead, which is compatible), and sincepan.rscontains the identical logic I changed inn.rs, I was able to get real compiler and test feedback there:cargo test --no-default-features --features pancurses-backend— builds cleanly, all tests pass, including the two new ones.cargo clippy --no-default-features --features pancurses-backend— no warnings.cargo fmt --check— no formatting diff introduced by this change.cargo test(default features / crossterm backend) and the full workspace test suite — all 144+ tests pass, unaffected as expected.So: the new unit tests for the shared disambiguation logic are genuinely verified (compiled and passing) via the pancurses backend, and I'm confident the code compiles for
n.rstoo since it's the same Rust with onlyncurses::vspancurses::symbol names swapped. What I could not verify is live runtime behavior against a real terminal emulator with an actual legacy ncurses 5.x build — I don't have hardware/terminal access to reproduce the original bug interactively. If a maintainer can test on macOS (or another environment linking ncurses < 6/ABI 5), that would be the ideal confirmation. I'm confident in the root-cause analysis (it's grounded directly in ncurses' own C source, not guesswork), moderately-to-highly confident in the fix given the passing pancurses-backend tests, but flagging honestly that this hasn't been confirmed against real wheel-scroll input on the affected hardware.