summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 18:39:53 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitf99806de546c837ab6f2d5b1e3e8accc95e63a7e (patch)
tree9b4c70a524f36e1966cbfbedd2d85525a3de01b6
parent14dc0b452cf646fe5c9f060fbe66afb01f153ac0 (diff)
downloadpardes-f99806de546c837ab6f2d5b1e3e8accc95e63a7e.tar.gz
pardes-f99806de546c837ab6f2d5b1e3e8accc95e63a7e.zip
Over a program that tracks the mouse, B2 and B3 are pardes's execute and look, as in any pane, and Shift sends them to the program as its buttons 2 and 3; B1 and the wheel stay the program's, Shift keeping them pardes's
The Shift that sends B2 or B3 is left out of their reports and of the drags they hold: it only chose the target. A B2 or B3 sweep of pardes's moves nothing to the program under 1003. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/tags.md18
-rw-r--r--src/CHANGELOG.md7
-rw-r--r--src/gui/gui.zig12
-rw-r--r--src/mouse.zig47
-rw-r--r--test/panes.zig8
-rw-r--r--test/snapshots/chordcut.snap12
6 files changed, 75 insertions, 29 deletions
diff --git a/docs/tags.md b/docs/tags.md
index ec454bb8..43dc6686 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -30,14 +30,16 @@ 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.
+A program that tracks the mouse gets B1 and the wheel; B2 and B3 stay
+pardes's; Shift swaps each. In raw terminal input, a program that asked for
+the mouse (xterm's 1000, 1002 or 1003: htop, vim with `mouse=a`, codex)
+gets B1's clicks and drags and the wheel over its grid, reported in the
+format it chose and with its modifiers; Shift-B1 selects as ever and
+Shift-wheel scrolls pardes's scrollback. B2 and B3 execute and look there
+as in any pane, sweeps and chords included; Shift-B2 and Shift-B3 go to the
+program as its buttons 2 and 3, the Shift left out. 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,
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 0d9c42f4..014744df 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## Unreleased
+
+- Over a program that tracks the mouse, B2 and B3 are pardes's execute and
+ look again, sweeps and chords included; Shift-B2 and Shift-B3 go to the
+ program as its buttons 2 and 3. B1 and the wheel stay the program's, and
+ Shift keeps them pardes's.
+
## 0.0.11
- A look on a diff names a file as the repository does: git's `a/`/`b/`
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 1e58d3ba..ea0e2862 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -5042,7 +5042,7 @@ 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" {
+test "a click over codex in a terminal reaches its pty with its modifiers; Shift swaps B1 to pardes and B2 to the program" {
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 });
@@ -5077,9 +5077,17 @@ test "a click over codex in a terminal reaches its pty with its modifiers; with
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));
+ // Shift-B1 is pardes's select; B2 is pardes's execute and Shift-B2
+ // the program's, reported without the Shift.
+ in.post(.{ .mouse = pointerMouse(.left, .press, mc, .{ .shift = true }) });
+ in.post(.{ .mouse = pointerMouse(.left, .release, mc, .{ .shift = true }) });
+ try std.testing.expectEqualStrings("", try take(core, &sent));
+ in.post(.{ .mouse = pointerMouse(.middle, .press, mc, .{}) });
+ in.post(.{ .mouse = pointerMouse(.middle, .release, mc, .{}) });
+ try std.testing.expectEqualStrings("", 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));
+ try std.testing.expectEqualStrings("\x1b[<1;6;4M\x1b[<1;6;4m", try take(core, &sent));
}
test "a wheel over codex in a terminal reaches its pty as the mouse reports it asked for" {
diff --git a/src/mouse.zig b/src/mouse.zig
index ecb9bb57..41362249 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -335,7 +335,14 @@ 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 };
+pub const Grab = struct {
+ id: usize,
+ serial: u32,
+ held: u3 = 0,
+ /// Taken by Shift-B2 or Shift-B3: the Shift chose the program, so its
+ /// reports leave it out.
+ unshift: bool = false,
+};
/// The 0-based cell of a terminal program's grid under screen cell
/// `col`,`row`, or null over chrome (tag, gutter, scrollbar, anything past
@@ -360,8 +367,10 @@ fn tracksMouse(pane: *const Pane) bool {
/// 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.
+/// chrome are never its. B1 is the program's and Shift-B1 pardes's select;
+/// B2 and B3 are pardes's execute and look, and Shift-B2 and Shift-B3 the
+/// program's, reported without the Shift that chose it. 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,
@@ -372,9 +381,10 @@ fn programMouse(p: *Pardes, m: Mouse) bool {
};
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 mods: panes.terminal.MouseMods = .{ .shift = m.shift, .alt = m.alt, .ctrl = m.ctrl };
var buf: [64]u8 = undefined;
if (p.mouse_grab) |*grab| {
+ if (grab.unshift) mods.shift = false;
const pane = p.panes[grab.id] orelse {
p.mouse_grab = null;
return false;
@@ -400,7 +410,10 @@ fn programMouse(p: *Pardes, m: Mouse) bool {
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;
+ if (p.drag != .none or p.header_drag or p.header_sweep != null) return false;
+ const shifted_to_program = m.button == .middle or m.button == .right;
+ if (m.shift != shifted_to_program) return false;
+ if (shifted_to_program) mods.shift = false;
const mapped = at orelse return false;
const id = paneAt(p, mapped.col, mapped.row) orelse return false;
const pane = p.panes[id].?;
@@ -415,7 +428,7 @@ fn programMouse(p: *Pardes, m: Mouse) bool {
p.active = id;
pane.focus = .body;
pane.closePrompt();
- p.mouse_grab = .{ .id = id, .serial = pane.serial, .held = bit };
+ p.mouse_grab = .{ .id = id, .serial = pane.serial, .held = bit, .unshift = shifted_to_program };
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;
@@ -2033,9 +2046,17 @@ test "a program tracking the mouse gets its clicks, drags and moves over its gri
// 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 }));
+ // B2 and B3 are pardes's execute and look, as in any pane.
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .right, .kind = .press }));
+ try std.testing.expect(p.drag == .select);
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .right, .kind = .release }));
+ try std.testing.expect(p.drag == .none);
+ // Shift-B3 is the program's, the Shift that chose it left out: meta
+ // and ctrl, 2 + 8 + 16.
+ try std.testing.expectEqualStrings("\x1b[<26;6;4M", try send(p, &out, col, row, .{ .button = .right, .kind = .press, .shift = true, .alt = true, .ctrl = true }));
+ try std.testing.expectEqualStrings("\x1b[<2;6;4m", try send(p, &out, col, row, .{ .button = .right, .kind = .release, .shift = true }));
+ try std.testing.expectEqualStrings("\x1b[<1;6;4M", try send(p, &out, col, row, .{ .button = .middle, .kind = .press, .shift = true }));
+ try std.testing.expectEqualStrings("\x1b[<1;6;4m", try send(p, &out, col, row, .{ .button = .middle, .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.
@@ -2066,6 +2087,14 @@ test "a program tracking the mouse gets its clicks, drags and moves over its gri
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 }));
+ // B2 held for pardes: its sweep's moves are not the program's; held
+ // for the program with Shift, they are its button-2 drags.
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .middle, .kind = .press }));
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .middle, .kind = .drag, .dx = 1 }));
+ try std.testing.expectEqualStrings("", try send(p, &out, col, row, .{ .button = .middle, .kind = .release, .dx = 1 }));
+ try std.testing.expectEqualStrings("\x1b[<1;6;4M", try send(p, &out, col, row, .{ .button = .middle, .kind = .press, .shift = true }));
+ try std.testing.expectEqualStrings("\x1b[<33;7;4M", try send(p, &out, col, row, .{ .button = .middle, .kind = .drag, .dx = 1, .shift = true }));
+ try std.testing.expectEqualStrings("\x1b[<1;7;4m", try send(p, &out, col, row, .{ .button = .middle, .kind = .release, .dx = 1, .shift = true }));
// Legacy X10 bytes: a release is button 3. (Turning 1003 off turns
// tracking off, as xterm's one tracking mode goes.)
diff --git a/test/panes.zig b/test/panes.zig
index 6f321abf..935defb5 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -981,11 +981,11 @@ const TtySelectionTests = struct {
const target = p.rects[1];
const tx = target.x + config.GUTTER + 2;
const ty = bodyY(p, target);
- // Shift: over a program that tracks the mouse the chord is
- // pardes's only so, and elsewhere Shift changes nothing.
+ // Over a program that tracks the mouse B1 is its own, so
+ // the chord's B1 holds Shift; B3 is pardes's either way.
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 = .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 = true } });
var written: std.Io.Writer.Allocating = .init(p.gpa);
defer written.deinit();
diff --git a/test/snapshots/chordcut.snap b/test/snapshots/chordcut.snap
index fddd9e92..b1f164a2 100644
--- a/test/snapshots/chordcut.snap
+++ b/test/snapshots/chordcut.snap
@@ -124,10 +124,10 @@ snapstyle clickdismiss
press middle 59 23
release middle 59 23
stable 700 15000
-# 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
+# tty paste chord: over a mouse-listening app B1 is the app's, so the
+# chord's B1 holds Shift (B3 is pardes's either way); 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
@@ -137,8 +137,8 @@ text stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[?1006h'; cat -v
key enter
stable 700 8000
press shift-left 40 25
-press shift-right 40 25
-release shift-right 40 25
+press right 40 25
+release right 40 25
release shift-left 40 25
stable 700 8000
snap ttyprobe