From c553eda4cb221677ecd633032d456d036acc88bd Mon Sep 17 00:00:00 2001 From: Jarne Date: Sun, 27 Sep 2026 17:33:00 +0200 Subject: [PATCH 1/2] Add support for executing commands in Ghostty --- Commandoak.xcodeproj/project.pbxproj | 10 ++- Commandoak/Commandoak.entitlements | 1 + Commandoak/Logic/ExecuteCommand.swift | 72 +++++++++++++++++++ .../View/Menu/CommandsMenuBarView.swift | 26 +------ 4 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 Commandoak/Logic/ExecuteCommand.swift diff --git a/Commandoak.xcodeproj/project.pbxproj b/Commandoak.xcodeproj/project.pbxproj index 1ec4240..f883043 100644 --- a/Commandoak.xcodeproj/project.pbxproj +++ b/Commandoak.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -58,6 +58,10 @@ 8B89EDFF2FE8076D0030F39D /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 8B2DDED130696C2600010840 /* Logic */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Logic; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 8B2A59AF2B8E72A700C2CAAF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -146,6 +150,7 @@ children = ( 8B50B2032B8BB4DC006CBF57 /* Info.plist */, 8B2A59A32B8E5E4800C2CAAF /* Model */, + 8B2DDED130696C2600010840 /* Logic */, 8B2A59A42B8E5E5200C2CAAF /* View */, 8B50B1E62B8B8084006CBF57 /* CommandoakApp.swift */, 8B50B1EC2B8B8085006CBF57 /* Assets.xcassets */, @@ -197,6 +202,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 8B2DDED130696C2600010840 /* Logic */, + ); name = Commandoak; productName = Commandoak; productReference = 8B50B1E32B8B8084006CBF57 /* Commandoak.app */; diff --git a/Commandoak/Commandoak.entitlements b/Commandoak/Commandoak.entitlements index c330f0f..ada1520 100644 --- a/Commandoak/Commandoak.entitlements +++ b/Commandoak/Commandoak.entitlements @@ -11,6 +11,7 @@ com.apple.security.temporary-exception.apple-events com.apple.Terminal + com.mitchellh.ghostty diff --git a/Commandoak/Logic/ExecuteCommand.swift b/Commandoak/Logic/ExecuteCommand.swift new file mode 100644 index 0000000..85d88fc --- /dev/null +++ b/Commandoak/Logic/ExecuteCommand.swift @@ -0,0 +1,72 @@ +// +// ExecuteCommand.swift +// Commandoak +// + +import AppKit +import Foundation + +class ExecuteCommand { + static func runCommand(command: String) { + let terminalAppBundleIdentifier = "com.mitchellh.ghostty" // TODO: change to setting value + + switch terminalAppBundleIdentifier { + case "com.apple.Terminal": + runCommandInSystemTerminal(command: command) + case "com.mitchellh.ghostty": + runCommandInGhostty(command: command) + default: + print("Invalid terminal application bundle identifier") + } + } + + private static func runCommandInSystemTerminal(command: String) { + guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Terminal") else { + print("Cannot find Terminal application") + return + } + + let conf = NSWorkspace.OpenConfiguration() + + let event = NSAppleEventDescriptor( + eventClass: kAECoreSuite, + eventID: kAEDoScript, + targetDescriptor: nil, + returnID: AEReturnID(kAutoGenerateReturnID), + transactionID: AETransactionID(kAnyTransactionID) + ) + event.setParam(NSAppleEventDescriptor(string: command), forKeyword: keyDirectObject) + conf.appleEvent = event + + NSWorkspace.shared.openApplication(at: url, configuration: conf) + } + + private static func fourCharCode(_ string: String) -> FourCharCode { + string.utf8.reduce(0) { ($0 << 8) | FourCharCode($1) } + } + + private static func runCommandInGhostty(command: String) { + guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.mitchellh.ghostty") else { + print("Cannot find Ghostty application") + return + } + + let conf = NSWorkspace.OpenConfiguration() + + let surfaceConfig = NSAppleEventDescriptor.record() + // scripting codes documented here: https://github.com/ghostty-org/ghostty/blob/main/macos/Ghostty.sdef + surfaceConfig.setDescriptor(NSAppleEventDescriptor(string: command + "\n"), forKeyword: fourCharCode("GScI")) + + let event = NSAppleEventDescriptor( + eventClass: fourCharCode("Ghst"), + eventID: fourCharCode("NWin"), + targetDescriptor: nil, + returnID: AEReturnID(kAutoGenerateReturnID), + transactionID: AETransactionID(kAnyTransactionID) + ) + event.setParam(surfaceConfig, forKeyword: fourCharCode("GNwS")) + conf.appleEvent = event + + NSWorkspace.shared.openApplication(at: url, configuration: conf) + } +} diff --git a/Commandoak/View/Menu/CommandsMenuBarView.swift b/Commandoak/View/Menu/CommandsMenuBarView.swift index 4124969..eec0add 100644 --- a/Commandoak/View/Menu/CommandsMenuBarView.swift +++ b/Commandoak/View/Menu/CommandsMenuBarView.swift @@ -35,12 +35,12 @@ struct CommandsMenuBarView: View { if kbShortcut != nil { Button("\(cmd.icon) \(cmd.name)") { - runCommand(command: cmd.execute) + ExecuteCommand.runCommand(command: cmd.execute) } .keyboardShortcut(kbShortcut) } else { Button("\(cmd.icon) \(cmd.name)") { - runCommand(command: cmd.execute) + ExecuteCommand.runCommand(command: cmd.execute) } } } @@ -69,28 +69,6 @@ struct CommandsMenuBarView: View { return nil } } - - private func runCommand(command: String) { - guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Terminal") else { - print("Cannot find Terminal application") - - return - } - - let conf = NSWorkspace.OpenConfiguration() - - let event = NSAppleEventDescriptor( - eventClass: kAECoreSuite, - eventID: kAEDoScript, - targetDescriptor: nil, - returnID: AEReturnID(kAutoGenerateReturnID), - transactionID: AETransactionID(kAnyTransactionID) - ) - event.setParam(NSAppleEventDescriptor(string: command), forKeyword: keyDirectObject) - conf.appleEvent = event - - NSWorkspace.shared.openApplication(at: url, configuration: conf) - } } #Preview { From 487c1d5bad3a1dfd35ecdf2a070650a70071d142 Mon Sep 17 00:00:00 2001 From: Jarne Date: Sun, 27 Sep 2026 17:46:36 +0200 Subject: [PATCH 2/2] Add app settings model and view to choose between preferred terminal app to use --- Commandoak.xcodeproj/project.pbxproj | 4 +++ Commandoak/Logic/ExecuteCommand.swift | 14 ++++------ Commandoak/Model/AppSettings.swift | 39 +++++++++++++++++++++++++++ Commandoak/View/SettingsView.swift | 9 ++++++- 4 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 Commandoak/Model/AppSettings.swift diff --git a/Commandoak.xcodeproj/project.pbxproj b/Commandoak.xcodeproj/project.pbxproj index f883043..fcc0616 100644 --- a/Commandoak.xcodeproj/project.pbxproj +++ b/Commandoak.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 8B2A59C42B8E783300C2CAAF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B2A59C32B8E783300C2CAAF /* main.swift */; }; 8B50B1E72B8B8084006CBF57 /* CommandoakApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B50B1E62B8B8084006CBF57 /* CommandoakApp.swift */; }; 8B50B1E92B8B8084006CBF57 /* CommandsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B50B1E82B8B8084006CBF57 /* CommandsView.swift */; }; + 8B3A0C012F0000000000A001 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B3A0C022F0000000000A002 /* AppSettings.swift */; }; 8B50B1EB2B8B8084006CBF57 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B50B1EA2B8B8084006CBF57 /* Command.swift */; }; 8B50B1ED2B8B8085006CBF57 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B50B1EC2B8B8085006CBF57 /* Assets.xcassets */; }; 8B50B1F02B8B8085006CBF57 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B50B1EF2B8B8085006CBF57 /* Preview Assets.xcassets */; }; @@ -48,6 +49,7 @@ 8B50B1E32B8B8084006CBF57 /* Commandoak.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Commandoak.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8B50B1E62B8B8084006CBF57 /* CommandoakApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandoakApp.swift; sourceTree = ""; }; 8B50B1E82B8B8084006CBF57 /* CommandsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandsView.swift; sourceTree = ""; }; + 8B3A0C022F0000000000A002 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; 8B50B1EA2B8B8084006CBF57 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; 8B50B1EC2B8B8085006CBF57 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 8B50B1EF2B8B8085006CBF57 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -83,6 +85,7 @@ 8B2A59A32B8E5E4800C2CAAF /* Model */ = { isa = PBXGroup; children = ( + 8B3A0C022F0000000000A002 /* AppSettings.swift */, 8B50B1EA2B8B8084006CBF57 /* Command.swift */, ); path = Model; @@ -285,6 +288,7 @@ files = ( 8B50B2022B8BABD7006CBF57 /* CommandsMenuBarView.swift in Sources */, 8B50B1E92B8B8084006CBF57 /* CommandsView.swift in Sources */, + 8B3A0C012F0000000000A001 /* AppSettings.swift in Sources */, 8B50B1EB2B8B8084006CBF57 /* Command.swift in Sources */, 8B2A59A72B8E5E9C00C2CAAF /* SettingsView.swift in Sources */, 8B50B1E72B8B8084006CBF57 /* CommandoakApp.swift in Sources */, diff --git a/Commandoak/Logic/ExecuteCommand.swift b/Commandoak/Logic/ExecuteCommand.swift index 85d88fc..2e04fbc 100644 --- a/Commandoak/Logic/ExecuteCommand.swift +++ b/Commandoak/Logic/ExecuteCommand.swift @@ -8,20 +8,16 @@ import Foundation class ExecuteCommand { static func runCommand(command: String) { - let terminalAppBundleIdentifier = "com.mitchellh.ghostty" // TODO: change to setting value - - switch terminalAppBundleIdentifier { - case "com.apple.Terminal": + switch AppSettings.terminalApp { + case .appleTerminal: runCommandInSystemTerminal(command: command) - case "com.mitchellh.ghostty": + case .ghostty: runCommandInGhostty(command: command) - default: - print("Invalid terminal application bundle identifier") } } private static func runCommandInSystemTerminal(command: String) { - guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Terminal") else { + guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: TerminalApp.appleTerminal.id) else { print("Cannot find Terminal application") return } @@ -46,7 +42,7 @@ class ExecuteCommand { } private static func runCommandInGhostty(command: String) { - guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.mitchellh.ghostty") else { + guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: TerminalApp.ghostty.id) else { print("Cannot find Ghostty application") return } diff --git a/Commandoak/Model/AppSettings.swift b/Commandoak/Model/AppSettings.swift new file mode 100644 index 0000000..dc39bfd --- /dev/null +++ b/Commandoak/Model/AppSettings.swift @@ -0,0 +1,39 @@ +// +// AppSettings.swift +// Commandoak +// + +import Foundation + +enum TerminalApp: String, CaseIterable, Identifiable { + case appleTerminal = "com.apple.Terminal" + case ghostty = "com.mitchellh.ghostty" + + var id: String { + rawValue + } + + var displayName: String { + switch self { + case .appleTerminal: + return "Apple Terminal" + case .ghostty: + return "Ghostty" + } + } +} + +enum AppSettings { + static let terminalAppKey = "terminalApp" + static let defaultTerminalApp = TerminalApp.appleTerminal + + static var terminalApp: TerminalApp { + get { + UserDefaults.standard.string(forKey: terminalAppKey) + .flatMap(TerminalApp.init(rawValue:)) ?? defaultTerminalApp + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: terminalAppKey) + } + } +} diff --git a/Commandoak/View/SettingsView.swift b/Commandoak/View/SettingsView.swift index 9e0faa8..adf660f 100644 --- a/Commandoak/View/SettingsView.swift +++ b/Commandoak/View/SettingsView.swift @@ -10,13 +10,20 @@ import SwiftUI struct SettingsView: View { @State private var autostart: Bool = false + @AppStorage(AppSettings.terminalAppKey) private var terminalApp: TerminalApp = AppSettings.defaultTerminalApp var body: some View { Form { Toggle("Start on login", isOn: $autostart) .onChange(of: autostart, save) - .padding() + .padding(.bottom, 8) + Picker("Terminal", selection: $terminalApp) { + ForEach(TerminalApp.allCases) { app in + Text(app.displayName).tag(app) + } + } } + .padding() .onAppear(perform: load) }