a73x

ec887038

Add visible selection copy extraction

a73x   2026-04-09 18:00

Commit message
Add visible selection copy extraction

src/main.zig
Old New
@@ -524,6 +524,13 @@ fn isClipboardPasteEvent(ev: wayland_client.KeyboardEvent) bool {
524 ev.keysym == c.XKB_KEY_V; 524 ev.keysym == c.XKB_KEY_V;
525 } 525 }
526 526
527 fn isClipboardCopyEvent(ev: wayland_client.KeyboardEvent) bool {
528 return ev.action == .press and
529 ev.modifiers.ctrl and
530 ev.modifiers.shift and
531 ev.keysym == c.XKB_KEY_C;
532 }
533
527 fn remainingRepeatTimeoutMs(deadline_ns: ?i128) ?i32 { 534 fn remainingRepeatTimeoutMs(deadline_ns: ?i128) ?i32 {
528 const deadline = deadline_ns orelse return null; 535 const deadline = deadline_ns orelse return null;
529 const now = std.time.nanoTimestamp(); 536 const now = std.time.nanoTimestamp();
@@ -541,6 +548,80 @@ fn shouldRenderFrame(terminal_dirty: bool, window_dirty: bool, forced: bool) boo
541 return terminal_dirty or window_dirty or forced; 548 return terminal_dirty or window_dirty or forced;
542 } 549 }
543 550
551 fn extractSelectedText(
552 alloc: std.mem.Allocator,
553 row_data: anytype,
554 span: SelectionSpan,
555 ) ![]u8 {
556 const normalized = span.normalized();
557 var out: std.ArrayListUnmanaged(u8) = .empty;
558 errdefer out.deinit(alloc);
559
560 const start_row: usize = @intCast(normalized.start.row);
561 const end_row: usize = @intCast(normalized.end.row);
562 var row_idx = start_row;
563 while (row_idx <= end_row) : (row_idx += 1) {
564 if (row_idx > start_row) try out.append(alloc, '\n');
565 try appendSelectedRowText(alloc, &out, row_data[row_idx], normalized, row_idx);
566 }
567
568 return out.toOwnedSlice(alloc);
569 }
570
571 fn appendSelectedRowText(
572 alloc: std.mem.Allocator,
573 out: *std.ArrayListUnmanaged(u8),
574 row_cells: anytype,
575 span: SelectionSpan,
576 row_idx: usize,
577 ) !void {
578 const cells = row_cells.items(.raw);
579 if (cells.len == 0) return;
580
581 const start_col: usize = if (row_idx == span.start.row)
582 @intCast(span.start.col)
583 else
584 0;
585 if (start_col >= cells.len) return;
586
587 var end_col: usize = if (row_idx == span.end.row)
588 @intCast(span.end.col)
589 else
590 cells.len - 1;
591 if (end_col >= cells.len) end_col = cells.len - 1;
592
593 while (end_col >= start_col) {
594 const cell = cells[end_col];
595 if (cellContributesVisibleText(cell)) break;
596 if (end_col == 0) return;
597 end_col -= 1;
598 }
599
600 var col = start_col;
601 while (col <= end_col) : (col += 1) {
602 const cell = cells[col];
603 if (!cellContributesVisibleText(cell)) continue;
604 try appendCellText(alloc, out, cell);
605 }
606 }
607
608 fn cellContributesVisibleText(cell: anytype) bool {
609 return cell.hasText() and cell.wide != .spacer_tail and cell.wide != .spacer_head;
610 }
611
612 fn appendCellText(
613 alloc: std.mem.Allocator,
614 out: *std.ArrayListUnmanaged(u8),
615 cell: anytype,
616 ) !void {
617 const cp = cell.codepoint();
618 if (cp == 0) return;
619
620 var utf8_buf: [4]u8 = undefined;
621 const utf8_len = try std.unicode.utf8Encode(cp, &utf8_buf);
622 try out.appendSlice(alloc, utf8_buf[0..utf8_len]);
623 }
624
544 const GridPoint = struct { 625 const GridPoint = struct {
545 col: u32, 626 col: u32,
546 row: u32, 627 row: u32,
@@ -2285,6 +2366,70 @@ test "isClipboardPasteEvent matches Ctrl-Shift-V press" {
2285 })); 2366 }));
2286 } 2367 }
2287 2368
2369 test "isClipboardCopyEvent matches Ctrl-Shift-C press" {
2370 try std.testing.expect(isClipboardCopyEvent(.{
2371 .keysym = c.XKB_KEY_C,
2372 .modifiers = .{ .ctrl = true, .shift = true },
2373 .action = .press,
2374 .utf8 = [_]u8{0} ** 8,
2375 .utf8_len = 0,
2376 }));
2377 try std.testing.expect(!isClipboardCopyEvent(.{
2378 .keysym = c.XKB_KEY_C,
2379 .modifiers = .{ .ctrl = true, .shift = true },
2380 .action = .repeat,
2381 .utf8 = [_]u8{0} ** 8,
2382 .utf8_len = 0,
2383 }));
2384 try std.testing.expect(!isClipboardCopyEvent(.{
2385 .keysym = c.XKB_KEY_c,
2386 .modifiers = .{ .ctrl = true, .shift = true },
2387 .action = .press,
2388 .utf8 = [_]u8{0} ** 8,
2389 .utf8_len = 0,
2390 }));
2391 }
2392
2393 test "extractSelectedText trims trailing blanks on each visible row" {
2394 var term = try vt.Terminal.init(std.testing.allocator, .{
2395 .cols = 8,
2396 .rows = 2,
2397 });
2398 defer term.deinit();
2399
2400 term.write("ab\r\nc");
2401 try term.snapshot();
2402
2403 const span = SelectionSpan{
2404 .start = .{ .col = 0, .row = 0 },
2405 .end = .{ .col = 7, .row = 1 },
2406 };
2407 const text = try extractSelectedText(std.testing.allocator, term.render_state.row_data.items(.cells), span);
2408 defer std.testing.allocator.free(text);
2409
2410 try std.testing.expectEqualStrings("ab\nc", text);
2411 }
2412
2413 test "extractSelectedText respects partial first and last rows" {
2414 var term = try vt.Terminal.init(std.testing.allocator, .{
2415 .cols = 8,
2416 .rows = 2,
2417 });
2418 defer term.deinit();
2419
2420 term.write("abc\r\ndef");
2421 try term.snapshot();
2422
2423 const span = SelectionSpan{
2424 .start = .{ .col = 1, .row = 0 },
2425 .end = .{ .col = 1, .row = 1 },
2426 };
2427 const text = try extractSelectedText(std.testing.allocator, term.render_state.row_data.items(.cells), span);
2428 defer std.testing.allocator.free(text);
2429
2430 try std.testing.expectEqualStrings("bc\nde", text);
2431 }
2432
2288 test "drainSelectionPipeThenRoundtrip drains large paste before roundtrip" { 2433 test "drainSelectionPipeThenRoundtrip drains large paste before roundtrip" {
2289 const payload_len: usize = 8192; 2434 const payload_len: usize = 8192;
2290 const payload = try std.testing.allocator.alloc(u8, payload_len); 2435 const payload = try std.testing.allocator.alloc(u8, payload_len);