a73x

d0084ff5

Copy visible selection to clipboard

a73x   2026-04-09 18:53

Commit message
Copy visible selection to clipboard

Wire Ctrl+Shift+C into the keyboard event loop in runTerminal, threading
the Wayland event serial through a new copySelectionText helper. Add
serial field (default 0) to KeyboardEvent and populate it from the key
event in the keyboard listener.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

src/main.zig
Old New
@@ -273,6 +273,10 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
273 } 273 }
274 continue; 274 continue;
275 } 275 }
276 if (isClipboardCopyEvent(ev)) {
277 _ = try copySelectionText(alloc, clipboard, term, activeSelectionSpan(selection), ev.serial);
278 continue;
279 }
276 if (ev.utf8_len > 0) { 280 if (ev.utf8_len > 0) {
277 _ = try p.write(ev.utf8[0..ev.utf8_len]); 281 _ = try p.write(ev.utf8[0..ev.utf8_len]);
278 } else if (try encodeKeyboardEvent(term, ev, &key_buf)) |encoded| { 282 } else if (try encodeKeyboardEvent(term, ev, &key_buf)) |encoded| {
@@ -555,6 +559,22 @@ fn isClipboardCopyEvent(ev: wayland_client.KeyboardEvent) bool {
555 ev.keysym == c.XKB_KEY_C; 559 ev.keysym == c.XKB_KEY_C;
556 } 560 }
557 561
562 fn copySelectionText(
563 alloc: std.mem.Allocator,
564 clipboard: ?*wayland_client.Clipboard,
565 term: *vt.Terminal,
566 selection: ?SelectionSpan,
567 serial: u32,
568 ) !bool {
569 const cb = clipboard orelse return false;
570 const span = selection orelse return false;
571 const text = try extractSelectedText(alloc, term.render_state.row_data.items(.cells), span);
572 defer alloc.free(text);
573 if (text.len == 0) return false;
574 try cb.setSelectionText(text, serial);
575 return true;
576 }
577
558 fn remainingRepeatTimeoutMs(deadline_ns: ?i128) ?i32 { 578 fn remainingRepeatTimeoutMs(deadline_ns: ?i128) ?i32 {
559 const deadline = deadline_ns orelse return null; 579 const deadline = deadline_ns orelse return null;
560 const now = std.time.nanoTimestamp(); 580 const now = std.time.nanoTimestamp();
@@ -2555,6 +2575,19 @@ test "isClipboardCopyEvent matches Ctrl-Shift-C press" {
2555 })); 2575 }));
2556 } 2576 }
2557 2577
2578 test "copySelectionText returns false for empty visible selection" {
2579 var term = try vt.Terminal.init(std.testing.allocator, .{ .cols = 4, .rows = 1 });
2580 defer term.deinit();
2581
2582 try std.testing.expect(!try copySelectionText(
2583 std.testing.allocator,
2584 null,
2585 term,
2586 null,
2587 0,
2588 ));
2589 }
2590
2558 test "extractSelectedText trims trailing blanks on each visible row" { 2591 test "extractSelectedText trims trailing blanks on each visible row" {
2559 var term = try vt.Terminal.init(std.testing.allocator, .{ 2592 var term = try vt.Terminal.init(std.testing.allocator, .{
2560 .cols = 8, 2593 .cols = 8,
src/wayland.zig
Old New
@@ -24,6 +24,7 @@ pub const KeyboardEvent = struct {
24 action: Action, 24 action: Action,
25 utf8: [8]u8, 25 utf8: [8]u8,
26 utf8_len: u8, 26 utf8_len: u8,
27 serial: u32 = 0,
27 28
28 pub const Action = enum { press, release, repeat }; 29 pub const Action = enum { press, release, repeat };
29 }; 30 };
@@ -751,6 +752,7 @@ fn keyboardListener(_: *wl.Keyboard, event: wl.Keyboard.Event, kb: *Keyboard) vo
751 .action = action, 752 .action = action,
752 .utf8 = utf8, 753 .utf8 = utf8,
753 .utf8_len = @intCast(@min(len, 8)), 754 .utf8_len = @intCast(@min(len, 8)),
755 .serial = k.serial,
754 }; 756 };
755 757
756 kb.event_queue.append(kb.alloc, ev) catch |err| { 758 kb.event_queue.append(kb.alloc, ev) catch |err| {