summaryrefslogtreecommitdiff
path: root/src
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
parentfc2738eea560769c8d150a7cc955fe8d54121298 (diff)
downloadpardes-ffc700d14d61fd5e6ca874a186932300c447c836.tar.gz
pardes-ffc700d14d61fd5e6ca874a186932300c447c836.zip
Mirror raw terminal mouse selections for text transfer
Diffstat (limited to 'src')
-rw-r--r--src/panes.zig44
-rw-r--r--src/pardes.zig125
2 files changed, 155 insertions, 14 deletions
diff --git a/src/panes.zig b/src/panes.zig
index a39afe01..b2f05cdf 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -44,10 +44,13 @@ pub const Pane = struct {
hi: usize,
prefix: [32]u8 = @splat(0),
prefix_len: u8 = 0,
+ raw_terminal: bool = false,
+ prompt_bytes: usize = 0,
};
pub const PointerSelection = struct {
gesture: Sel,
rows: []PointerRow,
+ raw_text: ?[]u8 = null,
};
/// A modal line selection (helix `x`): whole rows [r0, r1], absolute.
@@ -174,10 +177,21 @@ pub const Pane = struct {
msg_len: u16 = 0,
pub fn clearPointerSelection(pane: *Pane, slot: usize) void {
- if (pane.pointer_selections[slot]) |selection| pane.gpa.free(selection.rows);
+ if (pane.pointer_selections[slot]) |selection| {
+ pane.gpa.free(selection.rows);
+ if (selection.raw_text) |text| pane.gpa.free(text);
+ }
pane.pointer_selections[slot] = null;
}
+ pub fn rawPointerText(pane: *const Pane, gesture: Sel) ?[]const u8 {
+ if (gesture.state != .done or gesture.source_id == 0) return null;
+ for (pane.pointer_selections) |selection| {
+ if (selection) |saved| if (std.meta.eql(saved.gesture, gesture)) return saved.raw_text;
+ }
+ return null;
+ }
+
pub fn pointerSelection(pane: *const Pane, gesture: Sel) ?[]PointerRow {
if (gesture.state != .done or gesture.source_id == 0) return null;
for (pane.pointer_selections) |selection| {
@@ -6330,7 +6344,7 @@ pub const Terminal = struct {
var index: usize = 0;
for (pane.sel) |gesture| if (pane.pointerSelection(gesture)) |rows| {
for (rows) |*row| {
- pins[index] = trackPosition(pages, &row.row, overlay, &failed);
+ pins[index] = trackPosition(pages, &row.row, if (row.raw_terminal) null else overlay, &failed);
index += 1;
}
};
@@ -6482,7 +6496,8 @@ pub const Terminal = struct {
pub fn enterTty(p: *Pardes, id: usize) void {
const pane = p.panes[id] orelse return;
- if (comptime enabled) if (pane.terminal != null and pane.cur_pinned and pane.terminal.?.vt.cursorIsAtPrompt()) handoff: {
+ const selected = pane.rawPointerText(pane.sel[@intFromEnum(config.select_button)]) != null;
+ if (comptime enabled) if (!selected and pane.terminal != null and pane.cur_pinned and pane.terminal.?.vt.cursorIsAtPrompt()) handoff: {
const screen = pane.terminal.?.vt.screens.active;
const goff: i32 = @intCast(screen.pages.scrollbar().offset);
const vp_row = pane.gridRow(pane.cur_row) - goff;
@@ -6511,9 +6526,9 @@ pub const Terminal = struct {
pane.msel.active = false;
pane.vsel.active = false;
pane.nsel = 0;
- // A pinned row scrolls away. Raw mode must follow the program's live
- // cursor, and Last must not restore a stale modal spot on the way back.
- pane.cur_pinned = false;
+ // The raw renderer follows the live cursor. A captured mouse selection
+ // keeps its modal endpoint for the next return to editor mode.
+ pane.cur_pinned = selected;
pane.select = false;
pane.append_at = null;
pane.sticky_col = -1;
@@ -6590,6 +6605,19 @@ pub const Terminal = struct {
return .{ .cut = cols };
}
+ pub fn promptPrefixBytes(pane: *Pane, row: i32, raw: []const u8) usize {
+ if (comptime !enabled) return 0;
+ if (row < 0) return 0;
+ const state = pane.terminal orelse return 0;
+ const pin = state.vt.screens.active.pages.pin(.{ .screen = .{ .x = 0, .y = @intCast(row) } }) orelse return 0;
+ if (pin.rowAndCell().row.semantic_prompt == .none) return 0;
+ return switch (promptCut(pin)) {
+ .keep => 0,
+ .blank => raw.len,
+ .cut => |cols| @min(raw.len, File.rawAtDisplay(raw, cols)),
+ };
+ }
+
fn promptRow(pin: ghostty_vt.Pin, raw: []const u8) []const u8 {
const cols = switch (promptCut(pin)) {
.keep => return raw,
@@ -6765,6 +6793,10 @@ pub const Terminal = struct {
/// Consume a rewritten overlay, freeing the terminal edit text it replaces.
pub fn setEditText(p: *Pardes, pane: *Pane, new: []u8) void {
const overlay = if (pane.ovl) |*value| value else return p.gpa.free(new);
+ for (0..pane.sel.len) |button| {
+ pane.clearPointerSelection(button);
+ pane.sel[button].state = .none;
+ }
p.gpa.free(overlay.text);
overlay.text = new;
}
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;