From c4cb26fe6f061dd4bcfb9e9df7b7d4e4d434d10a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 7 Jul 2026 22:12:33 -0300 Subject: acme mouse chords: 1-2 Cut / 1-3 Paste while a left selection drag holds (both in one hold = Snarf/copy; the chorded left release is inert), plus 2-3 / 3-1 / 3-2 cancel an execute or look drag mid-gesture (the 2-1 argument chord already existed). chordcut.snap covers cut/copy/paste and both cancels; tutor documents the chords. --- src/pardes.zig | 90 ++++++++++++- src/tutor.txt | 16 ++- test/snapshots/chordcut.golden | 279 +++++++++++++++++++++++++++++++++++++++++ test/snapshots/chordcut.snap | 101 +++++++++++++++ 4 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 test/snapshots/chordcut.golden create mode 100644 test/snapshots/chordcut.snap diff --git a/src/pardes.zig b/src/pardes.zig index f647ed00..9397f445 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -659,7 +659,9 @@ const Drag = union(enum) { border_v: struct { left_col: usize, cur_x: u16 }, border_h: struct { col: usize, top_idx: usize, cur_y: u16 }, move: struct { id: usize, cur_x: u16, cur_y: u16 }, - select: struct { id: usize, button: Mouse.Button }, + /// chorded: a 1-2/1-3 cut/paste chord fired during this left drag — + /// the drag's own release is then inert + select: struct { id: usize, button: Mouse.Button, chorded: bool = false }, }; const Rect = struct { x: u16, y: u16, w: u16, h: u16 }; @@ -3549,6 +3551,13 @@ pub const Pardes = struct { } return; } + // acme 3-1: a left tap during a right (look) drag cancels + // the look; the rest of the gesture is inert + if (p.drag == .select and p.drag.select.button == .right) { + if (p.panes[p.drag.select.id]) |t| t.sel[@intFromEnum(Mouse.Button.right)].state = .none; + p.drag = .none; + return; + } if (mrow < TOPBAR_H) return; // topbar: left click deliberately inert // resize handles first: a pane's own trailing edge (v: the // left column's last col; h: the upper pane's last row) @@ -3596,6 +3605,19 @@ pub const Pardes = struct { }, .middle, .right => switch (m.kind) { .press => { + // acme 1-2 / 1-3: middle (Cut) or right (Paste) tapped + // while the left button holds a selection drag + if (p.drag == .select and p.drag.select.button == .left) { + p.chordCutPaste(m.button == .middle); + return; + } + // acme 2-3 / 3-2: tapping the OTHER of middle/right during + // an execute or look drag cancels it (textselect23's mask) + if (p.drag == .select) { + if (p.panes[p.drag.select.id]) |t| t.sel[@intFromEnum(p.drag.select.button)].state = .none; + p.drag = .none; + return; + } if (mrow < TOPBAR_H) { // global tagbar: MIDDLE runs the command under the click // (a stray left click must never Kill) @@ -3713,6 +3735,11 @@ pub const Pardes = struct { }, .select => |s| { const pane = p.panes[s.id] orelse return; + if (s.chorded) { + // a cut/paste chord consumed this drag; the release is inert + pane.sel[@intFromEnum(s.button)].state = .none; + return; + } const b = @intFromEnum(s.button); pane.sel[b].state = .done; if (s.button == .left) { @@ -3749,6 +3776,67 @@ pub const Pardes = struct { } } + /// acme 1-2 (Cut) / 1-3 (Paste): middle or right tapped while the left + /// button holds a selection drag. The first tap converts the mouse + /// selection into the modal one (vsel anchored at the drag start, cursor + /// at its end; a plain click is a bare cursor). Cut yanks + deletes the + /// selection; Paste replaces it with the yank register, or splices the + /// register in literally at a bare cursor — so 1-2 then 1-3 in one hold + /// nets out to Snarf (copy: the text returns, the register keeps it). + fn chordCutPaste(p: *Pardes, cut: bool) void { + const s = &p.drag.select; + const pane = p.panes[s.id] orelse return; + if (!s.chorded) { + s.chorded = true; + const sl = pane.sel[0]; + pane.sel[0].state = .none; + if (pane.mode == .tty) return; // the pty owns the cursor there + const pfx: i32 = if (pane.file != null) PREFIX_W else 0; + const hs: i32 = if (pane.file != null) pane.hscroll else 0; + const row0 = @max(0, sl.r0 - @as(i32, BOX_H)) + pane.scroll(); + const row1 = @max(0, sl.r1 - @as(i32, BOX_H)) + pane.scroll(); + const col0 = @max(0, sl.c0 - pfx) + hs; + const col1 = @max(0, sl.c1 - pfx) + hs; + pane.cur_row = row1; + pane.cur_col = col1; + pane.cur_off = 0; + pane.cur_pinned = true; + pane.msel.active = false; + pane.pending = 0; + if (!pane.isTerminal()) pane.mode = .normal; + pane.vsel = .{ .active = row0 != row1 or col0 != col1, .row = row0, .col = col0, .explicit = false }; + } + if (pane.mode == .tty) return; + if (cut) { + if (pane.vsel.active) p.normalDelete(pane, true); + } else if (pane.vsel.active) { + p.normalReplaceYank(pane); + } else if (pane.file) |*f| { + // bare cursor: splice the register in literally (no linewise + // magic) so a paste right after a cut restores the text exactly + const y = p.yank orelse return; + if (y.len == 0) return; + const at = modal.Cursor{ .row = @intCast(@max(0, pane.cur_row)), .col = @intCast(@max(0, pane.cur_col)) }; + p.pushUndo(pane); + const new = modal.insertAt(p.gpa, f.content, at, y) catch return; + p.setFileContent(f, new); + pane.vsel = .{ .active = y.len > 1, .row = @intCast(at.row), .col = @intCast(at.col), .explicit = false }; + const end = modal.advanceBy(at, y); + if (end.col > 0) { + pane.cur_row = @intCast(end.row); + pane.cur_col = @intCast(end.col - 1); + } else { + // the register ended in '\n': the cursor lands ON that newline + pane.cur_row = @intCast(end.row -| 1); + pane.cur_col = @intCast(modal.lineSlice(f.content, end.row -| 1).len); + } + pane.cur_off = 0; + pane.cur_pinned = true; + pane.sticky_col = -1; + pane.ensureCursorVisible(); + } else p.normalPaste(pane, true); // terminal: splice run text at the cursor + } + /// tty mode: encode the key the way a terminal would and send it to the pty. fn forwardKey(p: *Pardes, id: usize, key: Key) void { var cbuf: [1]u8 = undefined; diff --git a/src/tutor.txt b/src/tutor.txt index 815cd4f7..f61ef8cc 100644 --- a/src/tutor.txt +++ b/src/tutor.txt @@ -66,12 +66,24 @@ opens/focuses a terminal there and ls's it; a file opens a file pane (scrolled to a ":line" suffix if present). - SELECT-THEN-ACT: there is no held-button chord. Instead: + SELECT-THEN-ACT: - MIDDLE-drag over text selects AND runs it on release (one gesture); a no-drag middle click auto-expands to the word under the cursor. - or select with the keyboard (`v` chars / `x` lines), then press Tab to execute or Enter to look — the acme chords on the keyboard. + CHORDS (acme's held-button combos): + 1-2 hold LEFT on a selection, tap MIDDLE: Cut it into the yank + register (the snarf buffer). + 1-3 hold LEFT, tap RIGHT: Paste the register over the selection, + or splice it in at a bare click point. So 1-2 then 1-3 in the + SAME hold = copy (Snarf): the text comes back, the register + keeps it. After a chord the left release is inert. + 2-1 hold a MIDDLE drag over a command, tap LEFT: the kept left + selection rides along as the command's ARGUMENT. + 2-3 / 3-1 / 3-2 tapping the other button during a MIDDLE (execute) + or RIGHT (look) drag CANCELS the gesture. + THE TAG: each pane has a one-line tag: its MODE (NOR/INS/TTY) + directory or file path + builtins. File panes show "Save Del" by default; Save writes the current file to disk, Del closes the window. Clicking a tag @@ -393,6 +405,8 @@ MOUSE (acme): L select/focus+pin M execute R look(open) select-then-act: middle-drag, or v/x then Tab + chords: 1-2 cut 1-3 paste (both in one hold = snarf) + 2-1 = middle-exec with the left selection as argument file tag = mode + path + "Save Del"; other tags show Del top bar = Kill Newcol Tutor Debug Colors NextColor TTY: a terminal IS a pane; Ctrl-b toggles the live shell by default, diff --git a/test/snapshots/chordcut.golden b/test/snapshots/chordcut.golden new file mode 100644 index 00000000..b7c0122f --- /dev/null +++ b/test/snapshots/chordcut.golden @@ -0,0 +1,279 @@ +== snap opened grid=60x30 cursor=7,2 +|Kill Newcol Tutor Debug Colors NextColor Dump +| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 line 2 f.txt g.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +|╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| f.txt +| +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| +== snap cut grid=60x30 cursor=7,3 +|Kill Newcol Tutor Debug Colors NextColor Dump +| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 2 f.txt g.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| f.txt +| +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| +== snap copied grid=60x30 cursor=10,4 +|Kill Newcol Tutor Debug Colors NextColor Dump +| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 2 f.txt g.txt +| 3 line 3 +| 4 line 4 +| 5 line 5 +| 6 +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| f.txt +| +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| +== snap pointpaste grid=60x30 cursor=16,5 +|Kill Newcol Tutor Debug Colors NextColor Dump +| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 2 f.txt g.txt +| 3 line 3 +| 4 line 4line +| 5 line 5 +| 6 +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| f.txt +| +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| +== snap replaced grid=60x30 cursor=11,6 +|Kill Newcol Tutor Debug Colors NextColor Dump +| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 2 f.txt g.txt +| 3 line 3 +| 4 line 4line +| 5 lline 5 +| 6 +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| f.txt +| +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| +== snap cancelexec grid=60x30 cursor=25,3 +|Kill Newcol Tutor Debug Colors NextColor Dump +| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 2echo B''OO g.txt f.txt g.txt +| 3 line 3 +| 4 line 4line +| 5 lline 5 +| 6 +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| f.txt +| +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| +== snap execafter grid=60x30 cursor=25,3 +|Kill Newcol Tutor Debug Colors NextColor Dump +| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 2echo B''OO g.txt f.txt g.txt +| 3 line 3 +| 4 line 4line +| 5 lline 5 +| 6 +| NOR /tmp/pardes-snap/chordcu +| f.txt g.txt +| +| +| +| f.txt +| BOO +| +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| +== snap cancellook grid=60x30 cursor=25,3 +|Kill Newcol Tutor Debug Colors NextColor Dump +| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 2echo B''OO g.txt f.txt g.txt +| 3 line 3 +| 4 line 4line +| 5 lline 5 +| 6 +| NOR /tmp/pardes-snap/chordcu +| f.txt g.txt +| +| +| +| f.txt +| BOO +| +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| +== snap lookafter grid=60x30 cursor=7,5 +|Kill Newcol Tutor Debug Colors NextColor Dump +| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 +|╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ f.txt g.txt +| NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 line 2 +| 3 +| NOR /tmp/pardes-snap/chordcu +| f.txt g.txt +| +| +| +| f.txt +| BOO +| +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| diff --git a/test/snapshots/chordcut.snap b/test/snapshots/chordcut.snap new file mode 100644 index 00000000..fc033e39 --- /dev/null +++ b/test/snapshots/chordcut.snap @@ -0,0 +1,101 @@ +# acme 1-2/1-3 chords in a file pane: with the left button holding a drag +# selection, tapping middle Cuts it into the snarf (yank) register; tapping +# right Pastes the register (replacing the selection, or splicing at a bare +# click point); cut then paste in ONE hold nets out to copy/Snarf. The left +# release after a chord is inert (the cursor stays where the op left it). +# Also the cancel chords: tapping right during a middle (execute) drag or +# left during a right (look) drag cancels the gesture entirely. +lines f.txt 5 +lines g.txt 2 +start 30 60 -n 3 +wait 8000 f.txt +stable 700 20000 +# open f.txt: click a body spot, type the name, look it +press left 5 8 +release left 5 8 +stable 400 5000 +key i +text f.txt +key esc +settle 700 +stable 400 5000 +press right 6 8 +release right 6 8 +wait 10000 line 3 +stable 700 15000 +snap opened +# 1-2 Cut: left-drag over "line" on line 2, tap middle while holding +press left 8 4 +drag left 11 4 +press middle 11 4 +release middle 11 4 +release left 11 4 +stable 400 5000 +snap cut +# 1-2 then 1-3 in one hold = copy: line 3 keeps its text, register loaded +press left 8 5 +drag left 11 5 +press middle 11 5 +release middle 11 5 +press right 11 5 +release right 11 5 +release left 11 5 +stable 400 5000 +snap copied +# 1-3 at a bare click point: hold left past the end of line 4, tap right — +# the register splices in at the click +press left 14 6 +press right 14 6 +release right 14 6 +release left 14 6 +stable 400 5000 +snap pointpaste +# 1-3 over a fresh selection replaces it with the register +press left 9 7 +drag left 11 7 +press right 11 7 +release right 11 7 +release left 11 7 +stable 400 5000 +snap replaced +# arm the cancel tests: append `echo B''OO g.txt` to line 2 +press left 10 4 +release left 10 4 +stable 400 5000 +key i +text echo B''OO g.txt +key esc +settle 700 +stable 400 5000 +# 2-3 cancel (acme textselect2): tap right during the middle execute-drag +# over `echo B''OO` — the command never runs (no BOO anywhere) +press middle 10 4 +drag middle 19 4 +press right 19 4 +release right 19 4 +release middle 19 4 +stable 700 8000 +snap cancelexec +# the same drag released clean proves the word was live: BOO appears +press middle 10 4 +drag middle 19 4 +release middle 19 4 +wait 10000 BOO +stable 700 10000 +snap execafter +# 3-1 cancel (acme textselect3): tap left during the right look-drag over +# g.txt — no pane opens +press right 21 4 +drag right 25 4 +press left 25 4 +release left 25 4 +release right 25 4 +stable 700 8000 +snap cancellook +# the same look released clean opens g.txt +press right 21 4 +drag right 25 4 +release right 25 4 +wait 10000 line 2 +stable 700 10000 +snap lookafter -- cgit v1.3