diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 14:39:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | e56a8ba396566eaab818b6c742850b5842e880bb (patch) | |
| tree | 346e0ccf2b1a738bee2b7a185ff560e324c85417 /src/mouse.zig | |
| parent | 1d0f8cd158bb47941c0187d32422787c92e8feeb (diff) | |
| parent | 387d6a39bad76c5eb2f56328f7cf7869a9a10559 (diff) | |
| download | pardes-e56a8ba396566eaab818b6c742850b5842e880bb.tar.gz pardes-e56a8ba396566eaab818b6c742850b5842e880bb.zip | |
Merge: round 26 + codex wheel (the 0.8 candidate)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/mouse.zig')
| -rw-r--r-- | src/mouse.zig | 85 |
1 files changed, 83 insertions, 2 deletions
diff --git a/src/mouse.zig b/src/mouse.zig index 2280020d..a0ec6e47 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -310,6 +310,23 @@ pub fn sameBodyCell(a: ?Mouse.BodyHit, b: ?Mouse.BodyHit) bool { return first.serial == second.serial and first.row == second.row and first.col == second.col; } +/// 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 { + 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; + for (0..@intCast(config.wheel_rows)) |_| p.emitWrite(id, bytes); + return true; +} + 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. @@ -508,7 +525,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 + else if (!programWheel(p, id, if (m.button == .wheel_up) .up else .down, mcol, mrow, m.ctrl)) pane.scrollBy(if (m.button == .wheel_up) -config.wheel_rows else config.wheel_rows); }, .wheel_left, .wheel_right => { @@ -516,7 +533,9 @@ 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 (pane.hasPdf()) { + if (programWheel(p, id, if (m.button == .wheel_right) .right else .left, mcol, mrow, m.ctrl)) { + // the program's + } else if (pane.hasPdf()) { panes.Pdf.horizontalWheel(p, pane, if (m.button == .wheel_right) 1 else -1); // ponytail: no right clamp — overscroll shows blank and the // next cursor move or left wheel pulls it back @@ -1793,3 +1812,65 @@ test "a chorded exec an event reader holds is told with its argument, and writte } _ = th.call(p, .{ .tag = 4, .op = .release, .node = event, .handle = h }); } + +/// Every byte the core wrote to pane 0's pty since the last call. +fn ptyWrites(p: *Pardes, out: *std.ArrayList(u8)) ![]const u8 { + out.clearRetainingCapacity(); + while (p.nextEffect()) |effect| { + if (effect == .write and effect.write.pane == 0) try out.appendSlice(p.gpa, effect.write.bytes.slice()); + } + return out.items; +} + +test "a wheel over a program that owns it is the program's, in the form it asked for; a shell's scrolls" { + 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].?; + const r = p.rects[0]; + // 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 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" }, + // 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" }, + // codex's sign-in, less: the alternate screen asking for arrow keys + // (1007), in the cursor keys' mode. + .{ .modes = "\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1007h", .button = .wheel_down, .sent = "\x1b[B" }, + .{ .modes = "\x1b[?1h", .button = .wheel_up, .sent = "\x1bOA" }, + .{ .modes = "", .button = .wheel_right, .sent = "" }, + // With alternate scroll off the alternate screen keeps the wheel. + .{ .modes = "\x1b[?1007l", .button = .wheel_up, .sent = "" }, + }; + 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 } }); + try std.testing.expectEqualStrings(case.sent, try ptyWrites(p, &out)); + } + // A shell on the main screen: the wheel scrolls its scrollback, and + // the shell hears nothing. + p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1l\x1b[?1049l" } }); + for (0..40) |i| { + var line: [16]u8 = undefined; + p.update(.{ .output = .{ .pane = 0, .bytes = try std.fmt.bufPrint(&line, "{d}\r\n", .{i}) } }); + } + _ = try p.render(frame.allocator()); + _ = try ptyWrites(p, &out); + const was = pane.scroll(); + 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); +} |
