32c6ea16
Fix clipboard source transfer threading
a73x 2026-04-09 18:26
diff --git a/src/wayland.zig b/src/wayland.zig index dcf5d2d..98e3014 100644 --- a/src/wayland.zig +++ b/src/wayland.zig @@ -326,6 +326,69 @@ const OwnedSelectionState = struct { } }; const CancelSelectionDecision = struct { destroy_source: *wl.DataSource, cleared_active_selection: bool, }; fn replaceSelectionSource( slot: *?*wl.DataSource, new_source: *wl.DataSource, ) ?*wl.DataSource { const old_source = slot.*; slot.* = new_source; return old_source; } fn cancelSelectionSource( slot: *?*wl.DataSource, source: *wl.DataSource, ) CancelSelectionDecision { if (slot.* == source) { slot.* = null; return .{ .destroy_source = source, .cleared_active_selection = true, }; } return .{ .destroy_source = source, .cleared_active_selection = false, }; } fn writeClipboardTransfer(payload: []const u8, fd: i32) !void { defer _ = c.close(fd); var written: usize = 0; while (written < payload.len) { const n = try posix.write(fd, payload[written..]); if (n == 0) return error.BrokenPipe; written += n; } } const ClipboardTransfer = struct { alloc: std.mem.Allocator, payload: []u8, fd: i32, fn init(alloc: std.mem.Allocator, payload: []const u8, fd: i32) !ClipboardTransfer { return .{ .alloc = alloc, .payload = try alloc.dupe(u8, payload), .fd = fd, }; } fn run(self: ClipboardTransfer) void { defer self.alloc.free(self.payload); writeClipboardTransfer(self.payload, self.fd) catch |err| { std.log.err("clipboard transfer failed: {any}", .{err}); }; } }; pub const Clipboard = struct { alloc: std.mem.Allocator, display: *wl.Display, @@ -399,37 +462,36 @@ pub const Clipboard = struct { source.offer("text/plain;charset=utf-8"); source.offer("text/plain"); if (self.selection_source) |old_source| old_source.destroy(); self.selection_source = source; if (replaceSelectionSource(&self.selection_source, source)) |old_source| { old_source.destroy(); } self.data_device.setSelection(source, serial); _ = self.display.flush(); } fn handleCancelledSource(self: *Clipboard, source: *wl.DataSource) void { if (self.selection_source == source) { self.selection_source = null; source.destroy(); const decision = cancelSelectionSource(&self.selection_source, source); if (decision.cleared_active_selection) { self.owned_selection.clear(self.alloc); return; } source.destroy(); decision.destroy_source.destroy(); } fn sendOwnedSelection(self: *Clipboard, mime_type: [*:0]const u8, fd: i32) void { defer _ = c.close(fd); if (!self.owned_selection.canServeMime(std.mem.span(mime_type))) return; const text = self.owned_selection.text orelse return; var written: usize = 0; while (written < text.len) { const n = posix.write(fd, text[written..]) catch |err| { std.log.err("clipboard write failed: {any}", .{err}); return; }; if (n == 0) return; written += n; } const transfer = ClipboardTransfer.init(self.alloc, text, fd) catch |err| { std.log.err("clipboard transfer setup failed: {any}", .{err}); _ = c.close(fd); return; }; const thread = std.Thread.spawn(.{}, ClipboardTransfer.run, .{transfer}) catch |err| { std.log.err("clipboard transfer thread spawn failed: {any}", .{err}); self.alloc.free(transfer.payload); _ = c.close(fd); return; }; thread.detach(); } }; @@ -874,6 +936,55 @@ test "OwnedSelectionState replaces text and recognizes offered mime types" { try std.testing.expectEqual(@as(u32, 0), state.serial); } test "replaceSelectionSource returns prior source to destroy" { const old_source: *wl.DataSource = @ptrFromInt(0x1000); const new_source: *wl.DataSource = @ptrFromInt(0x2000); var slot: ?*wl.DataSource = null; try std.testing.expect(replaceSelectionSource(&slot, old_source) == null); try std.testing.expect(slot == old_source); const replaced = replaceSelectionSource(&slot, new_source); try std.testing.expect(replaced == old_source); try std.testing.expect(slot == new_source); } test "cancelledSelectionSource clears active source and ignores stale source" { const stale_source: *wl.DataSource = @ptrFromInt(0x1000); const active_source: *wl.DataSource = @ptrFromInt(0x2000); var slot: ?*wl.DataSource = active_source; const stale = cancelSelectionSource(&slot, stale_source); try std.testing.expect(stale.destroy_source == stale_source); try std.testing.expect(!stale.cleared_active_selection); try std.testing.expect(slot == active_source); const active = cancelSelectionSource(&slot, active_source); try std.testing.expect(active.destroy_source == active_source); try std.testing.expect(active.cleared_active_selection); try std.testing.expect(slot == null); } test "writeClipboardTransfer writes payload and closes fd" { const pipefds = try std.posix.pipe(); defer std.posix.close(pipefds[0]); try writeClipboardTransfer("clipboard payload", pipefds[1]); var out = std.ArrayList(u8).empty; defer out.deinit(std.testing.allocator); var buf: [64]u8 = undefined; while (true) { const n = try std.posix.read(pipefds[0], &buf); if (n == 0) break; try out.appendSlice(std.testing.allocator, buf[0..n]); } try std.testing.expectEqualStrings("clipboard payload", out.items); } fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void { switch (event) { .ping => |p| wm_base.pong(p.serial),