a73x

6b990766

Clean up clipboard code after review: remove dead serial field and add guard comments

a73x   2026-04-09 18:58

- Remove `serial` field from `OwnedSelectionState` and the `serial` param from
  `replaceText`; the stored serial was never read in production code after being
  written — the value was already consumed by `data_device.setSelection` before
  `replaceText` was called. Update the test accordingly.
- Revert reviewer's `term` → `&term` suggestion: `Terminal.init` returns `*Terminal`,
  so `term` is already the correct type; `&term` would produce `**Terminal` (confirmed
  via build error).
- Add ordered-guard comment to `copySelectionText` explaining why the `text.len == 0`
  branch cannot be unit-tested without a live Wayland connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff --git a/src/main.zig b/src/main.zig
index 67fface..cc94c5d 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -566,6 +566,12 @@ fn copySelectionText(
    selection: ?SelectionSpan,
    serial: u32,
) !bool {
    // Guards fire in order:
    //   1. clipboard == null  → false (no Wayland clipboard available)
    //   2. selection == null  → false (no active selection span)
    //   3. text.len == 0     → false (selection covers only blank cells)
    // Guard 3 can only be reached with a real clipboard; it cannot be unit-tested
    // without a live Wayland connection.
    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);
diff --git a/src/wayland.zig b/src/wayland.zig
index 7a89ec9..31366aa 100644
--- a/src/wayland.zig
+++ b/src/wayland.zig
@@ -296,7 +296,6 @@ const ClipboardOffer = struct {

const OwnedSelectionState = struct {
    text: ?[]u8 = null,
    serial: u32 = 0,

    fn deinit(self: *OwnedSelectionState, alloc: std.mem.Allocator) void {
        self.clear(alloc);
@@ -306,18 +305,15 @@ const OwnedSelectionState = struct {
        self: *OwnedSelectionState,
        alloc: std.mem.Allocator,
        text: []const u8,
        serial: u32,
    ) !void {
        const dup = try alloc.dupe(u8, text);
        self.clear(alloc);
        self.text = dup;
        self.serial = serial;
    }

    fn clear(self: *OwnedSelectionState, alloc: std.mem.Allocator) void {
        if (self.text) |text| alloc.free(text);
        self.text = null;
        self.serial = 0;
    }

    fn canServeMime(self: *const OwnedSelectionState, mime: []const u8) bool {
@@ -480,7 +476,7 @@ pub const Clipboard = struct {
        const source = try self.data_device_manager.createDataSource();
        errdefer source.destroy();

        try self.owned_selection.replaceText(self.alloc, text, serial);
        try self.owned_selection.replaceText(self.alloc, text);

        source.setListener(*Clipboard, dataSourceListener, self);
        source.offer("text/plain;charset=utf-8");
@@ -940,20 +936,17 @@ test "OwnedSelectionState replaces text and recognizes offered mime types" {
    var state = OwnedSelectionState{};
    defer state.deinit(std.testing.allocator);

    try state.replaceText(std.testing.allocator, "first", 17);
    try state.replaceText(std.testing.allocator, "first");
    try std.testing.expectEqualStrings("first", state.text.?);
    try std.testing.expectEqual(@as(u32, 17), state.serial);
    try std.testing.expect(state.canServeMime("text/plain;charset=utf-8"));
    try std.testing.expect(state.canServeMime("text/plain"));
    try std.testing.expect(!state.canServeMime("text/html"));

    try state.replaceText(std.testing.allocator, "second", 21);
    try state.replaceText(std.testing.allocator, "second");
    try std.testing.expectEqualStrings("second", state.text.?);
    try std.testing.expectEqual(@as(u32, 21), state.serial);

    state.clear(std.testing.allocator);
    try std.testing.expect(state.text == null);
    try std.testing.expectEqual(@as(u32, 0), state.serial);
}

test "replaceSelectionSource returns prior source to destroy" {