diff --git a/.github/contributing.md b/.github/contributing.md index a1b1f4c9..09b0106f 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -1,22 +1,16 @@ # Contribution Guidelines -Thank you for your interest in contributing to PHP Monitor. +Thank you for your interest in contributing to PHP Monitor! -I consider this project a bit of a nice side-project to my daily gig, so it is very much a personal affair where I love to tinker around. +While the code of the latest PHP Monitor release is public, many things are constantly in flux that may not be pushed to this repository yet. -**While the code of the latest PHP Monitor release is public, many things are constantly in flux that may not be pushed to this repository yet.** +In particular, certain changes may only be available to [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen) via the [EAP repository](https://github.com/phpmon/early-access). -I don't mean to be rude, but I don't want other people involved with the project beyond simply contributing a few small things here and there, as has been the case in the past. +Please consider creating an issue before working on anything related to the project, so that I can confirm you are not just wasting your time. -The extra mental overhead of having additional contributors to report to, whose code will need to be reviewed... it's a lot and it makes working on PHP Monitor less enjoyable for me. +**Making any changes in a fork and opening a pull request WITHOUT properly documenting your changes and referencing an issue may require me to close your PR.** -Plus, at this point, the majority of PHP Monitor's main functionality is also done. - -As a result, I may refer you to this file at some point. Again, I don't wish to be rude, but this general rule stands: - -**Making any changes in a fork and opening a pull request without opening an issue first will most likely result in your PR being closed without mercy.** - -To repeat, I am **not opposed** to small contributions and fixes, if they are **meaningful or insightful**. +To repeat, I am not opposed to small contributions and fixes, if they are meaningful or insightful, but low effort changes are generally not accepted. To learn more, please check out the [pull request template](/.github/pull_request_template.md) which contains more information about my contribution requirements. (This will also show up when you open a new PR.) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3477d277..4128754a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,25 +2,26 @@ Hello there! Thank you for considering a pull request for PHP Monitor. Please read the text below first before you submit your PR. -## Do not PR unless... +## Keep this in mind! -In order to make development and maintenance of PHP Monitor easier, I ask that you _avoid_ making a pull request in the following situations: - -* No issue has been associated with the changes you‘d like to merge -* You have not announced you will be addressing a particular issue -* The PR is a low effort change: e.g. commits that only fix typos or phrasing may not be accepted - -(If you believe the phrasing of particular text in the app is unclear or incorrect, please open an issue first.) - -In short: It is usually best to *get in touch first* if you are making substantial changes. +- Some code changes available to the sponsor repository may not be pushed to the public repository yet, so it's common that the public repository is a little behind. +- Because of this, it is usually best to *get in touch first* if you are making substantial changes. +- Low effort changes may not be accepted. +- When in doubt, open an issue or discussion and ask me if it's worth doing something. ## About destination branches -Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Pull requests that target `main` will be closed without mercy.** +Please keep in mind that `main` is reserved for the current code state of the latest release and should generally *not* be the destination branch unless a new release is happening. -Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released. +**Pull requests that target `main` will usually be retargeted.** -There may be a newer branch available, which is an appropriate place for bigger changes, but please keep in mind that it is usually best to announce you‘ll be working on such a change before you spend the time, since as the lead contributor I might not even want said change in the app. Thank you. +Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released, although that branch may not be available or up-to-date at all times. + +There may be a newer branch available, which is an appropriate place for bigger changes, but please keep in mind that it is usually best to announce you‘ll be working on such a change before you spend the time, since as the lead contributor I might not even want said change in the app. + +Thank you. + +--- ## Your changes @@ -29,7 +30,7 @@ There may be a newer branch available, which is an appropriate place for bigger * Affected parts of the app: shared code / UI code / CLI (remove what does not apply) * Estimated impact on performance: none / low / high (remove what does not apply) * Made a new build with Xcode and tested this: yes / no (remove what does not apply) -* Tested on macOS version + architecture: (e.g. "Monterey on M1" or "Big Sur on Intel") +* Tested on macOS version + architecture: (e.g. "Tahoe on M4" or "Ventura on Intel") * References issue(s): (please reference the issue here, using # and the number of the issue) (please describe what you have changed here) \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index 719c6af2..dfc492ba 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -16,6 +16,12 @@ included: excluded: - phpmon/Vendor +type_body_length: + warning: 300 + +function_body_length: + warning: 100 + line_length: ignores_function_declarations: true ignores_comments: true diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 4109bcda..b330969a 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 0310B17A2EB8F3FF00A8B140 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 0310B1792EB8F3FF00A8B140 /* CrashReporter */; }; 0310B17C2EB8F40100A8B140 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 0310B17B2EB8F40100A8B140 /* CrashReporter */; }; 0310B17E2EB8F40400A8B140 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 0310B17D2EB8F40400A8B140 /* CrashReporter */; }; + 0317C17E2ED87CAB005479D2 /* NVAppUpdater in Frameworks */ = {isa = PBXBuildFile; productRef = 0317C17D2ED87CAB005479D2 /* NVAppUpdater */; }; + 0317C1812ED87CE1005479D2 /* NVAppUpdater in Frameworks */ = {isa = PBXBuildFile; productRef = 0317C1802ED87CE1005479D2 /* NVAppUpdater */; }; + 0317C1832ED87CEA005479D2 /* NVAppUpdater in Frameworks */ = {isa = PBXBuildFile; productRef = 0317C1822ED87CEA005479D2 /* NVAppUpdater */; }; 031E2B692B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; 031E2B6A2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; 031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; @@ -28,6 +31,10 @@ 0329A9A42E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */; }; 0329A9A52E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */; }; 0329A9A62E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */; }; + 032C7A022EE43B7600758D98 /* Suspendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C7A012EE43B7400758D98 /* Suspendable.swift */; }; + 032C7A032EE43B7600758D98 /* Suspendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C7A012EE43B7400758D98 /* Suspendable.swift */; }; + 032C7A042EE43B7600758D98 /* Suspendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C7A012EE43B7400758D98 /* Suspendable.swift */; }; + 032C7A052EE43B7600758D98 /* Suspendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C7A012EE43B7400758D98 /* Suspendable.swift */; }; 032DAC282E8BEB5B0018E01C /* RealWebApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealWebApi.swift */; }; 032DAC292E8BEB5B0018E01C /* RealWebApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealWebApi.swift */; }; 032DAC2A2E8BEB5B0018E01C /* RealWebApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealWebApi.swift */; }; @@ -63,6 +70,36 @@ 036C39122E5C8D42008DAEDF /* PackagistError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C390E2E5C8D3B008DAEDF /* PackagistError.swift */; }; 036C39142E5CB822008DAEDF /* TestBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C39132E5CB820008DAEDF /* TestBundle.swift */; }; 036C3A212E5CBBAA008DAEDF /* ValetConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */; }; + 0379C49F2ED71D050035D7EA /* Startup+Launch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */; }; + 0379C4A02ED71D050035D7EA /* Startup+Launch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */; }; + 0379C4A12ED71D050035D7EA /* Startup+Launch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */; }; + 0379C4A22ED71D050035D7EA /* Startup+Launch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */; }; + 0379C4A42ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; }; + 0379C4A52ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; }; + 0379C4A62ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; }; + 0379C4A72ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; }; + 037F44162EDB0AAA002EBF75 /* FSNotifierTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */; }; + 037F44182EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; }; + 037F44192EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; }; + 037F441A2EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; }; + 037F441B2EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; }; + 037F441D2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */; }; + 037F441E2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */; }; + 037F441F2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */; }; + 037F44202EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */; }; + 037F44222EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; }; + 037F44232EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; }; + 037F44242EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; }; + 037F44252EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; }; + 0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; + 0386B0B52ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; + 0386B0B62ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; + 0386B0B72ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; + 0386B0BC2ED36DF800CA6795 /* LockedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B82ED36DF800CA6795 /* LockedTests.swift */; }; + 038A2B7E2EDDB24C00173ACF /* App+UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038A2B7D2EDDB24400173ACF /* App+UUID.swift */; }; + 038A2B7F2EDDB24C00173ACF /* App+UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038A2B7D2EDDB24400173ACF /* App+UUID.swift */; }; + 038A2B802EDDB24C00173ACF /* App+UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038A2B7D2EDDB24400173ACF /* App+UUID.swift */; }; + 038A2B812EDDB24C00173ACF /* App+UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038A2B7D2EDDB24400173ACF /* App+UUID.swift */; }; 0392CDE62EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; }; 0392CDE72EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; }; 0392CDE82EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; }; @@ -71,10 +108,6 @@ 0392CDEC2EB25371009176DA /* SecurePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDEA2EB25371009176DA /* SecurePopoverView.swift */; }; 0392CDED2EB25371009176DA /* SecurePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDEA2EB25371009176DA /* SecurePopoverView.swift */; }; 0392CDEE2EB25371009176DA /* SecurePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDEA2EB25371009176DA /* SecurePopoverView.swift */; }; - 0396160D2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; }; - 0396160E2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; }; - 0396160F2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; }; - 039616102E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; }; 039C29182E8AA314007F5FAB /* TestableWebApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29172E8AA311007F5FAB /* TestableWebApi.swift */; }; 039C29192E8AA314007F5FAB /* TestableWebApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29172E8AA311007F5FAB /* TestableWebApi.swift */; }; 039C291A2E8AA314007F5FAB /* TestableWebApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29172E8AA311007F5FAB /* TestableWebApi.swift */; }; @@ -297,10 +330,6 @@ C4415E8E2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */; }; C4415E8F2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */; }; C4415E902B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */; }; - C441CC562AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */; }; - C441CC572AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */; }; - C441CC582AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */; }; - C441CC592AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */; }; C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */; }; C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */; }; C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4463FCB29804BCB007B93D5 /* RCFile.swift */; }; @@ -414,7 +443,6 @@ C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; }; C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; }; C46FA98C2822F08F00D78807 /* PhpConfigurationFileTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA98A2822F08F00D78807 /* PhpConfigurationFileTest.swift */; }; - C47014FC2C46D31B0069AAE7 /* NVAppUpdater in Frameworks */ = {isa = PBXBuildFile; productRef = C47014FB2C46D31B0069AAE7 /* NVAppUpdater */; }; C47014FF2C46D57C0069AAE7 /* NVAlert in Frameworks */ = {isa = PBXBuildFile; productRef = C47014FE2C46D57C0069AAE7 /* NVAlert */; }; C47015022C46D6910069AAE7 /* NVAlertExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47015012C46D6910069AAE7 /* NVAlertExtension.swift */; }; C47015032C46D7F00069AAE7 /* NVAlertExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47015012C46D6910069AAE7 /* NVAlertExtension.swift */; }; @@ -545,7 +573,6 @@ C471E84B28F9BB650021E251 /* ServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E76132854A65300B4FE0C /* ServicesManager.swift */; }; C471E84D28F9BB650021E251 /* Valet+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* Valet+Alerts.swift */; }; C471E84E28F9BB650021E251 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; }; - C471E84F28F9BB650021E251 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C471E85028F9BB650021E251 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; }; C471E85128F9BB650021E251 /* MainMenu+Switcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */; }; C471E85228F9BB650021E251 /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */; }; @@ -585,8 +612,6 @@ C471E87728F9BB650021E251 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CDA892288F1A71007CE25F /* Keys.swift */; }; C471E87828F9BB650021E251 /* TerminalProgressWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* TerminalProgressWindowController.swift */; }; C471E87928F9BB650021E251 /* ProgressVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44A874728905BB000498BC4 /* ProgressVC.swift */; }; - C471E87B28F9BB650021E251 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; - C471E87C28F9BB650021E251 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */; }; C471E87D28F9BB650021E251 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; }; C471E87E28F9BB650021E251 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; }; C471E87F28F9BB650021E251 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; }; @@ -632,7 +657,6 @@ C471E8AE28F9BB8F0021E251 /* ServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E76132854A65300B4FE0C /* ServicesManager.swift */; }; C471E8B028F9BB8F0021E251 /* Valet+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* Valet+Alerts.swift */; }; C471E8B128F9BB8F0021E251 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; }; - C471E8B228F9BB8F0021E251 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C471E8B328F9BB8F0021E251 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; }; C471E8B428F9BB8F0021E251 /* MainMenu+Switcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */; }; C471E8B528F9BB8F0021E251 /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */; }; @@ -672,8 +696,6 @@ C471E8DA28F9BB8F0021E251 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CDA892288F1A71007CE25F /* Keys.swift */; }; C471E8DB28F9BB8F0021E251 /* TerminalProgressWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* TerminalProgressWindowController.swift */; }; C471E8DC28F9BB8F0021E251 /* ProgressVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44A874728905BB000498BC4 /* ProgressVC.swift */; }; - C471E8DE28F9BB8F0021E251 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; - C471E8DF28F9BB8F0021E251 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */; }; C471E8E028F9BB8F0021E251 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; }; C471E8E128F9BB8F0021E251 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; }; C471E8E228F9BB8F0021E251 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; }; @@ -737,10 +759,6 @@ C48DDD0E29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; C48DDD0F29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; C48DDD1029C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; - C490E3B629BCA367006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; }; - C490E3B829BCA367006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; }; - C490E3B929BCA368006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; }; - C490E3BA29BCA368006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; }; C490E3BB29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; C490E3BC29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; C490E3BD29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; @@ -824,14 +842,9 @@ C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C1019A27C65C6F001FACC2 /* Process.swift */; }; C4C3643928AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3643828AE4FCE00C0770E /* StatusMenu+Items.swift */; }; C4C3643A28AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3643828AE4FCE00C0770E /* StatusMenu+Items.swift */; }; - C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; }; C4C8900328F0E28800CE5E97 /* FileSystemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */; }; C4C8900528F0E3D100CE5E97 /* RealFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */; }; - C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; - C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; - C4C8E81B276F54E5003AC782 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */; }; - C4C8E81C276F54E5003AC782 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */; }; C4CB250529B28BB800CA4492 /* MainMenuTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB250429B28BB800CA4492 /* MainMenuTest.swift */; }; C4CB6E65292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; C4CB6E66292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; @@ -942,7 +955,6 @@ C4F30B08278E195800755FCE /* brew-services.json in Resources */ = {isa = PBXBuildFile; fileRef = C4F30B06278E195800755FCE /* brew-services.json */; }; C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; }; C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; }; - C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; }; C4F361612836BFD9003598CC /* MainMenu+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F361602836BFD9003598CC /* MainMenu+Actions.swift */; }; C4F520672AF03791006787F2 /* ExtensionEnumeratorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F520662AF03791006787F2 /* ExtensionEnumeratorTest.swift */; }; @@ -1020,6 +1032,7 @@ 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateScheduler.swift; sourceTree = ""; }; 0329A9A02E92A2A800A62A12 /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; 0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WarningManager+Evaluations.swift"; sourceTree = ""; }; + 032C7A012EE43B7400758D98 /* Suspendable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suspendable.swift; sourceTree = ""; }; 032DAC272E8BEB590018E01C /* RealWebApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealWebApi.swift; sourceTree = ""; }; 032DAC2C2E8BEB690018E01C /* WebApiProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebApiProtocol.swift; sourceTree = ""; }; 0336CAAF2B0D0CDA009A1034 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -1037,9 +1050,17 @@ 036C39092E5C8CBD008DAEDF /* PackagistP2Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagistP2Response.swift; sourceTree = ""; }; 036C390E2E5C8D3B008DAEDF /* PackagistError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagistError.swift; sourceTree = ""; }; 036C39132E5CB820008DAEDF /* TestBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundle.swift; sourceTree = ""; }; + 0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Launch.swift"; sourceTree = ""; }; + 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+DetectApps.swift"; sourceTree = ""; }; + 037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSNotifierTest.swift; sourceTree = ""; }; + 037F44172EDB27B7002EBF75 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = ""; }; + 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigWatchManager.swift; sourceTree = ""; }; + 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewWatchManager.swift; sourceTree = ""; }; + 0386B0B32ED36C3D00CA6795 /* Locked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = ""; }; + 0386B0B82ED36DF800CA6795 /* LockedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedTests.swift; sourceTree = ""; }; + 038A2B7D2EDDB24400173ACF /* App+UUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+UUID.swift"; sourceTree = ""; }; 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidator.swift; sourceTree = ""; }; 0392CDEA2EB25371009176DA /* SecurePopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurePopoverView.swift; sourceTree = ""; }; - 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggableEvent.swift; sourceTree = ""; }; 039C29172E8AA311007F5FAB /* TestableWebApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableWebApi.swift; sourceTree = ""; }; 039C291C2E8AA399007F5FAB /* TestableWebApiTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableWebApiTest.swift; sourceTree = ""; }; 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetUpgrader.swift; sourceTree = ""; }; @@ -1153,7 +1174,6 @@ C44067F827E2585E0045BD4E /* DomainListTypeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTypeCell.swift; sourceTree = ""; }; C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTLSCell.swift; sourceTree = ""; }; C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewFormulaeObservable.swift; sourceTree = ""; }; - C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigFSNotifier.swift; sourceTree = ""; }; C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHelper.swift; sourceTree = ""; }; C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionPopoverView.swift; sourceTree = ""; }; C4463FCB29804BCB007B93D5 /* RCFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RCFile.swift; sourceTree = ""; }; @@ -1224,7 +1244,6 @@ C49DA9BC2D67AC49006F9CF4 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; C49DA9BD2D67B298006F9CF4 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; C49EAA5129B12A5A00AB28FC /* Measurements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurements.swift; sourceTree = ""; }; - C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+BrewWatch.swift"; sourceTree = ""; }; C4A81CA328C67101008DD9D1 /* PMTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMTableView.swift; sourceTree = ""; }; C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListKindCell.swift; sourceTree = ""; }; C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = ""; }; @@ -1257,12 +1276,9 @@ C4C0E8E627F88B41002D32A9 /* DomainScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainScanner.swift; sourceTree = ""; }; C4C1019A27C65C6F001FACC2 /* Process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Process.swift; sourceTree = ""; }; C4C3643828AE4FCE00C0770E /* StatusMenu+Items.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Items.swift"; sourceTree = ""; }; - C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Startup.swift"; sourceTree = ""; }; C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = ""; }; C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemProtocol.swift; sourceTree = ""; }; C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealFileSystem.swift; sourceTree = ""; }; - C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = ""; }; - C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigWatchManager.swift; sourceTree = ""; }; C4CB250429B28BB800CA4492 /* MainMenuTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuTest.swift; sourceTree = ""; }; C4CB6E64292C362C002E9027 /* Homebrew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Homebrew.swift; sourceTree = ""; }; C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = ""; }; @@ -1333,7 +1349,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C47014FC2C46D31B0069AAE7 /* NVAppUpdater in Frameworks */, + 0317C1832ED87CEA005479D2 /* NVAppUpdater in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1342,6 +1358,8 @@ buildActionMask = 2147483647; files = ( C47014FF2C46D57C0069AAE7 /* NVAlert in Frameworks */, + 0317C17E2ED87CAB005479D2 /* NVAppUpdater in Frameworks */, + 0317C1812ED87CE1005479D2 /* NVAppUpdater in Frameworks */, 03D8462B2EB6418F006EFE3C /* CrashReporter in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1441,12 +1459,20 @@ path = Parsers; sourceTree = ""; }; - 0396160B2E74A617002DD7F6 /* Analytics */ = { + 037F44142EDB0A9F002EBF75 /* Watchers */ = { isa = PBXGroup; children = ( - 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */, + 037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */, ); - path = Analytics; + path = Watchers; + sourceTree = ""; + }; + 0386B0BD2ED36E2500CA6795 /* Helpers */ = { + isa = PBXGroup; + children = ( + 0386B0B82ED36DF800CA6795 /* LockedTests.swift */, + ); + path = Helpers; sourceTree = ""; }; 039C29112E8AA159007F5FAB /* Http */ = { @@ -1459,13 +1485,6 @@ path = Http; sourceTree = ""; }; - 039C291E2E8AA39B007F5FAB /* Api */ = { - isa = PBXGroup; - children = ( - ); - path = Api; - sourceTree = ""; - }; 03ACC6432ECCAAF70070D4CD /* WebApi */ = { isa = PBXGroup; children = ( @@ -1527,6 +1546,7 @@ 5489625628312F95004F647A /* Protocols */ = { isa = PBXGroup; children = ( + 032C7A012EE43B7400758D98 /* Suspendable.swift */, 5489625728312FAD004F647A /* CreatedFromFile.swift */, ); path = Protocols; @@ -2083,7 +2103,6 @@ isa = PBXGroup; children = ( C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */, - C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */, C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */, C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */, C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */, @@ -2099,6 +2118,7 @@ C4811D2822D70D9C00B5F6B3 /* Helpers */ = { isa = PBXGroup; children = ( + 0386B0B32ED36C3D00CA6795 /* Locked.swift */, C476FF9722B0DD830098105B /* Alert.swift */, 54B48B5E275F66AE006D90C5 /* Application.swift */, C474B00524C0E98C00066A22 /* LocalNotification.swift */, @@ -2139,7 +2159,6 @@ C4AF9F6B275445D300D44ED0 /* Integrations */ = { isa = PBXGroup; children = ( - 0396160B2E74A617002DD7F6 /* Analytics */, 036C38FB2E5C8827008DAEDF /* Packagist */, C4463FD029804C13007B93D5 /* Common */, C4C0E8DA27F887CC002D32A9 /* Nginx */, @@ -2179,10 +2198,13 @@ C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */, 03D846312EB64E35006EFE3C /* CrashReporter.swift */, C4811D2322D70A4700B5F6B3 /* App.swift */, + 038A2B7D2EDDB24400173ACF /* App+UUID.swift */, + 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */, C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */, C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */, C4EED88827A48778006D7272 /* InterAppHandler.swift */, C4D8016522B1584700C6DA1B /* Startup.swift */, + 0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */, 03BFF5262E312C39007F96FA /* Startup+Timers.swift */, C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */, C40FE736282ABA4F00A302C2 /* AppVersion.swift */, @@ -2302,11 +2324,10 @@ C4C8E81D276F5686003AC782 /* Watcher */ = { isa = PBXGroup; children = ( - C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */, - C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */, C41ADCE72970CCC700120423 /* FSNotifier.swift */, - C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */, - C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */, + 037F44172EDB27B7002EBF75 /* Debouncer.swift */, + 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */, + 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */, ); path = Watcher; sourceTree = ""; @@ -2410,12 +2431,13 @@ isa = PBXGroup; children = ( C40C7F1C27720E1400DDDCDC /* Test Files */, - 039C291E2E8AA39B007F5FAB /* Api */, C4C1019927C65A4D001FACC2 /* Commands */, 036C39062E5C8890008DAEDF /* Integration */, 036C3A232E5CBC57008DAEDF /* Parsers */, 03D53E902E8AE089001B1671 /* Testables */, 036575C62EA12E2200BA41BF /* Versions */, + 0386B0BD2ED36E2500CA6795 /* Helpers */, + 037F44142EDB0A9F002EBF75 /* Watchers */, ); path = unit; sourceTree = ""; @@ -2474,7 +2496,7 @@ ); name = "PHP Monitor Self-Updater"; packageProductDependencies = ( - C47014FB2C46D31B0069AAE7 /* NVAppUpdater */, + 0317C1822ED87CEA005479D2 /* NVAppUpdater */, ); productName = "PHP Monitor Updater"; productReference = C406A5F0298AD2CE00B5B85A /* PHP Monitor Self-Updater.app */; @@ -2498,6 +2520,8 @@ packageProductDependencies = ( C47014FE2C46D57C0069AAE7 /* NVAlert */, 03D8462A2EB6418F006EFE3C /* CrashReporter */, + 0317C17D2ED87CAB005479D2 /* NVAppUpdater */, + 0317C1802ED87CE1005479D2 /* NVAppUpdater */, ); productName = phpmon; productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */; @@ -2622,9 +2646,9 @@ ); mainGroup = C41C1B2A22B0097F00E7CF16; packageReferences = ( - C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */, C47014FD2C46D57C0069AAE7 /* XCRemoteSwiftPackageReference "NVAlert" */, 03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */, + 0317C17F2ED87CE1005479D2 /* XCRemoteSwiftPackageReference "NVAppUpdater" */, ); productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */; projectDirPath = ""; @@ -2799,7 +2823,7 @@ 0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */, C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */, C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */, - C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, + 037F441F2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */, C43B8FD52BA9BAD3000C02BE /* UnavailableContentView.swift in Sources */, 032DAC2A2E8BEB5B0018E01C /* RealWebApi.swift in Sources */, 54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, @@ -2812,12 +2836,13 @@ C4D5576429C77CC5001A44CD /* PhpVersionManagerWindowController.swift in Sources */, C4E49DED28F764A00026AC4E /* TestableCommand.swift in Sources */, C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */, + 037F44222EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */, C40C7F2827721FF600DDDCDC /* Valet+Alerts.swift in Sources */, C463E380284930EE00422731 /* PresetHelper.swift in Sources */, C41C02A927E61A65009F26CB /* FakeValetSite.swift in Sources */, C4E2E85C28FC282B003B070C /* TestableConfiguration.swift in Sources */, - 039616102E74A61E002DD7F6 /* LoggableEvent.swift in Sources */, C4C0E8DF27F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */, + 037F44192EDB27BA002EBF75 /* Debouncer.swift in Sources */, C44B3A4628E5C70100718CB1 /* TimeIntervalExtension.swift in Sources */, 03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */, C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */, @@ -2841,6 +2866,7 @@ C40C5C9C2846A40600E28255 /* Preset.swift in Sources */, C4B79EBC29CA38DB00A483EE /* BrewCommand.swift in Sources */, C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */, + 0379C4A42ED720220035D7EA /* App+DetectApps.swift in Sources */, C4B6091A2853AAD300C95265 /* SectionHeaderView.swift in Sources */, C436B39D29F3C42500B6A64E /* PreferencesTabs.swift in Sources */, C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */, @@ -2857,6 +2883,7 @@ C4F361612836BFD9003598CC /* MainMenu+Actions.swift in Sources */, C46EBC4A28DB966A007ACC74 /* TestableShell.swift in Sources */, C44C198D276E3A1C0072762D /* TerminalProgressWindowController.swift in Sources */, + 0379C49F2ED71D050035D7EA /* Startup+Launch.swift in Sources */, 54D9E0B827E4F51E003B9AD9 /* KeyCombo.swift in Sources */, C4C0E8E727F88B41002D32A9 /* DomainScanner.swift in Sources */, C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */, @@ -2889,6 +2916,7 @@ C41CA5ED2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */, 03B675EA2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */, C412E5FC25700D5300A1FB67 /* HomebrewDecodable.swift in Sources */, + 032C7A042EE43B7600758D98 /* Suspendable.swift in Sources */, 03BFF52E2E313244007F96FA /* StatusMenu+Driver.swift in Sources */, C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */, C45E76142854A65300B4FE0C /* ServicesManager.swift in Sources */, @@ -2897,9 +2925,7 @@ 03DAD3A72EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */, C46EBC4728DB9644007ACC74 /* RealShell.swift in Sources */, C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */, - C441CC562AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */, C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */, - C4C8E81B276F54E5003AC782 /* ConfigWatchManager.swift in Sources */, C417DC74277614690015E6EE /* Helpers.swift in Sources */, C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */, C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */, @@ -2907,7 +2933,6 @@ C42759672627662800093CAE /* NSMenuExtension.swift in Sources */, C422DDAA28A2C49900CEAC97 /* PhpDoctorView.swift in Sources */, C469E6FE294CF7B200A82AB2 /* FakeValetProxy.swift in Sources */, - C490E3B629BCA367006D2DE6 /* App+BrewWatch.swift in Sources */, C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */, C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */, C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */, @@ -2932,6 +2957,7 @@ C4D3660B29113F20006BD146 /* System.swift in Sources */, C4D36601291132B7006BD146 /* ValetScanners.swift in Sources */, 03ACC6472ECCBA130070D4CD /* CaskFile+API.swift in Sources */, + 0386B0B52ED36C3D00CA6795 /* Locked.swift in Sources */, C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */, C40C7F1E2772136000DDDCDC /* PhpEnvironments.swift in Sources */, C4B79EB629CA387F00A483EE /* BrewPhpFormulaeHandler.swift in Sources */, @@ -2959,7 +2985,6 @@ C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */, C44067F927E2585E0045BD4E /* DomainListTypeCell.swift in Sources */, 54D9E0BA27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */, - C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */, C4821C5A2C2DEDE200357A68 /* AppMenu.swift in Sources */, C42106662AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */, C4B79ECB29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */, @@ -2967,6 +2992,7 @@ 03D846282EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */, 03CC1FE62E3D22120050FC18 /* InstallHomebrew.swift in Sources */, C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */, + 038A2B7E2EDDB24C00173ACF /* App+UUID.swift in Sources */, C43BCD4429FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */, C4E2E84A28FC1E70003B070C /* DataExtension.swift in Sources */, C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */, @@ -3029,6 +3055,7 @@ 035983A22E97FA9100218DC7 /* Container.swift in Sources */, C471E84528F9BB650021E251 /* App+GlobalHotkey.swift in Sources */, C4513F922B13E2FB001AD760 /* PhpExtensionManagerView.swift in Sources */, + 0379C4A22ED71D050035D7EA /* Startup+Launch.swift in Sources */, C471E84628F9BB650021E251 /* InterAppHandler.swift in Sources */, 032DAC2B2E8BEB5B0018E01C /* RealWebApi.swift in Sources */, C471E84728F9BB650021E251 /* Startup.swift in Sources */, @@ -3039,7 +3066,6 @@ C471E84D28F9BB650021E251 /* Valet+Alerts.swift in Sources */, C471E84E28F9BB650021E251 /* MainMenu.swift in Sources */, C40934A4298EEB2C00D25014 /* CaskFile.swift in Sources */, - C471E84F28F9BB650021E251 /* MainMenu+Startup.swift in Sources */, C471E85028F9BB650021E251 /* MainMenu+Async.swift in Sources */, C471E85128F9BB650021E251 /* MainMenu+Switcher.swift in Sources */, C471E85228F9BB650021E251 /* MainMenu+FixMyValet.swift in Sources */, @@ -3055,6 +3081,7 @@ C4611E5B2AEAD2E30010BE24 /* ConfigManagerWindowController.swift in Sources */, C471E85A28F9BB650021E251 /* DomainListTypeCell.swift in Sources */, C471E85B28F9BB650021E251 /* DomainListKindCell.swift in Sources */, + 032C7A052EE43B7600758D98 /* Suspendable.swift in Sources */, C4611E5E2AEAD2FB0010BE24 /* ConfigManagerView.swift in Sources */, 031F24822EA1071A00CFB8D9 /* Container+Fake.swift in Sources */, C4BF56AD2949381100379603 /* FakeValetInteractor.swift in Sources */, @@ -3072,8 +3099,10 @@ C471E86328F9BB650021E251 /* PMTableView.swift in Sources */, C471E86428F9BB650021E251 /* Warning.swift in Sources */, 03C29A772EC88E3100FBA25E /* ValetServicesDataManager.swift in Sources */, + 037F441B2EDB27BA002EBF75 /* Debouncer.swift in Sources */, C40175BA2903108900763A68 /* ValetInteractor.swift in Sources */, C43931C729C4BD610069165B /* PhpVersionManagerView.swift in Sources */, + 0379C4A52ED720220035D7EA /* App+DetectApps.swift in Sources */, 036C390A2E5C8CC5008DAEDF /* PackagistP2Response.swift in Sources */, C4463FCE29804BCB007B93D5 /* RCFile.swift in Sources */, C45B9150295608E300F4EC78 /* ValetServicesManager.swift in Sources */, @@ -3104,8 +3133,6 @@ C471E87728F9BB650021E251 /* Keys.swift in Sources */, C471E87828F9BB650021E251 /* TerminalProgressWindowController.swift in Sources */, C471E87928F9BB650021E251 /* ProgressVC.swift in Sources */, - C471E87B28F9BB650021E251 /* App+ConfigWatch.swift in Sources */, - C471E87C28F9BB650021E251 /* ConfigWatchManager.swift in Sources */, C471E87D28F9BB650021E251 /* Preset.swift in Sources */, C471E87E28F9BB650021E251 /* PresetHelper.swift in Sources */, C471E87F28F9BB650021E251 /* WarningView.swift in Sources */, @@ -3136,8 +3163,10 @@ C4415E8F2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */, C471E7D828F9BA8F0021E251 /* FileSystemProtocol.swift in Sources */, 03BFF52F2E313244007F96FA /* StatusMenu+Driver.swift in Sources */, + 037F44242EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */, C471E7F328F9BAC70021E251 /* PhpHelper.swift in Sources */, C46DC7A62C7B5BC900F19D17 /* Favorites.swift in Sources */, + 038A2B812EDDB24C00173ACF /* App+UUID.swift in Sources */, C471E7E728F9BAC20021E251 /* Constants.swift in Sources */, C471E81628F9BAE80021E251 /* DateExtension.swift in Sources */, 03CC1FF72E3D23130050FC18 /* ZshRunCommand.swift in Sources */, @@ -3157,8 +3186,6 @@ C471E7E828F9BAC20021E251 /* Actions.swift in Sources */, C40D72612A018AE30054A067 /* BrewFormula+UI.swift in Sources */, C471E82528F9BB2E0021E251 /* ComposerWindow.swift in Sources */, - 0396160D2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */, - C441CC582AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */, C471E80828F9BAD40021E251 /* PhpExtension.swift in Sources */, C471E7F928F9BACB0021E251 /* PhpSwitcher.swift in Sources */, 03ACC6482ECCBA130070D4CD /* CaskFile+API.swift in Sources */, @@ -3166,8 +3193,8 @@ 031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */, C471E82728F9BB310021E251 /* BrewDiagnostics.swift in Sources */, C471E7DB28F9BA8F0021E251 /* RealShell.swift in Sources */, - C490E3B929BCA368006D2DE6 /* App+BrewWatch.swift in Sources */, C471E7FF28F9BAD10021E251 /* Xdebug.swift in Sources */, + 037F441D2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */, C409349F298EE8E900D25014 /* AppUpdater.swift in Sources */, 03BFF5292E312C3D007F96FA /* Startup+Timers.swift in Sources */, C471E7F228F9BAC70021E251 /* PhpEnvironments.swift in Sources */, @@ -3205,6 +3232,7 @@ C40D725C2A018ACC0054A067 /* BusyStatus.swift in Sources */, 03DAD3A92EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */, C4821C5C2C2DEDE200357A68 /* AppMenu.swift in Sources */, + 0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */, 0392CDED2EB25371009176DA /* SecurePopoverView.swift in Sources */, C471E81328F9BAE80021E251 /* XibLoadable.swift in Sources */, C4D3661C291173EA006BD146 /* DictionaryExtension.swift in Sources */, @@ -3226,13 +3254,14 @@ 033D45A12B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */, C471E89328F9BB8F0021E251 /* Application.swift in Sources */, 036C39022E5C883B008DAEDF /* Packagist.swift in Sources */, + 0379C4A72ED720220035D7EA /* App+DetectApps.swift in Sources */, C471E89428F9BB8F0021E251 /* LocalNotification.swift in Sources */, - C441CC592AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */, C40934A5298EEB2C00D25014 /* CaskFile.swift in Sources */, C471E89528F9BB8F0021E251 /* MenuBarImageGenerator.swift in Sources */, C40D725D2A018ACC0054A067 /* BusyStatus.swift in Sources */, C471E89628F9BB8F0021E251 /* PMWindowController.swift in Sources */, C471E89728F9BB8F0021E251 /* VersionExtractor.swift in Sources */, + 0386B0B72ED36C3D00CA6795 /* Locked.swift in Sources */, C47DF1B2299D5A3B0007055D /* LoginItemManager.swift in Sources */, C4E2E86728FC2F1B003B070C /* XCPMApplication.swift in Sources */, C471E89828F9BB8F0021E251 /* ValetProxy.swift in Sources */, @@ -3259,16 +3288,17 @@ 03DAD3A82EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */, C471E8A828F9BB8F0021E251 /* App+GlobalHotkey.swift in Sources */, C471E8A928F9BB8F0021E251 /* InterAppHandler.swift in Sources */, + 0379C4A02ED71D050035D7EA /* Startup+Launch.swift in Sources */, C471E8AA28F9BB8F0021E251 /* Startup.swift in Sources */, C471E8AB28F9BB8F0021E251 /* EnvironmentCheck.swift in Sources */, C471E8AD28F9BB8F0021E251 /* AppVersion.swift in Sources */, C471E8AE28F9BB8F0021E251 /* ServicesManager.swift in Sources */, C471E8B028F9BB8F0021E251 /* Valet+Alerts.swift in Sources */, C471E8B128F9BB8F0021E251 /* MainMenu.swift in Sources */, - C471E8B228F9BB8F0021E251 /* MainMenu+Startup.swift in Sources */, C471E8B328F9BB8F0021E251 /* MainMenu+Async.swift in Sources */, C471E8B428F9BB8F0021E251 /* MainMenu+Switcher.swift in Sources */, C471E8B528F9BB8F0021E251 /* MainMenu+FixMyValet.swift in Sources */, + 038A2B7F2EDDB24C00173ACF /* App+UUID.swift in Sources */, C471E8B628F9BB8F0021E251 /* MainMenu+Actions.swift in Sources */, 03ACC64A2ECCBA130070D4CD /* CaskFile+API.swift in Sources */, C471E8B728F9BB8F0021E251 /* StatusMenu.swift in Sources */, @@ -3329,8 +3359,6 @@ C471E8DC28F9BB8F0021E251 /* ProgressVC.swift in Sources */, 03D846262EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */, C490E3BF29BCA376006D2DE6 /* Measurements.swift in Sources */, - C471E8DE28F9BB8F0021E251 /* App+ConfigWatch.swift in Sources */, - C471E8DF28F9BB8F0021E251 /* ConfigWatchManager.swift in Sources */, C4CB250529B28BB800CA4492 /* MainMenuTest.swift in Sources */, C40D72622A018AE30054A067 /* BrewFormula+UI.swift in Sources */, C4B79ECE29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */, @@ -3352,6 +3380,7 @@ C456A0CE2AA6166F0080144F /* BytePhpPreference.swift in Sources */, C4FD87A829AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */, C45B9151295608E300F4EC78 /* ValetServicesManager.swift in Sources */, + 037F44202EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */, C47015032C46D7F00069AAE7 /* NVAlertExtension.swift in Sources */, C471E8EC28F9BB8F0021E251 /* SwiftUIHelper.swift in Sources */, C471E8EE28F9BB8F0021E251 /* HotKey.swift in Sources */, @@ -3360,7 +3389,6 @@ C471E8F128F9BB8F0021E251 /* KeyCombo.swift in Sources */, C471E8F228F9BB8F0021E251 /* ModifierFlagsExtension.swift in Sources */, C471E7F028F9BAC30021E251 /* Paths.swift in Sources */, - 0396160F2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */, 03CC1FE72E3D22120050FC18 /* InstallHomebrew.swift in Sources */, C4CE7F9929683B43000102CF /* PhpVersionNumberCollection.swift in Sources */, C471E7FC28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */, @@ -3373,7 +3401,6 @@ C4611E5A2AEAD2E20010BE24 /* ConfigManagerWindowController.swift in Sources */, C471E80E28F9BAE80021E251 /* DateExtension.swift in Sources */, 036C390F2E5C8D42008DAEDF /* PackagistError.swift in Sources */, - C490E3BA29BCA368006D2DE6 /* App+BrewWatch.swift in Sources */, 03BFF5272E312C3D007F96FA /* Startup+Timers.swift in Sources */, C471E7D028F9BA630021E251 /* FileSystemProtocol.swift in Sources */, C471E81228F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */, @@ -3389,6 +3416,8 @@ C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */, C471E82328F9BB2E0021E251 /* ComposerJson.swift in Sources */, C471E82128F9BB2E0021E251 /* ProjectTypeDetection.swift in Sources */, + 037F44232EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */, + 037F441A2EDB27BA002EBF75 /* Debouncer.swift in Sources */, 032DAC282E8BEB5B0018E01C /* RealWebApi.swift in Sources */, C471E7EF28F9BAC30021E251 /* Actions.swift in Sources */, C471E82228F9BB2E0021E251 /* ComposerWindow.swift in Sources */, @@ -3433,6 +3462,7 @@ C4D3661D291173EA006BD146 /* DictionaryExtension.swift in Sources */, C471E80D28F9BAE80021E251 /* ArrayExtension.swift in Sources */, 035983A32E97FA9100218DC7 /* Container.swift in Sources */, + 032C7A032EE43B7600758D98 /* Suspendable.swift in Sources */, C471E7CD28F9BA600021E251 /* ShellProtocol.swift in Sources */, C471E7EC28F9BAC30021E251 /* Events.swift in Sources */, C471E7CE28F9BA600021E251 /* RealShell.swift in Sources */, @@ -3470,15 +3500,18 @@ 54B48B60275F66AE006D90C5 /* Application.swift in Sources */, 039C291A2E8AA314007F5FAB /* TestableWebApi.swift in Sources */, C4FE011228084FC200D1DE6D /* SelectionVC.swift in Sources */, + 038A2B802EDDB24C00173ACF /* App+UUID.swift in Sources */, 03C099472EA15C8E00B76D43 /* Container+Real.swift in Sources */, C4D3661B291173EA006BD146 /* DictionaryExtension.swift in Sources */, C45D654D29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */, C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */, C493084B279F331F009C240B /* AddSiteVC.swift in Sources */, C44A874928905BB000498BC4 /* ProgressVC.swift in Sources */, + 0379C4A62ED720220035D7EA /* App+DetectApps.swift in Sources */, C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */, C485707528BF454F00539B36 /* StatsView.swift in Sources */, C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */, + 0386B0B62ED36C3D00CA6795 /* Locked.swift in Sources */, 54D9E0BB27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */, 03D846332EB64E39006EFE3C /* CrashReporter.swift in Sources */, C485707328BF454300539B36 /* OnboardingView.swift in Sources */, @@ -3494,17 +3527,18 @@ C485707A28BF457800539B36 /* PhpDoctorView.swift in Sources */, C4C0E8E827F88B41002D32A9 /* DomainScanner.swift in Sources */, C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */, + 037F44162EDB0AAA002EBF75 /* FSNotifierTest.swift in Sources */, C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */, C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */, C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */, C4F780CA25D80B75000DBC97 /* HomebrewDecodable.swift in Sources */, - C4C8E81C276F54E5003AC782 /* ConfigWatchManager.swift in Sources */, C4F319C927B034A500AFF46F /* Stats.swift in Sources */, C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */, 54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */, C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */, 03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */, + 0379C4A12ED71D050035D7EA /* Startup+Launch.swift in Sources */, C451AFF72969E40F0078E617 /* HelpButton.swift in Sources */, C47DF1B0299D5A3B0007055D /* LoginItemManager.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, @@ -3516,7 +3550,6 @@ 0392CDE92EB23B8F009176DA /* CertificateValidator.swift in Sources */, C4821C5B2C2DEDE200357A68 /* AppMenu.swift in Sources */, C463E381284930EE00422731 /* PresetHelper.swift in Sources */, - C441CC572AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */, C4F520672AF03791006787F2 /* ExtensionEnumeratorTest.swift in Sources */, C46FA98C2822F08F00D78807 /* PhpConfigurationFileTest.swift in Sources */, C4D5576529C77CC5001A44CD /* PhpVersionManagerWindowController.swift in Sources */, @@ -3531,7 +3564,6 @@ C42106672AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */, C46DC7A52C7B5BC900F19D17 /* Favorites.swift in Sources */, 032DAC2F2E8BEB6B0018E01C /* WebApiProtocol.swift in Sources */, - C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */, 54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */, C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */, @@ -3546,12 +3578,14 @@ C4E2E85D28FC282B003B070C /* TestableConfiguration.swift in Sources */, C485706E28BF451C00539B36 /* OnboardingWindowController.swift in Sources */, C4BB393A2981AFC700F8E797 /* PhpVersionSource.swift in Sources */, + 037F44252EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */, C4CB6E66292C362C002E9027 /* Homebrew.swift in Sources */, C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C4C3643A28AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */, C42759682627662800093CAE /* NSMenuExtension.swift in Sources */, 03BFF52D2E313244007F96FA /* StatusMenu+Driver.swift in Sources */, C4AFC4B429C4F43300BF4E0D /* HomebrewUpgradableTest.swift in Sources */, + 037F441E2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */, C4E2E84828FC1D93003B070C /* TestableConfigurationTest.swift in Sources */, C4D936CB27E3EE4A00BD69FE /* DomainListCellProtocol.swift in Sources */, C4513F962B13E30C001AD760 /* BrewExtensionsObservable.swift in Sources */, @@ -3588,7 +3622,6 @@ 5489625928313231004F647A /* CreatedFromFile.swift in Sources */, C4513F932B13E2FB001AD760 /* PhpExtensionManagerView.swift in Sources */, 54D9E0B327E4F51E003B9AD9 /* HotKeysController.swift in Sources */, - 0396160E2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */, C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */, C4E2E86528FC2F1B003B070C /* XCPMApplication.swift in Sources */, C489E0BC2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */, @@ -3620,6 +3653,7 @@ C485707228BF453800539B36 /* SwiftUIHelper.swift in Sources */, 03C29A792EC88E3100FBA25E /* ValetServicesDataManager.swift in Sources */, C4EA3C482BA4F947007B0BA7 /* CustomButtonStyles.swift in Sources */, + 0386B0BC2ED36DF800CA6795 /* LockedTests.swift in Sources */, C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */, C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */, 036C39042E5C883B008DAEDF /* Packagist.swift in Sources */, @@ -3628,21 +3662,21 @@ C485707828BF456300539B36 /* Warning.swift in Sources */, 033D459F2B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */, C4513F8F2B13E2E5001AD760 /* PhpExtensionManagerWindowController.swift in Sources */, + 037F44182EDB27BA002EBF75 /* Debouncer.swift in Sources */, C415938027A1B54F00D2E1B7 /* ProjectTypeDetection.swift in Sources */, C40F505628ECA64E004AD45B /* TestableConfigurations.swift in Sources */, C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */, C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */, + 032C7A022EE43B7600758D98 /* Suspendable.swift in Sources */, C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */, C4BF56AC2949381100379603 /* FakeValetInteractor.swift in Sources */, C471E79428F9B23B0021E251 /* FileSystemProtocol.swift in Sources */, - C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */, C485707C28BF459500539B36 /* NoWarningsView.swift in Sources */, 03D846252EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */, 0329A9A42E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */, 033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */, C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */, C40B24F227A310770018C7D2 /* Events.swift in Sources */, - C490E3B829BCA367006D2DE6 /* App+BrewWatch.swift in Sources */, C44AD3F72912EF7100997FF4 /* RealFileSystemTest.swift in Sources */, C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */, C4C0E8E027F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */, @@ -3960,7 +3994,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1735; + CURRENT_PROJECT_VERSION = 1810; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; @@ -3979,7 +4013,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.11.1; + MARKETING_VERSION = 25.12; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4004,7 +4038,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1735; + CURRENT_PROJECT_VERSION = 1810; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; @@ -4023,7 +4057,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.11.1; + MARKETING_VERSION = 25.12; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4186,7 +4220,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1735; + CURRENT_PROJECT_VERSION = 1810; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; @@ -4205,7 +4239,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.11.1; + MARKETING_VERSION = 25.12; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME) EAP"; @@ -4379,7 +4413,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1735; + CURRENT_PROJECT_VERSION = 1810; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; @@ -4398,7 +4432,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.11.1; + MARKETING_VERSION = 25.12; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME) EAP"; @@ -4615,6 +4649,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 0317C17F2ED87CE1005479D2 /* XCRemoteSwiftPackageReference "NVAppUpdater" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/nicoverbruggen/NVAppUpdater"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; 03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/microsoft/plcrashreporter.git"; @@ -4623,20 +4665,12 @@ minimumVersion = 1.12.0; }; }; - C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/nicoverbruggen/NVAppUpdater"; - requirement = { - branch = main; - kind = branch; - }; - }; C47014FD2C46D57C0069AAE7 /* XCRemoteSwiftPackageReference "NVAlert" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nicoverbruggen/NVAlert"; requirement = { - kind = exactVersion; - version = 1.1.0; + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -4657,16 +4691,25 @@ package = 03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */; productName = CrashReporter; }; + 0317C17D2ED87CAB005479D2 /* NVAppUpdater */ = { + isa = XCSwiftPackageProductDependency; + productName = NVAppUpdater; + }; + 0317C1802ED87CE1005479D2 /* NVAppUpdater */ = { + isa = XCSwiftPackageProductDependency; + package = 0317C17F2ED87CE1005479D2 /* XCRemoteSwiftPackageReference "NVAppUpdater" */; + productName = NVAppUpdater; + }; + 0317C1822ED87CEA005479D2 /* NVAppUpdater */ = { + isa = XCSwiftPackageProductDependency; + package = 0317C17F2ED87CE1005479D2 /* XCRemoteSwiftPackageReference "NVAppUpdater" */; + productName = NVAppUpdater; + }; 03D8462A2EB6418F006EFE3C /* CrashReporter */ = { isa = XCSwiftPackageProductDependency; package = 03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */; productName = CrashReporter; }; - C47014FB2C46D31B0069AAE7 /* NVAppUpdater */ = { - isa = XCSwiftPackageProductDependency; - package = C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */; - productName = NVAppUpdater; - }; C47014FE2C46D57C0069AAE7 /* NVAlert */ = { isa = XCSwiftPackageProductDependency; package = C47014FD2C46D57C0069AAE7 /* XCRemoteSwiftPackageReference "NVAlert" */; diff --git a/README.md b/README.md index a04d57bf..da90b88d 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,6 @@ PHP Monitor is a universal application that runs natively on Apple Silicon **and * Homebrew `php` formula is installed * Optional but recommended: Laravel Valet -_Starting with PHP Monitor 6.0, you do not need to have Laravel Valet installed for PHP Monitor to work. To get access to all features of PHP Monitor however, installing Valet is **recommended**._ - For more information, please see [SECURITY.md](./SECURITY.md) to find out which version of the app is currently supported. ## 🚀 How to install @@ -43,7 +41,7 @@ valet install valet trust ``` -Currently, PHP Monitor is compatible with Laravel Valet v2, v3 and v4. Each of these versions of Valet support slightly different PHP versions, which is why legacy versions remain supported. Please note that some features are not available in older versions of Valet, like site isolation. +Currently, PHP Monitor is compatible with Laravel Valet v2 up to v4. Each of these versions of Valet support slightly different PHP versions, which is why legacy versions remain supported. Please note that some features are not available in older versions of Valet, like site isolation. #### Manual installation (recommended, first time only) @@ -51,15 +49,15 @@ Once that's done, you can [download the latest release](https://github.com/nicov #### Installation via Homebrew -*Prior to version 5.8, this was the recommended way of installing PHP Monitor.* - -If you prefer to install the app via Homebrew, you can also run the following: +Alternatively, if you prefer to install the app via Homebrew, you can also run the following: ```sh brew tap nicoverbruggen/homebrew-cask brew install --cask phpmon ``` +(You may want to disable the built-in update check if you install PHP Monitor this way.) + ## ⬆️ How to update The recommended method of updating the app to the latest version is to use **the built-in updater**. @@ -88,11 +86,9 @@ Initially, I had an Alfred workflow for this — but it has now been replaced wi _**Disclaimer**: The author is not affiliated with Laravel or the Laravel team, nor Beyond Code, who maintain Laravel Herd. PHP Monitor is an independent project._ -If you don't need to customize your local PHP setup and just want an easy and ready-to-go environment to start coding, [Laravel Herd](https://herd.laravel.com) is probably more than sufficient for many use cases. They also offer paid features that may be useful to you or your team. +If you don't need to customize your local PHP setup and just want an easy and ready-to-go environment to start coding, [Laravel Herd](https://herd.laravel.com) is a great alternative to PHP Monitor, since it does not rely on Homebrew. The app also offers paid features that may be useful to you or your team. -At this point, many people enjoy using Herd. However, Herd may not be for everyone, which is why other solutions to run PHP locally exist. If you need more customization and flexibility I encourage you to consider PHP Monitor in combination with Laravel Valet. - -If you want to get as close as you can to a real server environment your best bet is probably to use a Docker container. I _highly_ recommend that you try different setups, and use what you like best. +Herd may not be for everyone, which is why other solutions to run PHP locally exist. PHP Monitor preceded Herd and will remain supported. I _highly_ recommend that you try different tools, and use what you like best. ## 🤬 The app won't start?! @@ -674,3 +670,5 @@ I have done my best to annotate as much as humanly possible, and have avoided us I also have a few tests for key parts of the application that I found needed to be tested. In the future, I would like to add even more tests for some of the UI stuff, but for now the tests are more unit tests than feature tests. For more detailed information for developers, please see [the documentation file for developers](./DEVELOPER.md). + +You may also find the [privacy policy](https://phpmon.app/privacy-policy) useful to learn more about how PHP Monitor deals with your data. (Spoiler alert: PHP Monitor respects your privacy!) diff --git a/SECURITY.md b/SECURITY.md index a8c15309..56b0f158 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc | Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version | | ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- -| 25 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)
Sonoma (14.0+)
Sequoia (15.0+)
Tahoe (26.0+) | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.4 (w/ Valet 3.x)
PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended
2.16.2 minimum | +| 25 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)
Sonoma (14.0+)
Sequoia (15.0+)
Tahoe (26.0+) | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.4 (w/ Valet 3.x)
PHP 7.1-PHP 8.6 (w/ Valet 4.x)| 3.0 or higher recommended
2.16.2 minimum | ## Legacy versions diff --git a/phpmon-updater/main.swift b/phpmon-updater/main.swift index b60b77e6..6a815670 100644 --- a/phpmon-updater/main.swift +++ b/phpmon-updater/main.swift @@ -3,7 +3,7 @@ // PHP Monitor Self-Updater // // Created by Nico Verbruggen on 01/02/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Common/Command/CommandProtocol.swift b/phpmon/Common/Command/CommandProtocol.swift index 0fe4a799..f30795f6 100644 --- a/phpmon/Common/Command/CommandProtocol.swift +++ b/phpmon/Common/Command/CommandProtocol.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 12/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Command/RealCommand.swift b/phpmon/Common/Command/RealCommand.swift index 2c92cc36..7c21cf60 100644 --- a/phpmon/Common/Command/RealCommand.swift +++ b/phpmon/Common/Command/RealCommand.swift @@ -2,7 +2,7 @@ // Command.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa @@ -15,6 +15,8 @@ public class RealCommand: CommandProtocol { withStandardError: Bool ) -> String { let task = Process() + var output = "" + task.launchPath = path task.arguments = arguments @@ -26,10 +28,27 @@ public class RealCommand: CommandProtocol { } task.launch() + task.waitUntilExit() - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let output: String = String.init(data: data, encoding: String.Encoding.utf8)! + defer { + try? pipe.fileHandleForReading.close() + } + // Handle termination + if task.terminationReason == .uncaughtSignal { + Log.err("The command `\(path) w/ args: \(arguments)` likely crashed. Returning UNCAUGHT_SIGNAL.") + return "PHPMON_COMMAND_UNCAUGHT_SIGNAL" + } + + // Try reading from file handle and close it + if let data = try? pipe.fileHandleForReading.readToEnd(), + let string = String(data: data, encoding: .utf8) { + output = string + } else { + return "PHPMON_FILE_HANDLE_READ_FAILURE" + } + + // Trim newline output if necessary if trimNewlines { return output.components(separatedBy: .newlines) .filter({ !$0.isEmpty }) diff --git a/phpmon/Common/Command/TestableCommand.swift b/phpmon/Common/Command/TestableCommand.swift index a3679db5..ccf2615b 100644 --- a/phpmon/Common/Command/TestableCommand.swift +++ b/phpmon/Common/Command/TestableCommand.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 12/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Core/Actions.swift b/phpmon/Common/Core/Actions.swift index a7d05fae..f407b7b0 100644 --- a/phpmon/Common/Core/Actions.swift +++ b/phpmon/Common/Core/Actions.swift @@ -2,7 +2,7 @@ // Services.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Core/Constants.swift b/phpmon/Common/Core/Constants.swift index bfb97bbb..e1a2e27b 100644 --- a/phpmon/Common/Core/Constants.swift +++ b/phpmon/Common/Core/Constants.swift @@ -2,7 +2,7 @@ // Constants.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Common/Core/Events.swift b/phpmon/Common/Core/Events.swift index 972f0198..efa92c0b 100644 --- a/phpmon/Common/Core/Events.swift +++ b/phpmon/Common/Core/Events.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 23/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Core/Helpers.swift b/phpmon/Common/Core/Helpers.swift index 0e0909e9..db5fb60a 100644 --- a/phpmon/Common/Core/Helpers.swift +++ b/phpmon/Common/Core/Helpers.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 24/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // // MARK: Common Shell Commands diff --git a/phpmon/Common/Core/Homebrew.swift b/phpmon/Common/Core/Homebrew.swift index 5d009d19..360c44c1 100644 --- a/phpmon/Common/Core/Homebrew.swift +++ b/phpmon/Common/Core/Homebrew.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/11/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Core/Logger.swift b/phpmon/Common/Core/Logger.swift index f3ce40d0..47a7bc02 100644 --- a/phpmon/Common/Core/Logger.swift +++ b/phpmon/Common/Core/Logger.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Core/Paths.swift b/phpmon/Common/Core/Paths.swift index 47ee9ff0..c5ad8fc4 100644 --- a/phpmon/Common/Core/Paths.swift +++ b/phpmon/Common/Core/Paths.swift @@ -2,7 +2,7 @@ // Paths.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Core/Process.swift b/phpmon/Common/Core/Process.swift index 24b6ec42..aaa2859d 100644 --- a/phpmon/Common/Core/Process.swift +++ b/phpmon/Common/Core/Process.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 23/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Errors/AlertableError.swift b/phpmon/Common/Errors/AlertableError.swift index a76f730f..a64f992c 100644 --- a/phpmon/Common/Errors/AlertableError.swift +++ b/phpmon/Common/Errors/AlertableError.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 06/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Errors/Errors.swift b/phpmon/Common/Errors/Errors.swift index 74ce17b9..cf580498 100644 --- a/phpmon/Common/Errors/Errors.swift +++ b/phpmon/Common/Errors/Errors.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 08/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Extensions/ArrayExtension.swift b/phpmon/Common/Extensions/ArrayExtension.swift index cad6c964..2b68d50f 100644 --- a/phpmon/Common/Extensions/ArrayExtension.swift +++ b/phpmon/Common/Extensions/ArrayExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 11/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Extensions/DataExtension.swift b/phpmon/Common/Extensions/DataExtension.swift index b4c38e95..eacb75cb 100644 --- a/phpmon/Common/Extensions/DataExtension.swift +++ b/phpmon/Common/Extensions/DataExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Extensions/DateExtension.swift b/phpmon/Common/Extensions/DateExtension.swift index 1ddafab4..2c4dbf9c 100644 --- a/phpmon/Common/Extensions/DateExtension.swift +++ b/phpmon/Common/Extensions/DateExtension.swift @@ -2,7 +2,7 @@ // Date.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Common/Extensions/DictionaryExtension.swift b/phpmon/Common/Extensions/DictionaryExtension.swift index 65e357ad..3abbc4d2 100644 --- a/phpmon/Common/Extensions/DictionaryExtension.swift +++ b/phpmon/Common/Extensions/DictionaryExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 01/11/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Extensions/NSMenuExtension.swift b/phpmon/Common/Extensions/NSMenuExtension.swift index 33142ee5..ab1555db 100644 --- a/phpmon/Common/Extensions/NSMenuExtension.swift +++ b/phpmon/Common/Extensions/NSMenuExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 14/04/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Common/Extensions/NSMenuItemExtension.swift b/phpmon/Common/Extensions/NSMenuItemExtension.swift index 9c107b38..abc494ef 100644 --- a/phpmon/Common/Extensions/NSMenuItemExtension.swift +++ b/phpmon/Common/Extensions/NSMenuItemExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 18/08/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa @@ -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/Extensions/NSWindowExtension.swift b/phpmon/Common/Extensions/NSWindowExtension.swift index 73b47820..a06becbd 100644 --- a/phpmon/Common/Extensions/NSWindowExtension.swift +++ b/phpmon/Common/Extensions/NSWindowExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 17/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Extensions/NVAlertExtension.swift b/phpmon/Common/Extensions/NVAlertExtension.swift index 061b925b..567c045b 100644 --- a/phpmon/Common/Extensions/NVAlertExtension.swift +++ b/phpmon/Common/Extensions/NVAlertExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/07/2024. -// Copyright © 2024 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -18,6 +18,6 @@ extension NVAlert { return NVAlert().withInformation( title: "\(key).title".localized, subtitle: "\(key).description".localized - ).withPrimary(text: "generic.ok".localized).show() + ).withPrimary(text: "generic.ok".localized).show(urgency: .bringToFront) } } diff --git a/phpmon/Common/Extensions/StringExtension.swift b/phpmon/Common/Extensions/StringExtension.swift index 6cadf9ce..595e1c96 100644 --- a/phpmon/Common/Extensions/StringExtension.swift +++ b/phpmon/Common/Extensions/StringExtension.swift @@ -2,7 +2,7 @@ // StringExtension.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation import SwiftUI diff --git a/phpmon/Common/Extensions/TimeIntervalExtension.swift b/phpmon/Common/Extensions/TimeIntervalExtension.swift index 39e2b6aa..850b757a 100644 --- a/phpmon/Common/Extensions/TimeIntervalExtension.swift +++ b/phpmon/Common/Extensions/TimeIntervalExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 29/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -14,6 +14,10 @@ extension TimeInterval { static func minutes(_ value: Double) -> TimeInterval { value * 60 } static func hours(_ value: Double) -> TimeInterval { value * 3600 } static func days(_ value: Double) -> TimeInterval { value * 86400 } + + var nanoseconds: UInt64 { + return UInt64(self * 1_000_000_000) + } } extension Date { diff --git a/phpmon/Common/Extensions/XibLoadable.swift b/phpmon/Common/Extensions/XibLoadable.swift index c799567d..90eef02a 100644 --- a/phpmon/Common/Extensions/XibLoadable.swift +++ b/phpmon/Common/Extensions/XibLoadable.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/02/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Filesystem/FileSystemProtocol.swift b/phpmon/Common/Filesystem/FileSystemProtocol.swift index 98e6b0e7..b0b68386 100644 --- a/phpmon/Common/Filesystem/FileSystemProtocol.swift +++ b/phpmon/Common/Filesystem/FileSystemProtocol.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 08/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Filesystem/RealFileSystem.swift b/phpmon/Common/Filesystem/RealFileSystem.swift index 494dbdfc..04e633f1 100644 --- a/phpmon/Common/Filesystem/RealFileSystem.swift +++ b/phpmon/Common/Filesystem/RealFileSystem.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 08/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Filesystem/TestableFileSystem.swift b/phpmon/Common/Filesystem/TestableFileSystem.swift index 20ab6f6e..545d08e5 100644 --- a/phpmon/Common/Filesystem/TestableFileSystem.swift +++ b/phpmon/Common/Filesystem/TestableFileSystem.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -49,7 +49,7 @@ class TestableFileSystem: FileSystemProtocol { /** Serial dispatch queue for ensuring thread-safe access to the `files` dictionary. */ - private let accessQueue = DispatchQueue(label: "com.testablefilesystem.accessQueue") + private let accessQueue = DispatchQueue(label: "com.nicoverbruggen.phpmon.fs_access") // MARK: - Basics diff --git a/phpmon/Common/Helpers/Alert.swift b/phpmon/Common/Helpers/Alert.swift index 64809e2c..bc9ef878 100644 --- a/phpmon/Common/Helpers/Alert.swift +++ b/phpmon/Common/Helpers/Alert.swift @@ -2,7 +2,7 @@ // Alert.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Common/Helpers/Application.swift b/phpmon/Common/Helpers/Application.swift index c530823c..950d0c99 100644 --- a/phpmon/Common/Helpers/Application.swift +++ b/phpmon/Common/Helpers/Application.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 07/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -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,32 @@ 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/Common/Helpers/LocalNotification.swift b/phpmon/Common/Helpers/LocalNotification.swift index eb8de9ae..804a3b2c 100644 --- a/phpmon/Common/Helpers/LocalNotification.swift +++ b/phpmon/Common/Helpers/LocalNotification.swift @@ -2,7 +2,7 @@ // LocalNotification.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Helpers/Locked.swift b/phpmon/Common/Helpers/Locked.swift new file mode 100644 index 00000000..fce34f1e --- /dev/null +++ b/phpmon/Common/Helpers/Locked.swift @@ -0,0 +1,57 @@ +// +// Locked.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/11/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +/** + A thread-safe wrapper for a value that can be accessed from multiple threads. + + Uses `NSLock` internally to ensure only one thread can read or write at a time, + preventing race conditions where two threads might try to access or modify + the value simultaneously. + + ## Usage + + ```swift + private let _counter = Locked(0) + var counter: Int { + get { _counter.value } + set { _counter.value = newValue } + } + ``` + + Without locking, if Thread A reads a value while Thread B is writing to it, + Thread A might see a partially-written or inconsistent state, leading to crashes + or corrupted data. The lock ensures operations happen one at a time. + + Use with care. Using structured concurrency w/ `actor` or delegating to + `MainActor` is generally preferred, but this approach may be necessary in + situations where adopting structured concurrency would otherwise be + too challenging or a huge refactor. + */ +final class Locked: @unchecked Sendable { + private var _value: T + private let lock = NSLock() + + init(_ value: T) { + self._value = value + } + + var value: T { + get { + lock.lock() + defer { lock.unlock() } + return _value + } + set { + lock.lock() + defer { lock.unlock() } + _value = newValue + } + } +} diff --git a/phpmon/Common/Helpers/LoginItemManager.swift b/phpmon/Common/Helpers/LoginItemManager.swift index bea1aa17..6597e8c5 100644 --- a/phpmon/Common/Helpers/LoginItemManager.swift +++ b/phpmon/Common/Helpers/LoginItemManager.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 15/02/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import AppKit diff --git a/phpmon/Common/Helpers/Measurements.swift b/phpmon/Common/Helpers/Measurements.swift index d0ff2318..510a37c6 100644 --- a/phpmon/Common/Helpers/Measurements.swift +++ b/phpmon/Common/Helpers/Measurements.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 02/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Helpers/MenuBarImageGenerator.swift b/phpmon/Common/Helpers/MenuBarImageGenerator.swift index 744d62a5..d91f8622 100644 --- a/phpmon/Common/Helpers/MenuBarImageGenerator.swift +++ b/phpmon/Common/Helpers/MenuBarImageGenerator.swift @@ -2,7 +2,7 @@ // ImageGenerator.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Common/Helpers/PMWindowController.swift b/phpmon/Common/Helpers/PMWindowController.swift index 30e18570..5c74dfaa 100644 --- a/phpmon/Common/Helpers/PMWindowController.swift +++ b/phpmon/Common/Helpers/PMWindowController.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 05/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Common/Helpers/System.swift b/phpmon/Common/Helpers/System.swift index 10c4a435..465f43f0 100644 --- a/phpmon/Common/Helpers/System.swift +++ b/phpmon/Common/Helpers/System.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 01/11/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Helpers/VersionExtractor.swift b/phpmon/Common/Helpers/VersionExtractor.swift index 324c8963..2f05b7ba 100644 --- a/phpmon/Common/Helpers/VersionExtractor.swift +++ b/phpmon/Common/Helpers/VersionExtractor.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Http/RealWebApi.swift b/phpmon/Common/Http/RealWebApi.swift index d0a3aaf5..1a614c65 100644 --- a/phpmon/Common/Http/RealWebApi.swift +++ b/phpmon/Common/Http/RealWebApi.swift @@ -82,7 +82,11 @@ class RealWebApi: WebApiProtocol { var defaultHeaders: HttpHeaders { return [ - "User-Agent": "phpmon-nur/2.0", + // Fun fact: NUR stands for "NSURLSession Update Requester" + "User-Agent": "phpmon-nur/3.0", + // Optional randomized API session UUID + "X-phpmon-session-uuid": App.shared.getApiId(), + // Required fields "X-phpmon-version": "\(App.shortVersion) (\(App.bundleVersion))", "X-phpmon-os-version": "\(App.macVersion)", "X-phpmon-bundle-id": "\(App.identifier)" diff --git a/phpmon/Common/PHP/ActivePhpInstallation.swift b/phpmon/Common/PHP/ActivePhpInstallation.swift index f9846523..c7fe5e4e 100644 --- a/phpmon/Common/PHP/ActivePhpInstallation.swift +++ b/phpmon/Common/PHP/ActivePhpInstallation.swift @@ -2,7 +2,7 @@ // ActivePhpInstallation.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/Extensions/Xdebug.swift b/phpmon/Common/PHP/Extensions/Xdebug.swift index c62c67ff..6e2ab457 100644 --- a/phpmon/Common/PHP/Extensions/Xdebug.swift +++ b/phpmon/Common/PHP/Extensions/Xdebug.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 01/05/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/Homebrew/HomebrewDecodable.swift b/phpmon/Common/PHP/Homebrew/HomebrewDecodable.swift index 713f9b10..70fbb6e0 100644 --- a/phpmon/Common/PHP/Homebrew/HomebrewDecodable.swift +++ b/phpmon/Common/PHP/Homebrew/HomebrewDecodable.swift @@ -2,7 +2,7 @@ // HomebrewDecodable.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/Homebrew/HomebrewService.swift b/phpmon/Common/PHP/Homebrew/HomebrewService.swift index aa854041..c461e74a 100644 --- a/phpmon/Common/PHP/Homebrew/HomebrewService.swift +++ b/phpmon/Common/PHP/Homebrew/HomebrewService.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 11/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift b/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift index 68afc27f..02b2d287 100644 --- a/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift +++ b/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -106,14 +106,28 @@ class PhpEnvironments { } } + // MARK: - Thread-Safe PHP Version Storage + /** All versions of PHP that are currently supported. */ - var availablePhpVersions: [String] = [] + private let _availablePhpVersions = Locked<[String]>([]) + var availablePhpVersions: [String] { + get { _availablePhpVersions.value } + set { _availablePhpVersions.value = newValue } + } /** All versions of PHP that are currently installed but not compatible. */ - var incompatiblePhpVersions: [String] = [] + private let _incompatiblePhpVersions = Locked<[String]>([]) + var incompatiblePhpVersions: [String] { + get { _incompatiblePhpVersions.value } + set { _incompatiblePhpVersions.value = newValue } + } /** Cached information about the PHP installations. */ - var cachedPhpInstallations: [String: PhpInstallation] = [:] + private let _cachedPhpInstallations = Locked<[String: PhpInstallation]>([:]) + var cachedPhpInstallations: [String: PhpInstallation] { + get { _cachedPhpInstallations.value } + set { _cachedPhpInstallations.value = newValue } + } /** Information about the currently linked PHP installation. */ var currentInstall: ActivePhpInstallation? { diff --git a/phpmon/Common/PHP/PHP Version/PhpHelper.swift b/phpmon/Common/PHP/PHP Version/PhpHelper.swift index 5a6b2eb4..2c83d909 100644 --- a/phpmon/Common/PHP/PHP Version/PhpHelper.swift +++ b/phpmon/Common/PHP/PHP Version/PhpHelper.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 17/03/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/PHP Version/PhpVersionNumberCollection.swift b/phpmon/Common/PHP/PHP Version/PhpVersionNumberCollection.swift index 356ba6ef..e6cb1ab3 100644 --- a/phpmon/Common/PHP/PHP Version/PhpVersionNumberCollection.swift +++ b/phpmon/Common/PHP/PHP Version/PhpVersionNumberCollection.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 06/01/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/PHP Version/VersionNumber.swift b/phpmon/Common/PHP/PHP Version/VersionNumber.swift index dbbf4184..903b4298 100644 --- a/phpmon/Common/PHP/PHP Version/VersionNumber.swift +++ b/phpmon/Common/PHP/PHP Version/VersionNumber.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 23/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/PhpConfigurationFile.swift b/phpmon/Common/PHP/PhpConfigurationFile.swift index 8e000413..8499633f 100644 --- a/phpmon/Common/PHP/PhpConfigurationFile.swift +++ b/phpmon/Common/PHP/PhpConfigurationFile.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/05/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -83,7 +83,7 @@ class PhpConfigurationFile: CreatedFromFile { Replaces the value for a specific (existing) key with a new value. The key must exist for this to work. */ - public func replace(key: String, value: String) throws { + public func replace(key: String, value: String) async throws { // Ensure that the key exists guard let item = getConfig(for: key) else { throw ReplacementErrors.missingKey @@ -102,14 +102,11 @@ class PhpConfigurationFile: CreatedFromFile { self.lines[item.lineIndex] = components.joined(separator: "=") // Ensure the watchers aren't tripped up by config changes - ConfigWatchManager.ignoresModificationsToConfigValues = true - - // Finally, join the string and save the file atomatically again - try self.lines.joined(separator: "\n") - .write(toFile: self.filePath, atomically: true, encoding: .utf8) - - // Ensure watcher behaviour is reverted - ConfigWatchManager.ignoresModificationsToConfigValues = false + try await ConfigWatchManager.withSuspended { + // Finally, join the string and save the file atomically again + try self.lines.joined(separator: "\n") + .write(toFile: self.filePath, atomically: true, encoding: .utf8) + } // Reload the original file self.reload() diff --git a/phpmon/Common/PHP/PhpExtension.swift b/phpmon/Common/PHP/PhpExtension.swift index 3c80e8b7..d811301b 100644 --- a/phpmon/Common/PHP/PhpExtension.swift +++ b/phpmon/Common/PHP/PhpExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 31/01/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/PhpInstallation.swift b/phpmon/Common/PHP/PhpInstallation.swift index f4080920..db3d2a26 100644 --- a/phpmon/Common/PHP/PhpInstallation.swift +++ b/phpmon/Common/PHP/PhpInstallation.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 28/11/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -94,11 +94,24 @@ class PhpInstallation { withStandardError: true ).trimmingCharacters(in: .whitespacesAndNewlines) + // The PHP executable did not return any output + if testCommand.isEmpty + || testCommand.contains("HANDLE_READ_FAILURE") { + Log.err("No output. PHP \(self.versionNumber.short) is not healthy!") + self.isHealthy = false + } + + // The PHP executable crashed with an uncaught signal when we tried to run this + if testCommand.contains("UNCAUGHT_SIGNAL") { + Log.err("Uncaught signal, PHP \(self.versionNumber.short) is not healthy!") + self.isHealthy = false + } + // If the "dyld: Library not loaded" issue pops up, we have an unhealthy PHP installation // and we will need to reinstall this version of PHP via Homebrew. if testCommand.contains("Library not loaded") && testCommand.contains("dyld") { + Log.err("dyld error, PHP \(self.versionNumber.short) is not healthy!") self.isHealthy = false - Log.err("The PHP installation of \(self.versionNumber.short) is not healthy!") } } } diff --git a/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift b/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift index 32c5747d..e4b7517a 100644 --- a/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift +++ b/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 14/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift index 09058527..cf3245c3 100644 --- a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift +++ b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 24/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/PHP/Switcher/PhpSwitcher.swift b/phpmon/Common/PHP/Switcher/PhpSwitcher.swift index d0ffe8ea..dd0ab1f2 100644 --- a/phpmon/Common/PHP/Switcher/PhpSwitcher.swift +++ b/phpmon/Common/PHP/Switcher/PhpSwitcher.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 24/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Protocols/CreatedFromFile.swift b/phpmon/Common/Protocols/CreatedFromFile.swift index 90deeb0c..7e492061 100644 --- a/phpmon/Common/Protocols/CreatedFromFile.swift +++ b/phpmon/Common/Protocols/CreatedFromFile.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 15/05/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Protocols/Suspendable.swift b/phpmon/Common/Protocols/Suspendable.swift new file mode 100644 index 00000000..d7f6e209 --- /dev/null +++ b/phpmon/Common/Protocols/Suspendable.swift @@ -0,0 +1,58 @@ +// +// Suspendable.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 06/12/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +/** + A protocol for actors that manage filesystem watchers and can temporarily + suspend their responses to changes. + + This is useful when the application itself makes changes to watched files, + preventing duplicate work or unwanted side effects. + */ +protocol Suspendable: Actor { + /** + Suspends responding to filesystem events. + Events are still observed but handlers won't fire. + */ + func suspend() async + + /** + Resumes responding to filesystem events. + Handlers will fire normally for observed events. + */ + func resume() async + + /** + Executes an action while suspended, ensuring resume happens + even if the action throws. + + - Parameter action: The async throwing closure to execute while suspended + - Returns: The result of the action + - Throws: Rethrows any error from the action + */ + func withSuspended(_ action: () async throws -> T) async rethrows -> T +} + +extension Suspendable { + /** + Default implementation of withSuspended that ensures proper + suspend/resume lifecycle even when errors occur. + */ + func withSuspended(_ action: () async throws -> T) async rethrows -> T { + await suspend() + do { + let result = try await action() + await resume() + return result + } catch { + await resume() + throw error + } + } +} diff --git a/phpmon/Common/Shell/RealShell.swift b/phpmon/Common/Shell/RealShell.swift index 110ae917..9887fa7c 100644 --- a/phpmon/Common/Shell/RealShell.swift +++ b/phpmon/Common/Shell/RealShell.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -80,6 +80,23 @@ class RealShell: ShellProtocol { return task } + /** + Reads the entire output of a `Pipe` and returns it as a UTF‑8 string. + Closes the pipe's file handler when done. + */ + private static func getStringOutput(from pipe: Pipe) -> String { + // 1. Read all data (safely). + let rawData = (try? pipe.fileHandleForReading.readToEnd()) ?? Data() + + // 2. Convert to string (safely). + let result = String(data: rawData, encoding: .utf8) ?? "" + + // 3. Close the handle quietly. + try? pipe.fileHandleForReading.close() + + return result + } + // MARK: - Public API /** @@ -114,11 +131,8 @@ class RealShell: ShellProtocol { return .out("", "") } - let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" - let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" - - try? outputPipe.fileHandleForReading.close() - try? errorPipe.fileHandleForReading.close() + let stdOut = RealShell.getStringOutput(from: outputPipe) + let stdErr = RealShell.getStringOutput(from: errorPipe) if Log.shared.verbosity == .cli { log(process: process, stdOut: stdOut, stdErr: stdErr) @@ -145,20 +159,17 @@ class RealShell: ShellProtocol { process.terminationHandler = { [weak self] _ in if process.terminationReason == .uncaughtSignal { Log.err("The command `\(command)` likely crashed. Returning empty output.") - continuation.resume(returning: .out("", "")) + return continuation.resume(returning: .out("", "")) } - let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" - let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" - - try? outputPipe.fileHandleForReading.close() - try? errorPipe.fileHandleForReading.close() + let stdOut = RealShell.getStringOutput(from: outputPipe) + let stdErr = RealShell.getStringOutput(from: errorPipe) if Log.shared.verbosity == .cli { self?.log(process: process, stdOut: stdOut, stdErr: stdErr) } - continuation.resume(returning: .out(stdOut, stdErr)) + return continuation.resume(returning: .out(stdOut, stdErr)) } process.launch() @@ -208,6 +219,7 @@ class RealShell: ShellProtocol { process.standardError = errorPipe let output = ShellOutput.empty() + let serialQueue = DispatchQueue(label: "com.nicoverbruggen.phpmon.shell_output") return try await withCheckedThrowingContinuation({ continuation in let timeoutTask = Task { @@ -224,8 +236,10 @@ class RealShell: ShellProtocol { outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in let data = fileHandle.availableData if !data.isEmpty, let string = String(data: data, encoding: .utf8) { - output.out += string - didReceiveOutput(string, .stdOut) + serialQueue.async { + output.out += string + didReceiveOutput(string, .stdOut) + } } } @@ -233,8 +247,10 @@ class RealShell: ShellProtocol { errorPipe.fileHandleForReading.readabilityHandler = { fileHandle in let data = fileHandle.availableData if !data.isEmpty, let string = String(data: data, encoding: .utf8) { - output.err += string - didReceiveOutput(string, .stdErr) + serialQueue.async { + output.err += string + didReceiveOutput(string, .stdErr) + } } } @@ -249,20 +265,18 @@ class RealShell: ShellProtocol { let remainingOut = outputPipe.fileHandleForReading.readDataToEndOfFile() let remainingErr = errorPipe.fileHandleForReading.readDataToEndOfFile() - if !remainingOut.isEmpty, let string = String(data: remainingOut, encoding: .utf8) { - output.out += string - didReceiveOutput(string, .stdOut) - } + serialQueue.async { + if !remainingOut.isEmpty, let string = String(data: remainingOut, encoding: .utf8) { + output.out += string + didReceiveOutput(string, .stdOut) + } - if !remainingErr.isEmpty, let string = String(data: remainingErr, encoding: .utf8) { - output.err += string - didReceiveOutput(string, .stdErr) - } + if !remainingErr.isEmpty, let string = String(data: remainingErr, encoding: .utf8) { + output.err += string + didReceiveOutput(string, .stdErr) + } - if !output.err.isEmpty { - continuation.resume(returning: (process, .err(output.err))) - } else { - continuation.resume(returning: (process, .out(output.out))) + continuation.resume(returning: (process, output)) } } @@ -275,9 +289,3 @@ class RealShell: ShellProtocol { self.PATH = RealShell.getPath() } } - -extension TimeInterval { - var nanoseconds: UInt64 { - return UInt64(self * 1_000_000_000) - } -} diff --git a/phpmon/Common/Shell/ShellProtocol.swift b/phpmon/Common/Shell/ShellProtocol.swift index afce9142..0fa17f0b 100644 --- a/phpmon/Common/Shell/ShellProtocol.swift +++ b/phpmon/Common/Shell/ShellProtocol.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -67,7 +67,7 @@ enum ShellStream: Codable { case stdOut, stdErr, stdIn } -class ShellOutput { +class ShellOutput: @unchecked Sendable { var out: String var err: String diff --git a/phpmon/Common/Shell/TestableShell.swift b/phpmon/Common/Shell/TestableShell.swift index 3d9c3326..cd417ee6 100644 --- a/phpmon/Common/Shell/TestableShell.swift +++ b/phpmon/Common/Shell/TestableShell.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/State/BusyStatus.swift b/phpmon/Common/State/BusyStatus.swift index 551d3621..d7b72b9d 100644 --- a/phpmon/Common/State/BusyStatus.swift +++ b/phpmon/Common/State/BusyStatus.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 02/05/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Common/Testables/TestableConfiguration.swift b/phpmon/Common/Testables/TestableConfiguration.swift index 70ab6e41..9299ea2f 100644 --- a/phpmon/Common/Testables/TestableConfiguration.swift +++ b/phpmon/Common/Testables/TestableConfiguration.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -55,7 +55,6 @@ public struct TestableConfiguration: Codable { private var primaryPhpVersion: VersionNumber? private var secondaryPhpVersions: [VersionNumber] = [] - // swiftlint:disable function_body_length mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) { if primary { if primaryPhpVersion != nil { @@ -125,7 +124,6 @@ public struct TestableConfiguration: Codable { ) } } - // swiftlint:enable function_body_length // MARK: Interactions diff --git a/phpmon/Container/Container+Real.swift b/phpmon/Container/Container+Real.swift index ded175d2..cb435d0e 100644 --- a/phpmon/Container/Container+Real.swift +++ b/phpmon/Container/Container+Real.swift @@ -7,9 +7,9 @@ // extension Container { - public static func real() -> Container { + public static func real(minimal: Bool = false) -> Container { let container = Container() - container.bind() + container.bind(coreOnly: minimal) return container } } diff --git a/phpmon/Container/Container.swift b/phpmon/Container/Container.swift index 9f20e40b..f9892739 100644 --- a/phpmon/Container/Container.swift +++ b/phpmon/Container/Container.swift @@ -51,11 +51,18 @@ class Container { /// (Swapping instances for specific dependencies can be introduced later with dedicated /// methods if it ever becomes truly necessary.) /// - public func bind() { + /// - Parameter coreOnly: Only binds `shell`, `filesystem`, `command`, `paths` and `webApi`. + /// Use this to prevent slowing down tests for a minimal container. + /// + public func bind(coreOnly: Bool = false) { if self.bound { fatalError("You cannot call `bind` on a Container more than once.") } + defer { + self.bound = true + } + // These are the most basic building blocks. We need these before // any of the other classes can be initialized! self.shell = RealShell(container: self) @@ -64,6 +71,10 @@ class Container { self.paths = Paths(container: self) self.webApi = RealWebApi(container: self) + if coreOnly { + return + } + // Please note that the order in which these are initialized, matters! // For example, preferences leverages the Paths instance, so don't just // swap these around for no reason... the order is very intentional. @@ -71,9 +82,6 @@ class Container { self.phpEnvs = PhpEnvironments(container: self) self.favorites = Favorites() self.warningManager = WarningManager(container: self) - - // At this point, our container has been bound. - self.bound = true } /** diff --git a/phpmon/Credits.html b/phpmon/Credits.html index 8b36c736..7b0b4217 100644 --- a/phpmon/Credits.html +++ b/phpmon/Credits.html @@ -14,11 +14,141 @@

Do you enjoy using the app? Is it helping you save time? Leave a star on GitHub!

+

Having issues? Consult the FAQ section, I did my best to ensure everything is documented.

+ +

Privacy concerns? Don't worry! Get informed by visiting the privacy policy.

+

Want to support further development of PHP Monitor? You can financially support the continued development of this app.

+

Get the latest on social media. Follow me on Twitter (X), Bluesky or Mastodon to learn about what's brewing and when new updates drop.

+

Special thanks to all current and past sponsors of PHP Monitor, who have helped to make further development of the app possible.

-

Made possible by these GitHub Sponsors: @abdusfauzi, @abicons, @adibnoh, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @ash-jc-allen, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @casenxu, @caseyalee, @cgreuling, @cjcox17, @clescuyer, @codelinde, @designhammer, @Diewy, @drfraker, @driftingly, @duellsy, @e9li, @edalzell, @EYOND, @faithfm, @frankmichel, @gekich, @gpluess, @gwleuverink, @hopkins385, @incon, @intrepidws, @israaraujo, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @jorisnoo, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @kholisabdullah, @Laravel-Backpack, @leganz, @lucianvacaroiu,@martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @megabubbleteam, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rastitkac, @rderimay, @renecum, @richardhulbert, @richardtape, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @slaFFik, @spatie, @SRWieZ, @stefanbauer, @stefanzweifel, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @vintagesucks, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.
(This is a historical list of sponsors, not current sponsors. Some names have been omitted due to their sponsorships being private. Thank you all!)

+ +

Made possible by these GitHub Sponsors: + @abdusfauzi, + @abicons, + @ace-of-aces, + @adibnoh, + @adrolli, + @andresayej, + @andyunleashed, + @anzacorp, + @argirisp, + @ash-jc-allen, + @AshPowell, + @aurawindsurfing, + @awsmug, + @barrycarton, + @BertvanHoekelen, + @calebporzio, + @casenxu, + @caseyalee, + @cgreuling, + @cjcox17, + @clescuyer, + @codelinde, + @designhammer, + @Diewy, + @drfraker, + @driftingly, + @duellsy, + @e9li, + @edalzell, + @EYOND, + @faithfm, + @frankmichel, + @gekich, + @gpluess, + @gwleuverink, + @hopkins385, + @ianlandsman, + @incon, + @intrepidws, + @israaraujo, + @jacksleight, + @JacobBennett, + @jasonvarga, + @jeromegamez, + @jimmyaldape, + @jimmysawczuk, + @joetannenbaum, + @jolora, + @jorisnoo, + @joshuablum, + @jpeinelt, + @jreviews, + @JustSteveKing, + @Kajvdh, + @KFoobar, + @kholisabdullah, + @Laravel-Backpack, + @leganz, + @lucianvacaroiu, + @marianoviola, + @martinleveille, + @mathiasonea, + @matthewmnewman, + @mcastillo1030, + @megabubbletea, + @mennen-online, + @mike-healy, + @mostafakram, + @mpociot, + @MrMicky-FR, + @MrMooky, + @murdercode, + @nckrtl, + @nhedger, + @ninjaparade, + @ozanuzer, + @pepatel, + @philbraun, + @pickuse2013, + @pk-informatics, + @Plytas, + @rastitkac, + @rderimay, + @renecum, + @richardhulbert, + @richardtape, + @rickyjohnston, + @rico, + @RobertBoes, + @runofthemill, + @SahinU88, + @sdebacker, + @sdevore, + @shadracnicholas, + @simonhamp, + @slaFFik, + @spatie, + @SRWieZ, + @stefanbauer, + @stefanzweifel, + @StriveMedia, + @studentiyot, + @swilla, + @Tailcode-Studio, + @theutz, + @ThomasEnssner, + @tillkruss, + @timothyrowan, + @ttnppedr, + @victorsyin, + @vincent-tarrit, + @vintagesucks, + @WheresMarco, + @xPand4B, + @xuandung38, + @yeslandi89, + @zackkatz, + @zacksmash, + @zaherg. +
+
+ (This is a historical list of sponsors, these are not all current sponsors. Some names have been omitted due to private sponsorships. Thank you, everyone!)

+

Localization credits:
‐ English, Dutch by @nicoverbruggen
‐ Vietnamese by @xuandung38
@@ -29,6 +159,6 @@
Other languages are considered experimental, and were generated via a local LLM. If you have feedback or concerns, please don't hesitate to get in touch.

-
+ diff --git a/phpmon/Domain/App/App+ActivationPolicy.swift b/phpmon/Domain/App/App+ActivationPolicy.swift index 8f01606a..946129e5 100644 --- a/phpmon/Domain/App/App+ActivationPolicy.swift +++ b/phpmon/Domain/App/App+ActivationPolicy.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 05/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Domain/App/App+DetectApps.swift b/phpmon/Domain/App/App+DetectApps.swift new file mode 100644 index 00000000..6f7f210f --- /dev/null +++ b/phpmon/Domain/App/App+DetectApps.swift @@ -0,0 +1,32 @@ +// +// App+DetectApps.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 26/11/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +extension App { + /** + Detect which applications are installed that can be used to open a domain's source directory. + */ + public func detectApplications() async { + Log.info("Detecting applications...") + + // Start by detecting the default applications + var detected = await Application.detectPresetApplications(container) + + // Next up, scan for additional apps + let customApps = Preferences.custom.scanApps?.map { appName in + return Application(container, appName, .user_supplied) + } ?? [] + + // Append any detected apps + for app in customApps where await app.isInstalled() { + detected.append(app) + } + + App.shared.detectedApplications = detected + Log.info("Detected applications: \(detected.map { $0.name })") + } +} diff --git a/phpmon/Domain/App/App+GlobalHotkey.swift b/phpmon/Domain/App/App+GlobalHotkey.swift index a59701be..76ad0194 100644 --- a/phpmon/Domain/App/App+GlobalHotkey.swift +++ b/phpmon/Domain/App/App+GlobalHotkey.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 05/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Domain/App/App+UUID.swift b/phpmon/Domain/App/App+UUID.swift new file mode 100644 index 00000000..55de32ba --- /dev/null +++ b/phpmon/Domain/App/App+UUID.swift @@ -0,0 +1,74 @@ +// +// App+UUID.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 01/12/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +struct Api { + /** + How long a API ID (uuid) is valid. + + Currently set to this length to accomodate potential failures + for the update check. + */ + static let uuidValidityDuration: TimeInterval = .hours(36) + + static let uuidKey = "api.uuid" + static let uuidTimestampKey = "api.uuid.timestamp" +} + +extension App { + /** + Returns a unique UUID that automatically refreshes every 36 hours. + It is used to more accurately throttle API requests for a given IP, + since multiple users could be coming from one residential/business IP. + + - Important: This UUID is NOT used for tracking. + It is used for legitimate purposes only. + Read more below to find out how this UUID is used. + + How it is used: + + - Allows identifying which IP addresses might be throttled too quickly + (example: many requests with the same IP, but different unique UUIDs) + + - Allows counting how many unique users checked for updates in 24 hours + (example: previous assumption was: 1 IP = 1 user; not always true!) + + The UUID is stored in UserDefaults and regenerated when it has expired. + Because I only use this for user counting, the ID is reset after 36 hours. + */ + func getApiId() -> String { + let defaults = UserDefaults.standard + + // Check if we have a stored UUID and timestamp + if let storedUUID = defaults.string(forKey: Api.uuidKey), + let storedTimestamp = defaults.object(forKey: Api.uuidTimestampKey) as? Date { + + // Check if the UUID is still valid (less than X hours old) + if Date().timeIntervalSince(storedTimestamp) < Api.uuidValidityDuration { + return storedUUID + } + } + + // Generate a new UUID if we don't have one or it's expired + return regenerate() + } + + /** + Regenerates a UUID for a given duration. + */ + private func regenerate() -> String { + let newUUID = UUID().uuidString + let defaults = UserDefaults.standard + + defaults.set(newUUID, forKey: Api.uuidKey) + defaults.set(Date(), forKey: Api.uuidTimestampKey) + + return newUUID + } +} diff --git a/phpmon/Domain/App/App.swift b/phpmon/Domain/App/App.swift index 41e12599..e387b21d 100644 --- a/phpmon/Domain/App/App.swift +++ b/phpmon/Domain/App/App.swift @@ -2,7 +2,7 @@ // StateManager.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa @@ -134,14 +134,18 @@ class App { */ var openWindows: [String] = [] - // MARK: - App Watchers + // MARK: - FS Watchers - /** Individual filesystem watchers, which are, i.e. responsible for watching the Homebrew folders. */ - var watchers: [String: FSNotifier] = [:] - - /** + /** The `ConfigWatchManager` is responsible for watching the `.ini` files and the `.conf.d` folder. This manager object can immediately start or stop all watchers (or pause them) all at once. */ - var watchManager: ConfigWatchManager! + var configWatchManager: ConfigWatchManager? + + /** + The `HomebrewWatchManager` is responsible for watching the Homebrew binaries folder. + This allows PHP Monitor to respond to external `brew` changes executed by the user. + */ + var homebrewWatchManager: HomebrewWatchManager? + } diff --git a/phpmon/Domain/App/AppDelegate+InterApp.swift b/phpmon/Domain/App/AppDelegate+InterApp.swift index a3cec555..7f388ebc 100644 --- a/phpmon/Domain/App/AppDelegate+InterApp.swift +++ b/phpmon/Domain/App/AppDelegate+InterApp.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 20/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Domain/App/AppDelegate+MenuOutlets.swift b/phpmon/Domain/App/AppDelegate+MenuOutlets.swift index 52fb01a5..bab7ae3b 100644 --- a/phpmon/Domain/App/AppDelegate+MenuOutlets.swift +++ b/phpmon/Domain/App/AppDelegate+MenuOutlets.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 05/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/App/AppDelegate+Notifications.swift b/phpmon/Domain/App/AppDelegate+Notifications.swift index f97e9e37..6a7cf6d3 100644 --- a/phpmon/Domain/App/AppDelegate+Notifications.swift +++ b/phpmon/Domain/App/AppDelegate+Notifications.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 06/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/App/AppDelegate.swift b/phpmon/Domain/App/AppDelegate.swift index a90c929f..916f2845 100644 --- a/phpmon/Domain/App/AppDelegate.swift +++ b/phpmon/Domain/App/AppDelegate.swift @@ -2,7 +2,7 @@ // AppDelegate.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa @@ -53,6 +53,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele #if DEBUG logger.verbosity = .performance + Log.info("Extra verbose mode is enabled by default on DEBUG builds.") + if let profile = CommandLine.arguments.first(where: { $0.matches(pattern: "--configuration:*") }) { AppDelegate.initializeTestingProfile(profile.replacing("--configuration:", with: "")) } @@ -113,8 +115,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele // Make sure notifications will work setupNotifications() + // Start with the regular busy icon + MainMenu.shared.setStatusBar(image: NSImage.statusBarIcon) + Task { // Make sure the menu performs its initial checks - await MainMenu.shared.startup() + await Startup.check(App.shared.container) } } diff --git a/phpmon/Domain/App/AppUpdater.swift b/phpmon/Domain/App/AppUpdater.swift index e792fe80..58330402 100644 --- a/phpmon/Domain/App/AppUpdater.swift +++ b/phpmon/Domain/App/AppUpdater.swift @@ -3,140 +3,145 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/02/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation import Cocoa import NVAlert +/** + The potential different outcomes of a check for updates. + */ enum UpdateCheckResult { case success case networkError case parseError } +/** + Instead of using `UpdateCheck` which is a more simplified update checking process + included in `NVAppUpdater`, we have a slightly more complex setup here. + */ class AppUpdater { var caskFile: CaskFile! var latestVersionOnline: AppVersion! var interactive: Bool = false public func checkForUpdates(userInitiated: Bool) async -> UpdateCheckResult { + // If user initiated, we always expect to see an alert self.interactive = userInitiated + // Log that we're looking for updates Log.info("The app will search for updates...") - let caskUrl = Constants.Urls.UpdateCheckEndpoint - - guard let caskFile = try? await CaskFile.fromUrl(App.shared.container, caskUrl) else { - presentCouldNotRetrieveUpdateIfInteractive() + // Attempt to get the latest CaskFile from the API + guard let caskFile = try? await CaskFile.fromUrl( + App.shared.container, + Constants.Urls.UpdateCheckEndpoint + ) else { + // ERROR #1: The endpoint is unreachable or the response is invalid. + Log.err("Could not get a valid CaskFile from the endpoint.") + if interactive { + await presentCouldNotRetrieveUpdate() + } return .networkError } + // We will now persist the CaskFile so we can reference it later self.caskFile = caskFile - let currentVersion = AppVersion.fromCurrentVersion() - + // Let's parse the latest online version if we can guard let onlineVersion = AppVersion.from(caskFile.version) else { + // ERROR #2: The CaskFile's version string is invalid. Log.err("The version string from the CaskFile could not be read.") - presentCouldNotRetrieveUpdateIfInteractive() + if interactive { + await presentCouldNotRetrieveUpdate() + } return .parseError } + // We will now persist the version number so we can reference it later latestVersionOnline = onlineVersion - Log.info("The latest version read from '\(caskUrl.lastPathComponent)' is: v\(onlineVersion.computerReadable).") + Log.info("The latest version read from the endpoint is: v\(onlineVersion.computerReadable).") - if latestVersionOnline > currentVersion { - presentNewerVersionAvailableAlert() - } else if interactive { - presentNoNewerVersionAvailableAlert() + Task { // Present this concurrently w/ returning the .success value + if latestVersionOnline > AppVersion.fromCurrentVersion() { + await presentNewerVersionAvailableAlert() + } else if interactive { + await presentNoNewerVersionAvailableAlert() + } } return .success } - private func presentCouldNotRetrieveUpdateIfInteractive() { - if interactive { - return presentCouldNotRetrieveUpdate() - } else { - return - } - } - // MARK: - Alerts - public func presentNewerVersionAvailableAlert() { - let command = "brew upgrade phpmon" - - Task { @MainActor in - NVAlert().withInformation( - title: "updater.alerts.newer_version_available.title" - .localized(latestVersionOnline.humanReadable), - subtitle: "updater.alerts.newer_version_available.subtitle" - .localized, - description: BrewDiagnostics.shared.customCaskInstalled - ? "updater.installation_source.brew".localized(command) - : "updater.installation_source.direct".localized - ) - .withPrimary( - text: "updater.alerts.buttons.install".localized, - action: { vc in - self.cleanupCaskroom() - self.prepareForDownload() - vc.close(with: .OK) - } - ) - .withSecondary( - text: "updater.alerts.buttons.release_notes".localized, - action: { _ in - NSWorkspace.shared.open({ - if App.identifier.contains(".eap") { - return Constants.Urls.EarlyAccessChangelog - } else { - let urlSegments = self.caskFile.url.split(separator: "/") - let tag = urlSegments[urlSegments.count - 2] // ../download/{tag}/{file.zip} - return Constants.Urls.GitHubReleases.appendingPathComponent("/tag/\(tag)") - } - }()) - } - ) - .withTertiary(text: "updater.alerts.buttons.dismiss".localized, action: { vc in + @MainActor public func presentNewerVersionAvailableAlert() { + NVAlert().withInformation( + title: "updater.alerts.newer_version_available.title" + .localized(latestVersionOnline.humanReadable), + subtitle: "updater.alerts.newer_version_available.subtitle" + .localized, + description: BrewDiagnostics.shared.customCaskInstalled + ? "updater.installation_source.brew".localized("brew upgrade phpmon") + : "updater.installation_source.direct".localized + ) + .withPrimary( + text: "updater.alerts.buttons.install".localized, + action: { vc in + self.cleanupCaskroom() + self.prepareForDownload() vc.close(with: .OK) - }) - .show() - } + } + ) + .withSecondary( + text: "updater.alerts.buttons.release_notes".localized, + action: { _ in + NSWorkspace.shared.open({ + if App.identifier.contains(".eap") { + return Constants.Urls.EarlyAccessChangelog + } else { + let urlSegments = self.caskFile.url.split(separator: "/") + let tag = urlSegments[urlSegments.count - 2] // ../download/{tag}/{file.zip} + return Constants.Urls.GitHubReleases.appendingPathComponent("/tag/\(tag)") + } + }()) + } + ) + .withTertiary(text: "updater.alerts.buttons.dismiss".localized, action: { vc in + vc.close(with: .OK) + }) + .show(urgency: interactive ? .bringToFront : .urgentRequestAttention) } - public func presentNoNewerVersionAvailableAlert() { - Task { @MainActor in - NVAlert().withInformation( - title: "updater.alerts.is_latest_version.title".localized, - subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion), - description: "" - ) - .withPrimary(text: "generic.ok".localized) - .show() - } + @MainActor public func presentNoNewerVersionAvailableAlert() { + NVAlert().withInformation( + title: "updater.alerts.is_latest_version.title".localized, + subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion), + description: "" + ) + .withPrimary(text: "generic.ok".localized) + .show(urgency: .bringToFront) } - public func presentCouldNotRetrieveUpdate() { - Task { @MainActor in - NVAlert().withInformation( - title: "updater.alerts.cannot_check_for_update.title".localized, - subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized, - description: "updater.alerts.cannot_check_for_update.description".localized( - App.version - ) + @MainActor public func presentCouldNotRetrieveUpdate() { + NVAlert().withInformation( + title: "updater.alerts.cannot_check_for_update.title".localized, + subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized, + description: "updater.alerts.cannot_check_for_update.description".localized( + App.version ) - .withTertiary( - text: "updater.alerts.buttons.releases_on_github".localized, - action: { _ in - NSWorkspace.shared.open(Constants.Urls.GitHubReleases) - } - ) - .withPrimary(text: "generic.ok".localized) - .show() - } + ) + .withTertiary( + text: "updater.alerts.buttons.releases_on_github".localized, + action: { _ in + NSWorkspace.shared.open(Constants.Urls.GitHubReleases) + } + ) + .withPrimary(text: "generic.ok".localized) + .show(urgency: .bringToFront) } // MARK: - Preparing for Self-Updater diff --git a/phpmon/Domain/App/AppVersion.swift b/phpmon/Domain/App/AppVersion.swift index 5bd3303b..5e7f47cf 100644 --- a/phpmon/Domain/App/AppVersion.swift +++ b/phpmon/Domain/App/AppVersion.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 10/05/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/App/CrashReporter.swift b/phpmon/Domain/App/CrashReporter.swift index 28954dc4..5fef99e8 100644 --- a/phpmon/Domain/App/CrashReporter.swift +++ b/phpmon/Domain/App/CrashReporter.swift @@ -68,7 +68,7 @@ class CrashReporter { }) .withPrimary(text: "crash_reporter.send_report".localized, action: { alert in alert.close(with: .OK) - }).runModal() + }).runModal(urgency: .urgentRequestAttention) // Check the outcome of what the user chose if response == .abort { @@ -100,6 +100,7 @@ class CrashReporter { request.httpMethod = "POST" request.setValue("text/crash", forHTTPHeaderField: "Content-Type") request.setValue("phpmon-crashrep/1.0", forHTTPHeaderField: "User-Agent") + request.setValue(App.shared.getApiId(), forHTTPHeaderField: "X-phpmon-session-uuid") request.httpBody = text.data(using: .utf8) request.timeoutInterval = timeout diff --git a/phpmon/Domain/App/EnvironmentCheck.swift b/phpmon/Domain/App/EnvironmentCheck.swift index 6324fe7c..dbc96af3 100644 --- a/phpmon/Domain/App/EnvironmentCheck.swift +++ b/phpmon/Domain/App/EnvironmentCheck.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 10/08/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/App/InterAppHandler.swift b/phpmon/Domain/App/InterAppHandler.swift index 1d88bd73..43da2a2f 100644 --- a/phpmon/Domain/App/InterAppHandler.swift +++ b/phpmon/Domain/App/InterAppHandler.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 28/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/App/Services/FakeServicesManager.swift b/phpmon/Domain/App/Services/FakeServicesManager.swift index 967aace7..3b4fccc2 100644 --- a/phpmon/Domain/App/Services/FakeServicesManager.swift +++ b/phpmon/Domain/App/Services/FakeServicesManager.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 23/12/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/App/Services/Service.swift b/phpmon/Domain/App/Services/Service.swift index 9b822616..e94b73ca 100644 --- a/phpmon/Domain/App/Services/Service.swift +++ b/phpmon/Domain/App/Services/Service.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 23/12/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/App/Services/ServicesManager.swift b/phpmon/Domain/App/Services/ServicesManager.swift index 364c7c2d..fe45f1f7 100644 --- a/phpmon/Domain/App/Services/ServicesManager.swift +++ b/phpmon/Domain/App/Services/ServicesManager.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 11/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/App/Services/ValetServicesManager.swift b/phpmon/Domain/App/Services/ValetServicesManager.swift index be47e65b..44a02ccd 100644 --- a/phpmon/Domain/App/Services/ValetServicesManager.swift +++ b/phpmon/Domain/App/Services/ValetServicesManager.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 23/12/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -91,7 +91,7 @@ class ValetServicesManager: ServicesManager { description: "alert.service_error.extra".localized ) .withPrimary(text: "alert.service_error.button.close".localized) - .show() + .show(urgency: .bringToFront) } // If we do have a path to a log file, show a more complex alert w/ Show Log button @@ -112,6 +112,6 @@ class ValetServicesManager: ServicesManager { alert.close(with: .OK) }) - .show() + .show(urgency: .bringToFront) } } diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/App/Startup+Launch.swift similarity index 69% rename from phpmon/Domain/Menu/MainMenu+Startup.swift rename to phpmon/Domain/App/Startup+Launch.swift index fea4c0e6..fafc9659 100644 --- a/phpmon/Domain/Menu/MainMenu+Startup.swift +++ b/phpmon/Domain/App/Startup+Launch.swift @@ -1,25 +1,32 @@ // -// MainMenu+Startup.swift +// Startup+Launch.swift // PHP Monitor // -// Created by Nico Verbruggen on 03/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Created by Nico Verbruggen on 26/11/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // +import Foundation import Cocoa import NVAlert -extension MainMenu { +extension Startup { /** Kick off the startup of the rendering of the main menu. */ - func startup() async { - // Start with the icon - await MainActor.run { - self.setStatusBar(image: NSImage.statusBarIcon) - } + static func check(_ container: Container) async { + // Create a new instance of Startup w/ the container + let startup = Startup(container) - if await Startup().checkEnvironment() { + // Perform the startup checks + await startup.check() + } + + /** + Perform all checks and execute pass or fail results. + */ + private func check() async { + if await self.checkEnvironment() { await self.onEnvironmentPass() } else { await self.onEnvironmentFail() @@ -29,6 +36,7 @@ extension MainMenu { /** When the environment is all clear and the app can run, let's go. */ + @MainActor private func onEnvironmentPass() async { // Load additional preferences await container.preferences.loadCustomPreferences() @@ -60,16 +68,16 @@ extension MainMenu { await container.phpEnvs.reloadPhpVersions() // Set up the filesystem watcher for the Homebrew binaries - App.shared.prepareHomebrewWatchers() + await HomebrewWatchManager.prepare() // Check for other problems container.warningManager.evaluateWarnings() // Set up the config watchers on launch (updated automatically when switching) - App.shared.handlePhpConfigWatcher() + await ConfigWatchManager.handleWatcher() // Detect built-in and custom applications - await detectApplications() + await App.shared.detectApplications() // Load the rollback preset PresetHelper.loadRollbackPresetFromFile() @@ -150,52 +158,21 @@ extension MainMenu { /** When the environment is not OK, present an alert to inform the user. */ + @MainActor private func onEnvironmentFail() async { - Task { @MainActor [self] in - NVAlert() - .withInformation( - title: "alert.cannot_start.title".localized, - subtitle: "alert.cannot_start.subtitle".localized, - description: "alert.cannot_start.description".localized - ) - .withPrimary(text: "alert.cannot_start.retry".localized) - .withSecondary(text: "alert.cannot_start.close".localized, action: { vc in - vc.close(with: .alertSecondButtonReturn) - exit(1) - }) - .show() + NVAlert() + .withInformation( + title: "alert.cannot_start.title".localized, + subtitle: "alert.cannot_start.subtitle".localized, + description: "alert.cannot_start.description".localized + ) + .withPrimary(text: "alert.cannot_start.retry".localized) + .withSecondary(text: "alert.cannot_start.close".localized, action: { vc in + vc.close(with: .alertSecondButtonReturn) + exit(1) + }) + .show(urgency: .bringToFront) - Task { // An issue occurred, fire startup checks again after dismissal - await startup() - } - } - } - - /** - Detect which applications are installed that can be used to open a domain's source directory. - */ - private func detectApplications() async { - Log.info("Detecting applications...") - - App.shared.detectedApplications = await Application.detectPresetApplications(container) - - let customApps = Preferences.custom.scanApps?.map { appName in - return Application(container, appName, .user_supplied) - } ?? [] - - var detectedCustomApps: [Application] = [] - - for app in customApps where await app.isInstalled() { - detectedCustomApps.append(app) - } - - App.shared.detectedApplications - .append(contentsOf: detectedCustomApps) - - let appNames = App.shared.detectedApplications.map { app in - return app.name - } - - Log.info("Detected applications: \(appNames)") + await self.check() } } diff --git a/phpmon/Domain/App/Startup+Timers.swift b/phpmon/Domain/App/Startup+Timers.swift index 3d3fcbcc..738c1f0d 100644 --- a/phpmon/Domain/App/Startup+Timers.swift +++ b/phpmon/Domain/App/Startup+Timers.swift @@ -69,6 +69,6 @@ extension Startup { .withTertiary(text: "", action: { _ in NSWorkspace.shared.open(URL(string: "https://github.com/nicoverbruggen/phpmon/issues/294")!) }) - .show() + .show(urgency: .urgentRequestAttention) } } diff --git a/phpmon/Domain/App/Startup.swift b/phpmon/Domain/App/Startup.swift index 078d5f03..19245ac4 100644 --- a/phpmon/Domain/App/Startup.swift +++ b/phpmon/Domain/App/Startup.swift @@ -2,7 +2,7 @@ // Environment.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -10,6 +10,12 @@ import AppKit import NVAlert class Startup { + var container: Container + + init(_ container: Container) { + self.container = container + } + /** Checks the user's environment and checks if PHP Monitor can be used properly. This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more. @@ -68,7 +74,7 @@ class Startup { ) .withPrimary(text: check.buttonText, action: { _ in exit(1) - }).show() + }).show(urgency: .bringToFront) } NVAlert() @@ -78,7 +84,7 @@ class Startup { description: check.descriptionText ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } // MARK: - Check (List) diff --git a/phpmon/Domain/Integrations/Analytics/LoggableEvent.swift b/phpmon/Domain/Integrations/Analytics/LoggableEvent.swift deleted file mode 100644 index 373105af..00000000 --- a/phpmon/Domain/Integrations/Analytics/LoggableEvent.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// LoggableEvent.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 12/09/2025. -// Copyright © 2025 Nico Verbruggen. All rights reserved. -// - -// TODO: Add anonymous analytics system -// Batch events and dispatch them every hour. -// Reset the counts when send successfully. -// That's the plan. Currently not implemented! -// Also, there should be an opt-out. - -enum LoggableEvent: String { - case menuOpened = "menu_opened" - - case phpVersionSwitched = "php_version_switched" - - case openedDomainManagement = "opened_domain_management" - case openedPhpInstallations = "opened_php_installations" - case openedPhpExtensions = "opened_php_extensions" - - case openedSettings = "opened_settings" - - // TODO: Add more tracked things. -} diff --git a/phpmon/Domain/Integrations/Common/RCFile.swift b/phpmon/Domain/Integrations/Common/RCFile.swift index 84057f24..2ef0b365 100644 --- a/phpmon/Domain/Integrations/Common/RCFile.swift +++ b/phpmon/Domain/Integrations/Common/RCFile.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 24/01/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Composer/ComposerJson.swift b/phpmon/Domain/Integrations/Composer/ComposerJson.swift index 29bf8665..bbf28bf8 100644 --- a/phpmon/Domain/Integrations/Composer/ComposerJson.swift +++ b/phpmon/Domain/Integrations/Composer/ComposerJson.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Composer/ComposerWindow.swift b/phpmon/Domain/Integrations/Composer/ComposerWindow.swift index 4f827d7e..2371ac1a 100644 --- a/phpmon/Domain/Integrations/Composer/ComposerWindow.swift +++ b/phpmon/Domain/Integrations/Composer/ComposerWindow.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 08/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -77,7 +77,7 @@ import NVAlert withTimeout: .minutes(5) ) - if process.terminationStatus <= 0 { + if process.terminationStatus == 0 { composerUpdateSucceeded() } else { composerUpdateFailed() @@ -137,7 +137,7 @@ import NVAlert description: "alert.composer_missing.desc".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } deinit { diff --git a/phpmon/Domain/Integrations/Composer/ProjectTypeDetection.swift b/phpmon/Domain/Integrations/Composer/ProjectTypeDetection.swift index 5f2dafab..abb026db 100644 --- a/phpmon/Domain/Integrations/Composer/ProjectTypeDetection.swift +++ b/phpmon/Domain/Integrations/Composer/ProjectTypeDetection.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 26/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/Behaviors/BrewPermissionFixer.swift b/phpmon/Domain/Integrations/Homebrew/Behaviors/BrewPermissionFixer.swift index db6e5c18..b070e7f7 100644 --- a/phpmon/Domain/Integrations/Homebrew/Behaviors/BrewPermissionFixer.swift +++ b/phpmon/Domain/Integrations/Homebrew/Behaviors/BrewPermissionFixer.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 23/04/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/Brew.swift b/phpmon/Domain/Integrations/Homebrew/Brew.swift index 2097133b..6b1fca49 100644 --- a/phpmon/Domain/Integrations/Homebrew/Brew.swift +++ b/phpmon/Domain/Integrations/Homebrew/Brew.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 17/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/BrewDiagnostics.swift b/phpmon/Domain/Integrations/Homebrew/BrewDiagnostics.swift index 3fc52c73..c6c3266a 100644 --- a/phpmon/Domain/Integrations/Homebrew/BrewDiagnostics.swift +++ b/phpmon/Domain/Integrations/Homebrew/BrewDiagnostics.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 28/11/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift b/phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift index 36d6fec2..368b6876 100644 --- a/phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift +++ b/phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 27/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/BrewPhpFormula.swift b/phpmon/Domain/Integrations/Homebrew/BrewPhpFormula.swift index 591804de..1224a1c5 100644 --- a/phpmon/Domain/Integrations/Homebrew/BrewPhpFormula.swift +++ b/phpmon/Domain/Integrations/Homebrew/BrewPhpFormula.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 17/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/BrewPhpFormulaeHandler.swift b/phpmon/Domain/Integrations/Homebrew/BrewPhpFormulaeHandler.swift index bc1870c9..598d3ce5 100644 --- a/phpmon/Domain/Integrations/Homebrew/BrewPhpFormulaeHandler.swift +++ b/phpmon/Domain/Integrations/Homebrew/BrewPhpFormulaeHandler.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift b/phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift index b98d3f6a..cd5ae594 100644 --- a/phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift +++ b/phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 01/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift index 04a7fb9c..0dcae6b6 100644 --- a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift +++ b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/02/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift index b1d88c01..f1148180 100644 --- a/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -84,23 +84,35 @@ extension BrewCommand { _ onProgress: @escaping (BrewCommandProgress) -> Void ) async throws { var loggedMessages: [String] = [] + let (process, _): (Process, ShellOutput) - let (process, _) = try! await shell.attach( - command, - didReceiveOutput: { text, _ in - if !text.isEmpty { - Log.perf(text) - loggedMessages.append(text) - } + do { + (process, _) = try await shell.attach( + command, + didReceiveOutput: { text, _ in + if !text.isEmpty { + Log.perf(text) + loggedMessages.append(text) + } - if let (number, text) = self.reportInstallationProgress(text) { - onProgress(.create(value: number, title: getCommandTitle(), description: text)) - } - }, - withTimeout: .minutes(15) - ) + if let (number, text) = self.reportInstallationProgress(text) { + onProgress(.create(value: number, title: getCommandTitle(), description: text)) + } + }, + withTimeout: .minutes(15) + ) + } catch ShellError.timedOut { + // Possible if the brew command times out + Log.err("The `brew` command timed out after 15 minutes: \(command)") + throw BrewCommandError(error: "The command timed out after 15 minutes.", log: loggedMessages) + } catch { + // Possible if the async continuation fails + Log.err("Failed to execute brew command: \(command) - \(error)") + throw BrewCommandError(error: "Failed to execute command: \(error.localizedDescription)", log: loggedMessages) + } - if process.terminationStatus <= 0 { + // Finally, even if we got the command to execute, let's check the termination status + if process.terminationStatus == 0 { loggedMessages = [] return } else { diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/PHP Extensions/InstallPhpExtensionCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/PHP Extensions/InstallPhpExtensionCommand.swift index 44b832ca..af5cf85b 100644 --- a/phpmon/Domain/Integrations/Homebrew/Commands/PHP Extensions/InstallPhpExtensionCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Commands/PHP Extensions/InstallPhpExtensionCommand.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/PHP Extensions/RemovePhpExtensionCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/PHP Extensions/RemovePhpExtensionCommand.swift index 1727f3bc..6e598537 100644 --- a/phpmon/Domain/Integrations/Homebrew/Commands/PHP Extensions/RemovePhpExtensionCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Commands/PHP Extensions/RemovePhpExtensionCommand.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -64,7 +64,7 @@ class RemovePhpExtensionCommand: BrewCommand { withTimeout: .minutes(5) ) - if process.terminationStatus <= 0 { + if process.terminationStatus == 0 { onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized)) if let ext = existing { diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/PHP Versions/ModifyPhpVersionCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/PHP Versions/ModifyPhpVersionCommand.swift index 277f0ee9..1749f3a7 100644 --- a/phpmon/Domain/Integrations/Homebrew/Commands/PHP Versions/ModifyPhpVersionCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Commands/PHP Versions/ModifyPhpVersionCommand.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 28/04/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/PHP Versions/RemovePhpVersionCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/PHP Versions/RemovePhpVersionCommand.swift index ecafa759..bab21c76 100644 --- a/phpmon/Domain/Integrations/Homebrew/Commands/PHP Versions/RemovePhpVersionCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Commands/PHP Versions/RemovePhpVersionCommand.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -71,7 +71,7 @@ class RemovePhpVersionCommand: BrewCommand { withTimeout: .minutes(5) ) - if process.terminationStatus <= 0 { + if process.terminationStatus == 0 { onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized)) _ = await container.phpEnvs.detectPhpVersions() diff --git a/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift b/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift index f17a3572..a478d7fd 100644 --- a/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift b/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift index f8c097e7..1f05bd5d 100644 --- a/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift +++ b/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 15/03/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Packagist/ValetUpgrader.swift b/phpmon/Domain/Integrations/Packagist/ValetUpgrader.swift index ef6b3247..00664f66 100644 --- a/phpmon/Domain/Integrations/Packagist/ValetUpgrader.swift +++ b/phpmon/Domain/Integrations/Packagist/ValetUpgrader.swift @@ -81,7 +81,7 @@ class ValetUpgrader { .withPrimary(text: "generic.ok".localized, action: { vc in vc.close(with: .OK) }) - .show() + .show(urgency: .bringToFront) } @MainActor private static func notifyAboutUpgrade(latest: String, constraint: String, passing: Bool) { @@ -104,6 +104,6 @@ class ValetUpgrader { }) } - alert.show() + alert.show(urgency: .bringToFront) } } diff --git a/phpmon/Domain/Integrations/Valet/Domains/FakeValetInteractor.swift b/phpmon/Domain/Integrations/Valet/Domains/FakeValetInteractor.swift index 2636e18e..d481962e 100644 --- a/phpmon/Domain/Integrations/Valet/Domains/FakeValetInteractor.swift +++ b/phpmon/Domain/Integrations/Valet/Domains/FakeValetInteractor.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 13/12/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Domains/ValetInteractor.swift b/phpmon/Domain/Integrations/Valet/Domains/ValetInteractor.swift index b4edfaa1..333d9038 100644 --- a/phpmon/Domain/Integrations/Valet/Domains/ValetInteractor.swift +++ b/phpmon/Domain/Integrations/Valet/Domains/ValetInteractor.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Domains/ValetListable.swift b/phpmon/Domain/Integrations/Valet/Domains/ValetListable.swift index 625a84fb..6e4857c8 100644 --- a/phpmon/Domain/Integrations/Valet/Domains/ValetListable.swift +++ b/phpmon/Domain/Integrations/Valet/Domains/ValetListable.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 12/04/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Proxies/FakeValetProxy.swift b/phpmon/Domain/Integrations/Valet/Proxies/FakeValetProxy.swift index 05dc5a0a..a0e4799d 100644 --- a/phpmon/Domain/Integrations/Valet/Proxies/FakeValetProxy.swift +++ b/phpmon/Domain/Integrations/Valet/Proxies/FakeValetProxy.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/12/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift b/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift index 6df80906..95873ae9 100644 --- a/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift +++ b/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 30/03/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Scanners/DomainScanner.swift b/phpmon/Domain/Integrations/Valet/Scanners/DomainScanner.swift index 557402c5..44ebfb41 100644 --- a/phpmon/Domain/Integrations/Valet/Scanners/DomainScanner.swift +++ b/phpmon/Domain/Integrations/Valet/Scanners/DomainScanner.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 02/04/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Scanners/FakeDomainScanner.swift b/phpmon/Domain/Integrations/Valet/Scanners/FakeDomainScanner.swift index 842e2f4e..58fa9f3b 100644 --- a/phpmon/Domain/Integrations/Valet/Scanners/FakeDomainScanner.swift +++ b/phpmon/Domain/Integrations/Valet/Scanners/FakeDomainScanner.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 02/04/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // class FakeDomainScanner: DomainScanner { diff --git a/phpmon/Domain/Integrations/Valet/Scanners/ValetDomainScanner.swift b/phpmon/Domain/Integrations/Valet/Scanners/ValetDomainScanner.swift index 56ea6af3..086649e6 100644 --- a/phpmon/Domain/Integrations/Valet/Scanners/ValetDomainScanner.swift +++ b/phpmon/Domain/Integrations/Valet/Scanners/ValetDomainScanner.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 02/04/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Scanners/ValetScanners.swift b/phpmon/Domain/Integrations/Valet/Scanners/ValetScanners.swift index 2f155d61..39933966 100644 --- a/phpmon/Domain/Integrations/Valet/Scanners/ValetScanners.swift +++ b/phpmon/Domain/Integrations/Valet/Scanners/ValetScanners.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 01/11/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Sites/CertificateValidator.swift b/phpmon/Domain/Integrations/Valet/Sites/CertificateValidator.swift index 6346478b..ce0948b5 100644 --- a/phpmon/Domain/Integrations/Valet/Sites/CertificateValidator.swift +++ b/phpmon/Domain/Integrations/Valet/Sites/CertificateValidator.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Assistant on 29/10/2025. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Sites/FakeValetSite.swift b/phpmon/Domain/Integrations/Valet/Sites/FakeValetSite.swift index 0e93650a..70bcc9a4 100644 --- a/phpmon/Domain/Integrations/Valet/Sites/FakeValetSite.swift +++ b/phpmon/Domain/Integrations/Valet/Sites/FakeValetSite.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 19/03/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Sites/PhpVersionSource.swift b/phpmon/Domain/Integrations/Valet/Sites/PhpVersionSource.swift index de4f9491..d1aa2875 100644 --- a/phpmon/Domain/Integrations/Valet/Sites/PhpVersionSource.swift +++ b/phpmon/Domain/Integrations/Valet/Sites/PhpVersionSource.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 25/01/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift b/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift index 2d0808c7..5ff8e6f2 100644 --- a/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift +++ b/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 22/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift b/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift index e790d1f7..3880f04a 100644 --- a/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift +++ b/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -27,7 +27,7 @@ extension Valet { Preferences.update(.warnAboutNonStandardTLD, value: false) alert.close(with: .alertThirdButtonReturn) }) - .show() + .show(urgency: .urgentRequestAttention) } } } @@ -43,7 +43,7 @@ extension Valet { ) ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .urgentRequestAttention) } } @@ -68,7 +68,7 @@ extension Valet { description: "alert.php_fpm_broken.description".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .urgentRequestAttention) } } diff --git a/phpmon/Domain/Integrations/Valet/Valet.swift b/phpmon/Domain/Integrations/Valet/Valet.swift index da1d9152..93821c01 100644 --- a/phpmon/Domain/Integrations/Valet/Valet.swift +++ b/phpmon/Domain/Integrations/Valet/Valet.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 29/11/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Menu/AppMenu.swift b/phpmon/Domain/Menu/AppMenu.swift index f5f15022..6630dd52 100644 --- a/phpmon/Domain/Menu/AppMenu.swift +++ b/phpmon/Domain/Menu/AppMenu.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 27/06/2024. -// Copyright © 2024 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Domain/Menu/MainMenu+Actions.swift b/phpmon/Domain/Menu/MainMenu+Actions.swift index 139bb6e4..9308cd40 100644 --- a/phpmon/Domain/Menu/MainMenu+Actions.swift +++ b/phpmon/Domain/Menu/MainMenu+Actions.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 19/05/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa @@ -27,7 +27,7 @@ extension MainMenu { description: "phpman.unlinked.detail".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } } @@ -40,7 +40,7 @@ extension MainMenu { ) .withPrimary(text: "alert.fix_homebrew_permissions.ok".localized) .withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized) - .didSelectPrimary() { + .didSelectPrimary(urgency: .bringToFront) { return } @@ -54,7 +54,7 @@ extension MainMenu { description: "alert.fix_homebrew_permissions_done.desc".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } failure: { error in NVAlert.show(for: error as! HomebrewPermissionError) } @@ -110,14 +110,16 @@ extension MainMenu { return } - do { - try file.replace(key: "xdebug.mode", value: "off") + Task { + do { + try await file.replace(key: "xdebug.mode", value: "off") - Log.perf("Refreshing menu...") - MainMenu.shared.rebuild() - restartPhpFpm() - } catch { - Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)") + Log.perf("Refreshing menu...") + MainMenu.shared.rebuild() + restartPhpFpm() + } catch { + Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath).") + } } } @@ -128,27 +130,29 @@ extension MainMenu { return Log.info("xdebug.mode could not be found in any .ini file, aborting.") } - do { - var modes = Xdebug(container).activeModes + Task { + do { + var modes = Xdebug(container).activeModes - if let index = modes.firstIndex(of: sender.mode) { - modes.remove(at: index) - } else { - modes.append(sender.mode) + if let index = modes.firstIndex(of: sender.mode) { + modes.remove(at: index) + } else { + modes.append(sender.mode) + } + + var newValue = modes.joined(separator: ",") + if newValue.isEmpty { + newValue = "off" + } + + try await file.replace(key: "xdebug.mode", value: newValue) + + Log.perf("Refreshing menu...") + MainMenu.shared.rebuild() + restartPhpFpm() + } catch { + Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath).") } - - var newValue = modes.joined(separator: ",") - if newValue.isEmpty { - newValue = "off" - } - - try file.replace(key: "xdebug.mode", value: newValue) - - Log.perf("Refreshing menu...") - MainMenu.shared.rebuild() - restartPhpFpm() - } catch { - Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)") } } @@ -186,7 +190,7 @@ extension MainMenu { self.performRollback() }) .withSecondary(text: "alert.revert_description.cancel".localized) - .show() + .show(urgency: .bringToFront) } @objc func togglePreset(sender: PresetMenuItem) { @@ -206,7 +210,7 @@ extension MainMenu { NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions) alert.close(with: .OK) }) - .show() + .show(urgency: .bringToFront) } @objc func openPhpInfo() { @@ -262,13 +266,13 @@ extension MainMenu { if container.phpEnvs.availablePhpVersions.contains(version) { Task { MainMenu.shared.switchToPhpVersion(version) } } else { - Task { + Task { @MainActor in NVAlert().withInformation( title: "alert.php_switch_unavailable.title".localized, subtitle: "alert.php_switch_unavailable.subtitle".localized(version) ).withPrimary( text: "alert.php_switch_unavailable.ok".localized - ).show() + ).show(urgency: .bringToFront) } } } @@ -292,7 +296,7 @@ extension MainMenu { await PhpEnvironments.switcher.performSwitch(to: version) container.phpEnvs.currentInstall = ActivePhpInstallation(container) - App.shared.handlePhpConfigWatcher() + await ConfigWatchManager.handleWatcher() container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version) } @@ -307,7 +311,7 @@ extension MainMenu { await PhpEnvironments.switcher.performSwitch(to: version) container.phpEnvs.currentInstall = ActivePhpInstallation(container) - App.shared.handlePhpConfigWatcher() + await ConfigWatchManager.handleWatcher() container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version) } } @@ -333,7 +337,7 @@ extension MainMenu { await PhpEnvironments.switcher.performSwitch(to: version) container.phpEnvs.currentInstall = ActivePhpInstallation(container) - App.shared.handlePhpConfigWatcher() + await ConfigWatchManager.handleWatcher() container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version) } diff --git a/phpmon/Domain/Menu/MainMenu+Async.swift b/phpmon/Domain/Menu/MainMenu+Async.swift index 14ff0d57..b7b6cee4 100644 --- a/phpmon/Domain/Menu/MainMenu+Async.swift +++ b/phpmon/Domain/Menu/MainMenu+Async.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 06/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Menu/MainMenu+FixMyValet.swift b/phpmon/Domain/Menu/MainMenu+FixMyValet.swift index 15009303..0e095d57 100644 --- a/phpmon/Domain/Menu/MainMenu+FixMyValet.swift +++ b/phpmon/Domain/Menu/MainMenu+FixMyValet.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 20/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -31,7 +31,7 @@ extension MainMenu { ) .withPrimary(text: "alert.fix_my_valet.ok".localized) .withSecondary(text: "alert.fix_my_valet.cancel".localized) - .didSelectPrimary() { + .didSelectPrimary(urgency: .bringToFront) { Log.info("The user has chosen to abort Fix My Valet") return } @@ -54,7 +54,7 @@ extension MainMenu { subtitle: "alert.php_formula_missing.info".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } @MainActor private func presentAlertForSameVersion() { @@ -65,7 +65,7 @@ extension MainMenu { description: "alert.fix_my_valet_done.desc".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } @MainActor private func presentAlertForDifferentVersion(version: String) { @@ -87,7 +87,7 @@ extension MainMenu { .withTertiary(text: "", action: { _ in NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions) }) - .show() + .show(urgency: .bringToFront) } } diff --git a/phpmon/Domain/Menu/MainMenu+Switcher.swift b/phpmon/Domain/Menu/MainMenu+Switcher.swift index 1221964a..0284449a 100644 --- a/phpmon/Domain/Menu/MainMenu+Switcher.swift +++ b/phpmon/Domain/Menu/MainMenu+Switcher.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 08/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -84,7 +84,7 @@ extension MainMenu { ) .withPrimary(text: "alert.php_switch_failed.confirm".localized) .withSecondary(text: "alert.php_switch_failed.cancel".localized) - .didSelectPrimary() + .didSelectPrimary(urgency: .bringToFront) if outcome { MainMenu.shared.fixMyValet() } @@ -113,7 +113,7 @@ extension MainMenu { alert.close(with: .OK) self.terminateApp() }) - .show() + .show(urgency: .bringToFront) } private func reloadDomainListData() async { diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index 4085edfe..d4ddb55a 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -2,7 +2,7 @@ // MainMenu.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa @@ -81,6 +81,17 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate } } + /** + Dismisses the main menu if it's open. + */ + func dismissMenu(animated: Bool = true) { + if animated { + self.statusItem.menu?.cancelTracking() + } else { + self.statusItem.menu?.cancelTrackingWithoutAnimation() + } + } + // MARK: - User Interface /** Reloads which PHP versions is currently active. */ @@ -140,7 +151,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate description: "startup.unsupported_versions_explanation.desc".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } } @@ -206,7 +217,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate description: "lite_mode_explanation.description".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } } diff --git a/phpmon/Domain/Menu/StatusMenu+Items.swift b/phpmon/Domain/Menu/StatusMenu+Items.swift index 0fcd0e46..7f7c408e 100644 --- a/phpmon/Domain/Menu/StatusMenu+Items.swift +++ b/phpmon/Domain/Menu/StatusMenu+Items.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 18/08/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index 7e4a896e..fae30589 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -2,7 +2,7 @@ // MainMenuBuilder.swift // PHP Monitor // -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Domain/PHP/PhpGuard.swift b/phpmon/Domain/PHP/PhpGuard.swift index c93b5f60..4efb00a1 100644 --- a/phpmon/Domain/PHP/PhpGuard.swift +++ b/phpmon/Domain/PHP/PhpGuard.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 25/04/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -69,7 +69,7 @@ class PhpGuard { Stats.persistCurrentGlobalPhpVersion(version: currentVersion) alert.close(with: .OK) }) - .show() + .show(urgency: .urgentRequestAttention) } } } diff --git a/phpmon/Domain/Preferences/CustomPrefs.swift b/phpmon/Domain/Preferences/CustomPrefs.swift index c075b8c9..2ba5a4af 100644 --- a/phpmon/Domain/Preferences/CustomPrefs.swift +++ b/phpmon/Domain/Preferences/CustomPrefs.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 03/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Preferences/Keybinds/GlobalKeybindPreference.swift b/phpmon/Domain/Preferences/Keybinds/GlobalKeybindPreference.swift index f12b0e2b..baadc887 100644 --- a/phpmon/Domain/Preferences/Keybinds/GlobalKeybindPreference.swift +++ b/phpmon/Domain/Preferences/Keybinds/GlobalKeybindPreference.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 15/04/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Preferences/Keys.swift b/phpmon/Domain/Preferences/Keys.swift index 366a6ff8..2e3d39b2 100644 --- a/phpmon/Domain/Preferences/Keys.swift +++ b/phpmon/Domain/Preferences/Keys.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 25/07/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Preferences/MenuBarIcons.swift b/phpmon/Domain/Preferences/MenuBarIcons.swift index 0cb95a12..603bf33d 100644 --- a/phpmon/Domain/Preferences/MenuBarIcons.swift +++ b/phpmon/Domain/Preferences/MenuBarIcons.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 06/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Preferences/PreferenceName.swift b/phpmon/Domain/Preferences/PreferenceName.swift index 9689004c..f3201fff 100644 --- a/phpmon/Domain/Preferences/PreferenceName.swift +++ b/phpmon/Domain/Preferences/PreferenceName.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 07/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // /** diff --git a/phpmon/Domain/Preferences/Preferences.swift b/phpmon/Domain/Preferences/Preferences.swift index da90abed..1c1c9461 100644 --- a/phpmon/Domain/Preferences/Preferences.swift +++ b/phpmon/Domain/Preferences/Preferences.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 30/03/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Preferences/PreferencesTabs.swift b/phpmon/Domain/Preferences/PreferencesTabs.swift index 6f84a559..8da01fb3 100644 --- a/phpmon/Domain/Preferences/PreferencesTabs.swift +++ b/phpmon/Domain/Preferences/PreferencesTabs.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 22/04/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Preferences/PreferencesVC.swift b/phpmon/Domain/Preferences/PreferencesVC.swift index 72e45618..02f65302 100644 --- a/phpmon/Domain/Preferences/PreferencesVC.swift +++ b/phpmon/Domain/Preferences/PreferencesVC.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 30/03/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Domain/Preferences/PreferencesWindowController+Hotkey.swift b/phpmon/Domain/Preferences/PreferencesWindowController+Hotkey.swift index f5d7338a..1c485f39 100644 --- a/phpmon/Domain/Preferences/PreferencesWindowController+Hotkey.swift +++ b/phpmon/Domain/Preferences/PreferencesWindowController+Hotkey.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 25/07/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Domain/Preferences/PreferencesWindowController.swift b/phpmon/Domain/Preferences/PreferencesWindowController.swift index c925cab2..5e054377 100644 --- a/phpmon/Domain/Preferences/PreferencesWindowController.swift +++ b/phpmon/Domain/Preferences/PreferencesWindowController.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 02/04/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Domain/Preferences/Stats.swift b/phpmon/Domain/Preferences/Stats.swift index 7267ea05..2c63916d 100644 --- a/phpmon/Domain/Preferences/Stats.swift +++ b/phpmon/Domain/Preferences/Stats.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 29/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -135,7 +135,7 @@ class Stats { .withTertiary(text: "", action: { vc in vc.close(with: .alertThirdButtonReturn) NSWorkspace.shared.open(Constants.Urls.DonationPage) - }).didSelectPrimary() + }).didSelectPrimary(urgency: .normalRequestAttention) if donate { Log.info("The user is an absolute badass for choosing this option. Thank you.") diff --git a/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift b/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift index a5e8f3e5..cf89d37f 100644 --- a/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift +++ b/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 17/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Preferences/Views/HotkeyPreferenceView.swift b/phpmon/Domain/Preferences/Views/HotkeyPreferenceView.swift index 1c77d7a7..b76fcd86 100644 --- a/phpmon/Domain/Preferences/Views/HotkeyPreferenceView.swift +++ b/phpmon/Domain/Preferences/Views/HotkeyPreferenceView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 17/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift b/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift index e1dbbe99..80cb942b 100644 --- a/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift +++ b/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 06/02/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Presets/Preset.swift b/phpmon/Domain/Presets/Preset.swift index 61f2fb3e..cb243dc0 100644 --- a/phpmon/Domain/Presets/Preset.swift +++ b/phpmon/Domain/Presets/Preset.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 31/05/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -90,7 +90,7 @@ struct Preset: Codable, Equatable { // Apply the configuration changes first for conf in configuration { - applyConfigurationValue(key: conf.key, value: conf.value ?? "") + await applyConfigurationValue(key: conf.key, value: conf.value ?? "") } guard let install = container.phpEnvs.phpInstall else { @@ -153,13 +153,13 @@ struct Preset: Codable, Equatable { ) ).withPrimary( text: "alert.php_switch_unavailable.ok".localized - ).show() + ).show(urgency: .bringToFront) } return false } } - private func applyConfigurationValue(key: String, value: String) { + private func applyConfigurationValue(key: String, value: String) async { guard let file = container.phpEnvs.getConfigFile(forKey: key) else { return } @@ -167,7 +167,7 @@ struct Preset: Codable, Equatable { do { if file.has(key: key) { Log.info("Setting config value \(key) in \(file.filePath)") - try file.replace(key: key, value: value) + try await file.replace(key: key, value: value) } } catch { Log.err("Setting \(key) to \(value) failed.") diff --git a/phpmon/Domain/Presets/PresetHelper.swift b/phpmon/Domain/Presets/PresetHelper.swift index a0471fa4..97496b36 100644 --- a/phpmon/Domain/Presets/PresetHelper.swift +++ b/phpmon/Domain/Presets/PresetHelper.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 02/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/SwiftUI/Common/BlockingOverlayView.swift b/phpmon/Domain/SwiftUI/Common/BlockingOverlayView.swift index 59def65f..0b6777b5 100644 --- a/phpmon/Domain/SwiftUI/Common/BlockingOverlayView.swift +++ b/phpmon/Domain/SwiftUI/Common/BlockingOverlayView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 19/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift b/phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift index 319b1406..9ce44674 100644 --- a/phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift +++ b/phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 15/03/2024. -// Copyright © 2024 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI diff --git a/phpmon/Domain/SwiftUI/Common/HelpButton.swift b/phpmon/Domain/SwiftUI/Common/HelpButton.swift index f5a68b5f..46f06927 100644 --- a/phpmon/Domain/SwiftUI/Common/HelpButton.swift +++ b/phpmon/Domain/SwiftUI/Common/HelpButton.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 07/01/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/SwiftUI/Common/SwiftUIHelper.swift b/phpmon/Domain/SwiftUI/Common/SwiftUIHelper.swift index a60c4234..b71b5620 100644 --- a/phpmon/Domain/SwiftUI/Common/SwiftUIHelper.swift +++ b/phpmon/Domain/SwiftUI/Common/SwiftUIHelper.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 08/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift b/phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift index b852a2f2..6d4584f8 100644 --- a/phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift +++ b/phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 19/03/2024. -// Copyright © 2024 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI diff --git a/phpmon/Domain/SwiftUI/Domains/VersionPopoverView.swift b/phpmon/Domain/SwiftUI/Domains/VersionPopoverView.swift index 4a5cf148..f53afca7 100644 --- a/phpmon/Domain/SwiftUI/Domains/VersionPopoverView.swift +++ b/phpmon/Domain/SwiftUI/Domains/VersionPopoverView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 08/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI diff --git a/phpmon/Domain/SwiftUI/Menu/HeaderView.swift b/phpmon/Domain/SwiftUI/Menu/HeaderView.swift index 07997625..884982b3 100644 --- a/phpmon/Domain/SwiftUI/Menu/HeaderView.swift +++ b/phpmon/Domain/SwiftUI/Menu/HeaderView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 10/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI @@ -24,6 +24,7 @@ struct HeaderView: View { // MARK: - NSMenuItem + @MainActor static func asMenuItem( text: String, minimumWidth: CGFloat? = nil diff --git a/phpmon/Domain/SwiftUI/Menu/SectionHeaderView.swift b/phpmon/Domain/SwiftUI/Menu/SectionHeaderView.swift index 01e2f2aa..7d1534b1 100644 --- a/phpmon/Domain/SwiftUI/Menu/SectionHeaderView.swift +++ b/phpmon/Domain/SwiftUI/Menu/SectionHeaderView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 10/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI diff --git a/phpmon/Domain/SwiftUI/Menu/ServicesView.swift b/phpmon/Domain/SwiftUI/Menu/ServicesView.swift index b245211b..c13a4fe8 100644 --- a/phpmon/Domain/SwiftUI/Menu/ServicesView.swift +++ b/phpmon/Domain/SwiftUI/Menu/ServicesView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 10/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -12,6 +12,7 @@ import NVAlert struct ServicesView: View { + @MainActor static func asMenuItem(perRow: Int = 4) -> NSMenuItem { let view = { let rootView = Self(manager: ServicesManager.shared, perRow: perRow) @@ -88,7 +89,7 @@ struct ServicesView: View { description: "alert.\(type).desc".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } } } @@ -103,6 +104,13 @@ struct ServiceView: View { var service: Service @State var isBusy: Bool = false + @MainActor + private func toggleService() async { + isBusy = true + await ServicesManager.shared.toggleService(named: service.name) + isBusy = false + } + var body: some View { VStack(alignment: .center, spacing: 0) { Text(service.name.uppercased()) @@ -123,7 +131,7 @@ struct ServiceView: View { description: "alert.warnings.service_missing.description".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } } label: { Text("?") @@ -133,11 +141,7 @@ struct ServiceView: View { } if service.status == .error { Button { - Task { - isBusy = true - await ServicesManager.shared.toggleService(named: service.name) - isBusy = false - } + Task { await toggleService() } } label: { Text("E") .frame(width: 12.0, height: 12.0) @@ -148,11 +152,7 @@ struct ServiceView: View { } if service.status == .active || service.status == .inactive { Button { - Task { - isBusy = true - await ServicesManager.shared.toggleService(named: service.name) - isBusy = false - } + Task { await toggleService() } } label: { Image( systemName: service.status == .active ? "checkmark" : "xmark" diff --git a/phpmon/Domain/SwiftUI/Menu/StatsView.swift b/phpmon/Domain/SwiftUI/Menu/StatsView.swift index 030debb8..2874194d 100644 --- a/phpmon/Domain/SwiftUI/Menu/StatsView.swift +++ b/phpmon/Domain/SwiftUI/Menu/StatsView.swift @@ -3,13 +3,14 @@ // PHP Monitor // // Created by Nico Verbruggen on 09/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI struct StatsView: View { + @MainActor static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem { let item = NSMenuItem() let view = NSHostingView( @@ -84,6 +85,7 @@ struct StatsView: View { Divider().hidden() Button { Task { @MainActor in + MainMenu.shared.dismissMenu() MainMenu.shared.openConfigGUI() } } label: { diff --git a/phpmon/Domain/Terminal Alert/ProgressVC.swift b/phpmon/Domain/Terminal Alert/ProgressVC.swift index 74829cd4..7db4b150 100644 --- a/phpmon/Domain/Terminal Alert/ProgressVC.swift +++ b/phpmon/Domain/Terminal Alert/ProgressVC.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 26/07/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Terminal Alert/TerminalProgressWindowController.swift b/phpmon/Domain/Terminal Alert/TerminalProgressWindowController.swift index 5d4d3d3e..217c5530 100644 --- a/phpmon/Domain/Terminal Alert/TerminalProgressWindowController.swift +++ b/phpmon/Domain/Terminal Alert/TerminalProgressWindowController.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 18/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Domain/Watcher/App+BrewWatch.swift b/phpmon/Domain/Watcher/App+BrewWatch.swift deleted file mode 100644 index 692477fb..00000000 --- a/phpmon/Domain/Watcher/App+BrewWatch.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// App+BrewWatch.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 03/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import Foundation - -extension App { - - public func prepareHomebrewWatchers() { - let notifier = FSNotifier( - for: URL(fileURLWithPath: container.paths.binPath), - eventMask: .all, - onChange: { Task { await self.onHomebrewPhpModification() } } - ) - - App.shared.watchers["homebrewBinaries"] = notifier - } - - public func destroyHomebrewWatchers() { - // Removing requires termination and then removing reference - self.watchers["homebrewBinaries"]?.terminate() - self.watchers["homebrewBinaries"] = nil - } - - public func onHomebrewPhpModification() async { - // let previous = App.shared.container.phpEnvs.currentInstall?.version.text - Log.info("Something changed in the Homebrew binary directory...") - await container.phpEnvs.reloadPhpVersions() - await MainMenu.shared.refreshActiveInstallation() - - // - // TODO: PHP Guard 2.0 - // Check if the new and previous version of PHP are different - // if so, we can show a notification if needed or alert the user - // - // let new = App.shared.container.phpEnvs.currentInstall?.version.text - // - } -} diff --git a/phpmon/Domain/Watcher/App+ConfigWatch.swift b/phpmon/Domain/Watcher/App+ConfigWatch.swift deleted file mode 100644 index 43e06a93..00000000 --- a/phpmon/Domain/Watcher/App+ConfigWatch.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// App+ConfigWatch.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 30/03/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import Foundation - -extension App { - - func startWatchManager(_ url: URL) { - Log.perf("Starting config watch manager...") - self.watchManager = ConfigWatchManager(for: url) - - self.watchManager.didChange = { url in - Log.perf("Something has changed in: \(url)") - - // Check if the watcher has last updated the menu less than 0.75s ago - let distance = self.watchManager.lastUpdate?.distance(to: Date().timeIntervalSince1970) - if distance == nil || distance != nil && distance! > 0.75 { - Log.perf("Refreshing menu...") - Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() } - self.watchManager.lastUpdate = Date().timeIntervalSince1970 - } - } - } - - func handlePhpConfigWatcher(forceReload: Bool = false) { - if container.filesystem is TestableFileSystem { - Log.warn("Config watch manager is disabled when using testable filesystem.") - return - } - - guard let install = container.phpEnvs.phpInstall else { - Log.info("It appears as if no PHP installation is currently active.") - Log.info("The config watch manager be disabled until a PHP install is active.") - return - } - - let url = URL(fileURLWithPath: "\(container.paths.etcPath)/php/\(install.version.short)") - - // Check whether the manager exists and schedule on the main thread - // if we don't consistently do this, the app will create duplicate watchers - // due to timing issues, which creates retain cycles - Task { @MainActor in - // Watcher needs to be created - if self.watchManager == nil { - self.startWatchManager(url) - } - - // Watcher needs to be updated - if self.watchManager.url != url || forceReload { - self.watchManager.disable() - self.watchManager = nil - Log.perf("Watcher has stopped watching files. Starting new one...") - self.startWatchManager(url) - } - } - } - -} diff --git a/phpmon/Domain/Watcher/ConfigFSNotifier.swift b/phpmon/Domain/Watcher/ConfigFSNotifier.swift deleted file mode 100644 index d2d15bb9..00000000 --- a/phpmon/Domain/Watcher/ConfigFSNotifier.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// ConfigFSNotifier.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 24/10/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import Foundation - -class ConfigFSNotifier { - - enum Behaviour { - case reloadsMenu - case reloadsWatchers - } - - private var parent: ConfigWatchManager! - - private var monitoredFolderFileDescriptor: CInt = -1 - - private var folderMonitorSource: DispatchSourceFileSystemObject? - - let url: URL - - init( - for url: URL, - eventMask: DispatchSource.FileSystemEvent, - parent: ConfigWatchManager, - behaviour: ConfigFSNotifier.Behaviour = .reloadsMenu - ) { - self.url = url - self.parent = parent - self.startMonitoring(eventMask, behaviour: behaviour) - } - - func startMonitoring( - _ eventMask: DispatchSource.FileSystemEvent, - behaviour: ConfigFSNotifier.Behaviour - ) { - // Ensure our starting state is correct, we may already be monitoring! - guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else { - return - } - - // We'll try to open a file descriptor and validate it - monitoredFolderFileDescriptor = open(url.path, O_EVTONLY) - - // If our file descriptor here is still -1, there may have been an issue and we abort - guard monitoredFolderFileDescriptor >= 0 else { - Log.err("Failed to open file descriptor for \(url.path), not monitoring.") - return - } - - // Set the source (with proper file descriptor, event mask and using the right queue) - folderMonitorSource = DispatchSource.makeFileSystemObjectSource( - fileDescriptor: monitoredFolderFileDescriptor, - eventMask: eventMask, - queue: parent.folderMonitorQueue - ) - - // Set the event handler (fires depending on the event mask) - folderMonitorSource?.setEventHandler { [weak self] in - if behaviour == .reloadsWatchers - && !ConfigWatchManager.ignoresModificationsToConfigValues { - // Reload all configuration watchers - return App.shared.handlePhpConfigWatcher(forceReload: true) - } - - if let url = self?.url { - self?.parent.didChange?(url) - } - } - - // Cancellation handler, fired when we stop monitoring files - folderMonitorSource?.setCancelHandler { [weak self] in - guard let self = self else { return } - - close(self.monitoredFolderFileDescriptor) - self.monitoredFolderFileDescriptor = -1 - self.folderMonitorSource = nil - } - - folderMonitorSource?.resume() - } - - func stopMonitoring() { - folderMonitorSource?.cancel() - self.parent = nil - } -} diff --git a/phpmon/Domain/Watcher/ConfigWatchManager.swift b/phpmon/Domain/Watcher/ConfigWatchManager.swift index e3e9943b..b88ec72e 100644 --- a/phpmon/Domain/Watcher/ConfigWatchManager.swift +++ b/phpmon/Domain/Watcher/ConfigWatchManager.swift @@ -3,24 +3,75 @@ // PHP Monitor // // Created by Nico Verbruggen on 30/03/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation -class ConfigWatchManager { +actor ConfigWatchManager: Suspendable { - static var ignoresModificationsToConfigValues: Bool = false + enum Behaviour { + case reloadsMenu + case reloadsWatchers + } - let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent) + // MARK: Static methods - let url: URL - var didChange: ((URL) -> Void)? - var lastUpdate: TimeInterval? + /** + Handles the PHP configuration file(s) manager lifecycle. - var watchers: [ConfigFSNotifier] = [] + Creates a new manager w/ watchers if needed, or updates the watchers if the current + PHP version has changed. This will be called whenever the PHP version changes, or + when the application first starts. - init(for url: URL) { + - Important: This manager remains nil when a `TestableFileSystem` is in place. + */ + @MainActor + public static func handleWatcher(forceReload: Bool = false) async { + let container = App.shared.container + + if container.filesystem is TestableFileSystem { + Log.warn("ConfigWatchManager is disabled when using a testable filesystem.") + return + } + + guard let install = container.phpEnvs.phpInstall else { + Log.info("It appears as if no PHP installation is currently active.") + Log.info("The config watch manager is disabled until a PHP install is active.") + return + } + + let url = URL(fileURLWithPath: "\(container.paths.etcPath)/php/\(install.version.short)") + + // Create watcher if missing + guard let manager = App.shared.configWatchManager else { + let manager = ConfigWatchManager(for: url) + await manager.setupWatchers() + App.shared.configWatchManager = manager + return + } + + // Update existing watcher if needed + if await manager.url != url { + // URL changed - update to different PHP version + await manager.updateUrl(to: url) + } else if forceReload { + // Same URL - just reload watchers (e.g., conf.d files added/removed) + await manager.reloadWatchers() + } + } + + // MARK: Instance variables + + private var watchers: [FSNotifier] = [] + private var debouncer: Debouncer + + private(set) var url: URL + nonisolated private let debounceInterval: TimeInterval + + // MARK: Methods + + init(for url: URL, debounceInterval: TimeInterval = 0.75) { if App.shared.container.filesystem is TestableFileSystem { fatalError(""" ConfigWatchManager is currently incompatible with a testable filesystem!" @@ -29,6 +80,13 @@ class ConfigWatchManager { } self.url = url + self.debounceInterval = debounceInterval + self.debouncer = Debouncer() + } + + func setupWatchers() { + // Guard against double setup + assert(watchers.isEmpty, "setupWatchers() called when watchers already exist") // Add a watcher for php.ini self.addWatcher(for: self.url.appendingPathComponent("php.ini"), eventMask: .write) @@ -57,29 +115,106 @@ class ConfigWatchManager { })) } - func addWatcher( + private func clearWatchers() { + for watcher in self.watchers { + watcher.terminate() + } + self.watchers.removeAll() + } + + func reloadWatchers() { + Log.perf("Reloading configuration watchers...") + clearWatchers() + setupWatchers() + } + + func updateUrl(to newUrl: URL) { + Log.perf("Updating watcher URL from \(self.url.path) to \(newUrl.path)...") + clearWatchers() + self.url = newUrl + setupWatchers() + } + + private func handleConfigChange(at url: URL) async { + await debouncer.debounce(for: debounceInterval) { + Log.perf("Config file changed at \(url.path), debounce completed. Refreshing menu...") + Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() } + } + } + + private func addWatcher( for url: URL, eventMask: DispatchSource.FileSystemEvent, - behaviour: ConfigFSNotifier.Behaviour = .reloadsMenu + behaviour: Behaviour = .reloadsMenu ) { if !App.shared.container.filesystem.anyExists(url.path) { Log.warn("No watcher was created for \(url.path) because the requested file does not exist.") return } - let watcher = ConfigFSNotifier(for: url, eventMask: eventMask, parent: self, behaviour: behaviour) + let watcher = FSNotifier(for: url, eventMask: eventMask) { [weak self] in + guard let self = self else { return } + + Task { + if behaviour == .reloadsWatchers { + // Reload all configuration watchers on this manager + await self.reloadWatchers() + return + } + + await self.handleConfigChange(at: url) + } + } self.watchers.append(watcher) } - func disable() { + func disable() async { Log.perf("Turning off all individual existing watchers...") - self.watchers.forEach { (watcher) in - watcher.stopMonitoring() - } + await debouncer.cancel() + clearWatchers() } deinit { Log.perf("deinit: \(String(describing: self)).\(#function)") } + // MARK: - Suspendable Protocol + + /** + Performs a particular action while suspending the config watcher, + until the task is completed. + + This should be used when the application writes to PHP configuration files, + to prevent the watcher from responding to our own changes. + */ + public static func withSuspended(_ action: () async throws -> T) async rethrows -> T { + guard let manager = App.shared.configWatchManager else { + // If there's no manager, run the task as-is + return try await action() + } + + // Suspend, execute the action, and resume + return try await manager.withSuspended(action) + } + + /** + Suspends the `ConfigWatchManager`. + This prevents any changes to config files from causing events to fire. + */ + func suspend() async { + for watcher in watchers { + await watcher.suspend() + } + await debouncer.cancel() + } + + /** + Resumes the `ConfigWatchManager`. + Any changes to config files are picked up again. + */ + func resume() async { + for watcher in watchers { + await watcher.resume() + } + } } diff --git a/phpmon/Domain/Watcher/Debouncer.swift b/phpmon/Domain/Watcher/Debouncer.swift new file mode 100644 index 00000000..b7511529 --- /dev/null +++ b/phpmon/Domain/Watcher/Debouncer.swift @@ -0,0 +1,26 @@ +// +// Debouncer.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 29/11/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +actor Debouncer { + private var task: Task? + + func debounce(for duration: TimeInterval, action: @escaping () async -> Void) { + task?.cancel() + task = Task { + try? await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) + guard !Task.isCancelled else { return } + await action() + } + } + + func cancel() { + task?.cancel() + } +} diff --git a/phpmon/Domain/Watcher/FSNotifier.swift b/phpmon/Domain/Watcher/FSNotifier.swift index 01b4070b..1c3d3c53 100644 --- a/phpmon/Domain/Watcher/FSNotifier.swift +++ b/phpmon/Domain/Watcher/FSNotifier.swift @@ -3,25 +3,42 @@ // PHP Monitor // // Created by Nico Verbruggen on 13/01/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation -class FSNotifier { +actor FSNotifier { - public static var shared: FSNotifier! = nil + // MARK: Variables - let queue = DispatchQueue(label: "FSWatch2Queue", attributes: .concurrent) - var lastUpdate: TimeInterval? + /** The URL of the file or folder that is being observed. */ + nonisolated let url: URL - private var fileDescriptor: CInt = -1 - private var dispatchSource: DispatchSourceFileSystemObject? + /** Whether responding to events is currently on hold. */ + private(set) var isSuspended = false - internal let url: URL + // MARK: Internal Variables - init(for url: URL, eventMask: DispatchSource.FileSystemEvent, onChange: @escaping () -> Void) { + /** The queue that is used for the `dispatchSource`. */ + private nonisolated let queue: DispatchQueue + + /** An open file or folder required for observation. */ + private nonisolated(unsafe) var fileDescriptor: CInt = -1 + + /** A dispatch source that monitors events associated with a file or folder. */ + private nonisolated(unsafe) var dispatchSource: DispatchSourceFileSystemObject? + + // MARK: Methods + + init( + for url: URL, + eventMask: DispatchSource.FileSystemEvent, + queue: DispatchQueue? = nil, + onChange: @escaping () -> Void + ) { self.url = url + self.queue = queue ?? DispatchQueue(label: "com.nicoverbruggen.phpmon.fs_notifier") fileDescriptor = open(url.path, O_EVTONLY) @@ -36,35 +53,46 @@ class FSNotifier { queue: self.queue ) - dispatchSource?.setEventHandler(handler: { - let distance = self.lastUpdate?.distance(to: Date().timeIntervalSince1970) + dispatchSource?.setEventHandler(handler: { [weak self] in + Task { [weak self] in + guard let self = self else { return } - if distance == nil || distance != nil && distance! > 1.00 { - // FS event fired, checking in 1s, no duplicate FS events will be acted upon - self.lastUpdate = Date().timeIntervalSince1970 + // If our notifier is suspended, don't fire + guard await !self.isSuspended else { return } - Task { - await delay(seconds: 1) - onChange() - } + // If our notifier is not suspended, fire + onChange() } }) - dispatchSource?.setCancelHandler(handler: { [weak self] in - guard let self = self else { return } + dispatchSource?.setCancelHandler(handler: { close(self.fileDescriptor) self.fileDescriptor = -1 self.dispatchSource = nil }) + dispatchSource?.resume() } - func terminate() { + /** Suspends responding to filesystem events. This does not stop events from being observed! */ + func suspend() async { + self.isSuspended = true + Log.perf("FSNotifier for \(self.url.path) has been suspended.") + } + + /** Resumes responding to filesystem events. */ + func resume() async { + self.isSuspended = false + Log.perf("FSNotifier for \(self.url.path) has been resumed.") + } + + /** Terminates the file monitor, which will cause `deinit` to fire. */ + nonisolated func terminate() { dispatchSource?.cancel() } - deinit { - Log.perf("FSNotifier for \(self.url) will be deinitialized.") + nonisolated deinit { + Log.perf("deinit: FSNotifier @ \(self.url.path)") } } diff --git a/phpmon/Domain/Watcher/HomebrewWatchManager.swift b/phpmon/Domain/Watcher/HomebrewWatchManager.swift new file mode 100644 index 00000000..c9476930 --- /dev/null +++ b/phpmon/Domain/Watcher/HomebrewWatchManager.swift @@ -0,0 +1,167 @@ +// +// HomebrewWatchManager.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 29/11/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +actor HomebrewWatchManager: Suspendable { + + // MARK: Public API + + /** + Prepares the Homebrew watcher. This allows PHP Monitor to quickly respond to + external `brew` changes executed by the user. + + - Important: This manager remains nil when a `TestableFileSystem` is in place. + */ + @MainActor + public static func prepare() async { + let container = App.shared.container + + if container.filesystem is TestableFileSystem { + Log.warn("HomebrewWatchManager is disabled when using a testable filesystem.") + return + } + + let manager = HomebrewWatchManager( + for: URL(fileURLWithPath: container.paths.binPath), + debounceInterval: 5.0 + ) + + await manager.setupWatcher() + + App.shared.homebrewWatchManager = manager + } + + // MARK: - Instance variables + + /** + The underlying `FSNotifier` which will respond to filesystem events. + */ + private var watcher: FSNotifier? + + /** + The debouncer, responsible for ensuring events stop firing before + finally responding to changes in `homebrew/bin`. + */ + private var debouncer: Debouncer + + /** + The URL of the `homebrew/bin` path, that we will be watching, too. + */ + nonisolated let url: URL + + /** + The interval for the debounce. Prevents bulk changes from triggering + too many fired events. + */ + nonisolated let debounceInterval: TimeInterval + + // MARK: - Lifecycle + + init(for url: URL, debounceInterval: TimeInterval = 5.0) { + if App.shared.container.filesystem is TestableFileSystem { + fatalError(""" + HomebrewWatchManager is currently incompatible with a testable filesystem! + You are not allowed to instantiate these while using a testable filesystem. + """) + } + + self.url = url + self.debounceInterval = debounceInterval + self.debouncer = Debouncer() + } + + deinit { + Log.perf("deinit: \(String(describing: self)).\(#function)") + } + + // MARK: - Internal Methods + + /** + Sets up the watcher, assuming one does not exist. + The target directory must exist. + */ + private func setupWatcher() { + // Guard against double setup + assert(watcher == nil, "setupWatcher() called when watcher already exists") + + // Ensure that the target directory exists + if !App.shared.container.filesystem.anyExists(url.path) { + Log.warn("No watcher was created for \(url.path) because the requested directory does not exist.") + return + } + + // Create a new FSNotifier which will respond to all events. + // If files are created, removed, etc. in this `homebrew/bin` folder, the handler will fire. + self.watcher = FSNotifier(for: url, eventMask: .all) { [weak self] in + guard let self = self else { return } + + Task { + await self.onHomebrewPhpModification() + } + } + + Log.perf("A watcher exists for Homebrew binaries at: \(url.relativePath)") + } + + /** + Reloads PHP versions and refreshes the active PHP installation if any changes + are made to Homebrew binaries. Usually external changes to packages will trigger this. + + As such, PHP Monitor will check if anything has changed with PHP. + */ + private func onHomebrewPhpModification() async { + await debouncer.debounce(for: debounceInterval) { [weak self] in + guard let self = self else { return } + Log.info("No changes in `\(self.url.path)` occurred for \(self.debounceInterval) seconds. Reloading now.") + + // We reload the PHP versions in the background + await App.shared.container.phpEnvs.reloadPhpVersions() + + // Finally, refresh the active installation + await MainMenu.shared.refreshActiveInstallation() + } + } + + // MARK: - Suspendable Protocol + + /** + Performs a particular action while suspending the Homebrew watcher, + until the task is completed. + + Any operations that cause Homebrew to perform tasks (installing, + updating, removing packages) should be wrapped in this helper method, + to prevent the app from doing duplicate work. + */ + public static func withSuspended(_ action: () async throws -> T) async rethrows -> T { + guard let manager = App.shared.homebrewWatchManager else { + // If there's no manager, run the task as-is + return try await action() + } + + // Suspend, execute the action, and resume + return try await manager.withSuspended(action) + } + + /** + Suspends the `HomebrewWatchManager`. + This prevents any changes to `/homebrew/bin` from causing events to fire. + */ + func suspend() async { + await watcher?.suspend() + await debouncer.cancel() + } + + /** + Resumes the `HomebrewWatchManager`. + Any changes to `/homebrew/bin` are picked up again. + */ + func resume() async { + await watcher?.resume() + } +} diff --git a/phpmon/Modules/Domain List/Favorites.swift b/phpmon/Modules/Domain List/Favorites.swift index 823b73b1..27489794 100644 --- a/phpmon/Modules/Domain List/Favorites.swift +++ b/phpmon/Modules/Domain List/Favorites.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 25/08/2024. -// Copyright © 2024 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/Domain List/UI/AddProxyVC.swift b/phpmon/Modules/Domain List/UI/AddProxyVC.swift index b581b28a..cdeb204f 100644 --- a/phpmon/Modules/Domain List/UI/AddProxyVC.swift +++ b/phpmon/Modules/Domain List/UI/AddProxyVC.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 24/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/Domain List/UI/AddSiteVC.swift b/phpmon/Modules/Domain List/UI/AddSiteVC.swift index d9d7625d..6b5b99b3 100644 --- a/phpmon/Modules/Domain List/UI/AddSiteVC.swift +++ b/phpmon/Modules/Domain List/UI/AddSiteVC.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 24/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/Domain List/UI/Cells/DomainListCellProtocol.swift b/phpmon/Modules/Domain List/UI/Cells/DomainListCellProtocol.swift index 827336e9..d6e36cb7 100644 --- a/phpmon/Modules/Domain List/UI/Cells/DomainListCellProtocol.swift +++ b/phpmon/Modules/Domain List/UI/Cells/DomainListCellProtocol.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 03/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/Domain List/UI/Cells/DomainListKindCell.swift b/phpmon/Modules/Domain List/UI/Cells/DomainListKindCell.swift index ba547294..47759f85 100644 --- a/phpmon/Modules/Domain List/UI/Cells/DomainListKindCell.swift +++ b/phpmon/Modules/Domain List/UI/Cells/DomainListKindCell.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/03/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/Domain List/UI/Cells/DomainListNameCell.swift b/phpmon/Modules/Domain List/UI/Cells/DomainListNameCell.swift index 0330d0a7..3e3b0a38 100644 --- a/phpmon/Modules/Domain List/UI/Cells/DomainListNameCell.swift +++ b/phpmon/Modules/Domain List/UI/Cells/DomainListNameCell.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/03/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/Domain List/UI/Cells/DomainListPhpCell.swift b/phpmon/Modules/Domain List/UI/Cells/DomainListPhpCell.swift index 8848a8ca..d18a7e47 100644 --- a/phpmon/Modules/Domain List/UI/Cells/DomainListPhpCell.swift +++ b/phpmon/Modules/Domain List/UI/Cells/DomainListPhpCell.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/03/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/Domain List/UI/Cells/DomainListTLSCell.swift b/phpmon/Modules/Domain List/UI/Cells/DomainListTLSCell.swift index 70bef83d..70cbb70d 100644 --- a/phpmon/Modules/Domain List/UI/Cells/DomainListTLSCell.swift +++ b/phpmon/Modules/Domain List/UI/Cells/DomainListTLSCell.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/03/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/Domain List/UI/Cells/DomainListTypeCell.swift b/phpmon/Modules/Domain List/UI/Cells/DomainListTypeCell.swift index f7d494ce..3c6ed218 100644 --- a/phpmon/Modules/Domain List/UI/Cells/DomainListTypeCell.swift +++ b/phpmon/Modules/Domain List/UI/Cells/DomainListTypeCell.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/03/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/Domain List/UI/DomainListVC+Actions.swift b/phpmon/Modules/Domain List/UI/DomainListVC+Actions.swift index 46a7168d..579255b4 100644 --- a/phpmon/Modules/Domain List/UI/DomainListVC+Actions.swift +++ b/phpmon/Modules/Domain List/UI/DomainListVC+Actions.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 23/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -23,7 +23,7 @@ extension DomainListVC { subtitle: "domain_list.alert.invalid_folder_name_desc".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) return } @@ -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 @@ -283,7 +292,7 @@ extension DomainListVC { description: "domain_list.alerts_isolated_php_terminal.desc".localized ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } private func notifyAboutFailedSecureStatus(command: String) { @@ -293,7 +302,7 @@ extension DomainListVC { subtitle: "domain_list.alerts_status_not_changed.desc".localized(command) ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } private func notifyAboutFailedSiteIsolation(command: String) { @@ -304,6 +313,6 @@ extension DomainListVC { description: "domain_list.alerts_isolation_failed.desc".localized(command) ) .withPrimary(text: "generic.ok".localized) - .show() + .show(urgency: .bringToFront) } } diff --git a/phpmon/Modules/Domain List/UI/DomainListVC+ContextMenu.swift b/phpmon/Modules/Domain List/UI/DomainListVC+ContextMenu.swift index 9e81fcf2..392d7652 100644 --- a/phpmon/Modules/Domain List/UI/DomainListVC+ContextMenu.swift +++ b/phpmon/Modules/Domain List/UI/DomainListVC+ContextMenu.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 10/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa @@ -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) } } } diff --git a/phpmon/Modules/Domain List/UI/DomainListVC.swift b/phpmon/Modules/Domain List/UI/DomainListVC.swift index 274dbff0..36ac9831 100644 --- a/phpmon/Modules/Domain List/UI/DomainListVC.swift +++ b/phpmon/Modules/Domain List/UI/DomainListVC.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 30/03/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa @@ -114,6 +114,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource domains = Valet.getDomainListable() } + @MainActor private func addNoResultsView() { let child = NSHostingController( rootView: UnavailableContentView( @@ -142,8 +143,8 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource @MainActor public func setUIBusy() { // If it takes more than 0.5s to set the UI to not busy, show a spinner timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in - Task { - @MainActor in self.progressIndicator.startAnimation(true) + Task { @MainActor in + self.progressIndicator.startAnimation(true) self.labelProgressIndicator.stringValue = "phpman.steps.wait".localized self.progressIndicatorContainer.layer?.cornerRadius = 10 self.progressIndicatorContainer.isHidden = false diff --git a/phpmon/Modules/Domain List/UI/DomainListWindowController.swift b/phpmon/Modules/Domain List/UI/DomainListWindowController.swift index fe846eea..524e1d07 100644 --- a/phpmon/Modules/Domain List/UI/DomainListWindowController.swift +++ b/phpmon/Modules/Domain List/UI/DomainListWindowController.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 03/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/Domain List/UI/SelectionVC.swift b/phpmon/Modules/Domain List/UI/SelectionVC.swift index f5ea23af..7b4ece9d 100644 --- a/phpmon/Modules/Domain List/UI/SelectionVC.swift +++ b/phpmon/Modules/Domain List/UI/SelectionVC.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 14/04/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/Domain List/UI/Subclass/PMTableView.swift b/phpmon/Modules/Domain List/UI/Subclass/PMTableView.swift index 7080bd2b..d697e1ef 100644 --- a/phpmon/Modules/Domain List/UI/Subclass/PMTableView.swift +++ b/phpmon/Modules/Domain List/UI/Subclass/PMTableView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 05/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/Onboarding/OnboardingView.swift b/phpmon/Modules/Onboarding/OnboardingView.swift index 2192ba18..22b346c4 100644 --- a/phpmon/Modules/Onboarding/OnboardingView.swift +++ b/phpmon/Modules/Onboarding/OnboardingView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 08/07/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI diff --git a/phpmon/Modules/Onboarding/OnboardingWindowController.swift b/phpmon/Modules/Onboarding/OnboardingWindowController.swift index 3c434d42..c4ef23ca 100644 --- a/phpmon/Modules/Onboarding/OnboardingWindowController.swift +++ b/phpmon/Modules/Onboarding/OnboardingWindowController.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 25/06/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/PHP Config Editor/Data/BytePhpPreference.swift b/phpmon/Modules/PHP Config Editor/Data/BytePhpPreference.swift index 86f4e659..08f88817 100644 --- a/phpmon/Modules/PHP Config Editor/Data/BytePhpPreference.swift +++ b/phpmon/Modules/PHP Config Editor/Data/BytePhpPreference.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/09/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -62,11 +62,13 @@ class BytePhpPreference: PhpPreference { internalValue = "\(value)\(unit.rawValue)" } - do { - try PhpPreference.persistToIniFile(key: self.key, value: self.internalValue) - Log.info("The preference \(key) was updated to: \(value)") - } catch { - Log.info("The preference \(key) could not be updated") + Task { + do { + try await PhpPreference.persistToIniFile(key: self.key, value: self.internalValue) + Log.info("The preference \(key) was updated to: \(value)") + } catch { + Log.info("The preference \(key) could not be updated") + } } } diff --git a/phpmon/Modules/PHP Config Editor/Data/PhpPreference.swift b/phpmon/Modules/PHP Config Editor/Data/PhpPreference.swift index fc2a860d..ce95b4dc 100644 --- a/phpmon/Modules/PHP Config Editor/Data/PhpPreference.swift +++ b/phpmon/Modules/PHP Config Editor/Data/PhpPreference.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/09/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -20,9 +20,13 @@ class PhpPreference { self.key = key } - internal static func persistToIniFile(key: String, value: String) throws { + internal static func persistToIniFile(key: String, value: String) async throws { if let file = App.shared.container.phpEnvs.getConfigFile(forKey: key) { - return try file.replace(key: key, value: value) + // Do the replacement + try await file.replace(key: key, value: value) + // Reload the main menu item to reflect these new values + Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() } + return } throw PhpConfigurationFile.ReplacementErrors.missingFile diff --git a/phpmon/Modules/PHP Config Editor/UI/ByteLimitView.swift b/phpmon/Modules/PHP Config Editor/UI/ByteLimitView.swift index 54f52974..8e953807 100644 --- a/phpmon/Modules/PHP Config Editor/UI/ByteLimitView.swift +++ b/phpmon/Modules/PHP Config Editor/UI/ByteLimitView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 25/07/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI diff --git a/phpmon/Modules/PHP Config Editor/UI/ConfigManagerView.swift b/phpmon/Modules/PHP Config Editor/UI/ConfigManagerView.swift index dd2b154a..ebb6e1f8 100644 --- a/phpmon/Modules/PHP Config Editor/UI/ConfigManagerView.swift +++ b/phpmon/Modules/PHP Config Editor/UI/ConfigManagerView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 18/07/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -79,6 +79,7 @@ struct ConfigManagerView: View { alignment: .topTrailing ) } + .padding(.bottom, 15) }.frame(maxHeight: 485) } } diff --git a/phpmon/Modules/PHP Config Editor/UI/ConfigManagerWindowController.swift b/phpmon/Modules/PHP Config Editor/UI/ConfigManagerWindowController.swift index ae690f5a..1450d335 100644 --- a/phpmon/Modules/PHP Config Editor/UI/ConfigManagerWindowController.swift +++ b/phpmon/Modules/PHP Config Editor/UI/ConfigManagerWindowController.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 12/09/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/PHP Doctor/Data/PhpConfigChecker.swift b/phpmon/Modules/PHP Doctor/Data/PhpConfigChecker.swift index d0282306..7a880c59 100644 --- a/phpmon/Modules/PHP Doctor/Data/PhpConfigChecker.swift +++ b/phpmon/Modules/PHP Doctor/Data/PhpConfigChecker.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 24/02/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/PHP Doctor/Data/Warning.swift b/phpmon/Modules/PHP Doctor/Data/Warning.swift index 52bcf0a3..b5bd8a79 100644 --- a/phpmon/Modules/PHP Doctor/Data/Warning.swift +++ b/phpmon/Modules/PHP Doctor/Data/Warning.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 09/08/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/PHP Doctor/Data/WarningManager.swift b/phpmon/Modules/PHP Doctor/Data/WarningManager.swift index 685ff4fb..abc08d2a 100644 --- a/phpmon/Modules/PHP Doctor/Data/WarningManager.swift +++ b/phpmon/Modules/PHP Doctor/Data/WarningManager.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 09/08/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/PHP Doctor/UI/NoWarningsView.swift b/phpmon/Modules/PHP Doctor/UI/NoWarningsView.swift index 10b5a3ec..a73f0141 100644 --- a/phpmon/Modules/PHP Doctor/UI/NoWarningsView.swift +++ b/phpmon/Modules/PHP Doctor/UI/NoWarningsView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 15/08/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI diff --git a/phpmon/Modules/PHP Doctor/UI/PhpDoctorView.swift b/phpmon/Modules/PHP Doctor/UI/PhpDoctorView.swift index 082e3297..4465d1f0 100644 --- a/phpmon/Modules/PHP Doctor/UI/PhpDoctorView.swift +++ b/phpmon/Modules/PHP Doctor/UI/PhpDoctorView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 09/08/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI diff --git a/phpmon/Modules/PHP Doctor/UI/PhpDoctorWindowController.swift b/phpmon/Modules/PHP Doctor/UI/PhpDoctorWindowController.swift index 03bf450c..c426358b 100644 --- a/phpmon/Modules/PHP Doctor/UI/PhpDoctorWindowController.swift +++ b/phpmon/Modules/PHP Doctor/UI/PhpDoctorWindowController.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 09/08/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Cocoa diff --git a/phpmon/Modules/PHP Doctor/UI/WarningView.swift b/phpmon/Modules/PHP Doctor/UI/WarningView.swift index bcb997a4..4d2016da 100644 --- a/phpmon/Modules/PHP Doctor/UI/WarningView.swift +++ b/phpmon/Modules/PHP Doctor/UI/WarningView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 31/07/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import SwiftUI diff --git a/phpmon/Modules/PHP Extension Manager/Data/BrewExtensionsObservable.swift b/phpmon/Modules/PHP Extension Manager/Data/BrewExtensionsObservable.swift index 45f7b631..106072b1 100644 --- a/phpmon/Modules/PHP Extension Manager/Data/BrewExtensionsObservable.swift +++ b/phpmon/Modules/PHP Extension Manager/Data/BrewExtensionsObservable.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView+Actions.swift b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView+Actions.swift index a59173fa..786e7440 100644 --- a/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView+Actions.swift +++ b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView+Actions.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 21/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView.swift b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView.swift index 85749e14..8362b3a6 100644 --- a/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView.swift +++ b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 13/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerWindowController.swift b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerWindowController.swift index a0260b1f..c7469017 100644 --- a/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerWindowController.swift +++ b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerWindowController.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 13/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/PHP Version Manager/Data/BrewFormula+UI.swift b/phpmon/Modules/PHP Version Manager/Data/BrewFormula+UI.swift index a10df4b0..382e13db 100644 --- a/phpmon/Modules/PHP Version Manager/Data/BrewFormula+UI.swift +++ b/phpmon/Modules/PHP Version Manager/Data/BrewFormula+UI.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 02/05/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/PHP Version Manager/Data/BrewFormulaeObservable.swift b/phpmon/Modules/PHP Version Manager/Data/BrewFormulaeObservable.swift index 59436f2d..33f40215 100644 --- a/phpmon/Modules/PHP Version Manager/Data/BrewFormulaeObservable.swift +++ b/phpmon/Modules/PHP Version Manager/Data/BrewFormulaeObservable.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 13/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/Modules/PHP Version Manager/Data/Fake/FakeBrewFormulaeHandler.swift b/phpmon/Modules/PHP Version Manager/Data/Fake/FakeBrewFormulaeHandler.swift index fe8a6c87..35f0f325 100644 --- a/phpmon/Modules/PHP Version Manager/Data/Fake/FakeBrewFormulaeHandler.swift +++ b/phpmon/Modules/PHP Version Manager/Data/Fake/FakeBrewFormulaeHandler.swift @@ -3,12 +3,11 @@ // PHP Monitor // // Created by Nico Verbruggen on 27/05/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation -// swiftlint:disable function_body_length class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae { public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] { // Using the shared container is allowed since this only runs w/ UI tests @@ -91,4 +90,3 @@ class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae { ] } } -// swiftlint:enable function_body_length diff --git a/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView+Actions.swift b/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView+Actions.swift index 16776e83..a580b05c 100644 --- a/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView+Actions.swift +++ b/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView+Actions.swift @@ -3,15 +3,26 @@ // PHP Monitor // // Created by Nico Verbruggen on 07/11/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation import SwiftUI extension PhpVersionManagerView { + + // MARK: - Variables + + var hasUpdates: Bool { + return self.formulae.phpVersions.contains { formula in + return formula.hasUpgrade + } + } + + // MARK: - Executing Homebrew Commands + public func runCommand(_ command: ModifyPhpVersionCommand) async { - if App.shared.container.phpEnvs.isBusy { + if container.phpEnvs.isBusy { self.presentErrorAlert( title: "phpman.action_prevented_busy.title".localized, description: "phpman.action_prevented_busy.desc".localized, @@ -22,22 +33,25 @@ extension PhpVersionManagerView { do { self.setBusyStatus(true) - try await command.execute(shell: App.shared.container.shell) { progress in - Task { @MainActor in - self.status.title = progress.title - self.status.description = progress.description - self.status.busy = progress.value != 1 + try await HomebrewWatchManager.withSuspended { + try await command.execute(shell: container.shell) { progress in + Task { @MainActor in + self.status.title = progress.title + self.status.description = progress.description + self.status.busy = progress.value != 1 - // Whenever a key step is finished, refresh the PHP versions - if progress.value == 1 { - await self.handler.refreshPhpVersions(loadOutdated: false) + // Whenever a key step is finished, refresh the PHP versions + if progress.value == 1 { + await self.handler.refreshPhpVersions(loadOutdated: false) + } } } + // Finally, after completing the command, also refresh PHP versions + await self.handler.refreshPhpVersions(loadOutdated: false) + + // and mark the app as no longer busy + self.setBusyStatus(false) } - // Finally, after completing the command, also refresh PHP versions - await self.handler.refreshPhpVersions(loadOutdated: false) - // and mark the app as no longer busy - self.setBusyStatus(false) } catch let error { let error = error as! BrewCommandError let messages = error.log.suffix(2).joined(separator: "\n") @@ -80,9 +94,57 @@ extension PhpVersionManagerView { )) } + public func uninstall(_ formula: BrewPhpFormula) async { + let command = RemovePhpVersionCommand(container, formula: formula.name) + + do { + self.setBusyStatus(true) + try await HomebrewWatchManager.withSuspended { + try await command.execute(shell: container.shell) { progress in + Task { @MainActor in + self.status.title = progress.title + self.status.description = progress.description + self.status.busy = progress.value != 1 + + if progress.value == 1 { + await self.handler.refreshPhpVersions(loadOutdated: false) + self.setBusyStatus(false) + } + } + } + } + } catch { + self.setBusyStatus(false) + await self.handler.refreshPhpVersions(loadOutdated: false) + + self.presentErrorAlert( + title: "phpman.failures.uninstall.title".localized, + description: "phpman.failures.uninstall.desc".localized( + "brew uninstall \(formula.name) --force" + ), + button: "generic.ok".localized + ) + } + } + + // MARK: GUI + + /** + Mark the PHP Version Manager, as well as the PHP environments as busy. + */ + public func setBusyStatus(_ busy: Bool) { + Task { @MainActor in + container.phpEnvs.isBusy = busy + self.status.busy = busy + } + } + + /** + Ask the user to confirm the uninstall of a particular PHP version. + */ public func confirmUninstall(_ formula: BrewPhpFormula) async { - // Disallow removal of the currently active versipn - if formula.installedVersion == App.shared.container.phpEnvs.currentInstall?.version.text { + // Disallow removal of the currently active version + if formula.installedVersion == container.phpEnvs.currentInstall?.version.text { self.presentErrorAlert( title: "phpman.uninstall_prevented.title".localized, description: "phpman.uninstall_prevented.desc".localized, @@ -105,42 +167,9 @@ extension PhpVersionManagerView { ) } - public func uninstall(_ formula: BrewPhpFormula) async { - let command = RemovePhpVersionCommand(container, formula: formula.name) - - do { - self.setBusyStatus(true) - try await command.execute(shell: App.shared.container.shell) { progress in - Task { @MainActor in - self.status.title = progress.title - self.status.description = progress.description - self.status.busy = progress.value != 1 - - if progress.value == 1 { - await self.handler.refreshPhpVersions(loadOutdated: false) - self.setBusyStatus(false) - } - } - } - } catch { - self.setBusyStatus(false) - self.presentErrorAlert( - title: "phpman.failures.uninstall.title".localized, - description: "phpman.failures.uninstall.desc".localized( - "brew uninstall \(formula.name) --force" - ), - button: "generic.ok".localized - ) - } - } - - public func setBusyStatus(_ busy: Bool) { - Task { @MainActor in - App.shared.container.phpEnvs.isBusy = busy - self.status.busy = busy - } - } - + /** + Present a generic error alert attached to the window. + */ public func presentErrorAlert( title: String, description: String, @@ -158,9 +187,4 @@ extension PhpVersionManagerView { ) } - var hasUpdates: Bool { - return self.formulae.phpVersions.contains { formula in - return formula.hasUpgrade - } - } } diff --git a/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView.swift b/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView.swift index 85efb396..db73e176 100644 --- a/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView.swift +++ b/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 17/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -217,7 +217,9 @@ struct PhpVersionManagerView: View { HStack { if !formula.healthy { Button("phpman.buttons.repair".localizedForSwiftUI, role: .destructive) { - Task { await self.repairAll() } + Task { + await self.repairAll() + } } } @@ -227,11 +229,15 @@ struct PhpVersionManagerView: View { }) } else if formula.isInstalled { Button("phpman.buttons.uninstall".localizedForSwiftUI, role: .destructive) { - Task { await self.confirmUninstall(formula) } + Task { + await self.confirmUninstall(formula) + } } } else { Button("phpman.buttons.install".localizedForSwiftUI) { - Task { await self.install(formula) } + Task { + await self.install(formula) + } }.disabled(formula.hasUpgradedFormulaAlias || !formula.hasFormulaFile) } } diff --git a/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerWindowController.swift b/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerWindowController.swift index e0bf31fe..76b30caf 100644 --- a/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerWindowController.swift +++ b/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerWindowController.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 19/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/phpmon/en.lproj/Localizable.strings b/phpmon/en.lproj/Localizable.strings index 1c8ef6b2..fa9152d6 100644 --- a/phpmon/en.lproj/Localizable.strings +++ b/phpmon/en.lproj/Localizable.strings @@ -942,7 +942,7 @@ PHP Monitor will tell Valet to unsecure and re-secure all expired domains for yo "cert_alert.renew" = "Re-secure Domain(s)"; "cert_alert.cancel" = "Not Now"; -"crash_reporter.title" = "PHP Monitor crashed earlier, want to send a crash report?"; +"crash_reporter.title" = "PHP Monitor crashed earlier, do you want to send a crash report?"; "crash_reporter.subtitle" = "It is possible to send the crash report to the developer of the app, so this issue can be fixed. This is highly recommended. Would you like to do that?"; "crash_reporter.description" = "Without sending this crash report, the developer may not be aware of this particular issue. No logs or personal data is sent with the crash report, only the unsymbolicated crash report. No further action is necessary on your part. diff --git a/phpmon/zh-Hans.lproj/Localizable.strings b/phpmon/zh-Hans.lproj/Localizable.strings index 2904766f..e6d81285 100644 --- a/phpmon/zh-Hans.lproj/Localizable.strings +++ b/phpmon/zh-Hans.lproj/Localizable.strings @@ -519,7 +519,7 @@ "updater.alerts.cannot_check_for_update.description" = "当前安装的版本是:%@。您可以点击左侧的按钮进入最新版本列表(在 GitHub 上)"; "updater.alerts.buttons.releases_on_github" = "查看版本"; "updater.alerts.buttons.install" = "安装更新"; -"updater.alerts.buttons.dismiss" = "解除"; +"updater.alerts.buttons.dismiss" = "忽略更新"; "alert.warnings.tld_issue.title" = "您没有使用 `.test` 作为 Valet 的顶级域名"; "alert.warnings.tld_issue.subtitle" = "使用非默认顶级域名可能无法正常工作,并且不受官方支持"; "alert.warnings.tld_issue.description" = "PHP Monitor 仍将正常运行,但可能会出现一些问题:应用程序可能无法正确显示哪些域名已被安全保护。为获得最佳效果,请转到 Valet 配置文件(Valet 目录中的 config.json)并将 TLD 改回 `test`。"; diff --git a/tests/PHP Monitor.xctestplan b/tests/PHP Monitor.xctestplan index bc734b7e..751aafb1 100644 --- a/tests/PHP Monitor.xctestplan +++ b/tests/PHP Monitor.xctestplan @@ -30,7 +30,6 @@ } }, { - "enabled" : false, "target" : { "containerPath" : "container:PHP Monitor.xcodeproj", "identifier" : "C471E7BB28F9B90F0021E251", diff --git a/tests/Shared/TestableConfigurations.swift b/tests/Shared/TestableConfigurations.swift index 2988f885..367f5db1 100644 --- a/tests/Shared/TestableConfigurations.swift +++ b/tests/Shared/TestableConfigurations.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation @@ -122,18 +122,33 @@ class TestableConfigurations { : .instant(""), "brew info shivammathur/php/php --json" : .instant("Error: No available formula with the name \"shivammathur/php/php\"."), + + // TODO: refactor this so this list of responses happens dynamically? "/usr/bin/open -Ra \"PhpStorm\"" : .instant("Unable to find application named 'PhpStorm'", .stdErr), + "/usr/bin/open -Ra \"WebStorm\"" + : .instant("Unable to find application named 'WebStorm'", .stdErr), "/usr/bin/open -Ra \"Visual Studio Code\"" : .instant("Unable to find application named 'Visual Studio Code'", .stdErr), + "/usr/bin/open -Ra \"VSCodium\"" + : .instant("Unable to find application named 'VSCodium'", .stdErr), "/usr/bin/open -Ra \"Sublime Text\"" : .instant("Unable to find application named 'Sublime Text'", .stdErr), "/usr/bin/open -Ra \"Sublime Merge\"" : .instant("Unable to find application named 'Sublime Merge'", .stdErr), + "/usr/bin/open -Ra \"Tower\"" + : .instant("Unable to find application named 'Tower'", .stdErr), + "/usr/bin/open -Ra \"SourceTree\"" + : .instant("Unable to find application named 'SourceTree'", .stdErr), "/usr/bin/open -Ra \"iTerm\"" : .instant("Unable to find application named 'iTerm'", .stdErr), + "/usr/bin/open -Ra \"Ghostty\"" + : .instant("Unable to find application named 'Ghostty'", .stdErr), + "/opt/homebrew/bin/brew info php --json" : .instant(ShellStrings.shared.brewJson), + "/opt/homebrew/bin/brew info shivammathur/php/php --json" + : .instant(ShellStrings.shared.brewJson), "sudo /opt/homebrew/bin/brew services info --all --json" : .instant(ShellStrings.shared.brewServicesAsRoot), "/opt/homebrew/bin/brew services info --all --json" diff --git a/tests/Shared/Utility.swift b/tests/Shared/Utility.swift index 55096ad1..e064cfd4 100644 --- a/tests/Shared/Utility.swift +++ b/tests/Shared/Utility.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 14/02/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Foundation diff --git a/tests/Shared/XCPMApplication.swift b/tests/Shared/XCPMApplication.swift index 5a7f4434..2221edbb 100644 --- a/tests/Shared/XCPMApplication.swift +++ b/tests/Shared/XCPMApplication.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import XCTest diff --git a/tests/feature/FeatureTestCase.swift b/tests/feature/FeatureTestCase.swift index 3e5ad498..2bd226a4 100644 --- a/tests/feature/FeatureTestCase.swift +++ b/tests/feature/FeatureTestCase.swift @@ -3,7 +3,7 @@ // Feature Tests // // Created by Nico Verbruggen on 07/11/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import XCTest diff --git a/tests/feature/InternalSwitcherTest.swift b/tests/feature/InternalSwitcherTest.swift index 539573c4..9208a82e 100644 --- a/tests/feature/InternalSwitcherTest.swift +++ b/tests/feature/InternalSwitcherTest.swift @@ -3,7 +3,7 @@ // Feature Tests // // Created by Nico Verbruggen on 14/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import XCTest diff --git a/tests/ui/DomainsListTest.swift b/tests/ui/DomainsListTest.swift index 2fe2610e..48476871 100644 --- a/tests/ui/DomainsListTest.swift +++ b/tests/ui/DomainsListTest.swift @@ -3,7 +3,7 @@ // UI Tests // // Created by Nico Verbruggen on 14/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import XCTest diff --git a/tests/ui/MainMenuTest.swift b/tests/ui/MainMenuTest.swift index 93db5579..1b308908 100644 --- a/tests/ui/MainMenuTest.swift +++ b/tests/ui/MainMenuTest.swift @@ -3,7 +3,7 @@ // UI Tests // // Created by Nico Verbruggen on 03/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import XCTest @@ -92,21 +92,23 @@ final class MainMenuTest: UITestCase { // Should display loader assertExists(app.staticTexts["phpman.busy.title".localized], 1) - // After loading, should display PHP 8.2, PHP 8.3, PHP 8.4 + // After loading, should display various versions assertExists(app.staticTexts["PHP 8.2"], 5) assertExists(app.staticTexts["PHP 8.3"]) assertExists(app.staticTexts["PHP 8.4"]) + assertExists(app.staticTexts["PHP 8.5"]) // Should also display pre-release version - assertExists(app.staticTexts["PHP 8.5"]) + assertExists(app.staticTexts["PHP 8.6"]) assertExists(app.staticTexts["phpman.version.prerelease".localized.uppercased()]) assertExists(app.staticTexts["phpman.version.available_for_installation".localized]) // The pre-release version should be unavailable assertExists(app.staticTexts["phpman.version.unavailable".localized]) - // But not PHP 8.6 (yet) - assertNotExists(app.staticTexts["PHP 8.6"]) + // But not PHP 8.7 or 9.0 yet + assertNotExists(app.staticTexts["PHP 8.7"]) + assertNotExists(app.staticTexts["PHP 9.0"]) // Also, PHP 8.4 should have an update available assertExists(app.staticTexts["phpman.version.has_update".localized( diff --git a/tests/ui/StartupTest.swift b/tests/ui/StartupTest.swift index 2f4b3c24..4f413487 100644 --- a/tests/ui/StartupTest.swift +++ b/tests/ui/StartupTest.swift @@ -3,7 +3,7 @@ // UI Tests // // Created by Nico Verbruggen on 14/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import XCTest diff --git a/tests/ui/UITestCase.swift b/tests/ui/UITestCase.swift index 4a02651c..41aa5508 100644 --- a/tests/ui/UITestCase.swift +++ b/tests/ui/UITestCase.swift @@ -3,7 +3,7 @@ // UI Tests // // Created by Nico Verbruggen on 15/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import XCTest diff --git a/tests/ui/UpdateCheckTest.swift b/tests/ui/UpdateCheckTest.swift index ec5ce0c3..23c08483 100644 --- a/tests/ui/UpdateCheckTest.swift +++ b/tests/ui/UpdateCheckTest.swift @@ -3,7 +3,7 @@ // UI Tests // // Created by Nico Verbruggen on 13/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import XCTest diff --git a/tests/unit/Commands/CommandTest.swift b/tests/unit/Commands/CommandTest.swift index efdf4997..b3f26ae3 100644 --- a/tests/unit/Commands/CommandTest.swift +++ b/tests/unit/Commands/CommandTest.swift @@ -3,14 +3,14 @@ // PHP Monitor // // Created by Nico Verbruggen on 13/02/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing struct CommandTest { @Test func determinePhpVersion() { - let container = Container.real() + let container = Container.real(minimal: true) let version = container.command.execute( path: container.paths.php, diff --git a/tests/unit/Helpers/LockedTests.swift b/tests/unit/Helpers/LockedTests.swift new file mode 100644 index 00000000..af8f8a41 --- /dev/null +++ b/tests/unit/Helpers/LockedTests.swift @@ -0,0 +1,122 @@ +// +// LockedTests.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/11/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Testing +import Foundation + +@Suite("Locked Thread Safety") +struct LockedTests { + + @Test("Reading and writing from a single thread works correctly") + func singleThreadReadWrite() { + let locked = Locked(0) + + locked.value = 42 + #expect(locked.value == 42) + + locked.value = 100 + #expect(locked.value == 100) + } + + @Test("Concurrent writes do not cause data races") + func concurrentWritesAreThreadSafe() async { + let locked = Locked(0) + let iterations = 1000 + + // Spawn many concurrent tasks that all increment the counter + await withTaskGroup(of: Void.self) { group in + for _ in 0.. 0, "Value should have been incremented") + #expect(locked.value <= iterations, "Value should not exceed iterations") + } + + @Test("Concurrent reads and writes do not crash") + func concurrentReadsAndWritesDoNotCrash() async { + let locked = Locked<[String]>([]) + let iterations = 500 + + await withTaskGroup(of: Void.self) { group in + // Writers + for i in 0..([:]) + let iterations = 100 + + await withTaskGroup(of: Void.self) { group in + // Multiple tasks replacing the entire dictionary + for i in 0..(0) + let taskCount = 10 + let incrementsPerTask = 100 + + await withTaskGroup(of: Void.self) { group in + for _ in 0..= 0 && finalValue < 1000, "Value should be within expected range") + } +} diff --git a/tests/unit/Parsers/BytePhpPreferenceTest.swift b/tests/unit/Parsers/BytePhpPreferenceTest.swift index 2f8dc8a8..30a5fd42 100644 --- a/tests/unit/Parsers/BytePhpPreferenceTest.swift +++ b/tests/unit/Parsers/BytePhpPreferenceTest.swift @@ -3,7 +3,7 @@ // Unit Tests // // Created by Nico Verbruggen on 04/09/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing @@ -12,7 +12,9 @@ struct BytePhpPreferenceTest { var container: Container init () async throws { - container = Container.real() + container = Container.fake(commands: [ + "/opt/homebrew/bin/php -r echo ini_get('memory_limit');": "512M" + ]) } @Test func can_extract_memory_value() throws { diff --git a/tests/unit/Parsers/CaskFileParserTest.swift b/tests/unit/Parsers/CaskFileParserTest.swift index 48029564..9661214e 100644 --- a/tests/unit/Parsers/CaskFileParserTest.swift +++ b/tests/unit/Parsers/CaskFileParserTest.swift @@ -3,18 +3,17 @@ // Unit Tests // // Created by Nico Verbruggen on 04/02/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing import Foundation -@Suite(.serialized) struct CaskFileParserTest { var container: Container init() async throws { - container = Container.real() + container = Container.real(minimal: true) } var Shell: ShellProtocol { diff --git a/tests/unit/Parsers/ExtensionEnumeratorTest.swift b/tests/unit/Parsers/ExtensionEnumeratorTest.swift index 685f8e3b..fe49ee43 100644 --- a/tests/unit/Parsers/ExtensionEnumeratorTest.swift +++ b/tests/unit/Parsers/ExtensionEnumeratorTest.swift @@ -3,7 +3,7 @@ // Unit Tests // // Created by Nico Verbruggen on 30/10/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Parsers/HomebrewPackageTest.swift b/tests/unit/Parsers/HomebrewPackageTest.swift index dc1d0703..f002d5c3 100644 --- a/tests/unit/Parsers/HomebrewPackageTest.swift +++ b/tests/unit/Parsers/HomebrewPackageTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 14/02/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing @@ -53,7 +53,7 @@ struct HomebrewPackageTest { /// or the JSON API of the Homebrew output may have changed. @Test(.disabled("Uses system command; enable at your own risk")) func can_parse_services_json_from_cli_output() async throws { - let container = Container.real() + let container = Container.real(minimal: true) let services = try! JSONDecoder().decode( [HomebrewService].self, @@ -76,7 +76,7 @@ struct HomebrewPackageTest { /// or the JSON API of the Homebrew output may have changed. @Test(.disabled("Uses system command; enable at your own risk")) func can_load_extension_json_from_cli_output() async throws { - let container = Container.real() + let container = Container.real(minimal: true) let package = try! JSONDecoder().decode( [HomebrewPackage].self, diff --git a/tests/unit/Parsers/HomebrewUpgradableTest.swift b/tests/unit/Parsers/HomebrewUpgradableTest.swift index ba297ef8..fd65f88d 100644 --- a/tests/unit/Parsers/HomebrewUpgradableTest.swift +++ b/tests/unit/Parsers/HomebrewUpgradableTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 17/03/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Parsers/NginxConfigurationTest.swift b/tests/unit/Parsers/NginxConfigurationTest.swift index 8217de67..ccb5f744 100644 --- a/tests/unit/Parsers/NginxConfigurationTest.swift +++ b/tests/unit/Parsers/NginxConfigurationTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 29/11/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing @@ -13,7 +13,7 @@ struct NginxConfigurationTest { var container: Container init () async throws { - container = Container.real() + container = Container.real(minimal: true) } // MARK: - Test Files diff --git a/tests/unit/Parsers/PhpConfigurationFileTest.swift b/tests/unit/Parsers/PhpConfigurationFileTest.swift index 0dd0a8cf..bd6b973a 100644 --- a/tests/unit/Parsers/PhpConfigurationFileTest.swift +++ b/tests/unit/Parsers/PhpConfigurationFileTest.swift @@ -3,18 +3,17 @@ // PHP Monitor // // Created by Nico Verbruggen on 04/05/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing import Foundation -@Suite(.serialized) class PhpConfigurationFileTest { var container: Container init() { - self.container = Container.real() + self.container = Container.real(minimal: true) } static var phpIniFileUrl: URL { @@ -46,7 +45,7 @@ class PhpConfigurationFileTest { #expect(iniFile.get(for: "display_errors") == "On") } - @Test func can_customize_configuration_value() throws { + @Test func can_customize_configuration_value() async throws { let destination = Utility .copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! @@ -56,7 +55,7 @@ class PhpConfigurationFileTest { #expect(configurationFile.get(for: "error_reporting") == "E_ALL") // 1. Change the value - try! configurationFile.replace( + try! await configurationFile.replace( key: "error_reporting", value: "E_ALL & ~E_DEPRECATED & ~E_STRICT" ) @@ -66,14 +65,14 @@ class PhpConfigurationFileTest { ) // 2. Ensure that same key and value doesn't break subsequent saves - try! configurationFile.replace( + try! await configurationFile.replace( key: "error_reporting", value: "error_reporting" ) #expect(configurationFile.get(for: "error_reporting") == "error_reporting") // 3. Verify subsequent saves weren't broken - try! configurationFile.replace( + try! await configurationFile.replace( key: "error_reporting", value: "E_ALL" ) diff --git a/tests/unit/Parsers/PhpExtensionTest.swift b/tests/unit/Parsers/PhpExtensionTest.swift index 5660e0dc..2b348e9b 100644 --- a/tests/unit/Parsers/PhpExtensionTest.swift +++ b/tests/unit/Parsers/PhpExtensionTest.swift @@ -3,31 +3,28 @@ // PHP Monitor // // Created by Nico Verbruggen on 13/02/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing import Foundation -@Suite(.serialized) struct PhpExtensionTest { - var container: Container - - init () async throws { - container = Container.real() - } - static var phpIniFileUrl: URL { TestBundle.url(forResource: "php", withExtension: "ini")! } @Test func can_load_extension() throws { + let container = Container.real(minimal: true) + let extensions = PhpExtension.from(container, filePath: Self.phpIniFileUrl.path) #expect(!extensions.isEmpty) } @Test func extension_name_is_correct() throws { + let container = Container.real(minimal: true) + let extensions = PhpExtension.from(container, filePath: Self.phpIniFileUrl.path) let extensionNames = extensions.map { (ext) -> String in @@ -47,6 +44,8 @@ struct PhpExtensionTest { } @Test func extension_status_is_correct() throws { + let container = Container.real(minimal: true) + let extensions = PhpExtension.from(container, filePath: Self.phpIniFileUrl.path) // xdebug should be enabled @@ -57,6 +56,7 @@ struct PhpExtensionTest { } @Test func toggle_works_as_expected() async throws { + let container = Container.real(minimal: true) let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! let extensions = PhpExtension.from(container, filePath: destination.path) #expect(extensions.count == 6) diff --git a/tests/unit/Parsers/ValetConfigurationTest.swift b/tests/unit/Parsers/ValetConfigurationTest.swift index 5dd2d186..fc81cd48 100644 --- a/tests/unit/Parsers/ValetConfigurationTest.swift +++ b/tests/unit/Parsers/ValetConfigurationTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 29/11/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Parsers/ValetRcTest.swift b/tests/unit/Parsers/ValetRcTest.swift index 675d6584..435ef63f 100644 --- a/tests/unit/Parsers/ValetRcTest.swift +++ b/tests/unit/Parsers/ValetRcTest.swift @@ -3,7 +3,7 @@ // Unit Tests // // Created by Nico Verbruggen on 20/01/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Testables/Filesystem/RealFileSystemTest.swift b/tests/unit/Testables/Filesystem/RealFileSystemTest.swift index 65670ba0..783c36d7 100644 --- a/tests/unit/Testables/Filesystem/RealFileSystemTest.swift +++ b/tests/unit/Testables/Filesystem/RealFileSystemTest.swift @@ -3,26 +3,23 @@ // Unit Tests // // Created by Nico Verbruggen on 02/11/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing import Foundation -@Suite(.serialized) // serialized due to how unique temp directory works struct RealFileSystemTest { var filesystem: FileSystemProtocol init() throws { - let container = Container() - container.bind() - + let container = Container.real(minimal: true) filesystem = container.filesystem } private func createUniqueTemporaryDirectory() -> String { let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true) - let fullTempDirectoryPath = tempDirectoryURL.appendingPathComponent("phpmon-fs-tests").path + let fullTempDirectoryPath = tempDirectoryURL.appendingPathComponent("phpmon-fs-tests-\(UUID().uuidString)").path try? FileManager.default.removeItem(atPath: fullTempDirectoryPath) try! FileManager.default.createDirectory(atPath: fullTempDirectoryPath, withIntermediateDirectories: false) return fullTempDirectoryPath diff --git a/tests/unit/Testables/Filesystem/TestableFileSystemTest.swift b/tests/unit/Testables/Filesystem/TestableFileSystemTest.swift index 707df048..1dc60161 100644 --- a/tests/unit/Testables/Filesystem/TestableFileSystemTest.swift +++ b/tests/unit/Testables/Filesystem/TestableFileSystemTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 01/11/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Testables/Shell/RealShellTest.swift b/tests/unit/Testables/Shell/RealShellTest.swift index e4656652..2b38e950 100644 --- a/tests/unit/Testables/Shell/RealShellTest.swift +++ b/tests/unit/Testables/Shell/RealShellTest.swift @@ -3,19 +3,18 @@ // PHP Monitor // // Created by Nico Verbruggen on 28/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing import Foundation -@Suite(.serialized) struct RealShellTest { var container: Container init() async throws { // Reset to the default shell - container = Container.real() + container = Container.real(minimal: true) } @Test func system_shell_is_default() async { @@ -58,7 +57,7 @@ struct RealShellTest { @Test func system_shell_can_timeout_and_throw_error() async { await #expect(throws: ShellError.timedOut) { try await container.shell.attach( - "php -r \"sleep(1);\"", + "php -r \"sleep(30);\"", didReceiveOutput: { _, _ in }, withTimeout: .seconds(0.1) ) @@ -75,6 +74,54 @@ struct RealShellTest { } let duration = start.duration(to: .now) - #expect(duration < .milliseconds(2000)) // Should complete in ~700ms if parallel + #expect(duration < .milliseconds(3000)) // Should complete in ~700ms if parallel + } + + /** + This test verifies that concurrent writes to `output.out` and `output.err` + from multiple readability handlers don't cause data races or crashes, + and that the output is correct (for both stdout and stderr output). + + When Thread Sanitizer is enabled, this will also check if any potential + data races occur. None should, at this point. You can enable the + Thread Sanitizer by editing the Test Plan's Configurations. + + This test was added specifically to diagnose and fix one such reported + data race, which was fixed by adding a serial queue to the shell's + `attach()` method, since the readability handlers actually run + on separate threads. + */ + @Test func attach_handles_concurrent_stdout_stderr_writes_safely() async throws { + // Create a PHP script that will output lots of text to STDOUT and STDERR. + let phpScript = "php -r 'for ($i = 1; $i <= 500; $i++) { fwrite(STDOUT, \"stdout-$i\" . PHP_EOL); fwrite(STDERR, \"stderr-$i\" . PHP_EOL); flush(); }'" + + // Keep track of the total chunk count + var receivedChunks = 0 + + // We will now test the attach method + let (_, shellOutput) = try await container.shell.attach( + phpScript, + didReceiveOutput: { _, _ in + receivedChunks += 1 + }, + withTimeout: 5.0 + ) + + // Verify all output was captured without corruption + let stdoutLines = shellOutput.out + .components(separatedBy: "\n") + .filter { !$0.isEmpty } + let stderrLines = shellOutput.err + .components(separatedBy: "\n") + .filter { !$0.isEmpty } + + #expect(stdoutLines.count == 500) + #expect(stderrLines.count == 500) + + // Verify content integrity + for i in 1...200 { + #expect(stdoutLines.contains("stdout-\(i)")) + #expect(stderrLines.contains("stderr-\(i)")) + } } } diff --git a/tests/unit/Testables/Shell/TestableShellTest.swift b/tests/unit/Testables/Shell/TestableShellTest.swift index b74a31d0..036d9415 100644 --- a/tests/unit/Testables/Shell/TestableShellTest.swift +++ b/tests/unit/Testables/Shell/TestableShellTest.swift @@ -3,13 +3,12 @@ // PHP Monitor // // Created by Nico Verbruggen on 20/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing import Foundation -@Suite(.serialized) struct TestableShellTest { @Test func fake_shell_output_can_be_declared() async { let greeting = BatchFakeShellOutput(items: [ diff --git a/tests/unit/Testables/TestableConfigurationTest.swift b/tests/unit/Testables/TestableConfigurationTest.swift index 8719e9b1..595e5eef 100644 --- a/tests/unit/Testables/TestableConfigurationTest.swift +++ b/tests/unit/Testables/TestableConfigurationTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/10/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing @@ -11,7 +11,7 @@ import Foundation struct TestableConfigurationTest { @Test func configuration_can_be_saved_as_json() async { - let container = Container.real() + let container = Container.real(minimal: true) // WORKING var configuration = TestableConfigurations.working diff --git a/tests/unit/Testables/WebApi/RealWebApiTest.swift b/tests/unit/Testables/WebApi/RealWebApiTest.swift index 0a0995a3..1c4c848b 100644 --- a/tests/unit/Testables/WebApi/RealWebApiTest.swift +++ b/tests/unit/Testables/WebApi/RealWebApiTest.swift @@ -13,8 +13,7 @@ struct RealWebApiTest { private var container: Container init() throws { - self.container = Container() - container.bind() + self.container = Container.real(minimal: true) } var WebApi: RealWebApi { diff --git a/tests/unit/Versions/AppVersionTest.swift b/tests/unit/Versions/AppVersionTest.swift index 47a230cc..5fd966a5 100644 --- a/tests/unit/Versions/AppVersionTest.swift +++ b/tests/unit/Versions/AppVersionTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 10/05/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Versions/PhpVersionDetectionTest.swift b/tests/unit/Versions/PhpVersionDetectionTest.swift index 74615174..a704bf5a 100644 --- a/tests/unit/Versions/PhpVersionDetectionTest.swift +++ b/tests/unit/Versions/PhpVersionDetectionTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 01/04/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Versions/PhpVersionNumberTest.swift b/tests/unit/Versions/PhpVersionNumberTest.swift index 0834d2f2..8c04c41b 100644 --- a/tests/unit/Versions/PhpVersionNumberTest.swift +++ b/tests/unit/Versions/PhpVersionNumberTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 23/01/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Versions/ValetVersionExtractorTest.swift b/tests/unit/Versions/ValetVersionExtractorTest.swift index a7a3fb68..c3cfdab7 100644 --- a/tests/unit/Versions/ValetVersionExtractorTest.swift +++ b/tests/unit/Versions/ValetVersionExtractorTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 29/11/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Versions/VersionExtractorTest.swift b/tests/unit/Versions/VersionExtractorTest.swift index 3d9be867..4e5c275d 100644 --- a/tests/unit/Versions/VersionExtractorTest.swift +++ b/tests/unit/Versions/VersionExtractorTest.swift @@ -3,7 +3,7 @@ // PHP Monitor // // Created by Nico Verbruggen on 16/12/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. +// Copyright © 2025 Nico Verbruggen. All rights reserved. // import Testing diff --git a/tests/unit/Watchers/FSNotifierTest.swift b/tests/unit/Watchers/FSNotifierTest.swift new file mode 100644 index 00000000..a9ca987f --- /dev/null +++ b/tests/unit/Watchers/FSNotifierTest.swift @@ -0,0 +1,101 @@ +// +// FSNotifierTest.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 29/11/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Testing +import Foundation + +struct FSNotifierTest { + + @Test func notifier_fires_when_file_is_modified() async throws { + // Create a temporary file to monitor + let tempDir = FileManager.default.temporaryDirectory + let testFile = tempDir.appendingPathComponent("fs_notifier_test_\(UUID().uuidString).txt") + FileManager.default.createFile(atPath: testFile.path, contents: nil) + + // Our variable to keep track of + let eventFired = Locked(0) + + // Our debouncer + let debouncer = Debouncer() + + // Set up the notifier + let notifier = FSNotifier(for: testFile, eventMask: .write, onChange: { + Task { await debouncer.debounce(for: 1.0) { + eventFired.value += 1 + }} + }) + + // Cleanup for later + defer { + try? FileManager.default.removeItem(at: testFile) + notifier.terminate() + } + + // Modify the file, twice, debounce should work + try "hello".write(to: testFile, atomically: false, encoding: .utf8) + try "hello".write(to: testFile, atomically: false, encoding: .utf8) + + // Wait for the event to fire, verify it fired ONCE after 1 second debounce + await delay(seconds: 1.2) + #expect(eventFired.value == 1) + + // Try to write again (after debounce timing) + try "hello".write(to: testFile, atomically: false, encoding: .utf8) + + // Verify after another second, our second write is actually noted + await delay(seconds: 1.2) + #expect(eventFired.value == 2) + } + + @Test func notifier_suspends_and_resumes_correctly() async throws { + // Create a temporary file to monitor + let tempDir = FileManager.default.temporaryDirectory + let testFile = tempDir.appendingPathComponent("fs_notifier_test_\(UUID().uuidString).txt") + FileManager.default.createFile(atPath: testFile.path, contents: nil) + + // Our variable to keep track of + let eventFired = Locked(0) + + // Create notifier + let notifier = FSNotifier(for: testFile, eventMask: .write, onChange: { + Task { eventFired.value += 1 } + }) + + // Cleanup for later + defer { + try? FileManager.default.removeItem(at: testFile) + notifier.terminate() + } + + // Modify the file, twice + try "hello".write(to: testFile, atomically: false, encoding: .utf8) + await delay(seconds: 0.2) + #expect(eventFired.value == 1) + + // Try to write again (after debounce timing) + try "hello".write(to: testFile, atomically: false, encoding: .utf8) + await delay(seconds: 0.2) + #expect(eventFired.value == 2) + + // Now, we will suspend + await notifier.suspend() + + // Despite writing to the file, our event did not fire + try "hello".write(to: testFile, atomically: false, encoding: .utf8) + await delay(seconds: 0.2) + #expect(eventFired.value == 2) + + // Now, we will resume + await notifier.resume() + + // Our event should have fired again + try "hello".write(to: testFile, atomically: false, encoding: .utf8) + await delay(seconds: 0.2) + #expect(eventFired.value == 3) + } +}