diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index 4d2203882..28499843d 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -22,6 +22,47 @@ GHOSTTY_MAC_APP 1 + NSServices + + + NSMenuItem + + default + New Ghostty Tab Here + + NSMessage + openTab + NSRequiredContext + + NSTextContent + FilePath + + NSSendTypes + + NSFilenamesPboardType + public.plain-text + + + + NSMenuItem + + default + New Ghostty Window Here + + NSMessage + openWindow + NSRequiredContext + + NSTextContent + FilePath + + NSSendTypes + + NSFilenamesPboardType + public.plain-text + + + NSHighResolutionCapable NSAppleEventsUsageDescription diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 18da5c7cc..dd6823fd0 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */; }; A56B880B2A840447007A0E29 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56B880A2A840447007A0E29 /* Carbon.framework */; }; A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */; }; + A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */; }; A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; @@ -55,6 +56,7 @@ A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.SplitView.swift; sourceTree = ""; }; A56B880A2A840447007A0E29 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Shell.swift; sourceTree = ""; }; + A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceProvider.swift; sourceTree = ""; }; A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; @@ -89,6 +91,7 @@ A53426362A7DC53000EBB7A2 /* Features */ = { isa = PBXGroup; children = ( + A56D58872ACDE6BE00508D2C /* Services */, A53426372A7DC53A00EBB7A2 /* Primary Window */, A534263E2A7DCC5800EBB7A2 /* Settings */, ); @@ -156,6 +159,14 @@ path = Ghostty; sourceTree = ""; }; + A56D58872ACDE6BE00508D2C /* Services */ = { + isa = PBXGroup; + children = ( + A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */, + ); + path = Services; + sourceTree = ""; + }; A5A1F8862A489D7400D1E8BC /* Resources */ = { isa = PBXGroup; children = ( @@ -279,6 +290,7 @@ files = ( A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */, 85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */, + A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index f210dcf7e..e23214154 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -40,7 +40,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp private var ghostty: Ghostty.AppState = Ghostty.AppState() /// Manages windows and tabs, ensuring they're allocated/deallocated correctly - private var windowManager: PrimaryWindowManager! + var windowManager: PrimaryWindowManager! override init() { super.init() @@ -72,6 +72,10 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp // Initial config loading configDidReload(ghostty) + + // Register our service provider. This must happen after everything + // else is initialized. + NSApp.servicesProvider = ServiceProvider() } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { diff --git a/macos/Sources/Features/Services/ServiceProvider.swift b/macos/Sources/Features/Services/ServiceProvider.swift new file mode 100644 index 000000000..a95dd4923 --- /dev/null +++ b/macos/Sources/Features/Services/ServiceProvider.swift @@ -0,0 +1,68 @@ +import Foundation +import AppKit + +class ServiceProvider: NSObject { + static private let errorNoString = NSString(string: "Could not load any text from the clipboard.") + + /// The target for an open operation + enum OpenTarget { + case tab + case window + } + + @objc func openTab( + _ pasteboard: NSPasteboard, + userData: String?, + error: AutoreleasingUnsafeMutablePointer + ) { + guard let str = pasteboard.string(forType: .string) else { + error.pointee = Self.errorNoString + return + } + + openTerminal(str, target: .tab) + } + + @objc func openWindow( + _ pasteboard: NSPasteboard, + userData: String?, + error: AutoreleasingUnsafeMutablePointer + ) { + guard let str = pasteboard.string(forType: .string) else { + error.pointee = Self.errorNoString + return + } + + openTerminal(str, target: .window) + } + + private func openTerminal(_ path: String, target: OpenTarget) { + guard let delegateRaw = NSApp.delegate else { return } + guard let delegate = delegateRaw as? AppDelegate else { return } + guard let windowManager = delegate.windowManager else { return } + + // We only open in directories. + var isDirectory = ObjCBool(true) + guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { return } + guard isDirectory.boolValue else { return } + + // Build our config + var config = Ghostty.SurfaceConfiguration() + config.workingDirectory = path + + // If we don't have a window open through the window manager, we launch + // a new window even if they requested a tab. + guard let mainWindow = windowManager.mainWindow else { + windowManager.addNewWindow(withBaseConfig: config) + return + } + + switch (target) { + case .window: + windowManager.addNewWindow(withBaseConfig: config) + + case .tab: + windowManager.addNewTab(to: mainWindow, withBaseConfig: config) + } + } +}