macos: support setting multiple clipboard content types

pull/9418/head
Mitchell Hashimoto 2025-10-30 13:49:14 -07:00
parent df037d75a6
commit 0f1c46e4a4
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
7 changed files with 141 additions and 12 deletions

View File

@ -45,6 +45,11 @@ typedef enum {
GHOSTTY_CLIPBOARD_SELECTION, GHOSTTY_CLIPBOARD_SELECTION,
} ghostty_clipboard_e; } ghostty_clipboard_e;
typedef struct {
const char *mime;
const char *data;
} ghostty_clipboard_content_s;
typedef enum { typedef enum {
GHOSTTY_CLIPBOARD_REQUEST_PASTE, GHOSTTY_CLIPBOARD_REQUEST_PASTE,
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ, GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
@ -855,8 +860,9 @@ typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
void*, void*,
ghostty_clipboard_request_e); ghostty_clipboard_request_e);
typedef void (*ghostty_runtime_write_clipboard_cb)(void*, typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
const char*,
ghostty_clipboard_e, ghostty_clipboard_e,
const ghostty_clipboard_content_s*,
size_t,
bool); bool);
typedef void (*ghostty_runtime_close_surface_cb)(void*, bool); typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t, typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,

View File

@ -61,7 +61,8 @@ extension Ghostty {
action_cb: { app, target, action in App.action(app!, target: target, action: action) }, action_cb: { app, target, action in App.action(app!, target: target, action: action) },
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) }, read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) }, confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) }, write_clipboard_cb: { userdata, loc, content, len, confirm in
App.writeClipboard(userdata, location: loc, content: content, len: len, confirm: confirm) },
close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) } close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) }
) )
@ -276,8 +277,9 @@ extension Ghostty {
static func writeClipboard( static func writeClipboard(
_ userdata: UnsafeMutableRawPointer?, _ userdata: UnsafeMutableRawPointer?,
string: UnsafePointer<CChar>?,
location: ghostty_clipboard_e, location: ghostty_clipboard_e,
content: UnsafePointer<ghostty_clipboard_content_s>?,
len: Int,
confirm: Bool confirm: Bool
) {} ) {}
@ -364,23 +366,48 @@ extension Ghostty {
} }
} }
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer<CChar>?, location: ghostty_clipboard_e, confirm: Bool) { static func writeClipboard(
_ userdata: UnsafeMutableRawPointer?,
location: ghostty_clipboard_e,
content: UnsafePointer<ghostty_clipboard_content_s>?,
len: Int,
confirm: Bool
) {
let surface = self.surfaceUserdata(from: userdata) let surface = self.surfaceUserdata(from: userdata)
guard let pasteboard = NSPasteboard.ghostty(location) else { return } guard let pasteboard = NSPasteboard.ghostty(location) else { return }
guard let valueStr = String(cString: string!, encoding: .utf8) else { return } guard let content = content, len > 0 else { return }
// Convert the C array to Swift array
let contentArray = (0..<len).compactMap { i in
Ghostty.ClipboardContent.from(content: content[i])
}
guard !contentArray.isEmpty else { return }
if !confirm { if !confirm {
pasteboard.declareTypes([.string], owner: nil) // Declare all types
pasteboard.setString(valueStr, forType: .string) let types = contentArray.compactMap { item in
NSPasteboard.PasteboardType(mimeType: item.mime)
}
pasteboard.declareTypes(types, owner: nil)
// Set data for each type
for item in contentArray {
guard let type = NSPasteboard.PasteboardType(mimeType: item.mime) else { continue }
pasteboard.setString(item.data, forType: type)
}
return return
} }
// For confirmation, use the text/plain content if it exists
guard let textPlainContent = contentArray.first(where: { $0.mime == "text/plain" }) else {
return
}
NotificationCenter.default.post( NotificationCenter.default.post(
name: Notification.confirmClipboard, name: Notification.confirmClipboard,
object: surface, object: surface,
userInfo: [ userInfo: [
Notification.ConfirmClipboardStrKey: valueStr, Notification.ConfirmClipboardStrKey: textPlainContent.data,
Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write(pasteboard), Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write(pasteboard),
] ]
) )

View File

@ -299,6 +299,23 @@ extension Ghostty {
} }
} }
} }
struct ClipboardContent {
let mime: String
let data: String
static func from(content: ghostty_clipboard_content_s) -> ClipboardContent? {
guard let mimePtr = content.mime,
let dataPtr = content.data else {
return nil
}
return ClipboardContent(
mime: String(cString: mimePtr),
data: String(cString: dataPtr)
)
}
}
/// macos-icon /// macos-icon
enum MacOSIcon: String { enum MacOSIcon: String {

View File

@ -1,5 +1,30 @@
import AppKit import AppKit
import GhosttyKit import GhosttyKit
import UniformTypeIdentifiers
extension NSPasteboard.PasteboardType {
/// Initialize a pasteboard type from a MIME type string
init?(mimeType: String) {
// Explicit mappings for common MIME types
switch mimeType {
case "text/plain":
self = .string
return
default:
break
}
// Try to get UTType from MIME type
guard let utType = UTType(mimeType: mimeType) else {
// Fallback: use the MIME type directly as identifier
self.init(mimeType)
return
}
// Use the UTType's identifier
self.init(utType.identifier)
}
}
extension NSPasteboard { extension NSPasteboard {
/// The pasteboard to used for Ghostty selection. /// The pasteboard to used for Ghostty selection.

View File

@ -0,0 +1,33 @@
//
// NSPasteboardTests.swift
// GhosttyTests
//
// Tests for NSPasteboard.PasteboardType MIME type conversion.
//
import Testing
import AppKit
@testable import Ghostty
struct NSPasteboardTypeExtensionTests {
/// Test text/plain MIME type converts to .string
@Test func testTextPlainMimeType() async throws {
let pasteboardType = NSPasteboard.PasteboardType(mimeType: "text/plain")
#expect(pasteboardType != nil)
#expect(pasteboardType == .string)
}
/// Test text/html MIME type converts to .html
@Test func testTextHtmlMimeType() async throws {
let pasteboardType = NSPasteboard.PasteboardType(mimeType: "text/html")
#expect(pasteboardType != nil)
#expect(pasteboardType == .html)
}
/// Test image/png MIME type
@Test func testImagePngMimeType() async throws {
let pasteboardType = NSPasteboard.PasteboardType(mimeType: "image/png")
#expect(pasteboardType != nil)
#expect(pasteboardType == .png)
}
}

View File

@ -66,7 +66,13 @@ pub const App = struct {
) callconv(.c) void, ) callconv(.c) void,
/// Write the clipboard value. /// Write the clipboard value.
write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int, bool) callconv(.c) void, write_clipboard: *const fn (
SurfaceUD,
c_int,
[*]const CAPI.ClipboardContent,
usize,
bool,
) callconv(.c) void,
/// Close the current surface given by this function. /// Close the current surface given by this function.
close_surface: ?*const fn (SurfaceUD, bool) callconv(.c) void = null, close_surface: ?*const fn (SurfaceUD, bool) callconv(.c) void = null,
@ -707,8 +713,12 @@ pub const Surface = struct {
) !void { ) !void {
self.app.opts.write_clipboard( self.app.opts.write_clipboard(
self.userdata, self.userdata,
val.ptr,
@intCast(@intFromEnum(clipboard_type)), @intCast(@intFromEnum(clipboard_type)),
&.{.{
.mime = "text/plain",
.data = val.ptr,
}},
1,
confirm, confirm,
); );
} }
@ -1211,6 +1221,12 @@ pub const CAPI = struct {
cell_height_px: u32, cell_height_px: u32,
}; };
// ghostty_clipboard_content_s
const ClipboardContent = extern struct {
mime: [*:0]const u8,
data: [*:0]const u8,
};
// ghostty_text_s // ghostty_text_s
const Text = extern struct { const Text = extern struct {
tl_px_x: f64, tl_px_x: f64,

View File

@ -54,6 +54,11 @@ pub const Clipboard = enum(Backing) {
}; };
}; };
pub const ClipboardContent = struct {
mime: [:0]const u8,
data: [:0]const u8,
};
pub const ClipboardRequestType = enum(u8) { pub const ClipboardRequestType = enum(u8) {
paste, paste,
osc_52_read, osc_52_read,