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? var phpExtension: PhpExtension?
} }
class EditorMenuItem: NSMenuItem { class ApplicationMenuItem: NSMenuItem {
var editor: Application? var app: Application?
} }
class PresetMenuItem: NSMenuItem { class PresetMenuItem: NSMenuItem {

View File

@@ -14,7 +14,7 @@ import Foundation
class Application { class Application {
enum AppType { enum AppType {
case editor, browser, git_gui, terminal, user_supplied case editor, ide, browser, git_gui, terminal, user_supplied
} }
// MARK: - Container // MARK: - Container
@@ -29,24 +29,50 @@ class Application {
/// Application type. Depending on the type, a different action might occur. /// Application type. Depending on the type, a different action might occur.
let type: AppType 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. /// Initializer. Used to detect a specific app of a specific type.
init(_ container: Container, _ name: String, _ type: AppType) { init(_ container: Container, _ name: String, _ type: AppType) {
self.container = container self.container = container
self.name = name self.name = name
self.type = type 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.) (This will open the app if it isn't open yet.)
*/ */
@objc public func openDirectory(file: String) { @objc public func open(arg: String) {
Task { await container.shell.quiet("/usr/bin/open -a \"\(name)\" \"\(file)\"") } 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( let (process, output) = try! await container.shell.attach(
"/usr/bin/open -Ra \"\(name)\"", "/usr/bin/open -Ra \"\(name)\"",
didReceiveOutput: { _, _ in }, didReceiveOutput: { _, _ in },
@@ -71,11 +97,30 @@ class Application {
var detected: [Application] = [] var detected: [Application] = []
let detectable = [ 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, "Visual Studio Code", .editor),
Application(container, "VSCodium", .editor),
Application(container, "Sublime Text", .editor), Application(container, "Sublime Text", .editor),
// Git
Application(container, "Sublime Merge", .git_gui), 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() { 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)'") } Task { await App.shared.container.shell.quiet("open -b com.apple.terminal '\(selectedSite!.absolutePath)'") }
} }
@objc func openWithEditor(sender: EditorMenuItem) { @objc func openWithApp(sender: ApplicationMenuItem) {
guard let editor = sender.editor else { return } guard let site = selectedSite else { return }
editor.openDirectory(file: selectedSite!.absolutePath) 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 // MARK: - UI interaction

View File

@@ -98,15 +98,22 @@ extension DomainListVC {
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(HeaderView.asMenuItem(text: "domain_list.detected_apps".localized)) menu.addItem(HeaderView.asMenuItem(text: "domain_list.detected_apps".localized))
for editor in applications { for app in applications where app.type != .browser {
let editorMenuItem = EditorMenuItem( let menuItem = ApplicationMenuItem(
title: "domain_list.open_in".localized(editor.name), title: "domain_list.open_in".localized(app.name),
action: #selector(self.openWithEditor(sender:)), action: #selector(self.openWithApp(sender:)),
keyEquivalent: "", keyEquivalent: "",
systemImage: "arrow.up.right" 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)
} }
} }
} }