a73x

32c6ea16

Fix clipboard source transfer threading

a73x   2026-04-09 18:26

Commit message
Fix clipboard source transfer threading

src/wayland.zig
Old New
@@ -326,6 +326,69 @@ const OwnedSelectionState = struct {
326 } 326 }
327 }; 327 };
328 328
329 const CancelSelectionDecision = struct {
330 destroy_source: *wl.DataSource,
331 cleared_active_selection: bool,
332 };
333
334 fn replaceSelectionSource(
335 slot: *?*wl.DataSource,
336 new_source: *wl.DataSource,
337 ) ?*wl.DataSource {
338 const old_source = slot.*;
339 slot.* = new_source;
340 return old_source;
341 }
342
343 fn cancelSelectionSource(
344 slot: *?*wl.DataSource,
345 source: *wl.DataSource,
346 ) CancelSelectionDecision {
347 if (slot.* == source) {
348 slot.* = null;
349 return .{
350 .destroy_source = source,
351 .cleared_active_selection = true,
352 };
353 }
354 return .{
355 .destroy_source = source,
356 .cleared_active_selection = false,
357 };
358 }
359
360 fn writeClipboardTransfer(payload: []const u8, fd: i32) !void {
361 defer _ = c.close(fd);
362
363 var written: usize = 0;
364 while (written < payload.len) {
365 const n = try posix.write(fd, payload[written..]);
366 if (n == 0) return error.BrokenPipe;
367 written += n;
368 }
369 }
370
371 const ClipboardTransfer = struct {
372 alloc: std.mem.Allocator,
373 payload: []u8,
374 fd: i32,
375
376 fn init(alloc: std.mem.Allocator, payload: []const u8, fd: i32) !ClipboardTransfer {
377 return .{
378 .alloc = alloc,
379 .payload = try alloc.dupe(u8, payload),
380 .fd = fd,
381 };
382 }
383
384 fn run(self: ClipboardTransfer) void {
385 defer self.alloc.free(self.payload);
386 writeClipboardTransfer(self.payload, self.fd) catch |err| {
387 std.log.err("clipboard transfer failed: {any}", .{err});
388 };
389 }
390 };
391
329 pub const Clipboard = struct { 392 pub const Clipboard = struct {
330 alloc: std.mem.Allocator, 393 alloc: std.mem.Allocator,
331 display: *wl.Display, 394 display: *wl.Display,
@@ -399,37 +462,36 @@ pub const Clipboard = struct {
399 source.offer("text/plain;charset=utf-8"); 462 source.offer("text/plain;charset=utf-8");
400 source.offer("text/plain"); 463 source.offer("text/plain");
401 464
402 if (self.selection_source) |old_source| old_source.destroy(); 465 if (replaceSelectionSource(&self.selection_source, source)) |old_source| {
403 self.selection_source = source; 466 old_source.destroy();
467 }
404 self.data_device.setSelection(source, serial); 468 self.data_device.setSelection(source, serial);
405 _ = self.display.flush(); 469 _ = self.display.flush();
406 } 470 }
407 471
408 fn handleCancelledSource(self: *Clipboard, source: *wl.DataSource) void { 472 fn handleCancelledSource(self: *Clipboard, source: *wl.DataSource) void {
409 if (self.selection_source == source) { 473 const decision = cancelSelectionSource(&self.selection_source, source);
410 self.selection_source = null; 474 if (decision.cleared_active_selection) {
411 source.destroy();
412 self.owned_selection.clear(self.alloc); 475 self.owned_selection.clear(self.alloc);
413 return;
414 } 476 }
415 source.destroy(); 477 decision.destroy_source.destroy();
416 } 478 }
417 479
418 fn sendOwnedSelection(self: *Clipboard, mime_type: [*:0]const u8, fd: i32) void { 480 fn sendOwnedSelection(self: *Clipboard, mime_type: [*:0]const u8, fd: i32) void {
419 defer _ = c.close(fd);
420
421 if (!self.owned_selection.canServeMime(std.mem.span(mime_type))) return; 481 if (!self.owned_selection.canServeMime(std.mem.span(mime_type))) return;
422 const text = self.owned_selection.text orelse return; 482 const text = self.owned_selection.text orelse return;
423 483 const transfer = ClipboardTransfer.init(self.alloc, text, fd) catch |err| {
424 var written: usize = 0; 484 std.log.err("clipboard transfer setup failed: {any}", .{err});
425 while (written < text.len) { 485 _ = c.close(fd);
426 const n = posix.write(fd, text[written..]) catch |err| { 486 return;
427 std.log.err("clipboard write failed: {any}", .{err}); 487 };
428 return; 488 const thread = std.Thread.spawn(.{}, ClipboardTransfer.run, .{transfer}) catch |err| {
429 }; 489 std.log.err("clipboard transfer thread spawn failed: {any}", .{err});
430 if (n == 0) return; 490 self.alloc.free(transfer.payload);
431 written += n; 491 _ = c.close(fd);
432 } 492 return;
493 };
494 thread.detach();
433 } 495 }
434 }; 496 };
435 497
@@ -874,6 +936,55 @@ test "OwnedSelectionState replaces text and recognizes offered mime types" {
874 try std.testing.expectEqual(@as(u32, 0), state.serial); 936 try std.testing.expectEqual(@as(u32, 0), state.serial);
875 } 937 }
876 938
939 test "replaceSelectionSource returns prior source to destroy" {
940 const old_source: *wl.DataSource = @ptrFromInt(0x1000);
941 const new_source: *wl.DataSource = @ptrFromInt(0x2000);
942
943 var slot: ?*wl.DataSource = null;
944 try std.testing.expect(replaceSelectionSource(&slot, old_source) == null);
945 try std.testing.expect(slot == old_source);
946
947 const replaced = replaceSelectionSource(&slot, new_source);
948 try std.testing.expect(replaced == old_source);
949 try std.testing.expect(slot == new_source);
950 }
951
952 test "cancelledSelectionSource clears active source and ignores stale source" {
953 const stale_source: *wl.DataSource = @ptrFromInt(0x1000);
954 const active_source: *wl.DataSource = @ptrFromInt(0x2000);
955
956 var slot: ?*wl.DataSource = active_source;
957
958 const stale = cancelSelectionSource(&slot, stale_source);
959 try std.testing.expect(stale.destroy_source == stale_source);
960 try std.testing.expect(!stale.cleared_active_selection);
961 try std.testing.expect(slot == active_source);
962
963 const active = cancelSelectionSource(&slot, active_source);
964 try std.testing.expect(active.destroy_source == active_source);
965 try std.testing.expect(active.cleared_active_selection);
966 try std.testing.expect(slot == null);
967 }
968
969 test "writeClipboardTransfer writes payload and closes fd" {
970 const pipefds = try std.posix.pipe();
971 defer std.posix.close(pipefds[0]);
972
973 try writeClipboardTransfer("clipboard payload", pipefds[1]);
974
975 var out = std.ArrayList(u8).empty;
976 defer out.deinit(std.testing.allocator);
977
978 var buf: [64]u8 = undefined;
979 while (true) {
980 const n = try std.posix.read(pipefds[0], &buf);
981 if (n == 0) break;
982 try out.appendSlice(std.testing.allocator, buf[0..n]);
983 }
984
985 try std.testing.expectEqualStrings("clipboard payload", out.items);
986 }
987
877 fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void { 988 fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void {
878 switch (event) { 989 switch (event) {
879 .ping => |p| wm_base.pong(p.serial), 990 .ping => |p| wm_base.pong(p.serial),