From b91e5ebf8b529d0ced0bf18e83b17df0bd2374ce Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 8 Jul 2026 09:56:53 -0300 Subject: chord polish: the selection a 1-2/1-3 chord leaves behind is dismissed by the next left click instead of stretching to it (explicit v/x selections keep click-extend), and in a tty-mode shell 1-3 now pastes into the pty like a terminal emulator — the click is forwarded first (SGR or X10, only when the app listens for mouse) so mouse-aware programs paste at the click point, then the register arrives bracketed (mode 2004) or with \n->\r when unbracketed; 1-2 stays inert there. chordcut.snap pins both (a cat -v probe shows the exact wire bytes); tutor updated. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pardes.zig | 53 +++++++++++++++++- src/tutor.txt | 5 ++ test/snapshots/chordcut.golden | 124 +++++++++++++++++++++++++++++++++++++++++ test/snapshots/chordcut.snap | 46 +++++++++++++++ 4 files changed, 226 insertions(+), 2 deletions(-) diff --git a/src/pardes.zig b/src/pardes.zig index fc216b24..994b0785 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3805,6 +3805,10 @@ pub const Pardes = struct { if (!pane.isTerminal()) pane.mode = .normal; pane.msel.active = false; pane.pending = 0; + // a chord/motion selection is residue: a fresh click + // dismisses it rather than dragging the anchored span + // to the click (explicit v/x keeps vim's click-extend) + if (!pane.vsel.explicit) pane.vsel.active = false; } } else { // acme execute (middle) / look (right): a no-drag click @@ -3829,14 +3833,60 @@ pub const Pardes = struct { /// 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). + /// In a tty-mode shell 1-3 instead pastes into the pty (forwarded click + /// + bracketed paste) and 1-2 is inert. fn chordCutPaste(p: *Pardes, cut: bool) void { const s = &p.drag.select; const pane = p.panes[s.id] orelse return; + if (pane.mode == .tty) { + // tty: nothing can be cut — 1-2 stays inert; the pty owns the + // screen. 1-3 pastes like a terminal emulator: the click is + // forwarded first (only when the app listens for mouse) so + // mouse-aware programs put their cursor under it, then the + // register is typed — bracketed when the app set mode 2004 + // (readline/vim/helix strip the markers), else with \n turned + // to \r like any unbracketed paste. + if (!s.chorded) { + s.chorded = true; + pane.sel[0].state = .none; // drop the sweep highlight + } + if (cut) return; + const y = p.yank orelse return; + if (y.len == 0) return; + const m = &pane.vt.modes; + if (m.get(.mouse_event_normal) or m.get(.mouse_event_button) or m.get(.mouse_event_any)) { + // dragUpdate keeps sel[0] tracking the held left button, so + // the click lands where the mouse is at this tap; 1-based, + // body-relative (the tag row is ours, not the app's) + const col: u16 = @intCast(std.math.clamp(pane.sel[0].c1 + 1, 1, 9999)); + const row: u16 = @intCast(std.math.clamp(pane.sel[0].r1 - @as(i32, BOX_H) + 1, 1, 9999)); + var mb: [32]u8 = undefined; + if (m.get(.mouse_format_sgr)) { + p.emitWrite(s.id, std.fmt.bufPrint(&mb, "\x1b[<0;{d};{d}M\x1b[<0;{d};{d}m", .{ col, row, col, row }) catch return); + } else { + // ponytail: legacy X10 bytes; add utf8/urxvt formats if an app ever wants them + const cb: u8 = @intCast(32 + @min(col, 222)); + const rb: u8 = @intCast(32 + @min(row, 222)); + p.emitWrite(s.id, &[_]u8{ 0x1b, '[', 'M', 32, cb, rb, 0x1b, '[', 'M', 35, cb, rb }); + } + } + if (m.get(.bracketed_paste)) { + p.emitWrite(s.id, "\x1b[200~"); + p.emitWrite(s.id, y); + p.emitWrite(s.id, "\x1b[201~"); + } else { + const cp = p.scratch.allocator().dupe(u8, y) catch return; + for (cp) |*c| { + if (c.* == '\n') c.* = '\r'; + } + p.emitWrite(s.id, cp); + } + 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(); @@ -3852,7 +3902,6 @@ pub const Pardes = struct { 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) { diff --git a/src/tutor.txt b/src/tutor.txt index 50b92558..7a8c7026 100644 --- a/src/tutor.txt +++ b/src/tutor.txt @@ -85,6 +85,11 @@ — a kept left selection or a v/x one, wherever it lives. 2-3 / 3-1 / 3-2 tapping the other button during a MIDDLE (execute) or RIGHT (look) drag CANCELS the gesture. + In a TTY-mode shell 1-3 pastes into the program like a terminal: + the click is forwarded first (if it listens for mouse) so the + paste lands under it, then the register arrives bracketed. + The selection a chord leaves behind is dismissed by the next left + click (it doesn't stretch to the click). 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 diff --git a/test/snapshots/chordcut.golden b/test/snapshots/chordcut.golden index b7c0122f..41194c76 100644 --- a/test/snapshots/chordcut.golden +++ b/test/snapshots/chordcut.golden @@ -277,3 +277,127 @@ | | | +== style chordsel +|0: 0-44 #969696,#2c2a3e, 45-59 d,#2c2a3e, +|1: 0-1 d,#383c54, 2-29 #969696,#2c2a3e, 30-31 d,#383c54, 32-59 #969696,#2c2a3e, +|2: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-29 d,d, 30-31 d,#525252, 32-59 d,d, +|3: 0-1 d,#2a2a2a, 2-6 #5a5a5a,d, 7-29 d,d, 30-31 d,#525252, 32-59 d,d, +|4: 0-1 d,#6e4fd0, 2-29 #969696,#2c2a3e, 30-31 d,#525252, 32-59 d,d, +|5: 0-1 d,#525252, 2-6 #969696,#2c2a3e, 7-10 #d8c8a8,#4a3a1e, 11-29 d,d, 30-31 d,#525252, 32-59 d,d, +|6: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-29 d,d, 30-31 d,#525252, 32-59 d,d, +|7: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-29 d,d, 30-31 d,#525252, 32-59 d,d, +|8: 0-1 d,#383c54, 2-29 #969696,#2c2a3e, 30-31 d,#525252, 32-59 d,d, +|9: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|10: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|11: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|12: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|13: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|14: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|15: 0-1 d,#2a2a2a, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|16: 0-1 d,#383c54, 2-29 #969696,#2c2a3e, 30-31 d,#525252, 32-59 d,d, +|17: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|18: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|19: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|20: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|21: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|22: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|23: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|24: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|25: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|26: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|27: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|28: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|29: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +== style clickdismiss +|0: 0-44 #969696,#2c2a3e, 45-59 d,#2c2a3e, +|1: 0-1 d,#383c54, 2-29 #969696,#2c2a3e, 30-31 d,#383c54, 32-59 #969696,#2c2a3e, +|2: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-29 d,d, 30-31 d,#525252, 32-59 d,d, +|3: 0-1 d,#2a2a2a, 2-6 #5a5a5a,d, 7-29 d,d, 30-31 d,#525252, 32-59 d,d, +|4: 0-1 d,#6e4fd0, 2-29 #969696,#2c2a3e, 30-31 d,#525252, 32-59 d,d, +|5: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-29 d,d, 30-31 d,#525252, 32-59 d,d, +|6: 0-1 d,#525252, 2-6 #969696,#2c2a3e, 7-29 d,d, 30-31 d,#525252, 32-59 d,d, +|7: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-29 d,d, 30-31 d,#525252, 32-59 d,d, +|8: 0-1 d,#383c54, 2-29 #969696,#2c2a3e, 30-31 d,#525252, 32-59 d,d, +|9: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|10: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|11: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|12: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|13: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|14: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|15: 0-1 d,#2a2a2a, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|16: 0-1 d,#383c54, 2-29 #969696,#2c2a3e, 30-31 d,#525252, 32-59 d,d, +|17: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|18: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|19: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|20: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|21: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|22: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|23: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|24: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|25: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|26: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|27: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|28: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +|29: 0-1 d,#525252, 2-29 d,d, 30-31 d,#525252, 32-59 d,d, +== snap ttyprobe grid=60x30 cursor=12,15 +|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 +| NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 line 2 +| 3 +| TTY /tmp/pardes-snap/chordcu +| $ echo B''OO +| BOO +| $ stty -icanon -echo; printf +| '\x1b[?2004h\x1b[?1000h\x1b +| [?1006h'; cat -v +| ^[[<0;8;3M^[[<0;8;3m^[[200~l +| ine^[[201~ +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| +== snap ttyprompt grid=60x30 cursor=8,15 +|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 +| NOR /tmp/pardes-snap/chordcu +| 1 line 1 +| 2 line 2 +| 3 +| TTY /tmp/pardes-snap/chordcu +| $ stty -icanon -echo; printf +| '\x1b[?2004h\x1b[?1000h\x1b +| [?1006h'; cat -v +| ^[[<0;8;3M^[[<0;8;3m^[[200~l +| ine^[[201~ +| $ +| $ line +| NOR /tmp/pardes-snap/chordcu +| +| f.txt g.txt +| +| +| +| +| +| +| +| +| +| +| diff --git a/test/snapshots/chordcut.snap b/test/snapshots/chordcut.snap index fc033e39..8243098a 100644 --- a/test/snapshots/chordcut.snap +++ b/test/snapshots/chordcut.snap @@ -99,3 +99,49 @@ release right 25 4 wait 10000 line 2 stable 700 10000 snap lookafter +# a fresh left click dismisses the chord's leftover selection instead of +# dragging the anchored span to the click: copy `line` on f.txt line 1 +# (styled snap shows the selection), then click the next row (it's gone) +press left 8 6 +drag left 11 6 +press middle 11 6 +release middle 11 6 +press right 11 6 +release right 11 6 +release left 11 6 +stable 400 5000 +snapstyle chordsel +press left 8 7 +release left 8 7 +stable 400 5000 +snapstyle clickdismiss +# tty paste chord: a mouse-listening app gets the click forwarded (SGR, +# body-relative) then the register between bracketed-paste markers — +# cat -v prints the received bytes visibly +press left 5 11 +release left 5 11 +stable 400 5000 +key c-b +stable 600 8000 +text stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[?1006h'; cat -v +key enter +stable 700 8000 +press left 10 12 +press right 10 12 +release right 10 12 +release left 10 12 +stable 700 8000 +snap ttyprobe +# a plain prompt (mouse modes off) gets no click bytes — just the paste; +# readline's own 2004 strips the markers and inserts the register text +key c-c +stable 600 8000 +text stty sane; printf '\x1b[?1000l\x1b[?1006l' +key enter +stable 600 8000 +press left 10 12 +press right 10 12 +release right 10 12 +release left 10 12 +stable 700 8000 +snap ttyprompt -- cgit v1.3