From 05987da2750282a5b79850fa42089c62919723c5 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 26 Nov 2025 14:06:47 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20"Open=20With"?= =?UTF-8?q?=20with=20third-party=20apps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Icons are now loaded if possible (if the path could be inferred) - Some new apps that are detected by default: - Browsers: Safari, Google Chrome, Microsoft Edge, Firefox, Brave, Arc, Zen - Editors: WebStorm, VSCodium - Git GUI: Tower, SourceTree - Terminal: Ghostty - `openWithEditor` has been refactored to `openWithApp` which now lets browsers open domains from their URL, not from their local folder. --- .../Extensions/NSMenuItemExtension.swift | 4 +- phpmon/Common/Helpers/Application.swift | 61 ++++++++++++++++--- .../Domain List/UI/DomainListVC+Actions.swift | 15 ++++- .../UI/DomainListVC+ContextMenu.swift | 19 ++++-- 4 files changed, 80 insertions(+), 19 deletions(-) diff --git a/phpmon/Common/Extensions/NSMenuItemExtension.swift b/phpmon/Common/Extensions/NSMenuItemExtension.swift index 38d21fc0..abc494ef 100644 --- a/phpmon/Common/Extensions/NSMenuItemExtension.swift +++ b/phpmon/Common/Extensions/NSMenuItemExtension.swift @@ -84,8 +84,8 @@ class ExtensionMenuItem: NSMenuItem { var phpExtension: PhpExtension? } -class EditorMenuItem: NSMenuItem { - var editor: Application? +class ApplicationMenuItem: NSMenuItem { + var app: Application? } class PresetMenuItem: NSMenuItem { diff --git a/phpmon/Common/Helpers/Application.swift b/phpmon/Common/Helpers/Application.swift index 46f561b6..ffa40f36 100644 --- a/phpmon/Common/Helpers/Application.swift +++ b/phpmon/Common/Helpers/Application.swift @@ -14,7 +14,7 @@ import Foundation class Application { enum AppType { - case editor, browser, git_gui, terminal, user_supplied + case editor, ide, browser, git_gui, terminal, user_supplied } // MARK: - Container @@ -29,24 +29,50 @@ class Application { /// Application type. Depending on the type, a different action might occur. let type: AppType + /// The full path to the application bundle (if found) + var path: String? + /// Initializer. Used to detect a specific app of a specific type. init(_ container: Container, _ name: String, _ type: AppType) { self.container = container self.name = name self.type = type + self.path = determinePath() } /** - Attempt to open a specific directory in the app of choice. + Attempt to open a specific string (path or URL) in the app of choice. (This will open the app if it isn't open yet.) */ - @objc public func openDirectory(file: String) { - Task { await container.shell.quiet("/usr/bin/open -a \"\(name)\" \"\(file)\"") } + @objc public func open(arg: String) { + Task { await container.shell.quiet("/usr/bin/open -a \"\(name)\" \"\(arg)\"") } } - /** Checks if the app is installed. */ - func isInstalled() async -> Bool { + /** + Attempt to see if we can locate the app bundle in one of the two default locations: + - - First in `/Applications` (system-wide installed apps) + - - Second in `~/Applications` (user-specific installed apps) + If not in one of these default locations, the path will be `nil` and certain operations + will not be possible (i.e. determining icon via path to application). + */ + func determinePath() -> String? { + // Check global applications + if container.filesystem.directoryExists("/Applications/\(name).app") { + return "/Applications/\(name).app" + } + + // Check user applications + if container.filesystem.directoryExists("~/Applications/\(name).app") { + return "~/Applications/\(name).app".replacingTildeWithHomeDirectory + } + + return nil + } + + /** Checks if the app is installed and stores its path. */ + func isInstalled() async -> Bool { + // Then verify it's actually installed using the shell command let (process, output) = try! await container.shell.attach( "/usr/bin/open -Ra \"\(name)\"", didReceiveOutput: { _, _ in }, @@ -71,11 +97,30 @@ class Application { var detected: [Application] = [] let detectable = [ - Application(container, "PhpStorm", .editor), + // Browsers (for future Open In > Browser context menu) + Application(container, "Safari", .browser), + Application(container, "Google Chrome", .browser), + Application(container, "Microsoft Edge", .browser), + Application(container, "Firefox", .browser), + Application(container, "Brave", .browser), + Application(container, "Arc", .browser), + Application(container, "Zen", .browser), + + // Editors + Application(container, "PhpStorm", .ide), + Application(container, "WebStorm", .ide), Application(container, "Visual Studio Code", .editor), + Application(container, "VSCodium", .editor), Application(container, "Sublime Text", .editor), + + // Git Application(container, "Sublime Merge", .git_gui), - Application(container, "iTerm", .terminal) + Application(container, "Tower", .git_gui), + Application(container, "SourceTree", .git_gui), + + // Terminals + Application(container, "iTerm", .terminal), + Application(container, "Ghostty", .terminal) ] for app in detectable where await app.isInstalled() { diff --git a/phpmon/Modules/Domain List/UI/DomainListVC+Actions.swift b/phpmon/Modules/Domain List/UI/DomainListVC+Actions.swift index 6d0f457c..62764655 100644 --- a/phpmon/Modules/Domain List/UI/DomainListVC+Actions.swift +++ b/phpmon/Modules/Domain List/UI/DomainListVC+Actions.swift @@ -38,9 +38,18 @@ extension DomainListVC { Task { await App.shared.container.shell.quiet("open -b com.apple.terminal '\(selectedSite!.absolutePath)'") } } - @objc func openWithEditor(sender: EditorMenuItem) { - guard let editor = sender.editor else { return } - editor.openDirectory(file: selectedSite!.absolutePath) + @objc func openWithApp(sender: ApplicationMenuItem) { + guard let site = selectedSite else { return } + guard let app = sender.app else { return } + + if app.type == .browser { + guard let url = site.getListableUrl() else { return } + // Open the URL for the domain + app.open(arg: url.absoluteString) + } else { + // Open the directory for the domain + app.open(arg: site.absolutePath) + } } // MARK: - UI interaction diff --git a/phpmon/Modules/Domain List/UI/DomainListVC+ContextMenu.swift b/phpmon/Modules/Domain List/UI/DomainListVC+ContextMenu.swift index 4457740f..392d7652 100644 --- a/phpmon/Modules/Domain List/UI/DomainListVC+ContextMenu.swift +++ b/phpmon/Modules/Domain List/UI/DomainListVC+ContextMenu.swift @@ -98,15 +98,22 @@ extension DomainListVC { menu.addItem(NSMenuItem.separator()) menu.addItem(HeaderView.asMenuItem(text: "domain_list.detected_apps".localized)) - for editor in applications { - let editorMenuItem = EditorMenuItem( - title: "domain_list.open_in".localized(editor.name), - action: #selector(self.openWithEditor(sender:)), + for app in applications where app.type != .browser { + let menuItem = ApplicationMenuItem( + title: "domain_list.open_in".localized(app.name), + action: #selector(self.openWithApp(sender:)), keyEquivalent: "", systemImage: "arrow.up.right" ) - editorMenuItem.editor = editor - menu.addItem(editorMenuItem) + + if let applicationPath = app.path { + let icon = NSWorkspace.shared.icon(forFile: applicationPath) + icon.size = NSSize(width: 16, height: 16) + menuItem.image = icon + } + + menuItem.app = app + menu.addItem(menuItem) } } }