diff --git a/Commandoak.xcodeproj/project.pbxproj b/Commandoak.xcodeproj/project.pbxproj index 1ec4240..fcc0616 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 */ @@ -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 = ""; }; @@ -58,6 +60,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; @@ -79,6 +85,7 @@ 8B2A59A32B8E5E4800C2CAAF /* Model */ = { isa = PBXGroup; children = ( + 8B3A0C022F0000000000A002 /* AppSettings.swift */, 8B50B1EA2B8B8084006CBF57 /* Command.swift */, ); path = Model; @@ -146,6 +153,7 @@ children = ( 8B50B2032B8BB4DC006CBF57 /* Info.plist */, 8B2A59A32B8E5E4800C2CAAF /* Model */, + 8B2DDED130696C2600010840 /* Logic */, 8B2A59A42B8E5E5200C2CAAF /* View */, 8B50B1E62B8B8084006CBF57 /* CommandoakApp.swift */, 8B50B1EC2B8B8085006CBF57 /* Assets.xcassets */, @@ -197,6 +205,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 8B2DDED130696C2600010840 /* Logic */, + ); name = Commandoak; productName = Commandoak; productReference = 8B50B1E32B8B8084006CBF57 /* Commandoak.app */; @@ -277,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/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..2e04fbc --- /dev/null +++ b/Commandoak/Logic/ExecuteCommand.swift @@ -0,0 +1,68 @@ +// +// ExecuteCommand.swift +// Commandoak +// + +import AppKit +import Foundation + +class ExecuteCommand { + static func runCommand(command: String) { + switch AppSettings.terminalApp { + case .appleTerminal: + runCommandInSystemTerminal(command: command) + case .ghostty: + runCommandInGhostty(command: command) + } + } + + private static func runCommandInSystemTerminal(command: String) { + guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: TerminalApp.appleTerminal.id) 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: TerminalApp.ghostty.id) 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/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/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 { 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) }