From a06fc4ff114f42c85f79f14ad13602d6b2bd74a5 Mon Sep 17 00:00:00 2001 From: Alexandre Antonio Juca Date: Sat, 11 Jan 2025 23:45:21 +0100 Subject: [PATCH 1/2] feat: ensure text, files and URLs can be drag and dropped to terminal window --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 14143313e..67154a3af 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -230,6 +230,8 @@ extension Ghostty { ghostty_surface_set_color_scheme(surface, scheme) } + + registerForDraggedTypes([.string, .fileURL, .URL]) } required init?(coder: NSCoder) { @@ -389,6 +391,68 @@ extension Ghostty { self?.title = title } } + + // MARK: - Drag and Drop + + override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation { + if let _ = sender.draggingPasteboard.string(forType: .string) { + return .generic + } + + if let _ = sender.draggingPasteboard.string(forType: .URL) { + return .generic + } + + if let _ = sender.draggingPasteboard.string(forType: .fileURL) { + return .generic + } + return [] + } + + override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool { + if let droppedText = sender.draggingPasteboard.string(forType: .string) { + let content = Shell.escape(droppedText) + + DispatchQueue.main.async { + self.insertText( + content, + replacementRange: NSMakeRange(0, 0) + ) + } + + return true + } + + if let droppedURL = sender.draggingPasteboard.string(forType: .URL) { + let content = Shell.escape(droppedURL) + + DispatchQueue.main.async { + self.insertText( + content, + replacementRange: NSMakeRange(0, 0) + ) + } + + return true + } + + if let droppedFileURL = sender.draggingPasteboard.string(forType: .fileURL) { + guard let urlPath = URL(string: droppedFileURL)?.path(percentEncoded: false) else { + return false + } + + DispatchQueue.main.async { + self.insertText( + urlPath, + replacementRange: NSMakeRange(0, 0) + ) + } + + return true + } + + return false + } // MARK: Local Events From a2d2cfea59854abfeee364efa22f21233785ad37 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Jan 2025 19:19:49 -0800 Subject: [PATCH 2/2] macos: move drop implementation to separate extension --- macos/Sources/Ghostty/SurfaceView.swift | 16 --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 127 +++++++++--------- 2 files changed, 63 insertions(+), 80 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 4abf87c7f..beae50331 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -92,22 +92,6 @@ extension Ghostty { windowFocus = false } } - .onDrop(of: [.fileURL], isTargeted: nil) { providers in - providers.forEach { provider in - _ = provider.loadObject(ofClass: URL.self) { url, _ in - guard let url = url else { return } - let path = Shell.escape(url.path) - DispatchQueue.main.async { - surfaceView.insertText( - path, - replacementRange: NSMakeRange(0, 0) - ) - } - } - } - - return true - } #endif // If our geo size changed then we show the resize overlay as configured. diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 67154a3af..f5cb93580 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1,3 +1,4 @@ +import AppKit import SwiftUI import CoreText import UserNotifications @@ -230,8 +231,9 @@ extension Ghostty { ghostty_surface_set_color_scheme(surface, scheme) } - - registerForDraggedTypes([.string, .fileURL, .URL]) + + // The UTTypes that can be dragged onto this view. + registerForDraggedTypes(Array(Self.dropTypes)) } required init?(coder: NSCoder) { @@ -391,68 +393,6 @@ extension Ghostty { self?.title = title } } - - // MARK: - Drag and Drop - - override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation { - if let _ = sender.draggingPasteboard.string(forType: .string) { - return .generic - } - - if let _ = sender.draggingPasteboard.string(forType: .URL) { - return .generic - } - - if let _ = sender.draggingPasteboard.string(forType: .fileURL) { - return .generic - } - return [] - } - - override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool { - if let droppedText = sender.draggingPasteboard.string(forType: .string) { - let content = Shell.escape(droppedText) - - DispatchQueue.main.async { - self.insertText( - content, - replacementRange: NSMakeRange(0, 0) - ) - } - - return true - } - - if let droppedURL = sender.draggingPasteboard.string(forType: .URL) { - let content = Shell.escape(droppedURL) - - DispatchQueue.main.async { - self.insertText( - content, - replacementRange: NSMakeRange(0, 0) - ) - } - - return true - } - - if let droppedFileURL = sender.draggingPasteboard.string(forType: .fileURL) { - guard let urlPath = URL(string: droppedFileURL)?.path(percentEncoded: false) else { - return false - } - - DispatchQueue.main.async { - self.insertText( - urlPath, - replacementRange: NSMakeRange(0, 0) - ) - } - - return true - } - - return false - } // MARK: Local Events @@ -1573,3 +1513,62 @@ extension Ghostty.SurfaceView: NSMenuItemValidation { } } } + +// MARK: NSDraggingDestination + +extension Ghostty.SurfaceView { + static let dropTypes: Set = [ + .string, + .fileURL, + .URL + ] + + override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation { + guard let types = sender.draggingPasteboard.types else { return [] } + + // If the dragging object contains none of our types then we return none. + // This shouldn't happen because AppKit should guarantee that we only + // receive types we registered for but its good to check. + if Set(types).isDisjoint(with: Self.dropTypes) { + return [] + } + + // We use copy to get the proper icon + return .copy + } + + override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool { + let pb = sender.draggingPasteboard + + let content: String? + if let url = pb.string(forType: .URL) { + // URLs first, they get escaped as-is. + content = Ghostty.Shell.escape(url) + } else if let urls = pb.readObjects(forClasses: [NSURL.self]) as? [URL], + urls.count > 0 { + // File URLs next. They get escaped individually and then joined by a + // space if there are multiple. + content = urls + .map { Ghostty.Shell.escape($0.path) } + .joined(separator: " ") + } else if let str = pb.string(forType: .string) { + // Strings are not escaped because they may be copy/pasting a + // command they want to execute. + content = str + } else { + content = nil + } + + if let content { + DispatchQueue.main.async { + self.insertText( + content, + replacementRange: NSMakeRange(0, 0) + ) + } + return true + } + + return false + } +}