Skip to content

Conversation

@joshka-oai
Copy link

When an app enables mouse reporting, Ghostty reports wheel scrolling as repeated mouse button events (4/5 vertical, 6/7 horizontal). Previously, discrete scroll input could be expanded using a delta that already incorporated both OS-provided tick magnitude (sometimes >1 per detent) and Ghostty’s discrete multiplier, leading to too many events (e.g. 3×3 => 9) and overly-fast scrolling in TUIs.

Change discrete mouse reporting to emit a stable number of wheel events per scroll callback per axis: round(mouse_scroll_multiplier.discrete) (min 1), using only the sign of xoff/yoff and ignoring raw tick magnitude. Precision scroll reporting is unchanged.

Refs:


Credit to @ownia for starting this work in PR #8907. I did some things a bit differently though

  • removed the config and made this more correct without the need to configure anything
  • made the discrete multiplier actually the number of events still, just not doubly applied

AI Disclosure: I used Codex for writing this code (Seeded with a bunch of context of the previous commits, PRs, issues, discussions and review comments related to mouse wheel scrolling). I have checked all the code for reasonableness (though I am a zig n00b).

I tested this on macOS Sequoia 15.7.3 using codex --enable tui2 (wip - lots still to do there - not ready for real testing), which uses DEC mode 1006 (mouse SGR). Tested with trackpad and logitech m720 mouse. I don't have a native linux machine to check this on. I was previously seeing big jumps of 9 lines when using the mouse scroll wheel instead of 3 like most other terminals. After I saw this was being handle more correctly for that case (as well as the more simple output of printf '\e[?1000h\e[?1006h'; cat -v)

Hoping that this is reasonably simple enough to test and merge without stalling like the previous PR.
Feel free to ping me either here or the discord - happy to make changes as necessary.
@rockorager I wonder if you have perspective on this?

When an app enables mouse reporting, Ghostty reports wheel scrolling as repeated
mouse button events (4/5 vertical, 6/7 horizontal). Previously, discrete scroll
input could be expanded using a delta that already incorporated both OS-provided
tick magnitude (sometimes >1 per detent) and Ghostty’s discrete multiplier,
leading to too many events (e.g. 3×3 => 9) and overly-fast scrolling in TUIs.

Change discrete mouse reporting to emit a stable number of wheel events per
scroll callback per axis: round(mouse_scroll_multiplier.discrete) (min 1),
using only the sign of xoff/yoff and ignoring raw tick magnitude. Precision
scroll reporting is unchanged.

Refs:
- ghostty-org#8875
- ghostty-org#8907
@joshka-oai joshka-oai requested a review from a team as a code owner December 22, 2025 02:56
@MithicSpirit
Copy link

I have tried this, but the behavior is still bothersome to me, in that the scroll distance in neovim must always be an integer multiple of the scroll distance in the scrollback (which does not match behavior in other terminals like kitty). I agree that this is a good step forward, however, and it would be nice to at least have this merged in the meantime until a general solution is found.

@joshka-oai
Copy link
Author

must always be an integer multiple of the scroll distance in the scrollback

Can you explain this a bit differently. I'm not quite sure I understand what this means.

(also, closing the loop on a little bit of chatter on discord about this: https://discord.com/channels/1005603569187160125/1005603569711452192/1453287529557000387)

@MithicSpirit
Copy link

must always be an integer multiple of the scroll distance in the scrollback

Can you explain this a bit differently. I'm not quite sure I understand what this means.

This PR makes it so the number of wheel events sent to Neovim is exactly the mouse-scroll-multiplier. In Neovim, I can only configure it so it scrolls an integer number of lines per wheel event (the ver: field in the 'mousescroll' option). As such, the number of lines scrolled in Neovim must be an integer multiple of mouse-scroll-multiplier, which is the number of lines scrolled in the Ghostty scrollback.

This is a problem for me, because I like to have a larger scroll distance in the scrollback than in Neovim, which is impossible in this case (except for the degenerate case of zero scrolling).

I think that the only way to fix this would be to decouple the number of wheel events sent from the number of lines scrolled, which imo feels like "the right thing" (side note: I would also like to decouple the number of lines scrolled from how much GTK thinks should be scrolled, but that is very out of scope for this and is probably not a popular sentiment). I believe this is basically what #8907 did when mouse-scroll-single-event = true, and I've reimplemented that in a quick-and-dirty patch that I use when compiling Ghostty locally from tip.

@joshka-oai
Copy link
Author

Maybe the solution is to separate scroll multipliers for non mouse capture mode (precision / non precision) as well as specific ones for mouse capture mode. The former are effectively distance multipliers while the latter are event multipliers, so I can see why they'd be useful to keep about the same.

Does setting the discrete multiplier but leaving the precision multiplier fix your issue, or do you still need to have the extra config so that non mouse mode scrolls the amount it currently does.

The context on why I want the terminals to be approximately similar as an app developer is so that we can do reasonable things with events. See https://github.com/openai/codex/blob/main/codex-rs/tui2/docs/scroll_input_model.md for more.

But in as a general rule, the idea here is that the amount that the trackpad scrolls and the amount that the mouse wheel scrolls should be pretty similar (in a default configuration at least - if you want it different, then that's fine). That's because there's now way to determine whether the events coming from a trackpad or a mouse wheel event. We can scale specific terminals by the number of events (so if you want a terminal that to returns 1 for a wheel, then we just scale by 3 for that setting so it feels right). Ghostty was the one terminal where I couldn't easily come up with an approach that worked well for both mouse and keyboard events. Most others I was able to heuristically do so (or they were similar enough that it didn't matter).

P.s. I don't have a Linux machine to test this on. I could VM it, but I'd still have to wonder whether the VM passes through the right event fidelity. Thanks for testing there. Would be good to hear your direct observations on the behavior.

@MithicSpirit
Copy link

Maybe the solution is to separate scroll multipliers for non mouse capture mode (precision / non precision) as well as specific ones for mouse capture mode. The former are effectively distance multipliers while the latter are event multipliers, so I can see why they'd be useful to keep about the same.

Does setting the discrete multiplier but leaving the precision multiplier fix your issue, or do you still need to have the extra config so that non mouse mode scrolls the amount it currently does.

I think it makes sense to keep them about the same for precision, where you can just scroll less if you want a single event. In the case of discrete scrolling (which is what I use), it's important to be able to send individual events, which is currently only possible (with or without this PR) by setting the discrete multiplier to 1.

The context on why I want the terminals to be approximately similar as an app developer is so that we can do reasonable things with events. See openai/codex@main/codex-rs/tui2/docs/scroll_input_model.md for more.

I haven't seen this before, but I agree with the "Goals" (although the number of lines should be configurable from context, with my editor being only a couple lines and more in the scrollback buffer).

But in as a general rule, the idea here is that the amount that the trackpad scrolls and the amount that the mouse wheel scrolls should be pretty similar (in a default configuration at least - if you want it different, then that's fine). That's because there's now way to determine whether the events coming from a trackpad or a mouse wheel event. We can scale specific terminals by the number of events (so if you want a terminal that to returns 1 for a wheel, then we just scale by 3 for that setting so it feels right). Ghostty was the one terminal where I couldn't easily come up with an approach that worked well for both mouse and keyboard events. Most others I was able to heuristically do so (or they were similar enough that it didn't matter).

I think that makes sense for the most part. Fundamentally, what I think should happen is that Ghostty has some notion of "how much scrolling is one unit" (in the case of a mouse this should be one notch by default, and in the case of a trackpad it should be something that feels similar), and then that "one unit" either gets translated into one wheel event (maybe configurable for more events, but I don't see the point in that), or into a number of lines to be scrolled in the scrollback buffer (which should be configurable). In the latter case, it should also be done that a fractional unit is converted into the corresponding amount of lines scrolling (e.g., if one unit = 6 lines, and the trackpad is scrolled by half a unit, then only scroll 3 lines, even though no events would be sent).

While this is how my mental idealist model of what ought to be, I'm not sure what is the best way to encode this in a config for the user.

P.s. I don't have a Linux machine to test this on. I could VM it, but I'd still have to wonder whether the VM passes through the right event fidelity. Thanks for testing there. Would be good to hear your direct observations on the behavior.

Sure. I mostly use a mouse with discrete scrolling, but I do also have a trackpad I can use to test that out. I find that the trackpad is too sensitive everywhere (even outside the terminal), and Ghostty does thankfully does let me choose the sensitivity. However, I still run into the same issue as I did with the mouse, where it is either too fast in Neovim (due to too many events) or too slow in the scrollback, although to a lesser extent since I can just move my fingers a bit slower.

I also wonder how this would/should behave with mice that have high-resolution scrolling, but I can't test that right now.

@joshka-oai
Copy link
Author

Another way to think of this is that as a good baseline is that scrolling a full touchpad's worth of distance and a single scrollwheel's worth of distance should scroll about a page of text.

Obviously what a page is is dependent on monitor, font, content, and hardware (mac touchpads tend on the large size), so those numbers are pretty subjective, but these should be in the same ballpark as defaults. The second thing is that mouse events sent to the terminal as opposed to mouse scrolling should feel like they act similarly. The number that best represents that sort of precision is about 3x.

I understand wanting 1x to be the default here and absolutely think that configuring your terminal that way is the way to go, but I think feels like it's not the right sensible default. The reason being that there isn't a trackpad scroll event and a mouse wheel event, just a scroll event, we need something which is easy to treat as correct for both scenarios.

On the app side, we'll also allow you to adjust what 1 scroll event does (i.e. the in app multiple of a single event).

Thanks for checking on this stuff for me. I don't know the Linux desktop experience well enough to know how much what your config is doing is going to be specific to your distro / window manager / etc. Is that something that matters here?

@rockorager
Copy link
Member

I don't understand what conditions lead to the OS pre-multiplying the scroll. Do you have a reproducer for that? I had previously tested this code on linux and macos and found that one wheel scroll is sent, unless it's a precision mouse in which case it uses different events.

@MithicSpirit
Copy link

I think that makes sense as default behavior.

Thanks for checking on this stuff for me. I don't know the Linux desktop experience well enough to know how much what your config is doing is going to be specific to your distro / window manager / etc. Is that something that matters here?

Afaik it should be quite stable across distro and wm, but in principle it can vary between X11 and different wayland compositors, as well as by hardware. In practice, I've found that everything behaves quite similarly (probably since it's all using libinput behind-the-scenes). Some environments do allow manual configuration away from defaults (Hyprland has a scroll factor option), but if a user is doing that, it's on them imo.

Fwiw, I'm testing this on NixOS unstable with Hyprland.

@rockorager
Copy link
Member

Another way to think of this is that as a good baseline is that scrolling a full touchpad's worth of distance and a single scrollwheel's worth of distance should scroll about a page of text.

This seems way off. A single wheel event should not scroll one screen. I can't think of a single application that behaves that way.

@MithicSpirit
Copy link

A single wheel event should not scroll one screen.

My understanding of that is 360° rotation should be a full screen, regardless of how many events that is. If you have one event every 15°, then you should scroll 1/24 of the screen per event.

@rockorager
Copy link
Member

My understanding of that is 360° rotation should be a full screen, regardless of how many events that is. If you have one event every 15°, then you should scroll 1/24 of the screen per event.

Oh I see, one wheel rotation, not one detent.

Ghostty uses GTK and Swift, neither deliver degrees, just +/- 1.0 discrete wheel events. This is usually 15 degrees but not guaranteed. Either way - I don't think we should change the events to be based on some notion of "how many lines to scroll"...this isn't the semantics of the escape sequence for mouse reporting.

@joshka-oai
Copy link
Author

A single wheel event should not scroll one screen.

My understanding of that is 360° rotation should be a full screen, regardless of how many events that is. If you have one event every 15°, then you should scroll 1/24 of the screen per event.

Sorry, I meant that a person rapidly scrolling the wheel one long pull worth of a wheel would match a similar motion of a full trackpad. Many scroll events, not a single scroll event.

I don't understand what conditions lead to the OS pre-multiplying the scroll. Do you have a reproducer for that? I had previously tested this code on linux and macos and found that one wheel scroll is sent, unless it's a precision mouse in which case it uses different events.

It's possible that I've set my scrolling multiplier way back and forgot that they were not the defaults. I confirmed with other devs that this was the default setting, but it seems that the other people I had confirmed against could have also made similar settings. I'm not sure on this one and have trouble finding any deterministic info on the default values in docs:

image

One scroll notch on the wheel - with default Ghostty settings:

image

If this indeed isn't the default value in macOS, then it's possible that this fix is entirely misguided. I'll setup a new user sometime soon and see if that helps get to the bottom of what the default settings.

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.

3 participants