From ffc700d14d61fd5e6ca874a186932300c447c836 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 15 Sep 2026 13:01:28 -0300 Subject: Mirror raw terminal mouse selections for text transfer --- src/pardes.zig | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 8 deletions(-) (limited to 'src/pardes.zig') diff --git a/src/pardes.zig b/src/pardes.zig index 1b2e7ff5..0938736f 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -7464,20 +7464,25 @@ pub const Pardes = struct { if (pane.pointerSelection(gesture) != null) return; pane.clearPointerSelection(slot); // Refresh the source map before translating the rendered rectangle. + const raw = pane.isTerminal() and pane.mode == .tty; const body = try p.bodyText(p.scratch.allocator(), pane); - const terminal_lines = if (pane.file == null) try panes.Terminal.cursorLines(p, pane) else &.{}; + const terminal_lines = if (pane.file == null and !raw) try panes.Terminal.cursorLines(p, pane) else &.{}; const first = @max(0, @min(gesture.r0, gesture.r1) - BOX_H); const last = @min(@as(i32, pane.rows) - 1, @max(gesture.r0, gesture.r1) - BOX_H); if (first > last) return; const rows = try p.gpa.alloc(Pane.PointerRow, @intCast(last - first + 1)); + errdefer p.gpa.free(rows); + const raw_text = if (raw) try p.gpa.dupe(u8, try p.selectionText(pane, gesture)) else null; const prefix: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0; const left = @max(0, @min(gesture.c0, gesture.c1)); const right = @max(0, @max(gesture.c0, gesture.c1)); for (rows, 0..) |*row, index| { const visible = first + @as(i32, @intCast(index)); - const source = pane.wrapAt(visible); - const line = pointerSourceLine(pane, terminal_lines, source.line); - const next = pane.wrapAt(visible + 1); + var source = pane.wrapAt(visible); + if (raw) source = .{ .line = panes.Terminal.gridOffset(pane) + visible, .at = 0 }; + const line = if (raw) modal.lineSlice(body, @intCast(visible)) else pointerSourceLine(pane, terminal_lines, source.line); + var next = pane.wrapAt(visible + 1); + if (raw) next = .{ .line = source.line + 1, .at = 0 }; const end = if (next.line == source.line) @min(line.len, @as(usize, @intCast(@max(0, next.at)))) else line.len; const start: usize = @intCast(@max(0, source.at)); const lo = @min(end, panes.File.byteAtDisplayFrom(line, start, @intCast(@max(0, left - prefix)))); @@ -7486,6 +7491,8 @@ pub const Pardes = struct { .row = source.line, .lo = lo, .hi = if (right < prefix) lo else @min(end, modal.nextGrapheme(line, at)), + .raw_terminal = raw, + .prompt_bytes = if (raw) panes.Terminal.promptPrefixBytes(pane, source.line, line) else 0, }; const rendered = modal.lineSlice(body, @intCast(visible)); const prefix_end = @min(rendered.len, @as(usize, @intCast(@min(prefix, right + 1)))); @@ -7496,13 +7503,14 @@ pub const Pardes = struct { pane.next_pointer_selection += 1; gesture.source_id = pane.next_pointer_selection; pane.sel[slot] = gesture; - pane.pointer_selections[slot] = .{ .gesture = gesture, .rows = rows }; + pane.pointer_selections[slot] = .{ .gesture = gesture, .rows = rows, .raw_text = raw_text }; } /// block-selected text, newline-joined per row; reads the rendered screen /// so typed text and shell output select identically. Scratch-owned. fn selectionText(p: *Pardes, pane: *Pane, sl: Pane.Sel) ![]const u8 { const arena = p.scratch.allocator(); + if (pane.rawPointerText(sl)) |text| return arena.dupe(u8, text); if (pane.pointerSelection(sl)) |rows| { const terminal_lines = if (pane.file == null) try panes.Terminal.cursorLines(p, pane) else &.{}; var total: usize = rows.len -| 1; @@ -7684,8 +7692,12 @@ pub const Pardes = struct { fn pointerOperand(p: *Pardes, pane: *Pane, clicked: Pane.Sel) PointerOperand { const visible = clicked.r0 - @as(i32, BOX_H); const wrapped = pane.wrapAt(visible); - const row = wrapped.line; - const col = if (clicked.r0 >= BOX_H) + const raw = pane.isTerminal() and pane.mode == .tty and clicked.r0 >= BOX_H; + const row = if (raw) panes.Terminal.gridOffset(pane) + visible else wrapped.line; + const raw_line = if (raw) modal.lineSlice(p.bodyText(p.scratch.allocator(), pane) catch "", @intCast(@max(0, visible))) else ""; + const col = if (raw) + @as(i32, @intCast(panes.File.rawAtDisplay(raw_line, @intCast(@max(0, clicked.c0))))) + else if (clicked.r0 >= BOX_H) p.paneByteAtDisplay( pane, wrapped.line, @@ -7702,7 +7714,21 @@ pub const Pardes = struct { if (pane.file != null and clicked.c0 < panes.File.gutterWidth(pane)) break :inside false; const source_col: usize = @intCast(@max(0, col)); for (rows) |span| { - if (span.row == row and source_col >= span.lo and source_col < span.hi) + const target = if (span.raw_terminal) + (if (raw) span.row else pane.surfRow(span.row)) + else + (if (raw) pane.gridRow(span.row) else span.row); + var lo = span.lo; + var hi = span.hi; + if (span.raw_terminal and !raw) { + lo -|= span.prompt_bytes; + hi -|= span.prompt_bytes; + } else if (!span.raw_terminal and raw) { + const prefix = panes.Terminal.promptPrefixBytes(pane, row, raw_line); + lo += prefix; + hi += prefix; + } + if (target == row and source_col >= lo and source_col < hi) break :inside true; } break :inside false; @@ -8241,6 +8267,13 @@ pub const Pardes = struct { } fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) void { + const raw_selection = pane.rawPointerText(pane.sel[sel_slot]) != null; + const before = .{ pane.cur_row, pane.cur_col, pane.vsel, pane.msel, pane.select, pane.nsel, pane.mode }; + defer if (raw_selection and !std.meta.eql(before, .{ pane.cur_row, pane.cur_col, pane.vsel, pane.msel, pane.select, pane.nsel, pane.mode })) { + pane.clearPointerSelection(sel_slot); + pane.sel[sel_slot].state = .none; + }; + if (raw_selection and semantic == .edit and semantic.edit.kind == .yank) return p.normalYank(pane); p.pinPaneCursor(pane); const text = p.flatSurface(pane) catch return; const range = pane.primaryRange(text, 0); @@ -9861,6 +9894,10 @@ pub const Pardes = struct { fn normalYank(p: *Pardes, pane: *Pane) void { pane.select = false; + if (pane.rawPointerText(pane.sel[sel_slot])) |text| { + p.setYank(text); + return; + } if (pane.vsel.active) { p.setYank(p.vselText(pane)); return; @@ -11200,6 +11237,34 @@ pub const Pardes = struct { p.runBuiltin(cmd, id, "", p.withArg(operand, arg)); } + fn mirrorTtySelection(p: *Pardes, pane: *Pane) void { + const gesture = pane.sel[sel_slot]; + const rows = pane.pointerSelection(gesture) orelse return; + const text = pane.rawPointerText(gesture) orelse return; + if (rows.len == 0) return; + const endpoint = rows[if (gesture.r1 >= gesture.r0) rows.len - 1 else 0]; + const body = p.bodyText(p.scratch.allocator(), pane) catch return; + const line = modal.lineSlice(body, @intCast(@max(0, endpoint.row - panes.Terminal.gridOffset(pane)))); + const byte = if (gesture.c1 >= gesture.c0) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo; + pane.cur_row = pane.surfRow(endpoint.row); + var shown = std.mem.trimEnd(u8, line[@min(endpoint.prompt_bytes, line.len)..], " \t"); + if (pane.ovl) |overlay| { + const relative = pane.cur_row - overlay.row; + if (relative >= 0 and relative < modal.lineCount(overlay.text)) + shown = modal.lineSlice(overlay.text, @intCast(relative)); + } + pane.cur_col = @intCast(modal.graphemeStart(shown, @min(shown.len, byte -| endpoint.prompt_bytes))); + pane.cur_pinned = true; + pane.msel.active = false; + pane.vsel.active = false; + pane.nsel = 0; + pane.select = false; + pane.append_at = null; + pane.sticky_col = -1; + pane.normal.clear(); + p.setYank(text); + } + fn dragRelease(p: *Pardes, button: Mouse.Button) void { // A different button's release belongs to a chord or another host // stream; only the button which began this gesture may end it. @@ -11273,6 +11338,7 @@ pub const Pardes = struct { const dragged = sl.c0 != sl.c1 or sl.r0 != sl.r1; if (!dragged) pane.sel[sel_slot].state = .none; if (dragged) p.capturePointerSelection(pane, sel_slot) catch {}; + if (dragged and pane.mode == .tty) p.mirrorTtySelection(pane); const body_vis = sl.r1 - @as(i32, BOX_H); if (body_vis >= 0 and pane.mode != .tty) { const w = pane.wrapAt(body_vis); @@ -12779,7 +12845,50 @@ pub const Pardes = struct { return !cell.default and !(cell.len == 1 and cell.text[0] == ' '); } + fn paintTerminalSelection(p: *Pardes, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void { + const body = p.bodyText(p.scratch.allocator(), pane) catch return; + const raw = pane.mode == .tty; + const tx = r.x + config.GUTTER; + const width = r.w -| config.GUTTER; + const body_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H; + var lines = std.mem.splitScalar(u8, body, '\n'); + var visible: i32 = 0; + while (lines.next()) |line| : (visible += 1) { + if (visible >= r.h -| BOX_H) break; + const source_row = if (raw) panes.Terminal.gridOffset(pane) + visible else pane.wrapAt(visible).line; + for (rows) |row| { + const target = if (row.raw_terminal) + (if (raw) row.row else pane.surfRow(row.row)) + else + (if (raw) pane.gridRow(row.row) else row.row); + if (target != source_row) continue; + var lo = row.lo; + var hi = row.hi; + if (row.raw_terminal and !raw) { + lo -|= row.prompt_bytes; + hi -|= row.prompt_bytes; + } else if (!row.raw_terminal and raw) { + const prefix = panes.Terminal.promptPrefixBytes(pane, source_row, line); + lo += prefix; + hi += prefix; + } + lo = modal.graphemeStart(line, @min(lo, line.len)); + hi = @min(hi, line.len); + if (lo >= hi) continue; + var col = panes.File.rawDisplayCol(line, lo); + const end = panes.File.rawDisplayCol(line, hi); + while (col < end and col < width) : (col += 1) { + const cell = p.surface.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible))); + cell.default = false; + cell.style.bg = .{ .rgb = bg }; + if (fg) |ink| cell.style.fg = .{ .rgb = ink }; + } + } + } + } + fn paintSourceSelection(p: *Pardes, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void { + if (pane.isTerminal()) return p.paintTerminalSelection(pane, r, rows, bg, fg); const terminal_lines = if (pane.file == null) panes.Terminal.cursorLines(p, pane) catch return else &.{}; const tx = r.x + config.GUTTER; const width = r.w -| config.GUTTER; -- cgit v1.3