From c1f0ef73a9f3a80673d8be3e9ced5e50afaf65ac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Aug 2026 08:12:07 -0700 Subject: [PATCH] macos: serve copied files as text/uri-list in Kitty clipboard reads --- .../Extensions/NSPasteboard+Extension.swift | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift index fb152fa16..fc13c807a 100644 --- a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift @@ -54,20 +54,39 @@ extension NSPasteboard { return strings.joined(separator: " ") } + /// The file URLs on the pasteboard, e.g. files copied in Finder. + private var ghosttyFileURLs: [URL] { + (pasteboardItems ?? []).compactMap { item in + guard let plist = item.propertyList(forType: .fileURL), + let url = NSURL(pasteboardPropertyList: plist, ofType: .fileURL) as URL?, + url.isFileURL else { return nil } + return url + } + } + /// The data for the given MIME type, if the pasteboard can serve it. /// /// The canonical "text/plain" type uses the opinionated string /// contents so that e.g. copying a file yields its escaped path; - /// this matches what pasting into the terminal produces. All other + /// this matches what pasting into the terminal produces. Copied + /// files are additionally served as "text/uri-list" (RFC 2483, the + /// type X11/Wayland clipboards carry file copies under). All other /// types are mapped through UTType. func ghosttyData(forMime mime: String) -> Data? { - if mime == "text/plain" { + switch mime { + case "text/plain": guard let str = getOpinionatedStringContents() else { return nil } return Data(str.utf8) - } - guard let type = NSPasteboard.PasteboardType(mimeType: mime) else { return nil } - return data(forType: type) + case "text/uri-list": + let urls = ghosttyFileURLs + guard !urls.isEmpty else { return nil } + return Data(urls.map { $0.absoluteString + "\r\n" }.joined().utf8) + + default: + guard let type = NSPasteboard.PasteboardType(mimeType: mime) else { return nil } + return data(forType: type) + } } /// The MIME types available on the pasteboard, best-effort mapped @@ -84,6 +103,14 @@ extension NSPasteboard { seen.insert("text/plain") } + // Copied files are additionally served as a URI list. The + // generic mapping below never reports this since file URL + // pasteboard types have no MIME type. + if !ghosttyFileURLs.isEmpty { + result.append("text/uri-list") + seen.insert("text/uri-list") + } + for type in types ?? [] { guard let utType = UTType(type.rawValue), let mime = utType.preferredMIMEType,