diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 13:01:28 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | ffc700d14d61fd5e6ca874a186932300c447c836 (patch) | |
| tree | 364b733b061f4fec14cef0a12862fa125cbde35b /src | |
| parent | fc2738eea560769c8d150a7cc955fe8d54121298 (diff) | |
| download | pardes-ffc700d14d61fd5e6ca874a186932300c447c836.tar.gz pardes-ffc700d14d61fd5e6ca874a186932300c447c836.zip | |
Mirror raw terminal mouse selections for text transfer
Diffstat (limited to 'src')
| -rw-r--r-- | src/panes.zig | 44 | ||||
| -rw-r--r-- | src/pardes.zig | 125 |
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; |
