Skip to content

Fix mouse wheel-down not scrolling on legacy ncurses (fixes #722) - #883

Open
SamyakJ05 wants to merge 1 commit into
gyscos:mainfrom
SamyakJ05:fix/ncurses-wheel-down-722
Open

Fix mouse wheel-down not scrolling on legacy ncurses (fixes #722)#883
SamyakJ05 wants to merge 1 commit into
gyscos:mainfrom
SamyakJ05:fix/ncurses-wheel-down-722

Conversation

@SamyakJ05

Copy link
Copy Markdown

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 with NCURSES_MOUSE_VERSION >= 2 (configure.in ties 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() in lib_mouse.c explicitly downgrades a wheel-down event to a bare REPORT_MOUSE_POSITION bstate instead of BUTTON5_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; #endif in handle_wheel, and the earlier button > MAX_BUTTONS short-circuit in decode_X10_bstate() for the legacy X10 path). This makes a real wheel-down indistinguishable, at the bstate level, from an idle "mouse moved, no button held" report.

  • cursive's parse_mouse_event() (in both n.rs and pan.rs, which share this logic) already had code that looks like an attempt to handle exactly this ambiguity:

    self.last_mouse_button
        .map(MouseEvent::Hold)
        .or_else(|| {
            // In legacy mode, some buttons overlap,
            // so we need to disambiguate.
            (mevent.bstate == ncurses::BUTTON5_DOUBLE_CLICKED as mmask_t)
                .then_some(MouseEvent::WheelDown)
        })

    But this .or_else branch is dead code: it's nested inside the outer if mevent.bstate == REPORT_MOUSE_POSITION, so mevent.bstate is already known to equal REPORT_MOUSE_POSITION at that point. Comparing it against BUTTON5_DOUBLE_CLICKED can never be true — computing the actual bit values from ncurses' NCURSES_MOUSE_MASK(b,m) = m << ((b-1)*5) macro, BUTTON5_DOUBLE_CLICKED = 0x800000 while REPORT_MOUSE_POSITION = 0x10000000 — different bits entirely. So this branch never fires, and the code falls through to Hold(<last pressed button>) (or Unknown if 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 into last_mouse_button.map(Hold) and comes out as Hold(Middle) — never WheelDown.

What I changed

In both cursive/src/backends/curses/n.rs and cursive/src/backends/curses/pan.rs, replaced the dead disambiguation branch with a small pure function, disambiguate_position_report(last_button: Option<MouseButton>) -> Option<MouseEvent>:

  • If a button is currently held (last_button is Some), it's a drag/hold event for that button, same as before.
  • Otherwise (no button held), treat the bare REPORT_MOUSE_POSITION as WheelDown — 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_event itself needs a real ncurses::MEVENT/&mut self and 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 the puppet backend). 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-backend can't build here for reasons unrelated to this patch: the ncurses crate v6.0.1 itself fails to compile against the current stable Rust toolchain (cannot find value TRUE/OK/ERR in this scope inside the crate's own lib.rs/constants module) — I confirmed this is pre-existing by reproducing the identical failure on a clean checkout of main with the same feature flag, before any of my changes.

pancurses-backend, however, does build in this environment (it pulls in ncurses v5.101.0 instead, which is compatible), and since pan.rs contains the identical logic I changed in n.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.rs too since it's the same Rust with only ncurses:: vs pancurses:: 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.

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.
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.

[BUG] ScrollView not scrolling down

1 participant