diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-02 18:10:28 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | 62ef1882ab7878210f05f06504796849e412c318 (patch) | |
| tree | 5c29cf9b3ef36bf09eb17469ecc7cc1c4246a583 /main.zig | |
| parent | d9e394f92f0f2b91af2212ff0fef9e93dd9d42b1 (diff) | |
| download | pardes-62ef1882ab7878210f05f06504796849e412c318.tar.gz pardes-62ef1882ab7878210f05f06504796849e412c318.zip | |
added middle+left mouse chord
Diffstat (limited to 'main.zig')
| -rw-r--r-- | main.zig | 109 |
1 files changed, 91 insertions, 18 deletions
@@ -1551,9 +1551,14 @@ fn renderPane(root: vaxis.Window, alloc: std.mem.Allocator, t: *Term, r: Rect, a }; // recolor selected cells (pane-local visual coords), one pass per button — - // later buttons win on overlap; only while actively dragging. + // later buttons win on overlap. .dragging = a live drag (any button); a left + // .done is a FINISHED selection that is kept highlighted after release (it + // clears on the next left click or a new left drag, and feeds the middle+left + // chord as the argument). middle/right .done are transient (they fire their + // action on release) so they are not kept visible. for (t.sel, 0..) |sl, b| { - if (sl.state != .dragging) continue; + if (sl.state == .none) continue; + if (sl.state == .done and b != @intFromEnum(Button.left)) continue; const r0 = @min(sl.r0, sl.r1); const r1 = @max(sl.r0, sl.r1); const c0 = @max(0, @min(sl.c0, sl.c1)); @@ -2655,6 +2660,10 @@ fn focusDir(active: *usize, terms: *const [MAX_TERMS]?*Term, rects: *const [MAX_ // .middle = "execute" (a builtin, else run the text in a shell). Shared by the // mouse release and the normal-mode Enter (look) / Tab (execute) keys. Returns // true if the app should quit (the last pane was closed). +// +// `arg` is the middle+left chord argument: a kept left selection captured by a +// left press during the middle drag. For a CLI (`.send`) it is appended as a +// trailing argument; each builtin arm decides for itself whether to use it. fn actOnSelection( io: std.Io, gpa: std.mem.Allocator, @@ -2669,6 +2678,7 @@ fn actOnSelection( button: Button, id: usize, txt: []const u8, + arg: ?[]const u8, ) bool { const t = terms[id] orelse return false; // image-pane tagline toggles (middle-click, acme execute): Petscii on/off, the @@ -2827,6 +2837,15 @@ fn actOnSelection( if (dst) |dt| { padOutputBelowEdits(dt); termWrite(dt, cmd); + // a chord argument (a kept left selection) is passed to the CLI as a + // trailing argument; builtins above decide for themselves (most ignore). + if (arg) |a| { + const at = std.mem.trim(u8, a, " \t\r\n"); + if (at.len > 0) { + termWrite(dt, " "); + termWrite(dt, at); + } + } termWrite(dt, "\r"); } }, @@ -3030,6 +3049,11 @@ pub fn main(init: std.process.Init) !void { var hover_col: u16 = 0; // last mouse position; used to tint the resize border under it var hover_row: u16 = 0; var ctrl_w_pending: bool = false; // Ctrl-w prefix for helix-style pane focus + // middle+left chord argument: a kept left selection captured by a left press + // during an active middle select-drag. gpa-owned; consumed (and freed) on the + // middle release, freed on overwrite / app exit. + var chord_arg: ?[]u8 = null; + defer if (chord_arg) |a| gpa.free(a); while (true) { const event = try loop.nextEvent(); @@ -3047,6 +3071,18 @@ pub fn main(init: std.process.Init) !void { if (mcol >= r.x and mcol < r.x + r.w and mrow >= r.y and mrow < r.y + r.h) break i; } else null; + // Any press that is NOT the middle+left chord (a left press while a + // middle select-drag is active) abandons a pending chord argument; + // free it so it can't leak or feed a later, unrelated release. + if (mouse.type == .press and + !(mouse.button == .left and drag == .select and drag.select.button == .middle)) + { + if (chord_arg) |a| { + gpa.free(a); + chord_arg = null; + } + } + if (mouse.button == .wheel_down) { if (hovered) |id| paneScrollBy(terms[id].?, 1); } else if (mouse.button == .wheel_up) { @@ -3059,11 +3095,27 @@ pub fn main(init: std.process.Init) !void { if (mouse.button == .middle) { const word = wordAtCol(topbar_str, mcol); if (word.len > 0) - if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, .middle, active, word)) break; + if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, .middle, active, word, null)) break; } } else if (mouse.type == .press and mouse.button == .left) { var handled = false; + // middle+left chord: a left press while a middle select-drag is + // active captures the KEPT left selection as the argument for the + // middle execute (fired on the middle release below). Don't start + // a new left selection — the middle drag runs until its own + // release, which dispatches the command with chord_arg. + if (drag == .select and drag.select.button == .middle) { + handled = true; + if (terms[drag.select.id]) |t| { + const sl = t.sel[@intFromEnum(Button.left)]; + if (sl.state == .done) { + if (chord_arg) |old| gpa.free(old); + chord_arg = selectionText(gpa, t, sl) catch null; + } + } + } + // vertical resize handle = the LEFT pane's last column (its own // content, not the right pane's gutter). The border check runs before // the pane hit test, so a press there resizes instead of clicking. @@ -3183,6 +3235,7 @@ pub fn main(init: std.process.Init) !void { .none => {}, } } else if (mouse.type == .release) { + var end_drag = true; switch (drag) { .border_v => |d| { const c = d.left_col; @@ -3227,17 +3280,30 @@ pub fn main(init: std.process.Init) !void { }; }, .select => |s| { - if (terms[s.id]) |t| { + // A release of a button OTHER than the one driving this + // drag is the middle+left chord's left release: ignore it + // so the middle drag keeps going until its own release + // runs the command (with chord_arg). end_drag stays false. + const released: ?Button = switch (mouse.button) { + .left => .left, + .middle => .middle, + .right => .right, + else => null, + }; + if (released == null or released.? != s.button) { + end_drag = false; + } else if (terms[s.id]) |t| { t.sel[@intFromEnum(s.button)].state = .done; if (s.button == .left) { - // left-click in the body enters NORMAL mode and - // anchors the cursor at the selection end. The row is - // stored absolute (visual + scroll off) so it rides - // the scrollback. tag clicks (row < BOX_H) don't edit. - // In tty mode we DON'T do this: the mouse stays usable - // for selecting/copying without yanking you out of the - // shell — the selection is still recorded above. + // left release: keep the selection highlighted if a + // real drag selected a range (it stays until the next + // left click / drag); a plain click (no movement) + // clears it — the selection "goes away" on a left + // click. Either way pin the cursor + enter normal + // mode on text panes (tty keeps the mouse usable). const sl = t.sel[@intFromEnum(Button.left)]; + const dragged = sl.c0 != sl.c1 or sl.r0 != sl.r1; + if (!dragged) t.sel[@intFromEnum(Button.left)].state = .none; const body_vis = sl.r1 - @as(i32, BOX_H); if (body_vis >= 0 and t.mode != .tty) { const off: i32 = paneScroll(t); @@ -3254,17 +3320,24 @@ pub fn main(init: std.process.Init) !void { } else { // middle/right: acme execute/look. A no-drag click // expands to the file-ish word under the cursor first. + // chord_arg (a kept left selection captured by a left + // press during this drag) is passed as the argument to + // the command; builtins decide what to do with it. expandSel(gpa, t, &t.sel[@intFromEnum(s.button)]); - if (selectionText(gpa, t, t.sel[@intFromEnum(s.button)])) |txt| { + const maybe_txt = selectionText(gpa, t, t.sel[@intFromEnum(s.button)]) catch null; + const arg = chord_arg; + chord_arg = null; + defer if (arg) |a| gpa.free(a); + if (maybe_txt) |txt| { defer gpa.free(txt); - if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, s.button, s.id, txt)) break; - } else |_| {} + if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, s.button, s.id, txt, arg)) break; + } } } }, .none => {}, } - drag = .none; + if (end_drag) drag = .none; } }, .key_press => |key| kpress: { @@ -3335,7 +3408,7 @@ pub fn main(init: std.process.Init) !void { if (at.tag_edit) { if (tagChordText(frame_arena.allocator(), at)) |txt| { at.tag_sel = false; - if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, btn, active, txt)) break; + if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, btn, active, txt, null)) break; } break :kpress; } @@ -3343,13 +3416,13 @@ pub fn main(init: std.process.Init) !void { if (currentSelText(frame_arena.allocator(), at)) |txt| { at.vsel.active = false; at.msel.active = false; - if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, btn, active, txt)) break; + if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, btn, active, txt, null)) break; } else { var sel = cursorWordSel(at); expandSel(gpa, at, &sel); if (selectionText(gpa, at, sel)) |word| { defer gpa.free(word); - if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, btn, active, word)) break; + if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, btn, active, word, null)) break; } else |_| {} } break :kpress; |
