1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-07 20:10:08 +02:00

Compare commits

...

123 Commits

Author SHA1 Message Date
80bcbd085e 🚀 Version 25.07 2025-07-28 11:23:01 +02:00
ca65fca77d 🐛 Fix issue with securing domains
If you serve a single folder locally multiple times, e.g. as
`cdn.mydomain.test` and `mydomain.test`, securing would fail
for domain that came alphabetically last.

This has been resolved if you are running Valet 3 or newer by
leveraging the `valet secure $domain` syntax.
2025-07-28 11:22:23 +02:00
96975f8e57 ♻️ Clean up startup timers 2025-07-24 12:32:22 +02:00
729c1e8f2f Add driver to main menu 2025-07-23 17:24:54 +02:00
e94377ebb1 🐛 Prevent timeout message from showing incorrectly 2025-07-23 16:49:54 +02:00
769779970b 🔧 Bump build for EAP 2025-06-27 21:05:19 +02:00
2a27989a96 Detect Tempest framework 2025-06-27 20:59:26 +02:00
79430f7581 🚀 Version 25.06 2025-06-27 19:35:10 +02:00
76f585a50e 🍱 Updated icon asset 2025-06-21 20:38:36 +02:00
1bbc77967b 🍱 New EAP icon for new build 2025-06-20 16:00:58 +02:00
1e4e2afe68 🔧 Bump build 2025-06-20 15:21:14 +02:00
3caf90b0cd 🔧 Update schemas 2025-06-20 15:19:57 +02:00
445acffa4c 🍱 Further design tweaks 2025-06-20 12:56:58 +02:00
e90c068e95 🍱 Tweak design of icon, unify 2025-06-20 12:18:57 +02:00
c9428c300c 🍱 Apply fill to icon 2025-06-20 11:41:02 +02:00
807d9c55a4 🔧 Bump build for EAP 2025-06-19 13:29:52 +02:00
ca167537cf 🍱 Add icons to context menu 2025-06-19 13:28:58 +02:00
8ae2031ba5 🍱 Add icons to main menu 2025-06-19 12:50:04 +02:00
ec0ad13ad0 🔧 Bump build for EAP 2025-06-17 12:12:37 +02:00
9988b775c9 🐛 Add spinner back to domain list 2025-06-17 12:01:25 +02:00
8e24851014 🍱 Add icon w/ Icon Composer 2025-06-10 19:45:43 +02:00
c1d90cb909 🔧 Plan for version 25 later this year
PHP Monitor 25 will use a new numbering scheme, following the format:

25.06 = 25 . 06 . 0
        M    m    p

(Minor, minor and patch.)

So what was supposed to be PHP Monitor 7.3 will now be version 25, with
the minor version number decided by the release month.

The cut-off date for PHP Monitor 7.2 is currently 2025-11-30, so I have
time but I'd prefer to have two releases out this year:

- One release migrating to the new numbering scheme
- One release with full support for macOS 26

Since I plan on only doing maintenance releases with no patches except
when bugs pop up, the major/minor version notation combination should
suffice.

With this release, the minimum supported version of macOS will become
macOS Ventura 13.5.

That means that support is dropped for Monterey and Ventura's older
builds. Since Homebrew is also changing with built-in service management
I think it's time to drop support for these older versions of macOS,
which I no longer intend to test for.

Older versions of PHP Monitor will, of course, remain functional, but
since Homebrew is an ever-changing thing, I cannot guarantee nothing
will break in those older versions with the newer API.
2025-06-10 18:33:50 +02:00
4827c4a44b 🚀 Version 7.2 2025-03-22 12:05:54 +01:00
201554b237 🔧 Bump version to 7.2 2025-03-22 11:33:52 +01:00
e4bf6a9655 📝 Bump build, update credits and add sponsors 2025-03-22 11:32:59 +01:00
f9ea654ffc 🔧 Tweak brew output parsing
Improved the accuracy of the brew output. Often, when multiple console
messages were returned, the progress prompt in the PHP version manager
would display the earliest found step, not the latest, thus unfortunately
misrepresenting the progress of the installation steps.

This fixes that by reversing the return order, but also extracts
relevant information from the commands, too, so that contextual info
is now included (for pouring, installing and downloading steps).

(This makes it a little bit more transparent for the end user to find
out what is taking up all this time. I wish that Homebrew was faster,
too, but there's a reason I'm not using statically compiled PHP for
this project. Either way, this is a nice QoL change.)
2025-03-22 11:15:47 +01:00
8677628850 🔧 Bump build for new EAP release 2025-03-01 12:59:09 +01:00
e09b0156df 📝 Update README 2025-03-01 12:58:36 +01:00
41a83b1d91 🔨 Update .gitignore 2025-02-28 13:20:32 +01:00
a9e97b0bc9 🌐 Add Indonesian translations
These translations were added via a locally ran LLM.
If you identify any mistakes, please get in touch!
2025-02-21 13:27:39 +01:00
e4e12799ef 🌐 Add Spanish translations
These translations were added via a locally ran LLM. I know some Spanish, but
if you identify any mistakes, please get in touch!
2025-02-20 19:54:04 +01:00
4e25fffa7d 🌐 Add missing translations 2025-02-20 19:21:25 +01:00
3e319cd50f 🚀 Version 7.1 2024-11-26 15:24:18 +01:00
595dc8c028 Add info about test failures 2024-11-26 15:22:03 +01:00
f7b1679e97 🐛 Serial dispatch queue for test FS 2024-11-26 14:17:06 +01:00
9f1761d68e Checked and updated tests 2024-11-26 13:44:08 +01:00
871480d70c 📝 Updated README.md, SECURITY.md 2024-11-26 12:56:38 +01:00
2b1c1c12f8 Add info button for PHP upgrades 2024-11-25 16:43:19 +01:00
a22346ed35 🐛 Fix issue with formulae upgrades for tap 2024-11-25 14:04:17 +01:00
e3fa34d4f9 🔨 Adjust URL for unavailable PHP (wiki) 2024-11-25 13:40:02 +01:00
3d225ea79f ✍️ Clarify text about upgrading PHP (EN only) 2024-11-22 19:09:10 +01:00
d2cd387c18 Add button that redirects to wiki 2024-11-22 18:23:28 +01:00
48bb782e33 🚧 WIP: Changes related to unavailable formulae 2024-11-22 17:42:28 +01:00
9710ffa8da 🚧 WIP: Handle temporarily unavailable formulae 2024-11-22 13:26:09 +01:00
46408f5ee5 Indicate DEV builds in PHP Version Manager 2024-11-15 16:43:59 +01:00
2c39f1db8b 🔧 Upgrade checks for Xcode 16.1 2024-11-15 16:10:25 +01:00
f20286cbd9 Notify user if startup takes too long (> 30s) 2024-11-15 16:03:44 +01:00
f1fe42e563 Updated constants for PHP 8.4 & 8.5 support
Thankfully, these changes are simple. Before releasing, I will be
testing the new build, though.

Here's what constants I changed, and why:

- Homebrew PHP formulae are now consistently sourced from the
  `shivammathur/php` tap. This should make the transition to new PHP
  releases a little bit easier, but I need to verify this works without
  issues before publishing this update.

- Bumped the PHP formulae cutoff date to Nov 30, 2025.
  At this point, PHP 8.5 should be released.

- Added support for pre-release (daily) versions of PHP 8.5.
2024-11-15 15:22:53 +01:00
94abfe4b49 🐛 Fix crash bug (oops) 2024-10-31 22:51:49 +01:00
9778fd5c7b 🚀 Version 7.0.6 2024-10-31 22:43:03 +01:00
dba2ce5bf3 🚀 Version 7.0.5 2024-10-31 22:15:11 +01:00
4644c1ada4 👌 Move cut-off date to PHP 8.4 release day 2024-10-31 22:14:53 +01:00
cef19243ee 🔧 Bump build 2024-10-31 21:59:42 +01:00
b319ecab59 👌 Move cut-off date to PHP 8.4 release day 2024-10-31 21:56:13 +01:00
a47b139d92 👌 Cleanup 2024-08-31 22:14:56 +02:00
e026ecf60d ♻️ Various extension list improvements (#274)
Installing and removing extensions now scrolls to the extension afterwards, and animates this. This is done to emphasise that the operation succeeded.
2024-08-31 15:57:08 +02:00
3c0a4a6142 🌐 Updated localization 2024-08-26 15:04:37 +02:00
87ebb20284 ♻️ Use alternate cell identifier for favorite 2024-08-25 16:21:16 +02:00
d60c26c9b2 UserDefaults-backed storage for Favorites 2024-08-25 14:35:11 +02:00
5c9c51f580 Mark domain as favorite (UI only)
Please note that this functionality is currently not persistent.
As such, reloading the domain list will reset any changes you have made.
2024-08-25 13:35:27 +02:00
0c320074da ♻️ Avoid using non-Sendable Timer 2024-08-06 14:15:37 +02:00
e3ea712a99 Ensure all tests run and pass 2024-07-16 18:38:56 +02:00
4db478ca64 👌 Update copyright information 2024-07-16 18:30:56 +02:00
3064a07d69 📦 Use NVAppUpdater and NVAlert packages 2024-07-16 18:29:11 +02:00
f3e1b4de6f 🚀 Version 7.0.4 2024-06-28 11:47:25 +02:00
a3226b632f 🐛 Restart PHP-FPM after managing extensions
This is done to prevent issues like #292.
2024-06-28 11:37:01 +02:00
652878d97f 👌 Remove unused WIP class 2024-06-28 11:14:20 +02:00
032610ad5c 🌐 Add localization for "Actions" 2024-06-28 11:12:55 +02:00
2c2627dc9f Add AppMenu class for easy access to main menu 2024-06-27 21:23:59 +02:00
62587bdf65 WIP: Add context menu to main menu 2024-06-11 18:53:55 +02:00
5e9dae78f5 🐛 Fix Bedrock project detection (#288) 2024-06-06 21:30:06 +02:00
949ba5b559 🔧 Build self-updater as part of main target
The self-updater is now a requirement for the main app to be able to be
built. You no longer need the existing binary. This makes it easier for
anyone to just try out the app locally and makes reproducible builds
also possible.

(This is done because the self-updater code will soon be moved to a
separate package, and I want to make this entire updater process
as simple as possible.)

In order to avoid the self-updater app from appearing as a product when
archiving a build, SKIP_INSTALL is set to true. This avoids a variety
of annoying issues including the archive appearing under "Other Items".
2024-05-31 23:54:42 +02:00
ce88f897ef 🌐 Added Chinese translations, credits
- Chinese translations contributed by @guanguans (via #285).
- Updated the credits so that all translators are now also listed
  separately, since the GitHub issue has been closed.
2024-05-02 10:24:47 +02:00
fa9b51aaab 🚀 Version 7.0.3 2024-04-10 13:07:58 +02:00
b8affad5ee 📝 Updated SECURITY.md 2024-04-10 13:07:30 +02:00
41e5f5b4c3 🔧 Bump build for EAP release 2024-04-09 12:33:55 +02:00
79f6a60a16 👌 Cleanup dependencies for site isolation 2024-04-03 14:50:05 +02:00
06bc4ddb9a 👌 Improve removing indirect dependency
Mind you, the interaction with the domain list controller also needs to
be abstracted away, but this is fine, for now.
2024-04-03 14:11:45 +02:00
bf728a24f0 👌 Fix PHP version suggestions in popover
- If your Valet installation supports using site isolation, that will
  be displayed as the suggestion. If not, the traditional "Switch to
  PHP" options will be available.

- Overflowing buttons have been fixed. Three columns are now used with
  a LazyVGrid, to prevent text from being unreadable.
2024-04-02 15:25:06 +02:00
b7cad3af62 👌 Fix Swift 5.10 concurrency warnings 2024-03-30 17:40:09 +01:00
4a3dee3c50 🚀 Version 7.0.2 2024-03-30 17:14:04 +01:00
9d5a0ed745 🔧 Xcode upgrade check 2024-03-20 13:46:56 +01:00
b3b509409a Fix test 2024-03-20 10:47:52 +01:00
4934f35d0b 🍱 Bump width of no results view 2024-03-19 14:23:19 +01:00
92e7418158 👌 Cleanup 2024-03-19 14:22:10 +01:00
52ea64db40 👌 Use base translation for no domain results 2024-03-19 14:20:34 +01:00
f66e9b7340 Add UnavailableContentView to domains 2024-03-19 14:10:08 +01:00
2bf28fe247 👌 Allow resizable windows 2024-03-16 00:32:05 +01:00
c6e4f785bc 🍱 Use PHP icon for phpman 2024-03-15 23:38:12 +01:00
94fe7df3bd 🍱 Custom button for "upgrade all" 2024-03-15 23:18:09 +01:00
f373621a4a 👌 Polish PHP installation management
- Make spinner view opaque to hide incorrect info
  when refreshing PHP installations
- Resolve each PHP installation to a Homebrew version
- Fix an issue where certain PHP upgrades don't show up

In this build, resolving PHP upgrades happens based on the formula name.

This avoids issues where available PHP version upgrades would *not* be
detected correctly (based on the installed version).
2024-03-15 22:09:59 +01:00
5104a865fb 🚀 Version 7.0.1 2024-02-12 18:28:45 +01:00
7b10973330 🔧 Bump build 2024-02-12 16:33:01 +01:00
bc208bddf9 👌 Notify if list of extensions is empty (#275) 2024-02-12 16:30:18 +01:00
321b4aaf8b 🐛 Fix extension detection on Intel (#275) 2024-02-12 15:26:48 +01:00
b26fc3bc4b 🚀 Version 7.0 2024-02-11 21:35:40 +01:00
f758c5d63a 👌 Cut off bottom of marketing screenshot 2024-02-10 16:18:58 +01:00
c7510d778d 🍱 Update marketing screenshot 2024-02-10 16:17:24 +01:00
70c5aadb7f 🔧 Bump build for PHP Monitor EAP 2024-01-25 13:48:39 +01:00
a731f15cf7 🐛 Prevent PHP dropdown from resetting 2024-01-21 14:42:06 +01:00
ab4c436202 🔧 Bump build for PHP Monitor EAP 2024-01-21 14:23:52 +01:00
c0231690d4 🐛 Fix alternative installation check 2024-01-21 14:22:51 +01:00
988e9d3351 👌 Cleanup and add comments 2024-01-13 13:24:06 +01:00
2f119d4332 Fix even more tests 2024-01-13 13:22:21 +01:00
d83c629a7b Fix some tests 2024-01-13 12:46:33 +01:00
e7d98dbeae 🔧 Bump build for PHP Monitor EAP 2024-01-09 21:24:50 +01:00
f3d5946743 🌐 Localize extension management (for domains) 2024-01-09 21:20:43 +01:00
7728a1125c 📝 Update README to reflect php = PHP 8.3 2024-01-09 21:00:06 +01:00
3612351df7 📝 Update README to reflect php = PHP 8.3 2024-01-09 20:59:52 +01:00
8e912151fb Allow toggling extensions via site list 2024-01-09 20:55:16 +01:00
3a2209e604 🌐 Add translations for language choice 2024-01-09 20:06:40 +01:00
1f0b56cab6 Language choice, prompt user to restart app 2024-01-09 20:01:08 +01:00
e08d970edd 🏗️ WIP: Load preferred language strings 2024-01-09 19:44:29 +01:00
32c757e711 🏗️ WIP: Language override option 2024-01-09 19:28:39 +01:00
480cdb94ae 🏗️ WIP: Language picker, fix SelectPreferenceView 2024-01-09 18:58:02 +01:00
7fbcac5dc2 👌 Check symlinks after PHP version modification 2023-12-27 14:47:21 +01:00
4edb5f5015 👌 Use generated asset catalog symbol extensions 2023-12-27 13:03:24 +01:00
294f84ccb2 👌 Further cleanup 2023-12-27 12:57:08 +01:00
155b57eb9e 🐛 Add incorrect PHP symlink purge (#270)
This commit introduces a new diagnostics feature which is executed
when the app boots. When the app boots, the integrity of the PHP
symlinks are checked to ensure that all symlinks correctly link to
a valid PHP version. If any links to an incorrect PHP version,
then those outdated or incorrect symlinks will be removed.

For example, if `php@8.2` links to `../Cellar/php/8.3.0` then that is
an obvious reason for that symlink to be purged because it links to
the incorrect version. (This example behaviour has been noted in #270.)

This occurs prior to the rest of the startup process, so this way
no incorrect PHP versions can pop up in the version switcher, and
no incorrect PHP version is reported as being installed when
managing installed PHP versions.
2023-12-27 12:40:35 +01:00
ff61d8c52e 🚀 Version 6.2.2 2023-11-24 23:30:12 +01:00
da41673855 Fix broken tests 2023-11-24 23:29:25 +01:00
5bda727981 🔧 Bump version 2023-11-24 22:58:04 +01:00
8790b30706 🚀 Version 6.2.1 2023-11-02 17:17:40 +01:00
178 changed files with 5049 additions and 1256 deletions

4
.gitignore vendored
View File

@ -1,6 +1,4 @@
phpmon.xcodeproj/project.xcworkspace
phpmon.xcodeproj/xcuserdata
PHP Monitor.xcodeproj/project.xcworkspace PHP Monitor.xcodeproj/project.xcworkspace
PHP Monitor.xcodeproj/xcuserdata PHP Monitor.xcodeproj/xcuserdata
phpmon-updater/PHP Monitor Self-Updater.app/
.DS_Store .DS_Store

View File

@ -14,6 +14,17 @@ It also automatically runs when you try to build the project. You'll get a warni
swiftlint --fix swiftlint --fix
``` ```
## 📦 Swift Packages
Starting from PHP Monitor 7.1, the app now uses various first-party package dependencies.
The following package dependencies are in use:
* [`NVAppUpdater`](https://github.com/nicoverbruggen/NVAppUpdater)
* [`NVAlert`](https://github.com/nicoverbruggen/NVAlert)
You may need an internet connection to download these dependencies, or you can also clone the dependencies and include them manually.
## ⚙️ Preferences ## ⚙️ Preferences
You can find the persisted configuration file in `~/Library/Preferences/com.nicoverbruggen.phpmon.plist` You can find the persisted configuration file in `~/Library/Preferences/com.nicoverbruggen.phpmon.plist`
@ -33,16 +44,18 @@ defaults delete com.nicoverbruggen.phpmon && killall cfprefsd
If you'd like to build PHP Monitor yourself, you need: If you'd like to build PHP Monitor yourself, you need:
* Xcode (usually the latest version) * Xcode (usually the latest version)
* *PHP Monitor Self-Updater.app* in the `phpmon-updater` directory (You can build it yourself, it is included as a target OR copy the signed app so it is included w/ PHP Monitor)
* The contents of this repository
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.) Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
**Important**: The updater now gets automatically built and included as part of the main target.
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive. If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
### PHP Monitor Updater ## ✅ Testing
Select the separate target and build. You can then copy the product to the `phpmon-updater` directory. The binary will be re-signed when distributing the main build. In order to properly test everything, you will want to use the _PHP Monitor DEV_ target. There are unit and UI tests both.
You may sporadically see failures in UI tests due to the following error: `Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY`. This seems to be an issue with Xcode that Apple may need to resolve? You can retry the tests in question and they should eventually pass.
## 🚀 Release procedure ## 🚀 Release procedure

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1640"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1640"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1640"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1640"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1640"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,7 +1,7 @@
> **Note** > **Note**
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider [sponsoring](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️ > If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider [sponsoring](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️
<p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p> <p align="center"><img src="./docs/logo.svg" alt="PHP Monitor Logo" width="500px" /></p>
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up if you want to use all of the functionality of the app</u> (consult the FAQ below with info about how to set up your environment). **PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up if you want to use all of the functionality of the app</u> (consult the FAQ below with info about how to set up your environment).
@ -22,7 +22,7 @@ You can also add new domains as links, isolate sites, manage various services, a
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs. PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* Your user account can administer your computer (required for some functionality, e.g. certificate generation) * Your user account can administer your computer (required for some functionality, e.g. certificate generation)
* macOS 12.4 or later (Monterey, Ventura and Sonoma are supported) * macOS 13.5 or later
* Homebrew is installed in the default location (`/usr/local/homebrew` or `/opt/homebrew`) * Homebrew is installed in the default location (`/usr/local/homebrew` or `/opt/homebrew`)
* Homebrew `php` formula is installed * Homebrew `php` formula is installed
* Optional but recommended: Laravel Valet * Optional but recommended: Laravel Valet
@ -84,9 +84,13 @@ Initially, I had an Alfred workflow for this — but it has now been replaced wi
## 🐘 Why not use Laravel Herd? ## 🐘 Why not use Laravel Herd?
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. _**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 need more customization and flexibility I encourage you to consider PHP Monitor in combination with Laravel Valet or some other solution like Docker (with Laravel Sail, for example). 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.
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.
## 🤬 The app won't start?! ## 🤬 The app won't start?!
@ -120,7 +124,7 @@ For maximum compatibility with older PHP versions, you may wish to keep using Va
<details> <details>
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary> <summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2. Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.3.
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.) You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)

View File

@ -2,21 +2,26 @@
## Supported versions ## Supported versions
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up): Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up).
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version | | Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- | ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 6.2 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum | | 25 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+)<br/>Tahoe (26.0+)* | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
(*) Denotes preliminary supported based on the app being built with the latest version of the SDK prior to the release of the latest release of macOS. Please check out the pinned issue for more information.
## Legacy versions ## Legacy versions
These versions of PHP Monitor are no longer supported, but if youre using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions. These versions of PHP Monitor are no longer supported, but if youre using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version | | Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- | ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum | | 7.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum | | 7.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum | | 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum | | 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum | | 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 | | 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

1
assets/icon-2025.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.25033,-0.175723,0.175723,1.25033,14.4412,107.226)">
<path d="M133.3,83.75L120.4,83.75L120.4,54.437C120.4,52.134 118.465,50.25 116.1,50.25L107.5,50.25C105.135,50.25 103.2,52.134 103.2,54.438L103.2,96.312C103.2,98.616 105.135,100.5 107.5,100.5L133.3,100.5C135.665,100.5 137.6,98.616 137.6,96.312L137.6,87.938C137.6,85.634 135.665,83.75 133.3,83.75ZM335.4,184.25L326.8,184.25L326.8,127.666C326.8,121.019 324.059,114.633 319.221,109.922L265.525,57.63C260.688,52.92 254.13,50.25 247.304,50.25L223.6,50.25L223.6,25.125C223.6,11.254 212.044,0 197.8,0L25.8,0C11.556,0 0,11.254 0,25.125L0,192.625C0,206.496 11.556,217.75 25.8,217.75L34.4,217.75C34.4,245.492 57.513,268 86,268C114.487,268 137.6,245.492 137.6,217.75L206.4,217.75C206.4,245.492 229.512,268 258,268C286.488,268 309.6,245.492 309.6,217.75L335.4,217.75C340.13,217.75 344,213.981 344,209.375L344,192.625C344,188.019 340.13,184.25 335.4,184.25ZM86,242.875C71.756,242.875 60.2,231.621 60.2,217.75C60.2,203.879 71.756,192.625 86,192.625C100.244,192.625 111.8,203.879 111.8,217.75C111.8,231.621 100.244,242.875 86,242.875ZM111.8,150.75C78.529,150.75 51.6,124.526 51.6,92.125C51.6,59.725 78.529,33.5 111.8,33.5C145.071,33.5 172,59.724 172,92.125C172,124.525 145.071,150.75 111.8,150.75ZM258,242.875C243.756,242.875 232.2,231.621 232.2,217.75C232.2,203.879 243.756,192.625 258,192.625C272.244,192.625 283.8,203.879 283.8,217.75C283.8,231.621 272.244,242.875 258,242.875ZM301,134L223.6,134L223.6,75.375L247.304,75.375L301,127.666L301,134Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.96882e-14,321.533,-321.533,1.96882e-14,172.079,-41.4918)"><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(28,145,254);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,48 @@
{
"fill" : {
"linear-gradient" : [
"srgb:0.27800,0.58000,0.98800,1.00000",
"srgb:0.27800,0.58000,0.98800,1.00000"
]
},
"groups" : [
{
"blend-mode" : "screen",
"blur-material" : null,
"layers" : [
{
"blend-mode" : "normal",
"fill" : {
"solid" : "srgb:1.00000,0.99038,0.96423,1.00000"
},
"glass" : true,
"image-name" : "phpmon.svg",
"name" : "phpmon",
"position" : {
"scale" : 1.85,
"translation-in-points" : [
10.0234375,
8.21875
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

57
docs/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 450 KiB

View File

@ -1,46 +0,0 @@
//
// LaunchControl.swift
// PHP Monitor Self-Updater
//
// Created by Nico Verbruggen on 02/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class LaunchControl {
public static func smartRestart(priority: [String]) async {
for appPath in priority {
if FileManager.default.fileExists(atPath: appPath) {
let app = await LaunchControl.startApplication(at: appPath)
if app != nil {
return
}
}
}
}
public static func terminateApplications(bundleIds: [String]) async {
let runningApplications = NSWorkspace.shared.runningApplications
// Terminate all instances found
for id in bundleIds {
if let phpmon = runningApplications.first(where: {
(application) in return application.bundleIdentifier == id
}) {
phpmon.terminate()
}
}
}
public static func startApplication(at path: String) async -> NSRunningApplication? {
await withCheckedContinuation { continuation in
let url = NSURL(fileURLWithPath: path, isDirectory: true) as URL
let configuration = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.openApplication(at: url, configuration: configuration) { phpmon, error in
continuation.resume(returning: phpmon)
}
}
}
}

View File

@ -1,162 +0,0 @@
//
// Updater.swift
// PHP Monitor Updater
//
// Created by Nico Verbruggen on 01/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Updater: NSObject, NSApplicationDelegate {
var updaterDirectory: String = ""
var manifestPath: String = ""
var manifest: ReleaseManifest! = nil
func applicationDidFinishLaunching(_ aNotification: Notification) {
Task { await self.installUpdate() }
}
func installUpdate() async {
print("PHP MONITOR SELF-UPDATER by Nico Verbruggen")
print("===========================================")
self.updaterDirectory = "~/.config/phpmon/updater"
.replacingOccurrences(of: "~", with: NSHomeDirectory())
print("Updater directory set to: \(self.updaterDirectory)")
self.manifestPath = "\(updaterDirectory)/update.json"
// Fetch the manifest on the local filesystem
let manifest = await parseManifest()!
// Download the latest file
let zipPath = await download(manifest)
// Terminate all instances of PHP Monitor first
await LaunchControl.terminateApplications(bundleIds: [
"com.nicoverbruggen.phpmon.eap",
"com.nicoverbruggen.phpmon.dev",
"com.nicoverbruggen.phpmon"
])
// Install the app based on the zip
let appPath = await extractAndInstall(zipPath: zipPath)
// Restart PHP Monitor, this will also close the updater
_ = await LaunchControl.startApplication(at: appPath)
exit(1)
}
func applicationWillTerminate(_ aNotification: Notification) {
exit(1)
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return false
}
private func parseManifest() async -> ReleaseManifest? {
// Read out the correct information from the manifest JSON
print("Checking manifest file at \(manifestPath)...")
do {
let manifestText = try String(contentsOfFile: manifestPath)
manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestText.data(using: .utf8)!)
return manifest
} catch {
print("Parsing the manifest failed (or the manifest file doesn't exist)!")
await Alert.show(description: "The manifest file for a potential update was not found. Please try searching for updates again in PHP Monitor.")
}
return nil
}
private func download(_ manifest: ReleaseManifest) async -> String {
// Remove all zips
system_quiet("rm -rf \(updaterDirectory)/*.zip")
// Download the file (and follow redirects + no output on failure)
system_quiet("cd \"\(updaterDirectory)\" && curl \(manifest.url) -fLO --max-time 20")
// Identify the downloaded file
let filename = system("cd \"\(updaterDirectory)\" && ls | grep .zip")
.trimmingCharacters(in: .whitespacesAndNewlines)
// Ensure the zip exists
if filename.isEmpty {
print("The update has not been downloaded. Sadly, that means that PHP Monitor cannot not updated!")
await Alert.show(description: "The update could not be downloaded, or the file was not correctly written to disk. \n\nPlease try again. \n\n(Note that the download will time-out after 20 seconds, so for slow connections it is recommended to manually download the update.)")
}
// Calculate the checksum for the downloaded file
let checksum = system("openssl dgst -sha256 \"\(updaterDirectory)/\(filename)\" | awk '{print $NF}'")
.trimmingCharacters(in: .whitespacesAndNewlines)
// Compare the checksums
print("""
Comparing checksums...
Expected SHA256: \(manifest.sha256)
Actual SHA256: \(checksum)
""")
// Make sure the checksum matches before we do anything with the file
if checksum != manifest.sha256 {
print("The checksums failed to match. Cancelling!")
await Alert.show(description: "The downloaded update failed checksum validation. Please try again. If this issue persists, there may be an issue with the server and I do not recommend upgrading.")
}
// Return the path to the zip
return "\(updaterDirectory)/\(filename)"
}
private func extractAndInstall(zipPath: String) async -> String {
// Remove the directory that will contain the extracted update
system_quiet("rm -rf \"\(updaterDirectory)/extracted\"")
// Recreate the directory where we will unzip the .app file
system_quiet("mkdir -p \"\(updaterDirectory)/extracted\"")
// Make sure the updater directory exists
var isDirectory: ObjCBool = true
if !FileManager.default.fileExists(atPath: "\(updaterDirectory)/extracted", isDirectory: &isDirectory) {
await Alert.show(description: "The updater directory is missing. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
}
// Unzip the file
system_quiet("unzip \"\(zipPath)\" -d \"\(updaterDirectory)/extracted\"")
// Find the .app file
let app = system("ls \"\(updaterDirectory)/extracted\" | grep .app")
.trimmingCharacters(in: .whitespacesAndNewlines)
print("Finished extracting: \(updaterDirectory)/extracted/\(app)")
// Make sure the file was extracted
if app.isEmpty {
await Alert.show(description: "The downloaded file could not be extracted. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
}
// Remove the original app
print("Removing \(app) before replacing...")
system_quiet("rm -rf \"/Applications/\(app)\"")
// Move the new app in place
system_quiet("mv \"\(updaterDirectory)/extracted/\(app)\" \"/Applications/\(app)\"")
// Remove the zip
system_quiet("rm \"\(zipPath)\"")
// Remove the manifest
system_quiet("rm \"\(manifestPath)\"")
// Write a file that is only written when we upgraded successfully
system_quiet("touch \"\(updaterDirectory)/upgrade.success\"")
// Return the new location of the app
return "/Applications/\(app)"
}
}

View File

@ -1,34 +0,0 @@
//
// Utility.swift
// PHP Monitor Self-Updater
//
// Created by Nico Verbruggen on 02/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class Alert {
public static func show(description: String, shouldExit: Bool = true) async {
await withUnsafeContinuation { continuation in
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = "The app could not be updated."
alert.informativeText = description
alert.addButton(withTitle: "OK")
alert.alertStyle = .critical
alert.runModal()
if shouldExit {
exit(0)
}
continuation.resume()
}
}
}
}
public struct ReleaseManifest: Codable {
let url: String
let sha256: String
}

View File

@ -7,8 +7,17 @@
// //
import Cocoa import Cocoa
import NVAppUpdater
let app = NSApplication.shared let delegate = SelfUpdater(
let delegate = Updater() appName: "PHP Monitor",
app.delegate = delegate bundleIdentifiers: [
"com.nicoverbruggen.phpmon.eap",
"com.nicoverbruggen.phpmon.dev",
"com.nicoverbruggen.phpmon"
],
selfUpdaterPath: "~/.config/phpmon/updater"
)
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 B

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 B

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 499 KiB

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.172",
"green" : "0.182",
"red" : "0.182"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -24,7 +24,7 @@
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "0.988", "blue" : "0.988",
"green" : "0.723", "green" : "0.444",
"red" : "0.277" "red" : "0.277"
} }
}, },

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.300",
"blue" : "0.180",
"green" : "0.841",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.300",
"blue" : "0.426",
"green" : "0.809",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ValetDriverIcon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"filename" : "php.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 24 24"><path d="M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zm-2.595-1.382h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -20,6 +20,11 @@ class Actions {
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated) await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
} }
public static func restartPhpFpm(version: String) async {
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
await brew("services restart \(formula)", sudo: HomebrewFormulae.php.elevated)
}
public static func restartNginx() async { public static func restartNginx() async {
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated) await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
} }

View File

@ -18,19 +18,29 @@ struct Constants {
*/ */
static let MinimumRecommendedValetVersion = "2.16.2" static let MinimumRecommendedValetVersion = "2.16.2"
/**
The amount of seconds that is considered the threshold for
PHP Monitor to mark any given launch as a "slow" launch.
If the startup procedure was slow (or hangs), this message should
be displayed. This is based on an appropriate launch time on a
basic M1 Apple chip, with some margin for slower Intel chips.
*/
static let SlowBootThresholdInterval: TimeInterval = 30.0
/** /**
PHP Monitor supplies a hardcoded list of PHP packages in its own PHP Monitor supplies a hardcoded list of PHP packages in its own
PHP Version Manager. PHP Version Manager.
This hardcoded list will expire and will need to be modified when This hardcoded list will expire and will need to be modified when
the cutoff date occurs, which is when the `php` formula will the cutoff date occurs, which is when the `php` formula will
become PHP 8.4, and a new build will need to be made. become PHP 8.5, and a new build will need to be made.
If users launch an older version of the app, then a warning If users launch an older version of the app, then a warning
will be displayed to let them know that certain operations will be displayed to let them know that certain operations
will not work correctly and that they need to update their app. will not work correctly and that they need to update their app.
*/ */
static let PhpFormulaeCutoffDate = "2024-11-01" static let PhpFormulaeCutoffDate = "2025-11-30" // YYYY-MM-DD
/** /**
* The PHP versions that are considered pre-release versions. * The PHP versions that are considered pre-release versions.
@ -39,7 +49,8 @@ struct Constants {
*/ */
static var ExperimentalPhpVersions: Set<String> { static var ExperimentalPhpVersions: Set<String> {
let releaseDates = [ let releaseDates = [
"8.4": Date.fromString("2024-12-01") // PLACEHOLDER DATE "8.5": Date.fromString(Self.PhpFormulaeCutoffDate),
"8.4": Date.fromString("2024-11-22")
] ]
return Set(releaseDates return Set(releaseDates
@ -54,6 +65,17 @@ struct Constants {
}) })
} }
/**
The Homebrew services that should be automatically
detected and show up in the list of managed services.
*/
static let DetectedHomebrewServices: Set = [
"mailhog",
"mysql@",
"postgresql@",
"redis"
]
/** /**
* The PHP versions supported by this application. * The PHP versions supported by this application.
* Any other PHP versions are considered invalid. * Any other PHP versions are considered invalid.
@ -61,8 +83,8 @@ struct Constants {
static let DetectedPhpVersions: Set = [ static let DetectedPhpVersions: Set = [
"5.6", "5.6",
"7.0", "7.1", "7.2", "7.3", "7.4", "7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.3", "8.0", "8.1", "8.2", "8.3", "8.4",
"8.4" "8.5" // DEV
] ]
/** /**
@ -78,14 +100,13 @@ struct Constants {
3: // Valet v3 dropped support for v5.6 3: // Valet v3 dropped support for v5.6
[ [
"7.0", "7.1", "7.2", "7.3", "7.4", "7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.0", "8.1", "8.2", "8.3", "8.4"
"8.3", "8.4" // dev
], ],
4: // Valet v4 dropped support for v7.0 4: // Valet v4 dropped support for v7.0
[ [
"7.1", "7.2", "7.3", "7.4", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.0", "8.1", "8.2", "8.3", "8.4",
"8.3", "8.4" // dev "8.5" // DEV
] ]
] ]
@ -101,6 +122,14 @@ struct Constants {
string: "https://phpmon.app/faq" string: "https://phpmon.app/faq"
)! )!
static let WikiPhpUnavailable = URL(
string: "https://phpmon.app/php-unavailable"
)!
static let WikiPhpUpgrade = URL(
string: "https://phpmon.app/php-upgrade"
)!
static let DonationPayment = URL( static let DonationPayment = URL(
string: "https://phpmon.app/sponsor/now" string: "https://phpmon.app/sponsor/now"
)! )!

View File

@ -45,7 +45,6 @@ func grepContains(file: String, query: String) async -> Bool {
/** /**
Attempts to introduce sleep for a particular duration. Use with caution. Attempts to introduce sleep for a particular duration. Use with caution.
Only intended for testing purposes.
*/ */
func delay(seconds: Double) async { func delay(seconds: Double) async {
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))

View File

@ -103,6 +103,10 @@ public class Paths {
} }
public static var tapPath: String { public static var tapPath: String {
if shared.baseDir == .usr {
return "\(shared.baseDir.rawValue)/homebrew/Library/Taps"
}
return "\(shared.baseDir.rawValue)/Library/Taps" return "\(shared.baseDir.rawValue)/Library/Taps"
} }

View File

@ -8,6 +8,6 @@
import Foundation import Foundation
protocol AlertableError { public protocol AlertableError {
func getErrorMessageKey() -> String func getErrorMessageKey() -> String
} }

View File

@ -9,6 +9,24 @@
import Cocoa import Cocoa
extension NSMenuItem { extension NSMenuItem {
convenience init(
title: String,
action: Selector? = nil,
keyEquivalent: String = "",
keyModifier: NSEvent.ModifierFlags = [],
systemImage: String? = nil,
customImage: String? = nil,
) {
self.init(title: title, action: action, keyEquivalent: keyEquivalent)
self.keyEquivalentModifierMask = keyModifier
if systemImage != nil {
self.image = NSImage(systemSymbolName: systemImage!, accessibilityDescription: "")
}
if customImage != nil {
self.image = NSImage(named: customImage!)
}
}
convenience init( convenience init(
title: String, title: String,
action: Selector? = nil, action: Selector? = nil,
@ -26,12 +44,20 @@ extension NSMenuItem {
keyEquivalent: String = "", keyEquivalent: String = "",
keyModifier: NSEvent.ModifierFlags = [], keyModifier: NSEvent.ModifierFlags = [],
toolTip: String? = nil, toolTip: String? = nil,
systemImage: String? = nil,
customImage: String? = nil,
submenu: [NSMenuItem], submenu: [NSMenuItem],
target: NSObject? = nil target: NSObject? = nil
) { ) {
self.init(title: title, action: nil, keyEquivalent: keyEquivalent) self.init(title: title, action: nil, keyEquivalent: keyEquivalent)
self.keyEquivalentModifierMask = keyModifier self.keyEquivalentModifierMask = keyModifier
self.toolTip = toolTip self.toolTip = toolTip
if systemImage != nil {
self.image = NSImage(systemSymbolName: systemImage!, accessibilityDescription: "")
}
if customImage != nil {
self.image = NSImage(named: customImage!)
}
self.submenu = NSMenu(items: submenu, target: target) self.submenu = NSMenu(items: submenu, target: target)
} }
} }

View File

@ -0,0 +1,23 @@
//
// NVAlertExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/07/2024.
// Copyright © 2024 Nico Verbruggen. All rights reserved.
//
import Foundation
import NVAlert
extension NVAlert {
/**
Shows the modal for a particular error.
*/
@MainActor public static func show(for error: Error & AlertableError) {
let key = error.getErrorMessageKey()
return NVAlert().withInformation(
title: "\(key).title".localized,
subtitle: "\(key).description".localized
).withPrimary(text: "generic.ok".localized).show()
}
}

View File

@ -8,6 +8,18 @@ import Foundation
import SwiftUI import SwiftUI
struct Localization { struct Localization {
static var preferredLanguage: String? {
guard let language = Preferences.preferences[.languageOverride] as? String else {
return nil
}
if language.isEmpty {
return nil
}
return language
}
static var bundle: Bundle = { static var bundle: Bundle = {
if !isRunningTests { if !isRunningTests {
return Bundle.main return Bundle.main
@ -32,7 +44,15 @@ struct Localization {
extension String { extension String {
var localized: String { var localized: String {
let string = NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "") var preferredBundle: Bundle = Localization.bundle
if let preferred = Localization.preferredLanguage,
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
let bundle = Bundle(path: path) {
preferredBundle = bundle
}
let string = NSLocalizedString(self, tableName: nil, bundle: preferredBundle, value: "", comment: "")
// Fallback to English translation if the localized value is equal to the key (should not happen) // Fallback to English translation if the localized value is equal to the key (should not happen)
if string == self { if string == self {

View File

@ -75,7 +75,7 @@ class MenuBarImageGenerator {
// Then we'll fetch the image we want on the left // Then we'll fetch the image we want on the left
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
if iconType == nil { if iconType == nil || !MenuBarIcon.allCases.map({ $0.rawValue }).contains(iconType) {
Log.warn("Invalid icon type found, using the default") Log.warn("Invalid icon type found, using the default")
iconType = MenuBarIcon.iconPhp.rawValue iconType = MenuBarIcon.iconPhp.rawValue
} }

View File

@ -29,6 +29,8 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
App.shared.remove(window: windowName) App.shared.remove(window: windowName)
} }
func windowDidResize(_ notification: Notification) {}
deinit { deinit {
Log.perf("deinit: \(String(describing: self)).\(#function)") Log.perf("deinit: \(String(describing: self)).\(#function)")
} }

View File

@ -1,17 +0,0 @@
//
// WIP.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
func todo(_ context: String = "") {
if !context.isEmpty {
fatalError("To be implemented: \(context)")
}
fatalError("To be implemented")
}

View File

@ -37,6 +37,7 @@ class PhpEnvironments {
from: brewPhpAlias.data(using: .utf8)! from: brewPhpAlias.data(using: .utf8)!
).first! ).first!
PhpEnvironments.brewPhpAlias = self.homebrewPackage.version
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).") Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).")
// Check if that version actually corresponds to an older version // Check if that version actually corresponds to an older version

View File

@ -14,6 +14,8 @@ class PhpInstallation {
var iniFiles: [PhpConfigurationFile] = [] var iniFiles: [PhpConfigurationFile] = []
var isPreRelease: Bool = false
var isMissingBinary: Bool = false var isMissingBinary: Bool = false
var isHealthy: Bool = true var isHealthy: Bool = true
@ -22,6 +24,16 @@ class PhpInstallation {
return self.iniFiles.flatMap({ $0.extensions }) return self.iniFiles.flatMap({ $0.extensions })
} }
var formulaName: String {
let version = self.versionNumber.short
if version == PhpEnvironments.brewPhpAlias {
return "php"
}
return "php@\(self.versionNumber.short)"
}
/** /**
In order to determine details about a PHP installation, In order to determine details about a PHP installation,
well simply run `php-config --version` in the relevant directory. well simply run `php-config --version` in the relevant directory.
@ -49,6 +61,10 @@ class PhpInstallation {
trimNewlines: false trimNewlines: false
).trimmingCharacters(in: .whitespacesAndNewlines) ).trimmingCharacters(in: .whitespacesAndNewlines)
if longVersionString.contains("-dev") {
isPreRelease = true
}
// The parser should always work, or the string has to be very unusual. // The parser should always work, or the string has to be very unusual.
// If so, the app SHOULD crash, so that the users report what's up. // If so, the app SHOULD crash, so that the users report what's up.
versionNumber = try! VersionNumber.parse(longVersionString) versionNumber = try! VersionNumber.parse(longVersionString)

View File

@ -27,7 +27,7 @@ extension InternalSwitcher {
return corrections.contains(true) return corrections.contains(true)
} }
// MARK: - PHP FPM pool // MARK: - Corrections
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied { public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf" let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
@ -54,37 +54,7 @@ extension InternalSwitcher {
return false return false
} }
func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] { public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
return [
ExpectedConfigurationFile(
destination: "/php-fpm.d/valet-fpm.conf",
source: "/cli/stubs/etc-phpfpm-valet.conf",
replacements: [
"VALET_USER": Paths.whoami,
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
],
applies: { Valet.shared.version!.major > 2 }
),
ExpectedConfigurationFile(
destination: "/conf.d/error_log.ini",
source: "/cli/stubs/etc-phpfpm-error_log.ini",
replacements: [
"VALET_USER": Paths.whoami,
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
],
applies: { return true }
),
ExpectedConfigurationFile(
destination: "/conf.d/php-memory-limits.ini",
source: "/cli/stubs/php-memory-limits.ini",
replacements: [:],
applies: { return true }
)
]
}
func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
let files = self.getExpectedConfigurationFiles(for: version) let files = self.getExpectedConfigurationFiles(for: version)
// For each of the files, attempt to fix anything that is wrong // For each of the files, attempt to fix anything that is wrong
@ -124,6 +94,38 @@ extension InternalSwitcher {
return outcomes.contains(true) return outcomes.contains(true)
} }
// MARK: - Internals
private func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
return [
ExpectedConfigurationFile(
destination: "/php-fpm.d/valet-fpm.conf",
source: "/cli/stubs/etc-phpfpm-valet.conf",
replacements: [
"VALET_USER": Paths.whoami,
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
],
applies: { Valet.shared.version!.major > 2 }
),
ExpectedConfigurationFile(
destination: "/conf.d/error_log.ini",
source: "/cli/stubs/etc-phpfpm-error_log.ini",
replacements: [
"VALET_USER": Paths.whoami,
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
],
applies: { return true }
),
ExpectedConfigurationFile(
destination: "/conf.d/php-memory-limits.ini",
source: "/cli/stubs/php-memory-limits.ini",
replacements: [:],
applies: { return true }
)
]
}
} }
public struct ExpectedConfigurationFile { public struct ExpectedConfigurationFile {

View File

@ -8,9 +8,6 @@
import Foundation import Foundation
extension Process: @unchecked Sendable {}
extension Timer: @unchecked Sendable {}
class RealShell: ShellProtocol { class RealShell: ShellProtocol {
/** /**
The launch path of the terminal in question that is used. The launch path of the terminal in question that is used.
@ -184,25 +181,26 @@ class RealShell: ShellProtocol {
} }
return try await withCheckedThrowingContinuation({ continuation in return try await withCheckedThrowingContinuation({ continuation in
let timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in let task = Task {
try await Task.sleep(nanoseconds: timeout.nanoseconds)
// Only terminate if the process is still running // Only terminate if the process is still running
if process.isRunning { if process.isRunning {
process.terminationHandler = nil process.terminationHandler = nil
process.terminate() process.terminate()
return continuation.resume(throwing: ShellError.timedOut) continuation.resume(throwing: ShellError.timedOut)
} }
} }
process.terminationHandler = { [timer, output] process in process.terminationHandler = { [output] process in
timer.invalidate() task.cancel()
process.haltListening() process.haltListening()
if !output.err.isEmpty { if !output.err.isEmpty {
return continuation.resume(returning: (process, .err(output.err))) continuation.resume(returning: (process, .err(output.err)))
} else {
continuation.resume(returning: (process, .out(output.out)))
} }
return continuation.resume(returning: (process, .out(output.out)))
} }
process.launch() process.launch()
@ -210,3 +208,9 @@ class RealShell: ShellProtocol {
}) })
} }
} }
extension TimeInterval {
var nanoseconds: UInt64 {
return UInt64(self * 1_000_000_000)
}
}

View File

@ -73,12 +73,26 @@ public struct TestableConfiguration: Codable {
: .fake(.text) : .fake(.text)
]) { (_, new) in new } ]) { (_, new) in new }
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"] // PHP configuration files
= version.long self.shellOutput["/opt/homebrew/opt/php@\(version.short)/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
// PHP Homebrew operations
self.shellOutput["/opt/homebrew/bin/brew unlink php@\(version.short)"] = .delayed(0.2, "OK")
self.shellOutput["sudo /opt/homebrew/bin/brew services stop php@\(version.short)"] = .delayed(0.2, "OK")
self.shellOutput["sudo /opt/homebrew/bin/brew services start php@\(version.short)"] = .delayed(0.2, "OK")
self.shellOutput["/opt/homebrew/bin/brew link php@\(version.short) --overwrite --force"] = .delayed(0.2, "OK")
// PHP version output
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"] = version.long
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php -v"] = "OK"
if primary { if primary {
self.shellOutput["ls /opt/homebrew/opt | grep php"] // Files expected to be present for currently linked PHP version
= .instant("php") self.shellOutput["ls /opt/homebrew/opt | grep php"] =
.instant("php")
self.shellOutput["/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
self.filesystem["/opt/homebrew/opt/php"] self.filesystem["/opt/homebrew/opt/php"]
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)") = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
self.filesystem["/opt/homebrew/opt/php/bin/php"] self.filesystem["/opt/homebrew/opt/php/bin/php"]
@ -89,12 +103,8 @@ public struct TestableConfiguration: Codable {
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config") = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
self.commandOutput["/opt/homebrew/bin/php-config --version"] self.commandOutput["/opt/homebrew/bin/php-config --version"]
= version.long = version.long
self.commandOutput["/opt/homebrew/bin/php -r echo php_ini_scanned_files();"] =
"""
/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini,
"""
} else { } else {
// Output expected to be present for non-linked PHP versions
self.shellOutput["ls /opt/homebrew/opt | grep php@"] = self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
BatchFakeShellOutput.instant( BatchFakeShellOutput.instant(
self.secondaryPhpVersions self.secondaryPhpVersions

View File

@ -18,11 +18,11 @@ class TestableFileSystem: FileSystemProtocol {
self.files = files self.files = files
// Ensure that each of the ~ characters are replaced with the home directory path // Ensure that each of the ~ characters are replaced with the home directory path
for key in self.files.keys where key.contains("~") { accessQueue.sync {
self.files.renameKey( for (key, value) in files {
fromKey: key, let adjustedKey = key.contains("~") ? key.replacingOccurrences(of: "~", with: self.homeDirectory) : key
toKey: key.replacingOccurrences(of: "~", with: self.homeDirectory) self.files[adjustedKey] = value
) }
} }
// Ensure that intermediate directories are created // Ensure that intermediate directories are created
@ -46,38 +46,49 @@ class TestableFileSystem: FileSystemProtocol {
*/ */
private(set) var homeDirectory = "/Users/fake" private(set) var homeDirectory = "/Users/fake"
/**
Serial dispatch queue for ensuring thread-safe access to the `files` dictionary.
*/
private let accessQueue = DispatchQueue(label: "com.testablefilesystem.accessQueue")
// MARK: - Basics // MARK: - Basics
func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws { func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
if files[path] != nil { try accessQueue.sync {
throw TestableFileSystemError.alreadyExists if files[path] != nil {
throw TestableFileSystemError.alreadyExists
}
self.createIntermediateDirectories(path)
self.files[path] = .fake(.directory)
} }
self.createIntermediateDirectories(path)
self.files[path] = .fake(.directory)
} }
func writeAtomicallyToFile(_ path: String, content: String) throws { func writeAtomicallyToFile(_ path: String, content: String) throws {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
if files[path] != nil { try accessQueue.sync {
throw TestableFileSystemError.alreadyExists if files[path] != nil {
} throw TestableFileSystemError.alreadyExists
}
self.files[path] = .fake(.text, content) self.files[path] = .fake(.text, content)
}
} }
func getStringFromFile(_ path: String) throws -> String { func getStringFromFile(_ path: String) throws -> String {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else { return try accessQueue.sync {
throw TestableFileSystemError.fileMissing guard let file = files[path] else {
} throw TestableFileSystemError.fileMissing
}
return file.content ?? "" return file.content ?? ""
}
} }
func getShallowContentsOfDirectory(_ path: String) throws -> [String] { func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
@ -88,32 +99,36 @@ class TestableFileSystem: FileSystemProtocol {
seek = "\(seek)/" seek = "\(seek)/"
} }
return self.files.keys return accessQueue.sync {
.filter { $0.hasPrefix(seek) } self.files.keys
.map { $0.replacingOccurrences(of: seek, with: "") } .filter { $0.hasPrefix(seek) }
.filter { !$0.contains("/") } .map { $0.replacingOccurrences(of: seek, with: "") }
.filter { !$0.contains("/") }
}
} }
func getDestinationOfSymlink(_ path: String) throws -> String { func getDestinationOfSymlink(_ path: String) throws -> String {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else { return try accessQueue.sync {
throw TestableFileSystemError.fileMissing guard let file = files[path] else {
} throw TestableFileSystemError.fileMissing
}
if file.type != .symlink { if file.type != .symlink {
throw TestableFileSystemError.notSymlink throw TestableFileSystemError.notSymlink
} }
guard let pathToSymlink = file.content else { guard let pathToSymlink = file.content else {
throw TestableFileSystemError.invalidSymlink throw TestableFileSystemError.invalidSymlink
} }
if !files.keys.contains(pathToSymlink) { if !files.keys.contains(pathToSymlink) {
throw TestableFileSystemError.invalidSymlink throw TestableFileSystemError.invalidSymlink
} }
return pathToSymlink return pathToSymlink
}
} }
// MARK: - Move & Delete Files // MARK: - Move & Delete Files
@ -122,27 +137,31 @@ class TestableFileSystem: FileSystemProtocol {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
let newPath = newPath.replacingTildeWithHomeDirectory let newPath = newPath.replacingTildeWithHomeDirectory
self.files.keys.forEach { key in accessQueue.sync {
if key.hasPrefix(path) { self.files.keys.forEach { key in
self.files.renameKey( if key.hasPrefix(path) {
fromKey: key, self.files.renameKey(
toKey: key.replacingOccurrences(of: path, with: newPath) fromKey: key,
) toKey: key.replacingOccurrences(of: path, with: newPath)
)
}
} }
}
self.files.renameKey(fromKey: path, toKey: newPath) self.files.renameKey(fromKey: path, toKey: newPath)
}
} }
func remove(_ path: String) throws { func remove(_ path: String) throws {
// Remove recursively accessQueue.sync {
self.files.keys.forEach { key in // Remove recursively
if key.hasPrefix(path) { self.files.keys.forEach { key in
self.files.removeValue(forKey: key) if key.hasPrefix(path) {
self.files.removeValue(forKey: key)
}
} }
}
self.files.removeValue(forKey: path) self.files.removeValue(forKey: path)
}
} }
// MARK: Attributes // MARK: Attributes
@ -150,11 +169,13 @@ class TestableFileSystem: FileSystemProtocol {
func makeExecutable(_ path: String) throws { func makeExecutable(_ path: String) throws {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else { try accessQueue.sync {
throw TestableFileSystemError.fileMissing guard let file = files[path] else {
} throw TestableFileSystemError.fileMissing
}
file.type = .binary file.type = .binary
}
} }
// MARK: - Checks // MARK: - Checks
@ -162,93 +183,107 @@ class TestableFileSystem: FileSystemProtocol {
func isExecutableFile(_ path: String) -> Bool { func isExecutableFile(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
guard let file = files[path.replacingTildeWithHomeDirectory] else { return accessQueue.sync {
return false guard let file = files[path.replacingTildeWithHomeDirectory] else {
} return false
}
return file.type == .binary return file.type == .binary
}
} }
func isWriteableFile(_ path: String) -> Bool { func isWriteableFile(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
guard let file = files[path.replacingTildeWithHomeDirectory] else { return accessQueue.sync {
return false guard let file = files[path.replacingTildeWithHomeDirectory] else {
} return false
}
return !file.readOnly return !file.readOnly
}
} }
func anyExists(_ path: String) -> Bool { func anyExists(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
return files.keys.contains(path) return accessQueue.sync {
files.keys.contains(path)
}
} }
func fileExists(_ path: String) -> Bool { func fileExists(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else { return accessQueue.sync {
return false guard let file = files[path] else {
} return false
}
return [.binary, .symlink, .text].contains(file.type) return [.binary, .symlink, .text].contains(file.type)
}
} }
func directoryExists(_ path: String) -> Bool { func directoryExists(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else { return accessQueue.sync {
return false guard let file = files[path] else {
} return false
}
return [.directory].contains(file.type) return [.directory].contains(file.type)
}
} }
func isSymlink(_ path: String) -> Bool { func isSymlink(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else { return accessQueue.sync {
return false guard let file = files[path] else {
} return false
}
return file.type == .symlink return file.type == .symlink
}
} }
func isDirectory(_ path: String) -> Bool { func isDirectory(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else { return accessQueue.sync {
return false guard let file = files[path] else {
} return false
}
return file.type == .directory return file.type == .directory
}
} }
public func printContents() { public func printContents() {
for key in self.files.keys.sorted() { accessQueue.sync {
print("\(key) -> \(self.files[key]!.type)") for key in self.files.keys.sorted() {
print("\(key) -> \(self.files[key]!.type)")
}
} }
} }
private func createIntermediateDirectories(_ path: String) { private func createIntermediateDirectories(_ path: String) {
let path = path.replacingTildeWithHomeDirectory let path = path.replacingTildeWithHomeDirectory
let items = path.components(separatedBy: "/") let items = path.components(separatedBy: "/")
var preceding = "" var preceding = ""
var directoriesToCreate: [String] = []
for item in items { for item in items {
let key = preceding == "/" let key = preceding == "/" ? "/\(item)" : "\(preceding)/\(item)"
? "/\(item)" directoriesToCreate.append(key)
: "\(preceding)/\(item)"
if !self.files.keys.contains(key) {
self.files[key] = .fake(.directory)
}
preceding = key preceding = key
} }
for key in directoriesToCreate where !self.files.keys.contains(key) {
self.files[key] = .fake(.directory)
}
} }
} }

View File

@ -16,10 +16,19 @@
<p><b>Do you enjoy using the app? Is it helping you save time?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p> <p><b>Do you enjoy using the app? Is it helping you save time?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
<p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p> <p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p>
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p> <p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
<p><b>Get the latest on Twitter or Mastodon.</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p> <p><b>Get the latest on Bluesky or Mastodon.</b> Give me a <a href="https://bsky.app/profile/nicoverbruggen.be">follow on Bluesky</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
<p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p> <p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p>
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @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, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(Some names have been omitted due to their sponsorships being private. Thank you all!) <p><b>Made possible by these GitHub Sponsors</b>: @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.<br/>(This is a historical list of sponsors, not current sponsors. Some names have been omitted due to their sponsorships being private. Thank you all!)</p>
<p><b>Localization credits:</b></br>
&dash; English, Dutch</b> by @nicoverbruggen</br>
&dash; Vietnamese</b> by @xuandung38</br>
&dash; German</b> by @dsturm</br>
&dash; Portuguese</b> by @joseborges</br>
&dash; French</b> by @nhedger, @tplesnar</br>
&dash; Chinese</b> by @guanguans</br>
</br>
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.
</p>
<br/> <br/>
</body> </body>
</html> </html>

View File

@ -89,6 +89,9 @@ class App {
/** List of detected (installed) applications that PHP Monitor can work with. */ /** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = [] var detectedApplications: [Application] = []
/** Favorites storage, which keeps track of favorited domains. */
var favorites = Favorites.shared
/** The warning manager, responsible for keeping track of warnings. */ /** The warning manager, responsible for keeping track of warnings. */
var warnings = WarningManager.shared var warnings = WarningManager.shared

View File

@ -23,12 +23,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
*/ */
let state: App let state: App
/**
The MainMenu singleton is responsible for rendering the
menu bar item and its menu, as well as its actions.
*/
let menu: MainMenu
/** /**
The paths singleton that determines where Homebrew is installed, The paths singleton that determines where Homebrew is installed,
and where to look for binaries. and where to look for binaries.
@ -96,7 +90,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
} }
self.state = App.shared self.state = App.shared
self.menu = MainMenu.shared
self.paths = Paths.shared self.paths = Paths.shared
self.valet = Valet.shared self.valet = Valet.shared
self.brew = Brew.shared self.brew = Brew.shared
@ -109,6 +102,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
static func initializeTestingProfile(_ path: String) { static func initializeTestingProfile(_ path: String) {
Log.info("The configuration with path `\(path)` is being requested...") Log.info("The configuration with path `\(path)` is being requested...")
// Clear for PHP Guard
Stats.clearCurrentGlobalPhpVersion()
// Load the configuration file
TestableConfiguration.loadFrom(path: path).apply() TestableConfiguration.loadFrom(path: path).apply()
} }
@ -129,7 +125,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
setupNotifications() setupNotifications()
Task { // Make sure the menu performs its initial checks Task { // Make sure the menu performs its initial checks
await menu.startup() await MainMenu.shared.startup()
} }
} }

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import Cocoa import Cocoa
import NVAlert
class AppUpdater { class AppUpdater {
var caskFile: CaskFile! var caskFile: CaskFile!
@ -72,7 +73,7 @@ class AppUpdater {
: "brew upgrade phpmon" : "brew upgrade phpmon"
Task { @MainActor in Task { @MainActor in
BetterAlert().withInformation( NVAlert().withInformation(
title: "updater.alerts.newer_version_available.title" title: "updater.alerts.newer_version_available.title"
.localized(latestVersionOnline.humanReadable), .localized(latestVersionOnline.humanReadable),
subtitle: "updater.alerts.newer_version_available.subtitle" subtitle: "updater.alerts.newer_version_available.subtitle"
@ -112,7 +113,7 @@ class AppUpdater {
public func presentNoNewerVersionAvailableAlert() { public func presentNoNewerVersionAvailableAlert() {
Task { @MainActor in Task { @MainActor in
BetterAlert().withInformation( NVAlert().withInformation(
title: "updater.alerts.is_latest_version.title".localized, title: "updater.alerts.is_latest_version.title".localized,
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion), subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
description: "" description: ""
@ -124,7 +125,7 @@ class AppUpdater {
public func presentCouldNotRetrieveUpdate() { public func presentCouldNotRetrieveUpdate() {
Task { @MainActor in Task { @MainActor in
BetterAlert().withInformation( NVAlert().withInformation(
title: "updater.alerts.cannot_check_for_update.title".localized, title: "updater.alerts.cannot_check_for_update.title".localized,
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized, subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
description: "updater.alerts.cannot_check_for_update.description".localized( description: "updater.alerts.cannot_check_for_update.description".localized(

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22155"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/>
<capability name="Image references" minToolsVersion="12.0"/> <capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/> <capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
@ -63,6 +63,13 @@
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/> <action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="bPr-YU-lg4"/>
<menuItem title="actions" enabled="NO" id="cAS-FU-WUA" userLabel="actions" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<modifierMask key="keyEquivalentModifierMask"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_actions"/>
</userDefinedRuntimeAttributes>
</menuItem>
</items> </items>
</menu> </menu>
</menuItem> </menuItem>
@ -508,10 +515,10 @@
</objects> </objects>
<point key="canvasLocation" x="-374" y="2267"/> <point key="canvasLocation" x="-374" y="2267"/>
</scene> </scene>
<!--Better AlertVC--> <!--AlertVC-->
<scene sceneID="y9E-bB-wIG"> <scene sceneID="y9E-bB-wIG">
<objects> <objects>
<viewController storyboardIdentifier="noticeVC" id="hkw-9V-NxP" customClass="BetterAlertVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController"> <viewController storyboardIdentifier="noticeVC" id="hkw-9V-NxP" customClass="NVAlertVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="UPH-hV-Naz"> <view key="view" id="UPH-hV-Naz">
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/> <rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
@ -822,11 +829,11 @@ Gw
<scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i"> <scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/> <rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w"> <clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w">
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/> <rect key="frame" x="0.0" y="0.0" width="611" height="294"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target"> <tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="626" height="281"/> <rect key="frame" x="0.0" y="0.0" width="611" height="266"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/> <size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -916,6 +923,50 @@ Gw
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/> <outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
</connections> </connections>
</tableCellView> </tableCellView>
<tableCellView identifier="domainListNameCellFavorited" wantsLayer="YES" id="Byb-te-u65" customClass="DomainListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="69" y="54" width="200" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="aot-FJ-HIk">
<rect key="frame" x="33" y="26" width="145" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="LHu-UF-QlC">
<font key="font" metaFont="systemSemibold" size="13"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="GNH-l8-oki">
<rect key="frame" x="33" y="12" width="75" height="14"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="LNw-Ju-0Ot">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="3Wp-DX-An9">
<rect key="frame" x="5" y="4" width="20" height="47"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="Q76-fI-lkW">
<imageReference key="image" image="star.circle.fill" catalog="system" symbolScale="large"/>
</imageCell>
<color key="contentTintColor" name="AccentColor"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="3Wp-DX-An9" firstAttribute="leading" secondItem="Byb-te-u65" secondAttribute="leading" constant="5" id="CTd-ON-loK"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="aot-FJ-HIk" secondAttribute="trailing" constant="20" symbolic="YES" id="Csc-Dy-H4K"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="GNH-l8-oki" secondAttribute="trailing" constant="20" symbolic="YES" id="H10-MG-hCG"/>
<constraint firstItem="GNH-l8-oki" firstAttribute="leading" secondItem="aot-FJ-HIk" secondAttribute="leading" id="Hk0-x3-RyN"/>
<constraint firstItem="3Wp-DX-An9" firstAttribute="top" secondItem="Byb-te-u65" secondAttribute="top" constant="9" id="erH-dR-K7S"/>
<constraint firstItem="aot-FJ-HIk" firstAttribute="top" secondItem="Byb-te-u65" secondAttribute="top" constant="12" id="ktI-fg-qaX"/>
<constraint firstAttribute="bottom" secondItem="3Wp-DX-An9" secondAttribute="bottom" constant="9" id="uyc-26-gZb"/>
<constraint firstItem="aot-FJ-HIk" firstAttribute="leading" secondItem="Byb-te-u65" secondAttribute="leading" constant="35" id="vXE-jj-lLF"/>
<constraint firstItem="GNH-l8-oki" firstAttribute="top" secondItem="aot-FJ-HIk" secondAttribute="bottom" id="wSX-fR-O7a"/>
</constraints>
<connections>
<outlet property="labelPathName" destination="GNH-l8-oki" id="GC1-TA-lIk"/>
<outlet property="labelSiteName" destination="aot-FJ-HIk" id="HdZ-Rh-ua6"/>
</connections>
</tableCellView>
</prototypeCellViews> </prototypeCellViews>
</tableColumn> </tableColumn>
<tableColumn identifier="ENVIRONMENT" width="100" minWidth="100" maxWidth="150" id="hzb-XI-Out"> <tableColumn identifier="ENVIRONMENT" width="100" minWidth="100" maxWidth="150" id="hzb-XI-Out">
@ -1073,43 +1124,76 @@ Gw
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/>
</constraints> </constraints>
<scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT"> <scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
<rect key="frame" x="0.0" y="293" width="626" height="16"/> <rect key="frame" x="0.0" y="294" width="611" height="15"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</scroller> </scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10"> <scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
<rect key="frame" x="610" y="28" width="16" height="281"/> <rect key="frame" x="611" y="28" width="15" height="266"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</scroller> </scroller>
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh"> <tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
<rect key="frame" x="0.0" y="0.0" width="626" height="28"/> <rect key="frame" x="0.0" y="0.0" width="611" height="28"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</tableHeaderView> </tableHeaderView>
</scrollView> </scrollView>
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ"> <customView translatesAutoresizingMaskIntoConstraints="NO" id="wcV-ed-8Bv">
<rect key="frame" x="298" y="150" width="30" height="30"/> <rect key="frame" x="113" y="5" width="400" height="300"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/> <constraint firstAttribute="width" constant="400" id="HCo-LG-x3N"/>
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/> <constraint firstAttribute="height" constant="300" id="Xpi-Rl-xmb"/>
</constraints> </constraints>
</progressIndicator> </customView>
<visualEffectView hidden="YES" blendingMode="behindWindow" material="popover" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="r8h-6t-ZNm">
<rect key="frame" x="263" y="125" width="100" height="80"/>
<subviews>
<progressIndicator wantsLayer="YES" maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
<rect key="frame" x="35" y="35" width="30" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
</constraints>
</progressIndicator>
<textField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xoy-5Y-WDT">
<rect key="frame" x="15" y="14" width="71" height="13"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="PLEASE WAIT" id="tMX-Ky-caT">
<font key="font" metaFont="system" size="10"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="xoy-5Y-WDT" firstAttribute="top" secondItem="ZiS-Gq-TLQ" secondAttribute="bottom" constant="8" symbolic="YES" id="4pN-Xn-po4"/>
<constraint firstAttribute="width" constant="100" id="Fo3-MY-e5e"/>
<constraint firstItem="xoy-5Y-WDT" firstAttribute="centerX" secondItem="r8h-6t-ZNm" secondAttribute="centerX" id="JPe-3T-uYg"/>
<constraint firstAttribute="height" constant="80" id="hcO-TE-dKr"/>
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerX" secondItem="r8h-6t-ZNm" secondAttribute="centerX" id="sbD-l6-6kk"/>
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="r8h-6t-ZNm" secondAttribute="centerY" constant="-10" id="x7S-hb-YV1"/>
</constraints>
</visualEffectView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/> <constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/>
<constraint firstItem="wcV-ed-8Bv" firstAttribute="centerX" secondItem="rIZ-4U-bhj" secondAttribute="centerX" id="DPz-kQ-aP0"/>
<constraint firstItem="wcV-ed-8Bv" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" id="HCW-zJ-gSY"/>
<constraint firstItem="r8h-6t-ZNm" firstAttribute="centerX" secondItem="rIZ-4U-bhj" secondAttribute="centerX" id="JF1-EN-aWm"/>
<constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/> <constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/>
<constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/> <constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/>
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/> <constraint firstItem="r8h-6t-ZNm" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="dkm-LB-eCY"/>
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerX" secondItem="rIZ-4U-bhj" secondAttribute="centerX" id="eD8-TV-7dF"/>
<constraint firstAttribute="trailing" secondItem="p0j-eB-I2i" secondAttribute="trailing" id="zWH-TD-RZv"/> <constraint firstAttribute="trailing" secondItem="p0j-eB-I2i" secondAttribute="trailing" id="zWH-TD-RZv"/>
</constraints> </constraints>
</view> </view>
<connections> <connections>
<outlet property="labelProgressIndicator" destination="xoy-5Y-WDT" id="Wfj-oK-Bni"/>
<outlet property="noResultsView" destination="wcV-ed-8Bv" id="K3s-fb-1aN"/>
<outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/> <outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/>
<outlet property="progressIndicatorContainer" destination="r8h-6t-ZNm" id="x0d-1g-Kzw"/>
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/> <outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
</connections> </connections>
</viewController> </viewController>
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="323" y="723"/> <point key="canvasLocation" x="323" y="722.5"/>
</scene> </scene>
<!--Add ProxyVC--> <!--Add ProxyVC-->
<scene sceneID="g8z-pE-RL9"> <scene sceneID="g8z-pE-RL9">
@ -1338,7 +1422,7 @@ Gw
<visualEffectView blendingMode="behindWindow" material="toolTip" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="F37-zt-gM3"> <visualEffectView blendingMode="behindWindow" material="toolTip" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="F37-zt-gM3">
<rect key="frame" x="0.0" y="0.0" width="540" height="177"/> <rect key="frame" x="0.0" y="0.0" width="540" height="177"/>
<subviews> <subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI"> <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
<rect key="frame" x="13" y="13" width="114" height="32"/> <rect key="frame" x="13" y="13" width="114" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="LxP-t4-H2W"> <buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="LxP-t4-H2W">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -1357,7 +1441,7 @@ Gw
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pYe-Qu-qnK"> <stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pYe-Qu-qnK">
<rect key="frame" x="187" y="20" width="333" height="20"/> <rect key="frame" x="187" y="20" width="333" height="20"/>
<subviews> <subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="L5n-Gw-J27"> <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="L5n-Gw-J27">
<rect key="frame" x="-7" y="-7" width="172" height="32"/> <rect key="frame" x="-7" y="-7" width="172" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="8UP-Sw-TP6"> <buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="8UP-Sw-TP6">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -1368,7 +1452,7 @@ Gw
<action selector="pressedCreateLink:" target="gOD-Gu-zDG" id="77M-Ip-GMi"/> <action selector="pressedCreateLink:" target="gOD-Gu-zDG" id="77M-Ip-GMi"/>
</connections> </connections>
</button> </button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="01Z-IV-hv1"> <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="01Z-IV-hv1">
<rect key="frame" x="159" y="-7" width="181" height="32"/> <rect key="frame" x="159" y="-7" width="181" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="bJ4-q8-1Ej"> <buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="bJ4-q8-1Ej">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -1486,6 +1570,10 @@ Gw
<image name="Lock" width="30" height="30"/> <image name="Lock" width="30" height="30"/>
<image name="arrow.clockwise" catalog="system" width="14" height="16"/> <image name="arrow.clockwise" catalog="system" width="14" height="16"/>
<image name="plus" catalog="system" width="14" height="13"/> <image name="plus" catalog="system" width="14" height="13"/>
<image name="star.circle.fill" catalog="system" width="20" height="20"/>
<namedColor name="AccentColor">
<color red="0.0" green="0.46000000000000002" blue="0.89000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="IconColorGreen"> <namedColor name="IconColorGreen">
<color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import Cocoa import Cocoa
import NVAlert
class ValetServicesManager: ServicesManager { class ValetServicesManager: ServicesManager {
override init() { override init() {
@ -131,7 +132,7 @@ class ValetServicesManager: ServicesManager {
Log.err("The service '\(named)' is now reporting an error.") Log.err("The service '\(named)' is now reporting an error.")
guard let errorLogPath = after.error_log_path else { guard let errorLogPath = after.error_log_path else {
return BetterAlert().withInformation( return NVAlert().withInformation(
title: "alert.service_error.title".localized(named), title: "alert.service_error.title".localized(named),
subtitle: "alert.service_error.subtitle.no_error_log".localized(named), subtitle: "alert.service_error.subtitle.no_error_log".localized(named),
description: "alert.service_error.extra".localized description: "alert.service_error.extra".localized
@ -140,7 +141,7 @@ class ValetServicesManager: ServicesManager {
.show() .show()
} }
BetterAlert().withInformation( NVAlert().withInformation(
title: "alert.service_error.title".localized(named), title: "alert.service_error.title".localized(named),
subtitle: "alert.service_error.subtitle.error_log".localized(named), subtitle: "alert.service_error.subtitle.error_log".localized(named),
description: "alert.service_error.extra".localized description: "alert.service_error.extra".localized

View File

@ -0,0 +1,74 @@
//
// Startup+Timers.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/07/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
import NVAlert
extension Startup {
@MainActor static var startupTimer: Timer?
@MainActor static var launchTime: Date?
/** Returns a human-readable version to indicate how many seconds elapsed since boot. */
@MainActor static var humanReadableSinceBootTime: String {
return String(format: "%.2f", Date().timeIntervalSince(Self.launchTime!))
}
/** Starts the timeout timer that keeps track of how long the app takes to boot. */
@MainActor func startStartupTimer() {
Self.launchTime = Date()
Self.startupTimer = Timer.scheduledTimer(
timeInterval: Constants.SlowBootThresholdInterval, target: self,
selector: #selector(startupTimeout), userInfo: nil, repeats: false
)
}
/**
Invalidates and stops the startup timer.
This is only called if the slow boot threshold is not exceeded.
*/
@MainActor static func invalidateTimeoutTimer() {
if Self.startupTimer == nil {
return
}
Log.info("PHP Monitor was quick; elapsed time: \(Self.humanReadableSinceBootTime) sec.")
Self.startupTimer?.invalidate()
Self.startupTimer = nil
}
/**
Displays an alert for when the application startup process takes too long.
*/
@MainActor @objc func startupTimeout() {
Log.info("PHP Monitor was slow; elapsed time: \(Self.humanReadableSinceBootTime) sec.")
// Invalidate the timer
Self.startupTimer?.invalidate()
Self.startupTimer = nil
// Present an alert that lets the user know about the slow start
NVAlert()
.withInformation(
title: "startup.timeout.title".localized,
subtitle: "startup.timeout.subtitle".localized,
description: "startup.timeout.description".localized
)
.withPrimary(text: "alert.cannot_start.close".localized, action: { vc in
vc.close(with: .alertFirstButtonReturn)
exit(1)
})
.withSecondary(text: "startup.timeout.ignore".localized, action: { vc in
vc.close(with: .alertSecondButtonReturn)
})
.withTertiary(text: "", action: { _ in
NSWorkspace.shared.open(URL(string: "https://github.com/nicoverbruggen/phpmon/issues/294")!)
})
.show()
}
}

View File

@ -7,9 +7,9 @@
import Foundation import Foundation
import AppKit import AppKit
import NVAlert
class Startup { class Startup {
/** /**
Checks the user's environment and checks if PHP Monitor can be used properly. 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. This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
@ -21,6 +21,11 @@ class Startup {
// Do the important system setup checks // Do the important system setup checks
Log.info("The user is running PHP Monitor with the architecture: \(App.architecture)") Log.info("The user is running PHP Monitor with the architecture: \(App.architecture)")
// Set up a "background" timer on the main thread
Task { @MainActor in
startStartupTimer()
}
for group in self.groups { for group in self.groups {
if group.condition() { if group.condition() {
Log.info("Now running \(group.checks.count) \(group.name) checks!") Log.info("Now running \(group.checks.count) \(group.name) checks!")
@ -44,6 +49,7 @@ class Startup {
// If we get here, nothing has gone wrong. That's what we want! // If we get here, nothing has gone wrong. That's what we want!
initializeSwitcher() initializeSwitcher()
Log.info("PHP Monitor has determined the application has successfully passed all checks.") Log.info("PHP Monitor has determined the application has successfully passed all checks.")
Log.separator(as: .info) Log.separator(as: .info)
return true return true
} }
@ -55,7 +61,7 @@ class Startup {
*/ */
@MainActor private func showAlert(for check: EnvironmentCheck) { @MainActor private func showAlert(for check: EnvironmentCheck) {
if check.requiresAppRestart { if check.requiresAppRestart {
BetterAlert() NVAlert()
.withInformation( .withInformation(
title: check.titleText, title: check.titleText,
subtitle: check.subtitleText, subtitle: check.subtitleText,
@ -66,7 +72,7 @@ class Startup {
}).show() }).show()
} }
BetterAlert() NVAlert()
.withInformation( .withInformation(
title: check.titleText, title: check.titleText,
subtitle: check.subtitleText, subtitle: check.subtitleText,

Some files were not shown because too many files have changed in this diff Show More