From 8eeab5df9c09caafc07c93c2fc3baf6e6700afed Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 1 Oct 2026 12:44:06 -0300 Subject: Alt/Option-click is B2 and Super/Cmd-click is B3, sweeps included, as in plan9port: a one-button trackpad can execute and look The core decides it at the press (mouse.emulateButton) and keeps it to the release, whatever keys are let go between. Ctrl-click stays a definition, and a program tracking the mouse still gets the plain click (Alt as its meta, Super as nothing). The SDL, macOS and web shells now pass Super; the macOS and web shells pass Alt on a click too. A terminal's mouse report has no Super bit, so in a terminal only Alt-click is B2. Co-Authored-By: Claude Opus 5.5 --- src/gui/gui.zig | 5 ++-- src/macos.zig | 3 ++ src/macos/Sources/PardesView.swift | 1 + src/macos/pardes.h | 2 ++ src/mouse.zig | 58 ++++++++++++++++++++++++++++++++++++-- src/pardes.zig | 6 ++++ src/web.zig | 3 ++ src/web/app.mjs | 2 +- 8 files changed, 75 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/gui/gui.zig b/src/gui/gui.zig index e8dc99b9..65b3f926 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -4020,7 +4020,7 @@ fn wheelAt(g: *Gui, in: *Input, w: WheelTurn, mc: MouseCell) void { 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 }; +const PointerMods = struct { ctrl: bool = false, shift: bool = false, alt: bool = false, super: 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 { @@ -4035,12 +4035,13 @@ fn pointerMouse(button: pardes.Mouse.Button, kind: pardes.Mouse.Kind, mc: MouseC .ctrl = mods.ctrl, .shift = mods.shift, .alt = mods.alt, + .super = mods.super, }; } 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 }; + return .{ .ctrl = (m & c.SDL_KMOD_CTRL) != 0, .shift = (m & c.SDL_KMOD_SHIFT) != 0, .alt = (m & c.SDL_KMOD_ALT) != 0, .super = (m & c.SDL_KMOD_GUI) != 0 }; } fn mouseCellWithGeometry(g: *const Gui, core: ?*const pardes.Pardes, x: f32, y: f32, geometry: WindowGeometry) ?MouseCell { diff --git a/src/macos.zig b/src/macos.zig index e839816c..20463481 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -1725,6 +1725,9 @@ fn mouseWithHit(button_arg: c_int, kind_arg: c_int, col: u16, row: u16, mods: u3 .col = col, .row = row, .ctrl = mods & 1 != 0, + // Option-click is B2 and Cmd-click B3 (mouse.emulateButton). + .alt = mods & 2 != 0, + .super = mods & 8 != 0, } }); } diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift index 8ba9c84b..4ea4475a 100644 --- a/src/macos/Sources/PardesView.swift +++ b/src/macos/Sources/PardesView.swift @@ -562,6 +562,7 @@ private func modifiers(_ flags: NSEvent.ModifierFlags) -> UInt32 { if flags.contains(.control) { mods |= UInt32(PARDES_MOD_CTRL) } if flags.contains(.option) { mods |= UInt32(PARDES_MOD_ALT) } if flags.contains(.shift) { mods |= UInt32(PARDES_MOD_SHIFT) } + if flags.contains(.command) { mods |= UInt32(PARDES_MOD_SUPER) } return mods } diff --git a/src/macos/pardes.h b/src/macos/pardes.h index 46453429..33db3bb5 100644 --- a/src/macos/pardes.h +++ b/src/macos/pardes.h @@ -141,6 +141,8 @@ typedef struct { #define PARDES_MOD_CTRL 0x0001u #define PARDES_MOD_ALT 0x0002u #define PARDES_MOD_SHIFT 0x0004u +// Command. Only a click reads it (Cmd-click is B3); a Command key chord never reaches the core. +#define PARDES_MOD_SUPER 0x0008u // Keys that carry no text. The four editing keys are their ASCII controls; // everything else lives in a private-use plane so it can never collide with a diff --git a/src/mouse.zig b/src/mouse.zig index 41362249..21961cf8 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -446,7 +446,23 @@ fn programMouse(p: *Pardes, m: Mouse) bool { } } -pub fn handleMouse(p: *Pardes, m: Mouse) void { +/// plan9port's convention for a mouse or trackpad with one button: +/// Alt-click is B2 and Super-click (Cmd) is B3, sweeps included, decided at +/// the press and kept through its release, whatever keys are let go +/// meanwhile. Ctrl-click keeps its own meaning (a definition). Run after +/// programMouse: a program that takes the click gets it as it was. +fn emulateButton(p: *Pardes, m: *Mouse) void { + if (m.button != .left) return; + if (m.kind == .press) p.click_as = if (m.ctrl) null else if (m.super) .right else if (m.alt) .middle else null; + const as = p.click_as orelse return; + m.button = as; + m.alt = false; + m.super = false; + if (m.kind == .release) p.click_as = null; +} + +pub fn handleMouse(p: *Pardes, event: Mouse) void { + var m = event; // A click goes somewhere else: the question it leaves unanswered is // taken back, and the pane stays. A wheel only looks around. if (m.kind == .press and switch (m.button) { @@ -496,6 +512,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { } p.pointer_inside = true; if (programMouse(p, m)) return; + emulateButton(p, &m); if (m.button == .none and m.kind == .motion) { p.raw_hover_intent = true; look.refreshLookHoverFromRaw(p); @@ -2007,6 +2024,35 @@ test "a wheel over a program that owns it is the program's, in the form it asked try std.testing.expectEqualStrings("\x1b[<64;6;4M", try ptyWrites(p, &out)); } +test "Alt-click is B2 and Super-click B3, kept from the press through the release; Ctrl-click stays B1" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const file = try p.setTestFile("Newcol here\n"); + p.acknowledgePanelPresentation(&.{}); + const r = p.rects[0]; + const col = r.x + config.GUTTER + panes.File.gutterWidth(file) + 1; + const row = p.bodyTop(file, r); + // A Super-press sweeps with B3's selection, not B1's. + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = col, .row = row, .super = true } }); + try std.testing.expect(p.drag == .select and file.sel[2].state != .none and file.sel[0].state == .none); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = col, .row = row } }); + try std.testing.expect(p.drag == .none); + try std.testing.expectEqual(@as(?Mouse.Button, null), p.click_as); + // Ctrl with Alt is Ctrl's: a B1 click (a definition), never B2. + const cols = p.ncol; + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = col, .row = row, .ctrl = true, .alt = true } }); + try std.testing.expectEqual(@as(?Mouse.Button, null), p.click_as); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = col, .row = row, .ctrl = true } }); + try std.testing.expectEqual(cols, p.ncol); + // Alt-click on Newcol runs it, as B2 does, though Alt is let go + // before the button. + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = col, .row = row, .alt = true } }); + try std.testing.expectEqual(@as(?Mouse.Button, .middle), p.click_as); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = col, .row = row } }); + try std.testing.expectEqual(cols + 1, p.ncol); + try std.testing.expectEqual(@as(?Mouse.Button, null), p.click_as); +} + 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; @@ -2023,7 +2069,7 @@ test "a program tracking the mouse gets its clicks, drags and moves over its gri 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 Ev = struct { button: Mouse.Button, kind: Mouse.Kind, dx: i32 = 0, dy: i32 = 0, shift: bool = false, alt: bool = false, ctrl: bool = false, super: bool = false }; const send = struct { fn f(pp: *Pardes, o: *std.ArrayList(u8), c: u16, rw: u16, e: Ev) ![]const u8 { pp.update(.{ .mouse = .{ @@ -2034,6 +2080,7 @@ test "a program tracking the mouse gets its clicks, drags and moves over its gri .shift = e.shift, .alt = e.alt, .ctrl = e.ctrl, + .super = e.super, } }); return ptyWrites(pp, o); } @@ -2046,6 +2093,13 @@ 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 })); + // An Alt- or Super-click (B2, B3 elsewhere) is still the program's + // button 1, Alt its meta and Super no bit at all. + try std.testing.expectEqualStrings("\x1b[<8;6;4M", try send(p, &out, col, row, .{ .button = .left, .kind = .press, .alt = true })); + try std.testing.expectEqualStrings("\x1b[<0;6;4m", try send(p, &out, col, row, .{ .button = .left, .kind = .release })); + try std.testing.expectEqualStrings("\x1b[<0;6;4M", try send(p, &out, col, row, .{ .button = .left, .kind = .press, .super = true })); + try std.testing.expectEqualStrings("\x1b[<0;6;4m", try send(p, &out, col, row, .{ .button = .left, .kind = .release, .super = true })); + try std.testing.expect(p.drag == .none); // 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); diff --git a/src/pardes.zig b/src/pardes.zig index 41ed0e5a..38373266 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3515,6 +3515,9 @@ pub const Mouse = struct { shift: bool = false, /// Alt, which xterm's mouse reports call meta. alt: bool = false, + /// Super: Cmd on a Mac, the Windows key elsewhere. A terminal's mouse + /// report has no such bit, so only a pixel shell sets it. + super: 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, @@ -4414,6 +4417,9 @@ pub const Pardes = struct { /// The last plain B1 click, for acme's double-click (text.c:881, /// textselect: a second click at the same point within 500 ms). last_click: ?struct { id: usize, on_tag: bool, row: i32, col: i32, ms: i64 } = null, + /// The button an Alt- or Super-click of button 1 stands for, from its + /// press through its release (mouse.emulateButton). + click_as: ?Mouse.Button = null, /// A B1 chord's edits share the undo point it took (mouse.zig). chord_hold: bool = false, /// The look just run found nothing (look.zig missed). diff --git a/src/web.zig b/src/web.zig index ac0ecbbf..fed2ba5a 100644 --- a/src/web.zig +++ b/src/web.zig @@ -266,6 +266,9 @@ fn mouseWithHit(button_arg: u32, kind_arg: u32, col_arg: u32, row_arg: u32, mods .col = @intCast(@min(col_arg, std.math.maxInt(u16))), .row = @intCast(@min(row_arg, std.math.maxInt(u16))), .ctrl = mods & 1 != 0, + // Alt-click is B2 and Cmd/Super-click B3 (mouse.emulateButton). + .alt = mods & 2 != 0, + .super = mods & 8 != 0, } }); } diff --git a/src/web/app.mjs b/src/web/app.mjs index c053d663..d2405cf4 100644 --- a/src/web/app.mjs +++ b/src/web/app.mjs @@ -678,7 +678,7 @@ function rgb(r, g, b) { } function modifiers(event) { - return (event.ctrlKey ? 1 : 0) | (event.altKey ? 2 : 0) | (event.shiftKey ? 4 : 0); + return (event.ctrlKey ? 1 : 0) | (event.altKey ? 2 : 0) | (event.shiftKey ? 4 : 0) | (event.metaKey ? 8 : 0); } function pointerButton(button) { -- cgit v1.3