a73x

d0084ff5

Copy visible selection to clipboard

a73x   2026-04-09 18:53

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>

diff --git a/src/main.zig b/src/main.zig
index ed72de0..67fface 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -273,6 +273,10 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
                }
                continue;
            }
            if (isClipboardCopyEvent(ev)) {
                _ = try copySelectionText(alloc, clipboard, term, activeSelectionSpan(selection), ev.serial);
                continue;
            }
            if (ev.utf8_len > 0) {
                _ = try p.write(ev.utf8[0..ev.utf8_len]);
            } else if (try encodeKeyboardEvent(term, ev, &key_buf)) |encoded| {
@@ -555,6 +559,22 @@ fn isClipboardCopyEvent(ev: wayland_client.KeyboardEvent) bool {
        ev.keysym == c.XKB_KEY_C;
}

fn copySelectionText(
    alloc: std.mem.Allocator,
    clipboard: ?*wayland_client.Clipboard,
    term: *vt.Terminal,
    selection: ?SelectionSpan,
    serial: u32,
) !bool {
    const cb = clipboard orelse return false;
    const span = selection orelse return false;
    const text = try extractSelectedText(alloc, term.render_state.row_data.items(.cells), span);
    defer alloc.free(text);
    if (text.len == 0) return false;
    try cb.setSelectionText(text, serial);
    return true;
}

fn remainingRepeatTimeoutMs(deadline_ns: ?i128) ?i32 {
    const deadline = deadline_ns orelse return null;
    const now = std.time.nanoTimestamp();
@@ -2555,6 +2575,19 @@ test "isClipboardCopyEvent matches Ctrl-Shift-C press" {
    }));
}

test "copySelectionText returns false for empty visible selection" {
    var term = try vt.Terminal.init(std.testing.allocator, .{ .cols = 4, .rows = 1 });
    defer term.deinit();

    try std.testing.expect(!try copySelectionText(
        std.testing.allocator,
        null,
        term,
        null,
        0,
    ));
}

test "extractSelectedText trims trailing blanks on each visible row" {
    var term = try vt.Terminal.init(std.testing.allocator, .{
        .cols = 8,
diff --git a/src/wayland.zig b/src/wayland.zig
index 1da7ede..7a89ec9 100644
--- a/src/wayland.zig
+++ b/src/wayland.zig
@@ -24,6 +24,7 @@ pub const KeyboardEvent = struct {
    action: Action,
    utf8: [8]u8,
    utf8_len: u8,
    serial: u32 = 0,

    pub const Action = enum { press, release, repeat };
};
@@ -751,6 +752,7 @@ fn keyboardListener(_: *wl.Keyboard, event: wl.Keyboard.Event, kb: *Keyboard) vo
                .action = action,
                .utf8 = utf8,
                .utf8_len = @intCast(@min(len, 8)),
                .serial = k.serial,
            };

            kb.event_queue.append(kb.alloc, ev) catch |err| {