1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-03-28 06:50:08 +01:00

♻️ Refactor "Open With" with third-party apps

- 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.
This commit is contained in:
2025-11-26 14:06:47 +01:00
parent 7af61a80f5
commit 05987da275
4 changed files with 80 additions and 19 deletions

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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

View File

@@ -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)
}
}
}