From 54d1ef10c5f1f0529320f7c19f8f015570e83946 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 6 Feb 2026 19:10:38 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rework=20interapp=20handli?= =?UTF-8?q?ng,=20ensure=20preferences=20UI=20observes=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Common/Core/Events.swift | 1 + phpmon/Domain/App/App.swift | 3 ++ phpmon/Domain/App/AppDelegate+InterApp.swift | 31 ++++++++++++++++--- phpmon/Domain/App/Startup+Launch.swift | 6 ++++ .../Integrations/Valet/Valet+Alerts.swift | 2 +- phpmon/Domain/Preferences/Preferences.swift | 6 +++- .../Views/CheckboxPreferenceView.swift | 9 ++++++ .../Views/SelectPreferenceView.swift | 14 +++++++++ phpmon/en.lproj/Localizable.strings | 8 ++--- 9 files changed, 70 insertions(+), 10 deletions(-) diff --git a/phpmon/Common/Core/Events.swift b/phpmon/Common/Core/Events.swift index efa92c0b..12089c82 100644 --- a/phpmon/Common/Core/Events.swift +++ b/phpmon/Common/Core/Events.swift @@ -11,5 +11,6 @@ import Foundation class Events { static let ServicesUpdated = Notification.Name("ServicesUpdated") + static let PreferencesUpdated = Notification.Name("PreferencesUpdated") } diff --git a/phpmon/Domain/App/App.swift b/phpmon/Domain/App/App.swift index e387b21d..ae63bd9b 100644 --- a/phpmon/Domain/App/App.swift +++ b/phpmon/Domain/App/App.swift @@ -105,6 +105,9 @@ class App { /** The window controller of the PHP extension manager window. */ var phpExtensionManagerWindowController: PhpExtensionManagerWindowController? + /** URL that was received before the app finished booting. Will be processed once the startup procedure completes. */ + var deferredURL: URL? + /** List of detected (installed) applications that PHP Monitor can work with. */ var detectedApplications: [Application] = [] diff --git a/phpmon/Domain/App/AppDelegate+InterApp.swift b/phpmon/Domain/App/AppDelegate+InterApp.swift index 9afc6020..f2573432 100644 --- a/phpmon/Domain/App/AppDelegate+InterApp.swift +++ b/phpmon/Domain/App/AppDelegate+InterApp.swift @@ -17,17 +17,40 @@ extension AppDelegate { application URL. You can use the `phpmon://` protocol to communicate with the app. At this time you can trigger the site list using Alfred (or some other application) - by opening the following URL: `phpmon://list`. + by opening the following URL: `phpmon://list`. Various other commands are also made + available via the `InterApp` class, which is where all commands and their different + interactions are declared. - Please note that PHP Monitor needs to be running in the background for this to work. + Please note that PHP Monitor needs to be running in the background for this to work, + or the app will launch and the command will be deferred until the app is ready. */ @MainActor func application(_ application: NSApplication, open urls: [URL]) { + guard Startup.hasFinishedBooting else { + return deferURLs(urls) + } + + handleURLs(urls) + } + + @MainActor func deferURLs(_ urls: [URL]) { + Log.info("App has not finished booting, deferring phpmon:// command...") + App.shared.deferredURL = urls.first + } + + @MainActor func handleURLs(_ urls: [URL]) { if !Preferences.isEnabled(.allowProtocolForIntegrations) { - if UserDefaults.standard.bool(forKey: PersistentAppState.didPromptForIntegrations.rawValue) { + + // First, we'll check if we already prompted for integrations. + // If we have, we will not respond to the commands at all. + if UserDefaults.standard.bool( + forKey: PersistentAppState.didPromptForIntegrations.rawValue + ) { Log.info("Acting on commands via phpmon:// has been disabled.") return } + // However, if that isn't the case, we will prompt the user about integrations, + // which will give the user a chance to approve the command regardless. Log.info("Acting on commands via phpmon:// has been disabled. Prompting user...") if !promptToEnableIntegrations() { return @@ -58,7 +81,7 @@ extension AppDelegate { return false } - Preferences.update(.allowProtocolForIntegrations, value: true) + Preferences.update(.allowProtocolForIntegrations, value: true, notify: true) return true } diff --git a/phpmon/Domain/App/Startup+Launch.swift b/phpmon/Domain/App/Startup+Launch.swift index 7bfa4397..cae7410b 100644 --- a/phpmon/Domain/App/Startup+Launch.swift +++ b/phpmon/Domain/App/Startup+Launch.swift @@ -124,6 +124,12 @@ extension Startup { Startup.hasFinishedBooting = true Log.info("PHP Monitor is ready to serve!") + // Process the last URL that arrived during startup + if let url = App.shared.deferredURL { + AppDelegate.instance.handleURLs([url]) + App.shared.deferredURL = nil + } + // Enable the main menu item MainMenu.shared.statusItem.button?.isEnabled = true diff --git a/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift b/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift index 3880f04a..cb289e97 100644 --- a/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift +++ b/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift @@ -24,7 +24,7 @@ extension Valet { ) .withPrimary(text: "generic.ok".localized) .withTertiary(text: "alert.do_not_tell_again".localized, action: { alert in - Preferences.update(.warnAboutNonStandardTLD, value: false) + Preferences.update(.warnAboutNonStandardTLD, value: false, notify: true) alert.close(with: .alertThirdButtonReturn) }) .show(urgency: .urgentRequestAttention) diff --git a/phpmon/Domain/Preferences/Preferences.swift b/phpmon/Domain/Preferences/Preferences.swift index b89aa438..242c039a 100644 --- a/phpmon/Domain/Preferences/Preferences.swift +++ b/phpmon/Domain/Preferences/Preferences.swift @@ -168,7 +168,7 @@ class Preferences { }) } - static func update(_ preference: PreferenceName, value: Any?) { + static func update(_ preference: PreferenceName, value: Any?, notify: Bool = false) { if value == nil { UserDefaults.standard.removeObject(forKey: preference.rawValue) } else { @@ -178,5 +178,9 @@ class Preferences { // Update the preferences cache in memory! App.shared.container.preferences.cachedPreferences = Preferences.cache() + + if notify { + NotificationCenter.default.post(name: Events.PreferencesUpdated, object: nil) + } } } diff --git a/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift b/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift index cf89d37f..0d7743d3 100644 --- a/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift +++ b/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift @@ -65,6 +65,15 @@ class CheckboxPreferenceBehavior: CheckboxPreferenceViewBehavior { self.preference = preference self.button = button self.button.state = Preferences.isEnabled(self.preference) ? .on : .off + + NotificationCenter.default.addObserver( + self, selector: #selector(refreshState), + name: Events.PreferencesUpdated, object: nil + ) + } + + @objc func refreshState() { + self.button.state = Preferences.isEnabled(self.preference) ? .on : .off } public func toggled(checked: Bool) { diff --git a/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift b/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift index 80cb942b..e265a648 100644 --- a/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift +++ b/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift @@ -73,9 +73,23 @@ class SelectPreferenceView: NSView, XibLoadable { view.preference = preference view.action = action + NotificationCenter.default.addObserver( + view, selector: #selector(view.refreshState), + name: Events.PreferencesUpdated, object: nil + ) + return view } + @objc func refreshState() { + let value = Preferences.preferences[preference] as! String + self.options.enumerated().forEach { option in + if option.element.value == value { + self.popupButton.selectItem(at: option.offset) + } + } + } + @IBAction func valueChanged(_ sender: Any) { let index = self.popupButton.indexOfSelectedItem Preferences.update(self.preference, value: self.options[index].value) diff --git a/phpmon/en.lproj/Localizable.strings b/phpmon/en.lproj/Localizable.strings index def9ba04..bcb34b1f 100644 --- a/phpmon/en.lproj/Localizable.strings +++ b/phpmon/en.lproj/Localizable.strings @@ -954,8 +954,8 @@ PHP Monitor will tell Valet to unsecure and re-secure all expired domains for yo // THIRD-PARTY INTEGRATIONS -"alert.enable_integrations.title" = "An external application is trying to communicate with PHP Monitor"; -"alert.enable_integrations.subtitle" = "Do you want to allow third-party integrations (like Alfred or Raycast) to control PHP Monitor via the `phpmon://` protocol?\n\nThis notice appears because PHP Monitor just received an external command. If you triggered this intentionally, using a third-party app like Alfred or Raycast, it is safe to allow this.\n\nYou can change this setting later in Preferences."; -"alert.enable_integrations.desc" = "If you did not trigger this via Alfred or Raycast, there may be another application trying to control PHP Monitor. In that case, I recommend keeping this integration turned off, unless you are fine with another third party app controlling PHP Monitor for you, which could present a security risk."; -"alert.enable_integrations.ok" = "Allow"; +"alert.enable_integrations.title" = "An external application is trying to communicate with PHP Monitor. Do you want third-party applications to be able to communicate with PHP Monitor?"; +"alert.enable_integrations.subtitle" = "This notice appears because PHP Monitor just received an external command, and this feature is disabled by default. If you triggered this intentionally, using a third-party app like Alfred or Raycast, it is normally safe to allow this.\n\nYou can change this setting later in Preferences, you will be asked this question only once."; +"alert.enable_integrations.desc" = "If you did not trigger this via Alfred or Raycast, there may be another application trying to control PHP Monitor.\n\nIn such a case, I recommend keeping this integration turned off, unless you are fine with another third party app controlling PHP Monitor for you, which could present a potential security risk."; +"alert.enable_integrations.ok" = "Allow Integrations"; "alert.enable_integrations.cancel" = "Don't Allow";