config: add [mouse] table with scroll multiplier and alternate-scroll

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I58d4f2cb0010c167c7c317bf10dea99b6a6a6964
This commit is contained in:
raf 2026-06-25 13:01:34 +03:00
commit 15a4a97033
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
2 changed files with 56 additions and 3 deletions

View file

@ -16,6 +16,7 @@ pub struct Config {
pub cursor: Cursor, pub cursor: Cursor,
pub scrollback: Scrollback, pub scrollback: Scrollback,
pub bell: Bell, pub bell: Bell,
pub mouse: Mouse,
/// Chord → action, e.g. `"Ctrl+Shift+C" = "copy"`. Merged over the defaults; /// Chord → action, e.g. `"Ctrl+Shift+C" = "copy"`. Merged over the defaults;
/// a value of `"none"` unbinds. /// a value of `"none"` unbinds.
pub key_bindings: std::collections::HashMap<String, String>, pub key_bindings: std::collections::HashMap<String, String>,
@ -41,6 +42,27 @@ pub struct Bell {
pub visual: bool, pub visual: bool,
} }
/// `[mouse]`: pointer and wheel behaviour.
#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Mouse {
/// Multiplier applied to the lines scrolled per wheel notch.
pub scroll_multiplier: f64,
/// On the alternate screen, translate the wheel into arrow-key presses so
/// full-screen apps that did not request mouse reporting (less, man, …)
/// still scroll.
pub alternate_scroll: bool,
}
impl Default for Mouse {
fn default() -> Self {
Self {
scroll_multiplier: 1.0,
alternate_scroll: true,
}
}
}
/// `[colors]`: foreground/background, the 16 base palette entries, and accents. /// `[colors]`: foreground/background, the 16 base palette entries, and accents.
/// Each value is an X11 colour spec (`#rrggbb` or `rgb:rr/gg/bb`); unset entries /// Each value is an X11 colour spec (`#rrggbb` or `rgb:rr/gg/bb`); unset entries
/// keep the built-in default. /// keep the built-in default.

View file

@ -641,6 +641,24 @@ impl App {
} }
} }
/// Send `count` cursor-up/down keys to the shell for alternate-scroll,
/// honouring the application cursor-key mode (DECCKM).
fn alternate_scroll(&mut self, up: bool, count: isize) {
let app_cursor = self
.session
.as_ref()
.is_some_and(|s| s.term.grid().app_cursor());
let seq: &[u8] = match (up, app_cursor) {
(true, false) => b"\x1b[A",
(true, true) => b"\x1bOA",
(false, false) => b"\x1b[B",
(false, true) => b"\x1bOB",
};
for _ in 0..count {
self.write_to_pty(seq);
}
}
/// Scroll the viewport one page back (`up`) or toward the live screen. /// Scroll the viewport one page back (`up`) or toward the live screen.
fn scroll_page(&mut self, up: bool) { fn scroll_page(&mut self, up: bool) {
if let Some(session) = self.session.as_mut() { if let Some(session) = self.session.as_mut() {
@ -1955,19 +1973,32 @@ impl PointerHandler for App {
if raw == 0.0 { if raw == 0.0 {
continue; continue;
} }
let lines = (raw.abs() * scale).ceil().max(1.0) as isize; let mult = self.config.mouse.scroll_multiplier.max(0.0);
let lines = (raw.abs() * scale * mult).ceil().max(1.0) as isize;
let up = raw < 0.0;
// Reporting apps get wheel buttons (64 up / 65 down) as // Reporting apps get wheel buttons (64 up / 65 down) as
// presses, one per line, capped so a flick cannot flood. // presses, one per line, capped so a flick cannot flood.
if self.mouse_reporting() { if self.mouse_reporting() {
let code = if raw < 0.0 { 64 } else { 65 }; let code = if up { 64 } else { 65 };
for _ in 0..lines.clamp(1, 8) { for _ in 0..lines.clamp(1, 8) {
self.try_report_button(code, true); self.try_report_button(code, true);
} }
continue; continue;
} }
// On the alternate screen there is no scrollback to move, so
// (when enabled) translate the wheel into cursor-key presses
// for apps that did not request mouse reporting.
let alt = self
.session
.as_ref()
.is_some_and(|s| s.term.grid().alt_active());
if alt && self.config.mouse.alternate_scroll {
self.alternate_scroll(up, lines.clamp(1, 8));
continue;
}
// Positive axis = scroll down (toward live); the viewport // Positive axis = scroll down (toward live); the viewport
// scrolls the opposite way (negative offset delta). // scrolls the opposite way (negative offset delta).
let delta = if raw < 0.0 { lines } else { -lines }; let delta = if up { lines } else { -lines };
if let Some(session) = self.session.as_mut() { if let Some(session) = self.session.as_mut() {
session.term.scroll_view(delta); session.term.scroll_view(delta);
self.needs_draw = true; self.needs_draw = true;