macos: support setting multiple clipboard content types
parent
df037d75a6
commit
0f1c46e4a4
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue