a73x

6f0c995e

Fix clipboard paste deadlock

a73x   2026-04-08 15:27


diff --git a/src/main.zig b/src/main.zig
index 6082879..4375d32 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -313,6 +313,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
        try ctx.drawCells(
            @intCast(instances.items.len),
            .{ @floatFromInt(cell_w), @floatFromInt(cell_h) },
            default_bg,
        );
    }

@@ -508,7 +509,7 @@ fn runDrawSmokeTest(alloc: std.mem.Allocator) !void {
            _ = conn.display.readEvents();
        }
        _ = conn.display.dispatchPending();
        try ctx.drawCells(1, .{ cell_w, cell_h });
        try ctx.drawCells(1, .{ cell_w, cell_h }, .{ 0.0, 0.0, 0.0, 1.0 });
        _ = conn.display.flush();
        std.Thread.sleep(16 * std.time.ns_per_ms);
    }
@@ -566,6 +567,47 @@ test "isClipboardPasteEvent matches Ctrl-Shift-V press" {
    }));
}

test "drainSelectionPipeThenRoundtrip drains large paste before roundtrip" {
    const payload_len: usize = 8192;
    const payload = try std.testing.allocator.alloc(u8, payload_len);
    defer std.testing.allocator.free(payload);
    @memset(payload, 'p');

    const pipefds = try std.posix.pipe();
    defer std.posix.close(pipefds[0]);
    var write_fd_closed = false;
    defer if (!write_fd_closed) std.posix.close(pipefds[1]);

    var written: usize = 0;
    while (written < payload.len) {
        written += try std.posix.write(pipefds[1], payload[written..]);
    }
    std.posix.close(pipefds[1]);
    write_fd_closed = true;

    var roundtrip_ctx = struct {
        fd: std.posix.fd_t,
        called: bool = false,

        pub fn roundtrip(self: *@This()) !void {
            var probe: [1]u8 = undefined;
            const n = try std.posix.read(self.fd, &probe);
            try std.testing.expectEqual(@as(usize, 0), n);
            self.called = true;
        }
    }{ .fd = pipefds[0] };

    const text = try wayland_client.drainSelectionPipeThenRoundtrip(
        std.testing.allocator,
        pipefds[0],
        &roundtrip_ctx,
    );
    defer std.testing.allocator.free(text);

    try std.testing.expectEqualSlices(u8, payload, text);
    try std.testing.expect(roundtrip_ctx.called);
}

test "appendCellInstances emits a background quad for colored space" {
    var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty;
    defer instances.deinit(std.testing.allocator);
diff --git a/src/wayland.zig b/src/wayland.zig
index 5b80a87..5fe1b53 100644
--- a/src/wayland.zig
+++ b/src/wayland.zig
@@ -166,32 +166,50 @@ pub const Clipboard = struct {
        const offer = &(self.selection_offer orelse return null);
        const mime = choosePreferredMime(offer.mime_types.items) orelse return null;

        var pipefds: [2]std.posix.fd_t = undefined;
        try std.posix.pipe(&pipefds);
        const pipefds = try std.posix.pipe();
        defer std.posix.close(pipefds[0]);
        var write_fd_closed = false;
        defer if (!write_fd_closed) std.posix.close(pipefds[1]);

        offer.offer.receive(mime.ptr, pipefds[1]);
        _ = self.display.flush();
        _ = self.display.roundtrip();
        std.posix.close(pipefds[1]);
        write_fd_closed = true;

        var out = std.ArrayList(u8).empty;
        defer out.deinit(alloc);
        const roundtrip_ctx = struct {
            display: *wl.Display,

        var buf: [4096]u8 = undefined;
        while (true) {
            const n = std.posix.read(pipefds[0], &buf) catch |err| switch (err) {
                error.WouldBlock => break,
                else => return err,
            };
            if (n == 0) break;
            try out.appendSlice(alloc, buf[0..n]);
        }
            fn roundtrip(ctx: *@This()) !void {
                _ = ctx.display.roundtrip();
            }
        }{ .display = self.display };

        return try out.toOwnedSlice(alloc);
        return try drainSelectionPipeThenRoundtrip(alloc, pipefds[0], &roundtrip_ctx);
    }
};

pub fn drainSelectionPipeThenRoundtrip(
    alloc: std.mem.Allocator,
    read_fd: std.posix.fd_t,
    roundtrip_ctx: anytype,
) ![]u8 {
    var out = std.ArrayList(u8).empty;
    defer out.deinit(alloc);

    var buf: [4096]u8 = undefined;
    while (true) {
        const n = std.posix.read(read_fd, &buf) catch |err| switch (err) {
            error.WouldBlock => break,
            else => return err,
        };
        if (n == 0) break;
        try out.appendSlice(alloc, buf[0..n]);
    }

    try roundtrip_ctx.roundtrip();
    return try out.toOwnedSlice(alloc);
}

fn choosePreferredMime(mime_types: []const []const u8) ?[]const u8 {
    for (mime_types) |mime| {
        if (std.mem.eql(u8, mime, "text/plain;charset=utf-8")) return mime;
@@ -491,3 +509,41 @@ test "choosePreferredMime prefers UTF-8 plain text" {
    );
    try std.testing.expect(choosePreferredMime(&.{"application/octet-stream"}) == null);
}

test "drainSelectionPipeThenRoundtrip drains large payload before roundtrip" {
    const pipefds = try std.posix.pipe();
    defer std.posix.close(pipefds[0]);

    const payload = "clip" ** 20_000;
    const Writer = struct {
        fd: std.posix.fd_t,

        fn run(self: @This()) !void {
            defer std.posix.close(self.fd);

            var written: usize = 0;
            while (written < payload.len) {
                written += try std.posix.write(self.fd, payload[written..]);
            }
        }
    };

    var roundtrip_called = false;
    const RoundtripCtx = struct {
        called: *bool,

        pub fn roundtrip(self: *@This()) !void {
            self.called.* = true;
        }
    };

    const thread = try std.Thread.spawn(.{}, Writer.run, .{.{ .fd = pipefds[1] }});
    defer thread.join();

    var roundtrip_ctx = RoundtripCtx{ .called = &roundtrip_called };
    const text = try drainSelectionPipeThenRoundtrip(std.testing.allocator, pipefds[0], &roundtrip_ctx);
    defer std.testing.allocator.free(text);

    try std.testing.expect(roundtrip_called);
    try std.testing.expectEqualStrings(payload, text);
}