diff --git a/Astrix/Sources/Utilities/Constants.swift b/Astrix/Sources/Utilities/Constants.swift index aa94c2d..1d1c64f 100644 --- a/Astrix/Sources/Utilities/Constants.swift +++ b/Astrix/Sources/Utilities/Constants.swift @@ -55,7 +55,7 @@ public enum SupportedApps: String, CaseIterable { case hyper = "co.zeit.hyper" // MARK: - Editors case none = "NONE" - case xcode = "com.apple.Xcode" + case xcode = "com.apple.dt.Xcode" case vsCode = "com.microsoft.VSCode" case vsCodeInsiders = "com.microsoft.VSCodeInsiders" case atom = "com.github.atom" diff --git a/Astrix/Sources/Utilities/Scripting.swift b/Astrix/Sources/Utilities/Scripting.swift index 8f335bf..ab03581 100644 --- a/Astrix/Sources/Utilities/Scripting.swift +++ b/Astrix/Sources/Utilities/Scripting.swift @@ -47,10 +47,7 @@ class Scripting { .appendingPathExtension(Constants.Scripting.ToolsFileExtension) let toolsScript = """ on runCommand(command) - tell application "Finder" - activate - do shell script command - end tell + do shell script command end runCommand """ try writeScriptIfNeeded(at: toolsPath, with: toolsScript) @@ -81,7 +78,7 @@ class Scripting { return event } - private func isAppInstalled(bundleIdentifier: String) -> Bool { + public func isAppInstalled(bundleIdentifier: String) -> Bool { let workspace = NSWorkspace.shared return workspace.urlForApplication(withBundleIdentifier: bundleIdentifier) != nil } diff --git a/FinderTools/FinderSync.swift b/FinderTools/FinderSync.swift index 18df9e5..74b7d33 100644 --- a/FinderTools/FinderSync.swift +++ b/FinderTools/FinderSync.swift @@ -45,99 +45,33 @@ class FinderSync: FIFinderSync { switch menuKind { case .contextualMenuForContainer, - .contextualMenuForItems: - let itemPaths = FIFinderSyncController.default().selectedItemURLs() - let workspacePath = FIFinderSyncController.default().targetedURL() - + .contextualMenuForItems: let astrixMenu = NSMenu(title: "") - // Check if there are any selected files which could be copied to the clipboard - if itemPaths != nil && !itemPaths!.isEmpty { - if itemPaths!.first!.relativePath != workspacePath?.relativePath { - // Create items menu title - astrixMenu.addItem(Utilities.createTitleItem(title: "Item")) - - // Create the copy path for the items - var title = NSLocalizedString("Copy Path", comment: "") - if itemPaths!.count > 1 { - title = NSLocalizedString("Copy Paths", comment: "") - } - let copyPathItem = NSMenuItem(title: title, action: #selector(copyItemPath(_:)), keyEquivalent: "") - astrixMenu.addItem(copyPathItem) - } - } - - // Create workspace menu title - astrixMenu.addItem(Utilities.createTitleItem(title: "Workspace")) - - // Add all the workspace items - for item in getWorkspaceItems() { - astrixMenu.addItem(item) - } + addSection(ItemSection(), to: astrixMenu) + addSection(WorkspaceSection(), to: astrixMenu) + // Create the main menu button let mainMenuItem = NSMenuItem(title: "Astrix", action: nil, keyEquivalent: "") mainMenuItem.submenu = astrixMenu menu.addItem(mainMenuItem) case .toolbarItemMenu: - for item in getWorkspaceItems() { - menu.addItem(item) - } + addSection(WorkspaceSection(), to: menu) + addSection(SuggestionsSection(), to: menu) default: return nil } - return menu } - private func getWorkspaceItems() -> [NSMenuItem] { - let userDefaults = UserDefaults(suiteName: Constants.Id.DefaultsDomain) - let terminalKey = userDefaults?.string(forKey: Constants.Id.DefaultTerminalKey) ?? SupportedApps.none.rawValue - let editorKey = userDefaults?.string(forKey: Constants.Id.DefaultEditorKey) ?? SupportedApps.none.rawValue - - var result: [NSMenuItem] = [] - - // Open a terminal in this workspace - if terminalKey != SupportedApps.none.rawValue { - let openInTerminalItem = NSMenuItem(title: NSLocalizedString("Open in Terminal", comment: ""), action: #selector(openInTerminal(_:)), keyEquivalent: "") - result.append(openInTerminalItem) + private func addSection(_ section: AstrixSection, to menu: NSMenu, showTitle: Bool = true) { + let items = section.getSectionItems() + if showTitle && !items.isEmpty { + menu.addItem(Utilities.createTitleItem(title: section.sectionName)) } - // Open the editor in this workspace - if editorKey != SupportedApps.none.rawValue { - let openInEditorItem = NSMenuItem(title: NSLocalizedString("Open in Editor", comment: ""), action: #selector(openInEditor(_:)), keyEquivalent: "") - result.append(openInEditorItem) - } - - // Copy the path of the current workspace - let copyPathItem = NSMenuItem(title: NSLocalizedString("Copy Workspace Path", comment: ""), action: #selector(copyWorkspacePath(_:)), keyEquivalent: "") - result.append(copyPathItem) - - return result - } - - // MARK: -- Actions - /// Open the current workspace in a new terminal - @objc func openInTerminal(_ sender: AnyObject?) { - // Get the preferred terminal - let userDefaults = UserDefaults(suiteName: Constants.Id.DefaultsDomain) - let bundleIdString = userDefaults?.string(forKey: Constants.Id.DefaultTerminalKey) ?? SupportedApps.terminal.rawValue - let bundleId = SupportedApps(rawValue: bundleIdString) ?? .terminal - // Try to open the app with the bundle Id - if !Utilities.openApp(bundleId: bundleId) { - // Warn the user if it failed - Utilities.showNotification(title: NSLocalizedString("Oops!", comment: ""), body: NSLocalizedString("We were not able to open your terminal.", comment: "")) - } - } - /// Open the current workspace in the users preferred editor - @objc func openInEditor(_ sender: AnyObject?) { - // Get the preferred editor - let userDefaults = UserDefaults(suiteName: Constants.Id.DefaultsDomain) - let bundleIdString = userDefaults?.string(forKey: Constants.Id.DefaultEditorKey) ?? SupportedApps.none.rawValue - let bundleId = SupportedApps(rawValue: bundleIdString) ?? .none - // Try to open the app with the bundle Id - if !Utilities.openApp(bundleId: bundleId) { - // Warn the user if it failed - Utilities.showNotification(title: NSLocalizedString("Oops!", comment: ""), body: NSLocalizedString("We were not able to open your editor of choice.", comment: "")) + for item in items { + menu.addItem(item) } } @@ -166,8 +100,35 @@ class FinderSync: FIFinderSync { } } + // MARK: -- Workspace Actions + /// Open the current workspace in the users preferred terminal + @objc public func openInTerminal(_ sender: AnyObject?) { + // Get the preferred terminal + let userDefaults = UserDefaults(suiteName: Constants.Id.DefaultsDomain) + let bundleIdString = userDefaults?.string(forKey: Constants.Id.DefaultTerminalKey) ?? SupportedApps.terminal.rawValue + let bundleId = SupportedApps(rawValue: bundleIdString) ?? .terminal + // Try to open the app with the bundle Id + if !Utilities.openWorkspaceInApp(bundleId: bundleId) { + // Warn the user if it failed + Utilities.showNotification(title: NSLocalizedString("Oops!", comment: ""), body: NSLocalizedString("We were not able to open your terminal.", comment: "")) + } + } + + /// Open the current workspace in the users preferred editor + @objc open func openInEditor(_ sender: AnyObject?) { + // Get the preferred editor + let userDefaults = UserDefaults(suiteName: Constants.Id.DefaultsDomain) + let bundleIdString = userDefaults?.string(forKey: Constants.Id.DefaultEditorKey) ?? SupportedApps.none.rawValue + let bundleId = SupportedApps(rawValue: bundleIdString) ?? .none + // Try to open the app with the bundle Id + if !Utilities.openWorkspaceInApp(bundleId: bundleId) { + // Warn the user if it failed + Utilities.showNotification(title: NSLocalizedString("Oops!", comment: ""), body: NSLocalizedString("We were not able to open your editor of choice.", comment: "")) + } + } + /// Copy the path of the workspace folder - @objc func copyWorkspacePath(_ sender: AnyObject?) { + @objc open func copyWorkspacePath(_ sender: AnyObject?) { // Get the workspace path let workspacePath = FIFinderSyncController.default().targetedURL() @@ -183,4 +144,48 @@ class FinderSync: FIFinderSync { pasteboard.setString(workspacePath!.relativePath, forType: .string) Utilities.showNotification(title: NSLocalizedString("Copied!", comment: ""), body: NSLocalizedString("The path is copied to your clipboard.", comment: "")) } + + // MARK: -- Item Actions + /// Open the current selected file/folder in the users preferred editor + @objc open func openItemInEditor(_ sender: AnyObject?) { + // Get the preferred editor + let userDefaults = UserDefaults(suiteName: Constants.Id.DefaultsDomain) + let bundleIdString = userDefaults?.string(forKey: Constants.Id.DefaultEditorKey) ?? SupportedApps.none.rawValue + let bundleId = SupportedApps(rawValue: bundleIdString) ?? .none + // Try to open the app with the bundle Id + if !Utilities.openSelectedInApp(bundleId: bundleId) { + // Warn the user if it failed + Utilities.showNotification(title: NSLocalizedString("Oops!", comment: ""), body: NSLocalizedString("We were not able to open your editor of choice.", comment: "")) + } + } + + // MARK: -- Suggestion Actions + @objc func openInVSCode(_ sender: Any) { + // Try to open the app with the bundle Id + if !Utilities.openWorkspaceInApp(bundleId: .vsCode) { + // Warn the user if it failed + Utilities.showNotification(title: NSLocalizedString("Oops!", comment: ""), body: NSLocalizedString("We were not able to open the workspace in \(Utilities.getBundleApplicationName(bundleId: .vsCode)).", comment: "")) + } + } + @objc func openInVSCodeInsiders(_ sender: Any) { + // Try to open the app with the bundle Id + if !Utilities.openWorkspaceInApp(bundleId: .vsCodeInsiders) { + // Warn the user if it failed + Utilities.showNotification(title: NSLocalizedString("Oops!", comment: ""), body: NSLocalizedString("We were not able to open the workspace in \(Utilities.getBundleApplicationName(bundleId: .vsCodeInsiders)).", comment: "")) + } + } + @objc func openInCursor(_ sender: Any) { + // Try to open the app with the bundle Id + if !Utilities.openWorkspaceInApp(bundleId: .cursor) { + // Warn the user if it failed + Utilities.showNotification(title: NSLocalizedString("Oops!", comment: ""), body: NSLocalizedString("We were not able to open the workspace in \(Utilities.getBundleApplicationName(bundleId: .cursor)).", comment: "")) + } + } + @objc func openInXCode(_ sender: Any) { + // Try to open the app with the bundle Id + if !Utilities.openWorkspaceInApp(bundleId: .xcode) { + // Warn the user if it failed + Utilities.showNotification(title: NSLocalizedString("Oops!", comment: ""), body: NSLocalizedString("We were not able to open the workspace in \(Utilities.getBundleApplicationName(bundleId: .xcode)).", comment: "")) + } + } } diff --git a/FinderTools/Sources/AstrixSection.swift b/FinderTools/Sources/AstrixSection.swift new file mode 100644 index 0000000..43db208 --- /dev/null +++ b/FinderTools/Sources/AstrixSection.swift @@ -0,0 +1,7 @@ +import SwiftUI + +protocol AstrixSection { + var sectionName: String { get } + + func getSectionItems() -> [NSMenuItem] +} diff --git a/FinderTools/Sources/ItemSection.swift b/FinderTools/Sources/ItemSection.swift new file mode 100644 index 0000000..41e0de6 --- /dev/null +++ b/FinderTools/Sources/ItemSection.swift @@ -0,0 +1,33 @@ +// +// SuggestionsSection.swift +// FinderTools +// +// Created by Thom van den Broek on 19/11/2024. +// + +import SwiftUI +import FinderSync + +class ItemSection: AstrixSection { + var sectionName: String { NSLocalizedString("Item", comment: "Item section") } + + func getSectionItems() -> [NSMenuItem] { + var result: [NSMenuItem] = [] + + if let workspacePath = FIFinderSyncController.default().targetedURL(), + let itemPaths = FIFinderSyncController.default().selectedItemURLs() { + // Only allow it on single items + if itemPaths.count > 1 || workspacePath.relativePath == itemPaths.first?.relativePath { return [] } + + let userDefaults = UserDefaults(suiteName: Constants.Id.DefaultsDomain) + let editorKey = userDefaults?.string(forKey: Constants.Id.DefaultEditorKey) ?? SupportedApps.none.rawValue + + // Open the editor in this workspace + if editorKey != SupportedApps.none.rawValue { + let openInEditorItem = NSMenuItem(title: NSLocalizedString("Open in Editor", comment: ""), action: #selector(FinderSync.openItemInEditor(_:)), keyEquivalent: "") + result.append(openInEditorItem) + } + } + return result + } +} diff --git a/FinderTools/Sources/SuggestionsSection.swift b/FinderTools/Sources/SuggestionsSection.swift new file mode 100644 index 0000000..698c993 --- /dev/null +++ b/FinderTools/Sources/SuggestionsSection.swift @@ -0,0 +1,62 @@ +// +// SuggestionsSection.swift +// FinderTools +// +// Created by Thom van den Broek on 19/11/2024. +// + +import SwiftUI +import FinderSync + +class SuggestionsSection: AstrixSection { + var sectionName: String { NSLocalizedString("Suggestions", comment: "Suggestions section") } + + func getSectionItems() -> [NSMenuItem] { + var result: [NSMenuItem] = [] + + if let workspacePath = FIFinderSyncController.default().targetedURL() { + // check if the folder contains a .vscode folder + if FileManager.default.fileExists(atPath: (workspacePath.appendingPathComponent(".vscode")).path) { + if Scripting.shared.isAppInstalled(bundleIdentifier: SupportedApps.vsCode.rawValue) { + let item = NSMenuItem( + title: NSLocalizedString("Open in \(Utilities.getBundleApplicationName(bundleId: .vsCode))", comment: ""), + action: #selector(FinderSync.openInVSCode(_:)), + keyEquivalent: "" + ) + result.append(item) + } + if Scripting.shared.isAppInstalled(bundleIdentifier: SupportedApps.vsCodeInsiders.rawValue) { + let item = NSMenuItem( + title: NSLocalizedString("Open in \(Utilities.getBundleApplicationName(bundleId: .vsCodeInsiders))", comment: ""), + action: #selector(FinderSync.openInVSCodeInsiders(_:)), + keyEquivalent: "" + ) + result.append(item) + } + if Scripting.shared.isAppInstalled(bundleIdentifier: SupportedApps.cursor.rawValue) { + let item = NSMenuItem( + title: NSLocalizedString("Open in \(Utilities.getBundleApplicationName(bundleId: .cursor))", comment: ""), + action: #selector(FinderSync.openInCursor(_:)), + keyEquivalent: "" + ) + result.append(item) + } + } + + // check if there is any file ending with .xcodeproj + let (success, response) = Utilities.runCommand("cd '\(workspacePath.relativePath)' && find *.xcodeproj -d 0") + NSLog("success: \(success), res: \(response)") + NSLog("installed: \(Scripting.shared.isAppInstalled(bundleIdentifier: SupportedApps.xcode.rawValue))") + if success && Scripting.shared.isAppInstalled(bundleIdentifier: SupportedApps.xcode.rawValue) { + let item = NSMenuItem( + title: NSLocalizedString("Open in \(Utilities.getBundleApplicationName(bundleId: .xcode))", comment: ""), + action: #selector(FinderSync.openInXCode(_:)), + keyEquivalent: "" + ) + result.append(item) + } + } + + return result + } +} diff --git a/FinderTools/Utilities.swift b/FinderTools/Sources/Utilities.swift similarity index 64% rename from FinderTools/Utilities.swift rename to FinderTools/Sources/Utilities.swift index 493ac6d..2447e61 100644 --- a/FinderTools/Utilities.swift +++ b/FinderTools/Sources/Utilities.swift @@ -17,7 +17,7 @@ class Utilities { } /// Open the current folder with a specified app - static func openApp (bundleId: SupportedApps) -> Bool { + static func openWorkspaceInApp (bundleId: SupportedApps) -> Bool { if bundleId == .none { return false } // Get the workspace url @@ -26,27 +26,48 @@ class Utilities { // Create the open command let openCommand = getOpenCommand(bundleId: bundleId, url: url!) + let (success, _) = runCommand(openCommand) + return success + } + + /// Open the the selected file/folder with a specified app + static func openSelectedInApp (bundleId: SupportedApps) -> Bool { + if bundleId == .none { return false } + // Get the workspace url + let url = FIFinderSyncController.default().selectedItemURLs()?.first + if url == nil { return false } + + // Create the open command + let openCommand = getOpenCommand(bundleId: bundleId, url: url!) + let (success, _) = runCommand(openCommand) + return success + } + + static func runCommand(_ command: String) -> (success: Bool, response: String) { // Try and load the applescript - guard let scriptURL = Scripting.shared.getScriptURL(name: Constants.Scripting.ToolsFileName) else { return false } - guard FileManager.default.fileExists(atPath: scriptURL.path) else { return false } - guard let script = try? NSUserAppleScriptTask(url: scriptURL) else { return false } + guard let scriptURL = Scripting.shared.getScriptURL(name: Constants.Scripting.ToolsFileName) else { return (false, "") } + guard FileManager.default.fileExists(atPath: scriptURL.path) else { return (false, "") } + guard let script = try? NSUserAppleScriptTask(url: scriptURL) else { return (false, "") } - let event = Scripting.shared.getScriptEvent(functionName: "runCommand", openCommand) + let event = Scripting.shared.getScriptEvent(functionName: "runCommand", command) let semaphore = DispatchSemaphore(value: 0) var success = true + var response = "" // Execute the script - script.execute(withAppleEvent: event) { (_, error) in + script.execute(withAppleEvent: event) { (result, error) in if let error = error { NSLog("could not load script: \(error.localizedDescription)") success = false + } else if let scriptResult = result?.stringValue { + response = scriptResult } semaphore.signal() } _ = semaphore.wait(timeout: .distantFuture) - return success + return (success, response) } /// Create a local notification for the user @@ -70,4 +91,8 @@ class Utilities { item.isEnabled = false return item } + + static func getBundleApplicationName(bundleId: SupportedApps) -> String { + Constants.Scripting.SupportedEditorApplications.first(where: { $0.0 == bundleId })?.1 ?? "Unknown" + } } diff --git a/FinderTools/Sources/WorkspaceSection.swift b/FinderTools/Sources/WorkspaceSection.swift new file mode 100644 index 0000000..1258f09 --- /dev/null +++ b/FinderTools/Sources/WorkspaceSection.swift @@ -0,0 +1,38 @@ +// +// WorkspaceSection.swift +// FinderTools +// +// Created by Thom van den Broek on 19/11/2024. +// + +import SwiftUI +import FinderSync + +public class WorkspaceSection: AstrixSection { + var sectionName: String { NSLocalizedString("Workspace", comment: "Workspace section") } + + func getSectionItems() -> [NSMenuItem] { + let userDefaults = UserDefaults(suiteName: Constants.Id.DefaultsDomain) + let terminalKey = userDefaults?.string(forKey: Constants.Id.DefaultTerminalKey) ?? SupportedApps.none.rawValue + let editorKey = userDefaults?.string(forKey: Constants.Id.DefaultEditorKey) ?? SupportedApps.none.rawValue + + var result: [NSMenuItem] = [] + + // Open a terminal in this workspace + if terminalKey != SupportedApps.none.rawValue { + let openInTerminalItem = NSMenuItem(title: NSLocalizedString("Open in Terminal", comment: ""), action: #selector(FinderSync.openInTerminal(_:)), keyEquivalent: "") + result.append(openInTerminalItem) + } + // Open the editor in this workspace + if editorKey != SupportedApps.none.rawValue { + let openInEditorItem = NSMenuItem(title: NSLocalizedString("Open in Editor", comment: ""), action: #selector(FinderSync.openInEditor(_:)), keyEquivalent: "") + result.append(openInEditorItem) + } + + // Copy the path of the current workspace + let copyPathItem = NSMenuItem(title: NSLocalizedString("Copy Workspace Path", comment: ""), action: #selector(FinderSync.copyWorkspacePath(_:)), keyEquivalent: "") + result.append(copyPathItem) + + return result + } +} diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index a1c482d..0eb3efc 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -31,26 +31,6 @@ } } }, - "Copy Path" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kopieer pad" - } - } - } - }, - "Copy Paths" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kopieer paden" - } - } - } - }, "Copy Workspace Path" : { "localizations" : { "nl" : { @@ -132,6 +112,17 @@ } } }, + "Item" : { + "comment" : "Item section", + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Item" + } + } + } + }, "Let's go!" : { "localizations" : { "nl" : { @@ -252,6 +243,17 @@ } } }, + "Suggestions" : { + "comment" : "Suggestions section", + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suggesties" + } + } + } + }, "The path is copied to your clipboard." : { "localizations" : { "nl" : { @@ -362,6 +364,17 @@ } } }, + "Workspace" : { + "comment" : "Workspace section", + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Folder" + } + } + } + }, "You are all set to begin your journey with Astrix! Try adding Astrix to your Finder toolbar and enjoy a more streamlined workflow." : { "localizations" : { "nl" : {