Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Commandoak.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 70;
objects = {

/* Begin PBXBuildFile section */
Expand All @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
8B50B1E82B8B8084006CBF57 /* CommandsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandsView.swift; sourceTree = "<group>"; };
8B3A0C022F0000000000A002 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
8B50B1EA2B8B8084006CBF57 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
8B50B1EC2B8B8085006CBF57 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
8B50B1EF2B8B8085006CBF57 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
Expand All @@ -58,6 +60,10 @@
8B89EDFF2FE8076D0030F39D /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
8B2DDED130696C2600010840 /* Logic */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Logic; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
8B2A59AF2B8E72A700C2CAAF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
Expand All @@ -79,6 +85,7 @@
8B2A59A32B8E5E4800C2CAAF /* Model */ = {
isa = PBXGroup;
children = (
8B3A0C022F0000000000A002 /* AppSettings.swift */,
8B50B1EA2B8B8084006CBF57 /* Command.swift */,
);
path = Model;
Expand Down Expand Up @@ -146,6 +153,7 @@
children = (
8B50B2032B8BB4DC006CBF57 /* Info.plist */,
8B2A59A32B8E5E4800C2CAAF /* Model */,
8B2DDED130696C2600010840 /* Logic */,
8B2A59A42B8E5E5200C2CAAF /* View */,
8B50B1E62B8B8084006CBF57 /* CommandoakApp.swift */,
8B50B1EC2B8B8085006CBF57 /* Assets.xcassets */,
Expand Down Expand Up @@ -197,6 +205,9 @@
);
dependencies = (
);
fileSystemSynchronizedGroups = (
8B2DDED130696C2600010840 /* Logic */,
);
name = Commandoak;
productName = Commandoak;
productReference = 8B50B1E32B8B8084006CBF57 /* Commandoak.app */;
Expand Down Expand Up @@ -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 */,
Expand Down
1 change: 1 addition & 0 deletions Commandoak/Commandoak.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<key>com.apple.security.temporary-exception.apple-events</key>
<array>
<string>com.apple.Terminal</string>
<string>com.mitchellh.ghostty</string>
</array>
</dict>
</plist>
68 changes: 68 additions & 0 deletions Commandoak/Logic/ExecuteCommand.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
39 changes: 39 additions & 0 deletions Commandoak/Model/AppSettings.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
26 changes: 2 additions & 24 deletions Commandoak/View/Menu/CommandsMenuBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion Commandoak/View/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading