From 387d6a39bad76c5eb2f56328f7cf7869a9a10559 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 13:49:09 -0300 Subject: A wheel over a terminal whose program owns it goes to the program: its mouse report when it tracks the mouse, arrow keys on the alternate screen with alternate scroll, so codex, htop and less scroll codex 0.158 moved its chat onto the alternate screen with any-event SGR mouse tracking (1049, 1000/1002/1003/1006, 1007 off); its sign-in screen keeps 1007 on with tracking off. pardes never forwarded a wheel to a program, only scrolled ghostty's scrollback, which the alternate screen does not have, so the wheel did nothing there. A shell on the main screen still scrolls its scrollback as before. Co-Authored-By: Claude Opus 5.5 --- src/terminal.zig | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) (limited to 'src/terminal.zig') diff --git a/src/terminal.zig b/src/terminal.zig index 1a3b6a21..4751a09f 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -385,6 +385,55 @@ pub fn mouseFormatSgr(pane: *const Pane) bool { return state.vt.modes.get(.mouse_format_sgr); } +/// A wheel over the program is the program's: it tracks the mouse, or it is +/// on the alternate screen, which has no scrollback, with alternate scroll +/// (DECSET 1007, on unless turned off) asking for arrow keys instead. +pub fn ownsWheel(pane: *const Pane) bool { + if (comptime !enabled) return false; + const state = pane.terminal orelse return false; + return reportsMouse(pane) or + (state.vt.screens.active_key == .alternate and state.vt.modes.get(.mouse_alternate_scroll)); +} + +pub const Wheel = enum(u8) { up = 64, down = 65, left = 66, right = 67 }; + +/// The bytes a wheel turn at grid cell `col`,`row` (0-based) sends the +/// program, as xterm sends them: its mouse report in the format it asked +/// for, else, on the alternate screen, an arrow key (none for a sideways +/// turn). Null when the wheel is not the program's (`ownsWheel`) or the +/// cell cannot be said in the legacy format. +pub fn wheelBytes(pane: *const Pane, buf: *[32]u8, wheel: Wheel, col: u16, row: u16, ctrl: bool) ?[]const u8 { + if (comptime !enabled) return null; + const state = pane.terminal orelse return null; + if (!ownsWheel(pane)) return null; + const m = &state.vt.modes; + if (!reportsMouse(pane)) return switch (wheel) { + .up => if (m.get(.cursor_keys)) "\x1bOA" else "\x1b[A", + .down => if (m.get(.cursor_keys)) "\x1bOB" else "\x1b[B", + .left, .right => null, + }; + const code = @intFromEnum(wheel) + @as(u8, if (ctrl) 16 else 0); + const x = @as(u32, col) + 1; + const y = @as(u32, row) + 1; + if (m.get(.mouse_format_sgr)) return std.fmt.bufPrint(buf, "\x1b[<{d};{d};{d}M", .{ code, x, y }) catch null; + if (m.get(.mouse_format_urxvt)) return std.fmt.bufPrint(buf, "\x1b[{d};{d};{d}M", .{ 32 + @as(u32, code), x, y }) catch null; + buf[0..3].* = "\x1b[M".*; + buf[3] = 32 + code; + var n: usize = 4; + for ([_]u32{ x, y }) |v| { + const c = 32 + v; + if (m.get(.mouse_format_utf8)) { + if (c > 2047) return null; + n += std.unicode.utf8Encode(@intCast(c), buf[n..]) catch return null; + } else { + if (c > 255) return null; + buf[n] = @intCast(c); + n += 1; + } + } + return buf[0..n]; +} + /// The whole scrollback as plain text, `gpa`-owned: what a terminal's body /// reads and `Save` writes out. Logical lines: rows the terminal wrapped /// are joined again (ghostty's wrap flags, as pty/run's output is), and -- cgit v1.3