32c6ea16
Fix clipboard source transfer threading
a73x 2026-04-09 18:26
Commit message
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), |