ec887038
Add visible selection copy extraction
a73x 2026-04-09 18:00
diff --git a/src/main.zig b/src/main.zig index 5a4a7dd..c5ac0cf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -524,6 +524,13 @@ fn isClipboardPasteEvent(ev: wayland_client.KeyboardEvent) bool { ev.keysym == c.XKB_KEY_V; } fn isClipboardCopyEvent(ev: wayland_client.KeyboardEvent) bool { return ev.action == .press and ev.modifiers.ctrl and ev.modifiers.shift and ev.keysym == c.XKB_KEY_C; } fn remainingRepeatTimeoutMs(deadline_ns: ?i128) ?i32 { const deadline = deadline_ns orelse return null; const now = std.time.nanoTimestamp(); @@ -541,6 +548,80 @@ fn shouldRenderFrame(terminal_dirty: bool, window_dirty: bool, forced: bool) boo return terminal_dirty or window_dirty or forced; } fn extractSelectedText( alloc: std.mem.Allocator, row_data: anytype, span: SelectionSpan, ) ![]u8 { const normalized = span.normalized(); var out: std.ArrayListUnmanaged(u8) = .empty; errdefer out.deinit(alloc); const start_row: usize = @intCast(normalized.start.row); const end_row: usize = @intCast(normalized.end.row); var row_idx = start_row; while (row_idx <= end_row) : (row_idx += 1) { if (row_idx > start_row) try out.append(alloc, '\n'); try appendSelectedRowText(alloc, &out, row_data[row_idx], normalized, row_idx); } return out.toOwnedSlice(alloc); } fn appendSelectedRowText( alloc: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8), row_cells: anytype, span: SelectionSpan, row_idx: usize, ) !void { const cells = row_cells.items(.raw); if (cells.len == 0) return; const start_col: usize = if (row_idx == span.start.row) @intCast(span.start.col) else 0; if (start_col >= cells.len) return; var end_col: usize = if (row_idx == span.end.row) @intCast(span.end.col) else cells.len - 1; if (end_col >= cells.len) end_col = cells.len - 1; while (end_col >= start_col) { const cell = cells[end_col]; if (cellContributesVisibleText(cell)) break; if (end_col == 0) return; end_col -= 1; } var col = start_col; while (col <= end_col) : (col += 1) { const cell = cells[col]; if (!cellContributesVisibleText(cell)) continue; try appendCellText(alloc, out, cell); } } fn cellContributesVisibleText(cell: anytype) bool { return cell.hasText() and cell.wide != .spacer_tail and cell.wide != .spacer_head; } fn appendCellText( alloc: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8), cell: anytype, ) !void { const cp = cell.codepoint(); if (cp == 0) return; var utf8_buf: [4]u8 = undefined; const utf8_len = try std.unicode.utf8Encode(cp, &utf8_buf); try out.appendSlice(alloc, utf8_buf[0..utf8_len]); } const GridPoint = struct { col: u32, row: u32, @@ -2285,6 +2366,70 @@ test "isClipboardPasteEvent matches Ctrl-Shift-V press" { })); } test "isClipboardCopyEvent matches Ctrl-Shift-C press" { try std.testing.expect(isClipboardCopyEvent(.{ .keysym = c.XKB_KEY_C, .modifiers = .{ .ctrl = true, .shift = true }, .action = .press, .utf8 = [_]u8{0} ** 8, .utf8_len = 0, })); try std.testing.expect(!isClipboardCopyEvent(.{ .keysym = c.XKB_KEY_C, .modifiers = .{ .ctrl = true, .shift = true }, .action = .repeat, .utf8 = [_]u8{0} ** 8, .utf8_len = 0, })); try std.testing.expect(!isClipboardCopyEvent(.{ .keysym = c.XKB_KEY_c, .modifiers = .{ .ctrl = true, .shift = true }, .action = .press, .utf8 = [_]u8{0} ** 8, .utf8_len = 0, })); } test "extractSelectedText trims trailing blanks on each visible row" { var term = try vt.Terminal.init(std.testing.allocator, .{ .cols = 8, .rows = 2, }); defer term.deinit(); term.write("ab\r\nc"); try term.snapshot(); const span = SelectionSpan{ .start = .{ .col = 0, .row = 0 }, .end = .{ .col = 7, .row = 1 }, }; const text = try extractSelectedText(std.testing.allocator, term.render_state.row_data.items(.cells), span); defer std.testing.allocator.free(text); try std.testing.expectEqualStrings("ab\nc", text); } test "extractSelectedText respects partial first and last rows" { var term = try vt.Terminal.init(std.testing.allocator, .{ .cols = 8, .rows = 2, }); defer term.deinit(); term.write("abc\r\ndef"); try term.snapshot(); const span = SelectionSpan{ .start = .{ .col = 1, .row = 0 }, .end = .{ .col = 1, .row = 1 }, }; const text = try extractSelectedText(std.testing.allocator, term.render_state.row_data.items(.cells), span); defer std.testing.allocator.free(text); try std.testing.expectEqualStrings("bc\nde", text); } test "drainSelectionPipeThenRoundtrip drains large paste before roundtrip" { const payload_len: usize = 8192; const payload = try std.testing.allocator.alloc(u8, payload_len);