mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-08 04:20:07 +02:00
Compare commits
163 Commits
Author | SHA1 | Date | |
---|---|---|---|
4a3dee3c50 | |||
9d5a0ed745 | |||
b3b509409a | |||
4934f35d0b | |||
92e7418158 | |||
52ea64db40 | |||
f66e9b7340 | |||
2bf28fe247 | |||
c6e4f785bc | |||
94fe7df3bd | |||
f373621a4a | |||
5104a865fb | |||
7b10973330 | |||
bc208bddf9 | |||
321b4aaf8b | |||
b26fc3bc4b | |||
f758c5d63a | |||
c7510d778d | |||
70c5aadb7f | |||
a731f15cf7 | |||
ab4c436202 | |||
c0231690d4 | |||
988e9d3351 | |||
2f119d4332 | |||
d83c629a7b | |||
e7d98dbeae | |||
f3d5946743 | |||
7728a1125c | |||
3612351df7 | |||
8e912151fb | |||
3a2209e604 | |||
1f0b56cab6 | |||
e08d970edd | |||
32c757e711 | |||
480cdb94ae | |||
7fbcac5dc2 | |||
4edb5f5015 | |||
294f84ccb2 | |||
155b57eb9e | |||
a459f015e1 | |||
27676f13f4 | |||
b4b2d7052f | |||
6d25cf585e | |||
ba04c94c05 | |||
13447ba533 | |||
6f2e8f4b20 | |||
dc860074ef | |||
f586b8fcbe | |||
94714c3e7a | |||
904d05bdce | |||
ec30bee72b | |||
2fe3a4b7eb | |||
a7d5950aa0 | |||
e8306289ce | |||
ff61d8c52e | |||
da41673855 | |||
5bda727981 | |||
23cf575026 | |||
d3053b8fe3 | |||
7159ca8612 | |||
141c06d14b | |||
94c84aaab3 | |||
9ca16e72d5 | |||
67a00f979a | |||
1e4c45dcbd | |||
87c44f3ae3 | |||
f39732a0e6 | |||
3b78ac43d7 | |||
1f19b81530 | |||
d714d7ad4c | |||
4dce6c033e | |||
72a8a1e382 | |||
07b17f3f84 | |||
7f0f7ff3e9 | |||
c7c143c760 | |||
ee050af364 | |||
f7e2551587 | |||
cc0cc21e5f | |||
883ea05bd1 | |||
641bddfce7 | |||
2f7223fba5 | |||
3b23ce7805 | |||
a634d083a6 | |||
9a3dd2fa22 | |||
8790b30706 | |||
c42188b717 | |||
cc251686f9 | |||
6fd6241567 | |||
c8ab2e67f6 | |||
f82ab913c6 | |||
58943148fa | |||
8a46b9d374 | |||
a62ebcff92 | |||
541378f3f9 | |||
e6f1d7e834 | |||
20d19f2f92 | |||
91bc347e57 | |||
e05300b25b | |||
1ae7a20870 | |||
5594130ccd | |||
b9c7cdb3cc | |||
00b4760b85 | |||
9a35014d2a | |||
7cba25b52e | |||
c6c3996c7b | |||
03c96a1d16 | |||
a6fa4b240f | |||
7e78026d06 | |||
d5888c1c7a | |||
e40b9fe45a | |||
f5d0ad20cd | |||
0615927f2f | |||
3d1806c094 | |||
8a57557074 | |||
19f4819450 | |||
aa8309dd9a | |||
7977a4e177 | |||
51c100f6fe | |||
aebfc9dd09 | |||
f9acbd34d0 | |||
eb566bb523 | |||
528f213f17 | |||
f8e6aa988e | |||
93e841735e | |||
cb28243181 | |||
fc68e37458 | |||
ae6736102a | |||
3ef1a6e60d | |||
5e7c7bc903 | |||
94f3c1c7c5 | |||
20aad90ba9 | |||
8bd85d8354 | |||
90b068d200 | |||
943b5aa6af | |||
4bf475bae2 | |||
125b9bb198 | |||
72cbf6996d | |||
e7cc940f65 | |||
c8323a8c27 | |||
6805855f03 | |||
db101f5a66 | |||
2302d5a5ee | |||
5cfb0f452c | |||
7da20b4f20 | |||
f1b037ce26 | |||
e59347ed7f | |||
206dff289f | |||
02f579fe81 | |||
2a74b11462 | |||
371f98b875 | |||
7955c777e7 | |||
5c9b06d83b | |||
3c7bed0a9b | |||
54f83a0aed | |||
b041ca37be | |||
2b2b027317 | |||
cdbd959159 | |||
0c3b68734c | |||
8b0aeef2e6 | |||
aa406434d0 | |||
d320c49092 | |||
966033e052 | |||
7c192730e1 |
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1530"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1530"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1530"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1530"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1530"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
63
README.md
63
README.md
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p>
|
<p align="center"><img src="./docs/logo.png" 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 before you can use this 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).
|
||||||
|
|
||||||
<img src="./docs/screenshot.jpg" width="1280px" alt="phpmon screenshot (menu bar app)"/>
|
<img src="./docs/screenshot.jpg" width="1280px" alt="phpmon screenshot (menu bar app)"/>
|
||||||
|
|
||||||
@ -22,9 +22,10 @@ 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 and Ventura are supported)
|
* macOS 12.4 or later (Monterey, Ventura and Sonoma are supported)
|
||||||
* Homebrew is installed in `/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
|
||||||
|
|
||||||
_Starting with PHP Monitor 6.0, you do not need to have Laravel Valet installed for PHP Monitor to work. To get access to all features of PHP Monitor however, installing Valet is **recommended**._
|
_Starting with PHP Monitor 6.0, you do not need to have Laravel Valet installed for PHP Monitor to work. To get access to all features of PHP Monitor however, installing Valet is **recommended**._
|
||||||
|
|
||||||
@ -81,6 +82,12 @@ I wanted to be able to **see at a glance** which version of PHP was linked, and
|
|||||||
|
|
||||||
Initially, I had an Alfred workflow for this — but it has now been replaced with this utility, which also does a good job at displaying additional information at a glance, like the current PHP version, memory limits, and more.
|
Initially, I had an Alfred workflow for this — but it has now been replaced with this utility, which also does a good job at displaying additional information at a glance, like the current PHP version, memory limits, and more.
|
||||||
|
|
||||||
|
## 🐘 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.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
## 🤬 The app won't start?!
|
## 🤬 The app won't start?!
|
||||||
|
|
||||||
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
|
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
|
||||||
@ -103,7 +110,9 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
|
|||||||
> **Note**
|
> **Note**
|
||||||
> If you have versions of PHP installed that can be detected by PHP Monitor but is *not* supported by the currently active version of Valet, you will be alerted by an item in the menu with an exclamation mark emoji. (⚠️)
|
> If you have versions of PHP installed that can be detected by PHP Monitor but is *not* supported by the currently active version of Valet, you will be alerted by an item in the menu with an exclamation mark emoji. (⚠️)
|
||||||
|
|
||||||
Backports that are installable via PHP Monitor's **PHP Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
||||||
|
|
||||||
|
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
|
||||||
|
|
||||||
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
||||||
</details>
|
</details>
|
||||||
@ -111,11 +120,11 @@ 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 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.)
|
||||||
|
|
||||||
Please keep in mind that installing or updating PHP versions, even when done via PHP Monitor's **PHP Manager**, may cause other required formula dependencies (required software needed to keep those PHP versions functional) to be upgraded. It might not be very transparent when this happens, but this is likely the cause if installing a PHP version takes longer than expected: usually other dependencies are also being installed.
|
Please keep in mind that installing or updating PHP versions, even when done via PHP Monitor's **PHP Version Manager**, may cause other required formula dependencies (required software needed to keep those PHP versions functional) to be upgraded. It might not be very transparent when this happens, but this is likely the cause if installing a PHP version takes longer than expected: usually other dependencies are also being installed.
|
||||||
|
|
||||||
Additionally, upgrading one specific version of PHP may also cause other installed versions of PHP to *also* be updated in one go, if the dependencies for that one version also apply to the other (newer) version(s) of PHP. It's a bit tricky to manage PHP versions via Homebrew, and even PHP Monitor may encounter some difficulties.
|
Additionally, upgrading one specific version of PHP may also cause other installed versions of PHP to *also* be updated in one go, if the dependencies for that one version also apply to the other (newer) version(s) of PHP. It's a bit tricky to manage PHP versions via Homebrew, and even PHP Monitor may encounter some difficulties.
|
||||||
|
|
||||||
@ -134,6 +143,14 @@ If you are on an older version of macOS, you can do this by dragging *PHP Monito
|
|||||||
Super convenient!
|
Super convenient!
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>What features are unavailable in Standalone Mode?</strong></summary>
|
||||||
|
|
||||||
|
The services manager is disabled, and all other obvious Laravel Valet integrations (configuration finder, domains list, Fix My Valet) are also disabled.
|
||||||
|
|
||||||
|
(Most other features remain available.)
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary>
|
<summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary>
|
||||||
|
|
||||||
@ -160,7 +177,7 @@ If you're on an Apple Silicon-based Mac, you'll need to add:
|
|||||||
and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions:
|
and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions:
|
||||||
|
|
||||||
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
||||||
|
|
||||||
If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
|
If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
|
||||||
|
|
||||||
export PATH=$HOME/bin:/usr/local/bin:$PATH
|
export PATH=$HOME/bin:/usr/local/bin:$PATH
|
||||||
@ -179,8 +196,12 @@ Make sure PHP is linked correctly:
|
|||||||
|
|
||||||
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon)
|
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon)
|
||||||
|
|
||||||
|
**If you don't need Laravel Valet, you can stop here. PHP Monitor will work like this in Standalone Mode.**
|
||||||
|
|
||||||
|
If you'd like to have Valet as well, continue and install Valet with Composer, like this.
|
||||||
|
|
||||||
composer global require laravel/valet
|
composer global require laravel/valet
|
||||||
|
|
||||||
For optimal results, you should lock your PHP platform for global dependencies to the oldest version of PHP you intend to run. If that version is PHP 7.0, your `~/.composer/composer.json` file could look like this (please adjust the version accordingly!):
|
For optimal results, you should lock your PHP platform for global dependencies to the oldest version of PHP you intend to run. If that version is PHP 7.0, your `~/.composer/composer.json` file could look like this (please adjust the version accordingly!):
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -199,18 +220,13 @@ For optimal results, you should lock your PHP platform for global dependencies t
|
|||||||
Run `composer global update` again. This ensures that when you switch to a different global PHP version, [Valet won't break](https://github.com/nicoverbruggen/phpmon/issues/178). If it does, PHP Monitor will let you know what you can do about this.
|
Run `composer global update` again. This ensures that when you switch to a different global PHP version, [Valet won't break](https://github.com/nicoverbruggen/phpmon/issues/178). If it does, PHP Monitor will let you know what you can do about this.
|
||||||
|
|
||||||
Then, install Valet:
|
Then, install Valet:
|
||||||
|
|
||||||
valet install
|
valet install
|
||||||
|
|
||||||
This should install `dnsmasq` and set up Valet. Great, almost there!
|
This should install `dnsmasq` and set up Valet. Great, almost there!
|
||||||
|
|
||||||
valet trust
|
valet trust
|
||||||
|
|
||||||
You can now install PHP Monitor, if you haven't already:
|
|
||||||
|
|
||||||
brew tap nicoverbruggen/homebrew-cask
|
|
||||||
brew install --cask phpmon
|
|
||||||
|
|
||||||
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
|
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@ -219,13 +235,17 @@ Finally, run PHP Monitor. Since the app is notarized and signed with a developer
|
|||||||
|
|
||||||
PHP Monitor will check if an update is available every time you start the app.
|
PHP Monitor will check if an update is available every time you start the app.
|
||||||
|
|
||||||
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". You can always check for updates manually.
|
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". (You can always check for updates manually.)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>I have PHP Monitor installed, and it works. I want to upgrade my PHP installations to the latest version, what's the best way to do this?</strong></summary>
|
<summary><strong>I have PHP Monitor installed, and it works. I want to upgrade my PHP installations to the latest version, what's the best way to do this?</strong></summary>
|
||||||
|
|
||||||
|
The easiest way is to simply use the built-in **PHP Version Manager**, which will allow you to upgrade your PHP versions with one click.
|
||||||
|
|
||||||
|
If you want to do this manually, you can follow the instructions below.
|
||||||
|
|
||||||
It's easy to make a mistake here, and end up with an unlinked version of PHP or have versions missing from PHP Monitor.
|
It's easy to make a mistake here, and end up with an unlinked version of PHP or have versions missing from PHP Monitor.
|
||||||
|
|
||||||
Here's what I usually do:
|
Here's what I usually do:
|
||||||
@ -255,7 +275,7 @@ This should resolve the issue! If that does not fix the issue, run `brew link ph
|
|||||||
|
|
||||||
brew install php
|
brew install php
|
||||||
brew link php --force
|
brew link php --force
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@ -310,12 +330,14 @@ Make sure you have at least **Valet 3.0** installed, since support for isolation
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
|
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
|
||||||
|
|
||||||
The value you provided in your INI file is invalid. If that is the case, PHP will attempt to parse your value as bytes, which is usually unintended. (`1GB` will resolve to merely a few bytes, and all of your applications will run out of memory!)
|
The value you provided in your `.ini` file is invalid. If that is the case, PHP will attempt to parse your value as bytes, which is usually unintended. (`1GB` will resolve to merely a few bytes, and all of your applications will run out of memory!)
|
||||||
|
|
||||||
You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` is also allowed, or just an integer (which will result in N amount of bytes being the limit).
|
You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` is also allowed, or just an integer (which will result in N amount of bytes being the limit).
|
||||||
|
|
||||||
**Example**: Trying to use `1GB` as the memory limit, for example, will result in this exclamation mark. The correct way to set a 1GB limit is by using `1G` as the value. (Note: The displayed value will append `B` for clarity, so if you set `1G`, the value reported by PHP Monitor will be 1 GB.)
|
**Example**: Trying to use `1GB` as the memory limit, for example, will result in this exclamation mark. The correct way to set a 1GB limit is by using `1G` as the value. (Note: The displayed value will append `B` for clarity, so if you set `1G`, the value reported by PHP Monitor will be 1 GB.)
|
||||||
|
|
||||||
|
(If you are using Valet, you can adjust these limits in the `.conf.d/php-memory-limits.ini` file. Otherwise, you may need to adjust `php.ini`.)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@ -404,6 +426,9 @@ You can omit the `php` key in the preset if you do not wish for the preset to sw
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>How do I ensure additional Homebrew services are shown in the app?</strong></summary>
|
<summary><strong>How do I ensure additional Homebrew services are shown in the app?</strong></summary>
|
||||||
|
|
||||||
|
> **Info**
|
||||||
|
> Homebrew services aren't displayed if you are using Valet in Standalone Mode.
|
||||||
|
|
||||||
You must set these services up in a JSON file, located in `~/.config/phpmon/config.json`.
|
You must set these services up in a JSON file, located in `~/.config/phpmon/config.json`.
|
||||||
|
|
||||||
You can specify custom services in the configuration file for Homebrew services that run as your own user (not root).
|
You can specify custom services in the configuration file for Homebrew services that run as your own user (not root).
|
||||||
@ -594,7 +619,7 @@ This utility will detect which PHP versions you have installed via Homebrew, and
|
|||||||
|
|
||||||
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be a bit faster than Valet’s switcher.
|
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be a bit faster than Valet’s switcher.
|
||||||
|
|
||||||
If you're using Valet 3, versions of PHP-FPM required to keep isolated sites up and running will also be started or stopped as needed.
|
If you're using Valet 3 or newer, versions of PHP-FPM required to keep isolated sites up and running will also be started or stopped as needed.
|
||||||
|
|
||||||
### Config change detection
|
### Config change detection
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
|
|||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 6.0 | ✅ Universal binary | ✅ Yes | 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 | ✅ 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 |
|
||||||
|
|
||||||
## Legacy versions
|
## Legacy versions
|
||||||
|
|
||||||
@ -14,7 +14,9 @@ These versions of PHP Monitor are no longer supported, but if you’re using an
|
|||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 5.8 | ✅ Universal binary | ✅ Yes | 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.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.
Before Width: | Height: | Size: 627 KiB After Width: | Height: | Size: 723 KiB |
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
25
phpmon/Assets.xcassets/php.imageset/Contents.json
vendored
Normal file
25
phpmon/Assets.xcassets/php.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
1
phpmon/Assets.xcassets/php.imageset/php.svg
vendored
Normal file
1
phpmon/Assets.xcassets/php.imageset/php.svg
vendored
Normal 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 |
@ -14,8 +14,6 @@ class Actions {
|
|||||||
|
|
||||||
public static func linkPhp() async {
|
public static func linkPhp() async {
|
||||||
await brew("link php --overwrite --force")
|
await brew("link php --overwrite --force")
|
||||||
|
|
||||||
// TODO: Verify that this worked, if not, notify the user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func restartPhpFpm() async {
|
public static func restartPhpFpm() async {
|
||||||
|
@ -18,6 +18,53 @@ struct Constants {
|
|||||||
*/
|
*/
|
||||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||||
|
|
||||||
|
/**
|
||||||
|
PHP Monitor supplies a hardcoded list of PHP packages in its own
|
||||||
|
PHP Version Manager.
|
||||||
|
|
||||||
|
This hardcoded list will expire and will need to be modified when
|
||||||
|
the cutoff date occurs, which is when the `php` formula will
|
||||||
|
become PHP 8.4, and a new build will need to be made.
|
||||||
|
|
||||||
|
If users launch an older version of the app, then a warning
|
||||||
|
will be displayed to let them know that certain operations
|
||||||
|
will not work correctly and that they need to update their app.
|
||||||
|
*/
|
||||||
|
static let PhpFormulaeCutoffDate = "2024-11-01" // YYYY-MM-DD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PHP versions that are considered pre-release versions.
|
||||||
|
* Past a certain date, an experimental version "graduates"
|
||||||
|
* to a release version and is no longer marked as experimental.
|
||||||
|
*/
|
||||||
|
static var ExperimentalPhpVersions: Set<String> {
|
||||||
|
let releaseDates = [
|
||||||
|
"8.4": Date.fromString("2024-12-01") // PLACEHOLDER DATE
|
||||||
|
]
|
||||||
|
|
||||||
|
return Set(releaseDates
|
||||||
|
.filter { (_: String, date: Date?) in
|
||||||
|
guard let date else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return date > Date.now
|
||||||
|
}.map { (version: String, _: Date?) in
|
||||||
|
return version
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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.
|
||||||
@ -25,7 +72,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"
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,14 +89,14 @@ 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.3" // dev
|
"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.3" // dev
|
"8.4" // dev
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -82,6 +130,8 @@ struct Constants {
|
|||||||
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
|
// EAP URLs
|
||||||
|
|
||||||
static let EarlyAccessCaskFile = URL(
|
static let EarlyAccessCaskFile = URL(
|
||||||
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
||||||
)!
|
)!
|
||||||
|
@ -17,6 +17,7 @@ public class Paths {
|
|||||||
|
|
||||||
internal var baseDir: Paths.HomebrewDir
|
internal var baseDir: Paths.HomebrewDir
|
||||||
private var userName: String
|
private var userName: String
|
||||||
|
private var preferredShell: String
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Assume the default directory is correct
|
// Assume the default directory is correct
|
||||||
@ -31,7 +32,12 @@ public class Paths {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userName = identity()
|
userName = identity()
|
||||||
Log.info("The current username is `\(userName)`.")
|
preferredShell = preferred_shell()
|
||||||
|
|
||||||
|
if !isRunningSwiftUIPreview {
|
||||||
|
Log.info("The current username is `\(userName)`.")
|
||||||
|
Log.info("The user's shell is `\(preferredShell)`.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func detectBinaryPaths() {
|
public func detectBinaryPaths() {
|
||||||
@ -96,11 +102,23 @@ public class Paths {
|
|||||||
return "\(shared.baseDir.rawValue)/etc"
|
return "\(shared.baseDir.rawValue)/etc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var tapPath: String {
|
||||||
|
if shared.baseDir == .usr {
|
||||||
|
return "\(shared.baseDir.rawValue)/homebrew/Library/Taps"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(shared.baseDir.rawValue)/Library/Taps"
|
||||||
|
}
|
||||||
|
|
||||||
public static var caskroomPath: String {
|
public static var caskroomPath: String {
|
||||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var shell: String {
|
||||||
|
return shared.preferredShell
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Flexible Binaries
|
// MARK: - Flexible Binaries
|
||||||
// (these can be in multiple locations, so we scan common places because)
|
// (these can be in multiple locations, so we scan common places because)
|
||||||
// (PHP Monitor will not use the user's own PATH)
|
// (PHP Monitor will not use the user's own PATH)
|
||||||
|
@ -15,4 +15,10 @@ extension Date {
|
|||||||
return dateFormatter.string(from: self)
|
return dateFormatter.string(from: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func fromString(_ string: String) -> Date? {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
return dateFormatter.date(from: string)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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,13 +44,30 @@ struct Localization {
|
|||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
var localized: String {
|
var localized: String {
|
||||||
if #available(macOS 13, *) {
|
var preferredBundle: Bundle = Localization.bundle
|
||||||
return NSLocalizedString(
|
|
||||||
self, tableName: nil, bundle: Localization.bundle, value: "", comment: ""
|
if let preferred = Localization.preferredLanguage,
|
||||||
).replacingOccurrences(of: "Preferences", with: "Settings")
|
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
|
||||||
|
let bundle = Bundle(path: path) {
|
||||||
|
preferredBundle = bundle
|
||||||
}
|
}
|
||||||
|
|
||||||
return NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
|
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)
|
||||||
|
if string == self {
|
||||||
|
guard let path = Localization.bundle.path(forResource: "en", ofType: "lproj"),
|
||||||
|
let bundle = Bundle(path: path)
|
||||||
|
else { return self }
|
||||||
|
return NSLocalizedString(self, bundle: bundle, comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that on more recent versions of macOS, "Preferences" is replaced with "Settings"
|
||||||
|
if #available(macOS 13, *) {
|
||||||
|
return string.replacingOccurrences(of: "Preferences", with: "Settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
var localizedForSwiftUI: LocalizedStringKey {
|
var localizedForSwiftUI: LocalizedStringKey {
|
||||||
@ -131,4 +160,10 @@ extension String {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isNumber: Bool {
|
||||||
|
return self.range(
|
||||||
|
of: "^[0-9]*$", // 1
|
||||||
|
options: .regularExpression) != nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
|
|||||||
// MARK: — FS Attributes
|
// MARK: — FS Attributes
|
||||||
|
|
||||||
func makeExecutable(_ path: String) throws {
|
func makeExecutable(_ path: String) throws {
|
||||||
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Checks
|
// MARK: - Checks
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)")
|
||||||
}
|
}
|
||||||
@ -37,7 +39,7 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
|
|
||||||
extension NSWindowController {
|
extension NSWindowController {
|
||||||
|
|
||||||
public func positionWindowInTopLeftCorner(offsetY: CGFloat = 0, offsetX: CGFloat = 0) {
|
public func positionWindowInTopRightCorner(offsetY: CGFloat = 0, offsetX: CGFloat = 0) {
|
||||||
guard let frame = NSScreen.main?.frame else { return }
|
guard let frame = NSScreen.main?.frame else { return }
|
||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import Foundation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
Run a simple blocking Shell command on the user's own system.
|
Run a simple blocking Shell command on the user's own system.
|
||||||
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
|
|
||||||
*/
|
*/
|
||||||
public func system(_ command: String) -> String {
|
public func system(_ command: String) -> String {
|
||||||
let task = Process()
|
let task = Process()
|
||||||
@ -65,3 +64,11 @@ public func identity() -> String {
|
|||||||
|
|
||||||
return output.trimmingCharacters(in: .whitespacesAndNewlines)
|
return output.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Retrieves the user's preferred shell.
|
||||||
|
*/
|
||||||
|
public func preferred_shell() -> String {
|
||||||
|
return system("dscl . -read ~/ UserShell | sed 's/UserShell: //'")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
@ -62,13 +62,6 @@ class ActivePhpInstallation {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load extension information
|
|
||||||
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
|
||||||
|
|
||||||
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
|
||||||
iniFiles.append(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get configuration values
|
// Get configuration values
|
||||||
limits = Limits(
|
limits = Limits(
|
||||||
memory_limit: getByteCount(key: "memory_limit"),
|
memory_limit: getByteCount(key: "memory_limit"),
|
||||||
@ -76,15 +69,10 @@ class ActivePhpInstallation {
|
|||||||
post_max_size: getByteCount(key: "post_max_size")
|
post_max_size: getByteCount(key: "post_max_size")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Return a list of .ini files parsed after php.ini
|
let paths = ActiveShell.shared
|
||||||
let paths = Command.execute(
|
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
path: Paths.php,
|
.split(separator: "\n")
|
||||||
arguments: ["-r", "echo php_ini_scanned_files();"],
|
.map { String($0) }
|
||||||
trimNewlines: false
|
|
||||||
)
|
|
||||||
.replacingOccurrences(of: "\n", with: "")
|
|
||||||
.split(separator: ",")
|
|
||||||
.map { String($0) }
|
|
||||||
|
|
||||||
// See if any extensions are present in said .ini files
|
// See if any extensions are present in said .ini files
|
||||||
paths.forEach { (iniFilePath) in
|
paths.forEach { (iniFilePath) in
|
||||||
|
@ -13,7 +13,7 @@ class PhpEnvironments {
|
|||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loads the currently active PHP installation upon startup. May be empty.
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
self.currentInstall = ActivePhpInstallation.load()
|
self.currentInstall = ActivePhpInstallation.load()
|
||||||
@ -29,7 +29,7 @@ class PhpEnvironments {
|
|||||||
/**
|
/**
|
||||||
Determine which PHP version the `php` formula is aliased to.
|
Determine which PHP version the `php` formula is aliased to.
|
||||||
*/
|
*/
|
||||||
func determinePhpAlias() async {
|
@MainActor func determinePhpAlias() async {
|
||||||
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
||||||
|
|
||||||
self.homebrewPackage = try! JSONDecoder().decode(
|
self.homebrewPackage = try! JSONDecoder().decode(
|
||||||
@ -37,7 +37,28 @@ class PhpEnvironments {
|
|||||||
from: brewPhpAlias.data(using: .utf8)!
|
from: brewPhpAlias.data(using: .utf8)!
|
||||||
).first!
|
).first!
|
||||||
|
|
||||||
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!")
|
PhpEnvironments.brewPhpAlias = self.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
|
||||||
|
let phpConfigExecutablePath = "\(Paths.optPath)/php/bin/php-config"
|
||||||
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
|
let longVersionString = Command.execute(
|
||||||
|
path: phpConfigExecutablePath,
|
||||||
|
arguments: ["--version"],
|
||||||
|
trimNewlines: false
|
||||||
|
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
if let version = try? VersionNumber.parse(longVersionString) {
|
||||||
|
PhpEnvironments.brewPhpAlias = version.short
|
||||||
|
if version.short != homebrewPackage.version {
|
||||||
|
Log.info("[BREW] An older version of `php` is actually installed (\(version.short)).")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not determine the actual version of the php binary; assuming Homebrew is correct.")
|
||||||
|
PhpEnvironments.brewPhpAlias = homebrewPackage.version
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
@ -49,12 +70,10 @@ class PhpEnvironments {
|
|||||||
static let shared = PhpEnvironments()
|
static let shared = PhpEnvironments()
|
||||||
|
|
||||||
/** Whether the switcher is busy performing any actions. */
|
/** Whether the switcher is busy performing any actions. */
|
||||||
var isBusy: Bool = false {
|
@MainActor var isBusy: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
Task { @MainActor in
|
MainMenu.shared.refreshIcon()
|
||||||
MainMenu.shared.setBusyImage()
|
MainMenu.shared.rebuild()
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +87,14 @@ class PhpEnvironments {
|
|||||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||||
|
|
||||||
/** Information about the currently linked PHP installation. */
|
/** Information about the currently linked PHP installation. */
|
||||||
var currentInstall: ActivePhpInstallation?
|
var currentInstall: ActivePhpInstallation? {
|
||||||
|
didSet {
|
||||||
|
// Let the PHP extension manager, if it exists, know the version changed
|
||||||
|
if let version = currentInstall?.version.short {
|
||||||
|
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The version that the `php` formula via Brew is aliased to on the current system.
|
The version that the `php` formula via Brew is aliased to on the current system.
|
||||||
@ -79,7 +105,12 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
As such, we take that information from Homebrew.
|
As such, we take that information from Homebrew.
|
||||||
*/
|
*/
|
||||||
static var brewPhpAlias: String {
|
static var brewPhpAlias: String = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
It's possible for the alias to be newer than the actual installed version of PHP.
|
||||||
|
*/
|
||||||
|
static var homebrewBrewPhpAlias: String {
|
||||||
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
||||||
|
|
||||||
return PhpEnvironments.shared.homebrewPackage.version
|
return PhpEnvironments.shared.homebrewPackage.version
|
||||||
@ -146,7 +177,12 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
// Avoid inserting a duplicate
|
// Avoid inserting a duplicate
|
||||||
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||||
supportedVersions.insert(phpAlias)
|
let phpAliasInstall = PhpInstallation(phpAlias)
|
||||||
|
// Before inserting, ensure that the actual output matches the alias
|
||||||
|
// if that isn't the case, our formula remains out-of-date
|
||||||
|
if !phpAliasInstall.isMissingBinary {
|
||||||
|
supportedVersions.insert(phpAlias)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
availablePhpVersions = Array(supportedVersions)
|
availablePhpVersions = Array(supportedVersions)
|
||||||
|
@ -49,8 +49,10 @@ class PhpHelper {
|
|||||||
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
|
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
|
||||||
.resolvingSymlinksInPath().path
|
.resolvingSymlinksInPath().path
|
||||||
|
|
||||||
// The contents of the script!
|
// Check if the user uses Fish
|
||||||
let script = script(path, keyPhrase, version, dotless)
|
let script = Paths.shell.contains("/fish")
|
||||||
|
? fishScript(path, keyPhrase, version, dotless)
|
||||||
|
: zshScript(path, keyPhrase, version, dotless)
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try FileSystem.writeAtomicallyToFile(destination, content: script)
|
try FileSystem.writeAtomicallyToFile(destination, content: script)
|
||||||
@ -78,7 +80,7 @@ class PhpHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func script(
|
private static func zshScript(
|
||||||
_ path: String,
|
_ path: String,
|
||||||
_ keyPhrase: String,
|
_ keyPhrase: String,
|
||||||
_ version: String,
|
_ version: String,
|
||||||
@ -96,6 +98,22 @@ class PhpHelper {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func fishScript(
|
||||||
|
_ path: String,
|
||||||
|
_ keyPhrase: String,
|
||||||
|
_ version: String,
|
||||||
|
_ dotless: String
|
||||||
|
) -> String {
|
||||||
|
return """
|
||||||
|
#!\(Paths.binPath)/fish
|
||||||
|
# \(keyPhrase)
|
||||||
|
# It reflects the location of PHP \(version)'s binaries on your system.
|
||||||
|
# Usage: . pm\(dotless)
|
||||||
|
echo "PHP Monitor has enabled this terminal to use PHP \(version)."; \\
|
||||||
|
set -x PATH \(path) $PATH
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
private static func createSymlink(_ dotless: String) async {
|
private static func createSymlink(_ dotless: String) async {
|
||||||
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
||||||
let destination = "/usr/local/bin/pm\(dotless)"
|
let destination = "/usr/local/bin/pm\(dotless)"
|
||||||
|
@ -69,8 +69,9 @@ class PhpConfigurationFile: CreatedFromFile {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReplacementErrors: Error {
|
public enum ReplacementErrors: Error {
|
||||||
case missingKey
|
case missingKey
|
||||||
|
case missingFile
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -95,10 +96,16 @@ class PhpConfigurationFile: CreatedFromFile {
|
|||||||
// Replace the specific line
|
// Replace the specific line
|
||||||
self.lines[item.lineIndex] = components.joined(separator: "=")
|
self.lines[item.lineIndex] = components.joined(separator: "=")
|
||||||
|
|
||||||
|
// Ensure the watchers aren't tripped up by config changes
|
||||||
|
ConfigWatchManager.ignoresModificationsToConfigValues = true
|
||||||
|
|
||||||
// Finally, join the string and save the file atomatically again
|
// Finally, join the string and save the file atomatically again
|
||||||
try self.lines.joined(separator: "\n")
|
try self.lines.joined(separator: "\n")
|
||||||
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
|
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
// Ensure watcher behaviour is reverted
|
||||||
|
ConfigWatchManager.ignoresModificationsToConfigValues = false
|
||||||
|
|
||||||
// Reload the original file
|
// Reload the original file
|
||||||
self.reload()
|
self.reload()
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ class PhpExtension {
|
|||||||
|
|
||||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||||
|
|
||||||
self.enabled = !line.contains(";")
|
self.enabled = !line.starts(with: ";")
|
||||||
self.file = file
|
self.file = file
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ class PhpExtension {
|
|||||||
You may need to restart the other services in order for this change to apply.
|
You may need to restart the other services in order for this change to apply.
|
||||||
*/
|
*/
|
||||||
func toggle() async {
|
func toggle() async {
|
||||||
let newLine = enabled
|
let newLine = !line.starts(with: ";")
|
||||||
// DISABLED: Commented out line
|
// DISABLED: Commented out line
|
||||||
? "; \(line)"
|
? "; \(line)"
|
||||||
// ENABLED: Line where the comment delimiter (;) is removed
|
// ENABLED: Line where the comment delimiter (;) is removed
|
||||||
@ -84,14 +84,14 @@ class PhpExtension {
|
|||||||
|
|
||||||
await sed(file: file, original: line, replacement: newLine)
|
await sed(file: file, original: line, replacement: newLine)
|
||||||
|
|
||||||
enabled.toggle()
|
self.enabled = !newLine.starts(with: ";")
|
||||||
|
self.line = newLine
|
||||||
|
|
||||||
if !isRunningTests {
|
if !isRunningTests {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
MainMenu.shared.rebuild()
|
MainMenu.shared.rebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Static Methods
|
// MARK: - Static Methods
|
||||||
|
@ -12,19 +12,46 @@ class PhpInstallation {
|
|||||||
|
|
||||||
var versionNumber: VersionNumber
|
var versionNumber: VersionNumber
|
||||||
|
|
||||||
|
var iniFiles: [PhpConfigurationFile] = []
|
||||||
|
|
||||||
|
var isMissingBinary: Bool = false
|
||||||
|
|
||||||
var isHealthy: Bool = true
|
var isHealthy: Bool = true
|
||||||
|
|
||||||
|
var extensions: [PhpExtension] {
|
||||||
|
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,
|
||||||
we’ll simply run `php-config --version` in the relevant directory.
|
we’ll simply run `php-config --version` in the relevant directory.
|
||||||
*/
|
*/
|
||||||
init(_ version: String) {
|
init(_ version: String) {
|
||||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
|
||||||
|
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||||
|
|
||||||
let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
versionNumber = VersionNumber.make(from: version)!
|
||||||
|
|
||||||
self.versionNumber = VersionNumber.make(from: version)!
|
determineVersion(phpConfigExecutablePath, phpExecutablePath)
|
||||||
|
determineHealth(phpExecutablePath)
|
||||||
|
determineIniFiles(phpExecutablePath)
|
||||||
|
|
||||||
|
// Find all enabled extensions
|
||||||
|
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
|
||||||
|
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpConfigExecutablePath) {
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
let longVersionString = Command.execute(
|
let longVersionString = Command.execute(
|
||||||
path: phpConfigExecutablePath,
|
path: phpConfigExecutablePath,
|
||||||
@ -34,9 +61,15 @@ class PhpInstallation {
|
|||||||
|
|
||||||
// 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.
|
||||||
self.versionNumber = try! VersionNumber.parse(longVersionString)
|
versionNumber = try! VersionNumber.parse(longVersionString)
|
||||||
|
} else {
|
||||||
|
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
|
||||||
|
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
|
||||||
|
isMissingBinary = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineHealth(_ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpExecutablePath) {
|
if FileSystem.fileExists(phpExecutablePath) {
|
||||||
let testCommand = Command.execute(
|
let testCommand = Command.execute(
|
||||||
path: phpExecutablePath,
|
path: phpExecutablePath,
|
||||||
@ -53,4 +86,18 @@ class PhpInstallation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func determineIniFiles(_ phpExecutablePath: String) {
|
||||||
|
let paths = ActiveShell.shared
|
||||||
|
.sync("\(phpExecutablePath) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
|
.split(separator: "\n")
|
||||||
|
.map { String($0) }
|
||||||
|
|
||||||
|
// See if any extensions are present in said .ini files
|
||||||
|
paths.forEach { (iniFilePath) in
|
||||||
|
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
|
||||||
|
iniFiles.append(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 {
|
||||||
|
@ -86,14 +86,37 @@ class RealShell: ShellProtocol {
|
|||||||
|
|
||||||
// MARK: - Shellable Protocol
|
// MARK: - Shellable Protocol
|
||||||
|
|
||||||
|
func sync(_ command: String) -> ShellOutput {
|
||||||
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
|
let outputPipe = Pipe()
|
||||||
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
|
sleep(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
task.standardOutput = outputPipe
|
||||||
|
task.standardError = errorPipe
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
|
||||||
|
if Log.shared.verbosity == .cli {
|
||||||
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
func pipe(_ command: String) async -> ShellOutput {
|
func pipe(_ command: String) async -> ShellOutput {
|
||||||
let task = getShellProcess(for: command)
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
let outputPipe = Pipe()
|
let outputPipe = Pipe()
|
||||||
let errorPipe = Pipe()
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
// Seriously slow down how long it takes for the shell to return output
|
|
||||||
// (in order to debug or identify async issues)
|
|
||||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
Log.info("[SLOW SHELL] \(command)")
|
Log.info("[SLOW SHELL] \(command)")
|
||||||
await delay(seconds: 3.0)
|
await delay(seconds: 3.0)
|
||||||
@ -104,20 +127,20 @@ class RealShell: ShellProtocol {
|
|||||||
task.launch()
|
task.launch()
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
|
|
||||||
let stdOut = String(
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
let stdErr = String(
|
|
||||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
if Log.shared.verbosity == .cli {
|
if Log.shared.verbosity == .cli {
|
||||||
var args = task.arguments ?? []
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
}
|
||||||
var log = """
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func log(task: Process, stdOut: String, stdErr: String) {
|
||||||
|
var args = task.arguments ?? []
|
||||||
|
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||||
|
var log = """
|
||||||
|
|
||||||
<~~~~~~~~~~~~~~~~~~~~~~~
|
<~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
||||||
@ -126,22 +149,19 @@ class RealShell: ShellProtocol {
|
|||||||
\(stdOut)
|
\(stdOut)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if !stdErr.isEmpty {
|
if !stdErr.isEmpty {
|
||||||
log.append("""
|
log.append("""
|
||||||
[ERR]:
|
[ERR]:
|
||||||
\(stdErr)
|
\(stdErr)
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.append("""
|
log.append("""
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~>
|
~~~~~~~~~~~~~~~~~~~~~~~~>
|
||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
Log.info(log)
|
Log.info(log)
|
||||||
}
|
|
||||||
|
|
||||||
return .out(stdOut, stdErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func quiet(_ command: String) async {
|
func quiet(_ command: String) async {
|
||||||
|
@ -14,6 +14,16 @@ protocol ShellProtocol {
|
|||||||
*/
|
*/
|
||||||
var PATH: String { get }
|
var PATH: String { get }
|
||||||
|
|
||||||
|
/**
|
||||||
|
Run a command synchronously. Use with caution.
|
||||||
|
|
||||||
|
Common usage:
|
||||||
|
```
|
||||||
|
let output = Shell.sync("php -v")
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
func sync(_ command: String) -> ShellOutput
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Run a command asynchronously.
|
Run a command asynchronously.
|
||||||
Returns the most relevant output (prefers error output if it exists).
|
Returns the most relevant output (prefers error output if it exists).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// PhpFormulaeStatus.swift
|
// BusyStatus.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 02/05/2023.
|
// Created by Nico Verbruggen on 02/05/2023.
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PhpFormulaeStatus: ObservableObject {
|
class BusyStatus: ObservableObject {
|
||||||
@Published var busy: Bool
|
@Published var busy: Bool
|
||||||
@Published var title: String
|
@Published var title: String
|
||||||
@Published var description: String
|
@Published var description: String
|
||||||
@ -18,4 +18,12 @@ class PhpFormulaeStatus: ObservableObject {
|
|||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func notBusy() -> BusyStatus {
|
||||||
|
return BusyStatus(busy: false, title: "", description: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func busy() -> BusyStatus {
|
||||||
|
return BusyStatus(busy: false, title: "", description: "")
|
||||||
|
}
|
||||||
}
|
}
|
@ -43,6 +43,7 @@ public struct TestableConfiguration: Codable {
|
|||||||
private var primaryPhpVersion: VersionNumber?
|
private var primaryPhpVersion: VersionNumber?
|
||||||
private var secondaryPhpVersions: [VersionNumber] = []
|
private var secondaryPhpVersions: [VersionNumber] = []
|
||||||
|
|
||||||
|
// swiftlint:disable function_body_length
|
||||||
mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) {
|
mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) {
|
||||||
if primary {
|
if primary {
|
||||||
if primaryPhpVersion != nil {
|
if primaryPhpVersion != nil {
|
||||||
@ -56,6 +57,8 @@ public struct TestableConfiguration: Codable {
|
|||||||
self.filesystem = self.filesystem.merging([
|
self.filesystem = self.filesystem.merging([
|
||||||
"/opt/homebrew/opt/php@\(version.short)/bin/php"
|
"/opt/homebrew/opt/php@\(version.short)/bin/php"
|
||||||
: .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php"),
|
: .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php"),
|
||||||
|
"/opt/homebrew/opt/php@\(version.short)/bin/php-config"
|
||||||
|
: .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php-config"),
|
||||||
"/opt/homebrew/Cellar/php/\(version.long)/bin/php"
|
"/opt/homebrew/Cellar/php/\(version.long)/bin/php"
|
||||||
: .fake(.binary),
|
: .fake(.binary),
|
||||||
"/opt/homebrew/Cellar/php/\(version.long)/bin/php-config"
|
"/opt/homebrew/Cellar/php/\(version.long)/bin/php-config"
|
||||||
@ -70,9 +73,26 @@ public struct TestableConfiguration: Codable {
|
|||||||
: .fake(.text)
|
: .fake(.text)
|
||||||
]) { (_, new) in new }
|
]) { (_, new) in new }
|
||||||
|
|
||||||
|
// PHP configuration files
|
||||||
|
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"]
|
||||||
@ -80,22 +100,20 @@ public struct TestableConfiguration: Codable {
|
|||||||
self.filesystem["/opt/homebrew/bin/php"]
|
self.filesystem["/opt/homebrew/bin/php"]
|
||||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php")
|
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php")
|
||||||
self.filesystem["/opt/homebrew/bin/php-config"]
|
self.filesystem["/opt/homebrew/bin/php-config"]
|
||||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/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
|
||||||
.map { "php@\($0.short)" }
|
.map { "php@\($0.short)" }
|
||||||
.joined(separator: "\n")
|
.joined(separator: "\n")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// swiftlint:enable function_body_length
|
||||||
|
|
||||||
// MARK: Interactions
|
// MARK: Interactions
|
||||||
|
|
||||||
|
@ -19,6 +19,17 @@ public class TestableShell: ShellProtocol {
|
|||||||
|
|
||||||
var expectations: [String: BatchFakeShellOutput] = [:]
|
var expectations: [String: BatchFakeShellOutput] = [:]
|
||||||
|
|
||||||
|
func sync(_ command: String) -> ShellOutput {
|
||||||
|
// This assertion will only fire during test builds
|
||||||
|
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
|
||||||
|
|
||||||
|
guard let expectation = expectations[command] else {
|
||||||
|
return .err("No Expected Output")
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectation.syncOutput()
|
||||||
|
}
|
||||||
|
|
||||||
func quiet(_ command: String) async {
|
func quiet(_ command: String) async {
|
||||||
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
||||||
}
|
}
|
||||||
@ -112,6 +123,29 @@ struct BatchFakeShellOutput: Codable {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Outputs the fake shell output as expected, but does this synchronously.
|
||||||
|
*/
|
||||||
|
public func syncOutput(
|
||||||
|
ignoreDelay: Bool = false
|
||||||
|
) -> ShellOutput {
|
||||||
|
let output = ShellOutput.empty()
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
if !ignoreDelay {
|
||||||
|
Thread.sleep(forTimeInterval: item.delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.stream == .stdErr {
|
||||||
|
output.err += item.output
|
||||||
|
} else if item.stream == .stdOut {
|
||||||
|
output.out += item.output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
||||||
*/
|
*/
|
||||||
|
@ -17,7 +17,9 @@
|
|||||||
<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 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>
|
||||||
<br>
|
<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!)
|
||||||
|
<br/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@ -46,8 +46,10 @@ extension App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hotkey.keyDownHandler = {
|
hotkey.keyDownHandler = {
|
||||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
Task { @MainActor in
|
||||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||||
|
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,11 +74,17 @@ class App {
|
|||||||
/** The window controller of the onboarding window. */
|
/** The window controller of the onboarding window. */
|
||||||
var onboardingWindowController: OnboardingWindowController?
|
var onboardingWindowController: OnboardingWindowController?
|
||||||
|
|
||||||
/** The window controller of the warnings window. */
|
/** The window controller of the config manager window. */
|
||||||
var warningsWindowController: WarningsWindowController?
|
var phpConfigManagerWindowController: PhpConfigManagerWindowController?
|
||||||
|
|
||||||
/** The window controller of the warnings window. */
|
/** The window controller of the warnings window. */
|
||||||
var versionManagerWindowController: PhpVersionManagerWindowController?
|
var phpDoctorWindowController: PhpDoctorWindowController?
|
||||||
|
|
||||||
|
/** The window controller of the PHP version manager window. */
|
||||||
|
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
||||||
|
|
||||||
|
/** The window controller of the PHP extension manager window. */
|
||||||
|
var phpExtensionManagerWindowController: PhpExtensionManagerWindowController?
|
||||||
|
|
||||||
/** 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] = []
|
||||||
@ -86,9 +92,6 @@ class App {
|
|||||||
/** 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
|
||||||
|
|
||||||
/** The filesystem watchers, responsible for keeping track of changes to the PHP installation. */
|
|
||||||
var watchers: [FSNotifier.Kind: FSNotifier] = [:]
|
|
||||||
|
|
||||||
/** Timer that will periodically reload info about the user's PHP installation. */
|
/** Timer that will periodically reload info about the user's PHP installation. */
|
||||||
var timer: Timer?
|
var timer: Timer?
|
||||||
|
|
||||||
@ -117,8 +120,12 @@ class App {
|
|||||||
|
|
||||||
// MARK: - App Watchers
|
// MARK: - App Watchers
|
||||||
|
|
||||||
/**
|
/** Individual filesystem watchers, which are, i.e. responsible for watching the Homebrew folders. */
|
||||||
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
var watchers: [String: FSNotifier] = [:]
|
||||||
|
|
||||||
|
/**
|
||||||
|
The `ConfigWatchManager` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
||||||
|
This manager object can immediately start or stop all watchers (or pause them) all at once.
|
||||||
*/
|
*/
|
||||||
var watcher: PhpConfigWatcher!
|
var watchManager: ConfigWatchManager!
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,10 @@ import UserNotifications
|
|||||||
@NSApplicationMain
|
@NSApplicationMain
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
|
static var instance: AppDelegate {
|
||||||
|
return NSApplication.shared.delegate as! AppDelegate
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Variables
|
// MARK: - Variables
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,10 +88,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
Log.info("Extra CLI mode is on (`~/.config/phpmon/verbose` exists).")
|
Log.info("Extra CLI mode is on (`~/.config/phpmon/verbose` exists).")
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.separator(as: .info)
|
if !isRunningSwiftUIPreview {
|
||||||
Log.info("PHP MONITOR by Nico Verbruggen")
|
Log.separator(as: .info)
|
||||||
Log.info("Version \(App.version)")
|
Log.info("PHP MONITOR by Nico Verbruggen")
|
||||||
Log.separator(as: .info)
|
Log.info("Version \(App.version)")
|
||||||
|
Log.separator(as: .info)
|
||||||
|
}
|
||||||
|
|
||||||
self.state = App.shared
|
self.state = App.shared
|
||||||
self.menu = MainMenu.shared
|
self.menu = MainMenu.shared
|
||||||
@ -103,6 +109,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +123,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
startup procedure.
|
startup procedure.
|
||||||
*/
|
*/
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
// Prevent previews from kicking off a costly boot
|
||||||
|
if isRunningSwiftUIPreview {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure notifications will work
|
// Make sure notifications will work
|
||||||
setupNotifications()
|
setupNotifications()
|
||||||
|
|
||||||
@ -121,4 +135,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
await menu.startup()
|
await menu.startup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Menu Items
|
||||||
|
|
||||||
|
@IBOutlet weak var menuItemSites: NSMenuItem!
|
||||||
|
|
||||||
|
/**
|
||||||
|
Ensure relevant menu items in the main menu bar (not the pop-up menu)
|
||||||
|
are disabled or hidden when needed.
|
||||||
|
*/
|
||||||
|
public func configureMenuItems(standalone: Bool) {
|
||||||
|
if standalone {
|
||||||
|
menuItemSites.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
|
||||||
<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"/>
|
||||||
@ -34,18 +34,6 @@
|
|||||||
</items>
|
</items>
|
||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem title="File" id="XRy-v5-KNb">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="File" id="zA7-mh-f1x">
|
|
||||||
<items>
|
|
||||||
<menuItem title="Close" keyEquivalent="w" id="2FI-pQ-tuO">
|
|
||||||
<connections>
|
|
||||||
<action selector="performClose:" target="Ady-hI-5gd" id="ZHq-so-Sba"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Sites" id="9gy-d3-Pos">
|
<menuItem title="Sites" id="9gy-d3-Pos">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
<menu key="submenu" title="Sites" id="YTZ-bb-TOG">
|
<menu key="submenu" title="Sites" id="YTZ-bb-TOG">
|
||||||
@ -82,12 +70,12 @@
|
|||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
<menu key="submenu" title="Edit" id="8Pm-83-BlM">
|
<menu key="submenu" title="Edit" id="8Pm-83-BlM">
|
||||||
<items>
|
<items>
|
||||||
<menuItem title="Undo" keyEquivalent="z" id="jCt-Yf-FSE">
|
<menuItem title="Undo" enabled="NO" keyEquivalent="z" id="jCt-Yf-FSE">
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="undo:" target="Ady-hI-5gd" id="O3z-27-Ug0"/>
|
<action selector="undo:" target="Ady-hI-5gd" id="O3z-27-Ug0"/>
|
||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem title="Redo" keyEquivalent="Z" id="fCh-1M-Qyg">
|
<menuItem title="Redo" enabled="NO" keyEquivalent="Z" id="fCh-1M-Qyg">
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="redo:" target="Ady-hI-5gd" id="utE-Bv-fdY"/>
|
<action selector="redo:" target="Ady-hI-5gd" id="utE-Bv-fdY"/>
|
||||||
</connections>
|
</connections>
|
||||||
@ -297,6 +285,18 @@
|
|||||||
</items>
|
</items>
|
||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
|
<menuItem title="Window" id="XRy-v5-KNb">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Window" id="zA7-mh-f1x">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Close" keyEquivalent="w" id="2FI-pQ-tuO">
|
||||||
|
<connections>
|
||||||
|
<action selector="performClose:" target="Ady-hI-5gd" id="ZHq-so-Sba"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||||
@ -317,7 +317,11 @@
|
|||||||
</application>
|
</application>
|
||||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
|
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="menuItemSites" destination="9gy-d3-Pos" id="nul-IL-YuR"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="-360" y="-94"/>
|
<point key="canvasLocation" x="-360" y="-94"/>
|
||||||
</scene>
|
</scene>
|
||||||
@ -517,9 +521,6 @@
|
|||||||
<subviews>
|
<subviews>
|
||||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8zu-cF-KCX">
|
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8zu-cF-KCX">
|
||||||
<rect key="frame" x="383" y="13" width="104" height="32"/>
|
<rect key="frame" x="383" y="13" width="104" height="32"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="4Uf-fh-jWJ"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="Primary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="F26-vf-hFH">
|
<buttonCell key="cell" type="push" title="Primary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="F26-vf-hFH">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@ -527,15 +528,15 @@
|
|||||||
DQ
|
DQ
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="4Uf-fh-jWJ"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="primaryButtonAction:" target="hkw-9V-NxP" id="W7d-3b-pZT"/>
|
<action selector="primaryButtonAction:" target="hkw-9V-NxP" id="W7d-3b-pZT"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TCp-nS-HN2">
|
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TCp-nS-HN2">
|
||||||
<rect key="frame" x="281" y="13" width="104" height="32"/>
|
<rect key="frame" x="281" y="13" width="104" height="32"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="QWZ-BA-0g9"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="Secondary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="eCk-FC-9Zr">
|
<buttonCell key="cell" type="push" title="Secondary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="eCk-FC-9Zr">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@ -543,6 +544,9 @@ DQ
|
|||||||
Gw
|
Gw
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="QWZ-BA-0g9"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="secondaryButtonAction:" target="hkw-9V-NxP" id="YJs-Hu-lFP"/>
|
<action selector="secondaryButtonAction:" target="hkw-9V-NxP" id="YJs-Hu-lFP"/>
|
||||||
</connections>
|
</connections>
|
||||||
@ -681,9 +685,6 @@ DQ
|
|||||||
</button>
|
</button>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
||||||
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
|
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@ -691,6 +692,9 @@ DQ
|
|||||||
Gw
|
Gw
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
|
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
|
||||||
</connections>
|
</connections>
|
||||||
@ -933,13 +937,13 @@ Gw
|
|||||||
<subviews>
|
<subviews>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
|
||||||
<rect key="frame" x="27" y="18" width="70" height="18"/>
|
<rect key="frame" x="27" y="18" width="70" height="18"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="70" id="MBa-bB-DTB"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="inline" title="PHP X.X" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="Tfk-YR-L4B">
|
<buttonCell key="cell" type="inline" title="PHP X.X" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="Tfk-YR-L4B">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="smallSystemBold"/>
|
<font key="font" metaFont="smallSystemBold"/>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="70" id="MBa-bB-DTB"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="pressedPhpVersion:" target="T49-0U-d58" id="jVO-TS-F6d"/>
|
<action selector="pressedPhpVersion:" target="T49-0U-d58" id="jVO-TS-F6d"/>
|
||||||
</connections>
|
</connections>
|
||||||
@ -969,7 +973,7 @@ Gw
|
|||||||
</tableCellView>
|
</tableCellView>
|
||||||
</prototypeCellViews>
|
</prototypeCellViews>
|
||||||
</tableColumn>
|
</tableColumn>
|
||||||
<tableColumn identifier="KIND" width="36" minWidth="36" maxWidth="36" id="7EV-ZL-92u">
|
<tableColumn identifier="KIND" width="50" minWidth="50" maxWidth="120" id="7EV-ZL-92u">
|
||||||
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Kind">
|
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Kind">
|
||||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
@ -983,11 +987,11 @@ Gw
|
|||||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||||
<prototypeCellViews>
|
<prototypeCellViews>
|
||||||
<tableCellView identifier="domainListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="DomainListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableCellView identifier="domainListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="DomainListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="403" y="0.0" width="36" height="54"/>
|
<rect key="frame" x="403" y="0.0" width="50" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
|
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
|
||||||
<rect key="frame" x="9" y="18" width="18" height="18"/>
|
<rect key="frame" x="16" y="18" width="18" height="18"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="18" id="XcB-uw-szU"/>
|
<constraint firstAttribute="width" constant="18" id="XcB-uw-szU"/>
|
||||||
<constraint firstAttribute="height" constant="18" id="bGN-Vh-Sh0"/>
|
<constraint firstAttribute="height" constant="18" id="bGN-Vh-Sh0"/>
|
||||||
@ -1020,7 +1024,7 @@ Gw
|
|||||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||||
<prototypeCellViews>
|
<prototypeCellViews>
|
||||||
<tableCellView identifier="domainListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="DomainListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableCellView identifier="domainListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="DomainListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="456" y="0.0" width="97" height="54"/>
|
<rect key="frame" x="470" y="0.0" width="97" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
||||||
@ -1088,9 +1092,18 @@ Gw
|
|||||||
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</progressIndicator>
|
</progressIndicator>
|
||||||
|
<customView translatesAutoresizingMaskIntoConstraints="NO" id="wcV-ed-8Bv">
|
||||||
|
<rect key="frame" x="113" y="5" width="400" height="300"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="400" id="HCo-LG-x3N"/>
|
||||||
|
<constraint firstAttribute="height" constant="300" id="Xpi-Rl-xmb"/>
|
||||||
|
</constraints>
|
||||||
|
</customView>
|
||||||
</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="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="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/>
|
||||||
@ -1099,6 +1112,7 @@ Gw
|
|||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<connections>
|
<connections>
|
||||||
|
<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="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
||||||
</connections>
|
</connections>
|
||||||
@ -1190,9 +1204,6 @@ DQ
|
|||||||
</button>
|
</button>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
|
||||||
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="uCc-fF-wS2"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="D8g-GE-7TU">
|
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="D8g-GE-7TU">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@ -1200,6 +1211,9 @@ DQ
|
|||||||
Gw
|
Gw
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="uCc-fF-wS2"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
|
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
|
||||||
</connections>
|
</connections>
|
||||||
@ -1336,9 +1350,6 @@ Gw
|
|||||||
<subviews>
|
<subviews>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
|
<button 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"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="Zhu-D8-cLK"/>
|
|
||||||
</constraints>
|
|
||||||
<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"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@ -1346,6 +1357,9 @@ Gw
|
|||||||
Gw
|
Gw
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="Zhu-D8-cLK"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="pressedCancel:" target="gOD-Gu-zDG" id="wMp-sM-0A4"/>
|
<action selector="pressedCancel:" target="gOD-Gu-zDG" id="wMp-sM-0A4"/>
|
||||||
</connections>
|
</connections>
|
||||||
|
@ -23,13 +23,13 @@ class InterApp {
|
|||||||
|
|
||||||
@MainActor static func getCommands() -> [InterApp.Action] { return [
|
@MainActor static func getCommands() -> [InterApp.Action] { return [
|
||||||
InterApp.Action(command: "list", action: { _ in
|
InterApp.Action(command: "list", action: { _ in
|
||||||
DomainListVC.show()
|
if Valet.installed { DomainListVC.show() }
|
||||||
}),
|
}),
|
||||||
InterApp.Action(command: "services/stop", action: { _ in
|
InterApp.Action(command: "services/stop", action: { _ in
|
||||||
Task { MainMenu.shared.stopValetServices() }
|
if Valet.installed { Task { MainMenu.shared.stopValetServices() } }
|
||||||
}),
|
}),
|
||||||
InterApp.Action(command: "services/restart/all", action: { _ in
|
InterApp.Action(command: "services/restart/all", action: { _ in
|
||||||
Task { MainMenu.shared.restartValetServices() }
|
if Valet.installed { Task { MainMenu.shared.restartValetServices() } }
|
||||||
}),
|
}),
|
||||||
InterApp.Action(command: "services/restart/nginx", action: { _ in
|
InterApp.Action(command: "services/restart/nginx", action: { _ in
|
||||||
Task { MainMenu.shared.restartNginx() }
|
Task { MainMenu.shared.restartNginx() }
|
||||||
@ -47,7 +47,7 @@ class InterApp {
|
|||||||
Task { MainMenu.shared.openGlobalComposerFolder() }
|
Task { MainMenu.shared.openGlobalComposerFolder() }
|
||||||
}),
|
}),
|
||||||
InterApp.Action(command: "locate/valet", action: { _ in
|
InterApp.Action(command: "locate/valet", action: { _ in
|
||||||
Task { MainMenu.shared.openValetConfigFolder() }
|
if Valet.installed { Task { MainMenu.shared.openValetConfigFolder() } }
|
||||||
}),
|
}),
|
||||||
InterApp.Action(command: "phpinfo", action: { _ in
|
InterApp.Action(command: "phpinfo", action: { _ in
|
||||||
Task { MainMenu.shared.openPhpInfo() }
|
Task { MainMenu.shared.openPhpInfo() }
|
||||||
|
@ -46,22 +46,22 @@ class ServicesManager: ObservableObject {
|
|||||||
|
|
||||||
public var statusMessage: String {
|
public var statusMessage: String {
|
||||||
if self.services.isEmpty || !self.firstRunComplete {
|
if self.services.isEmpty || !self.firstRunComplete {
|
||||||
return "Loading..."
|
return "phpman.services.loading".localized
|
||||||
}
|
}
|
||||||
|
|
||||||
let statuses = self.services[0...2].map { $0.status }
|
let statuses = self.services[0...2].map { $0.status }
|
||||||
|
|
||||||
if statuses.contains(.missing) {
|
if statuses.contains(.missing) {
|
||||||
return "A key service is not installed."
|
return "phpman.services.not_installed".localized
|
||||||
}
|
}
|
||||||
if statuses.contains(.error) {
|
if statuses.contains(.error) {
|
||||||
return "A key service is reporting an error state."
|
return "phpman.services.error".localized
|
||||||
}
|
}
|
||||||
if statuses.contains(.inactive) {
|
if statuses.contains(.inactive) {
|
||||||
return "A key service is not running."
|
return "phpman.services.inactive".localized
|
||||||
}
|
}
|
||||||
|
|
||||||
return "All Valet services are OK."
|
return "phpman.services.all_ok".localized
|
||||||
}
|
}
|
||||||
|
|
||||||
public var statusColor: Color {
|
public var statusColor: Color {
|
||||||
|
@ -34,6 +34,10 @@ class ValetServicesManager: ServicesManager {
|
|||||||
these two commands are executed concurrently.
|
these two commands are executed concurrently.
|
||||||
*/
|
*/
|
||||||
override func reloadServicesStatus() async {
|
override func reloadServicesStatus() async {
|
||||||
|
if !Valet.installed {
|
||||||
|
return Log.info("Not reloading services because running in Standalone Mode.")
|
||||||
|
}
|
||||||
|
|
||||||
await withTaskGroup(of: [HomebrewService].self, body: { group in
|
await withTaskGroup(of: [HomebrewService].self, body: { group in
|
||||||
// First, retrieve the status of the formulae that run as root
|
// First, retrieve the status of the formulae that run as root
|
||||||
group.addTask {
|
group.addTask {
|
||||||
|
@ -142,7 +142,7 @@ class Startup {
|
|||||||
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
||||||
.contains("Library not loaded")
|
.contains("Library not loaded")
|
||||||
},
|
},
|
||||||
name: "`no dyld issue detected",
|
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||||
titleText: "startup.errors.dyld_library.title".localized,
|
titleText: "startup.errors.dyld_library.title".localized,
|
||||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||||
Paths.optPath
|
Paths.optPath
|
||||||
@ -241,6 +241,20 @@ class Startup {
|
|||||||
descriptionText: "startup.errors.which_alias_issue.desc".localized
|
descriptionText: "startup.errors.which_alias_issue.desc".localized
|
||||||
),
|
),
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
|
// Determine that Laravel Herd is not running (may cause conflicts)
|
||||||
|
// =================================================================================
|
||||||
|
EnvironmentCheck(
|
||||||
|
command: {
|
||||||
|
return NSWorkspace.shared.runningApplications.contains(where: { app in
|
||||||
|
return app.bundleIdentifier == "de.beyondco.herd"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
name: "Herd is not running",
|
||||||
|
titleText: "startup.errors.herd_running.title".localized,
|
||||||
|
subtitleText: "startup.errors.herd_running.subtitle".localized,
|
||||||
|
descriptionText: "startup.errors.herd_running.desc".localized
|
||||||
|
),
|
||||||
|
// =================================================================================
|
||||||
// Determine that Valet works correctly (no issues in platform detected)
|
// Determine that Valet works correctly (no issues in platform detected)
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
EnvironmentCheck(
|
EnvironmentCheck(
|
||||||
|
@ -62,12 +62,13 @@ struct ComposerJson: Decodable {
|
|||||||
public func getNotableDependencies() -> [String: String] {
|
public func getNotableDependencies() -> [String: String] {
|
||||||
var notable: [String: String] = [:]
|
var notable: [String: String] = [:]
|
||||||
|
|
||||||
var scan = Array(PhpFrameworks.DependencyList.keys)
|
let scan = Array(ProjectTypeDetection.CommonDependencyList.keys) +
|
||||||
scan.append("php")
|
Array(ProjectTypeDetection.SpecificDependencyList.keys) +
|
||||||
|
["php"]
|
||||||
|
|
||||||
scan.forEach { dependency in
|
scan.forEach { dependency in
|
||||||
if dependencies?[dependency] != nil {
|
if let resolvedDependency = dependencies?[dependency] {
|
||||||
notable[dependency] = dependencies![dependency]
|
notable[dependency] = resolvedDependency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +28,6 @@ import Foundation
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
MainMenu.shared.setBusyImage()
|
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
|
|
||||||
window = TerminalProgressWindowController.display(
|
window = TerminalProgressWindowController.display(
|
||||||
title: "alert.composer_progress.title".localized,
|
title: "alert.composer_progress.title".localized,
|
||||||
@ -106,14 +104,11 @@ import Foundation
|
|||||||
|
|
||||||
private func removeBusyStatus() {
|
private func removeBusyStatus() {
|
||||||
PhpEnvironments.shared.isBusy = false
|
PhpEnvironments.shared.isBusy = false
|
||||||
Task { @MainActor in
|
|
||||||
MainMenu.shared.updatePhpVersionInStatusBar()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Alert
|
// MARK: Alert
|
||||||
|
|
||||||
@MainActor private func presentMissingAlert() {
|
private func presentMissingAlert() {
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.composer_missing.title".localized,
|
title: "alert.composer_missing.title".localized,
|
||||||
|
@ -8,20 +8,20 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct PhpFrameworks {
|
struct ProjectTypeDetection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This list should probably be reversed when checked, because some of these
|
This list is only checked if the specific dependency list doesn't report a match.
|
||||||
will also require either `laravel/framework` or `symfony/symfony`.
|
|
||||||
*/
|
*/
|
||||||
public static let DependencyList = [
|
public static let CommonDependencyList = [
|
||||||
|
|
||||||
// COMMON FRAMEWORKS
|
|
||||||
"laravel/framework": "Laravel",
|
"laravel/framework": "Laravel",
|
||||||
"symfony/symfony": "Symfony",
|
"symfony/symfony": "Symfony",
|
||||||
"laravel/lumen": "Lumen",
|
"laravel/lumen": "Lumen"
|
||||||
|
]
|
||||||
|
|
||||||
// VARIOUS CMS
|
/**
|
||||||
|
This list is checked first to see if a project dependency can be mapped to a certain project type.
|
||||||
|
*/
|
||||||
|
public static let SpecificDependencyList = [
|
||||||
"roots/bedrock": "Bedrock",
|
"roots/bedrock": "Bedrock",
|
||||||
"cakephp/app": "CakePHP",
|
"cakephp/app": "CakePHP",
|
||||||
"craftcms/craft": "Craft",
|
"craftcms/craft": "Craft",
|
||||||
@ -37,30 +37,8 @@ struct PhpFrameworks {
|
|||||||
"johnpbloch/wordpress-core": "WordPress",
|
"johnpbloch/wordpress-core": "WordPress",
|
||||||
"zendframework/zendframework": "Zend",
|
"zendframework/zendframework": "Zend",
|
||||||
"zendframework/zend-mvc": "Zend",
|
"zendframework/zend-mvc": "Zend",
|
||||||
"typo3/cms-core": "Typo3"
|
"typo3/cms-core": "Typo3",
|
||||||
// "magento/*": "Magento",
|
"slim/slim": "Slim"
|
||||||
// "concrete5/*": "Concrete5",
|
|
||||||
// "contao/*": "Contao",
|
|
||||||
// "slim/*": "Slim",
|
|
||||||
]
|
|
||||||
|
|
||||||
public static let FileMapping: [String: [String]] = [
|
|
||||||
"Drupal": [
|
|
||||||
// Legacy installations
|
|
||||||
"/misc/drupal.js",
|
|
||||||
"/core/lib/Drupal.php",
|
|
||||||
// The default (new) installation w/ Composer puts the modules in /web
|
|
||||||
"/web/misc/drupal.js",
|
|
||||||
"/web/core/lib/Drupal.php"
|
|
||||||
],
|
|
||||||
"WordPress": [
|
|
||||||
"/wp-config.php",
|
|
||||||
"/wp-config-sample.php"
|
|
||||||
],
|
|
||||||
"Typo3": [
|
|
||||||
"/typo3",
|
|
||||||
"/public/typo3"
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,4 +60,25 @@ struct PhpFrameworks {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
File mapping is used as a fallback if neither specific nor framework matches could be done.
|
||||||
|
*/
|
||||||
|
public static let FileMapping: [String: [String]] = [
|
||||||
|
"Drupal": [
|
||||||
|
// Legacy installations
|
||||||
|
"/misc/drupal.js",
|
||||||
|
"/core/lib/Drupal.php",
|
||||||
|
// The default (new) installation w/ Composer puts the modules in /web
|
||||||
|
"/web/misc/drupal.js",
|
||||||
|
"/web/core/lib/Drupal.php"
|
||||||
|
],
|
||||||
|
"WordPress": [
|
||||||
|
"/wp-config.php",
|
||||||
|
"/wp-config-sample.php"
|
||||||
|
],
|
||||||
|
"Typo3": [
|
||||||
|
"/typo3",
|
||||||
|
"/public/typo3"
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
@ -8,16 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class BrewFormulaeObservable: ObservableObject {
|
|
||||||
@Published var phpVersions: [BrewFormula] = []
|
|
||||||
|
|
||||||
var upgradeable: [BrewFormula] {
|
|
||||||
return phpVersions.filter { formula in
|
|
||||||
formula.hasUpgrade
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Brew {
|
class Brew {
|
||||||
static let shared = Brew()
|
static let shared = Brew()
|
||||||
|
|
||||||
@ -45,9 +35,11 @@ class Brew {
|
|||||||
|
|
||||||
/// Each formula for each PHP version that can be installed.
|
/// Each formula for each PHP version that can be installed.
|
||||||
public static let phpVersionFormulae = [
|
public static let phpVersionFormulae = [
|
||||||
|
"8.4": "shivammathur/php/php@8.4",
|
||||||
|
"8.3": "php@8.3",
|
||||||
"8.2": "php@8.2",
|
"8.2": "php@8.2",
|
||||||
"8.1": "php@8.1",
|
"8.1": "php@8.1",
|
||||||
"8.0": "php@8.0",
|
"8.0": "shivammathur/php/php@8.0",
|
||||||
"7.4": "shivammathur/php/php@7.4",
|
"7.4": "shivammathur/php/php@7.4",
|
||||||
"7.3": "shivammathur/php/php@7.3",
|
"7.3": "shivammathur/php/php@7.3",
|
||||||
"7.2": "shivammathur/php/php@7.2",
|
"7.2": "shivammathur/php/php@7.2",
|
||||||
|
@ -27,6 +27,21 @@ class BrewDiagnostics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Logs a bunch of useful information during startup.
|
||||||
|
*/
|
||||||
|
public static func logBootInformation() {
|
||||||
|
Log.info(BrewDiagnostics.customCaskInstalled
|
||||||
|
? "[BREW] The app has been installed via Homebrew Cask."
|
||||||
|
: "[BREW] The app has been installed directly (optimal)."
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.info(BrewDiagnostics.usesNginxFullFormula
|
||||||
|
? "[BREW] The app will be using the `nginx-full` formula."
|
||||||
|
: "[BREW] The app will be using the `nginx` formula."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determines whether the PHP Monitor Cask is installed.
|
Determines whether the PHP Monitor Cask is installed.
|
||||||
*/
|
*/
|
||||||
@ -46,6 +61,43 @@ class BrewDiagnostics {
|
|||||||
return destination.contains("/nginx-full/")
|
return destination.contains("/nginx-full/")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
/**
|
||||||
|
It is possible to have outdated symlinks for PHP installations. This can mean that certain PHP installations
|
||||||
|
are going to be reported incorrectly (e.g. `php@8.2` links to an installation in a `8.3` folder after an upgrade).
|
||||||
|
|
||||||
|
To ensure this does not cause issues, PHP Monitor will automatically remove all incorrect PHP symlinks.
|
||||||
|
*/
|
||||||
|
public static func checkForOutdatedPhpInstallationSymlinks() async {
|
||||||
|
// Set up a regular expression
|
||||||
|
let regex = try! NSRegularExpression(pattern: "^php@[0-9]+\\.[0-9]+$", options: .caseInsensitive)
|
||||||
|
|
||||||
|
// Check for incorrect versions
|
||||||
|
if let contents = try? FileSystem.getShallowContentsOfDirectory("\(Paths.optPath)")
|
||||||
|
.filter({
|
||||||
|
let range = NSRange($0.startIndex..., in: $0)
|
||||||
|
return regex.firstMatch(in: $0, options: [], range: range) != nil
|
||||||
|
}) {
|
||||||
|
|
||||||
|
for symlink in contents {
|
||||||
|
let version = symlink.replacingOccurrences(of: "php@", with: "")
|
||||||
|
if let destination = try? FileSystem.getDestinationOfSymlink("\(Paths.optPath)/\(symlink)") {
|
||||||
|
if !destination.contains("Cellar/php/\(version)")
|
||||||
|
&& !destination.contains("Cellar/php@\(version)") {
|
||||||
|
Log.err("Symlink for \(symlink) is incorrect. Removing...")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove("\(Paths.optPath)/\(symlink)")
|
||||||
|
Log.info("Incorrect symlink for \(symlink) has been successfully removed.")
|
||||||
|
} catch {
|
||||||
|
Log.err("Symlink for \(symlink) was incorrect but could not be removed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not read symlink at: \(Paths.optPath)/\(symlink)! Symlink check skipped.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||||
This will then result in two different aliases claiming to point to the same formula (`php`).
|
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||||
|
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// BrewPhpExtension.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 27/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct BrewPhpExtension: Hashable, Comparable {
|
||||||
|
let name: String
|
||||||
|
let phpVersion: String
|
||||||
|
let isInstalled: Bool
|
||||||
|
let path: String
|
||||||
|
let dependencies: [String]
|
||||||
|
|
||||||
|
var extensionDependencies: [String] {
|
||||||
|
return dependencies
|
||||||
|
.filter {
|
||||||
|
$0.contains("shivammathur/extensions/") && $0.contains("@\(phpVersion)")
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
$0.replacingOccurrences(of: "shivammathur/extensions/", with: "")
|
||||||
|
.replacingOccurrences(of: "@\(phpVersion)", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var formulaName: String {
|
||||||
|
return "\(name)@\(phpVersion)"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(path: String, name: String, phpVersion: String) {
|
||||||
|
self.path = path
|
||||||
|
self.name = name
|
||||||
|
self.phpVersion = phpVersion
|
||||||
|
|
||||||
|
self.isInstalled = BrewPhpExtension.hasInstallationReceipt(
|
||||||
|
for: "\(name)@\(phpVersion)"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.dependencies = BrewPhpExtension.extractDependencies(from: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAlternativeInstall: Bool {
|
||||||
|
guard let php = PhpEnvironments.shared.cachedPhpInstallations[self.phpVersion] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let alreadyDiscovered = php.extensions.contains(where: { $0.name == self.name })
|
||||||
|
|
||||||
|
return alreadyDiscovered && !isInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func firstDependent(in exts: [BrewPhpExtension]) -> BrewPhpExtension? {
|
||||||
|
return exts
|
||||||
|
.filter({ $0.isInstalled })
|
||||||
|
.first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasInstallationReceipt(for formulaName: String) -> Bool {
|
||||||
|
return FileSystem.fileExists("\(Paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name < rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name == rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractDependencies(from path: String) -> [String] {
|
||||||
|
let regexPattern = #"depends_on "(.*)""#
|
||||||
|
var dependencies: [String] = []
|
||||||
|
|
||||||
|
guard let content = try? FileSystem.getStringFromFile(path) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let regex = try NSRegularExpression(pattern: regexPattern, options: [])
|
||||||
|
let range = NSRange(content.startIndex..<content.endIndex, in: content)
|
||||||
|
let matches = regex.matches(in: content, options: [], range: range)
|
||||||
|
|
||||||
|
for match in matches {
|
||||||
|
if let range = Range(match.range(at: 1), in: content) {
|
||||||
|
let dependencyName = String(content[range])
|
||||||
|
dependencies.append(dependencyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// BrewFormula.swift
|
// BrewPhpFormula.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 17/03/2023.
|
// Created by Nico Verbruggen on 17/03/2023.
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct BrewFormula {
|
struct BrewPhpFormula: Equatable {
|
||||||
/// Name of the formula.
|
/// Name of the formula.
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
@ -21,16 +21,54 @@ struct BrewFormula {
|
|||||||
/// The upgrade that is currently available, if it exists.
|
/// The upgrade that is currently available, if it exists.
|
||||||
let upgradeVersion: String?
|
let upgradeVersion: String?
|
||||||
|
|
||||||
|
// TODO: A rebuild attribute could be checked, to check if a Tap update exists for a pre-release version
|
||||||
|
|
||||||
|
/// Whether this formula is a stable version of PHP.
|
||||||
|
let prerelease: Bool
|
||||||
|
|
||||||
/// Whether the formula is currently installed.
|
/// Whether the formula is currently installed.
|
||||||
var isInstalled: Bool {
|
var isInstalled: Bool {
|
||||||
return installedVersion != nil
|
return installedVersion != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
name: String,
|
||||||
|
displayName: String,
|
||||||
|
installedVersion: String?,
|
||||||
|
upgradeVersion: String?,
|
||||||
|
prerelease: Bool = false
|
||||||
|
) {
|
||||||
|
self.name = name
|
||||||
|
self.displayName = displayName
|
||||||
|
self.installedVersion = installedVersion
|
||||||
|
self.upgradeVersion = upgradeVersion
|
||||||
|
self.prerelease = prerelease
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the formula can be upgraded.
|
/// Whether the formula can be upgraded.
|
||||||
var hasUpgrade: Bool {
|
var hasUpgrade: Bool {
|
||||||
return upgradeVersion != nil
|
return upgradeVersion != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this formula alias is different.
|
||||||
|
var hasUpgradedFormulaAlias: Bool {
|
||||||
|
return self.shortVersion == PhpEnvironments.homebrewBrewPhpAlias
|
||||||
|
&& PhpEnvironments.homebrewBrewPhpAlias != PhpEnvironments.brewPhpAlias
|
||||||
|
}
|
||||||
|
|
||||||
|
var unavailableAfterUpgrade: Bool {
|
||||||
|
if installedVersion == nil || upgradeVersion == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let installed = try? VersionNumber.parse(self.installedVersion!),
|
||||||
|
let upgrade = try? VersionNumber.parse(self.upgradeVersion!) {
|
||||||
|
return upgrade.short != installed.short
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/// The associated Homebrew folder with this PHP formula.
|
/// The associated Homebrew folder with this PHP formula.
|
||||||
var homebrewFolder: String {
|
var homebrewFolder: String {
|
||||||
let resolved = name
|
let resolved = name
|
||||||
@ -43,7 +81,7 @@ struct BrewFormula {
|
|||||||
/// The short version associated with this formula, if installed.
|
/// The short version associated with this formula, if installed.
|
||||||
var shortVersion: String? {
|
var shortVersion: String? {
|
||||||
guard let version = self.installedVersion else {
|
guard let version = self.installedVersion else {
|
||||||
return nil
|
return self.displayName.replacingOccurrences(of: "PHP ", with: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return VersionNumber.make(from: version)?.short ?? nil
|
return VersionNumber.make(from: version)?.short ?? nil
|
||||||
@ -64,6 +102,7 @@ struct BrewFormula {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?.isHealthy ?? nil
|
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?
|
||||||
|
.isHealthy ?? nil
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,22 +8,23 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol HandlesBrewFormulae {
|
protocol HandlesBrewPhpFormulae {
|
||||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula]
|
func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula]
|
||||||
func refreshPhpVersions(loadOutdated: Bool) async
|
func refreshPhpVersions(loadOutdated: Bool) async
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HandlesBrewFormulae {
|
extension HandlesBrewPhpFormulae {
|
||||||
public func refreshPhpVersions(loadOutdated: Bool) async {
|
public func refreshPhpVersions(loadOutdated: Bool) async {
|
||||||
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
Brew.shared.formulae.phpVersions = items
|
Brew.shared.formulae.phpVersions = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BrewFormulaeHandler: HandlesBrewFormulae {
|
class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] {
|
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
|
||||||
var outdated: [OutdatedFormula]?
|
var outdated: [OutdatedFormula]?
|
||||||
|
|
||||||
if loadOutdated {
|
if loadOutdated {
|
||||||
@ -43,20 +44,22 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Brew.phpVersionFormulae.map { (version, formula) in
|
return Brew.phpVersionFormulae.map { (version, formula) in
|
||||||
let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?.versionNumber.text
|
var fullVersion: String?
|
||||||
var upgradeVersion: String?
|
var upgradeVersion: String?
|
||||||
|
|
||||||
if let version = fullVersion {
|
if let install = PhpEnvironments.shared.cachedPhpInstallations[version] {
|
||||||
|
fullVersion = install.versionNumber.text
|
||||||
upgradeVersion = outdated?.first(where: { formula in
|
upgradeVersion = outdated?.first(where: { formula in
|
||||||
return formula.installed_versions.contains(version)
|
return formula.name == install.formulaName
|
||||||
})?.current_version
|
})?.current_version
|
||||||
}
|
}
|
||||||
|
|
||||||
return BrewFormula(
|
return BrewPhpFormula(
|
||||||
name: formula,
|
name: formula,
|
||||||
displayName: "PHP \(version)",
|
displayName: "PHP \(version)",
|
||||||
installedVersion: fullVersion,
|
installedVersion: fullVersion,
|
||||||
upgradeVersion: upgradeVersion
|
upgradeVersion: upgradeVersion,
|
||||||
|
prerelease: Constants.ExperimentalPhpVersions.contains(version)
|
||||||
)
|
)
|
||||||
}.sorted { $0.displayName > $1.displayName }
|
}.sorted { $0.displayName > $1.displayName }
|
||||||
}
|
}
|
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// BrewTapFormulae.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 01/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class BrewTapFormulae {
|
||||||
|
public static func from(tap: String) -> [String: [BrewPhpExtension]] {
|
||||||
|
let directory = "\(Paths.tapPath)/\(tap)/Formula"
|
||||||
|
|
||||||
|
let files = try? FileSystem.getShallowContentsOfDirectory(directory)
|
||||||
|
|
||||||
|
var availableExtensions = [String: [BrewPhpExtension]]()
|
||||||
|
|
||||||
|
guard let files = files else {
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
|
||||||
|
let regex = try! NSRegularExpression(pattern: "(\\w+)@(\\d+\\.\\d+)\\.rb")
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
let matches = regex.matches(in: file, range: NSRange(file.startIndex..., in: file))
|
||||||
|
if let match = matches.first {
|
||||||
|
if let phpExtensionRange = Range(match.range(at: 1), in: file),
|
||||||
|
let versionRange = Range(match.range(at: 2), in: file) {
|
||||||
|
// Determine what the extension's name is
|
||||||
|
let phpExtensionName = String(file[phpExtensionRange])
|
||||||
|
|
||||||
|
// Determine what PHP version this is for
|
||||||
|
let phpVersion = String(file[versionRange])
|
||||||
|
|
||||||
|
// Create a new BrewPhpExtension object (determines if installed)
|
||||||
|
let phpExtension = BrewPhpExtension(
|
||||||
|
path: "\(Paths.tapPath)/\(tap)/Formula/\(file)",
|
||||||
|
name: phpExtensionName,
|
||||||
|
phpVersion: phpVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
// Append the extension to the list
|
||||||
|
var extensions = availableExtensions[phpVersion, default: []]
|
||||||
|
extensions.append(phpExtension)
|
||||||
|
availableExtensions[phpVersion] = extensions.sorted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,8 @@ import Foundation
|
|||||||
|
|
||||||
protocol BrewCommand {
|
protocol BrewCommand {
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
||||||
|
|
||||||
|
func getCommandTitle() -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BrewCommand {
|
extension BrewCommand {
|
||||||
@ -31,6 +33,44 @@ extension BrewCommand {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
var loggedMessages: [String] = []
|
||||||
|
|
||||||
|
let (process, _) = try! await Shell.attach(
|
||||||
|
command,
|
||||||
|
didReceiveOutput: { text, _ in
|
||||||
|
if !text.isEmpty {
|
||||||
|
Log.perf(text)
|
||||||
|
loggedMessages.append(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (number, text) = self.reportInstallationProgress(text) {
|
||||||
|
onProgress(.create(value: number, title: getCommandTitle(), description: text))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withTimeout: .minutes(15)
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.terminationStatus <= 0 {
|
||||||
|
loggedMessages = []
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/php") {
|
||||||
|
let command = "brew tap shivammathur/php"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") {
|
||||||
|
let command = "brew tap shivammathur/extensions"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BrewCommandProgress {
|
struct BrewCommandProgress {
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// InstallPhpExtensionCommand.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 21/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class InstallPhpExtensionCommand: BrewCommand {
|
||||||
|
let installing: [BrewPhpExtension]
|
||||||
|
|
||||||
|
func getExtensionNames() -> String {
|
||||||
|
return installing.map { $0.name }.joined(separator: ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "phpman.steps.installing".localized(getExtensionNames())
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(install extensions: [BrewPhpExtension]) {
|
||||||
|
self.installing = extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
let progressTitle = "phpman.steps.wait".localized
|
||||||
|
|
||||||
|
onProgress(.create(
|
||||||
|
value: 0.2,
|
||||||
|
title: progressTitle,
|
||||||
|
description: "phpman.steps.preparing".localized
|
||||||
|
))
|
||||||
|
|
||||||
|
// Make sure the tap is installed
|
||||||
|
try await self.checkPhpTap(onProgress)
|
||||||
|
|
||||||
|
// Make sure that the extension(s) are installed
|
||||||
|
try await self.installPackages(onProgress)
|
||||||
|
|
||||||
|
// Finally, complete all operations
|
||||||
|
await self.completedOperations(onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installPackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
// If no installations are needed, early exit
|
||||||
|
if self.installing.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) install \(self.installing.map { $0.formulaName }.joined(separator: " ")) --force
|
||||||
|
"""
|
||||||
|
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||||
|
// Reload and restart PHP versions
|
||||||
|
onProgress(.create(value: 0.95, title: self.getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
// Check which version of PHP are now installed
|
||||||
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
|
// Keep track of the currently installed version
|
||||||
|
await MainMenu.shared.refreshActiveInstallation()
|
||||||
|
|
||||||
|
// Also rebuild the content of the main menu
|
||||||
|
await MainMenu.shared.rebuild()
|
||||||
|
|
||||||
|
// Let the UI know that the installation has been completed
|
||||||
|
onProgress(.create(
|
||||||
|
value: 1,
|
||||||
|
title: "phpman.steps.completed".localized,
|
||||||
|
description: "phpman.steps.success".localized
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// RemovePhpExtensionCommand.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 21/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class RemovePhpExtensionCommand: BrewCommand {
|
||||||
|
public let phpExtension: BrewPhpExtension
|
||||||
|
|
||||||
|
public init(remove formula: BrewPhpExtension) {
|
||||||
|
self.phpExtension = formula
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "phpman.steps.removing".localized(phpExtension.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
onProgress(.create(
|
||||||
|
value: 0.2,
|
||||||
|
title: getCommandTitle(),
|
||||||
|
description: "phpman.steps.removing".localized("`\(phpExtension.name)`...")
|
||||||
|
))
|
||||||
|
|
||||||
|
// Keep track of the file that contains the information about the extension
|
||||||
|
let existing = PhpEnvironments.shared
|
||||||
|
.cachedPhpInstallations[phpExtension.phpVersion]?
|
||||||
|
.extensions.first(where: { ext in
|
||||||
|
ext.name == phpExtension.name
|
||||||
|
})
|
||||||
|
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) remove \(phpExtension.formulaName) --force --ignore-dependencies
|
||||||
|
"""
|
||||||
|
|
||||||
|
var loggedMessages: [String] = []
|
||||||
|
|
||||||
|
let (process, _) = try! await Shell.attach(
|
||||||
|
command,
|
||||||
|
didReceiveOutput: { text, _ in
|
||||||
|
if !text.isEmpty {
|
||||||
|
Log.perf(text)
|
||||||
|
loggedMessages.append(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withTimeout: .minutes(5)
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.terminationStatus <= 0 {
|
||||||
|
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
if let ext = existing {
|
||||||
|
await performExtensionCleanup(for: ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
|
await MainMenu.shared.refreshActiveInstallation()
|
||||||
|
|
||||||
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "phpman.steps.failure".localized, log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performExtensionCleanup(for ext: PhpExtension) async {
|
||||||
|
if ext.file.hasSuffix("20-\(ext.name).ini") {
|
||||||
|
// The extension's default configuration file can be removed
|
||||||
|
Log.info("The extension was found in a default extension .ini location. Purging that .ini file.")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove(ext.file)
|
||||||
|
} catch {
|
||||||
|
Log.err("The file `\(ext.file)` could not be removed.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The extension's default configuration file cannot be removed, it should be disabled instead
|
||||||
|
Log.info("The extension was not found in a default location. Disabling the extension only.")
|
||||||
|
if ext.enabled {
|
||||||
|
await ext.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,23 +8,33 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class InstallAndUpgradeCommand: BrewCommand {
|
class ModifyPhpVersionCommand: BrewCommand {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let installing: [BrewFormula]
|
let installing: [BrewPhpFormula]
|
||||||
let upgrading: [BrewFormula]
|
let upgrading: [BrewPhpFormula]
|
||||||
let phpGuard: PhpGuard
|
let phpGuard: PhpGuard
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
||||||
The process will be executed in two steps: first upgrades, then installations.
|
The process will be executed in two steps: first upgrades, then installations.
|
||||||
|
|
||||||
Upgrades come first because... well, otherwise installations may very well break.
|
Upgrades come first because... well, otherwise installations may very well break.
|
||||||
Each version that is installed will need to be checked afterwards (if it is OK).
|
Each version that is installed will need to be checked afterwards. Installing a
|
||||||
|
newer formula may break other PHP installations, which in turn need to be fixed.
|
||||||
|
|
||||||
|
- Important: If any PHP formula is a major upgrade that causes a PHP "version" to be
|
||||||
|
uninstalled, this is remedied by running `upgradeMainPhpFormula()`. This process
|
||||||
|
will ensure that the upgrade is applied, but the also that old version is
|
||||||
|
re-installed and linked again.
|
||||||
*/
|
*/
|
||||||
public init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
upgrading: [BrewFormula],
|
upgrading: [BrewPhpFormula],
|
||||||
installing: [BrewFormula]
|
installing: [BrewPhpFormula]
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.installing = installing
|
self.installing = installing
|
||||||
@ -33,17 +43,32 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
let progressTitle = "Please wait..."
|
let progressTitle = "phpman.steps.wait".localized
|
||||||
|
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: progressTitle,
|
||||||
description: "PHP Monitor is preparing Homebrew..."
|
description: "phpman.steps.preparing".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
// Try to run all upgrade and installation operations
|
// Determine if a formula will become unavailable
|
||||||
try await self.upgradePackages(onProgress)
|
// This is the case when `php` will be bumped to a new version
|
||||||
try await self.installPackages(onProgress)
|
let unavailable = upgrading.first(where: { formula in
|
||||||
|
formula.unavailableAfterUpgrade
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make sure the tap is installed
|
||||||
|
try await self.checkPhpTap(onProgress)
|
||||||
|
|
||||||
|
if unavailable == nil {
|
||||||
|
// Try to run all upgrade and installation operations
|
||||||
|
try await self.upgradePackages(onProgress)
|
||||||
|
try await self.installPackages(onProgress)
|
||||||
|
} else {
|
||||||
|
// Simply upgrade `php` to the latest version
|
||||||
|
try await self.upgradeMainPhpFormula(unavailable!, onProgress)
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
}
|
||||||
|
|
||||||
// Re-check the installed versions
|
// Re-check the installed versions
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@ -55,6 +80,27 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
await self.completedOperations(onProgress)
|
await self.completedOperations(onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func upgradeMainPhpFormula(
|
||||||
|
_ unavailable: BrewPhpFormula,
|
||||||
|
_ onProgress: @escaping (BrewCommandProgress) -> Void
|
||||||
|
) async throws {
|
||||||
|
// Determine which version was previously available (that will become unavailable)
|
||||||
|
guard let short = try? VersionNumber
|
||||||
|
.parse(unavailable.installedVersion!).short else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade the main formula
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) upgrade php;
|
||||||
|
\(Paths.brew) install php@\(short);
|
||||||
|
"""
|
||||||
|
|
||||||
|
// Run the upgrade command
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
// If no upgrades are needed, early exit
|
// If no upgrades are needed, early exit
|
||||||
if self.upgrading.isEmpty {
|
if self.upgrading.isEmpty {
|
||||||
@ -117,35 +163,12 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
try await run(command, onProgress)
|
try await run(command, onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
|
||||||
var loggedMessages: [String] = []
|
|
||||||
|
|
||||||
let (process, _) = try! await Shell.attach(
|
|
||||||
command,
|
|
||||||
didReceiveOutput: { text, _ in
|
|
||||||
if !text.isEmpty {
|
|
||||||
Log.perf(text)
|
|
||||||
loggedMessages.append(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (number, text) = self.reportInstallationProgress(text) {
|
|
||||||
onProgress(.create(value: number, title: self.title, description: text))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
withTimeout: .minutes(15)
|
|
||||||
)
|
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
|
||||||
loggedMessages = []
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||||
// Reload and restart PHP versions
|
// Reload and restart PHP versions
|
||||||
onProgress(.create(value: 0.95, title: self.title, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: self.title, description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
// Ensure all symlinks are correctly linked
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Check which version of PHP are now installed
|
// Check which version of PHP are now installed
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@ -164,9 +187,8 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
// Let the UI know that the installation has been completed
|
// Let the UI know that the installation has been completed
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 1,
|
value: 1,
|
||||||
title: "Operation completed!",
|
title: "phpman.steps.completed".localized,
|
||||||
description: "The installation has succeeded."
|
description: "phpman.steps.success".localized
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -21,13 +21,15 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
self.phpGuard = PhpGuard()
|
self.phpGuard = PhpGuard()
|
||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func getCommandTitle() -> String {
|
||||||
let progressTitle = "Removing PHP \(version)..."
|
return "phpman.steps.removing".localized("PHP \(version)...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: getCommandTitle(),
|
||||||
description: "Please wait while Homebrew removes PHP \(version)..."
|
description: "phpman.steps.wait".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
let command = """
|
let command = """
|
||||||
@ -56,7 +58,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
if process.terminationStatus <= 0 {
|
||||||
onProgress(.create(value: 0.95, title: progressTitle, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
@ -66,7 +68,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(.create(value: 1, title: progressTitle, description: "The operation has succeeded."))
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
} else {
|
} else {
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
}
|
}
|
@ -9,6 +9,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class FakeCommand: BrewCommand {
|
class FakeCommand: BrewCommand {
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "Hello"
|
||||||
|
}
|
||||||
|
|
||||||
let version: String
|
let version: String
|
||||||
|
|
||||||
init(version: String) {
|
init(version: String) {
|
||||||
|
@ -141,7 +141,7 @@ class ValetSite: ValetListable {
|
|||||||
self.determineDriverViaComposer()
|
self.determineDriverViaComposer()
|
||||||
|
|
||||||
if self.driver == nil {
|
if self.driver == nil {
|
||||||
self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath)
|
self.driver = ProjectTypeDetection.detectFallbackDependency(self.absolutePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,10 +155,16 @@ class ValetSite: ValetListable {
|
|||||||
private func determineDriverViaComposer() {
|
private func determineDriverViaComposer() {
|
||||||
self.driverDeterminedByComposer = true
|
self.driverDeterminedByComposer = true
|
||||||
|
|
||||||
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
|
for (key, value) in ProjectTypeDetection.SpecificDependencyList
|
||||||
if self.notableComposerDependencies.keys.contains(key) {
|
where notableComposerDependencies.keys.contains(key) {
|
||||||
self.driver = value
|
self.driver = value
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in ProjectTypeDetection.CommonDependencyList
|
||||||
|
where notableComposerDependencies.keys.contains(key) {
|
||||||
|
self.driver = value
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,8 +233,14 @@ class Valet {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if let defaultPath = Valet.shared.config.defaultSite,
|
if let defaultPath = Valet.shared.config.defaultSite,
|
||||||
let site = ValetScanner.active.resolveSite(path: defaultPath) {
|
let defaultSite = ValetScanner.active.resolveSite(path: defaultPath) {
|
||||||
sites.insert(site, at: 0)
|
// Only insert the default site if it isn't already included in the list
|
||||||
|
if !sites.contains(where: { site in
|
||||||
|
site.absolutePath == defaultSite.absolutePath
|
||||||
|
&& site.name == defaultSite.name
|
||||||
|
}) {
|
||||||
|
sites.insert(defaultSite, at: 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info("\(sites.count) sites & \(proxies.count) proxies have been scanned.")
|
Log.info("\(sites.count) sites & \(proxies.count) proxies have been scanned.")
|
||||||
|
@ -283,12 +283,11 @@ extension MainMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@ -298,13 +297,12 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func switchToPhpVersion(_ version: String) {
|
@objc func switchToPhpVersion(_ version: String) {
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@ -325,13 +323,12 @@ extension MainMenu {
|
|||||||
*/
|
*/
|
||||||
func switchToPhp(_ version: String) async {
|
func switchToPhp(_ version: String) async {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
|
@ -45,21 +45,16 @@ extension MainMenu {
|
|||||||
.broadcastServicesUpdate
|
.broadcastServicesUpdate
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
if behaviours.contains(.reloadsPhpInstallation) {
|
if behaviours.contains(.reloadsPhpInstallation) || behaviours.contains(.setsBusyUI) {
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.setsBusyUI) {
|
|
||||||
setBusyImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
var error: Error?
|
var error: Error?
|
||||||
|
|
||||||
do { try execute() } catch let e { error = e }
|
do { try execute() } catch let e {
|
||||||
|
error = e
|
||||||
if behaviours.contains(.setsBusyUI) {
|
Log.err(e)
|
||||||
PhpEnvironments.shared.isBusy = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor [self, error] in
|
Task { @MainActor [self, error] in
|
||||||
@ -68,15 +63,18 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.updatesMenuBarContents) {
|
if behaviours.contains(.updatesMenuBarContents) {
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
} else if behaviours.contains(.setsBusyUI) {
|
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.broadcastServicesUpdate) {
|
if behaviours.contains(.broadcastServicesUpdate) {
|
||||||
Task { await ServicesManager.shared.reloadServicesStatus() }
|
Task { await ServicesManager.shared.reloadServicesStatus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if behaviours.contains(.setsBusyUI) {
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
if error != nil {
|
if error != nil {
|
||||||
return failure(error!)
|
return failure(error!)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ extension MainMenu {
|
|||||||
func startup() async {
|
func startup() async {
|
||||||
// Start with the icon
|
// Start with the icon
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
self.setStatusBar(image: NSImage.statusBarIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
if await Startup().checkEnvironment() {
|
if await Startup().checkEnvironment() {
|
||||||
@ -32,19 +32,14 @@ extension MainMenu {
|
|||||||
// Determine what the `php` formula is aliased to
|
// Determine what the `php` formula is aliased to
|
||||||
await PhpEnvironments.shared.determinePhpAlias()
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
|
||||||
|
// Make sure that broken symlinks are removed ASAP
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Initialize preferences
|
// Initialize preferences
|
||||||
_ = Preferences.shared
|
_ = Preferences.shared
|
||||||
|
|
||||||
// Determine install method
|
// Put some useful diagnostics information in log
|
||||||
Log.info(BrewDiagnostics.customCaskInstalled
|
BrewDiagnostics.logBootInformation()
|
||||||
? "[BREW] The app has been installed via Homebrew Cask."
|
|
||||||
: "[BREW] The app has been installed directly (optimal)."
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.info(BrewDiagnostics.usesNginxFullFormula
|
|
||||||
? "[BREW] The app will be using the `nginx-full` formula."
|
|
||||||
: "[BREW] The app will be using the `nginx` formula."
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attempt to find out more info about Valet
|
// Attempt to find out more info about Valet
|
||||||
if Valet.shared.version != nil {
|
if Valet.shared.version != nil {
|
||||||
@ -63,9 +58,6 @@ extension MainMenu {
|
|||||||
// Check for an alias conflict
|
// Check for an alias conflict
|
||||||
await BrewDiagnostics.checkForCaskConflict()
|
await BrewDiagnostics.checkForCaskConflict()
|
||||||
|
|
||||||
// Update the icon
|
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
|
|
||||||
// Attempt to find out if PHP-FPM is broken
|
// Attempt to find out if PHP-FPM is broken
|
||||||
PhpEnvironments.prepare()
|
PhpEnvironments.prepare()
|
||||||
|
|
||||||
@ -76,7 +68,6 @@ extension MainMenu {
|
|||||||
WarningManager.shared.evaluateWarnings()
|
WarningManager.shared.evaluateWarnings()
|
||||||
|
|
||||||
// Set up the config watchers on launch (updated automatically when switching)
|
// Set up the config watchers on launch (updated automatically when switching)
|
||||||
Log.info("Setting up watchers...")
|
|
||||||
App.shared.handlePhpConfigWatcher()
|
App.shared.handlePhpConfigWatcher()
|
||||||
|
|
||||||
// Detect built-in and custom applications
|
// Detect built-in and custom applications
|
||||||
@ -88,6 +79,9 @@ extension MainMenu {
|
|||||||
// Load the global hotkey
|
// Load the global hotkey
|
||||||
App.shared.loadGlobalHotkey()
|
App.shared.loadGlobalHotkey()
|
||||||
|
|
||||||
|
// Set up menu items
|
||||||
|
AppDelegate.instance.configureMenuItems(standalone: !Valet.installed)
|
||||||
|
|
||||||
if Valet.installed {
|
if Valet.installed {
|
||||||
// Preload all sites
|
// Preload all sites
|
||||||
await Valet.shared.startPreloadingSites()
|
await Valet.shared.startPreloadingSites()
|
||||||
@ -102,9 +96,33 @@ extension MainMenu {
|
|||||||
Valet.shared.notifyAboutUnsupportedTLD()
|
Valet.shared.notifyAboutUnsupportedTLD()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep track of which PHP versions are currently about to release
|
||||||
|
Log.info("Experimental PHP versions are: \(Constants.ExperimentalPhpVersions)")
|
||||||
|
|
||||||
// Find out which services are active
|
// Find out which services are active
|
||||||
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
||||||
|
|
||||||
|
// Post-launch stats and update check, but only if not running tests
|
||||||
|
await performPostLaunchActions()
|
||||||
|
|
||||||
|
// Check if the linked version has changed between launches of phpmon
|
||||||
|
PhpGuard().compareToLastGlobalVersion()
|
||||||
|
|
||||||
|
// We are ready!
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
|
||||||
|
// Finally!
|
||||||
|
Log.info("PHP Monitor is ready to serve!")
|
||||||
|
|
||||||
|
// Check if we upgraded from a previous version
|
||||||
|
AppUpdater.checkIfUpdateWasPerformed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Performs a set of post-launch actions, like incrementing stats and checking for updates.
|
||||||
|
(This code is skipped when running SwiftUI previews.)
|
||||||
|
*/
|
||||||
|
private func performPostLaunchActions() async {
|
||||||
if !isRunningSwiftUIPreview {
|
if !isRunningSwiftUIPreview {
|
||||||
Stats.incrementSuccessfulLaunchCount()
|
Stats.incrementSuccessfulLaunchCount()
|
||||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||||
@ -118,15 +136,6 @@ extension MainMenu {
|
|||||||
await AppUpdater().checkForUpdates(userInitiated: false)
|
await AppUpdater().checkForUpdates(userInitiated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the linked version has changed between launches of phpmon
|
|
||||||
PhpGuard().compareToLastGlobalVersion()
|
|
||||||
|
|
||||||
// We are ready!
|
|
||||||
Log.info("PHP Monitor is ready to serve!")
|
|
||||||
|
|
||||||
// Check if we upgraded just now
|
|
||||||
AppUpdater.checkIfUpdateWasPerformed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +16,9 @@ extension MainMenu {
|
|||||||
|
|
||||||
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
||||||
// Mark as no longer busy
|
// Mark as no longer busy
|
||||||
PhpEnvironments.shared.isBusy = false
|
Task { @MainActor in
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
Task { // Things to do after reloading domain list data
|
Task { // Things to do after reloading domain list data
|
||||||
if Valet.installed {
|
if Valet.installed {
|
||||||
@ -25,7 +27,7 @@ extension MainMenu {
|
|||||||
|
|
||||||
// Perform UI updates on main thread
|
// Perform UI updates on main thread
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
|
|
||||||
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
||||||
|
@ -37,8 +37,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
// MARK: - UI related
|
// MARK: - UI related
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Rebuilds the menu (either asynchronously or synchronously).
|
Rebuilds the menu on the main thread.
|
||||||
Defaults to rebuilding the menu asynchronously.
|
|
||||||
*/
|
*/
|
||||||
func rebuild() {
|
func rebuild() {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
@ -80,13 +79,15 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func refreshActiveInstallation() {
|
@objc func refreshActiveInstallation() {
|
||||||
if !PhpEnvironments.shared.isBusy {
|
if !PhpEnvironments.shared.isBusy {
|
||||||
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
} else {
|
} else {
|
||||||
Log.perf("Skipping version refresh due to busy status!")
|
Log.perf("Skipping version refresh due to busy status!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||||
|
@available(*, deprecated, message: "Use the busy status instead")
|
||||||
@objc func updatePhpVersionInStatusBar() {
|
@objc func updatePhpVersionInStatusBar() {
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
@ -139,7 +140,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func reloadPhpMonitorMenuInBackground() {
|
@objc func reloadPhpMonitorMenuInBackground() {
|
||||||
asyncExecution({
|
asyncExecution({
|
||||||
// This automatically reloads the menu
|
// This automatically reloads the menu
|
||||||
Log.info("Reloading information about the PHP installation (in the background)...")
|
Log.perf("Reloading information about the PHP installation (in the background)...")
|
||||||
}, behaviours: [
|
}, behaviours: [
|
||||||
.setsBusyUI,
|
.setsBusyUI,
|
||||||
.reloadsPhpInstallation,
|
.reloadsPhpInstallation,
|
||||||
@ -150,13 +151,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
|
|
||||||
/** Refreshes the icon with the PHP version. */
|
/** Refreshes the icon with the PHP version. */
|
||||||
@objc func refreshIcon() {
|
@objc func refreshIcon() {
|
||||||
|
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
Log.perf("Refreshing icon: currently busy")
|
||||||
|
setStatusBar(image: NSImage.statusBarIcon)
|
||||||
} else {
|
} else {
|
||||||
|
Log.perf("Refreshing icon: no longer busy")
|
||||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||||
// Static icon has been requested
|
// Static icon has been requested
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
setStatusBar(image: NSImage.statusBarIconStatic)
|
||||||
} else {
|
} else {
|
||||||
// The dynamic icon has been requested
|
// The dynamic icon has been requested
|
||||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||||
@ -172,13 +176,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon to be displayed as busy. */
|
|
||||||
@objc func setBusyImage() {
|
|
||||||
Task { @MainActor [self] in
|
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Menu Item Functionality
|
// MARK: - Menu Item Functionality
|
||||||
|
|
||||||
@objc func openAbout() {
|
@objc func openAbout() {
|
||||||
@ -203,7 +200,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func openWarnings() {
|
@objc func openWarnings() {
|
||||||
WarningsWindowController.show()
|
PhpDoctorWindowController.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func openConfigGUI() {
|
||||||
|
PhpConfigManagerWindowController.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openDomainList() {
|
@objc func openDomainList() {
|
||||||
@ -214,6 +215,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
PhpVersionManagerWindowController.show()
|
PhpVersionManagerWindowController.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openPhpExtensionManager() {
|
||||||
|
PhpExtensionManagerWindowController.show()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func openDonate() {
|
@objc func openDonate() {
|
||||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||||
}
|
}
|
||||||
@ -231,7 +236,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
func menuWillOpen(_ menu: NSMenu) {
|
func menuWillOpen(_ menu: NSMenu) {
|
||||||
// Make sure the shortcut key does not trigger this when the menu is open
|
// Make sure the shortcut key does not trigger this when the menu is open
|
||||||
App.shared.shortcutHotkey?.isPaused = true
|
App.shared.shortcutHotkey?.isPaused = true
|
||||||
Task { // Reload Homebrew services information asynchronously
|
|
||||||
|
// Exit early if Valet is not detected (i.e. standalone mode)
|
||||||
|
if !Valet.installed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Task { // Reload Homebrew services information asynchronously, but only if Valet is enabled
|
||||||
await ServicesManager.shared.reloadServicesStatus()
|
await ServicesManager.shared.reloadServicesStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import Cocoa
|
|||||||
|
|
||||||
extension StatusMenu {
|
extension StatusMenu {
|
||||||
|
|
||||||
func addPhpVersionMenuItems() {
|
@MainActor func addPhpVersionMenuItems() {
|
||||||
if PhpEnvironments.phpInstall == nil {
|
if PhpEnvironments.phpInstall == nil {
|
||||||
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
||||||
addItems([
|
addItems([
|
||||||
@ -34,7 +34,7 @@ extension StatusMenu {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPhpActionMenuItems() {
|
@MainActor func addPhpActionMenuItems() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
addItem(NSMenuItem(title: "mi_busy".localized))
|
addItem(NSMenuItem(title: "mi_busy".localized))
|
||||||
return
|
return
|
||||||
@ -54,7 +54,7 @@ extension StatusMenu {
|
|||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
func addServicesManagerMenuItem() {
|
@MainActor func addServicesManagerMenuItem() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSwitchToPhpMenuItems() {
|
@MainActor func addSwitchToPhpMenuItems() {
|
||||||
var shortcutKey = 1
|
var shortcutKey = 1
|
||||||
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
||||||
// Get the short and long version
|
// Get the short and long version
|
||||||
@ -102,14 +102,14 @@ extension StatusMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addLiteModeMenuItem() {
|
@MainActor func addLiteModeMenuItem() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPreferencesMenuItems() {
|
@MainActor func addPreferencesMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_preferences".localized,
|
NSMenuItem(title: "mi_preferences".localized,
|
||||||
@ -119,7 +119,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addCoreMenuItems() {
|
@MainActor func addCoreMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_about".localized,
|
NSMenuItem(title: "mi_about".localized,
|
||||||
@ -131,7 +131,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Valet
|
// MARK: - Valet
|
||||||
|
|
||||||
func addValetMenuItems() {
|
@MainActor func addValetMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_valet".localized),
|
HeaderView.asMenuItem(text: "mi_valet".localized),
|
||||||
NSMenuItem(title: "mi_valet_config".localized,
|
NSMenuItem(title: "mi_valet_config".localized,
|
||||||
@ -146,12 +146,15 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - PHP Configuration
|
// MARK: - PHP Configuration
|
||||||
|
|
||||||
func addConfigurationMenuItems() {
|
@MainActor func addConfigurationMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
||||||
NSMenuItem(title: "mi_php_version_manager".localized,
|
NSMenuItem(title: "mi_php_version_manager".localized,
|
||||||
action: #selector(MainMenu.openPhpVersionManager),
|
action: #selector(MainMenu.openPhpVersionManager),
|
||||||
keyEquivalent: "m"),
|
keyEquivalent: "m"),
|
||||||
|
NSMenuItem(title: "mi_php_ext_manager".localized,
|
||||||
|
action: #selector(MainMenu.openPhpExtensionManager),
|
||||||
|
keyEquivalent: "e"),
|
||||||
NSMenuItem(title: "mi_php_config".localized,
|
NSMenuItem(title: "mi_php_config".localized,
|
||||||
action: #selector(MainMenu.openActiveConfigFolder),
|
action: #selector(MainMenu.openActiveConfigFolder),
|
||||||
keyEquivalent: "c"),
|
keyEquivalent: "c"),
|
||||||
@ -166,7 +169,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Composer
|
// MARK: - Composer
|
||||||
|
|
||||||
func addComposerMenuItems() {
|
@MainActor func addComposerMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_composer".localized),
|
HeaderView.asMenuItem(text: "mi_composer".localized),
|
||||||
NSMenuItem(
|
NSMenuItem(
|
||||||
@ -187,7 +190,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Stats
|
// MARK: - Stats
|
||||||
|
|
||||||
func addStatsMenuItem() {
|
@MainActor func addStatsMenuItem() {
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("Not showing stats menu item if no PHP version is linked.")
|
Log.info("Not showing stats menu item if no PHP version is linked.")
|
||||||
return
|
return
|
||||||
@ -204,7 +207,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Extensions
|
// MARK: - Extensions
|
||||||
|
|
||||||
func addExtensionsMenuItems() {
|
@MainActor func addExtensionsMenuItems() {
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
||||||
return
|
return
|
||||||
@ -225,7 +228,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Presets
|
// MARK: - Presets
|
||||||
|
|
||||||
func addPresetsMenuItem() {
|
@MainActor func addPresetsMenuItem() {
|
||||||
guard let presets = Preferences.custom.presets else {
|
guard let presets = Preferences.custom.presets else {
|
||||||
addEmptyPresetHelp()
|
addEmptyPresetHelp()
|
||||||
return
|
return
|
||||||
|
@ -9,7 +9,7 @@ import Cocoa
|
|||||||
|
|
||||||
class StatusMenu: NSMenu {
|
class StatusMenu: NSMenu {
|
||||||
// swiftlint:disable cyclomatic_complexity
|
// swiftlint:disable cyclomatic_complexity
|
||||||
func addMenuItems() {
|
@MainActor func addMenuItems() {
|
||||||
addPhpVersionMenuItems()
|
addPhpVersionMenuItems()
|
||||||
addItem(NSMenuItem.separator())
|
addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ class BetterAlert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
|
||||||
windowController.window?.makeKeyAndOrderFront(nil)
|
windowController.window?.makeKeyAndOrderFront(nil)
|
||||||
windowController.window?.setCenterPosition(offsetY: 70)
|
windowController.window?.setCenterPosition(offsetY: 70)
|
||||||
return NSApplication.shared.runModal(for: windowController.window!)
|
return NSApplication.shared.runModal(for: windowController.window!)
|
||||||
|
@ -20,6 +20,7 @@ enum PreferenceName: String, Codable {
|
|||||||
case globalHotkey = "global_hotkey"
|
case globalHotkey = "global_hotkey"
|
||||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||||
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
||||||
|
case languageOverride = "language_override"
|
||||||
|
|
||||||
// APPEARANCE
|
// APPEARANCE
|
||||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||||
@ -84,7 +85,8 @@ enum PreferenceName: String, Codable {
|
|||||||
],
|
],
|
||||||
.string: [
|
.string: [
|
||||||
.globalHotkey,
|
.globalHotkey,
|
||||||
.iconTypeToDisplay
|
.iconTypeToDisplay,
|
||||||
|
.languageOverride
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ class Preferences {
|
|||||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||||
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
||||||
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
||||||
|
PreferenceName.languageOverride.rawValue: "",
|
||||||
|
|
||||||
/// Preferences: Appearance
|
/// Preferences: Appearance
|
||||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||||
|
@ -17,7 +17,9 @@ class GeneralPreferencesVC: GenericPreferenceVC {
|
|||||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||||
|
|
||||||
_ = vc.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
_ = vc
|
||||||
|
.addView(when: true, vc.getLanguageOptionsPV())
|
||||||
|
.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
||||||
.addView(when: true, vc.getAutoRestartServicesPV())
|
.addView(when: true, vc.getAutoRestartServicesPV())
|
||||||
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
||||||
.addView(when: true, vc.getShortcutPV())
|
.addView(when: true, vc.getShortcutPV())
|
||||||
|
@ -48,11 +48,44 @@ class GenericPreferenceVC: NSViewController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLanguageOptionsPV() -> NSView {
|
||||||
|
var options = Bundle.main.localizations
|
||||||
|
.filter({ $0 != "Base"})
|
||||||
|
.map({ lang in
|
||||||
|
return PreferenceDropdownOption(
|
||||||
|
label: Locale.current.localizedString(forLanguageCode: lang)!,
|
||||||
|
value: lang
|
||||||
|
)
|
||||||
|
})
|
||||||
|
options.insert(PreferenceDropdownOption(label: "System Default", value: ""), at: 0)
|
||||||
|
|
||||||
|
return SelectPreferenceView.make(
|
||||||
|
sectionText: "prefs.language".localized,
|
||||||
|
descriptionText: "prefs.language_options_desc".localized,
|
||||||
|
options: options,
|
||||||
|
preference: .languageOverride,
|
||||||
|
action: {
|
||||||
|
MainMenu.shared.refreshIcon()
|
||||||
|
MainMenu.shared.rebuild()
|
||||||
|
|
||||||
|
if let window = App.shared.preferencesWindowController?.window {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "alert.language_changed.title".localized
|
||||||
|
alert.informativeText = "alert.language_changed.subtitle".localized
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.addButton(withTitle: "generic.ok".localized)
|
||||||
|
alert.beginSheetModal(for: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func getIconOptionsPV() -> NSView {
|
func getIconOptionsPV() -> NSView {
|
||||||
return SelectPreferenceView.make(
|
return SelectPreferenceView.make(
|
||||||
sectionText: "",
|
sectionText: "",
|
||||||
descriptionText: "prefs.icon_options_desc".localized,
|
descriptionText: "prefs.icon_options_desc".localized,
|
||||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
options: MenuBarIcon.allCases
|
||||||
|
.map({ return PreferenceDropdownOption(label: $0.rawValue, value: $0.rawValue) }),
|
||||||
localizationPrefix: "prefs.icon_options",
|
localizationPrefix: "prefs.icon_options",
|
||||||
preference: .iconTypeToDisplay,
|
preference: .iconTypeToDisplay,
|
||||||
action: {
|
action: {
|
||||||
|
@ -65,9 +65,11 @@ class PreferencesWindowController: PMWindowController {
|
|||||||
App.shared.preferencesWindowController?.showWindow(self)
|
App.shared.preferencesWindowController?.showWindow(self)
|
||||||
|
|
||||||
if justCreated {
|
if justCreated {
|
||||||
App.shared.preferencesWindowController?.positionWindowInTopLeftCorner()
|
App.shared.preferencesWindowController?.positionWindowInTopRightCorner()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App.shared.preferencesWindowController?.window?.orderFrontRegardless()
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,22 +85,22 @@ class PreferencesWindowController: PMWindowController {
|
|||||||
return [
|
return [
|
||||||
PrefTabView(
|
PrefTabView(
|
||||||
viewController: GeneralPreferencesVC.fromStoryboard(),
|
viewController: GeneralPreferencesVC.fromStoryboard(),
|
||||||
label: "General",
|
label: "prefs.tabs.general".localized,
|
||||||
icon: "gearshape"
|
icon: "gearshape"
|
||||||
),
|
),
|
||||||
PrefTabView(
|
PrefTabView(
|
||||||
viewController: AppearancePreferencesVC.fromStoryboard(),
|
viewController: AppearancePreferencesVC.fromStoryboard(),
|
||||||
label: "Appearance",
|
label: "prefs.tabs.appearance".localized,
|
||||||
icon: "paintbrush"
|
icon: "paintbrush"
|
||||||
),
|
),
|
||||||
PrefTabView(
|
PrefTabView(
|
||||||
viewController: MenuStructurePreferencesVC.fromStoryboard(),
|
viewController: MenuStructurePreferencesVC.fromStoryboard(),
|
||||||
label: "Visibility",
|
label: "prefs.tabs.visibility".localized,
|
||||||
icon: "eye"
|
icon: "eye"
|
||||||
),
|
),
|
||||||
PrefTabView(
|
PrefTabView(
|
||||||
viewController: NotificationPreferencesVC.fromStoryboard(),
|
viewController: NotificationPreferencesVC.fromStoryboard(),
|
||||||
label: "Notifications",
|
label: "prefs.tabs.notifications".localized,
|
||||||
icon: "bell.badge"
|
icon: "bell.badge"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
@ -84,6 +84,10 @@ class Stats {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func clearCurrentGlobalPhpVersion() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: InternalStats.lastGlobalPhpVersion.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine if the sponsor message should be displayed.
|
Determine if the sponsor message should be displayed.
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
|
@ -9,30 +9,34 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class SelectPreferenceView: NSView, XibLoadable {
|
struct PreferenceDropdownOption {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectPreferenceView: NSView, XibLoadable {
|
||||||
@IBOutlet weak var labelSection: NSTextField!
|
@IBOutlet weak var labelSection: NSTextField!
|
||||||
@IBOutlet weak var labelDescription: NSTextField!
|
@IBOutlet weak var labelDescription: NSTextField!
|
||||||
@IBOutlet weak var popupButton: NSPopUpButton!
|
@IBOutlet weak var popupButton: NSPopUpButton!
|
||||||
|
|
||||||
var localizationPrefix: String = ""
|
var localizationPrefix: String?
|
||||||
var imagePrefix: String?
|
var imagePrefix: String?
|
||||||
|
|
||||||
var options: [String] = [] {
|
var options: [PreferenceDropdownOption] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
self.popupButton.removeAllItems()
|
self.popupButton.removeAllItems()
|
||||||
self.options.forEach { value in
|
self.options.forEach { option in
|
||||||
self.popupButton.addItem(
|
if let prefix = localizationPrefix {
|
||||||
withTitle: "\(localizationPrefix).\(value)".localized
|
self.popupButton.addItem(withTitle: "\(prefix).\(option.label)".localized)
|
||||||
)
|
} else {
|
||||||
|
self.popupButton.addItem(withTitle: option.label)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if imagePrefix == nil {
|
if let prefix = imagePrefix {
|
||||||
return
|
self.popupButton.itemArray.enumerated().forEach { item in
|
||||||
}
|
item.element.image = NSImage(named: "\(prefix)_\(self.options[item.offset].value)")
|
||||||
|
}
|
||||||
self.popupButton.itemArray.enumerated().forEach { item in
|
|
||||||
item.element.image = NSImage(named: "\(imagePrefix!)_\(self.options[item.offset])")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,19 +47,18 @@ class SelectPreferenceView: NSView, XibLoadable {
|
|||||||
didSet {
|
didSet {
|
||||||
let value = Preferences.preferences[preference] as! String
|
let value = Preferences.preferences[preference] as! String
|
||||||
self.options.enumerated().forEach { option in
|
self.options.enumerated().forEach { option in
|
||||||
if option.element == value {
|
if option.element.value == value {
|
||||||
self.popupButton.selectItem(at: option.offset)
|
self.popupButton.selectItem(at: option.offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable function_parameter_count
|
|
||||||
static func make(
|
static func make(
|
||||||
sectionText: String,
|
sectionText: String,
|
||||||
descriptionText: String,
|
descriptionText: String,
|
||||||
options: [String],
|
options: [PreferenceDropdownOption],
|
||||||
localizationPrefix: String,
|
localizationPrefix: String? = nil,
|
||||||
imagePrefix: String? = nil,
|
imagePrefix: String? = nil,
|
||||||
preference: PreferenceName,
|
preference: PreferenceName,
|
||||||
action: @escaping () -> Void) -> NSView {
|
action: @escaping () -> Void) -> NSView {
|
||||||
@ -72,11 +75,10 @@ class SelectPreferenceView: NSView, XibLoadable {
|
|||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
// swiftlint:enable function_parameter_count
|
|
||||||
|
|
||||||
@IBAction func valueChanged(_ sender: Any) {
|
@IBAction func valueChanged(_ sender: Any) {
|
||||||
let index = self.popupButton.indexOfSelectedItem
|
let index = self.popupButton.indexOfSelectedItem
|
||||||
Preferences.update(.iconTypeToDisplay, value: self.options[index])
|
Preferences.update(self.preference, value: self.options[index].value)
|
||||||
self.action()
|
self.action()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="-2" y="29" width="154" height="16"/>
|
<rect key="frame" x="-2" y="29" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
@ -58,7 +58,7 @@
|
|||||||
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
||||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
||||||
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" id="Ztd-uk-4aw"/>
|
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" id="aBU-J8-gRK"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -29,7 +29,7 @@ struct BlockingOverlayView<Content: View>: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
content().opacity(isBlocking ? 0.2 : 1)
|
content().opacity(isBlocking ? 0 : 1)
|
||||||
if isBlocking {
|
if isBlocking {
|
||||||
VStack {
|
VStack {
|
||||||
ActivityIndicator()
|
ActivityIndicator()
|
||||||
@ -44,7 +44,8 @@ struct BlockingOverlayView<Content: View>: View {
|
|||||||
.padding(.top, -4)
|
.padding(.top, -4)
|
||||||
}.padding(60)
|
}.padding(60)
|
||||||
}
|
}
|
||||||
}.background(Color.white)
|
}
|
||||||
|
.background(Color.spinnerBackground)
|
||||||
.disabled(isBlocking)
|
.disabled(isBlocking)
|
||||||
}
|
}
|
||||||
}
|
}
|
36
phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift
Normal file
36
phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// CustomButtonStyles.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 15/03/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct CustomButtonStyle: ButtonStyle {
|
||||||
|
@Environment(\.isEnabled) var isEnabled
|
||||||
|
|
||||||
|
public func makeBody(configuration: Self.Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.background(.statusColorBlue, in: .rect(cornerRadius: 8, style: .continuous))
|
||||||
|
.opacity({
|
||||||
|
if configuration.isPressed {
|
||||||
|
return 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isEnabled {
|
||||||
|
return 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1.0
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ButtonStyle where Self == CustomButtonStyle {
|
||||||
|
static var custom: CustomButtonStyle { .init() }
|
||||||
|
}
|
@ -27,15 +27,15 @@ struct HelpButton: View {
|
|||||||
.buttonStyle(BorderlessButtonStyle())
|
.buttonStyle(BorderlessButtonStyle())
|
||||||
.focusable(false)
|
.focusable(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
struct HelpButton_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
#Preview("Light Mode") {
|
||||||
Group {
|
HelpButton(action: {})
|
||||||
HelpButton(action: {}).padding()
|
.padding(100)
|
||||||
.previewDisplayName("Light Mode")
|
}
|
||||||
HelpButton(action: {}).padding().preferredColorScheme(.dark)
|
|
||||||
.previewDisplayName("Dark Mode")
|
#Preview("Dark Mode") {
|
||||||
}
|
HelpButton(action: {})
|
||||||
}
|
.padding(100)
|
||||||
}
|
.preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,9 @@ var isRunningSwiftUIPreview: Bool {
|
|||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
public static var appPrimary: Color = Color("AppColor")
|
public static var appPrimary: Color = Color("AppColor")
|
||||||
public static var appSecondary: Color = Color("AppSecondary")
|
|
||||||
|
// This next one is generated automatically via asset catalogs now
|
||||||
|
// public static var appSecondary: Color = Color("AppSecondary")
|
||||||
|
|
||||||
public static var debug: Color = {
|
public static var debug: Color = {
|
||||||
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
||||||
|
67
phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift
Normal file
67
phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
//
|
||||||
|
// NoDomainsView.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 19/03/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UnavailableContentView: View {
|
||||||
|
var title: String
|
||||||
|
var description: String
|
||||||
|
var icon: String
|
||||||
|
var button: String?
|
||||||
|
var action: (() -> Void)?
|
||||||
|
|
||||||
|
init(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
icon: String,
|
||||||
|
button: String? = nil,
|
||||||
|
action: (() -> Void)? = nil
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.icon = icon
|
||||||
|
self.button = button
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
Image(systemName: self.icon)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 48, height: 48)
|
||||||
|
.foregroundColor(Color.appPrimary)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
Text(self.title)
|
||||||
|
.font(.system(size: 18, weight: .bold))
|
||||||
|
|
||||||
|
Text(self.description)
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
if self.button != nil {
|
||||||
|
Button(self.button!) {
|
||||||
|
self.action!()
|
||||||
|
}.buttonStyle(.custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(30)
|
||||||
|
.frame(maxWidth: 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
UnavailableContentView(
|
||||||
|
title: "domain_list.domains_empty.title".localized,
|
||||||
|
description: "domain_list.domains_empty.desc".localized,
|
||||||
|
icon: "globe",
|
||||||
|
button: "domain_list.domains_empty.button".localized,
|
||||||
|
action: {}
|
||||||
|
)
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// NoDomainResults.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 15/08/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct NoDomainResults: View {
|
|
||||||
@State var searching: Bool = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .center, spacing: 15) {
|
|
||||||
Image(systemName: searching ? "magnifyingglass.circle.fill" : "questionmark.circle.fill")
|
|
||||||
.resizable()
|
|
||||||
.renderingMode(.template)
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
VStack(alignment: .center) {
|
|
||||||
Text(
|
|
||||||
searching
|
|
||||||
? "domain_list.no_domains_for_search_query".localizedForSwiftUI
|
|
||||||
: "domain_list.no_domains".localizedForSwiftUI
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minWidth: 0, maxWidth: .infinity)
|
|
||||||
.padding(25)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NoDomainResults_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
NoDomainResults()
|
|
||||||
}
|
|
||||||
}
|
|
@ -126,78 +126,82 @@ struct DisclaimerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VersionPopoverView_Previews: PreviewProvider {
|
#Preview("Unknown Requirement") {
|
||||||
static var previews: some View {
|
VersionPopoverView(
|
||||||
VersionPopoverView(
|
site: FakeValetSite(
|
||||||
site: FakeValetSite(
|
fakeWithName: "amazingwebsite",
|
||||||
fakeWithName: "amazingwebsite",
|
tld: "test",
|
||||||
tld: "test",
|
secure: true,
|
||||||
secure: true,
|
path: "/path/to/site",
|
||||||
path: "/path/to/site",
|
linked: true,
|
||||||
linked: true,
|
constraint: ""
|
||||||
constraint: ""
|
),
|
||||||
),
|
validPhpVersions: [],
|
||||||
validPhpVersions: [],
|
parent: nil
|
||||||
parent: nil
|
)
|
||||||
)
|
}
|
||||||
.previewDisplayName("Unknown Requirement")
|
|
||||||
|
#Preview("Requirement Matches") {
|
||||||
VersionPopoverView(
|
VersionPopoverView(
|
||||||
site: FakeValetSite(
|
site: FakeValetSite(
|
||||||
fakeWithName: "amazingwebsite",
|
fakeWithName: "amazingwebsite",
|
||||||
tld: "test",
|
tld: "test",
|
||||||
secure: true,
|
secure: true,
|
||||||
path: "/path/to/site",
|
path: "/path/to/site",
|
||||||
linked: true,
|
linked: true,
|
||||||
constraint: "^8.1"
|
constraint: "^8.1"
|
||||||
),
|
),
|
||||||
validPhpVersions: [],
|
validPhpVersions: [],
|
||||||
parent: nil
|
parent: nil
|
||||||
)
|
)
|
||||||
.previewDisplayName("Requirement Matches")
|
}
|
||||||
VersionPopoverView(
|
|
||||||
site: FakeValetSite(
|
#Preview("Isolated") {
|
||||||
fakeWithName: "anothersite",
|
VersionPopoverView(
|
||||||
tld: "test",
|
site: FakeValetSite(
|
||||||
secure: true,
|
fakeWithName: "anothersite",
|
||||||
path: "/path/to/site",
|
tld: "test",
|
||||||
linked: true,
|
secure: true,
|
||||||
constraint: "^8.0",
|
path: "/path/to/site",
|
||||||
isolated: "8.0"
|
linked: true,
|
||||||
),
|
constraint: "^8.0",
|
||||||
validPhpVersions: [],
|
isolated: "8.0"
|
||||||
parent: nil
|
),
|
||||||
)
|
validPhpVersions: [],
|
||||||
.previewDisplayName("Isolated")
|
parent: nil
|
||||||
VersionPopoverView(
|
)
|
||||||
site: FakeValetSite(
|
}
|
||||||
fakeWithName: "anothersite",
|
|
||||||
tld: "test",
|
#Preview("Isolated Mismatch") {
|
||||||
secure: true,
|
VersionPopoverView(
|
||||||
path: "/path/to/site",
|
site: FakeValetSite(
|
||||||
linked: true,
|
fakeWithName: "anothersite",
|
||||||
constraint: "^8.0",
|
tld: "test",
|
||||||
isolated: "7.4"
|
secure: true,
|
||||||
),
|
path: "/path/to/site",
|
||||||
validPhpVersions: [],
|
linked: true,
|
||||||
parent: nil
|
constraint: "^8.0",
|
||||||
)
|
isolated: "7.4"
|
||||||
.previewDisplayName("Isolated Mismatch")
|
),
|
||||||
VersionPopoverView(
|
validPhpVersions: [],
|
||||||
site: FakeValetSite(
|
parent: nil
|
||||||
fakeWithName: "anothersite",
|
)
|
||||||
tld: "test",
|
}
|
||||||
secure: true,
|
|
||||||
path: "/path/to/site",
|
#Preview("Recommend Alternatives") {
|
||||||
linked: true,
|
VersionPopoverView(
|
||||||
constraint: "^8.0"
|
site: FakeValetSite(
|
||||||
),
|
fakeWithName: "anothersite",
|
||||||
validPhpVersions: [
|
tld: "test",
|
||||||
VersionNumber(major: 8, minor: 0, patch: 0),
|
secure: true,
|
||||||
VersionNumber(major: 8, minor: 1, patch: 0)
|
path: "/path/to/site",
|
||||||
],
|
linked: true,
|
||||||
parent: nil
|
constraint: "^8.0"
|
||||||
)
|
),
|
||||||
.previewDisplayName("Recommend Alternatives")
|
validPhpVersions: [
|
||||||
}
|
VersionNumber(major: 8, minor: 0, patch: 0),
|
||||||
|
VersionNumber(major: 8, minor: 1, patch: 0)
|
||||||
|
],
|
||||||
|
parent: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,7 @@ struct HeaderView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HeaderView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
HeaderView(text: "Hello world")
|
||||||
HeaderView(text: "Hello world")
|
.frame(width: 330.0)
|
||||||
.frame(width: 330.0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -18,5 +18,6 @@ struct SectionHeaderView: View {
|
|||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.appSecondary)
|
.foregroundColor(.appSecondary)
|
||||||
.background(Color.debug)
|
.background(Color.debug)
|
||||||
|
.minimumScaleFactor(0.8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,23 +172,21 @@ struct ServiceView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ServicesView_Previews: PreviewProvider {
|
#Preview("Active 1") {
|
||||||
static var previews: some View {
|
ServicesView(manager: FakeServicesManager(
|
||||||
ServicesView(manager: FakeServicesManager(
|
formulae: ["php", "nginx", "dnsmasq"],
|
||||||
formulae: ["php", "nginx", "dnsmasq"],
|
status: .active
|
||||||
status: .active
|
), perRow: 4)
|
||||||
), perRow: 4)
|
.frame(width: 330.0)
|
||||||
.frame(width: 330.0)
|
}
|
||||||
.previewDisplayName("Active 1")
|
|
||||||
|
#Preview("Active 2") {
|
||||||
ServicesView(manager: FakeServicesManager(
|
ServicesView(manager: FakeServicesManager(
|
||||||
formulae: [
|
formulae: [
|
||||||
"php", "nginx", "dnsmasq", "thing1",
|
"php", "nginx", "dnsmasq", "thing1",
|
||||||
"thing2", "thing3", "thing4", "thing5"
|
"thing2", "thing3", "thing4", "thing5"
|
||||||
],
|
],
|
||||||
status: .inactive
|
status: .inactive
|
||||||
), perRow: 4)
|
), perRow: 4)
|
||||||
.frame(width: 330.0)
|
.frame(width: 330.0)
|
||||||
.previewDisplayName("Active 2")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -29,38 +29,80 @@ struct StatsView: View {
|
|||||||
@State var maxPostSize: String
|
@State var maxPostSize: String
|
||||||
@State var maxUploadSize: String
|
@State var maxUploadSize: String
|
||||||
|
|
||||||
|
init(memoryLimit: String, maxPostSize: String, maxUploadSize: String) {
|
||||||
|
self.memoryLimit = memoryLimit
|
||||||
|
self.maxPostSize = maxPostSize
|
||||||
|
self.maxUploadSize = maxUploadSize
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hasErrorState() -> Bool {
|
||||||
|
return self.memoryLimit == "⚠️"
|
||||||
|
&& self.maxPostSize == "⚠️"
|
||||||
|
&& self.maxUploadSize == "⚠️"
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 30) {
|
if self.hasErrorState() {
|
||||||
VStack(alignment: .center, spacing: 3) {
|
HStack {
|
||||||
SectionHeaderView(text: "mi_memory_limit".localized.uppercased())
|
Text("⚠️")
|
||||||
Text(memoryLimit)
|
.frame(maxWidth: 20, alignment: .center)
|
||||||
.fontWeight(.medium)
|
|
||||||
.font(.system(size: 16))
|
.font(.system(size: 16))
|
||||||
|
VStack {
|
||||||
|
Text("warnings.limits_error.title".localizedForSwiftUI)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
Text("warnings.limits_error.steps".localizedForSwiftUI)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
VStack(alignment: .center, spacing: 3) {
|
.padding(10)
|
||||||
SectionHeaderView(text: "mi_post_max_size".localized.uppercased())
|
.padding(.leading, 30)
|
||||||
Text(maxPostSize)
|
.padding(.trailing, 30)
|
||||||
.fontWeight(.medium)
|
} else {
|
||||||
.font(.system(size: 16))
|
HStack(alignment: .center, spacing: 10) {
|
||||||
}
|
VStack(alignment: .center, spacing: 3) {
|
||||||
VStack(alignment: .center, spacing: 3) {
|
SectionHeaderView(text: "mi_memory_limit".localized.uppercased())
|
||||||
SectionHeaderView(text: "mi_upload_max_filesize".localized.uppercased())
|
Text(memoryLimit)
|
||||||
Text(maxUploadSize)
|
.fontWeight(.medium)
|
||||||
.fontWeight(.medium)
|
.font(.system(size: 16))
|
||||||
.font(.system(size: 16))
|
}
|
||||||
|
Divider()
|
||||||
|
VStack(alignment: .center, spacing: 3) {
|
||||||
|
SectionHeaderView(text: "mi_post_max_size".localized.uppercased())
|
||||||
|
Text(maxPostSize)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.font(.system(size: 16))
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
VStack(alignment: .center, spacing: 3) {
|
||||||
|
SectionHeaderView(text: "mi_upload_max_filesize".localized.uppercased())
|
||||||
|
Text(maxUploadSize)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.font(.system(size: 16))
|
||||||
|
}
|
||||||
|
Divider().hidden()
|
||||||
|
Button {
|
||||||
|
Task { @MainActor in
|
||||||
|
MainMenu.shared.openConfigGUI()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "gearshape.fill")
|
||||||
|
}
|
||||||
|
.accessibility(identifier: "phpConfigButton")
|
||||||
|
.focusable(false)
|
||||||
|
.frame(minWidth: 30, alignment: .center)
|
||||||
}
|
}
|
||||||
|
.padding(5)
|
||||||
|
.background(Color.debug)
|
||||||
}
|
}
|
||||||
.padding(10)
|
|
||||||
.background(Color.debug)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StatsView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
StatsView(
|
||||||
StatsView(
|
memoryLimit: "1024 MB",
|
||||||
memoryLimit: "1024 MB",
|
maxPostSize: "1024 MB",
|
||||||
maxPostSize: "1024 MB",
|
maxUploadSize: "1024 MB"
|
||||||
maxUploadSize: "1024 MB"
|
).frame(height: 100)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,400 +0,0 @@
|
|||||||
//
|
|
||||||
// PhpFormulaeView.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 17/03/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// swiftlint:disable type_body_length
|
|
||||||
struct PhpFormulaeView: View {
|
|
||||||
@ObservedObject var formulae: BrewFormulaeObservable
|
|
||||||
@ObservedObject var status: PhpFormulaeStatus
|
|
||||||
var handler: HandlesBrewFormulae
|
|
||||||
|
|
||||||
init(
|
|
||||||
formulae: BrewFormulaeObservable,
|
|
||||||
handler: HandlesBrewFormulae
|
|
||||||
) {
|
|
||||||
self.formulae = formulae
|
|
||||||
self.handler = handler
|
|
||||||
|
|
||||||
self.status = PhpFormulaeStatus(
|
|
||||||
busy: true,
|
|
||||||
title: "phpman.busy.title".localized,
|
|
||||||
description: "phpman.busy.description.outdated".localized
|
|
||||||
)
|
|
||||||
|
|
||||||
Task { [self] in
|
|
||||||
await self.initialLoad()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func initialLoad() async {
|
|
||||||
guard let version = Brew.shared.version else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await delay(seconds: 1)
|
|
||||||
|
|
||||||
if version.major != 4 {
|
|
||||||
Task { @MainActor in
|
|
||||||
self.presentErrorAlert(
|
|
||||||
title: "phpman.warnings.unsupported.title".localized,
|
|
||||||
description: "phpman.warnings.unsupported.desc".localized(version.text),
|
|
||||||
button: "generic.ok".localized,
|
|
||||||
style: .warning
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await PhpEnvironments.detectPhpVersions()
|
|
||||||
await self.handler.refreshPhpVersions(loadOutdated: false)
|
|
||||||
await self.handler.refreshPhpVersions(loadOutdated: true)
|
|
||||||
self.status.busy = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reload() async {
|
|
||||||
Task { @MainActor in
|
|
||||||
self.status.busy = true
|
|
||||||
self.status.title = "phpman.busy.title".localized
|
|
||||||
self.status.description = "phpman.busy.description.outdated".localized
|
|
||||||
}
|
|
||||||
await self.handler.refreshPhpVersions(loadOutdated: true)
|
|
||||||
Task { @MainActor in
|
|
||||||
self.status.busy = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
HStack(alignment: .center, spacing: 15) {
|
|
||||||
Image(systemName: "arrow.down.to.line.circle.fill")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
.foregroundColor(Color.blue)
|
|
||||||
.padding(12)
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
|
||||||
Text("phpman.description".localizedForSwiftUI)
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
Text("phpman.disclaimer".localizedForSwiftUI)
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(10)
|
|
||||||
|
|
||||||
if self.hasUpdates {
|
|
||||||
Divider()
|
|
||||||
HStack(alignment: .center, spacing: 15) {
|
|
||||||
Text("phpman.has_updates.description".localizedForSwiftUI)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
|
|
||||||
Button("phpman.has_updates.button".localizedForSwiftUI, action: {
|
|
||||||
Task { await self.upgradeAll(self.formulae.upgradeable) }
|
|
||||||
|
|
||||||
})
|
|
||||||
.focusable(false)
|
|
||||||
.disabled(self.status.busy)
|
|
||||||
}
|
|
||||||
.padding(10)
|
|
||||||
} else {
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
HStack(alignment: .center, spacing: 15) {
|
|
||||||
Button {
|
|
||||||
Task { await self.reload() }
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "arrow.clockwise")
|
|
||||||
.buttonStyle(.automatic)
|
|
||||||
.controlSize(.large)
|
|
||||||
}
|
|
||||||
.focusable(false)
|
|
||||||
.disabled(self.status.busy)
|
|
||||||
|
|
||||||
Text("phpman.refresh.button.description".localizedForSwiftUI)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
}
|
|
||||||
.padding(10)
|
|
||||||
}
|
|
||||||
|
|
||||||
BlockingOverlayView(busy: self.status.busy, title: self.status.title, text: self.status.description) {
|
|
||||||
List(Array(formulae.phpVersions.enumerated()), id: \.1.name) { (index, formula) in
|
|
||||||
HStack {
|
|
||||||
Image(systemName: formula.icon)
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(width: 16, height: 16)
|
|
||||||
.foregroundColor(formula.iconColor)
|
|
||||||
.padding(.horizontal, 5)
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(formula.displayName).bold()
|
|
||||||
|
|
||||||
if formula.isInstalled && formula.hasUpgrade {
|
|
||||||
Text("phpman.version.has_update".localized(
|
|
||||||
formula.installedVersion!,
|
|
||||||
formula.upgradeVersion!
|
|
||||||
))
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
} else if formula.isInstalled && formula.installedVersion != nil {
|
|
||||||
Text("phpman.version.installed".localized(formula.installedVersion!))
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
} else {
|
|
||||||
Text("phpman.version.available_for_installation".localizedForSwiftUI)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !formula.healthy {
|
|
||||||
Text("phpman.version.broken".localizedForSwiftUI)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
|
|
||||||
if !formula.healthy {
|
|
||||||
Button("phpman.buttons.repair".localizedForSwiftUI, role: .destructive) {
|
|
||||||
Task { await self.repairAll() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if formula.isInstalled {
|
|
||||||
Button("phpman.buttons.uninstall".localizedForSwiftUI, role: .destructive) {
|
|
||||||
Task { await self.confirmUninstall(formula) }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button("phpman.buttons.install".localizedForSwiftUI) {
|
|
||||||
Task { await self.install(formula) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listRowBackground(index % 2 == 0
|
|
||||||
? Color.gray.opacity(0)
|
|
||||||
: Color.gray.opacity(0.08)
|
|
||||||
)
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.frame(width: 600, height: 600)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func runCommand(_ command: InstallAndUpgradeCommand) async {
|
|
||||||
do {
|
|
||||||
self.setBusyStatus(true)
|
|
||||||
try await command.execute { progress in
|
|
||||||
Task { @MainActor in
|
|
||||||
self.status.title = progress.title
|
|
||||||
self.status.description = progress.description
|
|
||||||
self.status.busy = progress.value != 1
|
|
||||||
|
|
||||||
// Whenever a key step is finished, refresh the PHP versions
|
|
||||||
if progress.value == 1 {
|
|
||||||
await self.handler.refreshPhpVersions(loadOutdated: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Finally, after completing the command, also refresh PHP versions
|
|
||||||
await self.handler.refreshPhpVersions(loadOutdated: false)
|
|
||||||
// and mark the app as no longer busy
|
|
||||||
self.setBusyStatus(false)
|
|
||||||
} catch let error {
|
|
||||||
let error = error as! BrewCommandError
|
|
||||||
let messages = error.log.suffix(2).joined(separator: "\n")
|
|
||||||
|
|
||||||
self.setBusyStatus(false)
|
|
||||||
await self.handler.refreshPhpVersions(loadOutdated: false)
|
|
||||||
|
|
||||||
self.presentErrorAlert(
|
|
||||||
title: "phpman.failures.install.title".localized,
|
|
||||||
description: "phpman.failures.install.desc".localized(messages),
|
|
||||||
button: "generic.ok".localized
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func repairAll() async {
|
|
||||||
await self.runCommand(InstallAndUpgradeCommand(
|
|
||||||
title: "Repairing installations...",
|
|
||||||
upgrading: [],
|
|
||||||
installing: []
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
public func upgradeAll(_ formulae: [BrewFormula]) async {
|
|
||||||
await self.runCommand(InstallAndUpgradeCommand(
|
|
||||||
title: "Installing updates...",
|
|
||||||
upgrading: formulae,
|
|
||||||
installing: []
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
public func install(_ formula: BrewFormula) async {
|
|
||||||
await self.runCommand(InstallAndUpgradeCommand(
|
|
||||||
title: "Installing \(formula.displayName)",
|
|
||||||
upgrading: [],
|
|
||||||
installing: [formula]
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
public func confirmUninstall(_ formula: BrewFormula) async {
|
|
||||||
// Disallow removal of the currently active versipn
|
|
||||||
if formula.installedVersion == PhpEnvironments.shared.currentInstall?.version.text {
|
|
||||||
self.presentErrorAlert(
|
|
||||||
title: "phpman.uninstall_prevented.title".localized,
|
|
||||||
description: "phpman.uninstall_prevented.desc".localized,
|
|
||||||
button: "generic.ok".localized
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Alert.confirm(
|
|
||||||
onWindow: App.shared.versionManagerWindowController!.window!,
|
|
||||||
messageText: "phpman.warnings.removal.title".localized(formula.displayName),
|
|
||||||
informativeText: "phpman.warnings.removal.desc".localized(formula.displayName),
|
|
||||||
buttonTitle: "phpman.warnings.removal.button".localized,
|
|
||||||
buttonIsDestructive: true,
|
|
||||||
secondButtonTitle: "generic.cancel".localized,
|
|
||||||
style: .warning,
|
|
||||||
onFirstButtonPressed: {
|
|
||||||
Task { await self.uninstall(formula) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func uninstall(_ formula: BrewFormula) async {
|
|
||||||
let command = RemovePhpVersionCommand(formula: formula.name)
|
|
||||||
|
|
||||||
do {
|
|
||||||
self.setBusyStatus(true)
|
|
||||||
try await command.execute { progress in
|
|
||||||
Task { @MainActor in
|
|
||||||
self.status.title = progress.title
|
|
||||||
self.status.description = progress.description
|
|
||||||
self.status.busy = progress.value != 1
|
|
||||||
|
|
||||||
if progress.value == 1 {
|
|
||||||
await self.handler.refreshPhpVersions(loadOutdated: false)
|
|
||||||
self.setBusyStatus(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
self.setBusyStatus(false)
|
|
||||||
self.presentErrorAlert(
|
|
||||||
title: "phpman.failures.uninstall.title".localized,
|
|
||||||
description: "phpman.failures.uninstall.desc".localized(
|
|
||||||
"brew uninstall \(formula.name) --force"
|
|
||||||
),
|
|
||||||
button: "generic.ok".localized
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setBusyStatus(_ busy: Bool) {
|
|
||||||
PhpEnvironments.shared.isBusy = busy
|
|
||||||
if busy {
|
|
||||||
Task { @MainActor in
|
|
||||||
MainMenu.shared.setBusyImage()
|
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
self.status.busy = busy
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Task { @MainActor in
|
|
||||||
MainMenu.shared.updatePhpVersionInStatusBar()
|
|
||||||
self.status.busy = busy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func presentErrorAlert(
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
button: String,
|
|
||||||
style: NSAlert.Style = .critical
|
|
||||||
) {
|
|
||||||
Alert.confirm(
|
|
||||||
onWindow: App.shared.versionManagerWindowController!.window!,
|
|
||||||
messageText: title,
|
|
||||||
informativeText: description,
|
|
||||||
buttonTitle: button,
|
|
||||||
secondButtonTitle: "",
|
|
||||||
style: style,
|
|
||||||
onFirstButtonPressed: {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasUpdates: Bool {
|
|
||||||
return self.formulae.phpVersions.contains { formula in
|
|
||||||
return formula.hasUpgrade
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// swiftlint:enable type_body_length
|
|
||||||
|
|
||||||
struct PhpFormulaeView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
PhpFormulaeView(
|
|
||||||
formulae: Brew.shared.formulae,
|
|
||||||
handler: FakeBrewFormulaeHandler()
|
|
||||||
).frame(width: 600, height: 600)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeBrewFormulaeHandler: HandlesBrewFormulae {
|
|
||||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] {
|
|
||||||
return [
|
|
||||||
BrewFormula(
|
|
||||||
name: "php",
|
|
||||||
displayName: "PHP 8.2",
|
|
||||||
installedVersion: "8.2.3",
|
|
||||||
upgradeVersion: "8.2.4"
|
|
||||||
),
|
|
||||||
BrewFormula(
|
|
||||||
name: "php@8.1",
|
|
||||||
displayName: "PHP 8.1",
|
|
||||||
installedVersion: "8.1.17",
|
|
||||||
upgradeVersion: nil
|
|
||||||
),
|
|
||||||
BrewFormula(
|
|
||||||
name: "php@8.0",
|
|
||||||
displayName: "PHP 8.0",
|
|
||||||
installedVersion: nil,
|
|
||||||
upgradeVersion: nil
|
|
||||||
),
|
|
||||||
BrewFormula(
|
|
||||||
name: "php@7.4",
|
|
||||||
displayName: "PHP 7.4",
|
|
||||||
installedVersion: nil,
|
|
||||||
upgradeVersion: nil
|
|
||||||
),
|
|
||||||
BrewFormula(
|
|
||||||
name: "php@7.3",
|
|
||||||
displayName: "PHP 7.3",
|
|
||||||
installedVersion: nil,
|
|
||||||
upgradeVersion: nil
|
|
||||||
),
|
|
||||||
BrewFormula(
|
|
||||||
name: "php@7.2",
|
|
||||||
displayName: "PHP 7.2",
|
|
||||||
installedVersion: nil,
|
|
||||||
upgradeVersion: nil
|
|
||||||
),
|
|
||||||
BrewFormula(
|
|
||||||
name: "php@7.1",
|
|
||||||
displayName: "PHP 7.1",
|
|
||||||
installedVersion: nil,
|
|
||||||
upgradeVersion: nil
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
//
|
|
||||||
// ProgressViewSubject.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 11/03/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class ProgressViewSubject: ObservableObject {
|
|
||||||
@Published var title: String
|
|
||||||
@Published var description: String?
|
|
||||||
@Published var progress: Double
|
|
||||||
|
|
||||||
init(title: String, description: String) {
|
|
||||||
self.title = title
|
|
||||||
self.description = description
|
|
||||||
self.progress = 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
//
|
|
||||||
// ProgressWindowView.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 11/03/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ProgressWindowView: View {
|
|
||||||
@ObservedObject var subject: ProgressViewSubject
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(subject.title)
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.bold()
|
|
||||||
if subject.description != nil {
|
|
||||||
Text(subject.description!)
|
|
||||||
.font(.system(size: 13))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.leading, 20)
|
|
||||||
.padding(.top, 12)
|
|
||||||
ProgressView(value: subject.progress)
|
|
||||||
.padding(.top, 0)
|
|
||||||
.padding(.bottom, 12)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor static func display(_ subject: ProgressViewSubject) async -> NSWindowController {
|
|
||||||
let view = ProgressWindowView(subject: subject)
|
|
||||||
|
|
||||||
let window = NSWindow(
|
|
||||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 240),
|
|
||||||
styleMask: [.titled, .closable, .utilityWindow],
|
|
||||||
backing: .buffered,
|
|
||||||
defer: false
|
|
||||||
)
|
|
||||||
|
|
||||||
window.title = ""
|
|
||||||
window.titlebarAppearsTransparent = true
|
|
||||||
window.contentView = NSHostingView(rootView: view)
|
|
||||||
let controller = NSWindowController(window: window)
|
|
||||||
controller.showWindow(nil)
|
|
||||||
controller.positionWindowInTopLeftCorner()
|
|
||||||
controller.window?.makeKeyAndOrderFront(self)
|
|
||||||
// NSApp.activate(ignoringOtherApps: true)
|
|
||||||
return controller
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ProgressWindowView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
ProgressWindowView(
|
|
||||||
subject: ProgressViewSubject(
|
|
||||||
title: "Long running task",
|
|
||||||
description: "Please be patient"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +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="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
<deployment identifier="macosx"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
@ -20,7 +20,7 @@ class TerminalProgressWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
|
|
||||||
windowController.showWindow(windowController)
|
windowController.showWindow(windowController)
|
||||||
windowController.window?.makeKeyAndOrderFront(nil)
|
windowController.window?.makeKeyAndOrderFront(nil)
|
||||||
windowController.positionWindowInTopLeftCorner()
|
windowController.positionWindowInTopRightCorner()
|
||||||
|
|
||||||
windowController.progressView?.labelTitle.stringValue = title
|
windowController.progressView?.labelTitle.stringValue = title
|
||||||
windowController.progressView?.labelDescription.stringValue = description
|
windowController.progressView?.labelDescription.stringValue = description
|
@ -17,13 +17,13 @@ extension App {
|
|||||||
onChange: { Task { await self.onHomebrewPhpModification() } }
|
onChange: { Task { await self.onHomebrewPhpModification() } }
|
||||||
)
|
)
|
||||||
|
|
||||||
App.shared.watchers[.homebrewBinaries] = notifier
|
App.shared.watchers["homebrewBinaries"] = notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
public func destroyHomebrewWatchers() {
|
public func destroyHomebrewWatchers() {
|
||||||
// Removing requires termination and then removing reference
|
// Removing requires termination and then removing reference
|
||||||
self.watchers[.homebrewBinaries]?.terminate()
|
self.watchers["homebrewBinaries"]?.terminate()
|
||||||
self.watchers[.homebrewBinaries] = nil
|
self.watchers["homebrewBinaries"] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func onHomebrewPhpModification() async {
|
public func onHomebrewPhpModification() async {
|
||||||
@ -31,10 +31,13 @@ extension App {
|
|||||||
Log.info("Something changed in the Homebrew binary directory...")
|
Log.info("Something changed in the Homebrew binary directory...")
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
await MainMenu.shared.refreshActiveInstallation()
|
await MainMenu.shared.refreshActiveInstallation()
|
||||||
// let new = PhpEnvironments.shared.currentInstall?.version.text
|
|
||||||
|
|
||||||
// TODO:
|
//
|
||||||
// Check if the new and previous version are different
|
// TODO: PHP Guard 2.0
|
||||||
// if so, we can show a notification if needed
|
// Check if the new and previous version of PHP are different
|
||||||
|
// if so, we can show a notification if needed or alert the user
|
||||||
|
//
|
||||||
|
// let new = PhpEnvironments.shared.currentInstall?.version.text
|
||||||
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,52 +10,52 @@ import Foundation
|
|||||||
|
|
||||||
extension App {
|
extension App {
|
||||||
|
|
||||||
func startWatcher(_ url: URL) {
|
func startWatchManager(_ url: URL) {
|
||||||
Log.perf("No watcher currently active...")
|
Log.perf("Starting config watch manager...")
|
||||||
self.watcher = PhpConfigWatcher(for: url)
|
self.watchManager = ConfigWatchManager(for: url)
|
||||||
|
|
||||||
self.watcher.didChange = { url in
|
self.watchManager.didChange = { url in
|
||||||
Log.perf("Something has changed in: \(url)")
|
Log.perf("Something has changed in: \(url)")
|
||||||
|
|
||||||
// Check if the watcher has last updated the menu less than 0.75s ago
|
// Check if the watcher has last updated the menu less than 0.75s ago
|
||||||
let distance = self.watcher.lastUpdate?.distance(to: Date().timeIntervalSince1970)
|
let distance = self.watchManager.lastUpdate?.distance(to: Date().timeIntervalSince1970)
|
||||||
if distance == nil || distance != nil && distance! > 0.75 {
|
if distance == nil || distance != nil && distance! > 0.75 {
|
||||||
Log.perf("Refreshing menu...")
|
Log.perf("Refreshing menu...")
|
||||||
Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() }
|
Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() }
|
||||||
self.watcher.lastUpdate = Date().timeIntervalSince1970
|
self.watchManager.lastUpdate = Date().timeIntervalSince1970
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePhpConfigWatcher(forceReload: Bool = false) {
|
func handlePhpConfigWatcher(forceReload: Bool = false) {
|
||||||
if ActiveFileSystem.shared is TestableFileSystem {
|
if ActiveFileSystem.shared is TestableFileSystem {
|
||||||
Log.warn("FS watcher is disabled when using testable filesystem.")
|
Log.warn("Config watch manager is disabled when using testable filesystem.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("It appears as if no PHP installation is currently active.")
|
Log.info("It appears as if no PHP installation is currently active.")
|
||||||
Log.info("The FS watcher will be disabled until a PHP install is active.")
|
Log.info("The config watch manager be disabled until a PHP install is active.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(install.version.short)")
|
let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(install.version.short)")
|
||||||
|
|
||||||
// Check whether the watcher exists and schedule on the main thread
|
// Check whether the manager exists and schedule on the main thread
|
||||||
// if we don't consistently do this, the app will create duplicate watchers
|
// if we don't consistently do this, the app will create duplicate watchers
|
||||||
// due to timing issues, which creates retain cycles.
|
// due to timing issues, which creates retain cycles
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
// Watcher needs to be created
|
// Watcher needs to be created
|
||||||
if self.watcher == nil {
|
if self.watchManager == nil {
|
||||||
self.startWatcher(url)
|
self.startWatchManager(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watcher needs to be updated
|
// Watcher needs to be updated
|
||||||
if self.watcher.url != url || forceReload {
|
if self.watchManager.url != url || forceReload {
|
||||||
self.watcher.disable()
|
self.watchManager.disable()
|
||||||
self.watcher = nil
|
self.watchManager = nil
|
||||||
Log.perf("Watcher has stopped watching files. Starting new one...")
|
Log.perf("Watcher has stopped watching files. Starting new one...")
|
||||||
self.startWatcher(url)
|
self.startWatchManager(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
76
phpmon/Domain/Watcher/ConfigFSNotifier.swift
Normal file
76
phpmon/Domain/Watcher/ConfigFSNotifier.swift
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//
|
||||||
|
// ConfigFSNotifier.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 24/10/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ConfigFSNotifier {
|
||||||
|
|
||||||
|
enum Behaviour {
|
||||||
|
case reloadsMenu
|
||||||
|
case reloadsWatchers
|
||||||
|
}
|
||||||
|
|
||||||
|
private var parent: ConfigWatchManager!
|
||||||
|
|
||||||
|
private var monitoredFolderFileDescriptor: CInt = -1
|
||||||
|
|
||||||
|
private var folderMonitorSource: DispatchSourceFileSystemObject?
|
||||||
|
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
init(
|
||||||
|
for url: URL,
|
||||||
|
eventMask: DispatchSource.FileSystemEvent,
|
||||||
|
parent: ConfigWatchManager,
|
||||||
|
behaviour: ConfigFSNotifier.Behaviour = .reloadsMenu
|
||||||
|
) {
|
||||||
|
self.url = url
|
||||||
|
self.parent = parent
|
||||||
|
self.startMonitoring(eventMask, behaviour: behaviour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMonitoring(
|
||||||
|
_ eventMask: DispatchSource.FileSystemEvent,
|
||||||
|
behaviour: ConfigFSNotifier.Behaviour
|
||||||
|
) {
|
||||||
|
guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoredFolderFileDescriptor = open(url.path, O_EVTONLY)
|
||||||
|
folderMonitorSource = DispatchSource.makeFileSystemObjectSource(
|
||||||
|
fileDescriptor: monitoredFolderFileDescriptor,
|
||||||
|
eventMask: eventMask,
|
||||||
|
queue: parent.folderMonitorQueue
|
||||||
|
)
|
||||||
|
|
||||||
|
folderMonitorSource?.setEventHandler { [weak self] in
|
||||||
|
if behaviour == .reloadsWatchers
|
||||||
|
&& !ConfigWatchManager.ignoresModificationsToConfigValues {
|
||||||
|
// Reload all configuration watchers
|
||||||
|
return App.shared.handlePhpConfigWatcher(forceReload: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.parent.didChange?(self!.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
folderMonitorSource?.setCancelHandler { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
close(self.monitoredFolderFileDescriptor)
|
||||||
|
self.monitoredFolderFileDescriptor = -1
|
||||||
|
self.folderMonitorSource = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
folderMonitorSource?.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopMonitoring() {
|
||||||
|
folderMonitorSource?.cancel()
|
||||||
|
self.parent = nil
|
||||||
|
}
|
||||||
|
}
|
82
phpmon/Domain/Watcher/ConfigWatchManager.swift
Normal file
82
phpmon/Domain/Watcher/ConfigWatchManager.swift
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
//
|
||||||
|
// ConfigWatchManager.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 30/03/2021.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ConfigWatchManager {
|
||||||
|
|
||||||
|
static var ignoresModificationsToConfigValues: Bool = false
|
||||||
|
|
||||||
|
let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent)
|
||||||
|
|
||||||
|
let url: URL
|
||||||
|
var didChange: ((URL) -> Void)?
|
||||||
|
var lastUpdate: TimeInterval?
|
||||||
|
|
||||||
|
var watchers: [ConfigFSNotifier] = []
|
||||||
|
|
||||||
|
init(for url: URL) {
|
||||||
|
if FileSystem is TestableFileSystem {
|
||||||
|
fatalError("""
|
||||||
|
ConfigWatchManager is currently incompatible with a testable filesystem!"
|
||||||
|
You are not allowed to instantiate these while using a testable filesystem.
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
// Add a watcher for php.ini
|
||||||
|
self.addWatcher(for: self.url.appendingPathComponent("php.ini"), eventMask: .write)
|
||||||
|
|
||||||
|
// Add a watcher for conf.d (in case a new file is added or a file is deleted)
|
||||||
|
// This watcher, when triggered, will restart all watchers
|
||||||
|
self.addWatcher(for: self.url.appendingPathComponent("conf.d"), eventMask: .all, behaviour: .reloadsWatchers)
|
||||||
|
|
||||||
|
// Scan the conf.d folder for .ini files, and add a watcher for each file
|
||||||
|
let filePaths = FileManager.default.enumerator(
|
||||||
|
atPath: self.url.appendingPathComponent("conf.d").path
|
||||||
|
)?.allObjects as! [String]
|
||||||
|
|
||||||
|
// Loop over the .ini files that we discovered
|
||||||
|
filePaths.filter { $0.contains(".ini") }.forEach { (file) in
|
||||||
|
// Add a watcher for each file we have discovered
|
||||||
|
self.addWatcher(for: self.url.appendingPathComponent("conf.d/\(file)"), eventMask: .write)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.perf("A watcher exists for the following config paths:")
|
||||||
|
Log.perf(self.watchers.map({ watcher in
|
||||||
|
return watcher.url.relativePath
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addWatcher(
|
||||||
|
for url: URL,
|
||||||
|
eventMask: DispatchSource.FileSystemEvent,
|
||||||
|
behaviour: ConfigFSNotifier.Behaviour = .reloadsMenu
|
||||||
|
) {
|
||||||
|
if !FileSystem.anyExists(url.path) {
|
||||||
|
Log.warn("No watcher was created for \(url.path) because the requested file does not exist.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let watcher = ConfigFSNotifier(for: url, eventMask: eventMask, parent: self, behaviour: behaviour)
|
||||||
|
self.watchers.append(watcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
func disable() {
|
||||||
|
Log.perf("Turning off all individual existing watchers...")
|
||||||
|
self.watchers.forEach { (watcher) in
|
||||||
|
watcher.stopMonitoring()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,12 +6,9 @@
|
|||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Foundation
|
||||||
|
|
||||||
class FSNotifier {
|
class FSNotifier {
|
||||||
enum Kind {
|
|
||||||
case homebrewLocks, homebrewBinaries
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var shared: FSNotifier! = nil
|
public static var shared: FSNotifier! = nil
|
||||||
|
|
||||||
@ -66,4 +63,5 @@ class FSNotifier {
|
|||||||
deinit {
|
deinit {
|
||||||
Log.perf("FSNotifier for \(self.url) will be deinitialized.")
|
Log.perf("FSNotifier for \(self.url) will be deinitialized.")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user