From 1334cc213e6db4be3f2fc695a06c13d49362a570 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Aug 2026 13:10:50 -0700 Subject: [PATCH] macos: answer ENOSYS for Kitty clipboard writes to primary A Kitty clipboard protocol (OSC 5522) write transaction targeting `loc=primary` replied `type=write:status=DONE` in the macOS app even though macOS has no primary selection and the data was silently discarded. The spec requires ENOSYS when the requested location is not available on the system, which the read path already answers correctly: https://sw.kovidgoyal.net/kitty/clipboard/ --- include/ghostty.h | 1 + .../Extensions/NSPasteboard+Extension.swift | 7 ++++++- src/apprt/embedded.zig | 16 +++++++++++++++- src/terminal/stream_terminal.zig | 12 ++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index cd91368e4..518af3ff2 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -75,6 +75,7 @@ typedef enum { typedef enum { GHOSTTY_CLIPBOARD_STANDARD, GHOSTTY_CLIPBOARD_SELECTION, + GHOSTTY_CLIPBOARD_PRIMARY, } ghostty_clipboard_e; // One representation of clipboard contents. The data is binary-safe with diff --git a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift index 5952cdb55..9dd016e74 100644 --- a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift @@ -132,7 +132,8 @@ extension NSPasteboard { return result } - /// The pasteboard for the Ghostty enum type. + /// The pasteboard for the Ghostty enum type. Returns nil for locations + /// macOS can't serve; callers report those as unsupported. static func ghostty(_ clipboard: ghostty_clipboard_e) -> NSPasteboard? { switch clipboard { case GHOSTTY_CLIPBOARD_STANDARD: @@ -141,6 +142,10 @@ extension NSPasteboard { case GHOSTTY_CLIPBOARD_SELECTION: return Self.ghosttySelection + case GHOSTTY_CLIPBOARD_PRIMARY: + // macOS has no primary selection. + return nil + default: return nil } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 7bab01257..525bc856d 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -702,7 +702,13 @@ pub const Surface = struct { ) bool { return switch (clipboard_type) { .standard => true, - .selection, .primary => self.app.opts.supports_selection_clipboard, + .selection => self.app.opts.supports_selection_clipboard, + + // No embedder can serve the primary selection: the embedding + // API has no option to declare support for it, and macOS (the + // only GUI embedder) has no primary selection. The selection + // flag above covers a distinct custom pasteboard, not this. + .primary => false, }; } @@ -717,6 +723,14 @@ pub const Surface = struct { // completion that requires confirmation diverts into the // apprt confirmation flow just like a read. if (state == .kitty_write) { + // A write to a location the embedder can't serve is + // rejected before the transaction completes so the caller + // answers ENOSYS without a permission prompt, matching + // reads, where the embedder reports an unsupported + // location from the read callback. The write callback only + // fires after any prompt, so it is too late to reject. + if (!self.supportsClipboard(clipboard_type)) return .unsupported; + const alloc = self.app.core_app.alloc; const state_ptr = try alloc.create(apprt.ClipboardRequest); errdefer alloc.destroy(state_ptr); diff --git a/src/terminal/stream_terminal.zig b/src/terminal/stream_terminal.zig index 2fe4b3043..5630570c5 100644 --- a/src/terminal/stream_terminal.zig +++ b/src/terminal/stream_terminal.zig @@ -3776,6 +3776,18 @@ test "kitty clipboard write result maps to response status" { "\x1B]5522;type=write:status=DONE\x07", S.responseSlice(), ); + + // A system without a primary selection answers a loc=primary write + // with ENOSYS, echoing the id. + S.reset(); + S.write_result = .unsupported; + s.nextSlice("\x1B]5522;type=write:loc=primary:id=p1\x1B\\"); + s.nextSlice("\x1B]5522;type=wdata\x1B\\"); + try testing.expectEqual(clipboard.Location.primary, S.last_location); + try testing.expectEqualStrings( + "\x1B]5522;type=write:status=ENOSYS:id=p1\x1B\\", + S.responseSlice(), + ); } test "kitty clipboard write without clipboard effect responds ENOSYS" {