mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-09 04:42:59 +02:00
Compare commits
113 Commits
Author | SHA1 | Date | |
---|---|---|---|
dba2ce5bf3 | |||
4644c1ada4 | |||
f3e1b4de6f | |||
a3226b632f | |||
652878d97f | |||
032610ad5c | |||
2c2627dc9f | |||
62587bdf65 | |||
|
5e9dae78f5 | ||
949ba5b559 | |||
ce88f897ef | |||
fa9b51aaab | |||
b8affad5ee | |||
41e5f5b4c3 | |||
79f6a60a16 | |||
06bc4ddb9a | |||
bf728a24f0 | |||
b7cad3af62 | |||
4a3dee3c50 | |||
9d5a0ed745 | |||
b3b509409a | |||
4934f35d0b | |||
92e7418158 | |||
52ea64db40 | |||
f66e9b7340 | |||
2bf28fe247 | |||
c6e4f785bc | |||
94fe7df3bd | |||
f373621a4a | |||
5104a865fb | |||
7b10973330 | |||
bc208bddf9 | |||
321b4aaf8b | |||
b26fc3bc4b | |||
f758c5d63a | |||
c7510d778d | |||
70c5aadb7f | |||
a731f15cf7 | |||
ab4c436202 | |||
c0231690d4 | |||
988e9d3351 | |||
2f119d4332 | |||
d83c629a7b | |||
e7d98dbeae | |||
f3d5946743 | |||
7728a1125c | |||
3612351df7 | |||
8e912151fb | |||
3a2209e604 | |||
1f0b56cab6 | |||
e08d970edd | |||
32c757e711 | |||
480cdb94ae | |||
7fbcac5dc2 | |||
4edb5f5015 | |||
294f84ccb2 | |||
155b57eb9e | |||
a459f015e1 | |||
27676f13f4 | |||
b4b2d7052f | |||
6d25cf585e | |||
ba04c94c05 | |||
13447ba533 | |||
6f2e8f4b20 | |||
dc860074ef | |||
f586b8fcbe | |||
94714c3e7a | |||
904d05bdce | |||
ec30bee72b | |||
2fe3a4b7eb | |||
a7d5950aa0 | |||
e8306289ce | |||
ff61d8c52e | |||
da41673855 | |||
5bda727981 | |||
23cf575026 | |||
d3053b8fe3 | |||
7159ca8612 | |||
141c06d14b | |||
94c84aaab3 | |||
9ca16e72d5 | |||
67a00f979a | |||
1e4c45dcbd | |||
87c44f3ae3 | |||
f39732a0e6 | |||
3b78ac43d7 | |||
1f19b81530 | |||
d714d7ad4c | |||
4dce6c033e | |||
72a8a1e382 | |||
07b17f3f84 | |||
7f0f7ff3e9 | |||
c7c143c760 | |||
ee050af364 | |||
f7e2551587 | |||
cc0cc21e5f | |||
883ea05bd1 | |||
641bddfce7 | |||
2f7223fba5 | |||
3b23ce7805 | |||
a634d083a6 | |||
9a3dd2fa22 | |||
8790b30706 | |||
c42188b717 | |||
cc251686f9 | |||
6fd6241567 | |||
c8ab2e67f6 | |||
f82ab913c6 | |||
58943148fa | |||
8a46b9d374 | |||
a62ebcff92 | |||
541378f3f9 | |||
e6f1d7e834 |
@@ -33,17 +33,13 @@ defaults delete com.nicoverbruggen.phpmon && killall cfprefsd
|
|||||||
If you'd like to build PHP Monitor yourself, you need:
|
If you'd like to build PHP Monitor yourself, you need:
|
||||||
|
|
||||||
* Xcode (usually the latest version)
|
* Xcode (usually the latest version)
|
||||||
* *PHP Monitor Self-Updater.app* in the `phpmon-updater` directory (You can build it yourself, it is included as a target OR copy the signed app so it is included w/ PHP Monitor)
|
|
||||||
* The contents of this repository
|
|
||||||
|
|
||||||
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
|
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
|
||||||
|
|
||||||
|
**Important**: The updater now gets automatically built and included as part of the main target.
|
||||||
|
|
||||||
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
||||||
|
|
||||||
### PHP Monitor Updater
|
|
||||||
|
|
||||||
Select the separate target and build. You can then copy the product to the `phpmon-updater` directory. The binary will be re-signed when distributing the main build.
|
|
||||||
|
|
||||||
## 🚀 Release procedure
|
## 🚀 Release procedure
|
||||||
|
|
||||||
1. Merge into `main`
|
1. Merge into `main`
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -112,13 +112,15 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
|
|||||||
|
|
||||||
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).
|
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
||||||
|
|
||||||
|
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
|
||||||
|
|
||||||
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
||||||
|
|
||||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
|
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.3.
|
||||||
|
|
||||||
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
||||||
|
|
||||||
|
12
SECURITY.md
12
SECURITY.md
@@ -4,18 +4,20 @@
|
|||||||
|
|
||||||
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
|
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
|
||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 6.1 | ✅ 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 |
|
| 7.0 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
|
||||||
## Legacy versions
|
## Legacy versions
|
||||||
|
|
||||||
These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
|
These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
|
||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 6.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 |
|
| 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
||||||
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 674 KiB After Width: | Height: | Size: 723 KiB |
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "1.000",
|
||||||
|
"green" : "1.000",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.172",
|
||||||
|
"green" : "0.182",
|
||||||
|
"red" : "0.182"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@@ -24,7 +24,7 @@
|
|||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.988",
|
"blue" : "0.988",
|
||||||
"green" : "0.723",
|
"green" : "0.444",
|
||||||
"red" : "0.277"
|
"red" : "0.277"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.300",
|
||||||
|
"blue" : "0.180",
|
||||||
|
"green" : "0.841",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.300",
|
||||||
|
"blue" : "0.426",
|
||||||
|
"green" : "0.809",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
25
phpmon/Assets.xcassets/php.imageset/Contents.json
vendored
Normal file
25
phpmon/Assets.xcassets/php.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "php.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true,
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
|
}
|
||||||
|
}
|
1
phpmon/Assets.xcassets/php.imageset/php.svg
vendored
Normal file
1
phpmon/Assets.xcassets/php.imageset/php.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 24 24"><path d="M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zm-2.595-1.382h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -14,14 +14,17 @@ class Actions {
|
|||||||
|
|
||||||
public static func linkPhp() async {
|
public static func linkPhp() async {
|
||||||
await brew("link php --overwrite --force")
|
await brew("link php --overwrite --force")
|
||||||
|
|
||||||
// TODO: Verify that this worked, if not, notify the user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func restartPhpFpm() async {
|
public static func restartPhpFpm() async {
|
||||||
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
|
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func restartPhpFpm(version: String) async {
|
||||||
|
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
|
||||||
|
await brew("services restart \(formula)", sudo: HomebrewFormulae.php.elevated)
|
||||||
|
}
|
||||||
|
|
||||||
public static func restartNginx() async {
|
public static func restartNginx() async {
|
||||||
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
|
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
|
||||||
}
|
}
|
||||||
|
@@ -19,10 +19,50 @@ struct Constants {
|
|||||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The PHP versions that are considered pre-release versions.
|
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 ExperimentalPhpVersions: Set = [
|
static let PhpFormulaeCutoffDate = "2024-11-31" // YYYY-MM-DD
|
||||||
"8.3", "8.4"
|
|
||||||
|
/**
|
||||||
|
* 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-11-22")
|
||||||
|
]
|
||||||
|
|
||||||
|
return Set(releaseDates
|
||||||
|
.filter { (_: String, date: Date?) in
|
||||||
|
guard let date else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return date > Date.now
|
||||||
|
}.map { (version: String, _: Date?) in
|
||||||
|
return version
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
The Homebrew services that should be automatically
|
||||||
|
detected and show up in the list of managed services.
|
||||||
|
*/
|
||||||
|
static let DetectedHomebrewServices: Set = [
|
||||||
|
"mailhog",
|
||||||
|
"mysql@",
|
||||||
|
"postgresql@",
|
||||||
|
"redis"
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,8 +72,7 @@ struct Constants {
|
|||||||
static let DetectedPhpVersions: Set = [
|
static let DetectedPhpVersions: Set = [
|
||||||
"5.6",
|
"5.6",
|
||||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3",
|
||||||
"8.3",
|
|
||||||
"8.4"
|
"8.4"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -50,14 +89,14 @@ struct Constants {
|
|||||||
3: // Valet v3 dropped support for v5.6
|
3: // Valet v3 dropped support for v5.6
|
||||||
[
|
[
|
||||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3",
|
||||||
"8.3", "8.4" // dev
|
"8.4" // dev
|
||||||
],
|
],
|
||||||
4: // Valet v4 dropped support for v7.0
|
4: // Valet v4 dropped support for v7.0
|
||||||
[
|
[
|
||||||
"7.1", "7.2", "7.3", "7.4",
|
"7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3",
|
||||||
"8.3", "8.4" // dev
|
"8.4" // dev
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -91,6 +130,8 @@ struct Constants {
|
|||||||
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
|
// EAP URLs
|
||||||
|
|
||||||
static let EarlyAccessCaskFile = URL(
|
static let EarlyAccessCaskFile = URL(
|
||||||
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
||||||
)!
|
)!
|
||||||
|
@@ -102,6 +102,14 @@ public class Paths {
|
|||||||
return "\(shared.baseDir.rawValue)/etc"
|
return "\(shared.baseDir.rawValue)/etc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var tapPath: String {
|
||||||
|
if shared.baseDir == .usr {
|
||||||
|
return "\(shared.baseDir.rawValue)/homebrew/Library/Taps"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(shared.baseDir.rawValue)/Library/Taps"
|
||||||
|
}
|
||||||
|
|
||||||
public static var caskroomPath: String {
|
public static var caskroomPath: String {
|
||||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||||
|
@@ -15,4 +15,10 @@ extension Date {
|
|||||||
return dateFormatter.string(from: self)
|
return dateFormatter.string(from: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func fromString(_ string: String) -> Date? {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
return dateFormatter.date(from: string)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,18 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct Localization {
|
struct Localization {
|
||||||
|
static var preferredLanguage: String? {
|
||||||
|
guard let language = Preferences.preferences[.languageOverride] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if language.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
|
||||||
static var bundle: Bundle = {
|
static var bundle: Bundle = {
|
||||||
if !isRunningTests {
|
if !isRunningTests {
|
||||||
return Bundle.main
|
return Bundle.main
|
||||||
@@ -32,7 +44,15 @@ struct Localization {
|
|||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
var localized: String {
|
var localized: String {
|
||||||
let string = NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
|
var preferredBundle: Bundle = Localization.bundle
|
||||||
|
|
||||||
|
if let preferred = Localization.preferredLanguage,
|
||||||
|
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
|
||||||
|
let bundle = Bundle(path: path) {
|
||||||
|
preferredBundle = bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
let string = NSLocalizedString(self, tableName: nil, bundle: preferredBundle, value: "", comment: "")
|
||||||
|
|
||||||
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
||||||
if string == self {
|
if string == self {
|
||||||
|
@@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
|
|||||||
// MARK: — FS Attributes
|
// MARK: — FS Attributes
|
||||||
|
|
||||||
func makeExecutable(_ path: String) throws {
|
func makeExecutable(_ path: String) throws {
|
||||||
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Checks
|
// MARK: - Checks
|
||||||
|
@@ -75,7 +75,7 @@ class MenuBarImageGenerator {
|
|||||||
|
|
||||||
// Then we'll fetch the image we want on the left
|
// Then we'll fetch the image we want on the left
|
||||||
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
||||||
if iconType == nil {
|
if iconType == nil || !MenuBarIcon.allCases.map({ $0.rawValue }).contains(iconType) {
|
||||||
Log.warn("Invalid icon type found, using the default")
|
Log.warn("Invalid icon type found, using the default")
|
||||||
iconType = MenuBarIcon.iconPhp.rawValue
|
iconType = MenuBarIcon.iconPhp.rawValue
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,8 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
App.shared.remove(window: windowName)
|
App.shared.remove(window: windowName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func windowDidResize(_ notification: Notification) {}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,6 @@ import Foundation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
Run a simple blocking Shell command on the user's own system.
|
Run a simple blocking Shell command on the user's own system.
|
||||||
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
|
|
||||||
*/
|
*/
|
||||||
public func system(_ command: String) -> String {
|
public func system(_ command: String) -> String {
|
||||||
let task = Process()
|
let task = Process()
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// WIP.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 01/11/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
func todo(_ context: String = "") {
|
|
||||||
if !context.isEmpty {
|
|
||||||
fatalError("To be implemented: \(context)")
|
|
||||||
}
|
|
||||||
|
|
||||||
fatalError("To be implemented")
|
|
||||||
}
|
|
@@ -62,13 +62,6 @@ class ActivePhpInstallation {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load extension information
|
|
||||||
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
|
||||||
|
|
||||||
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
|
||||||
iniFiles.append(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get configuration values
|
// Get configuration values
|
||||||
limits = Limits(
|
limits = Limits(
|
||||||
memory_limit: getByteCount(key: "memory_limit"),
|
memory_limit: getByteCount(key: "memory_limit"),
|
||||||
@@ -76,14 +69,9 @@ class ActivePhpInstallation {
|
|||||||
post_max_size: getByteCount(key: "post_max_size")
|
post_max_size: getByteCount(key: "post_max_size")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Return a list of .ini files parsed after php.ini
|
let paths = ActiveShell.shared
|
||||||
let paths = Command.execute(
|
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
path: Paths.php,
|
.split(separator: "\n")
|
||||||
arguments: ["-r", "echo php_ini_scanned_files();"],
|
|
||||||
trimNewlines: false
|
|
||||||
)
|
|
||||||
.replacingOccurrences(of: "\n", with: "")
|
|
||||||
.split(separator: ",")
|
|
||||||
.map { String($0) }
|
.map { String($0) }
|
||||||
|
|
||||||
// See if any extensions are present in said .ini files
|
// See if any extensions are present in said .ini files
|
||||||
|
@@ -13,7 +13,7 @@ class PhpEnvironments {
|
|||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loads the currently active PHP installation upon startup. May be empty.
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
self.currentInstall = ActivePhpInstallation.load()
|
self.currentInstall = ActivePhpInstallation.load()
|
||||||
@@ -29,7 +29,7 @@ class PhpEnvironments {
|
|||||||
/**
|
/**
|
||||||
Determine which PHP version the `php` formula is aliased to.
|
Determine which PHP version the `php` formula is aliased to.
|
||||||
*/
|
*/
|
||||||
func determinePhpAlias() async {
|
@MainActor func determinePhpAlias() async {
|
||||||
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
||||||
|
|
||||||
self.homebrewPackage = try! JSONDecoder().decode(
|
self.homebrewPackage = try! JSONDecoder().decode(
|
||||||
@@ -37,7 +37,28 @@ class PhpEnvironments {
|
|||||||
from: brewPhpAlias.data(using: .utf8)!
|
from: brewPhpAlias.data(using: .utf8)!
|
||||||
).first!
|
).first!
|
||||||
|
|
||||||
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!")
|
PhpEnvironments.brewPhpAlias = self.homebrewPackage.version
|
||||||
|
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).")
|
||||||
|
|
||||||
|
// Check if that version actually corresponds to an older version
|
||||||
|
let phpConfigExecutablePath = "\(Paths.optPath)/php/bin/php-config"
|
||||||
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
|
let longVersionString = Command.execute(
|
||||||
|
path: phpConfigExecutablePath,
|
||||||
|
arguments: ["--version"],
|
||||||
|
trimNewlines: false
|
||||||
|
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
if let version = try? VersionNumber.parse(longVersionString) {
|
||||||
|
PhpEnvironments.brewPhpAlias = version.short
|
||||||
|
if version.short != homebrewPackage.version {
|
||||||
|
Log.info("[BREW] An older version of `php` is actually installed (\(version.short)).")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not determine the actual version of the php binary; assuming Homebrew is correct.")
|
||||||
|
PhpEnvironments.brewPhpAlias = homebrewPackage.version
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
@@ -49,14 +70,12 @@ class PhpEnvironments {
|
|||||||
static let shared = PhpEnvironments()
|
static let shared = PhpEnvironments()
|
||||||
|
|
||||||
/** Whether the switcher is busy performing any actions. */
|
/** Whether the switcher is busy performing any actions. */
|
||||||
var isBusy: Bool = false {
|
@MainActor var isBusy: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
Task { @MainActor in
|
MainMenu.shared.refreshIcon()
|
||||||
MainMenu.shared.setBusyImage()
|
|
||||||
MainMenu.shared.rebuild()
|
MainMenu.shared.rebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** All versions of PHP that are currently supported. */
|
/** All versions of PHP that are currently supported. */
|
||||||
var availablePhpVersions: [String] = []
|
var availablePhpVersions: [String] = []
|
||||||
@@ -68,7 +87,14 @@ class PhpEnvironments {
|
|||||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||||
|
|
||||||
/** Information about the currently linked PHP installation. */
|
/** Information about the currently linked PHP installation. */
|
||||||
var currentInstall: ActivePhpInstallation?
|
var currentInstall: ActivePhpInstallation? {
|
||||||
|
didSet {
|
||||||
|
// Let the PHP extension manager, if it exists, know the version changed
|
||||||
|
if let version = currentInstall?.version.short {
|
||||||
|
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The version that the `php` formula via Brew is aliased to on the current system.
|
The version that the `php` formula via Brew is aliased to on the current system.
|
||||||
@@ -79,7 +105,12 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
As such, we take that information from Homebrew.
|
As such, we take that information from Homebrew.
|
||||||
*/
|
*/
|
||||||
static var brewPhpAlias: String {
|
static var brewPhpAlias: String = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
It's possible for the alias to be newer than the actual installed version of PHP.
|
||||||
|
*/
|
||||||
|
static var homebrewBrewPhpAlias: String {
|
||||||
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
||||||
|
|
||||||
return PhpEnvironments.shared.homebrewPackage.version
|
return PhpEnvironments.shared.homebrewPackage.version
|
||||||
@@ -146,8 +177,13 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
// Avoid inserting a duplicate
|
// Avoid inserting a duplicate
|
||||||
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||||
|
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)
|
supportedVersions.insert(phpAlias)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
availablePhpVersions = Array(supportedVersions)
|
availablePhpVersions = Array(supportedVersions)
|
||||||
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
|
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
|
||||||
|
@@ -67,7 +67,7 @@ class PhpExtension {
|
|||||||
|
|
||||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||||
|
|
||||||
self.enabled = !line.contains(";")
|
self.enabled = !line.starts(with: ";")
|
||||||
self.file = file
|
self.file = file
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ class PhpExtension {
|
|||||||
You may need to restart the other services in order for this change to apply.
|
You may need to restart the other services in order for this change to apply.
|
||||||
*/
|
*/
|
||||||
func toggle() async {
|
func toggle() async {
|
||||||
let newLine = enabled
|
let newLine = !line.starts(with: ";")
|
||||||
// DISABLED: Commented out line
|
// DISABLED: Commented out line
|
||||||
? "; \(line)"
|
? "; \(line)"
|
||||||
// ENABLED: Line where the comment delimiter (;) is removed
|
// ENABLED: Line where the comment delimiter (;) is removed
|
||||||
@@ -84,14 +84,14 @@ class PhpExtension {
|
|||||||
|
|
||||||
await sed(file: file, original: line, replacement: newLine)
|
await sed(file: file, original: line, replacement: newLine)
|
||||||
|
|
||||||
enabled.toggle()
|
self.enabled = !newLine.starts(with: ";")
|
||||||
|
self.line = newLine
|
||||||
|
|
||||||
if !isRunningTests {
|
if !isRunningTests {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
MainMenu.shared.rebuild()
|
MainMenu.shared.rebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Static Methods
|
// MARK: - Static Methods
|
||||||
|
@@ -12,19 +12,46 @@ class PhpInstallation {
|
|||||||
|
|
||||||
var versionNumber: VersionNumber
|
var versionNumber: VersionNumber
|
||||||
|
|
||||||
|
var iniFiles: [PhpConfigurationFile] = []
|
||||||
|
|
||||||
|
var isMissingBinary: Bool = false
|
||||||
|
|
||||||
var isHealthy: Bool = true
|
var isHealthy: Bool = true
|
||||||
|
|
||||||
|
var extensions: [PhpExtension] {
|
||||||
|
return self.iniFiles.flatMap({ $0.extensions })
|
||||||
|
}
|
||||||
|
|
||||||
|
var formulaName: String {
|
||||||
|
let version = self.versionNumber.short
|
||||||
|
|
||||||
|
if version == PhpEnvironments.brewPhpAlias {
|
||||||
|
return "php"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "php@\(self.versionNumber.short)"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
In order to determine details about a PHP installation,
|
In order to determine details about a PHP installation,
|
||||||
we’ll simply run `php-config --version` in the relevant directory.
|
we’ll simply run `php-config --version` in the relevant directory.
|
||||||
*/
|
*/
|
||||||
init(_ version: String) {
|
init(_ version: String) {
|
||||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
|
||||||
|
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||||
|
|
||||||
let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
versionNumber = VersionNumber.make(from: version)!
|
||||||
|
|
||||||
self.versionNumber = VersionNumber.make(from: version)!
|
determineVersion(phpConfigExecutablePath, phpExecutablePath)
|
||||||
|
determineHealth(phpExecutablePath)
|
||||||
|
determineIniFiles(phpExecutablePath)
|
||||||
|
|
||||||
|
// Find all enabled extensions
|
||||||
|
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
|
||||||
|
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpConfigExecutablePath) {
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
let longVersionString = Command.execute(
|
let longVersionString = Command.execute(
|
||||||
path: phpConfigExecutablePath,
|
path: phpConfigExecutablePath,
|
||||||
@@ -34,9 +61,15 @@ class PhpInstallation {
|
|||||||
|
|
||||||
// The parser should always work, or the string has to be very unusual.
|
// The parser should always work, or the string has to be very unusual.
|
||||||
// If so, the app SHOULD crash, so that the users report what's up.
|
// If so, the app SHOULD crash, so that the users report what's up.
|
||||||
self.versionNumber = try! VersionNumber.parse(longVersionString)
|
versionNumber = try! VersionNumber.parse(longVersionString)
|
||||||
|
} else {
|
||||||
|
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
|
||||||
|
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
|
||||||
|
isMissingBinary = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func determineHealth(_ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpExecutablePath) {
|
if FileSystem.fileExists(phpExecutablePath) {
|
||||||
let testCommand = Command.execute(
|
let testCommand = Command.execute(
|
||||||
path: phpExecutablePath,
|
path: phpExecutablePath,
|
||||||
@@ -53,4 +86,18 @@ class PhpInstallation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func determineIniFiles(_ phpExecutablePath: String) {
|
||||||
|
let paths = ActiveShell.shared
|
||||||
|
.sync("\(phpExecutablePath) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
|
.split(separator: "\n")
|
||||||
|
.map { String($0) }
|
||||||
|
|
||||||
|
// See if any extensions are present in said .ini files
|
||||||
|
paths.forEach { (iniFilePath) in
|
||||||
|
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
|
||||||
|
iniFiles.append(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@ extension InternalSwitcher {
|
|||||||
return corrections.contains(true)
|
return corrections.contains(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - PHP FPM pool
|
// MARK: - Corrections
|
||||||
|
|
||||||
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
||||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||||
@@ -54,37 +54,7 @@ extension InternalSwitcher {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||||
return [
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/php-fpm.d/valet-fpm.conf",
|
|
||||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
|
||||||
replacements: [
|
|
||||||
"VALET_USER": Paths.whoami,
|
|
||||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
|
||||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
|
||||||
],
|
|
||||||
applies: { Valet.shared.version!.major > 2 }
|
|
||||||
),
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/conf.d/error_log.ini",
|
|
||||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
|
||||||
replacements: [
|
|
||||||
"VALET_USER": Paths.whoami,
|
|
||||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
|
||||||
],
|
|
||||||
applies: { return true }
|
|
||||||
),
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/conf.d/php-memory-limits.ini",
|
|
||||||
source: "/cli/stubs/php-memory-limits.ini",
|
|
||||||
replacements: [:],
|
|
||||||
applies: { return true }
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
|
||||||
let files = self.getExpectedConfigurationFiles(for: version)
|
let files = self.getExpectedConfigurationFiles(for: version)
|
||||||
|
|
||||||
// For each of the files, attempt to fix anything that is wrong
|
// For each of the files, attempt to fix anything that is wrong
|
||||||
@@ -124,6 +94,38 @@ extension InternalSwitcher {
|
|||||||
return outcomes.contains(true)
|
return outcomes.contains(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Internals
|
||||||
|
|
||||||
|
private func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||||
|
return [
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/php-fpm.d/valet-fpm.conf",
|
||||||
|
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||||
|
replacements: [
|
||||||
|
"VALET_USER": Paths.whoami,
|
||||||
|
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||||
|
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||||
|
],
|
||||||
|
applies: { Valet.shared.version!.major > 2 }
|
||||||
|
),
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/conf.d/error_log.ini",
|
||||||
|
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||||
|
replacements: [
|
||||||
|
"VALET_USER": Paths.whoami,
|
||||||
|
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||||
|
],
|
||||||
|
applies: { return true }
|
||||||
|
),
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/conf.d/php-memory-limits.ini",
|
||||||
|
source: "/cli/stubs/php-memory-limits.ini",
|
||||||
|
replacements: [:],
|
||||||
|
applies: { return true }
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ExpectedConfigurationFile {
|
public struct ExpectedConfigurationFile {
|
||||||
|
@@ -86,14 +86,37 @@ class RealShell: ShellProtocol {
|
|||||||
|
|
||||||
// MARK: - Shellable Protocol
|
// MARK: - Shellable Protocol
|
||||||
|
|
||||||
|
func sync(_ command: String) -> ShellOutput {
|
||||||
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
|
let outputPipe = Pipe()
|
||||||
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
|
sleep(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
task.standardOutput = outputPipe
|
||||||
|
task.standardError = errorPipe
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
|
||||||
|
if Log.shared.verbosity == .cli {
|
||||||
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
func pipe(_ command: String) async -> ShellOutput {
|
func pipe(_ command: String) async -> ShellOutput {
|
||||||
let task = getShellProcess(for: command)
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
let outputPipe = Pipe()
|
let outputPipe = Pipe()
|
||||||
let errorPipe = Pipe()
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
// Seriously slow down how long it takes for the shell to return output
|
|
||||||
// (in order to debug or identify async issues)
|
|
||||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
Log.info("[SLOW SHELL] \(command)")
|
Log.info("[SLOW SHELL] \(command)")
|
||||||
await delay(seconds: 3.0)
|
await delay(seconds: 3.0)
|
||||||
@@ -104,17 +127,17 @@ class RealShell: ShellProtocol {
|
|||||||
task.launch()
|
task.launch()
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
|
|
||||||
let stdOut = String(
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
let stdErr = String(
|
|
||||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
if Log.shared.verbosity == .cli {
|
if Log.shared.verbosity == .cli {
|
||||||
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func log(task: Process, stdOut: String, stdErr: String) {
|
||||||
var args = task.arguments ?? []
|
var args = task.arguments ?? []
|
||||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||||
var log = """
|
var log = """
|
||||||
@@ -141,9 +164,6 @@ class RealShell: ShellProtocol {
|
|||||||
Log.info(log)
|
Log.info(log)
|
||||||
}
|
}
|
||||||
|
|
||||||
return .out(stdOut, stdErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func quiet(_ command: String) async {
|
func quiet(_ command: String) async {
|
||||||
_ = await self.pipe(command)
|
_ = await self.pipe(command)
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,16 @@ protocol ShellProtocol {
|
|||||||
*/
|
*/
|
||||||
var PATH: String { get }
|
var PATH: String { get }
|
||||||
|
|
||||||
|
/**
|
||||||
|
Run a command synchronously. Use with caution.
|
||||||
|
|
||||||
|
Common usage:
|
||||||
|
```
|
||||||
|
let output = Shell.sync("php -v")
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
func sync(_ command: String) -> ShellOutput
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Run a command asynchronously.
|
Run a command asynchronously.
|
||||||
Returns the most relevant output (prefers error output if it exists).
|
Returns the most relevant output (prefers error output if it exists).
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// PhpFormulaeStatus.swift
|
// BusyStatus.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 02/05/2023.
|
// Created by Nico Verbruggen on 02/05/2023.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PhpFormulaeStatus: ObservableObject {
|
class BusyStatus: ObservableObject {
|
||||||
@Published var busy: Bool
|
@Published var busy: Bool
|
||||||
@Published var title: String
|
@Published var title: String
|
||||||
@Published var description: String
|
@Published var description: String
|
||||||
@@ -18,4 +18,12 @@ class PhpFormulaeStatus: ObservableObject {
|
|||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func notBusy() -> BusyStatus {
|
||||||
|
return BusyStatus(busy: false, title: "", description: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func busy() -> BusyStatus {
|
||||||
|
return BusyStatus(busy: false, title: "", description: "")
|
||||||
|
}
|
||||||
}
|
}
|
@@ -73,12 +73,26 @@ public struct TestableConfiguration: Codable {
|
|||||||
: .fake(.text)
|
: .fake(.text)
|
||||||
]) { (_, new) in new }
|
]) { (_, new) in new }
|
||||||
|
|
||||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"]
|
// PHP configuration files
|
||||||
= version.long
|
self.shellOutput["/opt/homebrew/opt/php@\(version.short)/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||||
|
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||||
|
|
||||||
|
// PHP Homebrew operations
|
||||||
|
self.shellOutput["/opt/homebrew/bin/brew unlink php@\(version.short)"] = .delayed(0.2, "OK")
|
||||||
|
self.shellOutput["sudo /opt/homebrew/bin/brew services stop php@\(version.short)"] = .delayed(0.2, "OK")
|
||||||
|
self.shellOutput["sudo /opt/homebrew/bin/brew services start php@\(version.short)"] = .delayed(0.2, "OK")
|
||||||
|
self.shellOutput["/opt/homebrew/bin/brew link php@\(version.short) --overwrite --force"] = .delayed(0.2, "OK")
|
||||||
|
|
||||||
|
// PHP version output
|
||||||
|
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"] = version.long
|
||||||
|
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php -v"] = "OK"
|
||||||
|
|
||||||
if primary {
|
if primary {
|
||||||
self.shellOutput["ls /opt/homebrew/opt | grep php"]
|
// Files expected to be present for currently linked PHP version
|
||||||
= .instant("php")
|
self.shellOutput["ls /opt/homebrew/opt | grep php"] =
|
||||||
|
.instant("php")
|
||||||
|
self.shellOutput["/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||||
|
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||||
self.filesystem["/opt/homebrew/opt/php"]
|
self.filesystem["/opt/homebrew/opt/php"]
|
||||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
||||||
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
||||||
@@ -89,12 +103,8 @@ public struct TestableConfiguration: Codable {
|
|||||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
|
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
|
||||||
self.commandOutput["/opt/homebrew/bin/php-config --version"]
|
self.commandOutput["/opt/homebrew/bin/php-config --version"]
|
||||||
= version.long
|
= version.long
|
||||||
self.commandOutput["/opt/homebrew/bin/php -r echo php_ini_scanned_files();"] =
|
|
||||||
"""
|
|
||||||
/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini,
|
|
||||||
"""
|
|
||||||
} else {
|
} else {
|
||||||
|
// Output expected to be present for non-linked PHP versions
|
||||||
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
||||||
BatchFakeShellOutput.instant(
|
BatchFakeShellOutput.instant(
|
||||||
self.secondaryPhpVersions
|
self.secondaryPhpVersions
|
||||||
@@ -103,6 +113,7 @@ public struct TestableConfiguration: Codable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// swiftlint:enable function_body_length
|
||||||
|
|
||||||
// MARK: Interactions
|
// MARK: Interactions
|
||||||
|
|
||||||
|
@@ -19,6 +19,17 @@ public class TestableShell: ShellProtocol {
|
|||||||
|
|
||||||
var expectations: [String: BatchFakeShellOutput] = [:]
|
var expectations: [String: BatchFakeShellOutput] = [:]
|
||||||
|
|
||||||
|
func sync(_ command: String) -> ShellOutput {
|
||||||
|
// This assertion will only fire during test builds
|
||||||
|
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
|
||||||
|
|
||||||
|
guard let expectation = expectations[command] else {
|
||||||
|
return .err("No Expected Output")
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectation.syncOutput()
|
||||||
|
}
|
||||||
|
|
||||||
func quiet(_ command: String) async {
|
func quiet(_ command: String) async {
|
||||||
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
||||||
}
|
}
|
||||||
@@ -112,6 +123,29 @@ struct BatchFakeShellOutput: Codable {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Outputs the fake shell output as expected, but does this synchronously.
|
||||||
|
*/
|
||||||
|
public func syncOutput(
|
||||||
|
ignoreDelay: Bool = false
|
||||||
|
) -> ShellOutput {
|
||||||
|
let output = ShellOutput.empty()
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
if !ignoreDelay {
|
||||||
|
Thread.sleep(forTimeInterval: item.delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.stream == .stdErr {
|
||||||
|
output.err += item.output
|
||||||
|
} else if item.stream == .stdOut {
|
||||||
|
output.out += item.output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
||||||
*/
|
*/
|
||||||
|
@@ -18,8 +18,15 @@
|
|||||||
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
||||||
<p><b>Get the latest on Twitter or Mastodon.</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
<p><b>Get the latest on Twitter or Mastodon.</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
||||||
<p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p>
|
<p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p>
|
||||||
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(Some names have been omitted due to their sponsorships being private. Thank you all!)
|
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(Some names have been omitted due to their sponsorships being private. Thank you all!)</p>
|
||||||
|
<p><b>Localization credits:</b></br>
|
||||||
|
‐ English, Dutch</b> by @nicoverbruggen</br>
|
||||||
|
‐ Vietnamese</b> by @xuandung38</br>
|
||||||
|
‐ German</b> by @dsturm</br>
|
||||||
|
‐ Portuguese</b> by @joseborges</br>
|
||||||
|
‐ French</b> by @nhedger, @tplesnar</br>
|
||||||
|
‐ Chinese</b> by @guanguans</br>
|
||||||
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@@ -83,6 +83,9 @@ class App {
|
|||||||
/** The window controller of the PHP version manager window. */
|
/** The window controller of the PHP version manager window. */
|
||||||
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
||||||
|
|
||||||
|
/** The window controller of the PHP extension manager window. */
|
||||||
|
var phpExtensionManagerWindowController: PhpExtensionManagerWindowController?
|
||||||
|
|
||||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||||
var detectedApplications: [Application] = []
|
var detectedApplications: [Application] = []
|
||||||
|
|
||||||
|
@@ -23,12 +23,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
*/
|
*/
|
||||||
let state: App
|
let state: App
|
||||||
|
|
||||||
/**
|
|
||||||
The MainMenu singleton is responsible for rendering the
|
|
||||||
menu bar item and its menu, as well as its actions.
|
|
||||||
*/
|
|
||||||
let menu: MainMenu
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The paths singleton that determines where Homebrew is installed,
|
The paths singleton that determines where Homebrew is installed,
|
||||||
and where to look for binaries.
|
and where to look for binaries.
|
||||||
@@ -96,7 +90,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.state = App.shared
|
self.state = App.shared
|
||||||
self.menu = MainMenu.shared
|
|
||||||
self.paths = Paths.shared
|
self.paths = Paths.shared
|
||||||
self.valet = Valet.shared
|
self.valet = Valet.shared
|
||||||
self.brew = Brew.shared
|
self.brew = Brew.shared
|
||||||
@@ -109,6 +102,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
static func initializeTestingProfile(_ path: String) {
|
static func initializeTestingProfile(_ path: String) {
|
||||||
Log.info("The configuration with path `\(path)` is being requested...")
|
Log.info("The configuration with path `\(path)` is being requested...")
|
||||||
|
// Clear for PHP Guard
|
||||||
|
Stats.clearCurrentGlobalPhpVersion()
|
||||||
|
// Load the configuration file
|
||||||
TestableConfiguration.loadFrom(path: path).apply()
|
TestableConfiguration.loadFrom(path: path).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +125,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
setupNotifications()
|
setupNotifications()
|
||||||
|
|
||||||
Task { // Make sure the menu performs its initial checks
|
Task { // Make sure the menu performs its initial checks
|
||||||
await menu.startup()
|
await MainMenu.shared.startup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22155"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/>
|
||||||
<capability name="Image references" minToolsVersion="12.0"/>
|
<capability name="Image references" minToolsVersion="12.0"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
||||||
@@ -63,6 +63,13 @@
|
|||||||
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="bPr-YU-lg4"/>
|
||||||
|
<menuItem title="actions" enabled="NO" id="cAS-FU-WUA" userLabel="actions" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_actions"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</menuItem>
|
||||||
</items>
|
</items>
|
||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
@@ -822,11 +829,11 @@ Gw
|
|||||||
<scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
|
<scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
||||||
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w">
|
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="611" height="294"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="281"/>
|
<rect key="frame" x="0.0" y="0.0" width="611" height="266"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<size key="intercellSpacing" width="17" height="0.0"/>
|
<size key="intercellSpacing" width="17" height="0.0"/>
|
||||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
@@ -1073,15 +1080,15 @@ Gw
|
|||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/>
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
|
<scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
|
||||||
<rect key="frame" x="0.0" y="293" width="626" height="16"/>
|
<rect key="frame" x="0.0" y="294" width="611" height="15"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
|
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
|
||||||
<rect key="frame" x="610" y="28" width="16" height="281"/>
|
<rect key="frame" x="611" y="28" width="15" height="266"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
|
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="28"/>
|
<rect key="frame" x="0.0" y="0.0" width="611" height="28"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</tableHeaderView>
|
</tableHeaderView>
|
||||||
</scrollView>
|
</scrollView>
|
||||||
@@ -1092,9 +1099,18 @@ Gw
|
|||||||
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</progressIndicator>
|
</progressIndicator>
|
||||||
|
<customView translatesAutoresizingMaskIntoConstraints="NO" id="wcV-ed-8Bv">
|
||||||
|
<rect key="frame" x="113" y="5" width="400" height="300"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="400" id="HCo-LG-x3N"/>
|
||||||
|
<constraint firstAttribute="height" constant="300" id="Xpi-Rl-xmb"/>
|
||||||
|
</constraints>
|
||||||
|
</customView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/>
|
<constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/>
|
||||||
|
<constraint firstItem="wcV-ed-8Bv" firstAttribute="centerX" secondItem="rIZ-4U-bhj" secondAttribute="centerX" id="DPz-kQ-aP0"/>
|
||||||
|
<constraint firstItem="wcV-ed-8Bv" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" id="HCW-zJ-gSY"/>
|
||||||
<constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/>
|
<constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/>
|
<constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/>
|
||||||
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/>
|
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/>
|
||||||
@@ -1103,6 +1119,7 @@ Gw
|
|||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<connections>
|
<connections>
|
||||||
|
<outlet property="noResultsView" destination="wcV-ed-8Bv" id="K3s-fb-1aN"/>
|
||||||
<outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/>
|
<outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/>
|
||||||
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
||||||
</connections>
|
</connections>
|
||||||
|
@@ -142,7 +142,7 @@ class Startup {
|
|||||||
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
||||||
.contains("Library not loaded")
|
.contains("Library not loaded")
|
||||||
},
|
},
|
||||||
name: "`no dyld issue detected",
|
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||||
titleText: "startup.errors.dyld_library.title".localized,
|
titleText: "startup.errors.dyld_library.title".localized,
|
||||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||||
Paths.optPath
|
Paths.optPath
|
||||||
|
@@ -28,8 +28,6 @@ import Foundation
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
MainMenu.shared.setBusyImage()
|
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
|
|
||||||
window = TerminalProgressWindowController.display(
|
window = TerminalProgressWindowController.display(
|
||||||
title: "alert.composer_progress.title".localized,
|
title: "alert.composer_progress.title".localized,
|
||||||
@@ -106,14 +104,11 @@ import Foundation
|
|||||||
|
|
||||||
private func removeBusyStatus() {
|
private func removeBusyStatus() {
|
||||||
PhpEnvironments.shared.isBusy = false
|
PhpEnvironments.shared.isBusy = false
|
||||||
Task { @MainActor in
|
|
||||||
MainMenu.shared.updatePhpVersionInStatusBar()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Alert
|
// MARK: Alert
|
||||||
|
|
||||||
@MainActor private func presentMissingAlert() {
|
private func presentMissingAlert() {
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.composer_missing.title".localized,
|
title: "alert.composer_missing.title".localized,
|
||||||
|
@@ -22,7 +22,7 @@ struct ProjectTypeDetection {
|
|||||||
This list is checked first to see if a project dependency can be mapped to a certain project type.
|
This list is checked first to see if a project dependency can be mapped to a certain project type.
|
||||||
*/
|
*/
|
||||||
public static let SpecificDependencyList = [
|
public static let SpecificDependencyList = [
|
||||||
"roots/bedrock": "Bedrock",
|
"roots/bedrock-autoloader": "Bedrock",
|
||||||
"cakephp/app": "CakePHP",
|
"cakephp/app": "CakePHP",
|
||||||
"craftcms/craft": "Craft",
|
"craftcms/craft": "Craft",
|
||||||
"drupal/core": "Drupal",
|
"drupal/core": "Drupal",
|
||||||
|
@@ -8,16 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class BrewFormulaeObservable: ObservableObject {
|
|
||||||
@Published var phpVersions: [BrewFormula] = []
|
|
||||||
|
|
||||||
var upgradeable: [BrewFormula] {
|
|
||||||
return phpVersions.filter { formula in
|
|
||||||
formula.hasUpgrade
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Brew {
|
class Brew {
|
||||||
static let shared = Brew()
|
static let shared = Brew()
|
||||||
|
|
||||||
@@ -45,10 +35,11 @@ class Brew {
|
|||||||
|
|
||||||
/// Each formula for each PHP version that can be installed.
|
/// Each formula for each PHP version that can be installed.
|
||||||
public static let phpVersionFormulae = [
|
public static let phpVersionFormulae = [
|
||||||
"8.3": "shivammathur/php/php@8.3",
|
"8.4": "shivammathur/php/php@8.4",
|
||||||
|
"8.3": "php@8.3",
|
||||||
"8.2": "php@8.2",
|
"8.2": "php@8.2",
|
||||||
"8.1": "php@8.1",
|
"8.1": "php@8.1",
|
||||||
"8.0": "php@8.0",
|
"8.0": "shivammathur/php/php@8.0",
|
||||||
"7.4": "shivammathur/php/php@7.4",
|
"7.4": "shivammathur/php/php@7.4",
|
||||||
"7.3": "shivammathur/php/php@7.3",
|
"7.3": "shivammathur/php/php@7.3",
|
||||||
"7.2": "shivammathur/php/php@7.2",
|
"7.2": "shivammathur/php/php@7.2",
|
||||||
|
@@ -27,6 +27,21 @@ class BrewDiagnostics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Logs a bunch of useful information during startup.
|
||||||
|
*/
|
||||||
|
public static func logBootInformation() {
|
||||||
|
Log.info(BrewDiagnostics.customCaskInstalled
|
||||||
|
? "[BREW] The app has been installed via Homebrew Cask."
|
||||||
|
: "[BREW] The app has been installed directly (optimal)."
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.info(BrewDiagnostics.usesNginxFullFormula
|
||||||
|
? "[BREW] The app will be using the `nginx-full` formula."
|
||||||
|
: "[BREW] The app will be using the `nginx` formula."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determines whether the PHP Monitor Cask is installed.
|
Determines whether the PHP Monitor Cask is installed.
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +61,43 @@ class BrewDiagnostics {
|
|||||||
return destination.contains("/nginx-full/")
|
return destination.contains("/nginx-full/")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
/**
|
||||||
|
It is possible to have outdated symlinks for PHP installations. This can mean that certain PHP installations
|
||||||
|
are going to be reported incorrectly (e.g. `php@8.2` links to an installation in a `8.3` folder after an upgrade).
|
||||||
|
|
||||||
|
To ensure this does not cause issues, PHP Monitor will automatically remove all incorrect PHP symlinks.
|
||||||
|
*/
|
||||||
|
public static func checkForOutdatedPhpInstallationSymlinks() async {
|
||||||
|
// Set up a regular expression
|
||||||
|
let regex = try! NSRegularExpression(pattern: "^php@[0-9]+\\.[0-9]+$", options: .caseInsensitive)
|
||||||
|
|
||||||
|
// Check for incorrect versions
|
||||||
|
if let contents = try? FileSystem.getShallowContentsOfDirectory("\(Paths.optPath)")
|
||||||
|
.filter({
|
||||||
|
let range = NSRange($0.startIndex..., in: $0)
|
||||||
|
return regex.firstMatch(in: $0, options: [], range: range) != nil
|
||||||
|
}) {
|
||||||
|
|
||||||
|
for symlink in contents {
|
||||||
|
let version = symlink.replacingOccurrences(of: "php@", with: "")
|
||||||
|
if let destination = try? FileSystem.getDestinationOfSymlink("\(Paths.optPath)/\(symlink)") {
|
||||||
|
if !destination.contains("Cellar/php/\(version)")
|
||||||
|
&& !destination.contains("Cellar/php@\(version)") {
|
||||||
|
Log.err("Symlink for \(symlink) is incorrect. Removing...")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove("\(Paths.optPath)/\(symlink)")
|
||||||
|
Log.info("Incorrect symlink for \(symlink) has been successfully removed.")
|
||||||
|
} catch {
|
||||||
|
Log.err("Symlink for \(symlink) was incorrect but could not be removed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not read symlink at: \(Paths.optPath)/\(symlink)! Symlink check skipped.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||||
This will then result in two different aliases claiming to point to the same formula (`php`).
|
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||||
|
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// BrewPhpExtension.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 27/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct BrewPhpExtension: Hashable, Comparable {
|
||||||
|
let name: String
|
||||||
|
let phpVersion: String
|
||||||
|
let isInstalled: Bool
|
||||||
|
let path: String
|
||||||
|
let dependencies: [String]
|
||||||
|
|
||||||
|
var extensionDependencies: [String] {
|
||||||
|
return dependencies
|
||||||
|
.filter {
|
||||||
|
$0.contains("shivammathur/extensions/") && $0.contains("@\(phpVersion)")
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
$0.replacingOccurrences(of: "shivammathur/extensions/", with: "")
|
||||||
|
.replacingOccurrences(of: "@\(phpVersion)", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var formulaName: String {
|
||||||
|
return "\(name)@\(phpVersion)"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(path: String, name: String, phpVersion: String) {
|
||||||
|
self.path = path
|
||||||
|
self.name = name
|
||||||
|
self.phpVersion = phpVersion
|
||||||
|
|
||||||
|
self.isInstalled = BrewPhpExtension.hasInstallationReceipt(
|
||||||
|
for: "\(name)@\(phpVersion)"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.dependencies = BrewPhpExtension.extractDependencies(from: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAlternativeInstall: Bool {
|
||||||
|
guard let php = PhpEnvironments.shared.cachedPhpInstallations[self.phpVersion] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let alreadyDiscovered = php.extensions.contains(where: { $0.name == self.name })
|
||||||
|
|
||||||
|
return alreadyDiscovered && !isInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func firstDependent(in exts: [BrewPhpExtension]) -> BrewPhpExtension? {
|
||||||
|
return exts
|
||||||
|
.filter({ $0.isInstalled })
|
||||||
|
.first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasInstallationReceipt(for formulaName: String) -> Bool {
|
||||||
|
return FileSystem.fileExists("\(Paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name < rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name == rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractDependencies(from path: String) -> [String] {
|
||||||
|
let regexPattern = #"depends_on "(.*)""#
|
||||||
|
var dependencies: [String] = []
|
||||||
|
|
||||||
|
guard let content = try? FileSystem.getStringFromFile(path) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let regex = try NSRegularExpression(pattern: regexPattern, options: [])
|
||||||
|
let range = NSRange(content.startIndex..<content.endIndex, in: content)
|
||||||
|
let matches = regex.matches(in: content, options: [], range: range)
|
||||||
|
|
||||||
|
for match in matches {
|
||||||
|
if let range = Range(match.range(at: 1), in: content) {
|
||||||
|
let dependencyName = String(content[range])
|
||||||
|
dependencies.append(dependencyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// BrewFormula.swift
|
// BrewPhpFormula.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 17/03/2023.
|
// Created by Nico Verbruggen on 17/03/2023.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct BrewFormula {
|
struct BrewPhpFormula: Equatable {
|
||||||
/// Name of the formula.
|
/// Name of the formula.
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ struct BrewFormula {
|
|||||||
/// The upgrade that is currently available, if it exists.
|
/// The upgrade that is currently available, if it exists.
|
||||||
let upgradeVersion: String?
|
let upgradeVersion: String?
|
||||||
|
|
||||||
|
// TODO: A rebuild attribute could be checked, to check if a Tap update exists for a pre-release version
|
||||||
|
|
||||||
/// Whether this formula is a stable version of PHP.
|
/// Whether this formula is a stable version of PHP.
|
||||||
let prerelease: Bool
|
let prerelease: Bool
|
||||||
|
|
||||||
@@ -48,6 +50,25 @@ struct BrewFormula {
|
|||||||
return upgradeVersion != nil
|
return upgradeVersion != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this formula alias is different.
|
||||||
|
var hasUpgradedFormulaAlias: Bool {
|
||||||
|
return self.shortVersion == PhpEnvironments.homebrewBrewPhpAlias
|
||||||
|
&& PhpEnvironments.homebrewBrewPhpAlias != PhpEnvironments.brewPhpAlias
|
||||||
|
}
|
||||||
|
|
||||||
|
var unavailableAfterUpgrade: Bool {
|
||||||
|
if installedVersion == nil || upgradeVersion == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let installed = try? VersionNumber.parse(self.installedVersion!),
|
||||||
|
let upgrade = try? VersionNumber.parse(self.upgradeVersion!) {
|
||||||
|
return upgrade.short != installed.short
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/// The associated Homebrew folder with this PHP formula.
|
/// The associated Homebrew folder with this PHP formula.
|
||||||
var homebrewFolder: String {
|
var homebrewFolder: String {
|
||||||
let resolved = name
|
let resolved = name
|
||||||
@@ -60,7 +81,7 @@ struct BrewFormula {
|
|||||||
/// The short version associated with this formula, if installed.
|
/// The short version associated with this formula, if installed.
|
||||||
var shortVersion: String? {
|
var shortVersion: String? {
|
||||||
guard let version = self.installedVersion else {
|
guard let version = self.installedVersion else {
|
||||||
return nil
|
return self.displayName.replacingOccurrences(of: "PHP ", with: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return VersionNumber.make(from: version)?.short ?? nil
|
return VersionNumber.make(from: version)?.short ?? nil
|
||||||
@@ -81,6 +102,7 @@ struct BrewFormula {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?.isHealthy ?? nil
|
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?
|
||||||
|
.isHealthy ?? nil
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -8,22 +8,23 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol HandlesBrewFormulae {
|
protocol HandlesBrewPhpFormulae {
|
||||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula]
|
func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula]
|
||||||
func refreshPhpVersions(loadOutdated: Bool) async
|
func refreshPhpVersions(loadOutdated: Bool) async
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HandlesBrewFormulae {
|
extension HandlesBrewPhpFormulae {
|
||||||
public func refreshPhpVersions(loadOutdated: Bool) async {
|
public func refreshPhpVersions(loadOutdated: Bool) async {
|
||||||
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
Brew.shared.formulae.phpVersions = items
|
Brew.shared.formulae.phpVersions = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BrewFormulaeHandler: HandlesBrewFormulae {
|
class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] {
|
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
|
||||||
var outdated: [OutdatedFormula]?
|
var outdated: [OutdatedFormula]?
|
||||||
|
|
||||||
if loadOutdated {
|
if loadOutdated {
|
||||||
@@ -43,17 +44,17 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Brew.phpVersionFormulae.map { (version, formula) in
|
return Brew.phpVersionFormulae.map { (version, formula) in
|
||||||
let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?.versionNumber.text
|
var fullVersion: String?
|
||||||
|
|
||||||
var upgradeVersion: String?
|
var upgradeVersion: String?
|
||||||
|
|
||||||
if let version = fullVersion {
|
if let install = PhpEnvironments.shared.cachedPhpInstallations[version] {
|
||||||
|
fullVersion = install.versionNumber.text
|
||||||
upgradeVersion = outdated?.first(where: { formula in
|
upgradeVersion = outdated?.first(where: { formula in
|
||||||
return formula.installed_versions.contains(version)
|
return formula.name == install.formulaName
|
||||||
})?.current_version
|
})?.current_version
|
||||||
}
|
}
|
||||||
|
|
||||||
return BrewFormula(
|
return BrewPhpFormula(
|
||||||
name: formula,
|
name: formula,
|
||||||
displayName: "PHP \(version)",
|
displayName: "PHP \(version)",
|
||||||
installedVersion: fullVersion,
|
installedVersion: fullVersion,
|
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// BrewTapFormulae.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 01/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class BrewTapFormulae {
|
||||||
|
public static func from(tap: String) -> [String: [BrewPhpExtension]] {
|
||||||
|
let directory = "\(Paths.tapPath)/\(tap)/Formula"
|
||||||
|
|
||||||
|
let files = try? FileSystem.getShallowContentsOfDirectory(directory)
|
||||||
|
|
||||||
|
var availableExtensions = [String: [BrewPhpExtension]]()
|
||||||
|
|
||||||
|
guard let files = files else {
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
|
||||||
|
let regex = try! NSRegularExpression(pattern: "(\\w+)@(\\d+\\.\\d+)\\.rb")
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
let matches = regex.matches(in: file, range: NSRange(file.startIndex..., in: file))
|
||||||
|
if let match = matches.first {
|
||||||
|
if let phpExtensionRange = Range(match.range(at: 1), in: file),
|
||||||
|
let versionRange = Range(match.range(at: 2), in: file) {
|
||||||
|
// Determine what the extension's name is
|
||||||
|
let phpExtensionName = String(file[phpExtensionRange])
|
||||||
|
|
||||||
|
// Determine what PHP version this is for
|
||||||
|
let phpVersion = String(file[versionRange])
|
||||||
|
|
||||||
|
// Create a new BrewPhpExtension object (determines if installed)
|
||||||
|
let phpExtension = BrewPhpExtension(
|
||||||
|
path: "\(Paths.tapPath)/\(tap)/Formula/\(file)",
|
||||||
|
name: phpExtensionName,
|
||||||
|
phpVersion: phpVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
// Append the extension to the list
|
||||||
|
var extensions = availableExtensions[phpVersion, default: []]
|
||||||
|
extensions.append(phpExtension)
|
||||||
|
availableExtensions[phpVersion] = extensions.sorted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,8 @@ import Foundation
|
|||||||
|
|
||||||
protocol BrewCommand {
|
protocol BrewCommand {
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
||||||
|
|
||||||
|
func getCommandTitle() -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BrewCommand {
|
extension BrewCommand {
|
||||||
@@ -31,6 +33,44 @@ extension BrewCommand {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
var loggedMessages: [String] = []
|
||||||
|
|
||||||
|
let (process, _) = try! await Shell.attach(
|
||||||
|
command,
|
||||||
|
didReceiveOutput: { text, _ in
|
||||||
|
if !text.isEmpty {
|
||||||
|
Log.perf(text)
|
||||||
|
loggedMessages.append(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (number, text) = self.reportInstallationProgress(text) {
|
||||||
|
onProgress(.create(value: number, title: getCommandTitle(), description: text))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withTimeout: .minutes(15)
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.terminationStatus <= 0 {
|
||||||
|
loggedMessages = []
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/php") {
|
||||||
|
let command = "brew tap shivammathur/php"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") {
|
||||||
|
let command = "brew tap shivammathur/extensions"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BrewCommandProgress {
|
struct BrewCommandProgress {
|
||||||
|
@@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
// Restart PHP-FPM
|
||||||
|
if let installed = self.installing.first {
|
||||||
|
await Actions.restartPhpFpm(version: installed.phpVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,91 @@
|
|||||||
|
//
|
||||||
|
// 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 Actions.restartPhpFpm(version: phpExtension.phpVersion)
|
||||||
|
|
||||||
|
await MainMenu.shared.refreshActiveInstallation()
|
||||||
|
|
||||||
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "phpman.steps.failure".localized, log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performExtensionCleanup(for ext: PhpExtension) async {
|
||||||
|
if ext.file.hasSuffix("20-\(ext.name).ini") {
|
||||||
|
// The extension's default configuration file can be removed
|
||||||
|
Log.info("The extension was found in a default extension .ini location. Purging that .ini file.")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove(ext.file)
|
||||||
|
} catch {
|
||||||
|
Log.err("The file `\(ext.file)` could not be removed.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The extension's default configuration file cannot be removed, it should be disabled instead
|
||||||
|
Log.info("The extension was not found in a default location. Disabling the extension only.")
|
||||||
|
if ext.enabled {
|
||||||
|
await ext.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -8,23 +8,33 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class InstallAndUpgradeCommand: BrewCommand {
|
class ModifyPhpVersionCommand: BrewCommand {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let installing: [BrewFormula]
|
let installing: [BrewPhpFormula]
|
||||||
let upgrading: [BrewFormula]
|
let upgrading: [BrewPhpFormula]
|
||||||
let phpGuard: PhpGuard
|
let phpGuard: PhpGuard
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
||||||
The process will be executed in two steps: first upgrades, then installations.
|
The process will be executed in two steps: first upgrades, then installations.
|
||||||
|
|
||||||
Upgrades come first because... well, otherwise installations may very well break.
|
Upgrades come first because... well, otherwise installations may very well break.
|
||||||
Each version that is installed will need to be checked afterwards (if it is OK).
|
Each version that is installed will need to be checked afterwards. Installing a
|
||||||
|
newer formula may break other PHP installations, which in turn need to be fixed.
|
||||||
|
|
||||||
|
- Important: If any PHP formula is a major upgrade that causes a PHP "version" to be
|
||||||
|
uninstalled, this is remedied by running `upgradeMainPhpFormula()`. This process
|
||||||
|
will ensure that the upgrade is applied, but the also that old version is
|
||||||
|
re-installed and linked again.
|
||||||
*/
|
*/
|
||||||
public init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
upgrading: [BrewFormula],
|
upgrading: [BrewPhpFormula],
|
||||||
installing: [BrewFormula]
|
installing: [BrewPhpFormula]
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.installing = installing
|
self.installing = installing
|
||||||
@@ -33,17 +43,32 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
let progressTitle = "Please wait..."
|
let progressTitle = "phpman.steps.wait".localized
|
||||||
|
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: progressTitle,
|
||||||
description: "PHP Monitor is preparing Homebrew..."
|
description: "phpman.steps.preparing".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
|
// 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 to run all upgrade and installation operations
|
||||||
try await self.upgradePackages(onProgress)
|
try await self.upgradePackages(onProgress)
|
||||||
try await self.installPackages(onProgress)
|
try await self.installPackages(onProgress)
|
||||||
|
} else {
|
||||||
|
// Simply upgrade `php` to the latest version
|
||||||
|
try await self.upgradeMainPhpFormula(unavailable!, onProgress)
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
}
|
||||||
|
|
||||||
// Re-check the installed versions
|
// Re-check the installed versions
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@@ -55,6 +80,27 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
await self.completedOperations(onProgress)
|
await self.completedOperations(onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func upgradeMainPhpFormula(
|
||||||
|
_ unavailable: BrewPhpFormula,
|
||||||
|
_ onProgress: @escaping (BrewCommandProgress) -> Void
|
||||||
|
) async throws {
|
||||||
|
// Determine which version was previously available (that will become unavailable)
|
||||||
|
guard let short = try? VersionNumber
|
||||||
|
.parse(unavailable.installedVersion!).short else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade the main formula
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) upgrade php;
|
||||||
|
\(Paths.brew) install php@\(short);
|
||||||
|
"""
|
||||||
|
|
||||||
|
// Run the upgrade command
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
// If no upgrades are needed, early exit
|
// If no upgrades are needed, early exit
|
||||||
if self.upgrading.isEmpty {
|
if self.upgrading.isEmpty {
|
||||||
@@ -117,35 +163,12 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
try await run(command, onProgress)
|
try await run(command, onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
|
||||||
var loggedMessages: [String] = []
|
|
||||||
|
|
||||||
let (process, _) = try! await Shell.attach(
|
|
||||||
command,
|
|
||||||
didReceiveOutput: { text, _ in
|
|
||||||
if !text.isEmpty {
|
|
||||||
Log.perf(text)
|
|
||||||
loggedMessages.append(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (number, text) = self.reportInstallationProgress(text) {
|
|
||||||
onProgress(.create(value: number, title: self.title, description: text))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
withTimeout: .minutes(15)
|
|
||||||
)
|
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
|
||||||
loggedMessages = []
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||||
// Reload and restart PHP versions
|
// Reload and restart PHP versions
|
||||||
onProgress(.create(value: 0.95, title: self.title, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: self.title, description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
// Ensure all symlinks are correctly linked
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Check which version of PHP are now installed
|
// Check which version of PHP are now installed
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@@ -164,9 +187,8 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
// Let the UI know that the installation has been completed
|
// Let the UI know that the installation has been completed
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 1,
|
value: 1,
|
||||||
title: "Operation completed!",
|
title: "phpman.steps.completed".localized,
|
||||||
description: "The installation has succeeded."
|
description: "phpman.steps.success".localized
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -21,13 +21,15 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
self.phpGuard = PhpGuard()
|
self.phpGuard = PhpGuard()
|
||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func getCommandTitle() -> String {
|
||||||
let progressTitle = "Removing PHP \(version)..."
|
return "phpman.steps.removing".localized("PHP \(version)...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: getCommandTitle(),
|
||||||
description: "Please wait while Homebrew removes PHP \(version)..."
|
description: "phpman.steps.wait".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
let command = """
|
let command = """
|
||||||
@@ -56,7 +58,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
if process.terminationStatus <= 0 {
|
||||||
onProgress(.create(value: 0.95, title: progressTitle, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(.create(value: 1, title: progressTitle, description: "The operation has succeeded."))
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
} else {
|
} else {
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
}
|
}
|
@@ -9,6 +9,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class FakeCommand: BrewCommand {
|
class FakeCommand: BrewCommand {
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "Hello"
|
||||||
|
}
|
||||||
|
|
||||||
let version: String
|
let version: String
|
||||||
|
|
||||||
init(version: String) {
|
init(version: String) {
|
||||||
|
41
phpmon/Domain/Menu/AppMenu.swift
Normal file
41
phpmon/Domain/Menu/AppMenu.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// AppMenu.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 27/06/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
class AppMenu {
|
||||||
|
|
||||||
|
// MARK: - Main Menu
|
||||||
|
|
||||||
|
static var appMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[0].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
static var sitesMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[1].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
static var editMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[2].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
static var windowMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[3].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
static var helpMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[4].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Submenu
|
||||||
|
|
||||||
|
static var actionsMenu: NSMenuItem? {
|
||||||
|
return sitesMenu?.items.last
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -283,12 +283,11 @@ extension MainMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@@ -298,13 +297,12 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func switchToPhpVersion(_ version: String) {
|
@objc func switchToPhpVersion(_ version: String) {
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@@ -325,13 +323,12 @@ extension MainMenu {
|
|||||||
*/
|
*/
|
||||||
func switchToPhp(_ version: String) async {
|
func switchToPhp(_ version: String) async {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
|
@@ -45,21 +45,16 @@ extension MainMenu {
|
|||||||
.broadcastServicesUpdate
|
.broadcastServicesUpdate
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
if behaviours.contains(.reloadsPhpInstallation) {
|
if behaviours.contains(.reloadsPhpInstallation) || behaviours.contains(.setsBusyUI) {
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.setsBusyUI) {
|
|
||||||
setBusyImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
var error: Error?
|
var error: Error?
|
||||||
|
|
||||||
do { try execute() } catch let e { error = e }
|
do { try execute() } catch let e {
|
||||||
|
error = e
|
||||||
if behaviours.contains(.setsBusyUI) {
|
Log.err(e)
|
||||||
PhpEnvironments.shared.isBusy = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor [self, error] in
|
Task { @MainActor [self, error] in
|
||||||
@@ -68,15 +63,18 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.updatesMenuBarContents) {
|
if behaviours.contains(.updatesMenuBarContents) {
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
} else if behaviours.contains(.setsBusyUI) {
|
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.broadcastServicesUpdate) {
|
if behaviours.contains(.broadcastServicesUpdate) {
|
||||||
Task { await ServicesManager.shared.reloadServicesStatus() }
|
Task { await ServicesManager.shared.reloadServicesStatus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if behaviours.contains(.setsBusyUI) {
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
if error != nil {
|
if error != nil {
|
||||||
return failure(error!)
|
return failure(error!)
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@ extension MainMenu {
|
|||||||
func startup() async {
|
func startup() async {
|
||||||
// Start with the icon
|
// Start with the icon
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
self.setStatusBar(image: NSImage.statusBarIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
if await Startup().checkEnvironment() {
|
if await Startup().checkEnvironment() {
|
||||||
@@ -32,19 +32,14 @@ extension MainMenu {
|
|||||||
// Determine what the `php` formula is aliased to
|
// Determine what the `php` formula is aliased to
|
||||||
await PhpEnvironments.shared.determinePhpAlias()
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
|
||||||
|
// Make sure that broken symlinks are removed ASAP
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Initialize preferences
|
// Initialize preferences
|
||||||
_ = Preferences.shared
|
_ = Preferences.shared
|
||||||
|
|
||||||
// Determine install method
|
// Put some useful diagnostics information in log
|
||||||
Log.info(BrewDiagnostics.customCaskInstalled
|
BrewDiagnostics.logBootInformation()
|
||||||
? "[BREW] The app has been installed via Homebrew Cask."
|
|
||||||
: "[BREW] The app has been installed directly (optimal)."
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.info(BrewDiagnostics.usesNginxFullFormula
|
|
||||||
? "[BREW] The app will be using the `nginx-full` formula."
|
|
||||||
: "[BREW] The app will be using the `nginx` formula."
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attempt to find out more info about Valet
|
// Attempt to find out more info about Valet
|
||||||
if Valet.shared.version != nil {
|
if Valet.shared.version != nil {
|
||||||
@@ -63,9 +58,6 @@ extension MainMenu {
|
|||||||
// Check for an alias conflict
|
// Check for an alias conflict
|
||||||
await BrewDiagnostics.checkForCaskConflict()
|
await BrewDiagnostics.checkForCaskConflict()
|
||||||
|
|
||||||
// Update the icon
|
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
|
|
||||||
// Attempt to find out if PHP-FPM is broken
|
// Attempt to find out if PHP-FPM is broken
|
||||||
PhpEnvironments.prepare()
|
PhpEnvironments.prepare()
|
||||||
|
|
||||||
@@ -76,7 +68,6 @@ extension MainMenu {
|
|||||||
WarningManager.shared.evaluateWarnings()
|
WarningManager.shared.evaluateWarnings()
|
||||||
|
|
||||||
// Set up the config watchers on launch (updated automatically when switching)
|
// Set up the config watchers on launch (updated automatically when switching)
|
||||||
Log.info("Setting up watchers...")
|
|
||||||
App.shared.handlePhpConfigWatcher()
|
App.shared.handlePhpConfigWatcher()
|
||||||
|
|
||||||
// Detect built-in and custom applications
|
// Detect built-in and custom applications
|
||||||
@@ -105,9 +96,33 @@ extension MainMenu {
|
|||||||
Valet.shared.notifyAboutUnsupportedTLD()
|
Valet.shared.notifyAboutUnsupportedTLD()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep track of which PHP versions are currently about to release
|
||||||
|
Log.info("Experimental PHP versions are: \(Constants.ExperimentalPhpVersions)")
|
||||||
|
|
||||||
// Find out which services are active
|
// Find out which services are active
|
||||||
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
||||||
|
|
||||||
|
// Post-launch stats and update check, but only if not running tests
|
||||||
|
await performPostLaunchActions()
|
||||||
|
|
||||||
|
// Check if the linked version has changed between launches of phpmon
|
||||||
|
PhpGuard().compareToLastGlobalVersion()
|
||||||
|
|
||||||
|
// We are ready!
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
|
||||||
|
// Finally!
|
||||||
|
Log.info("PHP Monitor is ready to serve!")
|
||||||
|
|
||||||
|
// Check if we upgraded from a previous version
|
||||||
|
AppUpdater.checkIfUpdateWasPerformed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Performs a set of post-launch actions, like incrementing stats and checking for updates.
|
||||||
|
(This code is skipped when running SwiftUI previews.)
|
||||||
|
*/
|
||||||
|
private func performPostLaunchActions() async {
|
||||||
if !isRunningSwiftUIPreview {
|
if !isRunningSwiftUIPreview {
|
||||||
Stats.incrementSuccessfulLaunchCount()
|
Stats.incrementSuccessfulLaunchCount()
|
||||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||||
@@ -121,15 +136,6 @@ extension MainMenu {
|
|||||||
await AppUpdater().checkForUpdates(userInitiated: false)
|
await AppUpdater().checkForUpdates(userInitiated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the linked version has changed between launches of phpmon
|
|
||||||
PhpGuard().compareToLastGlobalVersion()
|
|
||||||
|
|
||||||
// We are ready!
|
|
||||||
Log.info("PHP Monitor is ready to serve!")
|
|
||||||
|
|
||||||
// Check if we upgraded just now
|
|
||||||
AppUpdater.checkIfUpdateWasPerformed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -16,7 +16,9 @@ extension MainMenu {
|
|||||||
|
|
||||||
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
||||||
// Mark as no longer busy
|
// Mark as no longer busy
|
||||||
|
Task { @MainActor in
|
||||||
PhpEnvironments.shared.isBusy = false
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
Task { // Things to do after reloading domain list data
|
Task { // Things to do after reloading domain list data
|
||||||
if Valet.installed {
|
if Valet.installed {
|
||||||
@@ -25,7 +27,7 @@ extension MainMenu {
|
|||||||
|
|
||||||
// Perform UI updates on main thread
|
// Perform UI updates on main thread
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
|
|
||||||
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
||||||
|
@@ -37,8 +37,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
// MARK: - UI related
|
// MARK: - UI related
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Rebuilds the menu (either asynchronously or synchronously).
|
Rebuilds the menu on the main thread.
|
||||||
Defaults to rebuilding the menu asynchronously.
|
|
||||||
*/
|
*/
|
||||||
func rebuild() {
|
func rebuild() {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
@@ -80,13 +79,15 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func refreshActiveInstallation() {
|
@objc func refreshActiveInstallation() {
|
||||||
if !PhpEnvironments.shared.isBusy {
|
if !PhpEnvironments.shared.isBusy {
|
||||||
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
} else {
|
} else {
|
||||||
Log.perf("Skipping version refresh due to busy status!")
|
Log.perf("Skipping version refresh due to busy status!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||||
|
@available(*, deprecated, message: "Use the busy status instead")
|
||||||
@objc func updatePhpVersionInStatusBar() {
|
@objc func updatePhpVersionInStatusBar() {
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
@@ -139,7 +140,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func reloadPhpMonitorMenuInBackground() {
|
@objc func reloadPhpMonitorMenuInBackground() {
|
||||||
asyncExecution({
|
asyncExecution({
|
||||||
// This automatically reloads the menu
|
// This automatically reloads the menu
|
||||||
Log.info("Reloading information about the PHP installation (in the background)...")
|
Log.perf("Reloading information about the PHP installation (in the background)...")
|
||||||
}, behaviours: [
|
}, behaviours: [
|
||||||
.setsBusyUI,
|
.setsBusyUI,
|
||||||
.reloadsPhpInstallation,
|
.reloadsPhpInstallation,
|
||||||
@@ -150,13 +151,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
|
|
||||||
/** Refreshes the icon with the PHP version. */
|
/** Refreshes the icon with the PHP version. */
|
||||||
@objc func refreshIcon() {
|
@objc func refreshIcon() {
|
||||||
|
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
Log.perf("Refreshing icon: currently busy")
|
||||||
|
setStatusBar(image: NSImage.statusBarIcon)
|
||||||
} else {
|
} else {
|
||||||
|
Log.perf("Refreshing icon: no longer busy")
|
||||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||||
// Static icon has been requested
|
// Static icon has been requested
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
setStatusBar(image: NSImage.statusBarIconStatic)
|
||||||
} else {
|
} else {
|
||||||
// The dynamic icon has been requested
|
// The dynamic icon has been requested
|
||||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||||
@@ -172,13 +176,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon to be displayed as busy. */
|
|
||||||
@objc func setBusyImage() {
|
|
||||||
Task { @MainActor [self] in
|
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Menu Item Functionality
|
// MARK: - Menu Item Functionality
|
||||||
|
|
||||||
@objc func openAbout() {
|
@objc func openAbout() {
|
||||||
@@ -218,6 +215,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
PhpVersionManagerWindowController.show()
|
PhpVersionManagerWindowController.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openPhpExtensionManager() {
|
||||||
|
PhpExtensionManagerWindowController.show()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func openDonate() {
|
@objc func openDonate() {
|
||||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ import Cocoa
|
|||||||
|
|
||||||
extension StatusMenu {
|
extension StatusMenu {
|
||||||
|
|
||||||
func addPhpVersionMenuItems() {
|
@MainActor func addPhpVersionMenuItems() {
|
||||||
if PhpEnvironments.phpInstall == nil {
|
if PhpEnvironments.phpInstall == nil {
|
||||||
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
||||||
addItems([
|
addItems([
|
||||||
@@ -34,7 +34,7 @@ extension StatusMenu {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPhpActionMenuItems() {
|
@MainActor func addPhpActionMenuItems() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
addItem(NSMenuItem(title: "mi_busy".localized))
|
addItem(NSMenuItem(title: "mi_busy".localized))
|
||||||
return
|
return
|
||||||
@@ -54,7 +54,7 @@ extension StatusMenu {
|
|||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
func addServicesManagerMenuItem() {
|
@MainActor func addServicesManagerMenuItem() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSwitchToPhpMenuItems() {
|
@MainActor func addSwitchToPhpMenuItems() {
|
||||||
var shortcutKey = 1
|
var shortcutKey = 1
|
||||||
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
||||||
// Get the short and long version
|
// Get the short and long version
|
||||||
@@ -102,14 +102,14 @@ extension StatusMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addLiteModeMenuItem() {
|
@MainActor func addLiteModeMenuItem() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPreferencesMenuItems() {
|
@MainActor func addPreferencesMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_preferences".localized,
|
NSMenuItem(title: "mi_preferences".localized,
|
||||||
@@ -119,7 +119,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addCoreMenuItems() {
|
@MainActor func addCoreMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_about".localized,
|
NSMenuItem(title: "mi_about".localized,
|
||||||
@@ -131,7 +131,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Valet
|
// MARK: - Valet
|
||||||
|
|
||||||
func addValetMenuItems() {
|
@MainActor func addValetMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_valet".localized),
|
HeaderView.asMenuItem(text: "mi_valet".localized),
|
||||||
NSMenuItem(title: "mi_valet_config".localized,
|
NSMenuItem(title: "mi_valet_config".localized,
|
||||||
@@ -146,12 +146,15 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - PHP Configuration
|
// MARK: - PHP Configuration
|
||||||
|
|
||||||
func addConfigurationMenuItems() {
|
@MainActor func addConfigurationMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
||||||
NSMenuItem(title: "mi_php_version_manager".localized,
|
NSMenuItem(title: "mi_php_version_manager".localized,
|
||||||
action: #selector(MainMenu.openPhpVersionManager),
|
action: #selector(MainMenu.openPhpVersionManager),
|
||||||
keyEquivalent: "m"),
|
keyEquivalent: "m"),
|
||||||
|
NSMenuItem(title: "mi_php_ext_manager".localized,
|
||||||
|
action: #selector(MainMenu.openPhpExtensionManager),
|
||||||
|
keyEquivalent: "e"),
|
||||||
NSMenuItem(title: "mi_php_config".localized,
|
NSMenuItem(title: "mi_php_config".localized,
|
||||||
action: #selector(MainMenu.openActiveConfigFolder),
|
action: #selector(MainMenu.openActiveConfigFolder),
|
||||||
keyEquivalent: "c"),
|
keyEquivalent: "c"),
|
||||||
@@ -166,7 +169,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Composer
|
// MARK: - Composer
|
||||||
|
|
||||||
func addComposerMenuItems() {
|
@MainActor func addComposerMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_composer".localized),
|
HeaderView.asMenuItem(text: "mi_composer".localized),
|
||||||
NSMenuItem(
|
NSMenuItem(
|
||||||
@@ -187,7 +190,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Stats
|
// MARK: - Stats
|
||||||
|
|
||||||
func addStatsMenuItem() {
|
@MainActor func addStatsMenuItem() {
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("Not showing stats menu item if no PHP version is linked.")
|
Log.info("Not showing stats menu item if no PHP version is linked.")
|
||||||
return
|
return
|
||||||
@@ -200,21 +203,11 @@ extension StatusMenu {
|
|||||||
post: stats.post_max_size,
|
post: stats.post_max_size,
|
||||||
upload: stats.upload_max_filesize)
|
upload: stats.upload_max_filesize)
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: As soon as this does more than just edit memory limits, move this
|
|
||||||
/*
|
|
||||||
addItem(NSMenuItem.separator())
|
|
||||||
addItem(NSMenuItem(
|
|
||||||
title: "mi_manage_limits".localized,
|
|
||||||
action: #selector(MainMenu.openConfigGUI),
|
|
||||||
keyEquivalent: "l")
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Extensions
|
// MARK: - Extensions
|
||||||
|
|
||||||
func addExtensionsMenuItems() {
|
@MainActor func addExtensionsMenuItems() {
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
||||||
return
|
return
|
||||||
@@ -235,7 +228,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Presets
|
// MARK: - Presets
|
||||||
|
|
||||||
func addPresetsMenuItem() {
|
@MainActor func addPresetsMenuItem() {
|
||||||
guard let presets = Preferences.custom.presets else {
|
guard let presets = Preferences.custom.presets else {
|
||||||
addEmptyPresetHelp()
|
addEmptyPresetHelp()
|
||||||
return
|
return
|
||||||
@@ -249,7 +242,7 @@ extension StatusMenu {
|
|||||||
addLoadedPresets()
|
addLoadedPresets()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addEmptyPresetHelp() {
|
@MainActor private func addEmptyPresetHelp() {
|
||||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||||
NSMenuItem(title: "mi_no_presets".localized),
|
NSMenuItem(title: "mi_no_presets".localized),
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
@@ -258,7 +251,7 @@ extension StatusMenu {
|
|||||||
], target: MainMenu.shared))
|
], target: MainMenu.shared))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addLoadedPresets() {
|
@MainActor private func addLoadedPresets() {
|
||||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)
|
HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)
|
||||||
@@ -273,7 +266,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Xdebug
|
// MARK: - Xdebug
|
||||||
|
|
||||||
func addXdebugMenuItem() {
|
@MainActor func addXdebugMenuItem() {
|
||||||
if !Xdebug.enabled {
|
if !Xdebug.enabled {
|
||||||
addItem(NSMenuItem.separator())
|
addItem(NSMenuItem.separator())
|
||||||
return
|
return
|
||||||
@@ -293,7 +286,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - PHP Doctor
|
// MARK: - PHP Doctor
|
||||||
|
|
||||||
func addPhpDoctorMenuItem() {
|
@MainActor func addPhpDoctorMenuItem() {
|
||||||
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
|
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
|
||||||
!WarningManager.shared.hasWarnings() {
|
!WarningManager.shared.hasWarnings() {
|
||||||
return
|
return
|
||||||
@@ -309,7 +302,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - First Aid & Services
|
// MARK: - First Aid & Services
|
||||||
|
|
||||||
func addFirstAidAndServicesMenuItems() {
|
@MainActor func addFirstAidAndServicesMenuItems() {
|
||||||
let services = NSMenuItem(title: "mi_other".localized)
|
let services = NSMenuItem(title: "mi_other".localized)
|
||||||
|
|
||||||
var items: [NSMenuItem] = [
|
var items: [NSMenuItem] = [
|
||||||
@@ -366,7 +359,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Other helper methods to generate menu items
|
// MARK: - Other helper methods to generate menu items
|
||||||
|
|
||||||
func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
@MainActor func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
||||||
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
||||||
|
|
||||||
let menuItem = ExtensionMenuItem(
|
let menuItem = ExtensionMenuItem(
|
||||||
|
@@ -9,7 +9,7 @@ import Cocoa
|
|||||||
|
|
||||||
class StatusMenu: NSMenu {
|
class StatusMenu: NSMenu {
|
||||||
// swiftlint:disable cyclomatic_complexity
|
// swiftlint:disable cyclomatic_complexity
|
||||||
func addMenuItems() {
|
@MainActor func addMenuItems() {
|
||||||
addPhpVersionMenuItems()
|
addPhpVersionMenuItems()
|
||||||
addItem(NSMenuItem.separator())
|
addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
|
@@ -20,6 +20,7 @@ enum PreferenceName: String, Codable {
|
|||||||
case globalHotkey = "global_hotkey"
|
case globalHotkey = "global_hotkey"
|
||||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||||
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
||||||
|
case languageOverride = "language_override"
|
||||||
|
|
||||||
// APPEARANCE
|
// APPEARANCE
|
||||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||||
@@ -84,7 +85,8 @@ enum PreferenceName: String, Codable {
|
|||||||
],
|
],
|
||||||
.string: [
|
.string: [
|
||||||
.globalHotkey,
|
.globalHotkey,
|
||||||
.iconTypeToDisplay
|
.iconTypeToDisplay,
|
||||||
|
.languageOverride
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -51,6 +51,7 @@ class Preferences {
|
|||||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||||
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
||||||
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
||||||
|
PreferenceName.languageOverride.rawValue: "",
|
||||||
|
|
||||||
/// Preferences: Appearance
|
/// Preferences: Appearance
|
||||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||||
|
@@ -17,7 +17,9 @@ class GeneralPreferencesVC: GenericPreferenceVC {
|
|||||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||||
|
|
||||||
_ = vc.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
_ = vc
|
||||||
|
.addView(when: true, vc.getLanguageOptionsPV())
|
||||||
|
.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
||||||
.addView(when: true, vc.getAutoRestartServicesPV())
|
.addView(when: true, vc.getAutoRestartServicesPV())
|
||||||
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
||||||
.addView(when: true, vc.getShortcutPV())
|
.addView(when: true, vc.getShortcutPV())
|
||||||
|
@@ -48,11 +48,44 @@ class GenericPreferenceVC: NSViewController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLanguageOptionsPV() -> NSView {
|
||||||
|
var options = Bundle.main.localizations
|
||||||
|
.filter({ $0 != "Base"})
|
||||||
|
.map({ lang in
|
||||||
|
return PreferenceDropdownOption(
|
||||||
|
label: Locale.current.localizedString(forLanguageCode: lang)!,
|
||||||
|
value: lang
|
||||||
|
)
|
||||||
|
})
|
||||||
|
options.insert(PreferenceDropdownOption(label: "System Default", value: ""), at: 0)
|
||||||
|
|
||||||
|
return SelectPreferenceView.make(
|
||||||
|
sectionText: "prefs.language".localized,
|
||||||
|
descriptionText: "prefs.language_options_desc".localized,
|
||||||
|
options: options,
|
||||||
|
preference: .languageOverride,
|
||||||
|
action: {
|
||||||
|
MainMenu.shared.refreshIcon()
|
||||||
|
MainMenu.shared.rebuild()
|
||||||
|
|
||||||
|
if let window = App.shared.preferencesWindowController?.window {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "alert.language_changed.title".localized
|
||||||
|
alert.informativeText = "alert.language_changed.subtitle".localized
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.addButton(withTitle: "generic.ok".localized)
|
||||||
|
alert.beginSheetModal(for: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func getIconOptionsPV() -> NSView {
|
func getIconOptionsPV() -> NSView {
|
||||||
return SelectPreferenceView.make(
|
return SelectPreferenceView.make(
|
||||||
sectionText: "",
|
sectionText: "",
|
||||||
descriptionText: "prefs.icon_options_desc".localized,
|
descriptionText: "prefs.icon_options_desc".localized,
|
||||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
options: MenuBarIcon.allCases
|
||||||
|
.map({ return PreferenceDropdownOption(label: $0.rawValue, value: $0.rawValue) }),
|
||||||
localizationPrefix: "prefs.icon_options",
|
localizationPrefix: "prefs.icon_options",
|
||||||
preference: .iconTypeToDisplay,
|
preference: .iconTypeToDisplay,
|
||||||
action: {
|
action: {
|
||||||
|
@@ -68,6 +68,8 @@ class PreferencesWindowController: PMWindowController {
|
|||||||
App.shared.preferencesWindowController?.positionWindowInTopRightCorner()
|
App.shared.preferencesWindowController?.positionWindowInTopRightCorner()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App.shared.preferencesWindowController?.window?.orderFrontRegardless()
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -84,6 +84,10 @@ class Stats {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func clearCurrentGlobalPhpVersion() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: InternalStats.lastGlobalPhpVersion.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine if the sponsor message should be displayed.
|
Determine if the sponsor message should be displayed.
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
|
@@ -9,30 +9,34 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class SelectPreferenceView: NSView, XibLoadable {
|
struct PreferenceDropdownOption {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectPreferenceView: NSView, XibLoadable {
|
||||||
@IBOutlet weak var labelSection: NSTextField!
|
@IBOutlet weak var labelSection: NSTextField!
|
||||||
@IBOutlet weak var labelDescription: NSTextField!
|
@IBOutlet weak var labelDescription: NSTextField!
|
||||||
@IBOutlet weak var popupButton: NSPopUpButton!
|
@IBOutlet weak var popupButton: NSPopUpButton!
|
||||||
|
|
||||||
var localizationPrefix: String = ""
|
var localizationPrefix: String?
|
||||||
var imagePrefix: String?
|
var imagePrefix: String?
|
||||||
|
|
||||||
var options: [String] = [] {
|
var options: [PreferenceDropdownOption] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
self.popupButton.removeAllItems()
|
self.popupButton.removeAllItems()
|
||||||
self.options.forEach { value in
|
self.options.forEach { option in
|
||||||
self.popupButton.addItem(
|
if let prefix = localizationPrefix {
|
||||||
withTitle: "\(localizationPrefix).\(value)".localized
|
self.popupButton.addItem(withTitle: "\(prefix).\(option.label)".localized)
|
||||||
)
|
} else {
|
||||||
}
|
self.popupButton.addItem(withTitle: option.label)
|
||||||
|
}
|
||||||
if imagePrefix == nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let prefix = imagePrefix {
|
||||||
self.popupButton.itemArray.enumerated().forEach { item in
|
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 {
|
didSet {
|
||||||
let value = Preferences.preferences[preference] as! String
|
let value = Preferences.preferences[preference] as! String
|
||||||
self.options.enumerated().forEach { option in
|
self.options.enumerated().forEach { option in
|
||||||
if option.element == value {
|
if option.element.value == value {
|
||||||
self.popupButton.selectItem(at: option.offset)
|
self.popupButton.selectItem(at: option.offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable function_parameter_count
|
|
||||||
static func make(
|
static func make(
|
||||||
sectionText: String,
|
sectionText: String,
|
||||||
descriptionText: String,
|
descriptionText: String,
|
||||||
options: [String],
|
options: [PreferenceDropdownOption],
|
||||||
localizationPrefix: String,
|
localizationPrefix: String? = nil,
|
||||||
imagePrefix: String? = nil,
|
imagePrefix: String? = nil,
|
||||||
preference: PreferenceName,
|
preference: PreferenceName,
|
||||||
action: @escaping () -> Void) -> NSView {
|
action: @escaping () -> Void) -> NSView {
|
||||||
@@ -72,11 +75,10 @@ class SelectPreferenceView: NSView, XibLoadable {
|
|||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
// swiftlint:enable function_parameter_count
|
|
||||||
|
|
||||||
@IBAction func valueChanged(_ sender: Any) {
|
@IBAction func valueChanged(_ sender: Any) {
|
||||||
let index = self.popupButton.indexOfSelectedItem
|
let index = self.popupButton.indexOfSelectedItem
|
||||||
Preferences.update(.iconTypeToDisplay, value: self.options[index])
|
Preferences.update(self.preference, value: self.options[index].value)
|
||||||
self.action()
|
self.action()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@@ -13,16 +13,16 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="183" y="5" width="395" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="13" y="29" width="154" height="16"/>
|
<rect key="frame" x="-2" y="29" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YaB-Tg-Ir3">
|
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YaB-Tg-Ir3">
|
||||||
<rect key="frame" x="182" y="23" width="110" height="25"/>
|
<rect key="frame" x="167" y="23" width="110" height="25"/>
|
||||||
<popUpButtonCell key="cell" type="push" title="Icon Option" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SaA-mm-HBo" id="Su6-C4-aGo">
|
<popUpButtonCell key="cell" type="push" title="Icon Option" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SaA-mm-HBo" id="Su6-C4-aGo">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="menu"/>
|
<font key="font" metaFont="menu"/>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
||||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
||||||
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="15" id="Ztd-uk-4aw"/>
|
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" id="aBU-J8-gRK"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
|
@@ -29,7 +29,7 @@ struct BlockingOverlayView<Content: View>: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
content().opacity(isBlocking ? 0.2 : 1)
|
content().opacity(isBlocking ? 0 : 1)
|
||||||
if isBlocking {
|
if isBlocking {
|
||||||
VStack {
|
VStack {
|
||||||
ActivityIndicator()
|
ActivityIndicator()
|
||||||
@@ -44,7 +44,8 @@ struct BlockingOverlayView<Content: View>: View {
|
|||||||
.padding(.top, -4)
|
.padding(.top, -4)
|
||||||
}.padding(60)
|
}.padding(60)
|
||||||
}
|
}
|
||||||
}.background(Color.white)
|
}
|
||||||
|
.background(Color.spinnerBackground)
|
||||||
.disabled(isBlocking)
|
.disabled(isBlocking)
|
||||||
}
|
}
|
||||||
}
|
}
|
36
phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift
Normal file
36
phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// CustomButtonStyles.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 15/03/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct CustomButtonStyle: ButtonStyle {
|
||||||
|
@Environment(\.isEnabled) var isEnabled
|
||||||
|
|
||||||
|
public func makeBody(configuration: Self.Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.background(.statusColorBlue, in: .rect(cornerRadius: 8, style: .continuous))
|
||||||
|
.opacity({
|
||||||
|
if configuration.isPressed {
|
||||||
|
return 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isEnabled {
|
||||||
|
return 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1.0
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ButtonStyle where Self == CustomButtonStyle {
|
||||||
|
static var custom: CustomButtonStyle { .init() }
|
||||||
|
}
|
@@ -27,15 +27,15 @@ struct HelpButton: View {
|
|||||||
.buttonStyle(BorderlessButtonStyle())
|
.buttonStyle(BorderlessButtonStyle())
|
||||||
.focusable(false)
|
.focusable(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
struct HelpButton_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
#Preview("Light Mode") {
|
||||||
Group {
|
HelpButton(action: {})
|
||||||
HelpButton(action: {}).padding()
|
.padding(100)
|
||||||
.previewDisplayName("Light Mode")
|
}
|
||||||
HelpButton(action: {}).padding().preferredColorScheme(.dark)
|
|
||||||
.previewDisplayName("Dark Mode")
|
#Preview("Dark Mode") {
|
||||||
}
|
HelpButton(action: {})
|
||||||
}
|
.padding(100)
|
||||||
}
|
.preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,9 @@ var isRunningSwiftUIPreview: Bool {
|
|||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
public static var appPrimary: Color = Color("AppColor")
|
public static var appPrimary: Color = Color("AppColor")
|
||||||
public static var appSecondary: Color = Color("AppSecondary")
|
|
||||||
|
// This next one is generated automatically via asset catalogs now
|
||||||
|
// public static var appSecondary: Color = Color("AppSecondary")
|
||||||
|
|
||||||
public static var debug: Color = {
|
public static var debug: Color = {
|
||||||
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
||||||
|
67
phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift
Normal file
67
phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
//
|
||||||
|
// NoDomainsView.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 19/03/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UnavailableContentView: View {
|
||||||
|
var title: String
|
||||||
|
var description: String
|
||||||
|
var icon: String
|
||||||
|
var button: String?
|
||||||
|
var action: (() -> Void)?
|
||||||
|
|
||||||
|
init(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
icon: String,
|
||||||
|
button: String? = nil,
|
||||||
|
action: (() -> Void)? = nil
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.icon = icon
|
||||||
|
self.button = button
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
Image(systemName: self.icon)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 48, height: 48)
|
||||||
|
.foregroundColor(Color.appPrimary)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
Text(self.title)
|
||||||
|
.font(.system(size: 18, weight: .bold))
|
||||||
|
|
||||||
|
Text(self.description)
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
if self.button != nil {
|
||||||
|
Button(self.button!) {
|
||||||
|
self.action!()
|
||||||
|
}.buttonStyle(.custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(30)
|
||||||
|
.frame(maxWidth: 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
UnavailableContentView(
|
||||||
|
title: "domain_list.domains_empty.title".localized,
|
||||||
|
description: "domain_list.domains_empty.desc".localized,
|
||||||
|
icon: "globe",
|
||||||
|
button: "domain_list.domains_empty.button".localized,
|
||||||
|
action: {}
|
||||||
|
)
|
||||||
|
}
|
@@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// NoDomainResults.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 15/08/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct NoDomainResults: View {
|
|
||||||
@State var searching: Bool = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .center, spacing: 15) {
|
|
||||||
Image(systemName: searching ? "magnifyingglass.circle.fill" : "questionmark.circle.fill")
|
|
||||||
.resizable()
|
|
||||||
.renderingMode(.template)
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
VStack(alignment: .center) {
|
|
||||||
Text(
|
|
||||||
searching
|
|
||||||
? "domain_list.no_domains_for_search_query".localizedForSwiftUI
|
|
||||||
: "domain_list.no_domains".localizedForSwiftUI
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minWidth: 0, maxWidth: .infinity)
|
|
||||||
.padding(25)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NoDomainResults_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
NoDomainResults()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -14,8 +14,16 @@ struct VersionPopoverView: View {
|
|||||||
|
|
||||||
@State var validPhpVersions: [VersionNumber]
|
@State var validPhpVersions: [VersionNumber]
|
||||||
|
|
||||||
|
@State var prefersIsolationSuggestions: Bool
|
||||||
|
|
||||||
@State var parent: NSPopover!
|
@State var parent: NSPopover!
|
||||||
|
|
||||||
|
let rows = [
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible())
|
||||||
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text(getTitleText())
|
Text(getTitleText())
|
||||||
@@ -32,14 +40,29 @@ struct VersionPopoverView: View {
|
|||||||
message: "alert.php_suggestions".localized,
|
message: "alert.php_suggestions".localized,
|
||||||
color: Color("AppColor")
|
color: Color("AppColor")
|
||||||
)
|
)
|
||||||
HStack {
|
if prefersIsolationSuggestions {
|
||||||
|
// SITE ISOLATION (preferred)
|
||||||
|
LazyVGrid(columns: self.rows, alignment: .leading, spacing: 5, content: {
|
||||||
|
ForEach(validPhpVersions, id: \.self) { version in
|
||||||
|
Button("site_link.isolate_php".localized(version.short), action: {
|
||||||
|
App.shared.domainListWindowController?.contentVC
|
||||||
|
.isolateSite(site: site, version: version.short)
|
||||||
|
parent?.close()
|
||||||
|
}).padding(EdgeInsets(top: 3, leading: 0, bottom: 3, trailing: 0))
|
||||||
|
}
|
||||||
|
}).padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
} else {
|
||||||
|
// GLOBAL SWITCHER
|
||||||
|
LazyVGrid(columns: self.rows, alignment: .leading, spacing: 5, content: {
|
||||||
ForEach(validPhpVersions, id: \.self) { version in
|
ForEach(validPhpVersions, id: \.self) { version in
|
||||||
Button("site_link.switch_to_php".localized(version.short), action: {
|
Button("site_link.switch_to_php".localized(version.short), action: {
|
||||||
MainMenu.shared.switchToPhpVersion(version.short)
|
MainMenu.shared.switchToPhpVersion(version.short)
|
||||||
parent?.close()
|
parent?.close()
|
||||||
})
|
}).padding(EdgeInsets(top: 3, leading: 0, bottom: 3, trailing: 0))
|
||||||
}
|
}
|
||||||
}.padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0))
|
}).padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if site.preferredPhpVersionSource == .unknown {
|
if site.preferredPhpVersionSource == .unknown {
|
||||||
@@ -126,8 +149,7 @@ struct DisclaimerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VersionPopoverView_Previews: PreviewProvider {
|
#Preview("Unknown Requirement") {
|
||||||
static var previews: some View {
|
|
||||||
VersionPopoverView(
|
VersionPopoverView(
|
||||||
site: FakeValetSite(
|
site: FakeValetSite(
|
||||||
fakeWithName: "amazingwebsite",
|
fakeWithName: "amazingwebsite",
|
||||||
@@ -138,10 +160,12 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
|||||||
constraint: ""
|
constraint: ""
|
||||||
),
|
),
|
||||||
validPhpVersions: [],
|
validPhpVersions: [],
|
||||||
|
prefersIsolationSuggestions: false,
|
||||||
parent: nil
|
parent: nil
|
||||||
)
|
)
|
||||||
.previewDisplayName("Unknown Requirement")
|
}
|
||||||
|
|
||||||
|
#Preview("Requirement Matches") {
|
||||||
VersionPopoverView(
|
VersionPopoverView(
|
||||||
site: FakeValetSite(
|
site: FakeValetSite(
|
||||||
fakeWithName: "amazingwebsite",
|
fakeWithName: "amazingwebsite",
|
||||||
@@ -152,9 +176,12 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
|||||||
constraint: "^8.1"
|
constraint: "^8.1"
|
||||||
),
|
),
|
||||||
validPhpVersions: [],
|
validPhpVersions: [],
|
||||||
|
prefersIsolationSuggestions: false,
|
||||||
parent: nil
|
parent: nil
|
||||||
)
|
)
|
||||||
.previewDisplayName("Requirement Matches")
|
}
|
||||||
|
|
||||||
|
#Preview("Isolated") {
|
||||||
VersionPopoverView(
|
VersionPopoverView(
|
||||||
site: FakeValetSite(
|
site: FakeValetSite(
|
||||||
fakeWithName: "anothersite",
|
fakeWithName: "anothersite",
|
||||||
@@ -166,9 +193,12 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
|||||||
isolated: "8.0"
|
isolated: "8.0"
|
||||||
),
|
),
|
||||||
validPhpVersions: [],
|
validPhpVersions: [],
|
||||||
|
prefersIsolationSuggestions: false,
|
||||||
parent: nil
|
parent: nil
|
||||||
)
|
)
|
||||||
.previewDisplayName("Isolated")
|
}
|
||||||
|
|
||||||
|
#Preview("Isolated Mismatch") {
|
||||||
VersionPopoverView(
|
VersionPopoverView(
|
||||||
site: FakeValetSite(
|
site: FakeValetSite(
|
||||||
fakeWithName: "anothersite",
|
fakeWithName: "anothersite",
|
||||||
@@ -180,9 +210,12 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
|||||||
isolated: "7.4"
|
isolated: "7.4"
|
||||||
),
|
),
|
||||||
validPhpVersions: [],
|
validPhpVersions: [],
|
||||||
|
prefersIsolationSuggestions: false,
|
||||||
parent: nil
|
parent: nil
|
||||||
)
|
)
|
||||||
.previewDisplayName("Isolated Mismatch")
|
}
|
||||||
|
|
||||||
|
#Preview("Recommend Alternatives") {
|
||||||
VersionPopoverView(
|
VersionPopoverView(
|
||||||
site: FakeValetSite(
|
site: FakeValetSite(
|
||||||
fakeWithName: "anothersite",
|
fakeWithName: "anothersite",
|
||||||
@@ -194,10 +227,12 @@ struct VersionPopoverView_Previews: PreviewProvider {
|
|||||||
),
|
),
|
||||||
validPhpVersions: [
|
validPhpVersions: [
|
||||||
VersionNumber(major: 8, minor: 0, patch: 0),
|
VersionNumber(major: 8, minor: 0, patch: 0),
|
||||||
VersionNumber(major: 8, minor: 1, patch: 0)
|
VersionNumber(major: 8, minor: 1, patch: 0),
|
||||||
|
VersionNumber(major: 8, minor: 2, patch: 0),
|
||||||
|
VersionNumber(major: 8, minor: 3, patch: 0),
|
||||||
|
VersionNumber(major: 8, minor: 4, patch: 0)
|
||||||
],
|
],
|
||||||
|
prefersIsolationSuggestions: true,
|
||||||
parent: nil
|
parent: nil
|
||||||
)
|
)
|
||||||
.previewDisplayName("Recommend Alternatives")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -45,9 +45,7 @@ struct HeaderView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HeaderView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
|
||||||
HeaderView(text: "Hello world")
|
HeaderView(text: "Hello world")
|
||||||
.frame(width: 330.0)
|
.frame(width: 330.0)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -172,15 +172,15 @@ struct ServiceView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ServicesView_Previews: PreviewProvider {
|
#Preview("Active 1") {
|
||||||
static var previews: some View {
|
|
||||||
ServicesView(manager: FakeServicesManager(
|
ServicesView(manager: FakeServicesManager(
|
||||||
formulae: ["php", "nginx", "dnsmasq"],
|
formulae: ["php", "nginx", "dnsmasq"],
|
||||||
status: .active
|
status: .active
|
||||||
), perRow: 4)
|
), perRow: 4)
|
||||||
.frame(width: 330.0)
|
.frame(width: 330.0)
|
||||||
.previewDisplayName("Active 1")
|
}
|
||||||
|
|
||||||
|
#Preview("Active 2") {
|
||||||
ServicesView(manager: FakeServicesManager(
|
ServicesView(manager: FakeServicesManager(
|
||||||
formulae: [
|
formulae: [
|
||||||
"php", "nginx", "dnsmasq", "thing1",
|
"php", "nginx", "dnsmasq", "thing1",
|
||||||
@@ -189,6 +189,4 @@ struct ServicesView_Previews: PreviewProvider {
|
|||||||
status: .inactive
|
status: .inactive
|
||||||
), perRow: 4)
|
), perRow: 4)
|
||||||
.frame(width: 330.0)
|
.frame(width: 330.0)
|
||||||
.previewDisplayName("Active 2")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -89,6 +89,7 @@ struct StatsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "gearshape.fill")
|
Image(systemName: "gearshape.fill")
|
||||||
}
|
}
|
||||||
|
.accessibility(identifier: "phpConfigButton")
|
||||||
.focusable(false)
|
.focusable(false)
|
||||||
.frame(minWidth: 30, alignment: .center)
|
.frame(minWidth: 30, alignment: .center)
|
||||||
}
|
}
|
||||||
@@ -98,12 +99,10 @@ struct StatsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StatsView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
|
||||||
StatsView(
|
StatsView(
|
||||||
memoryLimit: "1024 MB",
|
memoryLimit: "1024 MB",
|
||||||
maxPostSize: "1024 MB",
|
maxPostSize: "1024 MB",
|
||||||
maxUploadSize: "1024 MB"
|
maxUploadSize: "1024 MB"
|
||||||
).frame(height: 100)
|
).frame(height: 100)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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.positionWindowInTopRightCorner()
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -56,7 +56,6 @@ class ConfigFSNotifier {
|
|||||||
return App.shared.handlePhpConfigWatcher(forceReload: true)
|
return App.shared.handlePhpConfigWatcher(forceReload: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
self?.parent.didChange?(self!.url)
|
self?.parent.didChange?(self!.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,21 +16,19 @@ class DomainListKindCell: NSTableCellView, DomainListCellProtocol {
|
|||||||
|
|
||||||
func populateCell(with site: ValetSite) {
|
func populateCell(with site: ValetSite) {
|
||||||
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
||||||
imageViewType.image = NSImage(
|
imageViewType.image = site.aliasPath == nil
|
||||||
named: site.aliasPath == nil
|
? NSImage.iconParked
|
||||||
? "IconParked"
|
: NSImage.iconLinked
|
||||||
: "IconLinked"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Unless, of course, this is a default site
|
// Unless, of course, this is a default site
|
||||||
if site.absolutePath == Valet.shared.config.defaultSite {
|
if site.absolutePath == Valet.shared.config.defaultSite {
|
||||||
imageViewType.image = NSImage(named: "IconDefault")
|
imageViewType.image = NSImage.iconDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateCell(with proxy: ValetProxy) {
|
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 {
|
if site.isolatedPhpVersion != nil {
|
||||||
imageViewPhpVersionOK.isHidden = false
|
imageViewPhpVersionOK.isHidden = false
|
||||||
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
|
imageViewPhpVersionOK.image = NSImage.isolated
|
||||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.isolated".localized(site.servingPhpVersion)
|
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.isolated".localized(site.servingPhpVersion)
|
||||||
} else {
|
} else {
|
||||||
imageViewPhpVersionOK.isHidden = (site.preferredPhpVersion == "???"
|
imageViewPhpVersionOK.isHidden = (site.preferredPhpVersion == "???"
|
||||||
|| !site.isCompatibleWithPreferredPhpVersion)
|
|| !site.isCompatibleWithPreferredPhpVersion)
|
||||||
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
|
imageViewPhpVersionOK.image = NSImage.checkmark
|
||||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.preferredPhpVersion)
|
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.preferredPhpVersion)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +70,12 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
|||||||
let button = self.buttonPhpVersion!
|
let button = self.buttonPhpVersion!
|
||||||
let popover = NSPopover()
|
let popover = NSPopover()
|
||||||
|
|
||||||
let view = VersionPopoverView(site: site, validPhpVersions: validPhpSuggestions, parent: popover)
|
let view = VersionPopoverView(
|
||||||
|
site: site,
|
||||||
|
validPhpVersions: validPhpSuggestions,
|
||||||
|
prefersIsolationSuggestions: Valet.enabled(feature: .isolatedSites),
|
||||||
|
parent: popover
|
||||||
|
)
|
||||||
|
|
||||||
popover.contentViewController = NSHostingController(rootView: view)
|
popover.contentViewController = NSHostingController(rootView: view)
|
||||||
popover.behavior = .transient
|
popover.behavior = .transient
|
||||||
|
@@ -110,17 +110,33 @@ extension DomainListVC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func isolateSite(sender: PhpMenuItem) {
|
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
||||||
guard let site = selectedSite else {
|
Task {
|
||||||
return
|
self.setUIBusy()
|
||||||
|
|
||||||
|
await sender.phpExtension?.toggle()
|
||||||
|
|
||||||
|
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
|
||||||
|
await Actions.restartPhpFpm()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadContextMenu()
|
||||||
|
|
||||||
|
self.setUINotBusy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func isolateSite(site: ValetSite, version: String) {
|
||||||
waitAndExecute {
|
waitAndExecute {
|
||||||
do {
|
do {
|
||||||
// Instruct Valet to isolate a given PHP version
|
// Instruct Valet to isolate a given PHP version
|
||||||
try await site.isolate(version: sender.version)
|
try await site.isolate(version: version)
|
||||||
// Reload the UI
|
// Reload the UI if it's the same site
|
||||||
|
if self.selectedSite?.absolutePath == site.absolutePath {
|
||||||
self.reloadSelectedRow()
|
self.reloadSelectedRow()
|
||||||
|
} else {
|
||||||
|
await self.reloadDomains()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Notify the user about a failed command
|
// Notify the user about a failed command
|
||||||
let error = error as! ValetInteractionError
|
let error = error as! ValetInteractionError
|
||||||
@@ -129,7 +145,15 @@ extension DomainListVC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func removeIsolatedSite() {
|
@objc func isolateSiteViaMenuItem(sender: PhpMenuItem) {
|
||||||
|
guard let site = selectedSite else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isolateSite(site: site, version: sender.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func removeIsolatedSiteViaMenuItem() {
|
||||||
guard let site = selectedSite else {
|
guard let site = selectedSite else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -13,22 +13,31 @@ extension DomainListVC {
|
|||||||
internal func reloadContextMenu() {
|
internal func reloadContextMenu() {
|
||||||
guard let selected = selected else {
|
guard let selected = selected else {
|
||||||
tableView.menu = nil
|
tableView.menu = nil
|
||||||
|
AppMenu.actionsMenu?.title = "mm_actions".localized
|
||||||
|
AppMenu.actionsMenu?.submenu = nil
|
||||||
|
AppMenu.actionsMenu?.isEnabled = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let selected = selected as? ValetSite {
|
if let selected = selected as? ValetSite {
|
||||||
addMenuItemsForSite(selected)
|
tableView.menu = addMenuItemsForSite(selected)
|
||||||
|
AppMenu.actionsMenu?.title = "mm_actions".localized + " (\(selected.name).\(selected.tld))"
|
||||||
|
AppMenu.actionsMenu?.submenu = tableView.menu
|
||||||
|
AppMenu.actionsMenu?.isEnabled = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let selected = selected as? ValetProxy {
|
if let selected = selected as? ValetProxy {
|
||||||
addMenuItemsForProxy(selected)
|
tableView.menu = addMenuItemsForProxy(selected)
|
||||||
|
AppMenu.actionsMenu?.title = "mm_actions".localized + " (\(selected.domain).\(selected.tld))"
|
||||||
|
AppMenu.actionsMenu?.submenu = tableView.menu
|
||||||
|
AppMenu.actionsMenu?.isEnabled = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Menu Items for Site
|
// MARK: - Menu Items for Site
|
||||||
|
|
||||||
private func addMenuItemsForSite(_ site: ValetSite) {
|
private func addMenuItemsForSite(_ site: ValetSite) -> NSMenu? {
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
|
|
||||||
addSystemApps(to: menu)
|
addSystemApps(to: menu)
|
||||||
@@ -42,11 +51,24 @@ extension DomainListVC {
|
|||||||
addDisabledIsolation(to: menu)
|
addDisabledIsolation(to: menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSeparator(to: menu)
|
||||||
|
|
||||||
|
if let extensions = site.isolatedPhpVersion?.extensions ?? PhpEnvironments.phpInstall?.extensions,
|
||||||
|
let version = site.isolatedPhpVersion?.versionNumber.short ?? PhpEnvironments.phpInstall?.version.short {
|
||||||
|
menu.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
|
||||||
|
addMenuItemsForExtensions(
|
||||||
|
to: menu,
|
||||||
|
for: extensions,
|
||||||
|
version: version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
menu.addItem(HeaderView.asMenuItem(text: "domain_list.actions".localized))
|
menu.addItem(HeaderView.asMenuItem(text: "domain_list.actions".localized))
|
||||||
|
|
||||||
addToggleSecure(to: menu, secured: site.secured)
|
addToggleSecure(to: menu, secured: site.secured)
|
||||||
addUnlink(to: menu, with: site)
|
addUnlink(to: menu, with: site)
|
||||||
|
|
||||||
tableView.menu = menu
|
return menu
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addSystemApps(to menu: NSMenu) {
|
private func addSystemApps(to menu: NSMenu) {
|
||||||
@@ -108,7 +130,7 @@ extension DomainListVC {
|
|||||||
for version in PhpEnvironments.shared.availablePhpVersions.reversed() {
|
for version in PhpEnvironments.shared.availablePhpVersions.reversed() {
|
||||||
let item = PhpMenuItem(
|
let item = PhpMenuItem(
|
||||||
title: "domain_list.always_use_php".localized(version),
|
title: "domain_list.always_use_php".localized(version),
|
||||||
action: #selector(self.isolateSite),
|
action: #selector(self.isolateSiteViaMenuItem),
|
||||||
keyEquivalent: ""
|
keyEquivalent: ""
|
||||||
)
|
)
|
||||||
if site.servingPhpVersion == version && site.isolatedPhpVersion != nil {
|
if site.servingPhpVersion == version && site.isolatedPhpVersion != nil {
|
||||||
@@ -124,7 +146,7 @@ extension DomainListVC {
|
|||||||
items.append(NSMenuItem.separator())
|
items.append(NSMenuItem.separator())
|
||||||
items.append(NSMenuItem(
|
items.append(NSMenuItem(
|
||||||
title: "domain_list.remove_isolation".localized,
|
title: "domain_list.remove_isolation".localized,
|
||||||
action: #selector(self.removeIsolatedSite)
|
action: #selector(self.removeIsolatedSiteViaMenuItem)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,15 +172,37 @@ extension DomainListVC {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addMenuItemsForExtensions(to menu: NSMenu, for extensions: [PhpExtension], version: String) {
|
||||||
|
var items: [NSMenuItem] = [
|
||||||
|
NSMenuItem(title: "domain_list.applies_to".localized(version))
|
||||||
|
]
|
||||||
|
|
||||||
|
for phpExtension in extensions {
|
||||||
|
let item = ExtensionMenuItem(
|
||||||
|
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
|
||||||
|
action: #selector(self.toggleExtension),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
item.state = phpExtension.enabled ? .on : .off
|
||||||
|
item.phpExtension = phpExtension
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.addItem(NSMenuItem(title: "domain_list.extensions".localized, submenu: items))
|
||||||
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Menu Items for Proxy
|
// MARK: - Menu Items for Proxy
|
||||||
|
|
||||||
private func addMenuItemsForProxy(_ proxy: ValetProxy) {
|
private func addMenuItemsForProxy(_ proxy: ValetProxy) -> NSMenu {
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
addOpenProxyInBrowser(to: menu)
|
addOpenProxyInBrowser(to: menu)
|
||||||
addSeparator(to: menu)
|
addSeparator(to: menu)
|
||||||
addToggleSecure(to: menu, secured: proxy.secured)
|
addToggleSecure(to: menu, secured: proxy.secured)
|
||||||
addRemoveProxy(to: menu)
|
addRemoveProxy(to: menu)
|
||||||
tableView.menu = menu
|
return menu
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addOpenProxyInBrowser(to menu: NSMenu) {
|
private func addOpenProxyInBrowser(to menu: NSMenu) {
|
||||||
|
@@ -8,12 +8,14 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import Carbon
|
import Carbon
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||||
|
|
||||||
// MARK: - Outlets
|
// MARK: - Outlets
|
||||||
|
|
||||||
@IBOutlet weak var tableView: PMTableView!
|
@IBOutlet weak var tableView: PMTableView!
|
||||||
|
@IBOutlet weak var noResultsView: NSView!
|
||||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||||
|
|
||||||
// MARK: - Variables
|
// MARK: - Variables
|
||||||
@@ -93,6 +95,8 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
tableView.doubleAction = #selector(self.doubleClicked(sender:))
|
tableView.doubleAction = #selector(self.doubleClicked(sender:))
|
||||||
|
|
||||||
|
addNoResultsView()
|
||||||
|
|
||||||
let mapping = [
|
let mapping = [
|
||||||
"SECURE": "domain_list.columns.secure",
|
"SECURE": "domain_list.columns.secure",
|
||||||
"DOMAIN": "domain_list.columns.domain",
|
"DOMAIN": "domain_list.columns.domain",
|
||||||
@@ -115,6 +119,25 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addNoResultsView() {
|
||||||
|
let child = NSHostingController(
|
||||||
|
rootView: UnavailableContentView(
|
||||||
|
title: "domain_list.domains_empty.title".localized,
|
||||||
|
description: "domain_list.domains_empty.desc".localized,
|
||||||
|
icon: "globe",
|
||||||
|
button: "domain_list.domains_empty.button".localized,
|
||||||
|
action: {
|
||||||
|
App.shared.domainListWindowController?
|
||||||
|
.pressedAddLink(nil)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.frame(width: 400, height: 300)
|
||||||
|
).view
|
||||||
|
|
||||||
|
self.noResultsView.addSubview(child)
|
||||||
|
child.frame = self.noResultsView.bounds
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Async Operations
|
// MARK: - Async Operations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,6 +155,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
tableView.alphaValue = 0.3
|
tableView.alphaValue = 0.3
|
||||||
tableView.isEnabled = false
|
tableView.isEnabled = false
|
||||||
tableView.selectRowIndexes([], byExtendingSelection: true)
|
tableView.selectRowIndexes([], byExtendingSelection: true)
|
||||||
|
noResultsView.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,6 +166,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
progressIndicator.stopAnimation(nil)
|
progressIndicator.stopAnimation(nil)
|
||||||
tableView.alphaValue = 1.0
|
tableView.alphaValue = 1.0
|
||||||
tableView.isEnabled = true
|
tableView.isEnabled = true
|
||||||
|
updateNoResultsView()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -282,9 +307,14 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.tableView.reloadData()
|
self.tableView.reloadData()
|
||||||
|
updateNoResultsView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateNoResultsView() {
|
||||||
|
self.noResultsView.isHidden = !domains.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
func searchedFor(text: String) {
|
func searchedFor(text: String) {
|
||||||
lastSearchedFor = text
|
lastSearchedFor = text
|
||||||
|
|
||||||
|
@@ -31,7 +31,7 @@ struct OnboardingTextItem: View {
|
|||||||
.opacity(self.unavailable ? 0.5 : 1)
|
.opacity(self.unavailable ? 0.5 : 1)
|
||||||
Text(description.localizedForSwiftUI)
|
Text(description.localizedForSwiftUI)
|
||||||
.foregroundColor(Color.secondary)
|
.foregroundColor(Color.secondary)
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 12))
|
||||||
.lineLimit(4)
|
.lineLimit(4)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.frame(minWidth: 0, maxWidth: 800, alignment: .leading)
|
.frame(minWidth: 0, maxWidth: 800, alignment: .leading)
|
||||||
@@ -51,7 +51,7 @@ struct OnboardingView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(nsImage: NSApp.applicationIconImage)
|
Image(nsImage: NSApp.applicationIconImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 80, height: 80)
|
.frame(width: 100, height: 100)
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, 5)
|
||||||
.padding(.trailing, 25)
|
.padding(.trailing, 25)
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
@@ -126,20 +126,19 @@ struct OnboardingView: View {
|
|||||||
Button("onboarding.tour.close".localized) {
|
Button("onboarding.tour.close".localized) {
|
||||||
App.shared.onboardingWindowController?.close()
|
App.shared.onboardingWindowController?.close()
|
||||||
}
|
}
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, 15)
|
||||||
.padding(.top, 10)
|
.padding(.top, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading)
|
.padding(.leading)
|
||||||
.padding(.trailing)
|
.padding(.trailing)
|
||||||
|
.padding(.bottom, 0)
|
||||||
}
|
}
|
||||||
|
.frame(width: 600)
|
||||||
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OnboardingView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
OnboardingView()
|
OnboardingView()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@ class OnboardingWindowController: PMWindowController {
|
|||||||
window.titlebarAppearsTransparent = true
|
window.titlebarAppearsTransparent = true
|
||||||
window.delegate = delegate ?? windowController
|
window.delegate = delegate ?? windowController
|
||||||
window.contentView = NSHostingView(rootView: OnboardingView())
|
window.contentView = NSHostingView(rootView: OnboardingView())
|
||||||
window.setContentSize(NSSize(width: 600, height: 600))
|
window.setContentSize(window.contentView!.fittingSize)
|
||||||
|
|
||||||
App.shared.onboardingWindowController = windowController
|
App.shared.onboardingWindowController = windowController
|
||||||
}
|
}
|
||||||
|
@@ -98,8 +98,7 @@ struct ByteLimitView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ByteLimitView_Previews: PreviewProvider {
|
#Preview("Byte Limit View") {
|
||||||
static var previews: some View {
|
|
||||||
PreferenceContainer(
|
PreferenceContainer(
|
||||||
name: "Max Size",
|
name: "Max Size",
|
||||||
description:
|
description:
|
||||||
@@ -108,9 +107,10 @@ struct ByteLimitView_Previews: PreviewProvider {
|
|||||||
) {
|
) {
|
||||||
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
|
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
|
||||||
}.frame(width: 600, height: 200)
|
}.frame(width: 600, height: 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Config Manager") {
|
||||||
ConfigManagerView()
|
ConfigManagerView()
|
||||||
.frame(width: 600, height: .infinity)
|
.frame(width: 600, height: .infinity)
|
||||||
.previewDisplayName("Config Manager")
|
.previewDisplayName("Config Manager")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -84,10 +84,6 @@ struct ConfigManagerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ConfigManagerView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
ConfigManagerView().frame(width: 600)
|
||||||
ConfigManagerView()
|
|
||||||
.frame(width: 600)
|
|
||||||
.previewDisplayName("Live Preview")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -34,8 +34,8 @@ class WarningManager: ObservableObject {
|
|||||||
.trimmingCharacters(in: .whitespacesAndNewlines) == "1"
|
.trimmingCharacters(in: .whitespacesAndNewlines) == "1"
|
||||||
},
|
},
|
||||||
name: "Running PHP Monitor with Rosetta on M1",
|
name: "Running PHP Monitor with Rosetta on M1",
|
||||||
title: "warnings.arm_compatibility.title".localized,
|
title: "warnings.arm_compatibility.title",
|
||||||
paragraphs: { return ["warnings.arm_compatibility.description".localized] },
|
paragraphs: { return ["warnings.arm_compatibility.description"] },
|
||||||
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-and-Apple-Silicon"
|
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-and-Apple-Silicon"
|
||||||
),
|
),
|
||||||
Warning(
|
Warning(
|
||||||
@@ -44,11 +44,11 @@ class WarningManager: ObservableObject {
|
|||||||
!FileSystem.isWriteableFile("/usr/local/bin/")
|
!FileSystem.isWriteableFile("/usr/local/bin/")
|
||||||
},
|
},
|
||||||
name: "Helpers cannot be symlinked and not in PATH",
|
name: "Helpers cannot be symlinked and not in PATH",
|
||||||
title: "warnings.helper_permissions.title".localized,
|
title: "warnings.helper_permissions.title",
|
||||||
paragraphs: { return [
|
paragraphs: { return [
|
||||||
"warnings.helper_permissions.description".localized,
|
"warnings.helper_permissions.description",
|
||||||
"warnings.helper_permissions.unavailable".localized,
|
"warnings.helper_permissions.unavailable",
|
||||||
"warnings.helper_permissions.symlink".localized
|
"warnings.helper_permissions.symlink"
|
||||||
] },
|
] },
|
||||||
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries"
|
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries"
|
||||||
),
|
),
|
||||||
@@ -58,7 +58,7 @@ class WarningManager: ObservableObject {
|
|||||||
return !PhpConfigChecker.shared.missing.isEmpty
|
return !PhpConfigChecker.shared.missing.isEmpty
|
||||||
},
|
},
|
||||||
name: "Your PHP installation is missing configuration files",
|
name: "Your PHP installation is missing configuration files",
|
||||||
title: "warnings.files_missing.title".localized,
|
title: "warnings.files_missing.title",
|
||||||
paragraphs: { return [
|
paragraphs: { return [
|
||||||
"warnings.files_missing.description".localized(
|
"warnings.files_missing.description".localized(
|
||||||
PhpConfigChecker.shared.missing.joined(separator: "\n• ")
|
PhpConfigChecker.shared.missing.joined(separator: "\n• ")
|
||||||
|
@@ -25,8 +25,6 @@ struct NoWarningsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NoWarningsView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
NoWarningsView().padding()
|
||||||
NoWarningsView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -89,19 +89,19 @@ struct PhpDoctorView: View {
|
|||||||
}
|
}
|
||||||
.listRowInsets(EdgeInsets())
|
.listRowInsets(EdgeInsets())
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
.frame(minHeight: 350, maxHeight: .infinity, alignment: .top)
|
||||||
}
|
}
|
||||||
|
.frame(width: 600)
|
||||||
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WarningListView_Previews: PreviewProvider {
|
#Preview("Empty List") {
|
||||||
static var previews: some View {
|
|
||||||
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
|
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
|
||||||
.frame(width: 600, height: 480)
|
.frame(width: 600, height: 480)
|
||||||
.previewDisplayName("Empty List")
|
}
|
||||||
|
|
||||||
|
#Preview("List With All Warnings") {
|
||||||
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
|
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
|
||||||
.frame(width: 600, height: 480)
|
.frame(width: 600, height: 480)
|
||||||
.previewDisplayName("List With All Warnings")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@ class PhpDoctorWindowController: PMWindowController {
|
|||||||
window.titlebarAppearsTransparent = true
|
window.titlebarAppearsTransparent = true
|
||||||
window.delegate = delegate ?? windowController
|
window.delegate = delegate ?? windowController
|
||||||
window.contentView = NSHostingView(rootView: PhpDoctorView())
|
window.contentView = NSHostingView(rootView: PhpDoctorView())
|
||||||
window.setContentSize(NSSize(width: 600, height: 480))
|
window.setContentSize(window.contentView!.fittingSize)
|
||||||
|
|
||||||
App.shared.phpDoctorWindowController = windowController
|
App.shared.phpDoctorWindowController = windowController
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,7 @@ struct WarningView: View {
|
|||||||
Text(title.localizedForSwiftUI)
|
Text(title.localizedForSwiftUI)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
ForEach(paragraphs, id: \.self) { paragraph in
|
ForEach(paragraphs, id: \.self) { paragraph in
|
||||||
Text(paragraph)
|
Text(paragraph.localizedForSwiftUI)
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,15 +47,17 @@ struct WarningView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WarningView_Previews: PreviewProvider {
|
#Preview("Light Mode") {
|
||||||
static var previews: some View {
|
|
||||||
WarningView(
|
WarningView(
|
||||||
title: "warnings.helper_permissions.title",
|
title: "warnings.helper_permissions.title",
|
||||||
paragraphs: ["warnings.helper_permissions.description"],
|
paragraphs: ["warnings.helper_permissions.description"],
|
||||||
documentationUrl: "https://nicoverbruggen.be"
|
documentationUrl: "https://nicoverbruggen.be"
|
||||||
)
|
)
|
||||||
.frame(width: 600, height: 105)
|
.frame(width: 600, height: 105)
|
||||||
|
.padding(25)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Dark Mode") {
|
||||||
WarningView(
|
WarningView(
|
||||||
title: "warnings.helper_permissions.title",
|
title: "warnings.helper_permissions.title",
|
||||||
paragraphs: ["warnings.helper_permissions.description"],
|
paragraphs: ["warnings.helper_permissions.description"],
|
||||||
@@ -63,7 +65,5 @@ struct WarningView_Previews: PreviewProvider {
|
|||||||
)
|
)
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
.frame(width: 600, height: 105)
|
.frame(width: 600, height: 105)
|
||||||
|
.padding(25)
|
||||||
// WarningListView().frame(width: 600, height: 580)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// BrewExtensionsObservable.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 21/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class BrewExtensionsObservable: ObservableObject {
|
||||||
|
@Published var phpVersion: String {
|
||||||
|
didSet {
|
||||||
|
self.loadExtensionData(for: phpVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var extensions: [BrewPhpExtension] = []
|
||||||
|
|
||||||
|
init(phpVersion: String) {
|
||||||
|
self.phpVersion = phpVersion
|
||||||
|
self.loadExtensionData(for: phpVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func loadExtensionData(for version: String) {
|
||||||
|
let tapFormulae = BrewTapFormulae.from(tap: "shivammathur/homebrew-extensions")
|
||||||
|
|
||||||
|
if let filteredTapFormulae = tapFormulae[version] {
|
||||||
|
self.extensions = filteredTapFormulae
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// PhpExtensionManagerView+Actions.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 21/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
extension PhpExtensionManagerView {
|
||||||
|
public func presentErrorAlert(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
button: String,
|
||||||
|
style: NSAlert.Style = .critical
|
||||||
|
) {
|
||||||
|
Alert.confirm(
|
||||||
|
onWindow: App.shared.phpExtensionManagerWindowController!.window!,
|
||||||
|
messageText: title,
|
||||||
|
informativeText: description,
|
||||||
|
buttonTitle: button,
|
||||||
|
secondButtonTitle: "",
|
||||||
|
style: style,
|
||||||
|
onFirstButtonPressed: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func install(_ ext: BrewPhpExtension) {
|
||||||
|
Task {
|
||||||
|
await self.runCommand(InstallPhpExtensionCommand(install: [ext]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func confirmUninstall(_ ext: BrewPhpExtension) {
|
||||||
|
Alert.confirm(
|
||||||
|
onWindow: App.shared.phpExtensionManagerWindowController!.window!,
|
||||||
|
messageText: "phpextman.warnings.removal.title".localized(ext.name),
|
||||||
|
informativeText: "phpextman.warnings.removal.desc".localized(ext.name),
|
||||||
|
buttonTitle: "phpextman.warnings.removal.button".localized,
|
||||||
|
buttonIsDestructive: true,
|
||||||
|
secondButtonTitle: "generic.cancel".localized,
|
||||||
|
style: .warning,
|
||||||
|
onFirstButtonPressed: {
|
||||||
|
Task {
|
||||||
|
await self.runCommand(RemovePhpExtensionCommand(remove: ext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func runCommand(_ command: BrewCommand) async {
|
||||||
|
if PhpEnvironments.shared.isBusy {
|
||||||
|
self.presentErrorAlert(
|
||||||
|
title: "phpman.action_prevented_busy.title".localized,
|
||||||
|
description: "phpman.action_prevented_busy.desc".localized,
|
||||||
|
button: "generic.ok".localized
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let phpVersionManaged = self.manager.phpVersion
|
||||||
|
|
||||||
|
do {
|
||||||
|
self.status.busy = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.manager.phpVersion = phpVersionManaged
|
||||||
|
self.status.busy = false
|
||||||
|
} catch let error {
|
||||||
|
let error = error as! BrewCommandError
|
||||||
|
let messages = error.log.suffix(2).joined(separator: "\n")
|
||||||
|
|
||||||
|
self.status.busy = false
|
||||||
|
|
||||||
|
self.presentErrorAlert(
|
||||||
|
title: "phpman.failures.install.title".localized,
|
||||||
|
description: "phpman.failures.install.desc".localized(messages),
|
||||||
|
button: "generic.ok".localized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user