summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 14:06:31 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit3f6733c805d873a7af6a6064ace0cf5916f481da (patch)
tree302874e21941dbf8b599b5e407f430e334594f62
parent387d6a39bad76c5eb2f56328f7cf7869a9a10559 (diff)
downloadpardes-3f6733c805d873a7af6a6064ace0cf5916f481da.tar.gz
pardes-3f6733c805d873a7af6a6064ace0cf5916f481da.zip
A mouse event carries Shift and Alt as well as Ctrl, from SDL and from the tty's input; a program's wheel report carries them as xterm counts them, and Shift keeps the wheel pardes's own scroll
The reports are ghostty's encoder now (shift+4, meta+8, ctrl+16), with the mode and format ghostty tracks. The detached wire carries the two new bits: version 9. The test shell's ESC]777;mouse takes an optional fifth field of modifiers. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/detached/wire.zig10
-rw-r--r--src/gui/gui.zig66
-rw-r--r--src/mouse.zig39
-rw-r--r--src/pardes.zig3
-rw-r--r--src/terminal.zig92
-rw-r--r--src/tty/tty.zig2
6 files changed, 159 insertions, 53 deletions
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index 051e9317..9a1ab608 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -12,7 +12,7 @@ const ShaderAnimation = pardes.config.Runtime.ShaderAnimation;
/// to be refused, which is what this number is for.
/// 8: one layer type: a tag band carries all its rows (not a layer per
/// line), and a frame carries the core's placed regions after its layers.
-pub const version: u16 = 8;
+pub const version: u16 = 9;
/// The Chrome palette is carried field by field as its declaration lists
/// them, so any change to it changes the wire: bump `version`, then this.
@@ -1052,6 +1052,8 @@ pub fn encodeClient(out: []u8, msg: ClientMsg) Error![]const u8 {
try w.putU16(m.col);
try w.putU16(m.row);
try w.putBool(m.ctrl);
+ try w.putBool(m.shift);
+ try w.putBool(m.alt);
try putBodyHit(&w, m.body_hit);
try putTagHit(&w, m.tag_hit);
},
@@ -1115,6 +1117,8 @@ pub fn decodeClient(tag: u8, payload: []const u8) Error!ClientMsg {
.col = try r.getU16(),
.row = try r.getU16(),
.ctrl = try r.getBool(),
+ .shift = try r.getBool(),
+ .alt = try r.getBool(),
.body_hit = try getBodyHit(&r),
.tag_hit = try getTagHit(&r),
} } },
@@ -1357,13 +1361,15 @@ test "detached wire: every Event variant round-trips" {
// the only place a value can be mistranslated and still decode.
for (std.enums.values(pardes.Mouse.Button)) |button| {
for (std.enums.values(pardes.Mouse.Kind)) |kind| {
- const m: pardes.Mouse = .{ .button = button, .kind = kind, .col = 4200, .row = 7, .ctrl = true };
+ const m: pardes.Mouse = .{ .button = button, .kind = kind, .col = 4200, .row = 7, .ctrl = true, .shift = true, .alt = true };
const got = (try roundClient(&buf, .{ .event = .{ .mouse = m } })).event.mouse;
try testing.expectEqual(m.button, got.button);
try testing.expectEqual(m.kind, got.kind);
try testing.expectEqual(m.col, got.col);
try testing.expectEqual(m.row, got.row);
try testing.expectEqual(m.ctrl, got.ctrl);
+ try testing.expectEqual(m.shift, got.shift);
+ try testing.expectEqual(m.alt, got.alt);
}
}
}
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 045f22d9..4ce554f3 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -3139,8 +3139,8 @@ const StdinFeed = struct {
var seq_start: usize = 0;
while (seq_start < len) {
- // ESC]777;mouse;<down|up|motion>;<SDL button, 0 for none>;<x>;<y>BEL
- // ESC]777;mouse;wheel;<amount>;<x>;<y>BEL
+ // ESC]777;mouse;<down|up|motion>;<SDL button, 0 for none>;<x>;<y>[;<mods>]BEL
+ // ESC]777;mouse;wheel;<amount>;<x>;<y>[;<mods>]BEL
// A mouse event at window pixels, built as SDL would and run
// through dispatch, so a driver exercises the real pointer path
// (pixel to cell, tag and body hits). SGR mouse carries only cells.
@@ -3159,6 +3159,9 @@ const StdinFeed = struct {
const button = std.fmt.parseInt(u8, second, 10) catch 0;
const x = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0;
const y = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0;
+ // An optional fifth field holds modifiers as xterm
+ // counts them: shift 4, meta (alt) 8, ctrl 16.
+ const held = std.fmt.parseInt(u8, parts.next() orelse "0", 10) catch 0;
var sev = std.mem.zeroes(c.SDL_Event);
if (std.mem.eql(u8, kind, "wheel")) {
// ESC]777;mouse;wheel;<y, SDL's sign: up positive>;<x>;<y>BEL:
@@ -3181,7 +3184,14 @@ const StdinFeed = struct {
sev.button.x = x;
sev.button.y = y;
}
+ const before = c.SDL_GetModState();
+ var mods: c.SDL_Keymod = 0;
+ if (held & 4 != 0) mods |= c.SDL_KMOD_LSHIFT;
+ if (held & 8 != 0) mods |= c.SDL_KMOD_LALT;
+ if (held & 16 != 0) mods |= c.SDL_KMOD_LCTRL;
+ c.SDL_SetModState(mods);
dispatch(gp, in, &sev);
+ c.SDL_SetModState(before);
out.n_events += 1;
}
seq_start += mouse_prefix.len + end + 1;
@@ -3256,6 +3266,8 @@ const StdinFeed = struct {
.col = @intCast(@max(m.col, 0)),
.row = @intCast(@max(m.row, 0)),
.ctrl = m.mods.ctrl,
+ .shift = m.mods.shift,
+ .alt = m.mods.alt,
} });
out.n_events += 1;
}
@@ -3526,6 +3538,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
};
g.pointer_mapped = true;
g.pointer_cell = mc;
+ const mods = pointerMods();
in.post(.{
.mouse = .{
.button = button,
@@ -3535,7 +3548,9 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.tag_hit = mc.tag_hit,
.body_hit = mc.body_hit,
.rule_above = mc.rule_above,
- .ctrl = (c.SDL_GetModState() & c.SDL_KMOD_CTRL) != 0,
+ .ctrl = mods.ctrl,
+ .shift = mods.shift,
+ .alt = mods.alt,
},
});
},
@@ -3559,6 +3574,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
};
g.pointer_mapped = true;
g.pointer_cell = mc;
+ const mods = pointerMods();
in.post(.{ .mouse = .{
.button = held orelse .none,
.kind = if (held != null) .drag else .motion,
@@ -3566,6 +3582,9 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.row = mc.row,
.tag_hit = mc.tag_hit,
.body_hit = mc.body_hit,
+ .ctrl = mods.ctrl,
+ .shift = mods.shift,
+ .alt = mods.alt,
} });
},
c.SDL_EVENT_MOUSE_WHEEL => {
@@ -3581,7 +3600,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
};
g.pointer_mapped = true;
g.pointer_cell = mc;
- wheelAt(g, in, .{ .x = w.x, .y = w.y, .integer_y = w.integer_y }, mc);
+ wheelAt(g, in, .{ .x = w.x, .y = w.y, .integer_y = w.integer_y, .mods = pointerMods() }, mc);
},
c.SDL_EVENT_FINGER_DOWN, c.SDL_EVENT_FINGER_MOTION, c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => {
const f = sev.tfinger;
@@ -3913,16 +3932,16 @@ fn wheelAt(g: *Gui, in: *Input, w: WheelTurn, mc: MouseCell) void {
.pane = @intCast(id),
.delta_pixels = -w.y * @as(f32, @floatFromInt(g.cell_h)),
} });
- } else if (pane.isTerminal() and pardes.panes.terminal.ownsWheel(pane)) {
+ } else if (!w.mods.shift and pane.isTerminal() and pardes.panes.terminal.ownsWheel(pane)) {
// The program's (codex, htop, less): every whole
// turn goes to it, no glide, for nothing of ours
- // moves.
+ // moves. Shift keeps the wheel pardes's.
resetScroll(g);
g.program_wheel = accumulateWheelDelta(g.program_wheel, w.y);
while (g.program_wheel >= 1) : (g.program_wheel -= 1)
- in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
+ in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit, .ctrl = w.mods.ctrl, .shift = w.mods.shift, .alt = w.mods.alt } });
while (g.program_wheel <= -1) : (g.program_wheel += 1)
- in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
+ in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit, .ctrl = w.mods.ctrl, .shift = w.mods.shift, .alt = w.mods.alt } });
} else {
g.scroll_pane = id;
g.scroll_col = mc.col;
@@ -3938,9 +3957,9 @@ fn wheelAt(g: *Gui, in: *Input, w: WheelTurn, mc: MouseCell) void {
} else {
g.scroll_delta = accumulateWheelDelta(g.scroll_delta, w.y);
while (g.scroll_delta >= 1) : (g.scroll_delta -= 1)
- in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
+ in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit, .ctrl = w.mods.ctrl, .shift = w.mods.shift, .alt = w.mods.alt } });
while (g.scroll_delta <= -1) : (g.scroll_delta += 1)
- in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
+ in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit, .ctrl = w.mods.ctrl, .shift = w.mods.shift, .alt = w.mods.alt } });
}
}
if (w.x != 0) in.post(.{ .mouse = .{
@@ -3950,10 +3969,21 @@ fn wheelAt(g: *Gui, in: *Input, w: WheelTurn, mc: MouseCell) void {
.row = mc.row,
.tag_hit = mc.tag_hit,
.body_hit = mc.body_hit,
+ .ctrl = w.mods.ctrl,
+ .shift = w.mods.shift,
+ .alt = w.mods.alt,
} });
}
-const WheelTurn = struct { x: f32 = 0, y: f32 = 0, integer_y: i32 = 0 };
+const WheelTurn = struct { x: f32 = 0, y: f32 = 0, integer_y: i32 = 0, mods: PointerMods = .{} };
+
+/// The modifiers held with a pointer event, as the core's Mouse carries them.
+const PointerMods = struct { ctrl: bool = false, shift: bool = false, alt: bool = false };
+
+fn pointerMods() PointerMods {
+ const m = c.SDL_GetModState();
+ return .{ .ctrl = (m & c.SDL_KMOD_CTRL) != 0, .shift = (m & c.SDL_KMOD_SHIFT) != 0, .alt = (m & c.SDL_KMOD_ALT) != 0 };
+}
fn mouseCellWithGeometry(g: *const Gui, core: ?*const pardes.Pardes, x: f32, y: f32, geometry: WindowGeometry) ?MouseCell {
const physical = windowPointToPixels(geometry, x, y);
@@ -4934,6 +4964,7 @@ test "a wheel over codex in a terminal reaches its pty as the mouse reports it a
g.scroll_notch = 0;
g.program_wheel = 0;
g.glide = .{};
+ defer g.glide.deinit(gpa);
g.cell_h = 20;
var in: Input = .{ .core = core };
var sent: std.ArrayList(u8) = .empty;
@@ -4967,6 +4998,15 @@ test "a wheel over codex in a terminal reaches its pty as the mouse reports it a
try std.testing.expectEqualStrings("", try Sent.take(core, &sent));
wheelAt(&g, &in, .{ .y = 0.5 }, mc);
try std.testing.expectEqualStrings("\x1b[<64;6;4M", try Sent.take(core, &sent));
+ // Alt is meta, 8; Shift keeps the turn pardes's own, a glide.
+ wheelAt(&g, &in, .{ .y = 1, .integer_y = 1, .mods = .{ .alt = true } }, mc);
+ try std.testing.expectEqualStrings("\x1b[<72;6;4M", try Sent.take(core, &sent));
+ wheelAt(&g, &in, .{ .y = 1, .integer_y = 1, .mods = .{ .shift = true } }, mc);
+ try std.testing.expectEqualStrings("", try Sent.take(core, &sent));
+ try std.testing.expectEqual(@as(i32, -1), g.scroll_notch);
+ stepGlide(&g, core, gpa);
+ try std.testing.expectEqualStrings("", try Sent.take(core, &sent));
+ resetScroll(&g);
// Its sign-in screen: mouse off, alternate scroll on, so arrow keys.
core.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1007h" } });
_ = try Sent.take(core, &sent);
@@ -5171,7 +5211,8 @@ fn stepGlide(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
const down = left > 0;
left += if (down) -1 else 1;
const was = pane.scroll();
- core.update(.{ .mouse = .{ .button = if (down) .wheel_down else .wheel_up, .kind = .press, .col = g.scroll_col, .row = g.scroll_row } });
+ // Shift: pardes's own scroll, never a program's wheel.
+ core.update(.{ .mouse = .{ .button = if (down) .wheel_down else .wheel_up, .kind = .press, .col = g.scroll_col, .row = g.scroll_row, .shift = true } });
if (pane.scroll() == was) break;
moved = true;
}
@@ -5344,6 +5385,7 @@ fn stepScroll(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
.kind = .press,
.col = g.scroll_col,
.row = g.scroll_row,
+ .shift = true, // pardes's own scroll, never a program's wheel
} });
if (pane.scroll() == was) {
resetScroll(g);
diff --git a/src/mouse.zig b/src/mouse.zig
index a0ec6e47..e80ea7f8 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -313,16 +313,24 @@ pub fn sameBodyCell(a: ?Mouse.BodyHit, b: ?Mouse.BodyHit) bool {
/// A wheel over a terminal whose program owns it (`terminal.ownsWheel`:
/// codex, htop, less) goes to the program as the program asked, one turn
/// per line, instead of scrolling a scrollback the alternate screen does
-/// not have. False when pardes scrolls.
-fn programWheel(p: *Pardes, id: usize, wheel: panes.terminal.Wheel, col: u16, row: u16, ctrl: bool) bool {
+/// not have. Shift keeps it pardes's, as xterm's Shift does. False when
+/// pardes scrolls.
+fn programWheel(p: *Pardes, id: usize, m: Mouse, col: u16, row: u16) bool {
+ if (m.shift) return false;
const pane = p.panes[id] orelse return false;
if (!pane.isTerminal() or !panes.terminal.ownsWheel(pane)) return false;
const r = p.rects[id];
const body_y = p.bodyTop(pane, r);
const gcol = std.math.clamp(col -| (r.x + config.GUTTER), 0, pane.cols -| 1);
const grow = std.math.clamp(row -| body_y, 0, pane.rows -| 1);
- var buf: [32]u8 = undefined;
- const bytes = panes.terminal.wheelBytes(pane, &buf, wheel, gcol, grow, ctrl) orelse return true;
+ var buf: [64]u8 = undefined;
+ const wheel: panes.terminal.MouseButton = switch (m.button) {
+ .wheel_up => .wheel_up,
+ .wheel_down => .wheel_down,
+ .wheel_left => .wheel_left,
+ else => .wheel_right,
+ };
+ const bytes = panes.terminal.wheelBytes(pane, &buf, wheel, gcol, grow, .{ .alt = m.alt, .ctrl = m.ctrl }) orelse return true;
for (0..@intCast(config.wheel_rows)) |_| p.emitWrite(id, bytes);
return true;
}
@@ -525,7 +533,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
const pane = p.panes[id].?;
if (pane.hasPdf())
panes.Pdf.verticalWheel(p, pane, if (m.button == .wheel_up) -1 else 1)
- else if (!programWheel(p, id, if (m.button == .wheel_up) .up else .down, mcol, mrow, m.ctrl))
+ else if (!programWheel(p, id, m, mcol, mrow))
pane.scrollBy(if (m.button == .wheel_up) -config.wheel_rows else config.wheel_rows);
},
.wheel_left, .wheel_right => {
@@ -533,7 +541,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
if (!config.wheelTick(&p.wheel_guard, false)) return;
const id = hovered orelse return;
const pane = p.panes[id].?;
- if (programWheel(p, id, if (m.button == .wheel_right) .right else .left, mcol, mrow, m.ctrl)) {
+ if (programWheel(p, id, m, mcol, mrow)) {
// the program's
} else if (pane.hasPdf()) {
panes.Pdf.horizontalWheel(p, pane, if (m.button == .wheel_right) 1 else -1);
@@ -1838,11 +1846,16 @@ test "a wheel over a program that owns it is the program's, in the form it asked
// Grid cell 6,4 (1-based), as a program counts.
const col = r.x + config.GUTTER + 5;
const row = p.bodyTop(pane, r) + 3;
- const Case = struct { modes: []const u8, button: Mouse.Button, ctrl: bool = false, sent: []const u8 };
+ const Case = struct { modes: []const u8, button: Mouse.Button, ctrl: bool = false, alt: bool = false, shift: bool = false, sent: []const u8 };
const cases = [_]Case{
// codex's chat (0.158+): the alternate screen, any-event tracking, SGR.
.{ .modes = "\x1b[?2004h\x1b[>7u\x1b[?1004h\x1b[?1049h\x1b[?1007l\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h", .button = .wheel_up, .sent = "\x1b[<64;6;4M" },
.{ .modes = "", .button = .wheel_down, .ctrl = true, .sent = "\x1b[<81;6;4M" },
+ // Meta is 8, both 24; Shift keeps the wheel pardes's, which has
+ // nothing to scroll on the alternate screen.
+ .{ .modes = "", .button = .wheel_up, .alt = true, .sent = "\x1b[<72;6;4M" },
+ .{ .modes = "", .button = .wheel_up, .alt = true, .ctrl = true, .sent = "\x1b[<88;6;4M" },
+ .{ .modes = "", .button = .wheel_up, .shift = true, .sent = "" },
// htop's legacy bytes (1000 alone), and 1015's.
.{ .modes = "\x1b[?1006l", .button = .wheel_down, .sent = "\x1b[M\x61\x26\x24" },
.{ .modes = "\x1b[?1015h", .button = .wheel_up, .sent = "\x1b[96;6;4M" },
@@ -1857,7 +1870,7 @@ test "a wheel over a program that owns it is the program's, in the form it asked
for (cases) |case| {
p.update(.{ .output = .{ .pane = 0, .bytes = case.modes } });
_ = try ptyWrites(p, &out);
- p.update(.{ .mouse = .{ .button = case.button, .kind = .press, .col = col, .row = row, .ctrl = case.ctrl } });
+ p.update(.{ .mouse = .{ .button = case.button, .kind = .press, .col = col, .row = row, .ctrl = case.ctrl, .alt = case.alt, .shift = case.shift } });
try std.testing.expectEqualStrings(case.sent, try ptyWrites(p, &out));
}
// A shell on the main screen: the wheel scrolls its scrollback, and
@@ -1873,4 +1886,14 @@ test "a wheel over a program that owns it is the program's, in the form it asked
p.update(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = col, .row = row } });
try std.testing.expectEqualStrings("", try ptyWrites(p, &out));
try std.testing.expect(pane.scroll() != was);
+ // A program tracking the mouse on the main screen: Shift and the
+ // wheel scroll the scrollback, the program hears nothing.
+ p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1000h\x1b[?1006h" } });
+ _ = try ptyWrites(p, &out);
+ const tracked = pane.scroll();
+ p.update(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = col, .row = row, .shift = true } });
+ try std.testing.expectEqualStrings("", try ptyWrites(p, &out));
+ try std.testing.expect(pane.scroll() != tracked);
+ p.update(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = col, .row = row } });
+ try std.testing.expectEqualStrings("\x1b[<64;6;4M", try ptyWrites(p, &out));
}
diff --git a/src/pardes.zig b/src/pardes.zig
index f0d22d57..d6533b3b 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -3452,6 +3452,9 @@ pub const Mouse = struct {
col: u16,
row: u16,
ctrl: bool = false,
+ shift: bool = false,
+ /// Alt, which xterm's mouse reports call meta.
+ alt: bool = false,
body_hit: ?BodyHit = null,
tag_hit: ?TagHit = null,
/// A pixel frontend's press on the rule it draws at the top of this row,
diff --git a/src/terminal.zig b/src/terminal.zig
index 4751a09f..4d16bef2 100644
--- a/src/terminal.zig
+++ b/src/terminal.zig
@@ -124,6 +124,9 @@ pub const State = struct {
replay: Replay,
reply: [256]u8 = undefined,
reply_len: u16 = 0,
+ /// The grid cell the program last heard the mouse at, so motion is
+ /// reported once per cell.
+ mouse_cell: if (enabled) ?ghostty_vt.Coordinate else void = if (enabled) null else {},
};
const GColor = ghostty_vt.color;
@@ -374,8 +377,7 @@ pub fn bracketedPaste(pane: *const Pane) bool {
pub fn reportsMouse(pane: *const Pane) bool {
if (comptime !enabled) return false;
const state = pane.terminal orelse return false;
- const m = &state.vt.modes;
- return m.get(.mouse_event_normal) or m.get(.mouse_event_button) or m.get(.mouse_event_any);
+ return state.vt.flags.mouse_event != .none;
}
/// ...and wants them in SGR (1006) rather than the legacy X10 bytes.
@@ -395,43 +397,71 @@ pub fn ownsWheel(pane: *const Pane) bool {
(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 };
+pub const MouseMods = struct { shift: bool = false, alt: bool = false, ctrl: bool = false };
+pub const MouseButton = enum { left, middle, right, wheel_up, wheel_down, wheel_left, wheel_right };
+pub const MouseAction = enum { press, release, motion };
+
+/// One mouse event for the program, as xterm reports it (ctlseqs; ghostty's
+/// encoder): the tracking mode it set (1000 clicks, 1002 drags, 1003 all
+/// motion) decides whether it hears of it, the format it set (1006 SGR,
+/// 1015 urxvt, 1005 UTF-8, else X10) how, modifiers as shift+4, meta+8,
+/// ctrl+16. `col`,`row` are the 0-based grid cell; a drag that left the
+/// grid is reported clamped to its edge, motion only when the cell
+/// changed. Empty when the program does not hear of it.
+pub fn mouseBytes(pane: *Pane, buf: *[64]u8, action: MouseAction, button: ?MouseButton, mods: MouseMods, col: i32, row: i32, any_pressed: bool) []const u8 {
+ if (comptime !enabled) return &.{};
+ const state = pane.terminal orelse return &.{};
+ const input = ghostty_vt.input;
+ var writer = std.Io.Writer.fixed(buf);
+ var opts: input.MouseEncodeOptions = .{
+ .event = state.vt.flags.mouse_event,
+ .format = state.vt.flags.mouse_format,
+ .size = .{
+ .screen = .{ .width = state.vt.cols, .height = state.vt.rows },
+ .cell = .{ .width = 1, .height = 1 },
+ .padding = .{},
+ },
+ .any_button_pressed = any_pressed,
+ .last_cell = &state.mouse_cell,
+ };
+ // A release is always reported; the rest only over the grid, or held.
+ if (action == .release) opts.any_button_pressed = true;
+ input.encodeMouse(&writer, .{
+ .action = switch (action) {
+ .press => .press,
+ .release => .release,
+ .motion => .motion,
+ },
+ .button = if (button) |b| switch (b) {
+ .left => .left,
+ .middle => .middle,
+ .right => .right,
+ .wheel_up => .four,
+ .wheel_down => .five,
+ .wheel_left => .six,
+ .wheel_right => .seven,
+ } else null,
+ .mods = .{ .shift = mods.shift, .alt = mods.alt, .ctrl = mods.ctrl },
+ .pos = .{ .x = @as(f32, @floatFromInt(col)) + 0.5, .y = @as(f32, @floatFromInt(row)) + 0.5 },
+ }, opts) catch return &.{};
+ return writer.buffered();
+}
/// 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 {
+/// program: its mouse report while it tracks the mouse, else, on the
+/// alternate screen, an arrow key in the cursor keys' mode (none for a
+/// sideways turn). Null when the wheel is not the program's (`ownsWheel`).
+pub fn wheelBytes(pane: *Pane, buf: *[64]u8, wheel: MouseButton, col: u16, row: u16, mods: MouseMods) ?[]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,
+ .wheel_up => if (m.get(.cursor_keys)) "\x1bOA" else "\x1b[A",
+ .wheel_down => if (m.get(.cursor_keys)) "\x1bOB" else "\x1b[B",
+ else => 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];
+ return mouseBytes(pane, buf, .press, wheel, mods, col, row, false);
}
/// The whole scrollback as plain text, `gpa`-owned: what a terminal's body
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index ef7f2761..0b933065 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -2261,6 +2261,8 @@ fn mouseEvent(m: vaxis.Mouse) ?pardes.Event {
.col = @intCast(m.col),
.row = @intCast(m.row),
.ctrl = m.mods.ctrl,
+ .shift = m.mods.shift,
+ .alt = m.mods.alt,
} };
}