diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index ab6dde118..fcd84e266 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -147,6 +147,7 @@ Features/Update/UpdatePopoverView.swift, Features/Update/UpdateSimulator.swift, Features/Update/UpdateViewModel.swift, + "Ghostty/Extensions/NSWorkspace+Ghostty.swift", "Ghostty/FullscreenMode+Extension.swift", Ghostty/Ghostty.Error.swift, Ghostty/Ghostty.Event.swift, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 0db39a09e..c0f739a2d 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -65,6 +65,7 @@ class AppDelegate: NSObject, @IBOutlet private var menuReturnToDefaultSize: NSMenuItem? @IBOutlet private var menuFloatOnTop: NSMenuItem? @IBOutlet private var menuUseAsDefault: NSMenuItem? + @IBOutlet private var menuSetAsDefaultTerminal: NSMenuItem? @IBOutlet private var menuIncreaseFontSize: NSMenuItem? @IBOutlet private var menuDecreaseFontSize: NSMenuItem? @@ -577,6 +578,7 @@ class AppDelegate: NSObject, self.menuChangeTabTitle?.setImageIfDesired(systemSymbolName: "pencil.line") self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope") self.menuReadonly?.setImageIfDesired(systemSymbolName: "eye.fill") + self.menuSetAsDefaultTerminal?.setImageIfDesired(systemSymbolName: "star.fill") self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward") self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye") self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right") @@ -1292,6 +1294,21 @@ extension AppDelegate { ud.removeObject(forKey: key) } } + + @IBAction func setAsDefaultTerminal(_ sender: NSMenuItem) { + do { + try NSWorkspace.shared.setGhosttyAsDefaultTerminal() + // Success - menu state will automatically update via validateMenuItem + } catch { + // Show error dialog + let alert = NSAlert() + alert.messageText = "Failed to Set Default Terminal" + alert.informativeText = "Ghostty could not be set as the default terminal application.\n\nError: \(error.localizedDescription)" + alert.alertStyle = .warning + alert.addButton(withTitle: "OK") + alert.runModal() + } + } } // MARK: NSMenuItemValidation @@ -1299,6 +1316,12 @@ extension AppDelegate { extension AppDelegate: NSMenuItemValidation { func validateMenuItem(_ item: NSMenuItem) -> Bool { switch item.action { + case #selector(setAsDefaultTerminal(_:)): + // Check if Ghostty is already the default terminal + let isDefault = NSWorkspace.shared.isGhosttyDefaultTerminal + // Disable menu item if already default (option A) + return !isDefault + case #selector(floatOnTop(_:)), #selector(useAsDefault(_:)): // Float on top items only active if the key window is a primary diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index e28344098..28c2a09c4 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -60,6 +60,7 @@ + @@ -109,6 +110,12 @@ + + + + + + diff --git a/macos/Sources/Ghostty/Extensions/NSWorkspace+Ghostty.swift b/macos/Sources/Ghostty/Extensions/NSWorkspace+Ghostty.swift new file mode 100644 index 000000000..e16169062 --- /dev/null +++ b/macos/Sources/Ghostty/Extensions/NSWorkspace+Ghostty.swift @@ -0,0 +1,34 @@ +import AppKit +import UniformTypeIdentifiers + +extension NSWorkspace { + /// Checks if Ghostty is the default terminal application. + /// - Returns: True if Ghostty is the default application for handling public.unix-executable files. + var isGhosttyDefaultTerminal: Bool { + let ghosttyURL = Bundle.main.bundleURL + guard let defaultAppURL = defaultApplicationURL(forContentType: "public.unix-executable") else { + return false + } + // Compare bundle paths + return ghosttyURL.path == defaultAppURL.path + } + + /// Sets Ghostty as the default terminal application. + /// - Throws: An error if the application bundle cannot be located or if setting the default fails. + func setGhosttyAsDefaultTerminal() throws { + let ghosttyURL = Bundle.main.bundleURL + + // Create UTType for unix executables + guard let unixExecutableType = UTType("public.unix-executable") else { + throw NSError( + domain: "com.mitchellh.ghostty", + code: 2, + userInfo: [NSLocalizedDescriptionKey: "Could not create UTType for public.unix-executable"] + ) + } + + // Use NSWorkspace API to set the default application + // This API is available on macOS 12.0+, Ghostty supports 13.0+, so it's compatible + setDefaultApplication(at: ghosttyURL, toOpen: unixExecutableType) + } +} diff --git a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift index bc2d028b5..809c927c7 100644 --- a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift @@ -26,4 +26,5 @@ extension NSWorkspace { guard let uti = UTType(filenameExtension: ext) else { return nil} return defaultApplicationURL(forContentType: uti.identifier) } + }