summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/tags.md9
-rw-r--r--src/gui/gui.zig85
-rw-r--r--src/mouse.zig203
-rw-r--r--src/pardes.zig3
-rw-r--r--src/terminal.zig7
-rw-r--r--test/panes.zig10
-rw-r--r--test/snapshot.zig5
-rw-r--r--test/snapshots/chordcut.snap15
8 files changed, 289 insertions, 48 deletions
diff --git a/docs/tags.md b/docs/tags.md
index 93c8a6fa..dfe0e3c0 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -30,6 +30,15 @@ A tag word runs as acme's does, in the pane's directory with no file named:
or select the name and middle-click `wc`, which takes a held selection as its
argument.
+A program that tracks the mouse gets it; hold Shift for pardes's own. In
+raw terminal input, a program that asked for the mouse (xterm's 1000, 1002
+or 1003: htop, vim with `mouse=a`, codex) gets the clicks, drags and wheel
+over its grid, reported in the format it chose and with its modifiers.
+Shift-click selects, executes and looks as ever, and Shift-wheel scrolls
+pardes's scrollback. A full-screen program that does not track the mouse
+gets the wheel as arrow keys (xterm's alternate scroll, 1007). Tags, grips
+and gutters stay pardes's.
+
`Repl python` in a terminal's tag binds it as that language's REPL
([fs.md](fs.md#repls)). `Repl` takes the languages a code fence names: ada,
bash, c, c_sharp, clojure, cpp, css, elixir, erlang, fortran, go, haskell,
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 4ce554f3..590d749d 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -3538,21 +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,
- .kind = if (b.down) .press else .release,
- .col = mc.col,
- .row = mc.row,
- .tag_hit = mc.tag_hit,
- .body_hit = mc.body_hit,
- .rule_above = mc.rule_above,
- .ctrl = mods.ctrl,
- .shift = mods.shift,
- .alt = mods.alt,
- },
- });
+ in.post(.{ .mouse = pointerMouse(button, if (b.down) .press else .release, mc, pointerMods()) });
},
c.SDL_EVENT_MOUSE_MOTION => {
const m = sev.motion;
@@ -3574,18 +3560,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,
- .col = mc.col,
- .row = mc.row,
- .tag_hit = mc.tag_hit,
- .body_hit = mc.body_hit,
- .ctrl = mods.ctrl,
- .shift = mods.shift,
- .alt = mods.alt,
- } });
+ in.post(.{ .mouse = pointerMouse(held orelse .none, if (held != null) .drag else .motion, mc, pointerMods()) });
},
c.SDL_EVENT_MOUSE_WHEEL => {
const w = sev.wheel;
@@ -3980,6 +3955,22 @@ const WheelTurn = struct { x: f32 = 0, y: f32 = 0, integer_y: i32 = 0, mods: Poi
/// 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 };
+/// The core's Mouse for an SDL press, release or move over cell `mc`.
+fn pointerMouse(button: pardes.Mouse.Button, kind: pardes.Mouse.Kind, mc: MouseCell, mods: PointerMods) pardes.Mouse {
+ return .{
+ .button = button,
+ .kind = kind,
+ .col = mc.col,
+ .row = mc.row,
+ .tag_hit = mc.tag_hit,
+ .body_hit = mc.body_hit,
+ .rule_above = (kind == .press or kind == .release) and mc.rule_above,
+ .ctrl = mods.ctrl,
+ .shift = mods.shift,
+ .alt = mods.alt,
+ };
+}
+
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 };
@@ -4948,6 +4939,46 @@ test "a wheel's notch is whole lines, a touchpad's fraction is not" {
try std.testing.expectEqual(@as(?i32, null), notchOf(std.math.nan(f32), 1));
}
+test "a click over codex in a terminal reaches its pty with its modifiers; with Shift it is pardes's" {
+ if (comptime !pardes.panes.terminal.enabled) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 60, .rows = 16, .tty_only = true });
+ defer core.deinit();
+ var frame: std.heap.ArenaAllocator = .init(gpa);
+ defer frame.deinit();
+ _ = try core.render(frame.allocator());
+ var in: Input = .{ .core = core };
+ var sent: std.ArrayList(u8) = .empty;
+ defer sent.deinit(gpa);
+ const take = struct {
+ fn f(core_: *pardes.Pardes, out: *std.ArrayList(u8)) ![]const u8 {
+ out.clearRetainingCapacity();
+ while (core_.nextEffect()) |effect| {
+ if (effect == .write and effect.write.pane == 0) try out.appendSlice(std.testing.allocator, effect.write.bytes.slice());
+ }
+ return out.items;
+ }
+ }.f;
+ _ = try take(core, &sent);
+ const pane = core.panes[0].?;
+ const r = core.rects[0];
+ const mc: MouseCell = .{ .col = r.x + config.GUTTER + 5, .row = core.bodyTop(pane, r) + 3 };
+ const next: MouseCell = .{ .col = mc.col + 1, .row = mc.row };
+ core.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1049h\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h" } });
+ _ = try take(core, &sent);
+ in.post(.{ .mouse = pointerMouse(.none, .motion, mc, .{}) });
+ try std.testing.expectEqualStrings("\x1b[<35;6;4M", try take(core, &sent));
+ in.post(.{ .mouse = pointerMouse(.left, .press, mc, .{ .ctrl = true }) });
+ try std.testing.expectEqualStrings("\x1b[<16;6;4M", try take(core, &sent));
+ in.post(.{ .mouse = pointerMouse(.left, .drag, next, .{}) });
+ try std.testing.expectEqualStrings("\x1b[<32;7;4M", try take(core, &sent));
+ in.post(.{ .mouse = pointerMouse(.left, .release, next, .{}) });
+ try std.testing.expectEqualStrings("\x1b[<0;7;4m", try take(core, &sent));
+ in.post(.{ .mouse = pointerMouse(.middle, .press, mc, .{ .shift = true }) });
+ in.post(.{ .mouse = pointerMouse(.middle, .release, mc, .{ .shift = true }) });
+ try std.testing.expectEqualStrings("", try take(core, &sent));
+}
+
test "a wheel over codex in a terminal reaches its pty as the mouse reports it asked for" {
if (comptime !pardes.panes.terminal.enabled) return error.SkipZigTest;
const gpa = std.testing.allocator;
diff --git a/src/mouse.zig b/src/mouse.zig
index e80ea7f8..ecb9bb57 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -335,6 +335,104 @@ fn programWheel(p: *Pardes, id: usize, m: Mouse, col: u16, row: u16) bool {
return true;
}
+pub const Grab = struct { id: usize, serial: u32, held: u3 = 0 };
+
+/// The 0-based cell of a terminal program's grid under screen cell
+/// `col`,`row`, or null over chrome (tag, gutter, scrollbar, anything past
+/// the grid).
+fn programCell(p: *const Pardes, id: usize, col: u16, row: u16) ?struct { col: i32, row: i32 } {
+ const pane = p.panes[id] orelse return null;
+ const r = p.rects[id];
+ const x0 = r.x + config.GUTTER;
+ const y0 = p.bodyTop(pane, r);
+ const h = @min(r.h -| pane.tag_rows, pane.rows);
+ if (col < x0 or col >= r.x + r.w or col - x0 >= pane.cols or row < y0 or row - y0 >= h) return null;
+ return .{ .col = col - x0, .row = row - y0 };
+}
+
+/// A pane whose program tracks the mouse (1000, 1002, 1003, or X10's 9)
+/// and has the keyboard's raw input: its clicks are the program's.
+fn tracksMouse(pane: *const Pane) bool {
+ return pane.isTerminal() and pane.body.mode == .tty and panes.terminal.reportsMouse(pane);
+}
+
+/// A program that tracks the mouse gets it over its grid, as xterm gives
+/// it: presses, releases, drags with a button down (1002) and, under 1003,
+/// every move, once per cell. A press it took keeps its drag and release
+/// wherever the pointer goes. Tags, gutters and the rest of pardes's
+/// chrome are never its, and Shift keeps pardes's own select, execute and
+/// look. True when the event was the program's.
+fn programMouse(p: *Pardes, m: Mouse) bool {
+ const button: ?panes.terminal.MouseButton = switch (m.button) {
+ .left => .left,
+ .middle => .middle,
+ .right => .right,
+ .none => null,
+ else => return false,
+ };
+ const bit: u3 = if (button) |b| @as(u3, 1) << @intCast(@intFromEnum(b)) else 0;
+ const at = p.presentation.pointer(p.screen_w, p.screen_h, @min(m.col, p.screen_w -| 1), @min(m.row, p.screen_h -| 1));
+ const mods: panes.terminal.MouseMods = .{ .shift = m.shift, .alt = m.alt, .ctrl = m.ctrl };
+ var buf: [64]u8 = undefined;
+ if (p.mouse_grab) |*grab| {
+ const pane = p.panes[grab.id] orelse {
+ p.mouse_grab = null;
+ return false;
+ };
+ if (pane.serial != grab.serial or !pane.isTerminal()) {
+ p.mouse_grab = null;
+ return false;
+ }
+ // Off the grid the cell runs on past its edge, and the encoder
+ // clamps it there.
+ const r = p.rects[grab.id];
+ const col: i32 = if (at) |a| @as(i32, a.col) - @as(i32, r.x + config.GUTTER) else 0;
+ const row: i32 = if (at) |a| @as(i32, a.row) - @as(i32, p.bodyTop(pane, r)) else 0;
+ const action: panes.terminal.MouseAction = switch (m.kind) {
+ .press => .press,
+ .release => .release,
+ .drag, .motion => .motion,
+ };
+ if (m.kind == .press) grab.held |= bit;
+ if (m.kind == .release) grab.held &= ~bit;
+ const bytes = panes.terminal.mouseBytes(pane, &buf, action, button, mods, col, row, grab.held != 0);
+ if (bytes.len > 0) p.emitWrite(grab.id, bytes);
+ if (grab.held == 0) p.mouse_grab = null;
+ return true;
+ }
+ if (m.shift or p.drag != .none or p.header_drag or p.header_sweep != null) return false;
+ const mapped = at orelse return false;
+ const id = paneAt(p, mapped.col, mapped.row) orelse return false;
+ const pane = p.panes[id].?;
+ if (!tracksMouse(pane)) return false;
+ const cell = programCell(p, id, mapped.col, mapped.row) orelse return false;
+ switch (m.kind) {
+ .press => {
+ if (id != p.active) if (p.panes[p.active]) |previous| {
+ previous.focus = .body;
+ previous.closePrompt();
+ };
+ p.active = id;
+ pane.focus = .body;
+ pane.closePrompt();
+ p.mouse_grab = .{ .id = id, .serial = pane.serial, .held = bit };
+ const bytes = panes.terminal.mouseBytes(pane, &buf, .press, button, mods, cell.col, cell.row, true);
+ if (bytes.len > 0) p.emitWrite(id, bytes);
+ return true;
+ },
+ // A release or drag of a press pardes never passed on stays
+ // pardes's.
+ .release, .drag => return false,
+ .motion => {
+ const bytes = panes.terminal.mouseBytes(pane, &buf, .motion, null, mods, cell.col, cell.row, false);
+ if (bytes.len > 0) p.emitWrite(id, bytes);
+ // Under 1003 the move is the program's; below that pardes
+ // hovers as ever.
+ return panes.terminal.tracksAllMotion(pane);
+ },
+ }
+}
+
pub fn handleMouse(p: *Pardes, m: Mouse) void {
// A click goes somewhere else: the question it leaves unanswered is
// taken back, and the pane stays. A wheel only looks around.
@@ -384,6 +482,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
}
}
p.pointer_inside = true;
+ if (programMouse(p, m)) return;
if (m.button == .none and m.kind == .motion) {
p.raw_hover_intent = true;
look.refreshLookHoverFromRaw(p);
@@ -1258,16 +1357,13 @@ fn chordCutPaste(p: *Pardes, cut: bool) void {
const y = p.registers.text(p.gpa, pardes.Registers.default) orelse return;
if (y.len == 0) return;
if (panes.terminal.reportsMouse(pane)) {
- const col: u16 = @intCast(std.math.clamp(pane.sel[sel_slot].c1 + 1, 1, 9999));
- const row: u16 = @intCast(std.math.clamp(pane.sel[sel_slot].r1 - @as(i32, pane.tag_rows) + 1, 1, 9999));
- var mb: [32]u8 = undefined;
- if (panes.terminal.mouseFormatSgr(pane)) {
- p.emitWrite(s.id, std.fmt.bufPrint(&mb, "\x1b[<0;{d};{d}M\x1b[<0;{d};{d}m", .{ col, row, col, row }) catch return);
- } else {
- // ponytail: legacy X10 bytes; add utf8/urxvt formats if an app ever wants them
- const cb: u8 = @intCast(32 + @min(col, 222));
- const rb: u8 = @intCast(32 + @min(row, 222));
- p.emitWrite(s.id, &[_]u8{ 0x1b, '[', 'M', 32, cb, rb, 0x1b, '[', 'M', 35, cb, rb });
+ // A click where the text goes, so the program's cursor is there.
+ const col = @max(0, pane.sel[sel_slot].c1);
+ const row = @max(0, pane.sel[sel_slot].r1 - @as(i32, pane.tag_rows));
+ var mb: [64]u8 = undefined;
+ for ([_]panes.terminal.MouseAction{ .press, .release }) |action| {
+ const bytes = panes.terminal.mouseBytes(pane, &mb, action, .left, .{}, col, row, action == .press);
+ if (bytes.len > 0) p.emitWrite(s.id, bytes);
}
}
edit.typeToTty(p, s.id, pane, y);
@@ -1897,3 +1993,90 @@ 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("\x1b[<64;6;4M", try ptyWrites(p, &out));
}
+
+test "a program tracking the mouse gets its clicks, drags and moves over its grid as xterm reports them; Shift and chrome stay pardes's" {
+ if (comptime !panes.terminal.enabled) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .cols = 60, .rows = 16, .tty_only = true });
+ defer p.deinit();
+ var frame = std.heap.ArenaAllocator.init(gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ var out: std.ArrayList(u8) = .empty;
+ defer out.deinit(gpa);
+ _ = try ptyWrites(p, &out);
+ const pane = p.panes[0].?;
+ try std.testing.expectEqual(.tty, pane.body.mode);
+ const r = p.rects[0];
+ const col = r.x + config.GUTTER + 5;
+ const row = p.bodyTop(pane, r) + 3;
+ const Ev = struct { button: Mouse.Button, kind: Mouse.Kind, dx: i32 = 0, dy: i32 = 0, shift: bool = false, alt: bool = false, ctrl: bool = false };
+ const send = struct {
+ fn f(pp: *Pardes, o: *std.ArrayList(u8), c: u16, rw: u16, e: Ev) ![]const u8 {
+ pp.update(.{ .mouse = .{
+ .button = e.button,
+ .kind = e.kind,
+ .col = @intCast(@as(i32, c) + e.dx),
+ .row = @intCast(@as(i32, rw) + e.dy),
+ .shift = e.shift,
+ .alt = e.alt,
+ .ctrl = e.ctrl,
+ } });
+ return ptyWrites(pp, o);
+ }
+ }.f;
+ // htop's modes: clicks in SGR, a release its own `m`.
+ p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1049h\x1b[?1h\x1b[?1006;1000h" } });
+ _ = try ptyWrites(p, &out);
+ try std.testing.expectEqualStrings("\x1b[<0;6;4M", try send(p, &out, col, row, .{ .button = .left, .kind = .press }));
+ try std.testing.expect(p.drag == .none);
+ // 1000 has no drags; the release is reported where it lands.
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .left, .kind = .drag, .dx = 1 }));
+ try std.testing.expectEqualStrings("\x1b[<0;7;4m", try send(p, &out, col, row, .{ .button = .left, .kind = .release, .dx = 1 }));
+ // Right with meta and ctrl: 2 + 8 + 16.
+ try std.testing.expectEqualStrings("\x1b[<26;6;4M", try send(p, &out, col, row, .{ .button = .right, .kind = .press, .alt = true, .ctrl = true }));
+ try std.testing.expectEqualStrings("\x1b[<2;6;4m", try send(p, &out, col, row, .{ .button = .right, .kind = .release }));
+ // Moves are no 1000 program's business: pardes hovers.
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .none, .kind = .motion }));
+ // Shift: pardes's own sweep, the program hears nothing of it.
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .left, .kind = .press, .shift = true }));
+ try std.testing.expect(p.drag == .select);
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .left, .kind = .drag, .dx = 2 }));
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .left, .kind = .release, .dx = 2 }));
+ try std.testing.expect(p.drag == .none);
+ // Chrome is never the program's: the tag, and the gutter.
+ const tag_row = p.tagTop(pane, r);
+ try std.testing.expectEqualStrings("", try send(p, &out, col, tag_row, .{ .button = .right, .kind = .press }));
+ try std.testing.expectEqualStrings("", try send(p, &out, col, tag_row, .{ .button = .right, .kind = .release }));
+ try std.testing.expectEqualStrings("", try send(p, &out, r.x, row, .{ .button = .left, .kind = .press }));
+ try std.testing.expectEqualStrings("", try send(p, &out, r.x, row, .{ .button = .left, .kind = .release }));
+
+ // codex's any-event tracking: a drag once per cell, carried past the
+ // grid's edge and clamped there, and bare moves.
+ p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1003h" } });
+ _ = try ptyWrites(p, &out);
+ try std.testing.expectEqualStrings("\x1b[<35;9;4M", try send(p, &out, col, row, .{ .button = .none, .kind = .motion, .dx = 3 }));
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .none, .kind = .motion, .dx = 3 }));
+ try std.testing.expectEqualStrings("\x1b[<0;6;4M", try send(p, &out, col, row, .{ .button = .left, .kind = .press }));
+ try std.testing.expectEqualStrings("\x1b[<32;7;4M", try send(p, &out, col, row, .{ .button = .left, .kind = .drag, .dx = 1 }));
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .left, .kind = .drag, .dx = 1 }));
+ // Up over the tag: row 1, the grid's first.
+ try std.testing.expectEqualStrings("\x1b[<32;7;1M", try send(p, &out, col + 1, tag_row, .{ .button = .left, .kind = .drag }));
+ try std.testing.expectEqualStrings("\x1b[<0;7;1m", try send(p, &out, col + 1, tag_row, .{ .button = .left, .kind = .release }));
+ try std.testing.expect(p.mouse_grab == null);
+ // Over the tag a bare move is pardes's.
+ try std.testing.expectEqualStrings("", try send(p, &out, col, tag_row, .{ .button = .none, .kind = .motion }));
+
+ // Legacy X10 bytes: a release is button 3. (Turning 1003 off turns
+ // tracking off, as xterm's one tracking mode goes.)
+ p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1003l\x1b[?1006l\x1b[?1000h" } });
+ _ = try ptyWrites(p, &out);
+ try std.testing.expectEqualStrings("\x1b[M &$", try send(p, &out, col, row, .{ .button = .left, .kind = .press }));
+ try std.testing.expectEqualStrings("\x1b[M#&$", try send(p, &out, col, row, .{ .button = .left, .kind = .release }));
+
+ // A pane out of the program's raw input (Esc to normal mode) clicks as
+ // pardes clicks.
+ pane.body.mode = .normal;
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .left, .kind = .press }));
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .left, .kind = .release }));
+}
diff --git a/src/pardes.zig b/src/pardes.zig
index d6533b3b..dcf0bf55 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4319,6 +4319,9 @@ pub const Pardes = struct {
unattended: bool = false,
haptic: HapticSlot = if (platform == .macos) .none else {},
drag: mouse.Drag = .none,
+ /// A press a terminal's program took (mouse.programMouse): its drags
+ /// and release are the program's too, wherever the pointer goes.
+ mouse_grab: ?mouse.Grab = null,
hover_col: u16 = 0,
hover_row: u16 = 0,
pointer_raw_col: u16 = 0,
diff --git a/src/terminal.zig b/src/terminal.zig
index 4d16bef2..63c5479f 100644
--- a/src/terminal.zig
+++ b/src/terminal.zig
@@ -380,6 +380,13 @@ pub fn reportsMouse(pane: *const Pane) bool {
return state.vt.flags.mouse_event != .none;
}
+/// ...every move of it, a button down or not (1003).
+pub fn tracksAllMotion(pane: *const Pane) bool {
+ if (comptime !enabled) return false;
+ const state = pane.terminal orelse return false;
+ return state.vt.flags.mouse_event == .any;
+}
+
/// ...and wants them in SGR (1006) rather than the legacy X10 bytes.
pub fn mouseFormatSgr(pane: *const Pane) bool {
if (comptime !enabled) return false;
diff --git a/test/panes.zig b/test/panes.zig
index d7fa3514..6f321abf 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -981,10 +981,12 @@ const TtySelectionTests = struct {
const target = p.rects[1];
const tx = target.x + config.GUTTER + 2;
const ty = bodyY(p, target);
- p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = tx, .row = ty } });
- p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = tx, .row = ty } });
- p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = tx, .row = ty } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = tx, .row = ty } });
+ // Shift: over a program that tracks the mouse the chord is
+ // pardes's only so, and elsewhere Shift changes nothing.
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = tx, .row = ty, .shift = true } });
+ p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = tx, .row = ty, .shift = true } });
+ p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = tx, .row = ty, .shift = true } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = tx, .row = ty, .shift = true } });
var written: std.Io.Writer.Allocating = .init(p.gpa);
defer written.deinit();
while (p.nextEffect()) |effect| {
diff --git a/test/snapshot.zig b/test/snapshot.zig
index a176da7a..5522fbb9 100644
--- a/test/snapshot.zig
+++ b/test/snapshot.zig
@@ -1489,6 +1489,11 @@ fn buttonCode(name: []const u8) ?u16 {
const base = buttonCode(name["ctrl-".len..]) orelse return null;
return base + 16;
}
+ // Shift: over a program that tracks the mouse, pardes's own gesture.
+ if (std.mem.startsWith(u8, name, "shift-")) {
+ const base = buttonCode(name["shift-".len..]) orelse return null;
+ return base + 4;
+ }
if (std.mem.eql(u8, name, "left")) return 0;
if (std.mem.eql(u8, name, "middle")) return 1;
if (std.mem.eql(u8, name, "right")) return 2;
diff --git a/test/snapshots/chordcut.snap b/test/snapshots/chordcut.snap
index 1baef7ec..fddd9e92 100644
--- a/test/snapshots/chordcut.snap
+++ b/test/snapshots/chordcut.snap
@@ -124,9 +124,10 @@ snapstyle clickdismiss
press middle 59 23
release middle 59 23
stable 700 15000
-# tty paste chord: a mouse-listening app gets the click forwarded (SGR,
-# body-relative) then the register between bracketed-paste markers —
-# cat -v prints the received bytes visibly
+# tty paste chord: over a mouse-listening app the chord is pardes's with
+# Shift held; the app gets a click where the text goes (SGR, body-relative)
+# then the register between bracketed-paste markers — cat -v prints the
+# received bytes visibly
press left 35 23
release left 35 23
stable 400 5000
@@ -135,10 +136,10 @@ stable 600 8000
text stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[?1006h'; cat -v
key enter
stable 700 8000
-press left 40 25
-press right 40 25
-release right 40 25
-release left 40 25
+press shift-left 40 25
+press shift-right 40 25
+release shift-right 40 25
+release shift-left 40 25
stable 700 8000
snap ttyprobe
# a plain prompt (mouse modes off) gets no click bytes — just the paste;