summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 13:01:28 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commitffc700d14d61fd5e6ca874a186932300c447c836 (patch)
tree364b733b061f4fec14cef0a12862fa125cbde35b /src/pardes.zig
parentfc2738eea560769c8d150a7cc955fe8d54121298 (diff)
downloadpardes-ffc700d14d61fd5e6ca874a186932300c447c836.tar.gz
pardes-ffc700d14d61fd5e6ca874a186932300c447c836.zip
Mirror raw terminal mouse selections for text transfer
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig125
1 files changed, 117 insertions, 8 deletions
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;