From f99806de546c837ab6f2d5b1e3e8accc95e63a7e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 18:39:53 -0300 Subject: 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 --- docs/tags.md | 18 +++++++++-------- src/CHANGELOG.md | 7 +++++++ src/gui/gui.zig | 12 +++++++++-- src/mouse.zig | 47 +++++++++++++++++++++++++++++++++++--------- test/panes.zig | 8 ++++---- test/snapshots/chordcut.snap | 12 +++++------ 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 -- cgit v1.3