mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-08 04:20:07 +02:00
Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
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:
|
||||
|
||||
* 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.)
|
||||
|
||||
**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.
|
||||
|
||||
### 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
|
||||
|
||||
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"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1540"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1540"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1540"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1540"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1540"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
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).
|
||||
|
||||
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
|
||||
|
||||
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
||||
|
||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
|
||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.3.
|
||||
|
||||
You can install other supported versions of PHP via PHP Monitor's **PHP 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):
|
||||
|
||||
| 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
|
||||
|
||||
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 |
|
||||
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
||||
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 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" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.988",
|
||||
"green" : "0.723",
|
||||
"green" : "0.444",
|
||||
"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 {
|
||||
await brew("link php --overwrite --force")
|
||||
|
||||
// TODO: Verify that this worked, if not, notify the user
|
||||
}
|
||||
|
||||
public static func restartPhpFpm() async {
|
||||
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 {
|
||||
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
|
||||
}
|
||||
|
@ -19,10 +19,50 @@ struct Constants {
|
||||
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 = [
|
||||
"8.3", "8.4"
|
||||
static let PhpFormulaeCutoffDate = "2024-11-01" // YYYY-MM-DD
|
||||
|
||||
/**
|
||||
* The PHP versions that are considered pre-release versions.
|
||||
* Past a certain date, an experimental version "graduates"
|
||||
* to a release version and is no longer marked as experimental.
|
||||
*/
|
||||
static var ExperimentalPhpVersions: Set<String> {
|
||||
let releaseDates = [
|
||||
"8.4": Date.fromString("2024-12-01") // PLACEHOLDER DATE
|
||||
]
|
||||
|
||||
return Set(releaseDates
|
||||
.filter { (_: String, date: Date?) in
|
||||
guard let date else {
|
||||
return false
|
||||
}
|
||||
|
||||
return date > Date.now
|
||||
}.map { (version: String, _: Date?) in
|
||||
return version
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
The Homebrew services that should be automatically
|
||||
detected and show up in the list of managed services.
|
||||
*/
|
||||
static let DetectedHomebrewServices: Set = [
|
||||
"mailhog",
|
||||
"mysql@",
|
||||
"postgresql@",
|
||||
"redis"
|
||||
]
|
||||
|
||||
/**
|
||||
@ -32,8 +72,7 @@ struct Constants {
|
||||
static let DetectedPhpVersions: Set = [
|
||||
"5.6",
|
||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2",
|
||||
"8.3",
|
||||
"8.0", "8.1", "8.2", "8.3",
|
||||
"8.4"
|
||||
]
|
||||
|
||||
@ -50,14 +89,14 @@ struct Constants {
|
||||
3: // Valet v3 dropped support for v5.6
|
||||
[
|
||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2",
|
||||
"8.3", "8.4" // dev
|
||||
"8.0", "8.1", "8.2", "8.3",
|
||||
"8.4" // dev
|
||||
],
|
||||
4: // Valet v4 dropped support for v7.0
|
||||
[
|
||||
"7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2",
|
||||
"8.3", "8.4" // dev
|
||||
"8.0", "8.1", "8.2", "8.3",
|
||||
"8.4" // dev
|
||||
]
|
||||
]
|
||||
|
||||
@ -91,6 +130,8 @@ struct Constants {
|
||||
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
||||
)!
|
||||
|
||||
// EAP URLs
|
||||
|
||||
static let EarlyAccessCaskFile = URL(
|
||||
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
||||
)!
|
||||
|
@ -102,6 +102,14 @@ public class Paths {
|
||||
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 {
|
||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||
|
@ -15,4 +15,10 @@ extension Date {
|
||||
return dateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
static func fromString(_ string: String) -> Date? {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||
return dateFormatter.date(from: string)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,6 +8,18 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct Localization {
|
||||
static var preferredLanguage: String? {
|
||||
guard let language = Preferences.preferences[.languageOverride] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if language.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
return language
|
||||
}
|
||||
|
||||
static var bundle: Bundle = {
|
||||
if !isRunningTests {
|
||||
return Bundle.main
|
||||
@ -32,7 +44,15 @@ struct Localization {
|
||||
|
||||
extension 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)
|
||||
if string == self {
|
||||
|
@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
|
||||
// MARK: — FS Attributes
|
||||
|
||||
func makeExecutable(_ path: String) throws {
|
||||
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
}
|
||||
|
||||
// MARK: - Checks
|
||||
|
@ -75,7 +75,7 @@ class MenuBarImageGenerator {
|
||||
|
||||
// Then we'll fetch the image we want on the left
|
||||
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
||||
if iconType == nil {
|
||||
if iconType == nil || !MenuBarIcon.allCases.map({ $0.rawValue }).contains(iconType) {
|
||||
Log.warn("Invalid icon type found, using the default")
|
||||
iconType = MenuBarIcon.iconPhp.rawValue
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
||||
App.shared.remove(window: windowName)
|
||||
}
|
||||
|
||||
func windowDidResize(_ notification: Notification) {}
|
||||
|
||||
deinit {
|
||||
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.
|
||||
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
|
||||
*/
|
||||
public func system(_ command: String) -> String {
|
||||
let task = Process()
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
// Load extension information
|
||||
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||
|
||||
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
||||
iniFiles.append(file)
|
||||
}
|
||||
|
||||
// Get configuration values
|
||||
limits = Limits(
|
||||
memory_limit: getByteCount(key: "memory_limit"),
|
||||
@ -76,15 +69,10 @@ class ActivePhpInstallation {
|
||||
post_max_size: getByteCount(key: "post_max_size")
|
||||
)
|
||||
|
||||
// Return a list of .ini files parsed after php.ini
|
||||
let paths = Command.execute(
|
||||
path: Paths.php,
|
||||
arguments: ["-r", "echo php_ini_scanned_files();"],
|
||||
trimNewlines: false
|
||||
)
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.split(separator: ",")
|
||||
.map { String($0) }
|
||||
let paths = ActiveShell.shared
|
||||
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||
.split(separator: "\n")
|
||||
.map { String($0) }
|
||||
|
||||
// See if any extensions are present in said .ini files
|
||||
paths.forEach { (iniFilePath) in
|
||||
|
@ -13,7 +13,7 @@ class PhpEnvironments {
|
||||
// MARK: - Initializer
|
||||
|
||||
/**
|
||||
|
||||
Loads the currently active PHP installation upon startup. May be empty.
|
||||
*/
|
||||
init() {
|
||||
self.currentInstall = ActivePhpInstallation.load()
|
||||
@ -29,7 +29,7 @@ class PhpEnvironments {
|
||||
/**
|
||||
Determine which PHP version the `php` formula is aliased to.
|
||||
*/
|
||||
func determinePhpAlias() async {
|
||||
@MainActor func determinePhpAlias() async {
|
||||
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
||||
|
||||
self.homebrewPackage = try! JSONDecoder().decode(
|
||||
@ -37,7 +37,28 @@ class PhpEnvironments {
|
||||
from: brewPhpAlias.data(using: .utf8)!
|
||||
).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
|
||||
@ -49,12 +70,10 @@ class PhpEnvironments {
|
||||
static let shared = PhpEnvironments()
|
||||
|
||||
/** Whether the switcher is busy performing any actions. */
|
||||
var isBusy: Bool = false {
|
||||
@MainActor var isBusy: Bool = false {
|
||||
didSet {
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.setBusyImage()
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +87,14 @@ class PhpEnvironments {
|
||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||
|
||||
/** Information about the currently linked PHP installation. */
|
||||
var currentInstall: ActivePhpInstallation?
|
||||
var currentInstall: ActivePhpInstallation? {
|
||||
didSet {
|
||||
// Let the PHP extension manager, if it exists, know the version changed
|
||||
if let version = currentInstall?.version.short {
|
||||
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The version that the `php` formula via Brew is aliased to on the current system.
|
||||
@ -79,7 +105,12 @@ class PhpEnvironments {
|
||||
|
||||
As such, we take that information from Homebrew.
|
||||
*/
|
||||
static var brewPhpAlias: String {
|
||||
static var brewPhpAlias: String = ""
|
||||
|
||||
/**
|
||||
It's possible for the alias to be newer than the actual installed version of PHP.
|
||||
*/
|
||||
static var homebrewBrewPhpAlias: String {
|
||||
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
||||
|
||||
return PhpEnvironments.shared.homebrewPackage.version
|
||||
@ -146,7 +177,12 @@ class PhpEnvironments {
|
||||
|
||||
// Avoid inserting a duplicate
|
||||
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||
supportedVersions.insert(phpAlias)
|
||||
let phpAliasInstall = PhpInstallation(phpAlias)
|
||||
// Before inserting, ensure that the actual output matches the alias
|
||||
// if that isn't the case, our formula remains out-of-date
|
||||
if !phpAliasInstall.isMissingBinary {
|
||||
supportedVersions.insert(phpAlias)
|
||||
}
|
||||
}
|
||||
|
||||
availablePhpVersions = Array(supportedVersions)
|
||||
|
@ -67,7 +67,7 @@ class PhpExtension {
|
||||
|
||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||
|
||||
self.enabled = !line.contains(";")
|
||||
self.enabled = !line.starts(with: ";")
|
||||
self.file = file
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ class PhpExtension {
|
||||
You may need to restart the other services in order for this change to apply.
|
||||
*/
|
||||
func toggle() async {
|
||||
let newLine = enabled
|
||||
let newLine = !line.starts(with: ";")
|
||||
// DISABLED: Commented out line
|
||||
? "; \(line)"
|
||||
// ENABLED: Line where the comment delimiter (;) is removed
|
||||
@ -84,14 +84,14 @@ class PhpExtension {
|
||||
|
||||
await sed(file: file, original: line, replacement: newLine)
|
||||
|
||||
enabled.toggle()
|
||||
self.enabled = !newLine.starts(with: ";")
|
||||
self.line = newLine
|
||||
|
||||
if !isRunningTests {
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Static Methods
|
||||
|
@ -12,19 +12,46 @@ class PhpInstallation {
|
||||
|
||||
var versionNumber: VersionNumber
|
||||
|
||||
var iniFiles: [PhpConfigurationFile] = []
|
||||
|
||||
var isMissingBinary: Bool = false
|
||||
|
||||
var isHealthy: Bool = true
|
||||
|
||||
var extensions: [PhpExtension] {
|
||||
return self.iniFiles.flatMap({ $0.extensions })
|
||||
}
|
||||
|
||||
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,
|
||||
we’ll simply run `php-config --version` in the relevant directory.
|
||||
*/
|
||||
init(_ version: String) {
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
|
||||
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||
|
||||
let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||
versionNumber = VersionNumber.make(from: version)!
|
||||
|
||||
self.versionNumber = VersionNumber.make(from: version)!
|
||||
determineVersion(phpConfigExecutablePath, phpExecutablePath)
|
||||
determineHealth(phpExecutablePath)
|
||||
determineIniFiles(phpExecutablePath)
|
||||
|
||||
// Find all enabled extensions
|
||||
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
|
||||
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
|
||||
}
|
||||
|
||||
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
|
||||
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||
let longVersionString = Command.execute(
|
||||
path: phpConfigExecutablePath,
|
||||
@ -34,9 +61,15 @@ class PhpInstallation {
|
||||
|
||||
// The parser should always work, or the string has to be very unusual.
|
||||
// If so, the app SHOULD crash, so that the users report what's up.
|
||||
self.versionNumber = try! VersionNumber.parse(longVersionString)
|
||||
versionNumber = try! VersionNumber.parse(longVersionString)
|
||||
} else {
|
||||
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
|
||||
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
|
||||
isMissingBinary = true
|
||||
}
|
||||
}
|
||||
|
||||
private func determineHealth(_ phpExecutablePath: String) {
|
||||
if FileSystem.fileExists(phpExecutablePath) {
|
||||
let testCommand = Command.execute(
|
||||
path: phpExecutablePath,
|
||||
@ -53,4 +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)
|
||||
}
|
||||
|
||||
// MARK: - PHP FPM pool
|
||||
// MARK: - Corrections
|
||||
|
||||
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||
@ -54,37 +54,7 @@ extension InternalSwitcher {
|
||||
return false
|
||||
}
|
||||
|
||||
func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||
return [
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/php-fpm.d/valet-fpm.conf",
|
||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||
],
|
||||
applies: { Valet.shared.version!.major > 2 }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/error_log.ini",
|
||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||
],
|
||||
applies: { return true }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/php-memory-limits.ini",
|
||||
source: "/cli/stubs/php-memory-limits.ini",
|
||||
replacements: [:],
|
||||
applies: { return true }
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||
public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||
let files = self.getExpectedConfigurationFiles(for: version)
|
||||
|
||||
// For each of the files, attempt to fix anything that is wrong
|
||||
@ -124,6 +94,38 @@ extension InternalSwitcher {
|
||||
return outcomes.contains(true)
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
private func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||
return [
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/php-fpm.d/valet-fpm.conf",
|
||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||
],
|
||||
applies: { Valet.shared.version!.major > 2 }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/error_log.ini",
|
||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||
],
|
||||
applies: { return true }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/php-memory-limits.ini",
|
||||
source: "/cli/stubs/php-memory-limits.ini",
|
||||
replacements: [:],
|
||||
applies: { return true }
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct ExpectedConfigurationFile {
|
||||
|
@ -86,14 +86,37 @@ class RealShell: ShellProtocol {
|
||||
|
||||
// MARK: - Shellable Protocol
|
||||
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
let task = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
|
||||
if Log.shared.verbosity == .cli {
|
||||
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
}
|
||||
|
||||
func pipe(_ command: String) async -> ShellOutput {
|
||||
let task = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
// Seriously slow down how long it takes for the shell to return output
|
||||
// (in order to debug or identify async issues)
|
||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||
Log.info("[SLOW SHELL] \(command)")
|
||||
await delay(seconds: 3.0)
|
||||
@ -104,20 +127,20 @@ class RealShell: ShellProtocol {
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
let stdOut = String(
|
||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!
|
||||
|
||||
let stdErr = String(
|
||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!
|
||||
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
|
||||
if Log.shared.verbosity == .cli {
|
||||
var args = task.arguments ?? []
|
||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||
var log = """
|
||||
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
}
|
||||
|
||||
private func log(task: Process, stdOut: String, stdErr: String) {
|
||||
var args = task.arguments ?? []
|
||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||
var log = """
|
||||
|
||||
<~~~~~~~~~~~~~~~~~~~~~~~
|
||||
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
||||
@ -126,22 +149,19 @@ class RealShell: ShellProtocol {
|
||||
\(stdOut)
|
||||
"""
|
||||
|
||||
if !stdErr.isEmpty {
|
||||
log.append("""
|
||||
if !stdErr.isEmpty {
|
||||
log.append("""
|
||||
[ERR]:
|
||||
\(stdErr)
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
log.append("""
|
||||
log.append("""
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~>
|
||||
|
||||
""")
|
||||
|
||||
Log.info(log)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
Log.info(log)
|
||||
}
|
||||
|
||||
func quiet(_ command: String) async {
|
||||
|
@ -14,6 +14,16 @@ protocol ShellProtocol {
|
||||
*/
|
||||
var PATH: String { get }
|
||||
|
||||
/**
|
||||
Run a command synchronously. Use with caution.
|
||||
|
||||
Common usage:
|
||||
```
|
||||
let output = Shell.sync("php -v")
|
||||
```
|
||||
*/
|
||||
func sync(_ command: String) -> ShellOutput
|
||||
|
||||
/**
|
||||
Run a command asynchronously.
|
||||
Returns the most relevant output (prefers error output if it exists).
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// PhpFormulaeStatus.swift
|
||||
// BusyStatus.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 02/05/2023.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpFormulaeStatus: ObservableObject {
|
||||
class BusyStatus: ObservableObject {
|
||||
@Published var busy: Bool
|
||||
@Published var title: String
|
||||
@Published var description: String
|
||||
@ -18,4 +18,12 @@ class PhpFormulaeStatus: ObservableObject {
|
||||
self.title = title
|
||||
self.description = description
|
||||
}
|
||||
|
||||
public static func notBusy() -> BusyStatus {
|
||||
return BusyStatus(busy: false, title: "", description: "")
|
||||
}
|
||||
|
||||
public static func busy() -> BusyStatus {
|
||||
return BusyStatus(busy: false, title: "", description: "")
|
||||
}
|
||||
}
|
@ -73,12 +73,26 @@ public struct TestableConfiguration: Codable {
|
||||
: .fake(.text)
|
||||
]) { (_, new) in new }
|
||||
|
||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"]
|
||||
= version.long
|
||||
// PHP configuration files
|
||||
self.shellOutput["/opt/homebrew/opt/php@\(version.short)/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||
|
||||
// PHP Homebrew operations
|
||||
self.shellOutput["/opt/homebrew/bin/brew unlink php@\(version.short)"] = .delayed(0.2, "OK")
|
||||
self.shellOutput["sudo /opt/homebrew/bin/brew services stop php@\(version.short)"] = .delayed(0.2, "OK")
|
||||
self.shellOutput["sudo /opt/homebrew/bin/brew services start php@\(version.short)"] = .delayed(0.2, "OK")
|
||||
self.shellOutput["/opt/homebrew/bin/brew link php@\(version.short) --overwrite --force"] = .delayed(0.2, "OK")
|
||||
|
||||
// PHP version output
|
||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"] = version.long
|
||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php -v"] = "OK"
|
||||
|
||||
if primary {
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php"]
|
||||
= .instant("php")
|
||||
// Files expected to be present for currently linked PHP version
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php"] =
|
||||
.instant("php")
|
||||
self.shellOutput["/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||
self.filesystem["/opt/homebrew/opt/php"]
|
||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
||||
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
||||
@ -89,12 +103,8 @@ public struct TestableConfiguration: Codable {
|
||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
|
||||
self.commandOutput["/opt/homebrew/bin/php-config --version"]
|
||||
= version.long
|
||||
self.commandOutput["/opt/homebrew/bin/php -r echo php_ini_scanned_files();"] =
|
||||
"""
|
||||
/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini,
|
||||
"""
|
||||
} else {
|
||||
|
||||
// Output expected to be present for non-linked PHP versions
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
||||
BatchFakeShellOutput.instant(
|
||||
self.secondaryPhpVersions
|
||||
@ -103,6 +113,7 @@ public struct TestableConfiguration: Codable {
|
||||
)
|
||||
}
|
||||
}
|
||||
// swiftlint:enable function_body_length
|
||||
|
||||
// MARK: Interactions
|
||||
|
||||
|
@ -19,6 +19,17 @@ public class TestableShell: ShellProtocol {
|
||||
|
||||
var expectations: [String: BatchFakeShellOutput] = [:]
|
||||
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
// This assertion will only fire during test builds
|
||||
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
|
||||
|
||||
guard let expectation = expectations[command] else {
|
||||
return .err("No Expected Output")
|
||||
}
|
||||
|
||||
return expectation.syncOutput()
|
||||
}
|
||||
|
||||
func quiet(_ command: String) async {
|
||||
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
||||
}
|
||||
@ -112,6 +123,29 @@ struct BatchFakeShellOutput: Codable {
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
Outputs the fake shell output as expected, but does this synchronously.
|
||||
*/
|
||||
public func syncOutput(
|
||||
ignoreDelay: Bool = false
|
||||
) -> ShellOutput {
|
||||
let output = ShellOutput.empty()
|
||||
|
||||
for item in items {
|
||||
if !ignoreDelay {
|
||||
Thread.sleep(forTimeInterval: item.delay)
|
||||
}
|
||||
|
||||
if item.stream == .stdErr {
|
||||
output.err += item.output
|
||||
} else if item.stream == .stdOut {
|
||||
output.out += item.output
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
||||
*/
|
||||
|
@ -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>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>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/>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
@ -83,6 +83,9 @@ class App {
|
||||
/** The window controller of the PHP version manager window. */
|
||||
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
||||
|
||||
/** The window controller of the PHP extension manager window. */
|
||||
var phpExtensionManagerWindowController: PhpExtensionManagerWindowController?
|
||||
|
||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||
var detectedApplications: [Application] = []
|
||||
|
||||
|
@ -23,12 +23,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
*/
|
||||
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,
|
||||
and where to look for binaries.
|
||||
@ -96,7 +90,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
}
|
||||
|
||||
self.state = App.shared
|
||||
self.menu = MainMenu.shared
|
||||
self.paths = Paths.shared
|
||||
self.valet = Valet.shared
|
||||
self.brew = Brew.shared
|
||||
@ -109,6 +102,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
|
||||
static func initializeTestingProfile(_ path: String) {
|
||||
Log.info("The configuration with path `\(path)` is being requested...")
|
||||
// Clear for PHP Guard
|
||||
Stats.clearCurrentGlobalPhpVersion()
|
||||
// Load the configuration file
|
||||
TestableConfiguration.loadFrom(path: path).apply()
|
||||
}
|
||||
|
||||
@ -129,7 +125,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
setupNotifications()
|
||||
|
||||
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"?>
|
||||
<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>
|
||||
<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="Named colors" minToolsVersion="9.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"/>
|
||||
</connections>
|
||||
</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>
|
||||
</menu>
|
||||
</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">
|
||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
||||
<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"/>
|
||||
<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">
|
||||
<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"/>
|
||||
<size key="intercellSpacing" width="17" height="0.0"/>
|
||||
<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"/>
|
||||
</constraints>
|
||||
<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"/>
|
||||
</scroller>
|
||||
<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"/>
|
||||
</scroller>
|
||||
<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"/>
|
||||
</tableHeaderView>
|
||||
</scrollView>
|
||||
@ -1092,9 +1099,18 @@ Gw
|
||||
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
||||
</constraints>
|
||||
</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>
|
||||
<constraints>
|
||||
<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 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"/>
|
||||
@ -1103,6 +1119,7 @@ Gw
|
||||
</constraints>
|
||||
</view>
|
||||
<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="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
||||
</connections>
|
||||
|
@ -142,7 +142,7 @@ class Startup {
|
||||
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
||||
.contains("Library not loaded")
|
||||
},
|
||||
name: "`no dyld issue detected",
|
||||
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||
titleText: "startup.errors.dyld_library.title".localized,
|
||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||
Paths.optPath
|
||||
|
@ -28,8 +28,6 @@ import Foundation
|
||||
}
|
||||
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
MainMenu.shared.setBusyImage()
|
||||
MainMenu.shared.rebuild()
|
||||
|
||||
window = TerminalProgressWindowController.display(
|
||||
title: "alert.composer_progress.title".localized,
|
||||
@ -106,14 +104,11 @@ import Foundation
|
||||
|
||||
private func removeBusyStatus() {
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.updatePhpVersionInStatusBar()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Alert
|
||||
|
||||
@MainActor private func presentMissingAlert() {
|
||||
private func presentMissingAlert() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.composer_missing.title".localized,
|
||||
|
@ -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.
|
||||
*/
|
||||
public static let SpecificDependencyList = [
|
||||
"roots/bedrock": "Bedrock",
|
||||
"roots/bedrock-autoloader": "Bedrock",
|
||||
"cakephp/app": "CakePHP",
|
||||
"craftcms/craft": "Craft",
|
||||
"drupal/core": "Drupal",
|
||||
|
@ -8,16 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class BrewFormulaeObservable: ObservableObject {
|
||||
@Published var phpVersions: [BrewFormula] = []
|
||||
|
||||
var upgradeable: [BrewFormula] {
|
||||
return phpVersions.filter { formula in
|
||||
formula.hasUpgrade
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Brew {
|
||||
static let shared = Brew()
|
||||
|
||||
@ -45,10 +35,11 @@ class Brew {
|
||||
|
||||
/// Each formula for each PHP version that can be installed.
|
||||
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.1": "php@8.1",
|
||||
"8.0": "php@8.0",
|
||||
"8.0": "shivammathur/php/php@8.0",
|
||||
"7.4": "shivammathur/php/php@7.4",
|
||||
"7.3": "shivammathur/php/php@7.3",
|
||||
"7.2": "shivammathur/php/php@7.2",
|
||||
|
@ -27,6 +27,21 @@ class BrewDiagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Logs a bunch of useful information during startup.
|
||||
*/
|
||||
public static func logBootInformation() {
|
||||
Log.info(BrewDiagnostics.customCaskInstalled
|
||||
? "[BREW] The app has been installed via Homebrew Cask."
|
||||
: "[BREW] The app has been installed directly (optimal)."
|
||||
)
|
||||
|
||||
Log.info(BrewDiagnostics.usesNginxFullFormula
|
||||
? "[BREW] The app will be using the `nginx-full` formula."
|
||||
: "[BREW] The app will be using the `nginx` formula."
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Determines whether the PHP Monitor Cask is installed.
|
||||
*/
|
||||
@ -46,6 +61,43 @@ class BrewDiagnostics {
|
||||
return destination.contains("/nginx-full/")
|
||||
}()
|
||||
|
||||
/**
|
||||
It is possible to have outdated symlinks for PHP installations. This can mean that certain PHP installations
|
||||
are going to be reported incorrectly (e.g. `php@8.2` links to an installation in a `8.3` folder after an upgrade).
|
||||
|
||||
To ensure this does not cause issues, PHP Monitor will automatically remove all incorrect PHP symlinks.
|
||||
*/
|
||||
public static func checkForOutdatedPhpInstallationSymlinks() async {
|
||||
// Set up a regular expression
|
||||
let regex = try! NSRegularExpression(pattern: "^php@[0-9]+\\.[0-9]+$", options: .caseInsensitive)
|
||||
|
||||
// Check for incorrect versions
|
||||
if let contents = try? FileSystem.getShallowContentsOfDirectory("\(Paths.optPath)")
|
||||
.filter({
|
||||
let range = NSRange($0.startIndex..., in: $0)
|
||||
return regex.firstMatch(in: $0, options: [], range: range) != nil
|
||||
}) {
|
||||
|
||||
for symlink in contents {
|
||||
let version = symlink.replacingOccurrences(of: "php@", with: "")
|
||||
if let destination = try? FileSystem.getDestinationOfSymlink("\(Paths.optPath)/\(symlink)") {
|
||||
if !destination.contains("Cellar/php/\(version)")
|
||||
&& !destination.contains("Cellar/php@\(version)") {
|
||||
Log.err("Symlink for \(symlink) is incorrect. Removing...")
|
||||
do {
|
||||
try FileSystem.remove("\(Paths.optPath)/\(symlink)")
|
||||
Log.info("Incorrect symlink for \(symlink) has been successfully removed.")
|
||||
} catch {
|
||||
Log.err("Symlink for \(symlink) was incorrect but could not be removed!")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.warn("Could not read symlink at: \(Paths.optPath)/\(symlink)! Symlink check skipped.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||
|
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
@ -0,0 +1,98 @@
|
||||
//
|
||||
// BrewPhpExtension.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 27/11/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct BrewPhpExtension: Hashable, Comparable {
|
||||
let name: String
|
||||
let phpVersion: String
|
||||
let isInstalled: Bool
|
||||
let path: String
|
||||
let dependencies: [String]
|
||||
|
||||
var extensionDependencies: [String] {
|
||||
return dependencies
|
||||
.filter {
|
||||
$0.contains("shivammathur/extensions/") && $0.contains("@\(phpVersion)")
|
||||
}
|
||||
.map {
|
||||
$0.replacingOccurrences(of: "shivammathur/extensions/", with: "")
|
||||
.replacingOccurrences(of: "@\(phpVersion)", with: "")
|
||||
}
|
||||
}
|
||||
|
||||
var formulaName: String {
|
||||
return "\(name)@\(phpVersion)"
|
||||
}
|
||||
|
||||
init(path: String, name: String, phpVersion: String) {
|
||||
self.path = path
|
||||
self.name = name
|
||||
self.phpVersion = phpVersion
|
||||
|
||||
self.isInstalled = BrewPhpExtension.hasInstallationReceipt(
|
||||
for: "\(name)@\(phpVersion)"
|
||||
)
|
||||
|
||||
self.dependencies = BrewPhpExtension.extractDependencies(from: path)
|
||||
}
|
||||
|
||||
var hasAlternativeInstall: Bool {
|
||||
guard let php = PhpEnvironments.shared.cachedPhpInstallations[self.phpVersion] else {
|
||||
return false
|
||||
}
|
||||
|
||||
let alreadyDiscovered = php.extensions.contains(where: { $0.name == self.name })
|
||||
|
||||
return alreadyDiscovered && !isInstalled
|
||||
}
|
||||
|
||||
internal func firstDependent(in exts: [BrewPhpExtension]) -> BrewPhpExtension? {
|
||||
return exts
|
||||
.filter({ $0.isInstalled })
|
||||
.first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") }
|
||||
}
|
||||
|
||||
static func hasInstallationReceipt(for formulaName: String) -> Bool {
|
||||
return FileSystem.fileExists("\(Paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
|
||||
}
|
||||
|
||||
static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||
return lhs.name < rhs.name
|
||||
}
|
||||
|
||||
static func == (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||
return lhs.name == rhs.name
|
||||
}
|
||||
|
||||
private static func extractDependencies(from path: String) -> [String] {
|
||||
let regexPattern = #"depends_on "(.*)""#
|
||||
var dependencies: [String] = []
|
||||
|
||||
guard let content = try? FileSystem.getStringFromFile(path) else {
|
||||
return []
|
||||
}
|
||||
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: regexPattern, options: [])
|
||||
let range = NSRange(content.startIndex..<content.endIndex, in: content)
|
||||
let matches = regex.matches(in: content, options: [], range: range)
|
||||
|
||||
for match in matches {
|
||||
if let range = Range(match.range(at: 1), in: content) {
|
||||
let dependencyName = String(content[range])
|
||||
dependencies.append(dependencyName)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// BrewFormula.swift
|
||||
// BrewPhpFormula.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 17/03/2023.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct BrewFormula {
|
||||
struct BrewPhpFormula: Equatable {
|
||||
/// Name of the formula.
|
||||
let name: String
|
||||
|
||||
@ -21,6 +21,8 @@ struct BrewFormula {
|
||||
/// The upgrade that is currently available, if it exists.
|
||||
let upgradeVersion: String?
|
||||
|
||||
// TODO: A rebuild attribute could be checked, to check if a Tap update exists for a pre-release version
|
||||
|
||||
/// Whether this formula is a stable version of PHP.
|
||||
let prerelease: Bool
|
||||
|
||||
@ -48,6 +50,25 @@ struct BrewFormula {
|
||||
return upgradeVersion != nil
|
||||
}
|
||||
|
||||
/// Whether this formula alias is different.
|
||||
var hasUpgradedFormulaAlias: Bool {
|
||||
return self.shortVersion == PhpEnvironments.homebrewBrewPhpAlias
|
||||
&& PhpEnvironments.homebrewBrewPhpAlias != PhpEnvironments.brewPhpAlias
|
||||
}
|
||||
|
||||
var unavailableAfterUpgrade: Bool {
|
||||
if installedVersion == nil || upgradeVersion == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if let installed = try? VersionNumber.parse(self.installedVersion!),
|
||||
let upgrade = try? VersionNumber.parse(self.upgradeVersion!) {
|
||||
return upgrade.short != installed.short
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/// The associated Homebrew folder with this PHP formula.
|
||||
var homebrewFolder: String {
|
||||
let resolved = name
|
||||
@ -60,7 +81,7 @@ struct BrewFormula {
|
||||
/// The short version associated with this formula, if installed.
|
||||
var shortVersion: String? {
|
||||
guard let version = self.installedVersion else {
|
||||
return nil
|
||||
return self.displayName.replacingOccurrences(of: "PHP ", with: "")
|
||||
}
|
||||
|
||||
return VersionNumber.make(from: version)?.short ?? nil
|
||||
@ -81,6 +102,7 @@ struct BrewFormula {
|
||||
return nil
|
||||
}
|
||||
|
||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?.isHealthy ?? nil
|
||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?
|
||||
.isHealthy ?? nil
|
||||
}
|
||||
}
|
@ -8,22 +8,23 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol HandlesBrewFormulae {
|
||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula]
|
||||
protocol HandlesBrewPhpFormulae {
|
||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula]
|
||||
func refreshPhpVersions(loadOutdated: Bool) async
|
||||
}
|
||||
|
||||
extension HandlesBrewFormulae {
|
||||
extension HandlesBrewPhpFormulae {
|
||||
public func refreshPhpVersions(loadOutdated: Bool) async {
|
||||
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
||||
Task { @MainActor in
|
||||
await PhpEnvironments.shared.determinePhpAlias()
|
||||
Brew.shared.formulae.phpVersions = items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BrewFormulaeHandler: HandlesBrewFormulae {
|
||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] {
|
||||
class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
|
||||
var outdated: [OutdatedFormula]?
|
||||
|
||||
if loadOutdated {
|
||||
@ -43,17 +44,17 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
||||
}
|
||||
|
||||
return Brew.phpVersionFormulae.map { (version, formula) in
|
||||
let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?.versionNumber.text
|
||||
|
||||
var fullVersion: 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
|
||||
return formula.installed_versions.contains(version)
|
||||
return formula.name == install.formulaName
|
||||
})?.current_version
|
||||
}
|
||||
|
||||
return BrewFormula(
|
||||
return BrewPhpFormula(
|
||||
name: formula,
|
||||
displayName: "PHP \(version)",
|
||||
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 {
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
||||
|
||||
func getCommandTitle() -> String
|
||||
}
|
||||
|
||||
extension BrewCommand {
|
||||
@ -31,6 +33,44 @@ extension BrewCommand {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
internal func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
var loggedMessages: [String] = []
|
||||
|
||||
let (process, _) = try! await Shell.attach(
|
||||
command,
|
||||
didReceiveOutput: { text, _ in
|
||||
if !text.isEmpty {
|
||||
Log.perf(text)
|
||||
loggedMessages.append(text)
|
||||
}
|
||||
|
||||
if let (number, text) = self.reportInstallationProgress(text) {
|
||||
onProgress(.create(value: number, title: getCommandTitle(), description: text))
|
||||
}
|
||||
},
|
||||
withTimeout: .minutes(15)
|
||||
)
|
||||
|
||||
if process.terminationStatus <= 0 {
|
||||
loggedMessages = []
|
||||
return
|
||||
} else {
|
||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||
}
|
||||
}
|
||||
|
||||
internal func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
if !BrewDiagnostics.installedTaps.contains("shivammathur/php") {
|
||||
let command = "brew tap shivammathur/php"
|
||||
try await run(command, onProgress)
|
||||
}
|
||||
|
||||
if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") {
|
||||
let command = "brew tap shivammathur/extensions"
|
||||
try await run(command, onProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BrewCommandProgress {
|
||||
|
@ -0,0 +1,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
|
||||
|
||||
class InstallAndUpgradeCommand: BrewCommand {
|
||||
|
||||
class ModifyPhpVersionCommand: BrewCommand {
|
||||
let title: String
|
||||
let installing: [BrewFormula]
|
||||
let upgrading: [BrewFormula]
|
||||
let installing: [BrewPhpFormula]
|
||||
let upgrading: [BrewPhpFormula]
|
||||
let phpGuard: PhpGuard
|
||||
|
||||
func getCommandTitle() -> String {
|
||||
return title
|
||||
}
|
||||
|
||||
/**
|
||||
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
||||
The process will be executed in two steps: first upgrades, then installations.
|
||||
|
||||
Upgrades come first because... well, otherwise installations may very well break.
|
||||
Each version that is installed will need to be checked afterwards (if it is OK).
|
||||
Each version that is installed will need to be checked afterwards. Installing a
|
||||
newer formula may break other PHP installations, which in turn need to be fixed.
|
||||
|
||||
- Important: If any PHP formula is a major upgrade that causes a PHP "version" to be
|
||||
uninstalled, this is remedied by running `upgradeMainPhpFormula()`. This process
|
||||
will ensure that the upgrade is applied, but the also that old version is
|
||||
re-installed and linked again.
|
||||
*/
|
||||
public init(
|
||||
title: String,
|
||||
upgrading: [BrewFormula],
|
||||
installing: [BrewFormula]
|
||||
upgrading: [BrewPhpFormula],
|
||||
installing: [BrewPhpFormula]
|
||||
) {
|
||||
self.title = title
|
||||
self.installing = installing
|
||||
@ -33,17 +43,32 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
}
|
||||
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
let progressTitle = "Please wait..."
|
||||
let progressTitle = "phpman.steps.wait".localized
|
||||
|
||||
onProgress(.create(
|
||||
value: 0.2,
|
||||
title: progressTitle,
|
||||
description: "PHP Monitor is preparing Homebrew..."
|
||||
description: "phpman.steps.preparing".localized
|
||||
))
|
||||
|
||||
// Try to run all upgrade and installation operations
|
||||
try await self.upgradePackages(onProgress)
|
||||
try await self.installPackages(onProgress)
|
||||
// Determine if a formula will become unavailable
|
||||
// This is the case when `php` will be bumped to a new version
|
||||
let unavailable = upgrading.first(where: { formula in
|
||||
formula.unavailableAfterUpgrade
|
||||
})
|
||||
|
||||
// Make sure the tap is installed
|
||||
try await self.checkPhpTap(onProgress)
|
||||
|
||||
if unavailable == nil {
|
||||
// Try to run all upgrade and installation operations
|
||||
try await self.upgradePackages(onProgress)
|
||||
try await self.installPackages(onProgress)
|
||||
} else {
|
||||
// Simply upgrade `php` to the latest version
|
||||
try await self.upgradeMainPhpFormula(unavailable!, onProgress)
|
||||
await PhpEnvironments.shared.determinePhpAlias()
|
||||
}
|
||||
|
||||
// Re-check the installed versions
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
@ -55,6 +80,27 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
await self.completedOperations(onProgress)
|
||||
}
|
||||
|
||||
private func upgradeMainPhpFormula(
|
||||
_ unavailable: BrewPhpFormula,
|
||||
_ onProgress: @escaping (BrewCommandProgress) -> Void
|
||||
) async throws {
|
||||
// Determine which version was previously available (that will become unavailable)
|
||||
guard let short = try? VersionNumber
|
||||
.parse(unavailable.installedVersion!).short else {
|
||||
return
|
||||
}
|
||||
|
||||
// Upgrade the main formula
|
||||
let command = """
|
||||
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||
\(Paths.brew) upgrade php;
|
||||
\(Paths.brew) install php@\(short);
|
||||
"""
|
||||
|
||||
// Run the upgrade command
|
||||
try await run(command, onProgress)
|
||||
}
|
||||
|
||||
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
// If no upgrades are needed, early exit
|
||||
if self.upgrading.isEmpty {
|
||||
@ -117,35 +163,12 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
try await run(command, onProgress)
|
||||
}
|
||||
|
||||
private func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
var loggedMessages: [String] = []
|
||||
|
||||
let (process, _) = try! await Shell.attach(
|
||||
command,
|
||||
didReceiveOutput: { text, _ in
|
||||
if !text.isEmpty {
|
||||
Log.perf(text)
|
||||
loggedMessages.append(text)
|
||||
}
|
||||
|
||||
if let (number, text) = self.reportInstallationProgress(text) {
|
||||
onProgress(.create(value: number, title: self.title, description: text))
|
||||
}
|
||||
},
|
||||
withTimeout: .minutes(15)
|
||||
)
|
||||
|
||||
if process.terminationStatus <= 0 {
|
||||
loggedMessages = []
|
||||
return
|
||||
} else {
|
||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||
}
|
||||
}
|
||||
|
||||
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||
// Reload and restart PHP versions
|
||||
onProgress(.create(value: 0.95, title: self.title, description: "Reloading PHP versions..."))
|
||||
onProgress(.create(value: 0.95, title: self.title, description: "phpman.steps.reloading".localized))
|
||||
|
||||
// Ensure all symlinks are correctly linked
|
||||
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||
|
||||
// Check which version of PHP are now installed
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
@ -164,9 +187,8 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
// Let the UI know that the installation has been completed
|
||||
onProgress(.create(
|
||||
value: 1,
|
||||
title: "Operation completed!",
|
||||
description: "The installation has succeeded."
|
||||
title: "phpman.steps.completed".localized,
|
||||
description: "phpman.steps.success".localized
|
||||
))
|
||||
}
|
||||
|
||||
}
|
@ -21,13 +21,15 @@ class RemovePhpVersionCommand: BrewCommand {
|
||||
self.phpGuard = PhpGuard()
|
||||
}
|
||||
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
let progressTitle = "Removing PHP \(version)..."
|
||||
func getCommandTitle() -> String {
|
||||
return "phpman.steps.removing".localized("PHP \(version)...")
|
||||
}
|
||||
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
onProgress(.create(
|
||||
value: 0.2,
|
||||
title: progressTitle,
|
||||
description: "Please wait while Homebrew removes PHP \(version)..."
|
||||
title: getCommandTitle(),
|
||||
description: "phpman.steps.wait".localized
|
||||
))
|
||||
|
||||
let command = """
|
||||
@ -56,7 +58,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
||||
)
|
||||
|
||||
if process.terminationStatus <= 0 {
|
||||
onProgress(.create(value: 0.95, title: progressTitle, description: "Reloading PHP versions..."))
|
||||
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
|
||||
@ -66,7 +68,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
||||
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 {
|
||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||
}
|
@ -9,6 +9,10 @@
|
||||
import Foundation
|
||||
|
||||
class FakeCommand: BrewCommand {
|
||||
func getCommandTitle() -> String {
|
||||
return "Hello"
|
||||
}
|
||||
|
||||
let version: String
|
||||
|
||||
init(version: String) {
|
||||
|
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
|
||||
}
|
||||
|
||||
setBusyImage()
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
PhpEnvironments.shared.delegate = self
|
||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||
|
||||
@ -298,13 +297,12 @@ extension MainMenu {
|
||||
}
|
||||
|
||||
@objc func switchToPhpVersion(_ version: String) {
|
||||
setBusyImage()
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
PhpEnvironments.shared.delegate = self
|
||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
|
||||
Task(priority: .userInitiated) { [unowned self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||
|
||||
@ -325,13 +323,12 @@ extension MainMenu {
|
||||
*/
|
||||
func switchToPhp(_ version: String) async {
|
||||
Task { @MainActor [self] in
|
||||
setBusyImage()
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
PhpEnvironments.shared.delegate = self
|
||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
}
|
||||
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||
|
||||
|
@ -45,21 +45,16 @@ extension MainMenu {
|
||||
.broadcastServicesUpdate
|
||||
]
|
||||
) {
|
||||
if behaviours.contains(.reloadsPhpInstallation) {
|
||||
if behaviours.contains(.reloadsPhpInstallation) || behaviours.contains(.setsBusyUI) {
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
}
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
setBusyImage()
|
||||
}
|
||||
|
||||
Task(priority: .userInitiated) { [unowned self] in
|
||||
var error: Error?
|
||||
|
||||
do { try execute() } catch let e { error = e }
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
do { try execute() } catch let e {
|
||||
error = e
|
||||
Log.err(e)
|
||||
}
|
||||
|
||||
Task { @MainActor [self, error] in
|
||||
@ -68,15 +63,18 @@ extension MainMenu {
|
||||
}
|
||||
|
||||
if behaviours.contains(.updatesMenuBarContents) {
|
||||
updatePhpVersionInStatusBar()
|
||||
} else if behaviours.contains(.setsBusyUI) {
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
}
|
||||
|
||||
if behaviours.contains(.broadcastServicesUpdate) {
|
||||
Task { await ServicesManager.shared.reloadServicesStatus() }
|
||||
}
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
}
|
||||
|
||||
if error != nil {
|
||||
return failure(error!)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ extension MainMenu {
|
||||
func startup() async {
|
||||
// Start with the icon
|
||||
Task { @MainActor in
|
||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
self.setStatusBar(image: NSImage.statusBarIcon)
|
||||
}
|
||||
|
||||
if await Startup().checkEnvironment() {
|
||||
@ -32,19 +32,14 @@ extension MainMenu {
|
||||
// Determine what the `php` formula is aliased to
|
||||
await PhpEnvironments.shared.determinePhpAlias()
|
||||
|
||||
// Make sure that broken symlinks are removed ASAP
|
||||
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||
|
||||
// Initialize preferences
|
||||
_ = Preferences.shared
|
||||
|
||||
// Determine install method
|
||||
Log.info(BrewDiagnostics.customCaskInstalled
|
||||
? "[BREW] The app has been installed via Homebrew Cask."
|
||||
: "[BREW] The app has been installed directly (optimal)."
|
||||
)
|
||||
|
||||
Log.info(BrewDiagnostics.usesNginxFullFormula
|
||||
? "[BREW] The app will be using the `nginx-full` formula."
|
||||
: "[BREW] The app will be using the `nginx` formula."
|
||||
)
|
||||
// Put some useful diagnostics information in log
|
||||
BrewDiagnostics.logBootInformation()
|
||||
|
||||
// Attempt to find out more info about Valet
|
||||
if Valet.shared.version != nil {
|
||||
@ -63,9 +58,6 @@ extension MainMenu {
|
||||
// Check for an alias conflict
|
||||
await BrewDiagnostics.checkForCaskConflict()
|
||||
|
||||
// Update the icon
|
||||
updatePhpVersionInStatusBar()
|
||||
|
||||
// Attempt to find out if PHP-FPM is broken
|
||||
PhpEnvironments.prepare()
|
||||
|
||||
@ -76,7 +68,6 @@ extension MainMenu {
|
||||
WarningManager.shared.evaluateWarnings()
|
||||
|
||||
// Set up the config watchers on launch (updated automatically when switching)
|
||||
Log.info("Setting up watchers...")
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
|
||||
// Detect built-in and custom applications
|
||||
@ -105,9 +96,33 @@ extension MainMenu {
|
||||
Valet.shared.notifyAboutUnsupportedTLD()
|
||||
}
|
||||
|
||||
// Keep track of which PHP versions are currently about to release
|
||||
Log.info("Experimental PHP versions are: \(Constants.ExperimentalPhpVersions)")
|
||||
|
||||
// Find out which services are active
|
||||
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
||||
|
||||
// Post-launch stats and update check, but only if not running tests
|
||||
await performPostLaunchActions()
|
||||
|
||||
// Check if the linked version has changed between launches of phpmon
|
||||
PhpGuard().compareToLastGlobalVersion()
|
||||
|
||||
// We are ready!
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
|
||||
// Finally!
|
||||
Log.info("PHP Monitor is ready to serve!")
|
||||
|
||||
// Check if we upgraded from a previous version
|
||||
AppUpdater.checkIfUpdateWasPerformed()
|
||||
}
|
||||
|
||||
/**
|
||||
Performs a set of post-launch actions, like incrementing stats and checking for updates.
|
||||
(This code is skipped when running SwiftUI previews.)
|
||||
*/
|
||||
private func performPostLaunchActions() async {
|
||||
if !isRunningSwiftUIPreview {
|
||||
Stats.incrementSuccessfulLaunchCount()
|
||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||
@ -121,15 +136,6 @@ extension MainMenu {
|
||||
await AppUpdater().checkForUpdates(userInitiated: false)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the linked version has changed between launches of phpmon
|
||||
PhpGuard().compareToLastGlobalVersion()
|
||||
|
||||
// We are ready!
|
||||
Log.info("PHP Monitor is ready to serve!")
|
||||
|
||||
// Check if we upgraded just now
|
||||
AppUpdater.checkIfUpdateWasPerformed()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,9 @@ extension MainMenu {
|
||||
|
||||
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
||||
// Mark as no longer busy
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
Task { @MainActor in
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
}
|
||||
|
||||
Task { // Things to do after reloading domain list data
|
||||
if Valet.installed {
|
||||
@ -25,7 +27,7 @@ extension MainMenu {
|
||||
|
||||
// Perform UI updates on main thread
|
||||
Task { @MainActor [self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
|
||||
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
||||
|
@ -37,8 +37,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
// MARK: - UI related
|
||||
|
||||
/**
|
||||
Rebuilds the menu (either asynchronously or synchronously).
|
||||
Defaults to rebuilding the menu asynchronously.
|
||||
Rebuilds the menu on the main thread.
|
||||
*/
|
||||
func rebuild() {
|
||||
Task { @MainActor [self] in
|
||||
@ -80,13 +79,15 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
@objc func refreshActiveInstallation() {
|
||||
if !PhpEnvironments.shared.isBusy {
|
||||
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
} else {
|
||||
Log.perf("Skipping version refresh due to busy status!")
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||
@available(*, deprecated, message: "Use the busy status instead")
|
||||
@objc func updatePhpVersionInStatusBar() {
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
@ -139,7 +140,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
@objc func reloadPhpMonitorMenuInBackground() {
|
||||
asyncExecution({
|
||||
// This automatically reloads the menu
|
||||
Log.info("Reloading information about the PHP installation (in the background)...")
|
||||
Log.perf("Reloading information about the PHP installation (in the background)...")
|
||||
}, behaviours: [
|
||||
.setsBusyUI,
|
||||
.reloadsPhpInstallation,
|
||||
@ -150,13 +151,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
|
||||
/** Refreshes the icon with the PHP version. */
|
||||
@objc func refreshIcon() {
|
||||
|
||||
Task { @MainActor [self] in
|
||||
if PhpEnvironments.shared.isBusy {
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
Log.perf("Refreshing icon: currently busy")
|
||||
setStatusBar(image: NSImage.statusBarIcon)
|
||||
} else {
|
||||
Log.perf("Refreshing icon: no longer busy")
|
||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||
// Static icon has been requested
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
||||
setStatusBar(image: NSImage.statusBarIconStatic)
|
||||
} else {
|
||||
// The dynamic icon has been requested
|
||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||
@ -172,13 +176,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the icon to be displayed as busy. */
|
||||
@objc func setBusyImage() {
|
||||
Task { @MainActor [self] in
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Menu Item Functionality
|
||||
|
||||
@objc func openAbout() {
|
||||
@ -218,6 +215,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
PhpVersionManagerWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openPhpExtensionManager() {
|
||||
PhpExtensionManagerWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openDonate() {
|
||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import Cocoa
|
||||
|
||||
extension StatusMenu {
|
||||
|
||||
func addPhpVersionMenuItems() {
|
||||
@MainActor func addPhpVersionMenuItems() {
|
||||
if PhpEnvironments.phpInstall == nil {
|
||||
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
||||
addItems([
|
||||
@ -34,7 +34,7 @@ extension StatusMenu {
|
||||
))
|
||||
}
|
||||
|
||||
func addPhpActionMenuItems() {
|
||||
@MainActor func addPhpActionMenuItems() {
|
||||
if PhpEnvironments.shared.isBusy {
|
||||
addItem(NSMenuItem(title: "mi_busy".localized))
|
||||
return
|
||||
@ -54,7 +54,7 @@ extension StatusMenu {
|
||||
self.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
func addServicesManagerMenuItem() {
|
||||
@MainActor func addServicesManagerMenuItem() {
|
||||
if PhpEnvironments.shared.isBusy {
|
||||
return
|
||||
}
|
||||
@ -65,7 +65,7 @@ extension StatusMenu {
|
||||
])
|
||||
}
|
||||
|
||||
func addSwitchToPhpMenuItems() {
|
||||
@MainActor func addSwitchToPhpMenuItems() {
|
||||
var shortcutKey = 1
|
||||
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
||||
// Get the short and long version
|
||||
@ -102,14 +102,14 @@ extension StatusMenu {
|
||||
}
|
||||
}
|
||||
|
||||
func addLiteModeMenuItem() {
|
||||
@MainActor func addLiteModeMenuItem() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
||||
])
|
||||
}
|
||||
|
||||
func addPreferencesMenuItems() {
|
||||
@MainActor func addPreferencesMenuItems() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_preferences".localized,
|
||||
@ -119,7 +119,7 @@ extension StatusMenu {
|
||||
])
|
||||
}
|
||||
|
||||
func addCoreMenuItems() {
|
||||
@MainActor func addCoreMenuItems() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_about".localized,
|
||||
@ -131,7 +131,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Valet
|
||||
|
||||
func addValetMenuItems() {
|
||||
@MainActor func addValetMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_valet".localized),
|
||||
NSMenuItem(title: "mi_valet_config".localized,
|
||||
@ -146,12 +146,15 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - PHP Configuration
|
||||
|
||||
func addConfigurationMenuItems() {
|
||||
@MainActor func addConfigurationMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
||||
NSMenuItem(title: "mi_php_version_manager".localized,
|
||||
action: #selector(MainMenu.openPhpVersionManager),
|
||||
keyEquivalent: "m"),
|
||||
NSMenuItem(title: "mi_php_ext_manager".localized,
|
||||
action: #selector(MainMenu.openPhpExtensionManager),
|
||||
keyEquivalent: "e"),
|
||||
NSMenuItem(title: "mi_php_config".localized,
|
||||
action: #selector(MainMenu.openActiveConfigFolder),
|
||||
keyEquivalent: "c"),
|
||||
@ -166,7 +169,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Composer
|
||||
|
||||
func addComposerMenuItems() {
|
||||
@MainActor func addComposerMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_composer".localized),
|
||||
NSMenuItem(
|
||||
@ -187,7 +190,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Stats
|
||||
|
||||
func addStatsMenuItem() {
|
||||
@MainActor func addStatsMenuItem() {
|
||||
guard let install = PhpEnvironments.phpInstall else {
|
||||
Log.info("Not showing stats menu item if no PHP version is linked.")
|
||||
return
|
||||
@ -200,21 +203,11 @@ extension StatusMenu {
|
||||
post: stats.post_max_size,
|
||||
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
|
||||
|
||||
func addExtensionsMenuItems() {
|
||||
@MainActor func addExtensionsMenuItems() {
|
||||
guard let install = PhpEnvironments.phpInstall else {
|
||||
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
||||
return
|
||||
@ -235,7 +228,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Presets
|
||||
|
||||
func addPresetsMenuItem() {
|
||||
@MainActor func addPresetsMenuItem() {
|
||||
guard let presets = Preferences.custom.presets else {
|
||||
addEmptyPresetHelp()
|
||||
return
|
||||
@ -249,7 +242,7 @@ extension StatusMenu {
|
||||
addLoadedPresets()
|
||||
}
|
||||
|
||||
private func addEmptyPresetHelp() {
|
||||
@MainActor private func addEmptyPresetHelp() {
|
||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||
NSMenuItem(title: "mi_no_presets".localized),
|
||||
NSMenuItem.separator(),
|
||||
@ -258,7 +251,7 @@ extension StatusMenu {
|
||||
], target: MainMenu.shared))
|
||||
}
|
||||
|
||||
private func addLoadedPresets() {
|
||||
@MainActor private func addLoadedPresets() {
|
||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||
NSMenuItem.separator(),
|
||||
HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)
|
||||
@ -273,7 +266,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Xdebug
|
||||
|
||||
func addXdebugMenuItem() {
|
||||
@MainActor func addXdebugMenuItem() {
|
||||
if !Xdebug.enabled {
|
||||
addItem(NSMenuItem.separator())
|
||||
return
|
||||
@ -293,7 +286,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - PHP Doctor
|
||||
|
||||
func addPhpDoctorMenuItem() {
|
||||
@MainActor func addPhpDoctorMenuItem() {
|
||||
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
|
||||
!WarningManager.shared.hasWarnings() {
|
||||
return
|
||||
@ -309,7 +302,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - First Aid & Services
|
||||
|
||||
func addFirstAidAndServicesMenuItems() {
|
||||
@MainActor func addFirstAidAndServicesMenuItems() {
|
||||
let services = NSMenuItem(title: "mi_other".localized)
|
||||
|
||||
var items: [NSMenuItem] = [
|
||||
@ -366,7 +359,7 @@ extension StatusMenu {
|
||||
|
||||
// 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 menuItem = ExtensionMenuItem(
|
||||
|
@ -9,7 +9,7 @@ import Cocoa
|
||||
|
||||
class StatusMenu: NSMenu {
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
func addMenuItems() {
|
||||
@MainActor func addMenuItems() {
|
||||
addPhpVersionMenuItems()
|
||||
addItem(NSMenuItem.separator())
|
||||
|
||||
|
@ -91,7 +91,7 @@ class BetterAlert {
|
||||
}
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
|
||||
windowController.window?.makeKeyAndOrderFront(nil)
|
||||
windowController.window?.setCenterPosition(offsetY: 70)
|
||||
return NSApplication.shared.runModal(for: windowController.window!)
|
||||
|
@ -20,6 +20,7 @@ enum PreferenceName: String, Codable {
|
||||
case globalHotkey = "global_hotkey"
|
||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
||||
case languageOverride = "language_override"
|
||||
|
||||
// APPEARANCE
|
||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||
@ -84,7 +85,8 @@ enum PreferenceName: String, Codable {
|
||||
],
|
||||
.string: [
|
||||
.globalHotkey,
|
||||
.iconTypeToDisplay
|
||||
.iconTypeToDisplay,
|
||||
.languageOverride
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ class Preferences {
|
||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
||||
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
||||
PreferenceName.languageOverride.rawValue: "",
|
||||
|
||||
/// Preferences: Appearance
|
||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||
|
@ -17,7 +17,9 @@ class GeneralPreferencesVC: GenericPreferenceVC {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||
|
||||
_ = vc.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
||||
_ = vc
|
||||
.addView(when: true, vc.getLanguageOptionsPV())
|
||||
.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
||||
.addView(when: true, vc.getAutoRestartServicesPV())
|
||||
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
||||
.addView(when: true, vc.getShortcutPV())
|
||||
|
@ -48,11 +48,44 @@ class GenericPreferenceVC: NSViewController {
|
||||
)
|
||||
}
|
||||
|
||||
func getLanguageOptionsPV() -> NSView {
|
||||
var options = Bundle.main.localizations
|
||||
.filter({ $0 != "Base"})
|
||||
.map({ lang in
|
||||
return PreferenceDropdownOption(
|
||||
label: Locale.current.localizedString(forLanguageCode: lang)!,
|
||||
value: lang
|
||||
)
|
||||
})
|
||||
options.insert(PreferenceDropdownOption(label: "System Default", value: ""), at: 0)
|
||||
|
||||
return SelectPreferenceView.make(
|
||||
sectionText: "prefs.language".localized,
|
||||
descriptionText: "prefs.language_options_desc".localized,
|
||||
options: options,
|
||||
preference: .languageOverride,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.rebuild()
|
||||
|
||||
if let window = App.shared.preferencesWindowController?.window {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "alert.language_changed.title".localized
|
||||
alert.informativeText = "alert.language_changed.subtitle".localized
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: "generic.ok".localized)
|
||||
alert.beginSheetModal(for: window)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func getIconOptionsPV() -> NSView {
|
||||
return SelectPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.icon_options_desc".localized,
|
||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
||||
options: MenuBarIcon.allCases
|
||||
.map({ return PreferenceDropdownOption(label: $0.rawValue, value: $0.rawValue) }),
|
||||
localizationPrefix: "prefs.icon_options",
|
||||
preference: .iconTypeToDisplay,
|
||||
action: {
|
||||
|
@ -68,6 +68,8 @@ class PreferencesWindowController: PMWindowController {
|
||||
App.shared.preferencesWindowController?.positionWindowInTopRightCorner()
|
||||
}
|
||||
|
||||
App.shared.preferencesWindowController?.window?.orderFrontRegardless()
|
||||
|
||||
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.
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@ -23,7 +23,7 @@
|
||||
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -31,7 +31,7 @@
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||
|
@ -9,30 +9,34 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class SelectPreferenceView: NSView, XibLoadable {
|
||||
struct PreferenceDropdownOption {
|
||||
let label: String
|
||||
let value: String
|
||||
}
|
||||
|
||||
class SelectPreferenceView: NSView, XibLoadable {
|
||||
@IBOutlet weak var labelSection: NSTextField!
|
||||
@IBOutlet weak var labelDescription: NSTextField!
|
||||
@IBOutlet weak var popupButton: NSPopUpButton!
|
||||
|
||||
var localizationPrefix: String = ""
|
||||
var localizationPrefix: String?
|
||||
var imagePrefix: String?
|
||||
|
||||
var options: [String] = [] {
|
||||
var options: [PreferenceDropdownOption] = [] {
|
||||
didSet {
|
||||
self.popupButton.removeAllItems()
|
||||
self.options.forEach { value in
|
||||
self.popupButton.addItem(
|
||||
withTitle: "\(localizationPrefix).\(value)".localized
|
||||
)
|
||||
self.options.forEach { option in
|
||||
if let prefix = localizationPrefix {
|
||||
self.popupButton.addItem(withTitle: "\(prefix).\(option.label)".localized)
|
||||
} else {
|
||||
self.popupButton.addItem(withTitle: option.label)
|
||||
}
|
||||
}
|
||||
|
||||
if imagePrefix == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.popupButton.itemArray.enumerated().forEach { item in
|
||||
item.element.image = NSImage(named: "\(imagePrefix!)_\(self.options[item.offset])")
|
||||
if let prefix = imagePrefix {
|
||||
self.popupButton.itemArray.enumerated().forEach { item in
|
||||
item.element.image = NSImage(named: "\(prefix)_\(self.options[item.offset].value)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,19 +47,18 @@ class SelectPreferenceView: NSView, XibLoadable {
|
||||
didSet {
|
||||
let value = Preferences.preferences[preference] as! String
|
||||
self.options.enumerated().forEach { option in
|
||||
if option.element == value {
|
||||
if option.element.value == value {
|
||||
self.popupButton.selectItem(at: option.offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable function_parameter_count
|
||||
static func make(
|
||||
sectionText: String,
|
||||
descriptionText: String,
|
||||
options: [String],
|
||||
localizationPrefix: String,
|
||||
options: [PreferenceDropdownOption],
|
||||
localizationPrefix: String? = nil,
|
||||
imagePrefix: String? = nil,
|
||||
preference: PreferenceName,
|
||||
action: @escaping () -> Void) -> NSView {
|
||||
@ -72,11 +75,10 @@ class SelectPreferenceView: NSView, XibLoadable {
|
||||
|
||||
return view
|
||||
}
|
||||
// swiftlint:enable function_parameter_count
|
||||
|
||||
@IBAction func valueChanged(_ sender: Any) {
|
||||
let index = self.popupButton.indexOfSelectedItem
|
||||
Preferences.update(.iconTypeToDisplay, value: self.options[index])
|
||||
Preferences.update(self.preference, value: self.options[index].value)
|
||||
self.action()
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="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>
|
||||
<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"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@ -13,16 +13,16 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||
<rect key="frame" x="183" y="5" width="395" height="14"/>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||
<rect key="frame" x="13" y="29" width="154" height="16"/>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||
<rect key="frame" x="-2" y="29" width="154" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||
</constraints>
|
||||
@ -33,7 +33,7 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<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">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<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 firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
||||
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" 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"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
|
@ -29,7 +29,7 @@ struct BlockingOverlayView<Content: View>: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
content().opacity(isBlocking ? 0.2 : 1)
|
||||
content().opacity(isBlocking ? 0 : 1)
|
||||
if isBlocking {
|
||||
VStack {
|
||||
ActivityIndicator()
|
||||
@ -44,7 +44,8 @@ struct BlockingOverlayView<Content: View>: View {
|
||||
.padding(.top, -4)
|
||||
}.padding(60)
|
||||
}
|
||||
}.background(Color.white)
|
||||
}
|
||||
.background(Color.spinnerBackground)
|
||||
.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())
|
||||
.focusable(false)
|
||||
}
|
||||
|
||||
struct HelpButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
HelpButton(action: {}).padding()
|
||||
.previewDisplayName("Light Mode")
|
||||
HelpButton(action: {}).padding().preferredColorScheme(.dark)
|
||||
.previewDisplayName("Dark Mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Light Mode") {
|
||||
HelpButton(action: {})
|
||||
.padding(100)
|
||||
}
|
||||
|
||||
#Preview("Dark Mode") {
|
||||
HelpButton(action: {})
|
||||
.padding(100)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
@ -27,7 +27,9 @@ var isRunningSwiftUIPreview: Bool {
|
||||
|
||||
extension Color {
|
||||
public static var appPrimary: Color = Color("AppColor")
|
||||
public static var appSecondary: Color = Color("AppSecondary")
|
||||
|
||||
// This next one is generated automatically via asset catalogs now
|
||||
// public static var appSecondary: Color = Color("AppSecondary")
|
||||
|
||||
public static var debug: Color = {
|
||||
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
||||
|
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 prefersIsolationSuggestions: Bool
|
||||
|
||||
@State var parent: NSPopover!
|
||||
|
||||
let rows = [
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible())
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(getTitleText())
|
||||
@ -32,14 +40,29 @@ struct VersionPopoverView: View {
|
||||
message: "alert.php_suggestions".localized,
|
||||
color: Color("AppColor")
|
||||
)
|
||||
HStack {
|
||||
ForEach(validPhpVersions, id: \.self) { version in
|
||||
Button("site_link.switch_to_php".localized(version.short), action: {
|
||||
MainMenu.shared.switchToPhpVersion(version.short)
|
||||
parent?.close()
|
||||
})
|
||||
}
|
||||
}.padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0))
|
||||
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
|
||||
Button("site_link.switch_to_php".localized(version.short), action: {
|
||||
MainMenu.shared.switchToPhpVersion(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 {
|
||||
if site.preferredPhpVersionSource == .unknown {
|
||||
@ -126,78 +149,90 @@ struct DisclaimerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct VersionPopoverView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: ""
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Unknown Requirement")
|
||||
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.1"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Requirement Matches")
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0",
|
||||
isolated: "8.0"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Isolated")
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0",
|
||||
isolated: "7.4"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Isolated Mismatch")
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0"
|
||||
),
|
||||
validPhpVersions: [
|
||||
VersionNumber(major: 8, minor: 0, patch: 0),
|
||||
VersionNumber(major: 8, minor: 1, patch: 0)
|
||||
],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Recommend Alternatives")
|
||||
}
|
||||
#Preview("Unknown Requirement") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: ""
|
||||
),
|
||||
validPhpVersions: [],
|
||||
prefersIsolationSuggestions: false,
|
||||
parent: nil
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("Requirement Matches") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.1"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
prefersIsolationSuggestions: false,
|
||||
parent: nil
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("Isolated") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0",
|
||||
isolated: "8.0"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
prefersIsolationSuggestions: false,
|
||||
parent: nil
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("Isolated Mismatch") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0",
|
||||
isolated: "7.4"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
prefersIsolationSuggestions: false,
|
||||
parent: nil
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("Recommend Alternatives") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0"
|
||||
),
|
||||
validPhpVersions: [
|
||||
VersionNumber(major: 8, minor: 0, 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
|
||||
)
|
||||
}
|
||||
|
@ -45,9 +45,7 @@ struct HeaderView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct HeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HeaderView(text: "Hello world")
|
||||
.frame(width: 330.0)
|
||||
}
|
||||
#Preview {
|
||||
HeaderView(text: "Hello world")
|
||||
.frame(width: 330.0)
|
||||
}
|
||||
|
@ -172,23 +172,21 @@ struct ServiceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ServicesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: ["php", "nginx", "dnsmasq"],
|
||||
status: .active
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
.previewDisplayName("Active 1")
|
||||
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: [
|
||||
"php", "nginx", "dnsmasq", "thing1",
|
||||
"thing2", "thing3", "thing4", "thing5"
|
||||
],
|
||||
status: .inactive
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
.previewDisplayName("Active 2")
|
||||
}
|
||||
#Preview("Active 1") {
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: ["php", "nginx", "dnsmasq"],
|
||||
status: .active
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
}
|
||||
|
||||
#Preview("Active 2") {
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: [
|
||||
"php", "nginx", "dnsmasq", "thing1",
|
||||
"thing2", "thing3", "thing4", "thing5"
|
||||
],
|
||||
status: .inactive
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
}
|
||||
|
@ -89,6 +89,7 @@ struct StatsView: View {
|
||||
} label: {
|
||||
Image(systemName: "gearshape.fill")
|
||||
}
|
||||
.accessibility(identifier: "phpConfigButton")
|
||||
.focusable(false)
|
||||
.frame(minWidth: 30, alignment: .center)
|
||||
}
|
||||
@ -98,12 +99,10 @@ struct StatsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct StatsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
StatsView(
|
||||
memoryLimit: "1024 MB",
|
||||
maxPostSize: "1024 MB",
|
||||
maxUploadSize: "1024 MB"
|
||||
).frame(height: 100)
|
||||
}
|
||||
#Preview {
|
||||
StatsView(
|
||||
memoryLimit: "1024 MB",
|
||||
maxPostSize: "1024 MB",
|
||||
maxUploadSize: "1024 MB"
|
||||
).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)
|
||||
}
|
||||
|
||||
|
||||
self?.parent.didChange?(self!.url)
|
||||
}
|
||||
|
||||
|
@ -16,21 +16,19 @@ class DomainListKindCell: NSTableCellView, DomainListCellProtocol {
|
||||
|
||||
func populateCell(with site: ValetSite) {
|
||||
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
||||
imageViewType.image = NSImage(
|
||||
named: site.aliasPath == nil
|
||||
? "IconParked"
|
||||
: "IconLinked"
|
||||
)
|
||||
imageViewType.image = site.aliasPath == nil
|
||||
? NSImage.iconParked
|
||||
: NSImage.iconLinked
|
||||
|
||||
// Unless, of course, this is a default site
|
||||
if site.absolutePath == Valet.shared.config.defaultSite {
|
||||
imageViewType.image = NSImage(named: "IconDefault")
|
||||
imageViewType.image = NSImage.iconDefault
|
||||
}
|
||||
|
||||
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
||||
}
|
||||
|
||||
func populateCell(with proxy: ValetProxy) {
|
||||
imageViewType.image = NSImage(named: "IconProxy")
|
||||
imageViewType.image = NSImage.iconProxy
|
||||
}
|
||||
}
|
||||
|
@ -34,14 +34,13 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||
|
||||
if site.isolatedPhpVersion != nil {
|
||||
imageViewPhpVersionOK.isHidden = false
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
|
||||
imageViewPhpVersionOK.image = NSImage.isolated
|
||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.isolated".localized(site.servingPhpVersion)
|
||||
} else {
|
||||
imageViewPhpVersionOK.isHidden = (site.preferredPhpVersion == "???"
|
||||
|| !site.isCompatibleWithPreferredPhpVersion)
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
|
||||
imageViewPhpVersionOK.image = NSImage.checkmark
|
||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.preferredPhpVersion)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +70,12 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||
let button = self.buttonPhpVersion!
|
||||
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.behavior = .transient
|
||||
|
@ -110,17 +110,33 @@ extension DomainListVC {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func isolateSite(sender: PhpMenuItem) {
|
||||
guard let site = selectedSite else {
|
||||
return
|
||||
}
|
||||
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
||||
Task {
|
||||
self.setUIBusy()
|
||||
|
||||
await sender.phpExtension?.toggle()
|
||||
|
||||
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
|
||||
await Actions.restartPhpFpm()
|
||||
}
|
||||
|
||||
reloadContextMenu()
|
||||
|
||||
self.setUINotBusy()
|
||||
}
|
||||
}
|
||||
|
||||
public func isolateSite(site: ValetSite, version: String) {
|
||||
waitAndExecute {
|
||||
do {
|
||||
// Instruct Valet to isolate a given PHP version
|
||||
try await site.isolate(version: sender.version)
|
||||
// Reload the UI
|
||||
self.reloadSelectedRow()
|
||||
try await site.isolate(version: version)
|
||||
// Reload the UI if it's the same site
|
||||
if self.selectedSite?.absolutePath == site.absolutePath {
|
||||
self.reloadSelectedRow()
|
||||
} else {
|
||||
await self.reloadDomains()
|
||||
}
|
||||
} catch {
|
||||
// Notify the user about a failed command
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
@ -13,22 +13,31 @@ extension DomainListVC {
|
||||
internal func reloadContextMenu() {
|
||||
guard let selected = selected else {
|
||||
tableView.menu = nil
|
||||
AppMenu.actionsMenu?.title = "mm_actions".localized
|
||||
AppMenu.actionsMenu?.submenu = nil
|
||||
AppMenu.actionsMenu?.isEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Menu Items for Site
|
||||
|
||||
private func addMenuItemsForSite(_ site: ValetSite) {
|
||||
private func addMenuItemsForSite(_ site: ValetSite) -> NSMenu? {
|
||||
let menu = NSMenu()
|
||||
|
||||
addSystemApps(to: menu)
|
||||
@ -42,11 +51,24 @@ extension DomainListVC {
|
||||
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))
|
||||
|
||||
addToggleSecure(to: menu, secured: site.secured)
|
||||
addUnlink(to: menu, with: site)
|
||||
|
||||
tableView.menu = menu
|
||||
return menu
|
||||
}
|
||||
|
||||
private func addSystemApps(to menu: NSMenu) {
|
||||
@ -108,7 +130,7 @@ extension DomainListVC {
|
||||
for version in PhpEnvironments.shared.availablePhpVersions.reversed() {
|
||||
let item = PhpMenuItem(
|
||||
title: "domain_list.always_use_php".localized(version),
|
||||
action: #selector(self.isolateSite),
|
||||
action: #selector(self.isolateSiteViaMenuItem),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
if site.servingPhpVersion == version && site.isolatedPhpVersion != nil {
|
||||
@ -124,7 +146,7 @@ extension DomainListVC {
|
||||
items.append(NSMenuItem.separator())
|
||||
items.append(NSMenuItem(
|
||||
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
|
||||
|
||||
private func addMenuItemsForProxy(_ proxy: ValetProxy) {
|
||||
private func addMenuItemsForProxy(_ proxy: ValetProxy) -> NSMenu {
|
||||
let menu = NSMenu()
|
||||
addOpenProxyInBrowser(to: menu)
|
||||
addSeparator(to: menu)
|
||||
addToggleSecure(to: menu, secured: proxy.secured)
|
||||
addRemoveProxy(to: menu)
|
||||
tableView.menu = menu
|
||||
return menu
|
||||
}
|
||||
|
||||
private func addOpenProxyInBrowser(to menu: NSMenu) {
|
||||
|
@ -8,12 +8,14 @@
|
||||
|
||||
import Cocoa
|
||||
import Carbon
|
||||
import SwiftUI
|
||||
|
||||
class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
@IBOutlet weak var tableView: PMTableView!
|
||||
@IBOutlet weak var noResultsView: NSView!
|
||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||
|
||||
// MARK: - Variables
|
||||
@ -93,6 +95,8 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
override func viewDidLoad() {
|
||||
tableView.doubleAction = #selector(self.doubleClicked(sender:))
|
||||
|
||||
addNoResultsView()
|
||||
|
||||
let mapping = [
|
||||
"SECURE": "domain_list.columns.secure",
|
||||
"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
|
||||
|
||||
/**
|
||||
@ -132,6 +155,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
tableView.alphaValue = 0.3
|
||||
tableView.isEnabled = false
|
||||
tableView.selectRowIndexes([], byExtendingSelection: true)
|
||||
noResultsView.isHidden = true
|
||||
}
|
||||
|
||||
/**
|
||||
@ -142,6 +166,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
progressIndicator.stopAnimation(nil)
|
||||
tableView.alphaValue = 1.0
|
||||
tableView.isEnabled = true
|
||||
updateNoResultsView()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -282,9 +307,14 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
|
||||
Task { @MainActor in
|
||||
self.tableView.reloadData()
|
||||
updateNoResultsView()
|
||||
}
|
||||
}
|
||||
|
||||
func updateNoResultsView() {
|
||||
self.noResultsView.isHidden = !domains.isEmpty
|
||||
}
|
||||
|
||||
func searchedFor(text: String) {
|
||||
lastSearchedFor = text
|
||||
|
||||
|
@ -31,7 +31,7 @@ struct OnboardingTextItem: View {
|
||||
.opacity(self.unavailable ? 0.5 : 1)
|
||||
Text(description.localizedForSwiftUI)
|
||||
.foregroundColor(Color.secondary)
|
||||
.font(.system(size: 13))
|
||||
.font(.system(size: 12))
|
||||
.lineLimit(4)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(minWidth: 0, maxWidth: 800, alignment: .leading)
|
||||
@ -51,7 +51,7 @@ struct OnboardingView: View {
|
||||
HStack {
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.frame(width: 100, height: 100)
|
||||
.padding(.bottom, 5)
|
||||
.padding(.trailing, 25)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
@ -126,20 +126,19 @@ struct OnboardingView: View {
|
||||
Button("onboarding.tour.close".localized) {
|
||||
App.shared.onboardingWindowController?.close()
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.bottom, 15)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
}
|
||||
.padding(.leading)
|
||||
.padding(.trailing)
|
||||
.padding(.bottom, 0)
|
||||
}
|
||||
.frame(width: 600)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
OnboardingView()
|
||||
}
|
||||
}
|
||||
#Preview {
|
||||
OnboardingView()
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class OnboardingWindowController: PMWindowController {
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.delegate = delegate ?? windowController
|
||||
window.contentView = NSHostingView(rootView: OnboardingView())
|
||||
window.setContentSize(NSSize(width: 600, height: 600))
|
||||
window.setContentSize(window.contentView!.fittingSize)
|
||||
|
||||
App.shared.onboardingWindowController = windowController
|
||||
}
|
||||
|
@ -98,19 +98,19 @@ struct ByteLimitView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ByteLimitView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PreferenceContainer(
|
||||
name: "Max Size",
|
||||
description:
|
||||
"Here's an extensive description that is obviously way too long but it should wrap." +
|
||||
"The point of the wrapping text is that is allows us to see what's going on with the layout here."
|
||||
) {
|
||||
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
|
||||
}.frame(width: 600, height: 200)
|
||||
|
||||
ConfigManagerView()
|
||||
.frame(width: 600, height: .infinity)
|
||||
.previewDisplayName("Config Manager")
|
||||
}
|
||||
#Preview("Byte Limit View") {
|
||||
PreferenceContainer(
|
||||
name: "Max Size",
|
||||
description:
|
||||
"Here's an extensive description that is obviously way too long but it should wrap." +
|
||||
"The point of the wrapping text is that is allows us to see what's going on with the layout here."
|
||||
) {
|
||||
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
|
||||
}.frame(width: 600, height: 200)
|
||||
}
|
||||
|
||||
#Preview("Config Manager") {
|
||||
ConfigManagerView()
|
||||
.frame(width: 600, height: .infinity)
|
||||
.previewDisplayName("Config Manager")
|
||||
}
|
||||
|
@ -84,10 +84,6 @@ struct ConfigManagerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigManagerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ConfigManagerView()
|
||||
.frame(width: 600)
|
||||
.previewDisplayName("Live Preview")
|
||||
}
|
||||
#Preview {
|
||||
ConfigManagerView().frame(width: 600)
|
||||
}
|
||||
|
@ -34,8 +34,8 @@ class WarningManager: ObservableObject {
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) == "1"
|
||||
},
|
||||
name: "Running PHP Monitor with Rosetta on M1",
|
||||
title: "warnings.arm_compatibility.title".localized,
|
||||
paragraphs: { return ["warnings.arm_compatibility.description".localized] },
|
||||
title: "warnings.arm_compatibility.title",
|
||||
paragraphs: { return ["warnings.arm_compatibility.description"] },
|
||||
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-and-Apple-Silicon"
|
||||
),
|
||||
Warning(
|
||||
@ -44,11 +44,11 @@ class WarningManager: ObservableObject {
|
||||
!FileSystem.isWriteableFile("/usr/local/bin/")
|
||||
},
|
||||
name: "Helpers cannot be symlinked and not in PATH",
|
||||
title: "warnings.helper_permissions.title".localized,
|
||||
title: "warnings.helper_permissions.title",
|
||||
paragraphs: { return [
|
||||
"warnings.helper_permissions.description".localized,
|
||||
"warnings.helper_permissions.unavailable".localized,
|
||||
"warnings.helper_permissions.symlink".localized
|
||||
"warnings.helper_permissions.description",
|
||||
"warnings.helper_permissions.unavailable",
|
||||
"warnings.helper_permissions.symlink"
|
||||
] },
|
||||
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries"
|
||||
),
|
||||
@ -58,7 +58,7 @@ class WarningManager: ObservableObject {
|
||||
return !PhpConfigChecker.shared.missing.isEmpty
|
||||
},
|
||||
name: "Your PHP installation is missing configuration files",
|
||||
title: "warnings.files_missing.title".localized,
|
||||
title: "warnings.files_missing.title",
|
||||
paragraphs: { return [
|
||||
"warnings.files_missing.description".localized(
|
||||
PhpConfigChecker.shared.missing.joined(separator: "\n• ")
|
||||
|
@ -25,8 +25,6 @@ struct NoWarningsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct NoWarningsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NoWarningsView()
|
||||
}
|
||||
#Preview {
|
||||
NoWarningsView().padding()
|
||||
}
|
||||
|
@ -89,19 +89,19 @@ struct PhpDoctorView: View {
|
||||
}
|
||||
.listRowInsets(EdgeInsets())
|
||||
.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 {
|
||||
static var previews: some View {
|
||||
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
|
||||
.frame(width: 600, height: 480)
|
||||
.previewDisplayName("Empty List")
|
||||
|
||||
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
|
||||
.frame(width: 600, height: 480)
|
||||
.previewDisplayName("List With All Warnings")
|
||||
}
|
||||
#Preview("Empty List") {
|
||||
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
|
||||
.frame(width: 600, height: 480)
|
||||
}
|
||||
|
||||
#Preview("List With All Warnings") {
|
||||
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
|
||||
.frame(width: 600, height: 480)
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class PhpDoctorWindowController: PMWindowController {
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.delegate = delegate ?? windowController
|
||||
window.contentView = NSHostingView(rootView: PhpDoctorView())
|
||||
window.setContentSize(NSSize(width: 600, height: 480))
|
||||
window.setContentSize(window.contentView!.fittingSize)
|
||||
|
||||
App.shared.phpDoctorWindowController = windowController
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ struct WarningView: View {
|
||||
Text(title.localizedForSwiftUI)
|
||||
.fontWeight(.bold)
|
||||
ForEach(paragraphs, id: \.self) { paragraph in
|
||||
Text(paragraph)
|
||||
Text(paragraph.localizedForSwiftUI)
|
||||
.font(.system(size: 13))
|
||||
}
|
||||
}
|
||||
@ -47,23 +47,23 @@ struct WarningView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct WarningView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WarningView(
|
||||
title: "warnings.helper_permissions.title",
|
||||
paragraphs: ["warnings.helper_permissions.description"],
|
||||
documentationUrl: "https://nicoverbruggen.be"
|
||||
)
|
||||
.frame(width: 600, height: 105)
|
||||
|
||||
WarningView(
|
||||
title: "warnings.helper_permissions.title",
|
||||
paragraphs: ["warnings.helper_permissions.description"],
|
||||
documentationUrl: "https://nicoverbruggen.be"
|
||||
)
|
||||
.preferredColorScheme(.dark)
|
||||
.frame(width: 600, height: 105)
|
||||
|
||||
// WarningListView().frame(width: 600, height: 580)
|
||||
}
|
||||
#Preview("Light Mode") {
|
||||
WarningView(
|
||||
title: "warnings.helper_permissions.title",
|
||||
paragraphs: ["warnings.helper_permissions.description"],
|
||||
documentationUrl: "https://nicoverbruggen.be"
|
||||
)
|
||||
.frame(width: 600, height: 105)
|
||||
.padding(25)
|
||||
}
|
||||
|
||||
#Preview("Dark Mode") {
|
||||
WarningView(
|
||||
title: "warnings.helper_permissions.title",
|
||||
paragraphs: ["warnings.helper_permissions.description"],
|
||||
documentationUrl: "https://nicoverbruggen.be"
|
||||
)
|
||||
.preferredColorScheme(.dark)
|
||||
.frame(width: 600, height: 105)
|
||||
.padding(25)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user