From 9b424399e746e663238fcdfd7aadb2ead0789444 Mon Sep 17 00:00:00 2001 From: Dustin Hilgaertner Date: Sun, 5 Apr 2026 14:27:42 -0500 Subject: [PATCH] Fix drag-and-drop files into terminal (#51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register GhosttySurfaceView for file URL drag types and implement NSDraggingDestination so dragging files from Finder types their shell-escaped paths into the terminal — matching standalone terminal behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CrowTerminal/GhosttySurfaceView.swift | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Packages/CrowTerminal/Sources/CrowTerminal/GhosttySurfaceView.swift b/Packages/CrowTerminal/Sources/CrowTerminal/GhosttySurfaceView.swift index 9d6a3aa..95e2520 100644 --- a/Packages/CrowTerminal/Sources/CrowTerminal/GhosttySurfaceView.swift +++ b/Packages/CrowTerminal/Sources/CrowTerminal/GhosttySurfaceView.swift @@ -28,6 +28,8 @@ public final class GhosttySurfaceView: NSView { wantsLayer = true layer?.isOpaque = true + + registerForDraggedTypes([.fileURL]) } @available(*, unavailable) @@ -323,6 +325,48 @@ public final class GhosttySurfaceView: NSView { NSLog("writeText: sent \(text.count) chars, \(parts.count - 1) newlines") } + // MARK: - Drag & Drop + + public override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + guard sender.draggingPasteboard.canReadObject( + forClasses: [NSURL.self], + options: [.urlReadingFileURLsOnly: true] + ) else { return [] } + return .copy + } + + public override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { + .copy + } + + public override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool { + true + } + + public override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + guard let surface else { return false } + guard let urls = sender.draggingPasteboard.readObjects( + forClasses: [NSURL.self], + options: [.urlReadingFileURLsOnly: true] + ) as? [URL], !urls.isEmpty else { return false } + + let escapedPaths = urls.map { Self.shellEscapePath($0.path) } + let text = escapedPaths.joined(separator: " ") + + text.withCString { ptr in + ghostty_surface_text(surface, ptr, UInt(text.utf8.count)) + } + return true + } + + private static func shellEscapePath(_ path: String) -> String { + let needsEscaping = path.contains { c in + " \t'\"\\()&|;!$`#*?[]{}~<>".contains(c) + } + guard needsEscaping else { return path } + return "'" + path.replacingOccurrences(of: "'", with: "'\\''") + "'" + } + // MARK: - Helpers private func translateMods(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {