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" {