mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-08 04:20:07 +02:00
Compare commits
147 Commits
Author | SHA1 | Date | |
---|---|---|---|
70c5aadb7f | |||
a731f15cf7 | |||
ab4c436202 | |||
c0231690d4 | |||
988e9d3351 | |||
2f119d4332 | |||
d83c629a7b | |||
e7d98dbeae | |||
f3d5946743 | |||
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 | |||
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 | |||
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 | |||
6fc613ac4c | |||
8240b676c1 | |||
cbebf75b48 | |||
40c24793f5 | |||
6a921d8e3e | |||
a3368effec | |||
7f4c6878e4 | |||
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"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
59
README.md
59
README.md
@ -3,7 +3,7 @@
|
||||
|
||||
<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)"/>
|
||||
|
||||
@ -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.
|
||||
|
||||
* 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)
|
||||
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
|
||||
* macOS 12.4 or later (Monterey, Ventura and Sonoma are supported)
|
||||
* Homebrew is installed in the default location (`/usr/local/homebrew` or `/opt/homebrew`)
|
||||
* 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**._
|
||||
|
||||
@ -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.
|
||||
|
||||
## 🐘 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?!
|
||||
|
||||
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**
|
||||
> 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.
|
||||
</details>
|
||||
@ -111,11 +120,11 @@ For maximum compatibility with older PHP versions, you may wish to keep using Va
|
||||
<details>
|
||||
<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.
|
||||
|
||||
@ -134,6 +143,14 @@ If you are on an older version of macOS, you can do this by dragging *PHP Monito
|
||||
Super convenient!
|
||||
</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>
|
||||
<summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary>
|
||||
|
||||
@ -179,6 +196,10 @@ Make sure PHP is linked correctly:
|
||||
|
||||
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
|
||||
|
||||
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!):
|
||||
@ -206,11 +227,6 @@ This should install `dnsmasq` and set up Valet. Great, almost there!
|
||||
|
||||
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.
|
||||
</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.
|
||||
|
||||
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>
|
||||
<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.
|
||||
|
||||
Here's what I usually do:
|
||||
@ -310,12 +330,14 @@ Make sure you have at least **Valet 3.0** installed, since support for isolation
|
||||
<details>
|
||||
<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).
|
||||
|
||||
**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>
|
||||
@ -404,6 +426,9 @@ You can omit the `php` key in the preset if you do not wish for the preset to sw
|
||||
<details>
|
||||
<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 can specify custom services in the configuration file for Homebrew services that run as your own user (not root).
|
||||
@ -584,9 +609,9 @@ Thank you very much for your contributions, kind words and support.
|
||||
|
||||
### Loading info about PHP in the background
|
||||
|
||||
This utility runs `php-config --version` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
|
||||
This app runs `php-config --version` in the background periodically, usually whenever your Homebrew configuration is modified. A filesystem watcher is used to determine if anything changes in your Homebrew's `bin` directory.
|
||||
|
||||
In order to save power, this only happens once every 60 seconds.
|
||||
PHP Monitor also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit). See also the section on *Config change detection* below.
|
||||
|
||||
### Switching PHP versions
|
||||
|
||||
@ -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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -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 |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 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
|
||||
|
||||
@ -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 |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 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.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 |
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 627 KiB After Width: | Height: | Size: 674 KiB |
@ -14,8 +14,6 @@ class Actions {
|
||||
|
||||
public static func linkPhp() async {
|
||||
await brew("link php --overwrite --force")
|
||||
|
||||
// TODO: Verify that this worked, if not, notify the user
|
||||
}
|
||||
|
||||
public static func restartPhpFpm() async {
|
||||
|
@ -18,6 +18,42 @@ struct Constants {
|
||||
*/
|
||||
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"
|
||||
|
||||
/**
|
||||
* 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 PHP versions supported by this application.
|
||||
* Any other PHP versions are considered invalid.
|
||||
@ -25,7 +61,8 @@ struct Constants {
|
||||
static let DetectedPhpVersions: Set = [
|
||||
"5.6",
|
||||
"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"
|
||||
]
|
||||
|
||||
/**
|
||||
@ -42,13 +79,13 @@ struct Constants {
|
||||
[
|
||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2",
|
||||
"8.3" // dev
|
||||
"8.3", "8.4" // dev
|
||||
],
|
||||
4: // Valet v4 dropped support for v7.0
|
||||
[
|
||||
"7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2",
|
||||
"8.3" // dev
|
||||
"8.3", "8.4" // dev
|
||||
]
|
||||
]
|
||||
|
||||
@ -82,6 +119,8 @@ struct Constants {
|
||||
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
||||
)!
|
||||
|
||||
// EAP URLs
|
||||
|
||||
static let EarlyAccessCaskFile = URL(
|
||||
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
||||
)!
|
||||
|
@ -17,6 +17,7 @@ public class Paths {
|
||||
|
||||
internal var baseDir: Paths.HomebrewDir
|
||||
private var userName: String
|
||||
private var preferredShell: String
|
||||
|
||||
init() {
|
||||
// Assume the default directory is correct
|
||||
@ -31,7 +32,12 @@ public class Paths {
|
||||
}
|
||||
|
||||
userName = identity()
|
||||
preferredShell = preferred_shell()
|
||||
|
||||
if !isRunningSwiftUIPreview {
|
||||
Log.info("The current username is `\(userName)`.")
|
||||
Log.info("The user's shell is `\(preferredShell)`.")
|
||||
}
|
||||
}
|
||||
|
||||
public func detectBinaryPaths() {
|
||||
@ -96,11 +102,19 @@ public class Paths {
|
||||
return "\(shared.baseDir.rawValue)/etc"
|
||||
}
|
||||
|
||||
public static var tapPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Library/Taps"
|
||||
}
|
||||
|
||||
public static var caskroomPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||
}
|
||||
|
||||
public static var shell: String {
|
||||
return shared.preferredShell
|
||||
}
|
||||
|
||||
// MARK: - Flexible Binaries
|
||||
// (these can be in multiple locations, so we scan common places because)
|
||||
// (PHP Monitor will not use the user's own PATH)
|
||||
|
@ -15,4 +15,10 @@ extension Date {
|
||||
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
|
||||
|
||||
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 = {
|
||||
if !isRunningTests {
|
||||
return Bundle.main
|
||||
@ -32,13 +44,30 @@ struct Localization {
|
||||
|
||||
extension String {
|
||||
var localized: String {
|
||||
if #available(macOS 13, *) {
|
||||
return NSLocalizedString(
|
||||
self, tableName: nil, bundle: Localization.bundle, value: "", comment: ""
|
||||
).replacingOccurrences(of: "Preferences", with: "Settings")
|
||||
var preferredBundle: Bundle = Localization.bundle
|
||||
|
||||
if let preferred = Localization.preferredLanguage,
|
||||
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
|
||||
let bundle = Bundle(path: path) {
|
||||
preferredBundle = bundle
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -131,4 +160,10 @@ extension String {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var isNumber: Bool {
|
||||
return self.range(
|
||||
of: "^[0-9]*$", // 1
|
||||
options: .regularExpression) != nil
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
|
||||
// MARK: — FS Attributes
|
||||
|
||||
func makeExecutable(_ path: String) throws {
|
||||
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
}
|
||||
|
||||
// MARK: - Checks
|
||||
|
@ -75,7 +75,7 @@ class MenuBarImageGenerator {
|
||||
|
||||
// Then we'll fetch the image we want on the left
|
||||
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")
|
||||
iconType = MenuBarIcon.iconPhp.rawValue
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
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 window = self.window else { return }
|
||||
|
||||
|
@ -10,7 +10,6 @@ import Foundation
|
||||
|
||||
/**
|
||||
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 {
|
||||
let task = Process()
|
||||
@ -65,3 +64,11 @@ public func identity() -> String {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
limits = Limits(
|
||||
memory_limit: getByteCount(key: "memory_limit"),
|
||||
@ -76,14 +69,9 @@ class ActivePhpInstallation {
|
||||
post_max_size: getByteCount(key: "post_max_size")
|
||||
)
|
||||
|
||||
// Return a list of .ini files parsed after php.ini
|
||||
let paths = Command.execute(
|
||||
path: Paths.php,
|
||||
arguments: ["-r", "echo php_ini_scanned_files();"],
|
||||
trimNewlines: false
|
||||
)
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.split(separator: ",")
|
||||
let paths = ActiveShell.shared
|
||||
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||
.split(separator: "\n")
|
||||
.map { String($0) }
|
||||
|
||||
// See if any extensions are present in said .ini files
|
||||
|
@ -13,7 +13,7 @@ class PhpEnvironments {
|
||||
// MARK: - Initializer
|
||||
|
||||
/**
|
||||
|
||||
Loads the currently active PHP installation upon startup. May be empty.
|
||||
*/
|
||||
init() {
|
||||
self.currentInstall = ActivePhpInstallation.load()
|
||||
@ -29,7 +29,7 @@ class PhpEnvironments {
|
||||
/**
|
||||
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
|
||||
|
||||
self.homebrewPackage = try! JSONDecoder().decode(
|
||||
@ -37,7 +37,27 @@ class PhpEnvironments {
|
||||
from: brewPhpAlias.data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!")
|
||||
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).")
|
||||
|
||||
// Check if that version actually corresponds to an older version
|
||||
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
|
||||
@ -49,14 +69,12 @@ class PhpEnvironments {
|
||||
static let shared = PhpEnvironments()
|
||||
|
||||
/** Whether the switcher is busy performing any actions. */
|
||||
var isBusy: Bool = false {
|
||||
@MainActor var isBusy: Bool = false {
|
||||
didSet {
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.setBusyImage()
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** All versions of PHP that are currently supported. */
|
||||
var availablePhpVersions: [String] = []
|
||||
@ -68,7 +86,14 @@ class PhpEnvironments {
|
||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||
|
||||
/** 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.
|
||||
@ -79,7 +104,12 @@ class PhpEnvironments {
|
||||
|
||||
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" }
|
||||
|
||||
return PhpEnvironments.shared.homebrewPackage.version
|
||||
@ -146,8 +176,13 @@ class PhpEnvironments {
|
||||
|
||||
// Avoid inserting a duplicate
|
||||
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||
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)
|
||||
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
|
||||
|
@ -49,8 +49,10 @@ class PhpHelper {
|
||||
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
|
||||
.resolvingSymlinksInPath().path
|
||||
|
||||
// The contents of the script!
|
||||
let script = script(path, keyPhrase, version, dotless)
|
||||
// Check if the user uses Fish
|
||||
let script = Paths.shell.contains("/fish")
|
||||
? fishScript(path, keyPhrase, version, dotless)
|
||||
: zshScript(path, keyPhrase, version, dotless)
|
||||
|
||||
Task { @MainActor in
|
||||
try FileSystem.writeAtomicallyToFile(destination, content: script)
|
||||
@ -78,7 +80,7 @@ class PhpHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static func script(
|
||||
private static func zshScript(
|
||||
_ path: String,
|
||||
_ keyPhrase: 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 {
|
||||
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
||||
let destination = "/usr/local/bin/pm\(dotless)"
|
||||
|
@ -69,8 +69,9 @@ class PhpConfigurationFile: CreatedFromFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
enum ReplacementErrors: Error {
|
||||
public enum ReplacementErrors: Error {
|
||||
case missingKey
|
||||
case missingFile
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,10 +96,16 @@ class PhpConfigurationFile: CreatedFromFile {
|
||||
// Replace the specific line
|
||||
self.lines[item.lineIndex] = components.joined(separator: "=")
|
||||
|
||||
// Ensure the watchers aren't tripped up by config changes
|
||||
ConfigWatchManager.ignoresModificationsToConfigValues = true
|
||||
|
||||
// Finally, join the string and save the file atomatically again
|
||||
try self.lines.joined(separator: "\n")
|
||||
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
|
||||
|
||||
// Ensure watcher behaviour is reverted
|
||||
ConfigWatchManager.ignoresModificationsToConfigValues = false
|
||||
|
||||
// Reload the original file
|
||||
self.reload()
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class PhpExtension {
|
||||
|
||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||
|
||||
self.enabled = !line.contains(";")
|
||||
self.enabled = !line.starts(with: ";")
|
||||
self.file = file
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ class PhpExtension {
|
||||
You may need to restart the other services in order for this change to apply.
|
||||
*/
|
||||
func toggle() async {
|
||||
let newLine = enabled
|
||||
let newLine = !line.starts(with: ";")
|
||||
// DISABLED: Commented out line
|
||||
? "; \(line)"
|
||||
// ENABLED: Line where the comment delimiter (;) is removed
|
||||
@ -84,14 +84,14 @@ class PhpExtension {
|
||||
|
||||
await sed(file: file, original: line, replacement: newLine)
|
||||
|
||||
enabled.toggle()
|
||||
self.enabled = !newLine.starts(with: ";")
|
||||
self.line = newLine
|
||||
|
||||
if !isRunningTests {
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Static Methods
|
||||
|
@ -12,19 +12,36 @@ class PhpInstallation {
|
||||
|
||||
var versionNumber: VersionNumber
|
||||
|
||||
var iniFiles: [PhpConfigurationFile] = []
|
||||
|
||||
var isMissingBinary: Bool = false
|
||||
|
||||
var isHealthy: Bool = true
|
||||
|
||||
var extensions: [PhpExtension] {
|
||||
return self.iniFiles.flatMap({ $0.extensions })
|
||||
}
|
||||
|
||||
/**
|
||||
In order to determine details about a PHP installation,
|
||||
we’ll simply run `php-config --version` in the relevant directory.
|
||||
*/
|
||||
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) {
|
||||
let longVersionString = Command.execute(
|
||||
path: phpConfigExecutablePath,
|
||||
@ -34,9 +51,15 @@ class PhpInstallation {
|
||||
|
||||
// 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.
|
||||
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) {
|
||||
let testCommand = Command.execute(
|
||||
path: phpExecutablePath,
|
||||
@ -53,4 +76,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)
|
||||
}
|
||||
|
||||
// MARK: - PHP FPM pool
|
||||
// MARK: - Corrections
|
||||
|
||||
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||
@ -54,37 +54,7 @@ extension InternalSwitcher {
|
||||
return false
|
||||
}
|
||||
|
||||
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 }
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||
public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||
let files = self.getExpectedConfigurationFiles(for: version)
|
||||
|
||||
// For each of the files, attempt to fix anything that is wrong
|
||||
@ -124,6 +94,38 @@ extension InternalSwitcher {
|
||||
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 {
|
||||
|
@ -86,14 +86,37 @@ class RealShell: ShellProtocol {
|
||||
|
||||
// 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 {
|
||||
let task = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = 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 {
|
||||
Log.info("[SLOW SHELL] \(command)")
|
||||
await delay(seconds: 3.0)
|
||||
@ -104,17 +127,17 @@ class RealShell: ShellProtocol {
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
let stdOut = String(
|
||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!
|
||||
|
||||
let stdErr = String(
|
||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!
|
||||
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)
|
||||
}
|
||||
|
||||
private func log(task: Process, stdOut: String, stdErr: String) {
|
||||
var args = task.arguments ?? []
|
||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||
var log = """
|
||||
@ -141,9 +164,6 @@ class RealShell: ShellProtocol {
|
||||
Log.info(log)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
}
|
||||
|
||||
func quiet(_ command: String) async {
|
||||
_ = await self.pipe(command)
|
||||
}
|
||||
|
@ -14,6 +14,16 @@ protocol ShellProtocol {
|
||||
*/
|
||||
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.
|
||||
Returns the most relevant output (prefers error output if it exists).
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// PhpFormulaeStatus.swift
|
||||
// BusyStatus.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 02/05/2023.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpFormulaeStatus: ObservableObject {
|
||||
class BusyStatus: ObservableObject {
|
||||
@Published var busy: Bool
|
||||
@Published var title: String
|
||||
@Published var description: String
|
||||
@ -18,4 +18,12 @@ class PhpFormulaeStatus: ObservableObject {
|
||||
self.title = title
|
||||
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 secondaryPhpVersions: [VersionNumber] = []
|
||||
|
||||
// swiftlint:disable function_body_length
|
||||
mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) {
|
||||
if primary {
|
||||
if primaryPhpVersion != nil {
|
||||
@ -56,6 +57,8 @@ public struct TestableConfiguration: Codable {
|
||||
self.filesystem = self.filesystem.merging([
|
||||
"/opt/homebrew/opt/php@\(version.short)/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"
|
||||
: .fake(.binary),
|
||||
"/opt/homebrew/Cellar/php/\(version.long)/bin/php-config"
|
||||
@ -70,9 +73,26 @@ public struct TestableConfiguration: Codable {
|
||||
: .fake(.text)
|
||||
]) { (_, 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 {
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php"]
|
||||
= .instant("php")
|
||||
// Files expected to be present for currently linked PHP version
|
||||
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"]
|
||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
||||
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
||||
@ -80,14 +100,11 @@ public struct TestableConfiguration: Codable {
|
||||
self.filesystem["/opt/homebrew/bin/php"]
|
||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php")
|
||||
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"]
|
||||
= 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 {
|
||||
// Output expected to be present for non-linked PHP versions
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
||||
BatchFakeShellOutput.instant(
|
||||
self.secondaryPhpVersions
|
||||
@ -96,6 +113,7 @@ public struct TestableConfiguration: Codable {
|
||||
)
|
||||
}
|
||||
}
|
||||
// swiftlint:enable function_body_length
|
||||
|
||||
// MARK: Interactions
|
||||
|
||||
|
@ -19,6 +19,17 @@ public class TestableShell: ShellProtocol {
|
||||
|
||||
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 {
|
||||
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
||||
}
|
||||
@ -112,6 +123,29 @@ struct BatchFakeShellOutput: Codable {
|
||||
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.
|
||||
*/
|
||||
|
@ -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>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>
|
||||
<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>
|
||||
</html>
|
||||
|
||||
|
@ -46,9 +46,11 @@ extension App {
|
||||
}
|
||||
|
||||
hotkey.keyDownHandler = {
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -74,11 +74,17 @@ class App {
|
||||
/** The window controller of the onboarding window. */
|
||||
var onboardingWindowController: OnboardingWindowController?
|
||||
|
||||
/** The window controller of the warnings window. */
|
||||
var warningsWindowController: WarningsWindowController?
|
||||
/** The window controller of the config manager window. */
|
||||
var phpConfigManagerWindowController: PhpConfigManagerWindowController?
|
||||
|
||||
/** 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. */
|
||||
var detectedApplications: [Application] = []
|
||||
@ -86,9 +92,6 @@ class App {
|
||||
/** The warning manager, responsible for keeping track of warnings. */
|
||||
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. */
|
||||
var timer: Timer?
|
||||
|
||||
@ -117,8 +120,12 @@ class App {
|
||||
|
||||
// MARK: - App Watchers
|
||||
|
||||
/** Individual filesystem watchers, which are, i.e. responsible for watching the Homebrew folders. */
|
||||
var watchers: [String: FSNotifier] = [:]
|
||||
|
||||
/**
|
||||
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
||||
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
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
|
||||
static var instance: AppDelegate {
|
||||
return NSApplication.shared.delegate as! AppDelegate
|
||||
}
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
/**
|
||||
@ -84,10 +88,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
Log.info("Extra CLI mode is on (`~/.config/phpmon/verbose` exists).")
|
||||
}
|
||||
|
||||
if !isRunningSwiftUIPreview {
|
||||
Log.separator(as: .info)
|
||||
Log.info("PHP MONITOR by Nico Verbruggen")
|
||||
Log.info("Version \(App.version)")
|
||||
Log.separator(as: .info)
|
||||
}
|
||||
|
||||
self.state = App.shared
|
||||
self.menu = MainMenu.shared
|
||||
@ -103,6 +109,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
|
||||
static func initializeTestingProfile(_ path: String) {
|
||||
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()
|
||||
}
|
||||
|
||||
@ -114,6 +123,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
startup procedure.
|
||||
*/
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
// Prevent previews from kicking off a costly boot
|
||||
if isRunningSwiftUIPreview {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure notifications will work
|
||||
setupNotifications()
|
||||
|
||||
@ -121,4 +135,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
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"?>
|
||||
<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="22155" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22155"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
||||
@ -34,18 +34,6 @@
|
||||
</items>
|
||||
</menu>
|
||||
</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">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Sites" id="YTZ-bb-TOG">
|
||||
@ -82,12 +70,12 @@
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="8Pm-83-BlM">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="jCt-Yf-FSE">
|
||||
<menuItem title="Undo" enabled="NO" keyEquivalent="z" id="jCt-Yf-FSE">
|
||||
<connections>
|
||||
<action selector="undo:" target="Ady-hI-5gd" id="O3z-27-Ug0"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="fCh-1M-Qyg">
|
||||
<menuItem title="Redo" enabled="NO" keyEquivalent="Z" id="fCh-1M-Qyg">
|
||||
<connections>
|
||||
<action selector="redo:" target="Ady-hI-5gd" id="utE-Bv-fdY"/>
|
||||
</connections>
|
||||
@ -297,6 +285,18 @@
|
||||
</items>
|
||||
</menu>
|
||||
</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">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
@ -317,7 +317,11 @@
|
||||
</application>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<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>
|
||||
<point key="canvasLocation" x="-360" y="-94"/>
|
||||
</scene>
|
||||
@ -425,7 +429,7 @@
|
||||
</toolbarItem>
|
||||
<searchToolbarItem implicitItemIdentifier="7C834FBE-7118-4082-A09F-7CBECEC1356A" label="Search" paletteLabel="Search" visibilityPriority="1001" id="G2g-jS-RVc">
|
||||
<nil key="toolTip"/>
|
||||
<searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
|
||||
<searchField key="view" focusRingType="none" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="vp9-vH-goQ">
|
||||
@ -517,9 +521,6 @@
|
||||
<subviews>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8zu-cF-KCX">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -527,15 +528,15 @@
|
||||
DQ
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="4Uf-fh-jWJ"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="primaryButtonAction:" target="hkw-9V-NxP" id="W7d-3b-pZT"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TCp-nS-HN2">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -543,6 +544,9 @@ DQ
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="QWZ-BA-0g9"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="secondaryButtonAction:" target="hkw-9V-NxP" id="YJs-Hu-lFP"/>
|
||||
</connections>
|
||||
@ -571,7 +575,7 @@ Gw
|
||||
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="wIl-uw-y3p"/>
|
||||
</constraints>
|
||||
</visualEffectView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
|
||||
<rect key="frame" x="98" y="153" width="384" height="19"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="380" id="WgB-hj-d4P"/>
|
||||
@ -582,7 +586,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
|
||||
<rect key="frame" x="98" y="127" width="384" height="16"/>
|
||||
<textFieldCell key="cell" selectable="YES" title="This is a slightly more expanded explanation." id="rY3-Nd-Iit">
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -606,7 +610,7 @@ Gw
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="7eT-Hw-EL9"/>
|
||||
</imageView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
|
||||
<rect key="frame" x="98" y="70" width="384" height="42"/>
|
||||
<textFieldCell key="cell" selectable="YES" id="7iW-Lc-DqO">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -681,9 +685,6 @@ DQ
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -691,11 +692,14 @@ DQ
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
|
||||
<rect key="frame" x="20" y="150" width="440" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -706,7 +710,7 @@ Gw
|
||||
<outlet property="delegate" destination="glS-wF-sEU" id="Dyf-0M-Gwj"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
|
||||
<rect key="frame" x="18" y="128" width="444" height="14"/>
|
||||
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -724,7 +728,7 @@ Gw
|
||||
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
|
||||
<rect key="frame" x="18" y="60" width="444" height="28"/>
|
||||
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="4gd-KM-5Fu">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -739,7 +743,7 @@ Gw
|
||||
<url key="url" string="file:///Users/"/>
|
||||
</pathCell>
|
||||
</pathControl>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
|
||||
<rect key="frame" x="18" y="209" width="128" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
|
||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||
@ -747,7 +751,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
||||
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
||||
<rect key="frame" x="140" y="23" width="180" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -882,7 +886,7 @@ Gw
|
||||
<rect key="frame" x="69" y="0.0" width="200" height="54"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
||||
<rect key="frame" x="3" y="26" width="145" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
|
||||
<font key="font" metaFont="systemSemibold" size="13"/>
|
||||
@ -890,7 +894,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
|
||||
<rect key="frame" x="3" y="12" width="75" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -933,13 +937,13 @@ Gw
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="smallSystemBold"/>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="70" id="MBa-bB-DTB"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="pressedPhpVersion:" target="T49-0U-d58" id="jVO-TS-F6d"/>
|
||||
</connections>
|
||||
@ -969,7 +973,7 @@ Gw
|
||||
</tableCellView>
|
||||
</prototypeCellViews>
|
||||
</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">
|
||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<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"/>
|
||||
<prototypeCellViews>
|
||||
<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"/>
|
||||
<subviews>
|
||||
<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>
|
||||
<constraint firstAttribute="width" constant="18" id="XcB-uw-szU"/>
|
||||
<constraint firstAttribute="height" constant="18" id="bGN-Vh-Sh0"/>
|
||||
@ -1020,10 +1024,10 @@ Gw
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<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"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
||||
<rect key="frame" x="6" y="26" width="93" height="14"/>
|
||||
<textFieldCell key="cell" alignment="left" title="Laravel" id="0lu-L6-oKr">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -1031,7 +1035,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
|
||||
<rect key="frame" x="6" y="15" width="93" height="11"/>
|
||||
<textFieldCell key="cell" alignment="left" title="PHP 8.0" id="puf-Jh-ham">
|
||||
<font key="font" metaFont="miniSystem"/>
|
||||
@ -1121,7 +1125,7 @@ Gw
|
||||
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
|
||||
<rect key="frame" x="20" y="196" width="500" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -1132,7 +1136,7 @@ Gw
|
||||
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
|
||||
<rect key="frame" x="18" y="221" width="325" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Proxy subject (usually: protocol, IP address and port)" id="G1Z-3f-BhL">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
@ -1140,7 +1144,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
|
||||
<rect key="frame" x="18" y="172" width="112" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
@ -1148,7 +1152,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
|
||||
<rect key="frame" x="20" y="147" width="500" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -1190,9 +1194,6 @@ DQ
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -1200,11 +1201,14 @@ DQ
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="uCc-fF-wS2"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
|
||||
<rect key="frame" x="18" y="128" width="504" height="14"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="sF1-RG-URI"/>
|
||||
@ -1225,7 +1229,7 @@ Gw
|
||||
<action selector="pressedSecure:" target="dwh-CF-6iv" id="b74-8T-AzO"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
|
||||
<rect key="frame" x="18" y="60" width="504" height="28"/>
|
||||
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -1233,7 +1237,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
|
||||
<rect key="frame" x="18" y="250" width="123" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
|
||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||
@ -1241,7 +1245,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
|
||||
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
|
||||
<rect key="frame" x="191" y="23" width="180" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -1336,9 +1340,6 @@ Gw
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -1346,6 +1347,9 @@ Gw
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="Zhu-D8-cLK"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="pressedCancel:" target="gOD-Gu-zDG" id="wMp-sM-0A4"/>
|
||||
</connections>
|
||||
@ -1385,7 +1389,7 @@ Gw
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
|
||||
<rect key="frame" x="18" y="138" width="504" height="19"/>
|
||||
<textFieldCell key="cell" selectable="YES" alignment="left" title="[i18n] What kind of domain would you like to set up?" id="agk-Nj-FLd">
|
||||
<font key="font" metaFont="systemBold" size="15"/>
|
||||
@ -1393,7 +1397,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
|
||||
<textField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
|
||||
<rect key="frame" x="18" y="60" width="504" height="70"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="tbl-AV-4qB"/>
|
||||
|
@ -23,13 +23,13 @@ class InterApp {
|
||||
|
||||
@MainActor static func getCommands() -> [InterApp.Action] { return [
|
||||
InterApp.Action(command: "list", action: { _ in
|
||||
DomainListVC.show()
|
||||
if Valet.installed { DomainListVC.show() }
|
||||
}),
|
||||
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
|
||||
Task { MainMenu.shared.restartValetServices() }
|
||||
if Valet.installed { Task { MainMenu.shared.restartValetServices() } }
|
||||
}),
|
||||
InterApp.Action(command: "services/restart/nginx", action: { _ in
|
||||
Task { MainMenu.shared.restartNginx() }
|
||||
@ -47,7 +47,7 @@ class InterApp {
|
||||
Task { MainMenu.shared.openGlobalComposerFolder() }
|
||||
}),
|
||||
InterApp.Action(command: "locate/valet", action: { _ in
|
||||
Task { MainMenu.shared.openValetConfigFolder() }
|
||||
if Valet.installed { Task { MainMenu.shared.openValetConfigFolder() } }
|
||||
}),
|
||||
InterApp.Action(command: "phpinfo", action: { _ in
|
||||
Task { MainMenu.shared.openPhpInfo() }
|
||||
|
@ -46,22 +46,22 @@ class ServicesManager: ObservableObject {
|
||||
|
||||
public var statusMessage: String {
|
||||
if self.services.isEmpty || !self.firstRunComplete {
|
||||
return "Loading..."
|
||||
return "phpman.services.loading".localized
|
||||
}
|
||||
|
||||
let statuses = self.services[0...2].map { $0.status }
|
||||
|
||||
if statuses.contains(.missing) {
|
||||
return "A key service is not installed."
|
||||
return "phpman.services.not_installed".localized
|
||||
}
|
||||
if statuses.contains(.error) {
|
||||
return "A key service is reporting an error state."
|
||||
return "phpman.services.error".localized
|
||||
}
|
||||
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 {
|
||||
|
@ -34,6 +34,10 @@ class ValetServicesManager: ServicesManager {
|
||||
these two commands are executed concurrently.
|
||||
*/
|
||||
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
|
||||
// First, retrieve the status of the formulae that run as root
|
||||
group.addTask {
|
||||
|
@ -142,7 +142,7 @@ class Startup {
|
||||
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
||||
.contains("Library not loaded")
|
||||
},
|
||||
name: "`no dyld issue detected",
|
||||
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||
titleText: "startup.errors.dyld_library.title".localized,
|
||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||
Paths.optPath
|
||||
@ -241,6 +241,20 @@ class Startup {
|
||||
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)
|
||||
// =================================================================================
|
||||
EnvironmentCheck(
|
||||
|
@ -62,12 +62,13 @@ struct ComposerJson: Decodable {
|
||||
public func getNotableDependencies() -> [String: String] {
|
||||
var notable: [String: String] = [:]
|
||||
|
||||
var scan = Array(PhpFrameworks.DependencyList.keys)
|
||||
scan.append("php")
|
||||
let scan = Array(ProjectTypeDetection.CommonDependencyList.keys) +
|
||||
Array(ProjectTypeDetection.SpecificDependencyList.keys) +
|
||||
["php"]
|
||||
|
||||
scan.forEach { dependency in
|
||||
if dependencies?[dependency] != nil {
|
||||
notable[dependency] = dependencies![dependency]
|
||||
if let resolvedDependency = dependencies?[dependency] {
|
||||
notable[dependency] = resolvedDependency
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,8 +28,6 @@ import Foundation
|
||||
}
|
||||
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
MainMenu.shared.setBusyImage()
|
||||
MainMenu.shared.rebuild()
|
||||
|
||||
window = TerminalProgressWindowController.display(
|
||||
title: "alert.composer_progress.title".localized,
|
||||
@ -106,14 +104,11 @@ import Foundation
|
||||
|
||||
private func removeBusyStatus() {
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.updatePhpVersionInStatusBar()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Alert
|
||||
|
||||
@MainActor private func presentMissingAlert() {
|
||||
private func presentMissingAlert() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.composer_missing.title".localized,
|
||||
|
@ -8,20 +8,20 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PhpFrameworks {
|
||||
|
||||
struct ProjectTypeDetection {
|
||||
/**
|
||||
This list should probably be reversed when checked, because some of these
|
||||
will also require either `laravel/framework` or `symfony/symfony`.
|
||||
This list is only checked if the specific dependency list doesn't report a match.
|
||||
*/
|
||||
public static let DependencyList = [
|
||||
|
||||
// COMMON FRAMEWORKS
|
||||
public static let CommonDependencyList = [
|
||||
"laravel/framework": "Laravel",
|
||||
"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",
|
||||
"cakephp/app": "CakePHP",
|
||||
"craftcms/craft": "Craft",
|
||||
@ -37,30 +37,8 @@ struct PhpFrameworks {
|
||||
"johnpbloch/wordpress-core": "WordPress",
|
||||
"zendframework/zendframework": "Zend",
|
||||
"zendframework/zend-mvc": "Zend",
|
||||
"typo3/cms-core": "Typo3"
|
||||
// "magento/*": "Magento",
|
||||
// "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"
|
||||
]
|
||||
"typo3/cms-core": "Typo3",
|
||||
"slim/slim": "Slim"
|
||||
]
|
||||
|
||||
/**
|
||||
@ -82,4 +60,25 @@ struct PhpFrameworks {
|
||||
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"
|
||||
]
|
||||
]
|
||||
}
|
@ -61,17 +61,25 @@ class BrewPermissionFixer {
|
||||
? "php"
|
||||
: "php@\(formula)"
|
||||
|
||||
let binaryPath = "\(Paths.optPath)/\(realFormula)/bin"
|
||||
|
||||
if isOwnedByRoot(path: binaryPath) {
|
||||
let borked = DueOwnershipFormula(
|
||||
formula: realFormula,
|
||||
path: binaryPath
|
||||
)
|
||||
let binFolderOwned = isOwnedByRoot(path: "\(Paths.optPath)/\(realFormula)/bin")
|
||||
let sbinFolderOwned = isOwnedByRoot(path: "\(Paths.optPath)/\(realFormula)/sbin")
|
||||
|
||||
if binFolderOwned || sbinFolderOwned {
|
||||
Log.warn("\(formula) is owned by root")
|
||||
|
||||
broken.append(borked)
|
||||
if binFolderOwned {
|
||||
broken.append(DueOwnershipFormula(
|
||||
formula: realFormula,
|
||||
path: "\(Paths.optPath)/\(realFormula)/bin"
|
||||
))
|
||||
}
|
||||
|
||||
if sbinFolderOwned {
|
||||
broken.append(DueOwnershipFormula(
|
||||
formula: realFormula,
|
||||
path: "\(Paths.optPath)/\(realFormula)/sbin"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,16 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class BrewFormulaeObservable: ObservableObject {
|
||||
@Published var phpVersions: [BrewFormula] = []
|
||||
|
||||
var upgradeable: [BrewFormula] {
|
||||
return phpVersions.filter { formula in
|
||||
formula.hasUpgrade
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Brew {
|
||||
static let shared = Brew()
|
||||
|
||||
@ -45,9 +35,11 @@ class Brew {
|
||||
|
||||
/// Each formula for each PHP version that can be installed.
|
||||
public static let phpVersionFormulae = [
|
||||
"8.4": "shivammathur/php/php@8.4",
|
||||
"8.3": "php@8.3",
|
||||
"8.2": "php@8.2",
|
||||
"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.3": "shivammathur/php/php@7.3",
|
||||
"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.
|
||||
*/
|
||||
@ -46,6 +61,43 @@ class BrewDiagnostics {
|
||||
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.
|
||||
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
|
||||
//
|
||||
// Created by Nico Verbruggen on 17/03/2023.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct BrewFormula {
|
||||
struct BrewPhpFormula: Equatable {
|
||||
/// Name of the formula.
|
||||
let name: String
|
||||
|
||||
@ -21,16 +21,54 @@ struct BrewFormula {
|
||||
/// The upgrade that is currently available, if it exists.
|
||||
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.
|
||||
var isInstalled: Bool {
|
||||
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.
|
||||
var hasUpgrade: Bool {
|
||||
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.
|
||||
var homebrewFolder: String {
|
||||
let resolved = name
|
||||
@ -43,7 +81,7 @@ struct BrewFormula {
|
||||
/// The short version associated with this formula, if installed.
|
||||
var shortVersion: String? {
|
||||
guard let version = self.installedVersion else {
|
||||
return nil
|
||||
return self.displayName.replacingOccurrences(of: "PHP ", with: "")
|
||||
}
|
||||
|
||||
return VersionNumber.make(from: version)?.short ?? nil
|
||||
@ -64,6 +102,7 @@ struct BrewFormula {
|
||||
return nil
|
||||
}
|
||||
|
||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?.isHealthy ?? nil
|
||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?
|
||||
.isHealthy ?? nil
|
||||
}
|
||||
}
|
@ -8,22 +8,23 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol HandlesBrewFormulae {
|
||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula]
|
||||
protocol HandlesBrewPhpFormulae {
|
||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula]
|
||||
func refreshPhpVersions(loadOutdated: Bool) async
|
||||
}
|
||||
|
||||
extension HandlesBrewFormulae {
|
||||
extension HandlesBrewPhpFormulae {
|
||||
public func refreshPhpVersions(loadOutdated: Bool) async {
|
||||
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
||||
Task { @MainActor in
|
||||
await PhpEnvironments.shared.determinePhpAlias()
|
||||
Brew.shared.formulae.phpVersions = items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BrewFormulaeHandler: HandlesBrewFormulae {
|
||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] {
|
||||
class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
|
||||
var outdated: [OutdatedFormula]?
|
||||
|
||||
if loadOutdated {
|
||||
@ -43,7 +44,9 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
||||
}
|
||||
|
||||
return Brew.phpVersionFormulae.map { (version, formula) in
|
||||
let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?.versionNumber.text
|
||||
let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?
|
||||
.versionNumber.text
|
||||
|
||||
var upgradeVersion: String?
|
||||
|
||||
if let version = fullVersion {
|
||||
@ -52,11 +55,12 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
||||
})?.current_version
|
||||
}
|
||||
|
||||
return BrewFormula(
|
||||
return BrewPhpFormula(
|
||||
name: formula,
|
||||
displayName: "PHP \(version)",
|
||||
installedVersion: fullVersion,
|
||||
upgradeVersion: upgradeVersion
|
||||
upgradeVersion: upgradeVersion,
|
||||
prerelease: Constants.ExperimentalPhpVersions.contains(version)
|
||||
)
|
||||
}.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 {
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
||||
|
||||
func getCommandTitle() -> String
|
||||
}
|
||||
|
||||
extension BrewCommand {
|
||||
@ -31,6 +33,44 @@ extension BrewCommand {
|
||||
}
|
||||
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 {
|
||||
|
@ -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
|
||||
|
||||
class InstallAndUpgradeCommand: BrewCommand {
|
||||
|
||||
class ModifyPhpVersionCommand: BrewCommand {
|
||||
let title: String
|
||||
let installing: [BrewFormula]
|
||||
let upgrading: [BrewFormula]
|
||||
let installing: [BrewPhpFormula]
|
||||
let upgrading: [BrewPhpFormula]
|
||||
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.
|
||||
The process will be executed in two steps: first upgrades, then installations.
|
||||
|
||||
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(
|
||||
title: String,
|
||||
upgrading: [BrewFormula],
|
||||
installing: [BrewFormula]
|
||||
upgrading: [BrewPhpFormula],
|
||||
installing: [BrewPhpFormula]
|
||||
) {
|
||||
self.title = title
|
||||
self.installing = installing
|
||||
@ -33,9 +43,32 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
}
|
||||
|
||||
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
|
||||
))
|
||||
|
||||
// Determine if a formula will become unavailable
|
||||
// This is the case when `php` will be bumped to a new version
|
||||
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
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
@ -47,6 +80,27 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
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 {
|
||||
// If no upgrades are needed, early exit
|
||||
if self.upgrading.isEmpty {
|
||||
@ -109,35 +163,12 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
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 {
|
||||
// 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
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
@ -147,7 +178,7 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
|
||||
// If a PHP version was active prior to running the operations, attempt to restore it
|
||||
if let version = phpGuard.currentVersion {
|
||||
await MainMenu.shared.switchToAnyPhpVersion(version, silently: true)
|
||||
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
||||
}
|
||||
|
||||
// Also rebuild the content of the main menu
|
||||
@ -156,9 +187,8 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
// Let the UI know that the installation has been completed
|
||||
onProgress(.create(
|
||||
value: 1,
|
||||
title: "Operation completed!",
|
||||
description: "The installation has succeeded."
|
||||
title: "phpman.steps.completed".localized,
|
||||
description: "phpman.steps.success".localized
|
||||
))
|
||||
}
|
||||
|
||||
}
|
@ -11,21 +11,25 @@ import Foundation
|
||||
class RemovePhpVersionCommand: BrewCommand {
|
||||
let formula: String
|
||||
let version: String
|
||||
let phpGuard: PhpGuard
|
||||
|
||||
init(formula: String) {
|
||||
self.version = formula
|
||||
.replacingOccurrences(of: "php@", with: "")
|
||||
.replacingOccurrences(of: "shivammathur/php/", with: "")
|
||||
self.formula = formula
|
||||
self.phpGuard = PhpGuard()
|
||||
}
|
||||
|
||||
func getCommandTitle() -> String {
|
||||
return "phpman.steps.removing".localized("PHP \(version)...")
|
||||
}
|
||||
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
let progressTitle = "Removing PHP \(version)..."
|
||||
|
||||
onProgress(.create(
|
||||
value: 0.2,
|
||||
title: progressTitle,
|
||||
description: "Please wait while Homebrew removes PHP \(version)..."
|
||||
title: getCommandTitle(),
|
||||
description: "phpman.steps.wait".localized
|
||||
))
|
||||
|
||||
let command = """
|
||||
@ -54,10 +58,17 @@ class RemovePhpVersionCommand: BrewCommand {
|
||||
)
|
||||
|
||||
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 MainMenu.shared.refreshActiveInstallation()
|
||||
onProgress(.create(value: 1, title: progressTitle, description: "The operation has succeeded."))
|
||||
|
||||
if let version = phpGuard.currentVersion {
|
||||
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
||||
}
|
||||
|
||||
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||
} else {
|
||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||
}
|
@ -9,6 +9,10 @@
|
||||
import Foundation
|
||||
|
||||
class FakeCommand: BrewCommand {
|
||||
func getCommandTitle() -> String {
|
||||
return "Hello"
|
||||
}
|
||||
|
||||
let version: String
|
||||
|
||||
init(version: String) {
|
||||
|
@ -141,7 +141,7 @@ class ValetSite: ValetListable {
|
||||
self.determineDriverViaComposer()
|
||||
|
||||
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() {
|
||||
self.driverDeterminedByComposer = true
|
||||
|
||||
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
|
||||
if self.notableComposerDependencies.keys.contains(key) {
|
||||
for (key, value) in ProjectTypeDetection.SpecificDependencyList
|
||||
where notableComposerDependencies.keys.contains(key) {
|
||||
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,
|
||||
let site = ValetScanner.active.resolveSite(path: defaultPath) {
|
||||
sites.insert(site, at: 0)
|
||||
let defaultSite = ValetScanner.active.resolveSite(path: defaultPath) {
|
||||
// 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.")
|
||||
|
@ -273,14 +273,36 @@ extension MainMenu {
|
||||
}
|
||||
}
|
||||
|
||||
func switchToPhpVersionAndWait(_ version: String, silently: Bool = false) async {
|
||||
if silently {
|
||||
MainMenu.shared.shouldSwitchSilently = true
|
||||
}
|
||||
|
||||
if !PhpEnvironments.shared.availablePhpVersions.contains(version) {
|
||||
Log.warn("This PHP version is currently unavailable, not switching!")
|
||||
return
|
||||
}
|
||||
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
PhpEnvironments.shared.delegate = self
|
||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||
|
||||
PhpEnvironments.shared.currentInstall = ActivePhpInstallation()
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
PhpEnvironments.shared.delegate?.switcherDidCompleteSwitch(to: version)
|
||||
}
|
||||
|
||||
@objc func switchToPhpVersion(_ version: String) {
|
||||
setBusyImage()
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
PhpEnvironments.shared.delegate = self
|
||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
|
||||
Task(priority: .userInitiated) { [unowned self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||
|
||||
@ -301,13 +323,12 @@ extension MainMenu {
|
||||
*/
|
||||
func switchToPhp(_ version: String) async {
|
||||
Task { @MainActor [self] in
|
||||
setBusyImage()
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
PhpEnvironments.shared.delegate = self
|
||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
}
|
||||
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||
|
||||
|
@ -45,21 +45,16 @@ extension MainMenu {
|
||||
.broadcastServicesUpdate
|
||||
]
|
||||
) {
|
||||
if behaviours.contains(.reloadsPhpInstallation) {
|
||||
if behaviours.contains(.reloadsPhpInstallation) || behaviours.contains(.setsBusyUI) {
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
}
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
setBusyImage()
|
||||
}
|
||||
|
||||
Task(priority: .userInitiated) { [unowned self] in
|
||||
var error: Error?
|
||||
|
||||
do { try execute() } catch let e { error = e }
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
do { try execute() } catch let e {
|
||||
error = e
|
||||
Log.err(e)
|
||||
}
|
||||
|
||||
Task { @MainActor [self, error] in
|
||||
@ -68,15 +63,18 @@ extension MainMenu {
|
||||
}
|
||||
|
||||
if behaviours.contains(.updatesMenuBarContents) {
|
||||
updatePhpVersionInStatusBar()
|
||||
} else if behaviours.contains(.setsBusyUI) {
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
}
|
||||
|
||||
if behaviours.contains(.broadcastServicesUpdate) {
|
||||
Task { await ServicesManager.shared.reloadServicesStatus() }
|
||||
}
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
}
|
||||
|
||||
if error != nil {
|
||||
return failure(error!)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ extension MainMenu {
|
||||
func startup() async {
|
||||
// Start with the icon
|
||||
Task { @MainActor in
|
||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
self.setStatusBar(image: NSImage.statusBarIcon)
|
||||
}
|
||||
|
||||
if await Startup().checkEnvironment() {
|
||||
@ -32,19 +32,14 @@ extension MainMenu {
|
||||
// Determine what the `php` formula is aliased to
|
||||
await PhpEnvironments.shared.determinePhpAlias()
|
||||
|
||||
// Make sure that broken symlinks are removed ASAP
|
||||
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||
|
||||
// Initialize preferences
|
||||
_ = Preferences.shared
|
||||
|
||||
// Determine install method
|
||||
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."
|
||||
)
|
||||
// Put some useful diagnostics information in log
|
||||
BrewDiagnostics.logBootInformation()
|
||||
|
||||
// Attempt to find out more info about Valet
|
||||
if Valet.shared.version != nil {
|
||||
@ -63,9 +58,6 @@ extension MainMenu {
|
||||
// Check for an alias conflict
|
||||
await BrewDiagnostics.checkForCaskConflict()
|
||||
|
||||
// Update the icon
|
||||
updatePhpVersionInStatusBar()
|
||||
|
||||
// Attempt to find out if PHP-FPM is broken
|
||||
PhpEnvironments.prepare()
|
||||
|
||||
@ -76,7 +68,6 @@ extension MainMenu {
|
||||
WarningManager.shared.evaluateWarnings()
|
||||
|
||||
// Set up the config watchers on launch (updated automatically when switching)
|
||||
Log.info("Setting up watchers...")
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
|
||||
// Detect built-in and custom applications
|
||||
@ -88,6 +79,9 @@ extension MainMenu {
|
||||
// Load the global hotkey
|
||||
App.shared.loadGlobalHotkey()
|
||||
|
||||
// Set up menu items
|
||||
AppDelegate.instance.configureMenuItems(standalone: !Valet.installed)
|
||||
|
||||
if Valet.installed {
|
||||
// Preload all sites
|
||||
await Valet.shared.startPreloadingSites()
|
||||
@ -102,9 +96,33 @@ extension MainMenu {
|
||||
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
|
||||
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 {
|
||||
Stats.incrementSuccessfulLaunchCount()
|
||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||
@ -118,15 +136,6 @@ extension MainMenu {
|
||||
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) {
|
||||
// Mark as no longer busy
|
||||
Task { @MainActor in
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
}
|
||||
|
||||
Task { // Things to do after reloading domain list data
|
||||
if Valet.installed {
|
||||
@ -25,7 +27,7 @@ extension MainMenu {
|
||||
|
||||
// Perform UI updates on main thread
|
||||
Task { @MainActor [self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
|
||||
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
||||
|
@ -37,8 +37,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
// MARK: - UI related
|
||||
|
||||
/**
|
||||
Rebuilds the menu (either asynchronously or synchronously).
|
||||
Defaults to rebuilding the menu asynchronously.
|
||||
Rebuilds the menu on the main thread.
|
||||
*/
|
||||
func rebuild() {
|
||||
Task { @MainActor [self] in
|
||||
@ -80,13 +79,15 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
@objc func refreshActiveInstallation() {
|
||||
if !PhpEnvironments.shared.isBusy {
|
||||
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
} else {
|
||||
Log.perf("Skipping version refresh due to busy status!")
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||
@available(*, deprecated, message: "Use the busy status instead")
|
||||
@objc func updatePhpVersionInStatusBar() {
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
@ -139,7 +140,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
@objc func reloadPhpMonitorMenuInBackground() {
|
||||
asyncExecution({
|
||||
// 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: [
|
||||
.setsBusyUI,
|
||||
.reloadsPhpInstallation,
|
||||
@ -150,13 +151,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
|
||||
/** Refreshes the icon with the PHP version. */
|
||||
@objc func refreshIcon() {
|
||||
|
||||
Task { @MainActor [self] in
|
||||
if PhpEnvironments.shared.isBusy {
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
Log.perf("Refreshing icon: currently busy")
|
||||
setStatusBar(image: NSImage.statusBarIcon)
|
||||
} else {
|
||||
Log.perf("Refreshing icon: no longer busy")
|
||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||
// Static icon has been requested
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
||||
setStatusBar(image: NSImage.statusBarIconStatic)
|
||||
} else {
|
||||
// The dynamic icon has been requested
|
||||
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
|
||||
|
||||
@objc func openAbout() {
|
||||
@ -203,7 +200,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
|
||||
@objc func openWarnings() {
|
||||
WarningsWindowController.show()
|
||||
PhpDoctorWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openConfigGUI() {
|
||||
PhpConfigManagerWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openDomainList() {
|
||||
@ -214,6 +215,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
PhpVersionManagerWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openPhpExtensionManager() {
|
||||
PhpExtensionManagerWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openDonate() {
|
||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||
}
|
||||
@ -231,7 +236,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
func menuWillOpen(_ menu: NSMenu) {
|
||||
// Make sure the shortcut key does not trigger this when the menu is open
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import Cocoa
|
||||
|
||||
extension StatusMenu {
|
||||
|
||||
func addPhpVersionMenuItems() {
|
||||
@MainActor func addPhpVersionMenuItems() {
|
||||
if PhpEnvironments.phpInstall == nil {
|
||||
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
||||
addItems([
|
||||
@ -34,7 +34,7 @@ extension StatusMenu {
|
||||
))
|
||||
}
|
||||
|
||||
func addPhpActionMenuItems() {
|
||||
@MainActor func addPhpActionMenuItems() {
|
||||
if PhpEnvironments.shared.isBusy {
|
||||
addItem(NSMenuItem(title: "mi_busy".localized))
|
||||
return
|
||||
@ -54,7 +54,7 @@ extension StatusMenu {
|
||||
self.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
func addServicesManagerMenuItem() {
|
||||
@MainActor func addServicesManagerMenuItem() {
|
||||
if PhpEnvironments.shared.isBusy {
|
||||
return
|
||||
}
|
||||
@ -65,7 +65,7 @@ extension StatusMenu {
|
||||
])
|
||||
}
|
||||
|
||||
func addSwitchToPhpMenuItems() {
|
||||
@MainActor func addSwitchToPhpMenuItems() {
|
||||
var shortcutKey = 1
|
||||
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
||||
// Get the short and long version
|
||||
@ -102,14 +102,14 @@ extension StatusMenu {
|
||||
}
|
||||
}
|
||||
|
||||
func addLiteModeMenuItem() {
|
||||
@MainActor func addLiteModeMenuItem() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
||||
])
|
||||
}
|
||||
|
||||
func addPreferencesMenuItems() {
|
||||
@MainActor func addPreferencesMenuItems() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_preferences".localized,
|
||||
@ -119,7 +119,7 @@ extension StatusMenu {
|
||||
])
|
||||
}
|
||||
|
||||
func addCoreMenuItems() {
|
||||
@MainActor func addCoreMenuItems() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_about".localized,
|
||||
@ -131,7 +131,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Valet
|
||||
|
||||
func addValetMenuItems() {
|
||||
@MainActor func addValetMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_valet".localized),
|
||||
NSMenuItem(title: "mi_valet_config".localized,
|
||||
@ -146,12 +146,15 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - PHP Configuration
|
||||
|
||||
func addConfigurationMenuItems() {
|
||||
@MainActor func addConfigurationMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
||||
NSMenuItem(title: "mi_php_version_manager".localized,
|
||||
action: #selector(MainMenu.openPhpVersionManager),
|
||||
keyEquivalent: "m"),
|
||||
NSMenuItem(title: "mi_php_ext_manager".localized,
|
||||
action: #selector(MainMenu.openPhpExtensionManager),
|
||||
keyEquivalent: "e"),
|
||||
NSMenuItem(title: "mi_php_config".localized,
|
||||
action: #selector(MainMenu.openActiveConfigFolder),
|
||||
keyEquivalent: "c"),
|
||||
@ -166,7 +169,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Composer
|
||||
|
||||
func addComposerMenuItems() {
|
||||
@MainActor func addComposerMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_composer".localized),
|
||||
NSMenuItem(
|
||||
@ -187,7 +190,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Stats
|
||||
|
||||
func addStatsMenuItem() {
|
||||
@MainActor func addStatsMenuItem() {
|
||||
guard let install = PhpEnvironments.phpInstall else {
|
||||
Log.info("Not showing stats menu item if no PHP version is linked.")
|
||||
return
|
||||
@ -204,7 +207,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
func addExtensionsMenuItems() {
|
||||
@MainActor func addExtensionsMenuItems() {
|
||||
guard let install = PhpEnvironments.phpInstall else {
|
||||
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
||||
return
|
||||
@ -225,7 +228,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Presets
|
||||
|
||||
func addPresetsMenuItem() {
|
||||
@MainActor func addPresetsMenuItem() {
|
||||
guard let presets = Preferences.custom.presets else {
|
||||
addEmptyPresetHelp()
|
||||
return
|
||||
|
@ -9,7 +9,7 @@ import Cocoa
|
||||
|
||||
class StatusMenu: NSMenu {
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
func addMenuItems() {
|
||||
@MainActor func addMenuItems() {
|
||||
addPhpVersionMenuItems()
|
||||
addItem(NSMenuItem.separator())
|
||||
|
||||
|
@ -91,6 +91,7 @@ class BetterAlert {
|
||||
}
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
windowController.window?.makeKeyAndOrderFront(nil)
|
||||
windowController.window?.setCenterPosition(offsetY: 70)
|
||||
return NSApplication.shared.runModal(for: windowController.window!)
|
||||
|
@ -20,6 +20,7 @@ enum PreferenceName: String, Codable {
|
||||
case globalHotkey = "global_hotkey"
|
||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
||||
case languageOverride = "language_override"
|
||||
|
||||
// APPEARANCE
|
||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||
@ -84,7 +85,8 @@ enum PreferenceName: String, Codable {
|
||||
],
|
||||
.string: [
|
||||
.globalHotkey,
|
||||
.iconTypeToDisplay
|
||||
.iconTypeToDisplay,
|
||||
.languageOverride
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ class Preferences {
|
||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
||||
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
||||
PreferenceName.languageOverride.rawValue: "",
|
||||
|
||||
/// Preferences: Appearance
|
||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||
|
@ -17,7 +17,9 @@ class GeneralPreferencesVC: GenericPreferenceVC {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||
.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.getAutomaticComposerUpdatePV())
|
||||
.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 {
|
||||
return SelectPreferenceView.make(
|
||||
sectionText: "",
|
||||
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",
|
||||
preference: .iconTypeToDisplay,
|
||||
action: {
|
||||
|
@ -65,9 +65,11 @@ class PreferencesWindowController: PMWindowController {
|
||||
App.shared.preferencesWindowController?.showWindow(self)
|
||||
|
||||
if justCreated {
|
||||
App.shared.preferencesWindowController?.positionWindowInTopLeftCorner()
|
||||
App.shared.preferencesWindowController?.positionWindowInTopRightCorner()
|
||||
}
|
||||
|
||||
App.shared.preferencesWindowController?.window?.orderFrontRegardless()
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
@ -83,22 +85,22 @@ class PreferencesWindowController: PMWindowController {
|
||||
return [
|
||||
PrefTabView(
|
||||
viewController: GeneralPreferencesVC.fromStoryboard(),
|
||||
label: "General",
|
||||
label: "prefs.tabs.general".localized,
|
||||
icon: "gearshape"
|
||||
),
|
||||
PrefTabView(
|
||||
viewController: AppearancePreferencesVC.fromStoryboard(),
|
||||
label: "Appearance",
|
||||
label: "prefs.tabs.appearance".localized,
|
||||
icon: "paintbrush"
|
||||
),
|
||||
PrefTabView(
|
||||
viewController: MenuStructurePreferencesVC.fromStoryboard(),
|
||||
label: "Visibility",
|
||||
label: "prefs.tabs.visibility".localized,
|
||||
icon: "eye"
|
||||
),
|
||||
PrefTabView(
|
||||
viewController: NotificationPreferencesVC.fromStoryboard(),
|
||||
label: "Notifications",
|
||||
label: "prefs.tabs.notifications".localized,
|
||||
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.
|
||||
|
||||
|
@ -1,8 +1,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>
|
||||
<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"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@ -23,7 +23,7 @@
|
||||
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -31,7 +31,7 @@
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||
|
@ -9,30 +9,34 @@
|
||||
import Foundation
|
||||
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 labelDescription: NSTextField!
|
||||
@IBOutlet weak var popupButton: NSPopUpButton!
|
||||
|
||||
var localizationPrefix: String = ""
|
||||
var localizationPrefix: String?
|
||||
var imagePrefix: String?
|
||||
|
||||
var options: [String] = [] {
|
||||
var options: [PreferenceDropdownOption] = [] {
|
||||
didSet {
|
||||
self.popupButton.removeAllItems()
|
||||
self.options.forEach { value in
|
||||
self.popupButton.addItem(
|
||||
withTitle: "\(localizationPrefix).\(value)".localized
|
||||
)
|
||||
}
|
||||
|
||||
if imagePrefix == nil {
|
||||
return
|
||||
self.options.forEach { option in
|
||||
if let prefix = localizationPrefix {
|
||||
self.popupButton.addItem(withTitle: "\(prefix).\(option.label)".localized)
|
||||
} else {
|
||||
self.popupButton.addItem(withTitle: option.label)
|
||||
}
|
||||
}
|
||||
|
||||
if let prefix = imagePrefix {
|
||||
self.popupButton.itemArray.enumerated().forEach { item in
|
||||
item.element.image = NSImage(named: "\(imagePrefix!)_\(self.options[item.offset])")
|
||||
item.element.image = NSImage(named: "\(prefix)_\(self.options[item.offset].value)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,19 +47,18 @@ class SelectPreferenceView: NSView, XibLoadable {
|
||||
didSet {
|
||||
let value = Preferences.preferences[preference] as! String
|
||||
self.options.enumerated().forEach { option in
|
||||
if option.element == value {
|
||||
if option.element.value == value {
|
||||
self.popupButton.selectItem(at: option.offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable function_parameter_count
|
||||
static func make(
|
||||
sectionText: String,
|
||||
descriptionText: String,
|
||||
options: [String],
|
||||
localizationPrefix: String,
|
||||
options: [PreferenceDropdownOption],
|
||||
localizationPrefix: String? = nil,
|
||||
imagePrefix: String? = nil,
|
||||
preference: PreferenceName,
|
||||
action: @escaping () -> Void) -> NSView {
|
||||
@ -72,11 +75,10 @@ class SelectPreferenceView: NSView, XibLoadable {
|
||||
|
||||
return view
|
||||
}
|
||||
// swiftlint:enable function_parameter_count
|
||||
|
||||
@IBAction func valueChanged(_ sender: Any) {
|
||||
let index = self.popupButton.indexOfSelectedItem
|
||||
Preferences.update(.iconTypeToDisplay, value: self.options[index])
|
||||
Preferences.update(self.preference, value: self.options[index].value)
|
||||
self.action()
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,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>
|
||||
<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"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@ -13,7 +13,7 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<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"/>
|
||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -21,7 +21,7 @@
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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 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 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"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
|
@ -27,15 +27,15 @@ struct HelpButton: View {
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
.focusable(false)
|
||||
}
|
||||
}
|
||||
|
||||
struct HelpButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
HelpButton(action: {}).padding()
|
||||
.previewDisplayName("Light Mode")
|
||||
HelpButton(action: {}).padding().preferredColorScheme(.dark)
|
||||
.previewDisplayName("Dark Mode")
|
||||
}
|
||||
}
|
||||
#Preview("Light Mode") {
|
||||
HelpButton(action: {})
|
||||
.padding(100)
|
||||
}
|
||||
|
||||
#Preview("Dark Mode") {
|
||||
HelpButton(action: {})
|
||||
.padding(100)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
@ -27,7 +27,9 @@ var isRunningSwiftUIPreview: Bool {
|
||||
|
||||
extension Color {
|
||||
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 = {
|
||||
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
||||
|
@ -30,8 +30,6 @@ struct NoDomainResults: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct NoDomainResults_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
#Preview {
|
||||
NoDomainResults()
|
||||
}
|
||||
}
|
||||
|
@ -126,8 +126,7 @@ struct DisclaimerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct VersionPopoverView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
#Preview("Unknown Requirement") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
@ -140,8 +139,9 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Unknown Requirement")
|
||||
}
|
||||
|
||||
#Preview("Requirement Matches") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
@ -154,7 +154,9 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Requirement Matches")
|
||||
}
|
||||
|
||||
#Preview("Isolated") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
@ -168,7 +170,9 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Isolated")
|
||||
}
|
||||
|
||||
#Preview("Isolated Mismatch") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
@ -182,7 +186,9 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Isolated Mismatch")
|
||||
}
|
||||
|
||||
#Preview("Recommend Alternatives") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
@ -198,6 +204,4 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
||||
],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Recommend Alternatives")
|
||||
}
|
||||
}
|
||||
|
@ -45,9 +45,7 @@ struct HeaderView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct HeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
#Preview {
|
||||
HeaderView(text: "Hello world")
|
||||
.frame(width: 330.0)
|
||||
}
|
||||
}
|
||||
|
@ -18,5 +18,6 @@ struct SectionHeaderView: View {
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.appSecondary)
|
||||
.background(Color.debug)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
}
|
||||
|
@ -172,15 +172,15 @@ struct ServiceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ServicesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
#Preview("Active 1") {
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: ["php", "nginx", "dnsmasq"],
|
||||
status: .active
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
.previewDisplayName("Active 1")
|
||||
}
|
||||
|
||||
#Preview("Active 2") {
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: [
|
||||
"php", "nginx", "dnsmasq", "thing1",
|
||||
@ -189,6 +189,4 @@ struct ServicesView_Previews: PreviewProvider {
|
||||
status: .inactive
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
.previewDisplayName("Active 2")
|
||||
}
|
||||
}
|
||||
|
@ -29,38 +29,80 @@ struct StatsView: View {
|
||||
@State var maxPostSize: 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 {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 30) {
|
||||
if self.hasErrorState() {
|
||||
HStack {
|
||||
Text("⚠️")
|
||||
.frame(maxWidth: 20, alignment: .center)
|
||||
.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))
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.padding(.leading, 30)
|
||||
.padding(.trailing, 30)
|
||||
} else {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
VStack(alignment: .center, spacing: 3) {
|
||||
SectionHeaderView(text: "mi_memory_limit".localized.uppercased())
|
||||
Text(memoryLimit)
|
||||
.fontWeight(.medium)
|
||||
.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()
|
||||
}
|
||||
.padding(10)
|
||||
} label: {
|
||||
Image(systemName: "gearshape.fill")
|
||||
}
|
||||
.accessibility(identifier: "phpConfigButton")
|
||||
.focusable(false)
|
||||
.frame(minWidth: 30, alignment: .center)
|
||||
}
|
||||
.padding(5)
|
||||
.background(Color.debug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StatsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
#Preview {
|
||||
StatsView(
|
||||
memoryLimit: "1024 MB",
|
||||
maxPostSize: "1024 MB",
|
||||
maxUploadSize: "1024 MB"
|
||||
)
|
||||
}
|
||||
).frame(height: 100)
|
||||
}
|
||||
|
@ -1,396 +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
|
||||
|
||||
if progress.value == 1 {
|
||||
self.setBusyStatus(false)
|
||||
await self.handler.refreshPhpVersions(loadOutdated: 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) --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"?>
|
||||
<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>
|
||||
<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"/>
|
||||
</dependencies>
|
||||
<scenes>
|
@ -20,7 +20,7 @@ class TerminalProgressWindowController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
windowController.showWindow(windowController)
|
||||
windowController.window?.makeKeyAndOrderFront(nil)
|
||||
windowController.positionWindowInTopLeftCorner()
|
||||
windowController.positionWindowInTopRightCorner()
|
||||
|
||||
windowController.progressView?.labelTitle.stringValue = title
|
||||
windowController.progressView?.labelDescription.stringValue = description
|
@ -17,13 +17,13 @@ extension App {
|
||||
onChange: { Task { await self.onHomebrewPhpModification() } }
|
||||
)
|
||||
|
||||
App.shared.watchers[.homebrewBinaries] = notifier
|
||||
App.shared.watchers["homebrewBinaries"] = notifier
|
||||
}
|
||||
|
||||
public func destroyHomebrewWatchers() {
|
||||
// Removing requires termination and then removing reference
|
||||
self.watchers[.homebrewBinaries]?.terminate()
|
||||
self.watchers[.homebrewBinaries] = nil
|
||||
self.watchers["homebrewBinaries"]?.terminate()
|
||||
self.watchers["homebrewBinaries"] = nil
|
||||
}
|
||||
|
||||
public func onHomebrewPhpModification() async {
|
||||
@ -31,10 +31,13 @@ extension App {
|
||||
Log.info("Something changed in the Homebrew binary directory...")
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
await MainMenu.shared.refreshActiveInstallation()
|
||||
// let new = PhpEnvironments.shared.currentInstall?.version.text
|
||||
|
||||
// TODO:
|
||||
// Check if the new and previous version are different
|
||||
// if so, we can show a notification if needed
|
||||
//
|
||||
// TODO: PHP Guard 2.0
|
||||
// Check if the new and previous version of PHP are different
|
||||
// if so, we can show a notification if needed or alert the user
|
||||
//
|
||||
// let new = PhpEnvironments.shared.currentInstall?.version.text
|
||||
//
|
||||
}
|
||||
}
|
||||
|
@ -10,52 +10,52 @@ import Foundation
|
||||
|
||||
extension App {
|
||||
|
||||
func startWatcher(_ url: URL) {
|
||||
Log.perf("No watcher currently active...")
|
||||
self.watcher = PhpConfigWatcher(for: url)
|
||||
func startWatchManager(_ url: URL) {
|
||||
Log.perf("Starting config watch manager...")
|
||||
self.watchManager = ConfigWatchManager(for: url)
|
||||
|
||||
self.watcher.didChange = { url in
|
||||
self.watchManager.didChange = { url in
|
||||
Log.perf("Something has changed in: \(url)")
|
||||
|
||||
// Check if the watcher has last updated the menu less than 0.75s ago
|
||||
let distance = self.watcher.lastUpdate?.distance(to: Date().timeIntervalSince1970)
|
||||
let distance = self.watchManager.lastUpdate?.distance(to: Date().timeIntervalSince1970)
|
||||
if distance == nil || distance != nil && distance! > 0.75 {
|
||||
Log.perf("Refreshing menu...")
|
||||
Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() }
|
||||
self.watcher.lastUpdate = Date().timeIntervalSince1970
|
||||
self.watchManager.lastUpdate = Date().timeIntervalSince1970
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlePhpConfigWatcher(forceReload: Bool = false) {
|
||||
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
|
||||
}
|
||||
|
||||
guard let install = PhpEnvironments.phpInstall else {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
// due to timing issues, which creates retain cycles.
|
||||
// due to timing issues, which creates retain cycles
|
||||
Task { @MainActor in
|
||||
// Watcher needs to be created
|
||||
if self.watcher == nil {
|
||||
self.startWatcher(url)
|
||||
if self.watchManager == nil {
|
||||
self.startWatchManager(url)
|
||||
}
|
||||
|
||||
// Watcher needs to be updated
|
||||
if self.watcher.url != url || forceReload {
|
||||
self.watcher.disable()
|
||||
self.watcher = nil
|
||||
if self.watchManager.url != url || forceReload {
|
||||
self.watchManager.disable()
|
||||
self.watchManager = nil
|
||||
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.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class FSNotifier {
|
||||
enum Kind {
|
||||
case homebrewLocks, homebrewBinaries
|
||||
}
|
||||
|
||||
public static var shared: FSNotifier! = nil
|
||||
|
||||
@ -66,4 +63,5 @@ class FSNotifier {
|
||||
deinit {
|
||||
Log.perf("FSNotifier for \(self.url) will be deinitialized.")
|
||||
}
|
||||
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
//
|
||||
// FolderWatcher.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 30/03/2021.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpConfigWatcher {
|
||||
|
||||
let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent)
|
||||
|
||||
let url: URL
|
||||
|
||||
var didChange: ((URL) -> Void)?
|
||||
|
||||
var lastUpdate: TimeInterval?
|
||||
|
||||
var watchers: [FSWatcher] = []
|
||||
|
||||
init(for url: URL) {
|
||||
if FileSystem is TestableFileSystem {
|
||||
fatalError("""
|
||||
PhpConfigWatcher is not compatible with testable FS! "
|
||||
You are not allowed to instantiate these while using a testable FS.
|
||||
""")
|
||||
}
|
||||
|
||||
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 enumerator = FileManager.default.enumerator(atPath: self.url.appendingPathComponent("conf.d").path)
|
||||
let filePaths = enumerator?.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: FSWatcherBehaviour = .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 = FSWatcher(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)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum FSWatcherBehaviour {
|
||||
case reloadsMenu
|
||||
case reloadsWatchers
|
||||
}
|
||||
|
||||
class FSWatcher {
|
||||
|
||||
private var parent: PhpConfigWatcher!
|
||||
|
||||
private var monitoredFolderFileDescriptor: CInt = -1
|
||||
|
||||
private var folderMonitorSource: DispatchSourceFileSystemObject?
|
||||
|
||||
let url: URL
|
||||
|
||||
init(
|
||||
for url: URL,
|
||||
eventMask: DispatchSource.FileSystemEvent,
|
||||
parent: PhpConfigWatcher,
|
||||
behaviour: FSWatcherBehaviour = .reloadsMenu
|
||||
) {
|
||||
self.url = url
|
||||
self.parent = parent
|
||||
self.startMonitoring(eventMask, behaviour: behaviour)
|
||||
}
|
||||
|
||||
func startMonitoring(
|
||||
_ eventMask: DispatchSource.FileSystemEvent,
|
||||
behaviour: FSWatcherBehaviour
|
||||
) {
|
||||
guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else {
|
||||
return
|
||||
}
|
||||
|
||||
// Open the file or folder referenced by URL for monitoring only.
|
||||
monitoredFolderFileDescriptor = open(url.path, O_EVTONLY)
|
||||
folderMonitorSource = DispatchSource.makeFileSystemObjectSource(
|
||||
fileDescriptor: monitoredFolderFileDescriptor,
|
||||
eventMask: eventMask,
|
||||
queue: parent.folderMonitorQueue
|
||||
)
|
||||
|
||||
// Define the block to call when a file change is detected.
|
||||
folderMonitorSource?.setEventHandler { [weak self] in
|
||||
// The default behaviour is to reload the menu
|
||||
switch behaviour {
|
||||
case .reloadsMenu:
|
||||
// Default behaviour: reload the menu items
|
||||
self?.parent.didChange?(self!.url)
|
||||
case .reloadsWatchers:
|
||||
// Alternative behaviour: reload all watchers
|
||||
App.shared.handlePhpConfigWatcher(forceReload: true)
|
||||
}
|
||||
}
|
||||
|
||||
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
|
||||
folderMonitorSource?.setCancelHandler { [weak self] in
|
||||
guard let self = self else { return }
|
||||
close(self.monitoredFolderFileDescriptor)
|
||||
self.monitoredFolderFileDescriptor = -1
|
||||
self.folderMonitorSource = nil
|
||||
}
|
||||
|
||||
// Start monitoring the directory via the source.
|
||||
folderMonitorSource?.resume()
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
folderMonitorSource?.cancel()
|
||||
self.parent = nil
|
||||
}
|
||||
}
|
@ -16,21 +16,19 @@ class DomainListKindCell: NSTableCellView, DomainListCellProtocol {
|
||||
|
||||
func populateCell(with site: ValetSite) {
|
||||
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
||||
imageViewType.image = NSImage(
|
||||
named: site.aliasPath == nil
|
||||
? "IconParked"
|
||||
: "IconLinked"
|
||||
)
|
||||
imageViewType.image = site.aliasPath == nil
|
||||
? NSImage.iconParked
|
||||
: NSImage.iconLinked
|
||||
|
||||
// Unless, of course, this is a default site
|
||||
if site.absolutePath == Valet.shared.config.defaultSite {
|
||||
imageViewType.image = NSImage(named: "IconDefault")
|
||||
imageViewType.image = NSImage.iconDefault
|
||||
}
|
||||
|
||||
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
||||
}
|
||||
|
||||
func populateCell(with proxy: ValetProxy) {
|
||||
imageViewType.image = NSImage(named: "IconProxy")
|
||||
imageViewType.image = NSImage.iconProxy
|
||||
}
|
||||
}
|
@ -34,14 +34,13 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||
|
||||
if site.isolatedPhpVersion != nil {
|
||||
imageViewPhpVersionOK.isHidden = false
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
|
||||
imageViewPhpVersionOK.image = NSImage.isolated
|
||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.isolated".localized(site.servingPhpVersion)
|
||||
} else {
|
||||
imageViewPhpVersionOK.isHidden = (site.preferredPhpVersion == "???"
|
||||
|| !site.isCompatibleWithPreferredPhpVersion)
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
|
||||
imageViewPhpVersionOK.image = NSImage.checkmark
|
||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.preferredPhpVersion)
|
||||
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user