summaryrefslogtreecommitdiff
path: root/main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-02 18:10:28 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit62ef1882ab7878210f05f06504796849e412c318 (patch)
tree5c29cf9b3ef36bf09eb17469ecc7cc1c4246a583 /main.zig
parentd9e394f92f0f2b91af2212ff0fef9e93dd9d42b1 (diff)
downloadpardes-62ef1882ab7878210f05f06504796849e412c318.tar.gz
pardes-62ef1882ab7878210f05f06504796849e412c318.zip
added middle+left mouse chord
Diffstat (limited to 'main.zig')
-rw-r--r--main.zig109
1 files changed, 91 insertions, 18 deletions
diff --git a/main.zig b/main.zig
index 205896d2..582fc133 100644
--- a/main.zig
+++ b/main.zig
@@ -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;