mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-08 04:20:07 +02:00
Compare commits
124 Commits
Author | SHA1 | Date | |
---|---|---|---|
94abfe4b49 | |||
cef19243ee | |||
b319ecab59 | |||
a47b139d92 | |||
e026ecf60d | |||
3c0a4a6142 | |||
87ebb20284 | |||
d60c26c9b2 | |||
5c9c51f580 | |||
0c320074da | |||
e3ea712a99 | |||
4db478ca64 | |||
3064a07d69 | |||
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 |
19
DEVELOPER.md
19
DEVELOPER.md
@ -14,6 +14,17 @@ It also automatically runs when you try to build the project. You'll get a warni
|
|||||||
swiftlint --fix
|
swiftlint --fix
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📦 Swift Packages
|
||||||
|
|
||||||
|
Starting from PHP Monitor 7.1, the app now uses various first-party package dependencies.
|
||||||
|
|
||||||
|
The following package dependencies are in use:
|
||||||
|
|
||||||
|
* [`NVAppUpdater`](https://github.com/nicoverbruggen/NVAppUpdater)
|
||||||
|
* [`NVAlert`](https://github.com/nicoverbruggen/NVAlert)
|
||||||
|
|
||||||
|
You may need an internet connection to download these dependencies, or you can also clone the dependencies and include them manually.
|
||||||
|
|
||||||
## ⚙️ Preferences
|
## ⚙️ Preferences
|
||||||
|
|
||||||
You can find the persisted configuration file in `~/Library/Preferences/com.nicoverbruggen.phpmon.plist`
|
You can find the persisted configuration file in `~/Library/Preferences/com.nicoverbruggen.phpmon.plist`
|
||||||
@ -33,17 +44,13 @@ defaults delete com.nicoverbruggen.phpmon && killall cfprefsd
|
|||||||
If you'd like to build PHP Monitor yourself, you need:
|
If you'd like to build PHP Monitor yourself, you need:
|
||||||
|
|
||||||
* Xcode (usually the latest version)
|
* Xcode (usually the latest version)
|
||||||
* *PHP Monitor Self-Updater.app* in the `phpmon-updater` directory (You can build it yourself, it is included as a target OR copy the signed app so it is included w/ PHP Monitor)
|
|
||||||
* The contents of this repository
|
|
||||||
|
|
||||||
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
|
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
|
||||||
|
|
||||||
|
**Important**: The updater now gets automatically built and included as part of the main target.
|
||||||
|
|
||||||
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
||||||
|
|
||||||
### PHP Monitor Updater
|
|
||||||
|
|
||||||
Select the separate target and build. You can then copy the product to the `phpmon-updater` directory. The binary will be re-signed when distributing the main build.
|
|
||||||
|
|
||||||
## 🚀 Release procedure
|
## 🚀 Release procedure
|
||||||
|
|
||||||
1. Merge into `main`
|
1. Merge into `main`
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1540"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -112,13 +112,15 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
|
|||||||
|
|
||||||
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
||||||
|
|
||||||
|
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
|
||||||
|
|
||||||
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
||||||
|
|
||||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
|
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.3.
|
||||||
|
|
||||||
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
||||||
|
|
||||||
|
12
SECURITY.md
12
SECURITY.md
@ -4,18 +4,20 @@
|
|||||||
|
|
||||||
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
|
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
|
||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 6.1 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 7.0 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
|
||||||
## Legacy versions
|
## Legacy versions
|
||||||
|
|
||||||
These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
|
These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
|
||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
||||||
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 674 KiB After Width: | Height: | Size: 723 KiB |
@ -1,46 +0,0 @@
|
|||||||
//
|
|
||||||
// LaunchControl.swift
|
|
||||||
// PHP Monitor Self-Updater
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 02/02/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
class LaunchControl {
|
|
||||||
public static func smartRestart(priority: [String]) async {
|
|
||||||
for appPath in priority {
|
|
||||||
if FileManager.default.fileExists(atPath: appPath) {
|
|
||||||
let app = await LaunchControl.startApplication(at: appPath)
|
|
||||||
if app != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func terminateApplications(bundleIds: [String]) async {
|
|
||||||
let runningApplications = NSWorkspace.shared.runningApplications
|
|
||||||
|
|
||||||
// Terminate all instances found
|
|
||||||
for id in bundleIds {
|
|
||||||
if let phpmon = runningApplications.first(where: {
|
|
||||||
(application) in return application.bundleIdentifier == id
|
|
||||||
}) {
|
|
||||||
phpmon.terminate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func startApplication(at path: String) async -> NSRunningApplication? {
|
|
||||||
await withCheckedContinuation { continuation in
|
|
||||||
let url = NSURL(fileURLWithPath: path, isDirectory: true) as URL
|
|
||||||
let configuration = NSWorkspace.OpenConfiguration()
|
|
||||||
NSWorkspace.shared.openApplication(at: url, configuration: configuration) { phpmon, error in
|
|
||||||
continuation.resume(returning: phpmon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,162 +0,0 @@
|
|||||||
//
|
|
||||||
// Updater.swift
|
|
||||||
// PHP Monitor Updater
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 01/02/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
class Updater: NSObject, NSApplicationDelegate {
|
|
||||||
|
|
||||||
var updaterDirectory: String = ""
|
|
||||||
var manifestPath: String = ""
|
|
||||||
var manifest: ReleaseManifest! = nil
|
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
||||||
Task { await self.installUpdate() }
|
|
||||||
}
|
|
||||||
|
|
||||||
func installUpdate() async {
|
|
||||||
print("PHP MONITOR SELF-UPDATER by Nico Verbruggen")
|
|
||||||
print("===========================================")
|
|
||||||
|
|
||||||
self.updaterDirectory = "~/.config/phpmon/updater"
|
|
||||||
.replacingOccurrences(of: "~", with: NSHomeDirectory())
|
|
||||||
|
|
||||||
print("Updater directory set to: \(self.updaterDirectory)")
|
|
||||||
|
|
||||||
self.manifestPath = "\(updaterDirectory)/update.json"
|
|
||||||
|
|
||||||
// Fetch the manifest on the local filesystem
|
|
||||||
let manifest = await parseManifest()!
|
|
||||||
|
|
||||||
// Download the latest file
|
|
||||||
let zipPath = await download(manifest)
|
|
||||||
|
|
||||||
// Terminate all instances of PHP Monitor first
|
|
||||||
await LaunchControl.terminateApplications(bundleIds: [
|
|
||||||
"com.nicoverbruggen.phpmon.eap",
|
|
||||||
"com.nicoverbruggen.phpmon.dev",
|
|
||||||
"com.nicoverbruggen.phpmon"
|
|
||||||
])
|
|
||||||
|
|
||||||
// Install the app based on the zip
|
|
||||||
let appPath = await extractAndInstall(zipPath: zipPath)
|
|
||||||
|
|
||||||
// Restart PHP Monitor, this will also close the updater
|
|
||||||
_ = await LaunchControl.startApplication(at: appPath)
|
|
||||||
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applicationWillTerminate(_ aNotification: Notification) {
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseManifest() async -> ReleaseManifest? {
|
|
||||||
// Read out the correct information from the manifest JSON
|
|
||||||
print("Checking manifest file at \(manifestPath)...")
|
|
||||||
|
|
||||||
do {
|
|
||||||
let manifestText = try String(contentsOfFile: manifestPath)
|
|
||||||
manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestText.data(using: .utf8)!)
|
|
||||||
return manifest
|
|
||||||
} catch {
|
|
||||||
print("Parsing the manifest failed (or the manifest file doesn't exist)!")
|
|
||||||
await Alert.show(description: "The manifest file for a potential update was not found. Please try searching for updates again in PHP Monitor.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func download(_ manifest: ReleaseManifest) async -> String {
|
|
||||||
// Remove all zips
|
|
||||||
system_quiet("rm -rf \(updaterDirectory)/*.zip")
|
|
||||||
|
|
||||||
// Download the file (and follow redirects + no output on failure)
|
|
||||||
system_quiet("cd \"\(updaterDirectory)\" && curl \(manifest.url) -fLO --max-time 20")
|
|
||||||
|
|
||||||
// Identify the downloaded file
|
|
||||||
let filename = system("cd \"\(updaterDirectory)\" && ls | grep .zip")
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
|
|
||||||
// Ensure the zip exists
|
|
||||||
if filename.isEmpty {
|
|
||||||
print("The update has not been downloaded. Sadly, that means that PHP Monitor cannot not updated!")
|
|
||||||
await Alert.show(description: "The update could not be downloaded, or the file was not correctly written to disk. \n\nPlease try again. \n\n(Note that the download will time-out after 20 seconds, so for slow connections it is recommended to manually download the update.)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the checksum for the downloaded file
|
|
||||||
let checksum = system("openssl dgst -sha256 \"\(updaterDirectory)/\(filename)\" | awk '{print $NF}'")
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
|
|
||||||
// Compare the checksums
|
|
||||||
print("""
|
|
||||||
Comparing checksums...
|
|
||||||
Expected SHA256: \(manifest.sha256)
|
|
||||||
Actual SHA256: \(checksum)
|
|
||||||
""")
|
|
||||||
|
|
||||||
// Make sure the checksum matches before we do anything with the file
|
|
||||||
if checksum != manifest.sha256 {
|
|
||||||
print("The checksums failed to match. Cancelling!")
|
|
||||||
await Alert.show(description: "The downloaded update failed checksum validation. Please try again. If this issue persists, there may be an issue with the server and I do not recommend upgrading.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the path to the zip
|
|
||||||
return "\(updaterDirectory)/\(filename)"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func extractAndInstall(zipPath: String) async -> String {
|
|
||||||
// Remove the directory that will contain the extracted update
|
|
||||||
system_quiet("rm -rf \"\(updaterDirectory)/extracted\"")
|
|
||||||
|
|
||||||
// Recreate the directory where we will unzip the .app file
|
|
||||||
system_quiet("mkdir -p \"\(updaterDirectory)/extracted\"")
|
|
||||||
|
|
||||||
// Make sure the updater directory exists
|
|
||||||
var isDirectory: ObjCBool = true
|
|
||||||
if !FileManager.default.fileExists(atPath: "\(updaterDirectory)/extracted", isDirectory: &isDirectory) {
|
|
||||||
await Alert.show(description: "The updater directory is missing. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unzip the file
|
|
||||||
system_quiet("unzip \"\(zipPath)\" -d \"\(updaterDirectory)/extracted\"")
|
|
||||||
|
|
||||||
// Find the .app file
|
|
||||||
let app = system("ls \"\(updaterDirectory)/extracted\" | grep .app")
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
|
|
||||||
print("Finished extracting: \(updaterDirectory)/extracted/\(app)")
|
|
||||||
|
|
||||||
// Make sure the file was extracted
|
|
||||||
if app.isEmpty {
|
|
||||||
await Alert.show(description: "The downloaded file could not be extracted. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the original app
|
|
||||||
print("Removing \(app) before replacing...")
|
|
||||||
system_quiet("rm -rf \"/Applications/\(app)\"")
|
|
||||||
|
|
||||||
// Move the new app in place
|
|
||||||
system_quiet("mv \"\(updaterDirectory)/extracted/\(app)\" \"/Applications/\(app)\"")
|
|
||||||
|
|
||||||
// Remove the zip
|
|
||||||
system_quiet("rm \"\(zipPath)\"")
|
|
||||||
|
|
||||||
// Remove the manifest
|
|
||||||
system_quiet("rm \"\(manifestPath)\"")
|
|
||||||
|
|
||||||
// Write a file that is only written when we upgraded successfully
|
|
||||||
system_quiet("touch \"\(updaterDirectory)/upgrade.success\"")
|
|
||||||
|
|
||||||
// Return the new location of the app
|
|
||||||
return "/Applications/\(app)"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
//
|
|
||||||
// Utility.swift
|
|
||||||
// PHP Monitor Self-Updater
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 02/02/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
class Alert {
|
|
||||||
public static func show(description: String, shouldExit: Bool = true) async {
|
|
||||||
await withUnsafeContinuation { continuation in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = "The app could not be updated."
|
|
||||||
alert.informativeText = description
|
|
||||||
alert.addButton(withTitle: "OK")
|
|
||||||
alert.alertStyle = .critical
|
|
||||||
alert.runModal()
|
|
||||||
if shouldExit {
|
|
||||||
exit(0)
|
|
||||||
}
|
|
||||||
continuation.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ReleaseManifest: Codable {
|
|
||||||
let url: String
|
|
||||||
let sha256: String
|
|
||||||
}
|
|
@ -7,8 +7,17 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAppUpdater
|
||||||
|
|
||||||
let app = NSApplication.shared
|
let delegate = SelfUpdater(
|
||||||
let delegate = Updater()
|
appName: "PHP Monitor",
|
||||||
app.delegate = delegate
|
bundleIdentifiers: [
|
||||||
|
"com.nicoverbruggen.phpmon.eap",
|
||||||
|
"com.nicoverbruggen.phpmon.dev",
|
||||||
|
"com.nicoverbruggen.phpmon"
|
||||||
|
],
|
||||||
|
selfUpdaterPath: "~/.config/phpmon/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
NSApplication.shared.delegate = delegate
|
||||||
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "1.000",
|
||||||
|
"green" : "1.000",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.172",
|
||||||
|
"green" : "0.182",
|
||||||
|
"red" : "0.182"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,7 @@
|
|||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.988",
|
"blue" : "0.988",
|
||||||
"green" : "0.723",
|
"green" : "0.444",
|
||||||
"red" : "0.277"
|
"red" : "0.277"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.300",
|
||||||
|
"blue" : "0.180",
|
||||||
|
"green" : "0.841",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.300",
|
||||||
|
"blue" : "0.426",
|
||||||
|
"green" : "0.809",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
25
phpmon/Assets.xcassets/php.imageset/Contents.json
vendored
Normal file
25
phpmon/Assets.xcassets/php.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "php.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true,
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
|
}
|
||||||
|
}
|
1
phpmon/Assets.xcassets/php.imageset/php.svg
vendored
Normal file
1
phpmon/Assets.xcassets/php.imageset/php.svg
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 24 24"><path d="M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zm-2.595-1.382h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -14,14 +14,17 @@ class Actions {
|
|||||||
|
|
||||||
public static func linkPhp() async {
|
public static func linkPhp() async {
|
||||||
await brew("link php --overwrite --force")
|
await brew("link php --overwrite --force")
|
||||||
|
|
||||||
// TODO: Verify that this worked, if not, notify the user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func restartPhpFpm() async {
|
public static func restartPhpFpm() async {
|
||||||
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
|
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func restartPhpFpm(version: String) async {
|
||||||
|
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
|
||||||
|
await brew("services restart \(formula)", sudo: HomebrewFormulae.php.elevated)
|
||||||
|
}
|
||||||
|
|
||||||
public static func restartNginx() async {
|
public static func restartNginx() async {
|
||||||
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
|
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,51 @@ struct Constants {
|
|||||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The PHP versions that are considered pre-release versions.
|
PHP Monitor supplies a hardcoded list of PHP packages in its own
|
||||||
|
PHP Version Manager.
|
||||||
|
|
||||||
|
This hardcoded list will expire and will need to be modified when
|
||||||
|
the cutoff date occurs, which is when the `php` formula will
|
||||||
|
become PHP 8.4, and a new build will need to be made.
|
||||||
|
|
||||||
|
If users launch an older version of the app, then a warning
|
||||||
|
will be displayed to let them know that certain operations
|
||||||
|
will not work correctly and that they need to update their app.
|
||||||
*/
|
*/
|
||||||
static let ExperimentalPhpVersions: Set = [
|
static let PhpFormulaeCutoffDate = "2024-11-30" // YYYY-MM-DD
|
||||||
"8.3", "8.4"
|
|
||||||
|
/**
|
||||||
|
* The PHP versions that are considered pre-release versions.
|
||||||
|
* Past a certain date, an experimental version "graduates"
|
||||||
|
* to a release version and is no longer marked as experimental.
|
||||||
|
*/
|
||||||
|
static var ExperimentalPhpVersions: Set<String> {
|
||||||
|
let releaseDates = [
|
||||||
|
// "8.5": Date.fromString("2025-11-01"), // PLACEHOLDER DATE
|
||||||
|
"8.4": Date.fromString("2024-11-22"),
|
||||||
|
]
|
||||||
|
|
||||||
|
return Set(releaseDates
|
||||||
|
.filter { (_: String, date: Date?) in
|
||||||
|
guard let date else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return date > Date.now
|
||||||
|
}.map { (version: String, _: Date?) in
|
||||||
|
return version
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
The Homebrew services that should be automatically
|
||||||
|
detected and show up in the list of managed services.
|
||||||
|
*/
|
||||||
|
static let DetectedHomebrewServices: Set = [
|
||||||
|
"mailhog",
|
||||||
|
"mysql@",
|
||||||
|
"postgresql@",
|
||||||
|
"redis"
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,9 +73,7 @@ struct Constants {
|
|||||||
static let DetectedPhpVersions: Set = [
|
static let DetectedPhpVersions: Set = [
|
||||||
"5.6",
|
"5.6",
|
||||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3", "8.4"
|
||||||
"8.3",
|
|
||||||
"8.4"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,14 +89,12 @@ struct Constants {
|
|||||||
3: // Valet v3 dropped support for v5.6
|
3: // Valet v3 dropped support for v5.6
|
||||||
[
|
[
|
||||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3", "8.4"
|
||||||
"8.3", "8.4" // dev
|
|
||||||
],
|
],
|
||||||
4: // Valet v4 dropped support for v7.0
|
4: // Valet v4 dropped support for v7.0
|
||||||
[
|
[
|
||||||
"7.1", "7.2", "7.3", "7.4",
|
"7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3", "8.4"
|
||||||
"8.3", "8.4" // dev
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -91,6 +128,8 @@ struct Constants {
|
|||||||
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
|
// EAP URLs
|
||||||
|
|
||||||
static let EarlyAccessCaskFile = URL(
|
static let EarlyAccessCaskFile = URL(
|
||||||
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
||||||
)!
|
)!
|
||||||
|
@ -45,7 +45,6 @@ func grepContains(file: String, query: String) async -> Bool {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
Attempts to introduce sleep for a particular duration. Use with caution.
|
Attempts to introduce sleep for a particular duration. Use with caution.
|
||||||
Only intended for testing purposes.
|
|
||||||
*/
|
*/
|
||||||
func delay(seconds: Double) async {
|
func delay(seconds: Double) async {
|
||||||
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
||||||
|
@ -102,6 +102,14 @@ public class Paths {
|
|||||||
return "\(shared.baseDir.rawValue)/etc"
|
return "\(shared.baseDir.rawValue)/etc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var tapPath: String {
|
||||||
|
if shared.baseDir == .usr {
|
||||||
|
return "\(shared.baseDir.rawValue)/homebrew/Library/Taps"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(shared.baseDir.rawValue)/Library/Taps"
|
||||||
|
}
|
||||||
|
|
||||||
public static var caskroomPath: String {
|
public static var caskroomPath: String {
|
||||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||||
|
@ -8,6 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol AlertableError {
|
public protocol AlertableError {
|
||||||
func getErrorMessageKey() -> String
|
func getErrorMessageKey() -> String
|
||||||
}
|
}
|
||||||
|
@ -15,4 +15,10 @@ extension Date {
|
|||||||
return dateFormatter.string(from: self)
|
return dateFormatter.string(from: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func fromString(_ string: String) -> Date? {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
return dateFormatter.date(from: string)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
23
phpmon/Common/Extensions/NVAlertExtension.swift
Normal file
23
phpmon/Common/Extensions/NVAlertExtension.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// NVAlertExtension.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 16/07/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
|
extension NVAlert {
|
||||||
|
/**
|
||||||
|
Shows the modal for a particular error.
|
||||||
|
*/
|
||||||
|
@MainActor public static func show(for error: Error & AlertableError) {
|
||||||
|
let key = error.getErrorMessageKey()
|
||||||
|
return NVAlert().withInformation(
|
||||||
|
title: "\(key).title".localized,
|
||||||
|
subtitle: "\(key).description".localized
|
||||||
|
).withPrimary(text: "generic.ok".localized).show()
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,18 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct Localization {
|
struct Localization {
|
||||||
|
static var preferredLanguage: String? {
|
||||||
|
guard let language = Preferences.preferences[.languageOverride] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if language.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
|
||||||
static var bundle: Bundle = {
|
static var bundle: Bundle = {
|
||||||
if !isRunningTests {
|
if !isRunningTests {
|
||||||
return Bundle.main
|
return Bundle.main
|
||||||
@ -32,7 +44,15 @@ struct Localization {
|
|||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
var localized: String {
|
var localized: String {
|
||||||
let string = NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
|
var preferredBundle: Bundle = Localization.bundle
|
||||||
|
|
||||||
|
if let preferred = Localization.preferredLanguage,
|
||||||
|
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
|
||||||
|
let bundle = Bundle(path: path) {
|
||||||
|
preferredBundle = bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
let string = NSLocalizedString(self, tableName: nil, bundle: preferredBundle, value: "", comment: "")
|
||||||
|
|
||||||
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
||||||
if string == self {
|
if string == self {
|
||||||
|
@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
|
|||||||
// MARK: — FS Attributes
|
// MARK: — FS Attributes
|
||||||
|
|
||||||
func makeExecutable(_ path: String) throws {
|
func makeExecutable(_ path: String) throws {
|
||||||
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Checks
|
// MARK: - Checks
|
||||||
|
@ -75,7 +75,7 @@ class MenuBarImageGenerator {
|
|||||||
|
|
||||||
// Then we'll fetch the image we want on the left
|
// Then we'll fetch the image we want on the left
|
||||||
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
||||||
if iconType == nil {
|
if iconType == nil || !MenuBarIcon.allCases.map({ $0.rawValue }).contains(iconType) {
|
||||||
Log.warn("Invalid icon type found, using the default")
|
Log.warn("Invalid icon type found, using the default")
|
||||||
iconType = MenuBarIcon.iconPhp.rawValue
|
iconType = MenuBarIcon.iconPhp.rawValue
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
App.shared.remove(window: windowName)
|
App.shared.remove(window: windowName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func windowDidResize(_ notification: Notification) {}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import Foundation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
Run a simple blocking Shell command on the user's own system.
|
Run a simple blocking Shell command on the user's own system.
|
||||||
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
|
|
||||||
*/
|
*/
|
||||||
public func system(_ command: String) -> String {
|
public func system(_ command: String) -> String {
|
||||||
let task = Process()
|
let task = Process()
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// WIP.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 01/11/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
func todo(_ context: String = "") {
|
|
||||||
if !context.isEmpty {
|
|
||||||
fatalError("To be implemented: \(context)")
|
|
||||||
}
|
|
||||||
|
|
||||||
fatalError("To be implemented")
|
|
||||||
}
|
|
@ -62,13 +62,6 @@ class ActivePhpInstallation {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load extension information
|
|
||||||
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
|
||||||
|
|
||||||
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
|
||||||
iniFiles.append(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get configuration values
|
// Get configuration values
|
||||||
limits = Limits(
|
limits = Limits(
|
||||||
memory_limit: getByteCount(key: "memory_limit"),
|
memory_limit: getByteCount(key: "memory_limit"),
|
||||||
@ -76,15 +69,10 @@ class ActivePhpInstallation {
|
|||||||
post_max_size: getByteCount(key: "post_max_size")
|
post_max_size: getByteCount(key: "post_max_size")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Return a list of .ini files parsed after php.ini
|
let paths = ActiveShell.shared
|
||||||
let paths = Command.execute(
|
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
path: Paths.php,
|
.split(separator: "\n")
|
||||||
arguments: ["-r", "echo php_ini_scanned_files();"],
|
.map { String($0) }
|
||||||
trimNewlines: false
|
|
||||||
)
|
|
||||||
.replacingOccurrences(of: "\n", with: "")
|
|
||||||
.split(separator: ",")
|
|
||||||
.map { String($0) }
|
|
||||||
|
|
||||||
// See if any extensions are present in said .ini files
|
// See if any extensions are present in said .ini files
|
||||||
paths.forEach { (iniFilePath) in
|
paths.forEach { (iniFilePath) in
|
||||||
|
@ -13,7 +13,7 @@ class PhpEnvironments {
|
|||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loads the currently active PHP installation upon startup. May be empty.
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
self.currentInstall = ActivePhpInstallation.load()
|
self.currentInstall = ActivePhpInstallation.load()
|
||||||
@ -29,7 +29,7 @@ class PhpEnvironments {
|
|||||||
/**
|
/**
|
||||||
Determine which PHP version the `php` formula is aliased to.
|
Determine which PHP version the `php` formula is aliased to.
|
||||||
*/
|
*/
|
||||||
func determinePhpAlias() async {
|
@MainActor func determinePhpAlias() async {
|
||||||
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
||||||
|
|
||||||
self.homebrewPackage = try! JSONDecoder().decode(
|
self.homebrewPackage = try! JSONDecoder().decode(
|
||||||
@ -37,7 +37,28 @@ class PhpEnvironments {
|
|||||||
from: brewPhpAlias.data(using: .utf8)!
|
from: brewPhpAlias.data(using: .utf8)!
|
||||||
).first!
|
).first!
|
||||||
|
|
||||||
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!")
|
PhpEnvironments.brewPhpAlias = self.homebrewPackage.version
|
||||||
|
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).")
|
||||||
|
|
||||||
|
// Check if that version actually corresponds to an older version
|
||||||
|
let phpConfigExecutablePath = "\(Paths.optPath)/php/bin/php-config"
|
||||||
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
|
let longVersionString = Command.execute(
|
||||||
|
path: phpConfigExecutablePath,
|
||||||
|
arguments: ["--version"],
|
||||||
|
trimNewlines: false
|
||||||
|
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
if let version = try? VersionNumber.parse(longVersionString) {
|
||||||
|
PhpEnvironments.brewPhpAlias = version.short
|
||||||
|
if version.short != homebrewPackage.version {
|
||||||
|
Log.info("[BREW] An older version of `php` is actually installed (\(version.short)).")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not determine the actual version of the php binary; assuming Homebrew is correct.")
|
||||||
|
PhpEnvironments.brewPhpAlias = homebrewPackage.version
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
@ -49,12 +70,10 @@ class PhpEnvironments {
|
|||||||
static let shared = PhpEnvironments()
|
static let shared = PhpEnvironments()
|
||||||
|
|
||||||
/** Whether the switcher is busy performing any actions. */
|
/** Whether the switcher is busy performing any actions. */
|
||||||
var isBusy: Bool = false {
|
@MainActor var isBusy: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
Task { @MainActor in
|
MainMenu.shared.refreshIcon()
|
||||||
MainMenu.shared.setBusyImage()
|
MainMenu.shared.rebuild()
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +87,14 @@ class PhpEnvironments {
|
|||||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||||
|
|
||||||
/** Information about the currently linked PHP installation. */
|
/** Information about the currently linked PHP installation. */
|
||||||
var currentInstall: ActivePhpInstallation?
|
var currentInstall: ActivePhpInstallation? {
|
||||||
|
didSet {
|
||||||
|
// Let the PHP extension manager, if it exists, know the version changed
|
||||||
|
if let version = currentInstall?.version.short {
|
||||||
|
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The version that the `php` formula via Brew is aliased to on the current system.
|
The version that the `php` formula via Brew is aliased to on the current system.
|
||||||
@ -79,7 +105,12 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
As such, we take that information from Homebrew.
|
As such, we take that information from Homebrew.
|
||||||
*/
|
*/
|
||||||
static var brewPhpAlias: String {
|
static var brewPhpAlias: String = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
It's possible for the alias to be newer than the actual installed version of PHP.
|
||||||
|
*/
|
||||||
|
static var homebrewBrewPhpAlias: String {
|
||||||
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
||||||
|
|
||||||
return PhpEnvironments.shared.homebrewPackage.version
|
return PhpEnvironments.shared.homebrewPackage.version
|
||||||
@ -146,7 +177,12 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
// Avoid inserting a duplicate
|
// Avoid inserting a duplicate
|
||||||
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||||
supportedVersions.insert(phpAlias)
|
let phpAliasInstall = PhpInstallation(phpAlias)
|
||||||
|
// Before inserting, ensure that the actual output matches the alias
|
||||||
|
// if that isn't the case, our formula remains out-of-date
|
||||||
|
if !phpAliasInstall.isMissingBinary {
|
||||||
|
supportedVersions.insert(phpAlias)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
availablePhpVersions = Array(supportedVersions)
|
availablePhpVersions = Array(supportedVersions)
|
||||||
|
@ -67,7 +67,7 @@ class PhpExtension {
|
|||||||
|
|
||||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||||
|
|
||||||
self.enabled = !line.contains(";")
|
self.enabled = !line.starts(with: ";")
|
||||||
self.file = file
|
self.file = file
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ class PhpExtension {
|
|||||||
You may need to restart the other services in order for this change to apply.
|
You may need to restart the other services in order for this change to apply.
|
||||||
*/
|
*/
|
||||||
func toggle() async {
|
func toggle() async {
|
||||||
let newLine = enabled
|
let newLine = !line.starts(with: ";")
|
||||||
// DISABLED: Commented out line
|
// DISABLED: Commented out line
|
||||||
? "; \(line)"
|
? "; \(line)"
|
||||||
// ENABLED: Line where the comment delimiter (;) is removed
|
// ENABLED: Line where the comment delimiter (;) is removed
|
||||||
@ -84,14 +84,14 @@ class PhpExtension {
|
|||||||
|
|
||||||
await sed(file: file, original: line, replacement: newLine)
|
await sed(file: file, original: line, replacement: newLine)
|
||||||
|
|
||||||
enabled.toggle()
|
self.enabled = !newLine.starts(with: ";")
|
||||||
|
self.line = newLine
|
||||||
|
|
||||||
if !isRunningTests {
|
if !isRunningTests {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
MainMenu.shared.rebuild()
|
MainMenu.shared.rebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Static Methods
|
// MARK: - Static Methods
|
||||||
|
@ -12,19 +12,46 @@ class PhpInstallation {
|
|||||||
|
|
||||||
var versionNumber: VersionNumber
|
var versionNumber: VersionNumber
|
||||||
|
|
||||||
|
var iniFiles: [PhpConfigurationFile] = []
|
||||||
|
|
||||||
|
var isMissingBinary: Bool = false
|
||||||
|
|
||||||
var isHealthy: Bool = true
|
var isHealthy: Bool = true
|
||||||
|
|
||||||
|
var extensions: [PhpExtension] {
|
||||||
|
return self.iniFiles.flatMap({ $0.extensions })
|
||||||
|
}
|
||||||
|
|
||||||
|
var formulaName: String {
|
||||||
|
let version = self.versionNumber.short
|
||||||
|
|
||||||
|
if version == PhpEnvironments.brewPhpAlias {
|
||||||
|
return "php"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "php@\(self.versionNumber.short)"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
In order to determine details about a PHP installation,
|
In order to determine details about a PHP installation,
|
||||||
we’ll simply run `php-config --version` in the relevant directory.
|
we’ll simply run `php-config --version` in the relevant directory.
|
||||||
*/
|
*/
|
||||||
init(_ version: String) {
|
init(_ version: String) {
|
||||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
|
||||||
|
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||||
|
|
||||||
let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
versionNumber = VersionNumber.make(from: version)!
|
||||||
|
|
||||||
self.versionNumber = VersionNumber.make(from: version)!
|
determineVersion(phpConfigExecutablePath, phpExecutablePath)
|
||||||
|
determineHealth(phpExecutablePath)
|
||||||
|
determineIniFiles(phpExecutablePath)
|
||||||
|
|
||||||
|
// Find all enabled extensions
|
||||||
|
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
|
||||||
|
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpConfigExecutablePath) {
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
let longVersionString = Command.execute(
|
let longVersionString = Command.execute(
|
||||||
path: phpConfigExecutablePath,
|
path: phpConfigExecutablePath,
|
||||||
@ -34,9 +61,15 @@ class PhpInstallation {
|
|||||||
|
|
||||||
// The parser should always work, or the string has to be very unusual.
|
// The parser should always work, or the string has to be very unusual.
|
||||||
// If so, the app SHOULD crash, so that the users report what's up.
|
// If so, the app SHOULD crash, so that the users report what's up.
|
||||||
self.versionNumber = try! VersionNumber.parse(longVersionString)
|
versionNumber = try! VersionNumber.parse(longVersionString)
|
||||||
|
} else {
|
||||||
|
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
|
||||||
|
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
|
||||||
|
isMissingBinary = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineHealth(_ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpExecutablePath) {
|
if FileSystem.fileExists(phpExecutablePath) {
|
||||||
let testCommand = Command.execute(
|
let testCommand = Command.execute(
|
||||||
path: phpExecutablePath,
|
path: phpExecutablePath,
|
||||||
@ -53,4 +86,18 @@ class PhpInstallation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func determineIniFiles(_ phpExecutablePath: String) {
|
||||||
|
let paths = ActiveShell.shared
|
||||||
|
.sync("\(phpExecutablePath) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
|
.split(separator: "\n")
|
||||||
|
.map { String($0) }
|
||||||
|
|
||||||
|
// See if any extensions are present in said .ini files
|
||||||
|
paths.forEach { (iniFilePath) in
|
||||||
|
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
|
||||||
|
iniFiles.append(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ extension InternalSwitcher {
|
|||||||
return corrections.contains(true)
|
return corrections.contains(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - PHP FPM pool
|
// MARK: - Corrections
|
||||||
|
|
||||||
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
||||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||||
@ -54,37 +54,7 @@ extension InternalSwitcher {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||||
return [
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/php-fpm.d/valet-fpm.conf",
|
|
||||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
|
||||||
replacements: [
|
|
||||||
"VALET_USER": Paths.whoami,
|
|
||||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
|
||||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
|
||||||
],
|
|
||||||
applies: { Valet.shared.version!.major > 2 }
|
|
||||||
),
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/conf.d/error_log.ini",
|
|
||||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
|
||||||
replacements: [
|
|
||||||
"VALET_USER": Paths.whoami,
|
|
||||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
|
||||||
],
|
|
||||||
applies: { return true }
|
|
||||||
),
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/conf.d/php-memory-limits.ini",
|
|
||||||
source: "/cli/stubs/php-memory-limits.ini",
|
|
||||||
replacements: [:],
|
|
||||||
applies: { return true }
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
|
||||||
let files = self.getExpectedConfigurationFiles(for: version)
|
let files = self.getExpectedConfigurationFiles(for: version)
|
||||||
|
|
||||||
// For each of the files, attempt to fix anything that is wrong
|
// For each of the files, attempt to fix anything that is wrong
|
||||||
@ -124,6 +94,38 @@ extension InternalSwitcher {
|
|||||||
return outcomes.contains(true)
|
return outcomes.contains(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Internals
|
||||||
|
|
||||||
|
private func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||||
|
return [
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/php-fpm.d/valet-fpm.conf",
|
||||||
|
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||||
|
replacements: [
|
||||||
|
"VALET_USER": Paths.whoami,
|
||||||
|
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||||
|
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||||
|
],
|
||||||
|
applies: { Valet.shared.version!.major > 2 }
|
||||||
|
),
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/conf.d/error_log.ini",
|
||||||
|
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||||
|
replacements: [
|
||||||
|
"VALET_USER": Paths.whoami,
|
||||||
|
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||||
|
],
|
||||||
|
applies: { return true }
|
||||||
|
),
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/conf.d/php-memory-limits.ini",
|
||||||
|
source: "/cli/stubs/php-memory-limits.ini",
|
||||||
|
replacements: [:],
|
||||||
|
applies: { return true }
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ExpectedConfigurationFile {
|
public struct ExpectedConfigurationFile {
|
||||||
|
@ -8,9 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Process: @unchecked Sendable {}
|
|
||||||
extension Timer: @unchecked Sendable {}
|
|
||||||
|
|
||||||
class RealShell: ShellProtocol {
|
class RealShell: ShellProtocol {
|
||||||
/**
|
/**
|
||||||
The launch path of the terminal in question that is used.
|
The launch path of the terminal in question that is used.
|
||||||
@ -86,14 +83,37 @@ class RealShell: ShellProtocol {
|
|||||||
|
|
||||||
// MARK: - Shellable Protocol
|
// MARK: - Shellable Protocol
|
||||||
|
|
||||||
|
func sync(_ command: String) -> ShellOutput {
|
||||||
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
|
let outputPipe = Pipe()
|
||||||
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
|
sleep(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
task.standardOutput = outputPipe
|
||||||
|
task.standardError = errorPipe
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
|
||||||
|
if Log.shared.verbosity == .cli {
|
||||||
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
func pipe(_ command: String) async -> ShellOutput {
|
func pipe(_ command: String) async -> ShellOutput {
|
||||||
let task = getShellProcess(for: command)
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
let outputPipe = Pipe()
|
let outputPipe = Pipe()
|
||||||
let errorPipe = Pipe()
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
// Seriously slow down how long it takes for the shell to return output
|
|
||||||
// (in order to debug or identify async issues)
|
|
||||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
Log.info("[SLOW SHELL] \(command)")
|
Log.info("[SLOW SHELL] \(command)")
|
||||||
await delay(seconds: 3.0)
|
await delay(seconds: 3.0)
|
||||||
@ -104,20 +124,20 @@ class RealShell: ShellProtocol {
|
|||||||
task.launch()
|
task.launch()
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
|
|
||||||
let stdOut = String(
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
let stdErr = String(
|
|
||||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
if Log.shared.verbosity == .cli {
|
if Log.shared.verbosity == .cli {
|
||||||
var args = task.arguments ?? []
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
}
|
||||||
var log = """
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func log(task: Process, stdOut: String, stdErr: String) {
|
||||||
|
var args = task.arguments ?? []
|
||||||
|
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||||
|
var log = """
|
||||||
|
|
||||||
<~~~~~~~~~~~~~~~~~~~~~~~
|
<~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
||||||
@ -126,22 +146,19 @@ class RealShell: ShellProtocol {
|
|||||||
\(stdOut)
|
\(stdOut)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if !stdErr.isEmpty {
|
if !stdErr.isEmpty {
|
||||||
log.append("""
|
log.append("""
|
||||||
[ERR]:
|
[ERR]:
|
||||||
\(stdErr)
|
\(stdErr)
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.append("""
|
log.append("""
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~>
|
~~~~~~~~~~~~~~~~~~~~~~~~>
|
||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
Log.info(log)
|
Log.info(log)
|
||||||
}
|
|
||||||
|
|
||||||
return .out(stdOut, stdErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func quiet(_ command: String) async {
|
func quiet(_ command: String) async {
|
||||||
@ -164,25 +181,26 @@ class RealShell: ShellProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return try await withCheckedThrowingContinuation({ continuation in
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
let timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in
|
let task = Task {
|
||||||
|
try await Task.sleep(nanoseconds: timeout.nanoseconds)
|
||||||
// Only terminate if the process is still running
|
// Only terminate if the process is still running
|
||||||
if process.isRunning {
|
if process.isRunning {
|
||||||
process.terminationHandler = nil
|
process.terminationHandler = nil
|
||||||
process.terminate()
|
process.terminate()
|
||||||
return continuation.resume(throwing: ShellError.timedOut)
|
continuation.resume(throwing: ShellError.timedOut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.terminationHandler = { [timer, output] process in
|
process.terminationHandler = { [output] process in
|
||||||
timer.invalidate()
|
task.cancel()
|
||||||
|
|
||||||
process.haltListening()
|
process.haltListening()
|
||||||
|
|
||||||
if !output.err.isEmpty {
|
if !output.err.isEmpty {
|
||||||
return continuation.resume(returning: (process, .err(output.err)))
|
continuation.resume(returning: (process, .err(output.err)))
|
||||||
|
} else {
|
||||||
|
continuation.resume(returning: (process, .out(output.out)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return continuation.resume(returning: (process, .out(output.out)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.launch()
|
process.launch()
|
||||||
@ -190,3 +208,9 @@ class RealShell: ShellProtocol {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimeInterval {
|
||||||
|
var nanoseconds: UInt64 {
|
||||||
|
return UInt64(self * 1_000_000_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,6 +14,16 @@ protocol ShellProtocol {
|
|||||||
*/
|
*/
|
||||||
var PATH: String { get }
|
var PATH: String { get }
|
||||||
|
|
||||||
|
/**
|
||||||
|
Run a command synchronously. Use with caution.
|
||||||
|
|
||||||
|
Common usage:
|
||||||
|
```
|
||||||
|
let output = Shell.sync("php -v")
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
func sync(_ command: String) -> ShellOutput
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Run a command asynchronously.
|
Run a command asynchronously.
|
||||||
Returns the most relevant output (prefers error output if it exists).
|
Returns the most relevant output (prefers error output if it exists).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// PhpFormulaeStatus.swift
|
// BusyStatus.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 02/05/2023.
|
// Created by Nico Verbruggen on 02/05/2023.
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PhpFormulaeStatus: ObservableObject {
|
class BusyStatus: ObservableObject {
|
||||||
@Published var busy: Bool
|
@Published var busy: Bool
|
||||||
@Published var title: String
|
@Published var title: String
|
||||||
@Published var description: String
|
@Published var description: String
|
||||||
@ -18,4 +18,12 @@ class PhpFormulaeStatus: ObservableObject {
|
|||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func notBusy() -> BusyStatus {
|
||||||
|
return BusyStatus(busy: false, title: "", description: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func busy() -> BusyStatus {
|
||||||
|
return BusyStatus(busy: false, title: "", description: "")
|
||||||
|
}
|
||||||
}
|
}
|
@ -73,12 +73,26 @@ public struct TestableConfiguration: Codable {
|
|||||||
: .fake(.text)
|
: .fake(.text)
|
||||||
]) { (_, new) in new }
|
]) { (_, new) in new }
|
||||||
|
|
||||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"]
|
// PHP configuration files
|
||||||
= version.long
|
self.shellOutput["/opt/homebrew/opt/php@\(version.short)/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||||
|
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||||
|
|
||||||
|
// PHP Homebrew operations
|
||||||
|
self.shellOutput["/opt/homebrew/bin/brew unlink php@\(version.short)"] = .delayed(0.2, "OK")
|
||||||
|
self.shellOutput["sudo /opt/homebrew/bin/brew services stop php@\(version.short)"] = .delayed(0.2, "OK")
|
||||||
|
self.shellOutput["sudo /opt/homebrew/bin/brew services start php@\(version.short)"] = .delayed(0.2, "OK")
|
||||||
|
self.shellOutput["/opt/homebrew/bin/brew link php@\(version.short) --overwrite --force"] = .delayed(0.2, "OK")
|
||||||
|
|
||||||
|
// PHP version output
|
||||||
|
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"] = version.long
|
||||||
|
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php -v"] = "OK"
|
||||||
|
|
||||||
if primary {
|
if primary {
|
||||||
self.shellOutput["ls /opt/homebrew/opt | grep php"]
|
// Files expected to be present for currently linked PHP version
|
||||||
= .instant("php")
|
self.shellOutput["ls /opt/homebrew/opt | grep php"] =
|
||||||
|
.instant("php")
|
||||||
|
self.shellOutput["/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||||
|
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||||
self.filesystem["/opt/homebrew/opt/php"]
|
self.filesystem["/opt/homebrew/opt/php"]
|
||||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
||||||
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
||||||
@ -89,12 +103,8 @@ public struct TestableConfiguration: Codable {
|
|||||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
|
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
|
||||||
self.commandOutput["/opt/homebrew/bin/php-config --version"]
|
self.commandOutput["/opt/homebrew/bin/php-config --version"]
|
||||||
= version.long
|
= version.long
|
||||||
self.commandOutput["/opt/homebrew/bin/php -r echo php_ini_scanned_files();"] =
|
|
||||||
"""
|
|
||||||
/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini,
|
|
||||||
"""
|
|
||||||
} else {
|
} else {
|
||||||
|
// Output expected to be present for non-linked PHP versions
|
||||||
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
||||||
BatchFakeShellOutput.instant(
|
BatchFakeShellOutput.instant(
|
||||||
self.secondaryPhpVersions
|
self.secondaryPhpVersions
|
||||||
@ -103,6 +113,7 @@ public struct TestableConfiguration: Codable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// swiftlint:enable function_body_length
|
||||||
|
|
||||||
// MARK: Interactions
|
// MARK: Interactions
|
||||||
|
|
||||||
|
@ -19,6 +19,17 @@ public class TestableShell: ShellProtocol {
|
|||||||
|
|
||||||
var expectations: [String: BatchFakeShellOutput] = [:]
|
var expectations: [String: BatchFakeShellOutput] = [:]
|
||||||
|
|
||||||
|
func sync(_ command: String) -> ShellOutput {
|
||||||
|
// This assertion will only fire during test builds
|
||||||
|
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
|
||||||
|
|
||||||
|
guard let expectation = expectations[command] else {
|
||||||
|
return .err("No Expected Output")
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectation.syncOutput()
|
||||||
|
}
|
||||||
|
|
||||||
func quiet(_ command: String) async {
|
func quiet(_ command: String) async {
|
||||||
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
||||||
}
|
}
|
||||||
@ -112,6 +123,29 @@ struct BatchFakeShellOutput: Codable {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Outputs the fake shell output as expected, but does this synchronously.
|
||||||
|
*/
|
||||||
|
public func syncOutput(
|
||||||
|
ignoreDelay: Bool = false
|
||||||
|
) -> ShellOutput {
|
||||||
|
let output = ShellOutput.empty()
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
if !ignoreDelay {
|
||||||
|
Thread.sleep(forTimeInterval: item.delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.stream == .stdErr {
|
||||||
|
output.err += item.output
|
||||||
|
} else if item.stream == .stdOut {
|
||||||
|
output.out += item.output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
||||||
*/
|
*/
|
||||||
|
@ -18,8 +18,15 @@
|
|||||||
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
||||||
<p><b>Get the latest on Twitter or Mastodon.</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
<p><b>Get the latest on Twitter or Mastodon.</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
||||||
<p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p>
|
<p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p>
|
||||||
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(Some names have been omitted due to their sponsorships being private. Thank you all!)
|
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(Some names have been omitted due to their sponsorships being private. Thank you all!)</p>
|
||||||
|
<p><b>Localization credits:</b></br>
|
||||||
|
‐ English, Dutch</b> by @nicoverbruggen</br>
|
||||||
|
‐ Vietnamese</b> by @xuandung38</br>
|
||||||
|
‐ German</b> by @dsturm</br>
|
||||||
|
‐ Portuguese</b> by @joseborges</br>
|
||||||
|
‐ French</b> by @nhedger, @tplesnar</br>
|
||||||
|
‐ Chinese</b> by @guanguans</br>
|
||||||
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@ -83,9 +83,15 @@ class App {
|
|||||||
/** The window controller of the PHP version manager window. */
|
/** The window controller of the PHP version manager window. */
|
||||||
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
||||||
|
|
||||||
|
/** The window controller of the PHP extension manager window. */
|
||||||
|
var phpExtensionManagerWindowController: PhpExtensionManagerWindowController?
|
||||||
|
|
||||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||||
var detectedApplications: [Application] = []
|
var detectedApplications: [Application] = []
|
||||||
|
|
||||||
|
/** Favorites storage, which keeps track of favorited domains. */
|
||||||
|
var favorites = Favorites.shared
|
||||||
|
|
||||||
/** The warning manager, responsible for keeping track of warnings. */
|
/** The warning manager, responsible for keeping track of warnings. */
|
||||||
var warnings = WarningManager.shared
|
var warnings = WarningManager.shared
|
||||||
|
|
||||||
|
@ -23,12 +23,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
*/
|
*/
|
||||||
let state: App
|
let state: App
|
||||||
|
|
||||||
/**
|
|
||||||
The MainMenu singleton is responsible for rendering the
|
|
||||||
menu bar item and its menu, as well as its actions.
|
|
||||||
*/
|
|
||||||
let menu: MainMenu
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The paths singleton that determines where Homebrew is installed,
|
The paths singleton that determines where Homebrew is installed,
|
||||||
and where to look for binaries.
|
and where to look for binaries.
|
||||||
@ -96,7 +90,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.state = App.shared
|
self.state = App.shared
|
||||||
self.menu = MainMenu.shared
|
|
||||||
self.paths = Paths.shared
|
self.paths = Paths.shared
|
||||||
self.valet = Valet.shared
|
self.valet = Valet.shared
|
||||||
self.brew = Brew.shared
|
self.brew = Brew.shared
|
||||||
@ -109,6 +102,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
static func initializeTestingProfile(_ path: String) {
|
static func initializeTestingProfile(_ path: String) {
|
||||||
Log.info("The configuration with path `\(path)` is being requested...")
|
Log.info("The configuration with path `\(path)` is being requested...")
|
||||||
|
// Clear for PHP Guard
|
||||||
|
Stats.clearCurrentGlobalPhpVersion()
|
||||||
|
// Load the configuration file
|
||||||
TestableConfiguration.loadFrom(path: path).apply()
|
TestableConfiguration.loadFrom(path: path).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +125,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
setupNotifications()
|
setupNotifications()
|
||||||
|
|
||||||
Task { // Make sure the menu performs its initial checks
|
Task { // Make sure the menu performs its initial checks
|
||||||
await menu.startup()
|
await MainMenu.shared.startup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class AppUpdater {
|
class AppUpdater {
|
||||||
var caskFile: CaskFile!
|
var caskFile: CaskFile!
|
||||||
@ -72,7 +73,7 @@ class AppUpdater {
|
|||||||
: "brew upgrade phpmon"
|
: "brew upgrade phpmon"
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "updater.alerts.newer_version_available.title"
|
title: "updater.alerts.newer_version_available.title"
|
||||||
.localized(latestVersionOnline.humanReadable),
|
.localized(latestVersionOnline.humanReadable),
|
||||||
subtitle: "updater.alerts.newer_version_available.subtitle"
|
subtitle: "updater.alerts.newer_version_available.subtitle"
|
||||||
@ -112,7 +113,7 @@ class AppUpdater {
|
|||||||
|
|
||||||
public func presentNoNewerVersionAvailableAlert() {
|
public func presentNoNewerVersionAvailableAlert() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "updater.alerts.is_latest_version.title".localized,
|
title: "updater.alerts.is_latest_version.title".localized,
|
||||||
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
|
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
|
||||||
description: ""
|
description: ""
|
||||||
@ -124,7 +125,7 @@ class AppUpdater {
|
|||||||
|
|
||||||
public func presentCouldNotRetrieveUpdate() {
|
public func presentCouldNotRetrieveUpdate() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "updater.alerts.cannot_check_for_update.title".localized,
|
title: "updater.alerts.cannot_check_for_update.title".localized,
|
||||||
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
|
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
|
||||||
description: "updater.alerts.cannot_check_for_update.description".localized(
|
description: "updater.alerts.cannot_check_for_update.description".localized(
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22155"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/>
|
||||||
<capability name="Image references" minToolsVersion="12.0"/>
|
<capability name="Image references" minToolsVersion="12.0"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
||||||
@ -63,6 +63,13 @@
|
|||||||
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="bPr-YU-lg4"/>
|
||||||
|
<menuItem title="actions" enabled="NO" id="cAS-FU-WUA" userLabel="actions" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_actions"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</menuItem>
|
||||||
</items>
|
</items>
|
||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
@ -508,10 +515,10 @@
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="-374" y="2267"/>
|
<point key="canvasLocation" x="-374" y="2267"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Better AlertVC-->
|
<!--AlertVC-->
|
||||||
<scene sceneID="y9E-bB-wIG">
|
<scene sceneID="y9E-bB-wIG">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="noticeVC" id="hkw-9V-NxP" customClass="BetterAlertVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="noticeVC" id="hkw-9V-NxP" customClass="NVAlertVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" id="UPH-hV-Naz">
|
<view key="view" id="UPH-hV-Naz">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
|
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
@ -819,14 +826,21 @@ Gw
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
|
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
|
||||||
|
<rect key="frame" x="298" y="150" width="30" height="30"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
|
||||||
|
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
||||||
|
</constraints>
|
||||||
|
</progressIndicator>
|
||||||
<scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
|
<scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
||||||
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w">
|
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="611" height="294"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="281"/>
|
<rect key="frame" x="0.0" y="0.0" width="611" height="266"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<size key="intercellSpacing" width="17" height="0.0"/>
|
<size key="intercellSpacing" width="17" height="0.0"/>
|
||||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
@ -916,6 +930,50 @@ Gw
|
|||||||
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
|
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tableCellView>
|
</tableCellView>
|
||||||
|
<tableCellView identifier="domainListNameCellFavorited" wantsLayer="YES" id="Byb-te-u65" customClass="DomainListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="69" y="54" width="200" height="54"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="aot-FJ-HIk">
|
||||||
|
<rect key="frame" x="33" y="26" width="145" height="16"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="LHu-UF-QlC">
|
||||||
|
<font key="font" metaFont="systemSemibold" size="13"/>
|
||||||
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="GNH-l8-oki">
|
||||||
|
<rect key="frame" x="33" y="12" width="75" height="14"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="LNw-Ju-0Ot">
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="3Wp-DX-An9">
|
||||||
|
<rect key="frame" x="5" y="4" width="20" height="47"/>
|
||||||
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="Q76-fI-lkW">
|
||||||
|
<imageReference key="image" image="star.circle.fill" catalog="system" symbolScale="large"/>
|
||||||
|
</imageCell>
|
||||||
|
<color key="contentTintColor" name="AccentColor"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="3Wp-DX-An9" firstAttribute="leading" secondItem="Byb-te-u65" secondAttribute="leading" constant="5" id="CTd-ON-loK"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="aot-FJ-HIk" secondAttribute="trailing" constant="20" symbolic="YES" id="Csc-Dy-H4K"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="GNH-l8-oki" secondAttribute="trailing" constant="20" symbolic="YES" id="H10-MG-hCG"/>
|
||||||
|
<constraint firstItem="GNH-l8-oki" firstAttribute="leading" secondItem="aot-FJ-HIk" secondAttribute="leading" id="Hk0-x3-RyN"/>
|
||||||
|
<constraint firstItem="3Wp-DX-An9" firstAttribute="top" secondItem="Byb-te-u65" secondAttribute="top" constant="9" id="erH-dR-K7S"/>
|
||||||
|
<constraint firstItem="aot-FJ-HIk" firstAttribute="top" secondItem="Byb-te-u65" secondAttribute="top" constant="12" id="ktI-fg-qaX"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="3Wp-DX-An9" secondAttribute="bottom" constant="9" id="uyc-26-gZb"/>
|
||||||
|
<constraint firstItem="aot-FJ-HIk" firstAttribute="leading" secondItem="Byb-te-u65" secondAttribute="leading" constant="35" id="vXE-jj-lLF"/>
|
||||||
|
<constraint firstItem="GNH-l8-oki" firstAttribute="top" secondItem="aot-FJ-HIk" secondAttribute="bottom" id="wSX-fR-O7a"/>
|
||||||
|
</constraints>
|
||||||
|
<connections>
|
||||||
|
<outlet property="labelPathName" destination="GNH-l8-oki" id="GC1-TA-lIk"/>
|
||||||
|
<outlet property="labelSiteName" destination="aot-FJ-HIk" id="HdZ-Rh-ua6"/>
|
||||||
|
</connections>
|
||||||
|
</tableCellView>
|
||||||
</prototypeCellViews>
|
</prototypeCellViews>
|
||||||
</tableColumn>
|
</tableColumn>
|
||||||
<tableColumn identifier="ENVIRONMENT" width="100" minWidth="100" maxWidth="150" id="hzb-XI-Out">
|
<tableColumn identifier="ENVIRONMENT" width="100" minWidth="100" maxWidth="150" id="hzb-XI-Out">
|
||||||
@ -1073,28 +1131,30 @@ Gw
|
|||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/>
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
|
<scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
|
||||||
<rect key="frame" x="0.0" y="293" width="626" height="16"/>
|
<rect key="frame" x="0.0" y="294" width="611" height="15"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
|
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
|
||||||
<rect key="frame" x="610" y="28" width="16" height="281"/>
|
<rect key="frame" x="611" y="28" width="15" height="266"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
|
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="28"/>
|
<rect key="frame" x="0.0" y="0.0" width="611" height="28"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</tableHeaderView>
|
</tableHeaderView>
|
||||||
</scrollView>
|
</scrollView>
|
||||||
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
|
<customView translatesAutoresizingMaskIntoConstraints="NO" id="wcV-ed-8Bv">
|
||||||
<rect key="frame" x="298" y="150" width="30" height="30"/>
|
<rect key="frame" x="113" y="5" width="400" height="300"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
|
<constraint firstAttribute="width" constant="400" id="HCo-LG-x3N"/>
|
||||||
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
<constraint firstAttribute="height" constant="300" id="Xpi-Rl-xmb"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</progressIndicator>
|
</customView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/>
|
<constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/>
|
||||||
|
<constraint firstItem="wcV-ed-8Bv" firstAttribute="centerX" secondItem="rIZ-4U-bhj" secondAttribute="centerX" id="DPz-kQ-aP0"/>
|
||||||
|
<constraint firstItem="wcV-ed-8Bv" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" id="HCW-zJ-gSY"/>
|
||||||
<constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/>
|
<constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/>
|
<constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/>
|
||||||
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/>
|
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/>
|
||||||
@ -1103,13 +1163,14 @@ Gw
|
|||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<connections>
|
<connections>
|
||||||
|
<outlet property="noResultsView" destination="wcV-ed-8Bv" id="K3s-fb-1aN"/>
|
||||||
<outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/>
|
<outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/>
|
||||||
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
||||||
</connections>
|
</connections>
|
||||||
</viewController>
|
</viewController>
|
||||||
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="323" y="723"/>
|
<point key="canvasLocation" x="323" y="722.5"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Add ProxyVC-->
|
<!--Add ProxyVC-->
|
||||||
<scene sceneID="g8z-pE-RL9">
|
<scene sceneID="g8z-pE-RL9">
|
||||||
@ -1486,6 +1547,10 @@ Gw
|
|||||||
<image name="Lock" width="30" height="30"/>
|
<image name="Lock" width="30" height="30"/>
|
||||||
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
|
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
|
||||||
<image name="plus" catalog="system" width="14" height="13"/>
|
<image name="plus" catalog="system" width="14" height="13"/>
|
||||||
|
<image name="star.circle.fill" catalog="system" width="20" height="20"/>
|
||||||
|
<namedColor name="AccentColor">
|
||||||
|
<color red="0.0" green="0.46000000000000002" blue="0.89000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
<namedColor name="IconColorGreen">
|
<namedColor name="IconColorGreen">
|
||||||
<color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class ValetServicesManager: ServicesManager {
|
class ValetServicesManager: ServicesManager {
|
||||||
override init() {
|
override init() {
|
||||||
@ -131,7 +132,7 @@ class ValetServicesManager: ServicesManager {
|
|||||||
Log.err("The service '\(named)' is now reporting an error.")
|
Log.err("The service '\(named)' is now reporting an error.")
|
||||||
|
|
||||||
guard let errorLogPath = after.error_log_path else {
|
guard let errorLogPath = after.error_log_path else {
|
||||||
return BetterAlert().withInformation(
|
return NVAlert().withInformation(
|
||||||
title: "alert.service_error.title".localized(named),
|
title: "alert.service_error.title".localized(named),
|
||||||
subtitle: "alert.service_error.subtitle.no_error_log".localized(named),
|
subtitle: "alert.service_error.subtitle.no_error_log".localized(named),
|
||||||
description: "alert.service_error.extra".localized
|
description: "alert.service_error.extra".localized
|
||||||
@ -140,7 +141,7 @@ class ValetServicesManager: ServicesManager {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.service_error.title".localized(named),
|
title: "alert.service_error.title".localized(named),
|
||||||
subtitle: "alert.service_error.subtitle.error_log".localized(named),
|
subtitle: "alert.service_error.subtitle.error_log".localized(named),
|
||||||
description: "alert.service_error.extra".localized
|
description: "alert.service_error.extra".localized
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class Startup {
|
class Startup {
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ class Startup {
|
|||||||
*/
|
*/
|
||||||
@MainActor private func showAlert(for check: EnvironmentCheck) {
|
@MainActor private func showAlert(for check: EnvironmentCheck) {
|
||||||
if check.requiresAppRestart {
|
if check.requiresAppRestart {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: check.titleText,
|
title: check.titleText,
|
||||||
subtitle: check.subtitleText,
|
subtitle: check.subtitleText,
|
||||||
@ -66,7 +67,7 @@ class Startup {
|
|||||||
}).show()
|
}).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: check.titleText,
|
title: check.titleText,
|
||||||
subtitle: check.subtitleText,
|
subtitle: check.subtitleText,
|
||||||
@ -142,7 +143,7 @@ class Startup {
|
|||||||
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
||||||
.contains("Library not loaded")
|
.contains("Library not loaded")
|
||||||
},
|
},
|
||||||
name: "`no dyld issue detected",
|
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||||
titleText: "startup.errors.dyld_library.title".localized,
|
titleText: "startup.errors.dyld_library.title".localized,
|
||||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||||
Paths.optPath
|
Paths.optPath
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
@MainActor class ComposerWindow {
|
@MainActor class ComposerWindow {
|
||||||
private var shouldNotify: Bool! = nil
|
private var shouldNotify: Bool! = nil
|
||||||
@ -28,8 +29,6 @@ import Foundation
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
MainMenu.shared.setBusyImage()
|
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
|
|
||||||
window = TerminalProgressWindowController.display(
|
window = TerminalProgressWindowController.display(
|
||||||
title: "alert.composer_progress.title".localized,
|
title: "alert.composer_progress.title".localized,
|
||||||
@ -106,15 +105,12 @@ import Foundation
|
|||||||
|
|
||||||
private func removeBusyStatus() {
|
private func removeBusyStatus() {
|
||||||
PhpEnvironments.shared.isBusy = false
|
PhpEnvironments.shared.isBusy = false
|
||||||
Task { @MainActor in
|
|
||||||
MainMenu.shared.updatePhpVersionInStatusBar()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Alert
|
// MARK: Alert
|
||||||
|
|
||||||
@MainActor private func presentMissingAlert() {
|
private func presentMissingAlert() {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.composer_missing.title".localized,
|
title: "alert.composer_missing.title".localized,
|
||||||
subtitle: "alert.composer_missing.subtitle".localized,
|
subtitle: "alert.composer_missing.subtitle".localized,
|
||||||
|
@ -22,7 +22,7 @@ struct ProjectTypeDetection {
|
|||||||
This list is checked first to see if a project dependency can be mapped to a certain project type.
|
This list is checked first to see if a project dependency can be mapped to a certain project type.
|
||||||
*/
|
*/
|
||||||
public static let SpecificDependencyList = [
|
public static let SpecificDependencyList = [
|
||||||
"roots/bedrock": "Bedrock",
|
"roots/bedrock-autoloader": "Bedrock",
|
||||||
"cakephp/app": "CakePHP",
|
"cakephp/app": "CakePHP",
|
||||||
"craftcms/craft": "Craft",
|
"craftcms/craft": "Craft",
|
||||||
"drupal/core": "Drupal",
|
"drupal/core": "Drupal",
|
||||||
|
@ -8,16 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class BrewFormulaeObservable: ObservableObject {
|
|
||||||
@Published var phpVersions: [BrewFormula] = []
|
|
||||||
|
|
||||||
var upgradeable: [BrewFormula] {
|
|
||||||
return phpVersions.filter { formula in
|
|
||||||
formula.hasUpgrade
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Brew {
|
class Brew {
|
||||||
static let shared = Brew()
|
static let shared = Brew()
|
||||||
|
|
||||||
@ -45,10 +35,11 @@ class Brew {
|
|||||||
|
|
||||||
/// Each formula for each PHP version that can be installed.
|
/// Each formula for each PHP version that can be installed.
|
||||||
public static let phpVersionFormulae = [
|
public static let phpVersionFormulae = [
|
||||||
"8.3": "shivammathur/php/php@8.3",
|
"8.4": "shivammathur/php/php@8.4",
|
||||||
|
"8.3": "php@8.3",
|
||||||
"8.2": "php@8.2",
|
"8.2": "php@8.2",
|
||||||
"8.1": "php@8.1",
|
"8.1": "php@8.1",
|
||||||
"8.0": "php@8.0",
|
"8.0": "shivammathur/php/php@8.0",
|
||||||
"7.4": "shivammathur/php/php@7.4",
|
"7.4": "shivammathur/php/php@7.4",
|
||||||
"7.3": "shivammathur/php/php@7.3",
|
"7.3": "shivammathur/php/php@7.3",
|
||||||
"7.2": "shivammathur/php/php@7.2",
|
"7.2": "shivammathur/php/php@7.2",
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class BrewDiagnostics {
|
class BrewDiagnostics {
|
||||||
/**
|
/**
|
||||||
@ -27,6 +28,21 @@ class BrewDiagnostics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Logs a bunch of useful information during startup.
|
||||||
|
*/
|
||||||
|
public static func logBootInformation() {
|
||||||
|
Log.info(BrewDiagnostics.customCaskInstalled
|
||||||
|
? "[BREW] The app has been installed via Homebrew Cask."
|
||||||
|
: "[BREW] The app has been installed directly (optimal)."
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.info(BrewDiagnostics.usesNginxFullFormula
|
||||||
|
? "[BREW] The app will be using the `nginx-full` formula."
|
||||||
|
: "[BREW] The app will be using the `nginx` formula."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determines whether the PHP Monitor Cask is installed.
|
Determines whether the PHP Monitor Cask is installed.
|
||||||
*/
|
*/
|
||||||
@ -46,6 +62,43 @@ class BrewDiagnostics {
|
|||||||
return destination.contains("/nginx-full/")
|
return destination.contains("/nginx-full/")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
/**
|
||||||
|
It is possible to have outdated symlinks for PHP installations. This can mean that certain PHP installations
|
||||||
|
are going to be reported incorrectly (e.g. `php@8.2` links to an installation in a `8.3` folder after an upgrade).
|
||||||
|
|
||||||
|
To ensure this does not cause issues, PHP Monitor will automatically remove all incorrect PHP symlinks.
|
||||||
|
*/
|
||||||
|
public static func checkForOutdatedPhpInstallationSymlinks() async {
|
||||||
|
// Set up a regular expression
|
||||||
|
let regex = try! NSRegularExpression(pattern: "^php@[0-9]+\\.[0-9]+$", options: .caseInsensitive)
|
||||||
|
|
||||||
|
// Check for incorrect versions
|
||||||
|
if let contents = try? FileSystem.getShallowContentsOfDirectory("\(Paths.optPath)")
|
||||||
|
.filter({
|
||||||
|
let range = NSRange($0.startIndex..., in: $0)
|
||||||
|
return regex.firstMatch(in: $0, options: [], range: range) != nil
|
||||||
|
}) {
|
||||||
|
|
||||||
|
for symlink in contents {
|
||||||
|
let version = symlink.replacingOccurrences(of: "php@", with: "")
|
||||||
|
if let destination = try? FileSystem.getDestinationOfSymlink("\(Paths.optPath)/\(symlink)") {
|
||||||
|
if !destination.contains("Cellar/php/\(version)")
|
||||||
|
&& !destination.contains("Cellar/php@\(version)") {
|
||||||
|
Log.err("Symlink for \(symlink) is incorrect. Removing...")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove("\(Paths.optPath)/\(symlink)")
|
||||||
|
Log.info("Incorrect symlink for \(symlink) has been successfully removed.")
|
||||||
|
} catch {
|
||||||
|
Log.err("Symlink for \(symlink) was incorrect but could not be removed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not read symlink at: \(Paths.optPath)/\(symlink)! Symlink check skipped.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||||
This will then result in two different aliases claiming to point to the same formula (`php`).
|
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||||
@ -131,7 +184,7 @@ class BrewDiagnostics {
|
|||||||
*/
|
*/
|
||||||
private static func presentAlertAboutConflict() {
|
private static func presentAlertAboutConflict() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.php_alias_conflict.title".localized,
|
title: "alert.php_alias_conflict.title".localized,
|
||||||
subtitle: "alert.php_alias_conflict.info".localized
|
subtitle: "alert.php_alias_conflict.info".localized
|
||||||
|
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// BrewPhpExtension.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 27/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct BrewPhpExtension: Hashable, Comparable {
|
||||||
|
let name: String
|
||||||
|
let phpVersion: String
|
||||||
|
let isInstalled: Bool
|
||||||
|
let path: String
|
||||||
|
let dependencies: [String]
|
||||||
|
|
||||||
|
var extensionDependencies: [String] {
|
||||||
|
return dependencies
|
||||||
|
.filter {
|
||||||
|
$0.contains("shivammathur/extensions/") && $0.contains("@\(phpVersion)")
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
$0.replacingOccurrences(of: "shivammathur/extensions/", with: "")
|
||||||
|
.replacingOccurrences(of: "@\(phpVersion)", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var formulaName: String {
|
||||||
|
return "\(name)@\(phpVersion)"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(path: String, name: String, phpVersion: String) {
|
||||||
|
self.path = path
|
||||||
|
self.name = name
|
||||||
|
self.phpVersion = phpVersion
|
||||||
|
|
||||||
|
self.isInstalled = BrewPhpExtension.hasInstallationReceipt(
|
||||||
|
for: "\(name)@\(phpVersion)"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.dependencies = BrewPhpExtension.extractDependencies(from: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAlternativeInstall: Bool {
|
||||||
|
guard let php = PhpEnvironments.shared.cachedPhpInstallations[self.phpVersion] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let alreadyDiscovered = php.extensions.contains(where: { $0.name == self.name })
|
||||||
|
|
||||||
|
return alreadyDiscovered && !isInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func firstDependent(in exts: [BrewPhpExtension]) -> BrewPhpExtension? {
|
||||||
|
return exts
|
||||||
|
.filter({ $0.isInstalled })
|
||||||
|
.first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasInstallationReceipt(for formulaName: String) -> Bool {
|
||||||
|
return FileSystem.fileExists("\(Paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name < rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name == rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractDependencies(from path: String) -> [String] {
|
||||||
|
let regexPattern = #"depends_on "(.*)""#
|
||||||
|
var dependencies: [String] = []
|
||||||
|
|
||||||
|
guard let content = try? FileSystem.getStringFromFile(path) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let regex = try NSRegularExpression(pattern: regexPattern, options: [])
|
||||||
|
let range = NSRange(content.startIndex..<content.endIndex, in: content)
|
||||||
|
let matches = regex.matches(in: content, options: [], range: range)
|
||||||
|
|
||||||
|
for match in matches {
|
||||||
|
if let range = Range(match.range(at: 1), in: content) {
|
||||||
|
let dependencyName = String(content[range])
|
||||||
|
dependencies.append(dependencyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// BrewFormula.swift
|
// BrewPhpFormula.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 17/03/2023.
|
// Created by Nico Verbruggen on 17/03/2023.
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct BrewFormula {
|
struct BrewPhpFormula: Equatable {
|
||||||
/// Name of the formula.
|
/// Name of the formula.
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
@ -21,6 +21,8 @@ struct BrewFormula {
|
|||||||
/// The upgrade that is currently available, if it exists.
|
/// The upgrade that is currently available, if it exists.
|
||||||
let upgradeVersion: String?
|
let upgradeVersion: String?
|
||||||
|
|
||||||
|
// TODO: A rebuild attribute could be checked, to check if a Tap update exists for a pre-release version
|
||||||
|
|
||||||
/// Whether this formula is a stable version of PHP.
|
/// Whether this formula is a stable version of PHP.
|
||||||
let prerelease: Bool
|
let prerelease: Bool
|
||||||
|
|
||||||
@ -48,6 +50,25 @@ struct BrewFormula {
|
|||||||
return upgradeVersion != nil
|
return upgradeVersion != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this formula alias is different.
|
||||||
|
var hasUpgradedFormulaAlias: Bool {
|
||||||
|
return self.shortVersion == PhpEnvironments.homebrewBrewPhpAlias
|
||||||
|
&& PhpEnvironments.homebrewBrewPhpAlias != PhpEnvironments.brewPhpAlias
|
||||||
|
}
|
||||||
|
|
||||||
|
var unavailableAfterUpgrade: Bool {
|
||||||
|
if installedVersion == nil || upgradeVersion == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let installed = try? VersionNumber.parse(self.installedVersion!),
|
||||||
|
let upgrade = try? VersionNumber.parse(self.upgradeVersion!) {
|
||||||
|
return upgrade.short != installed.short
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/// The associated Homebrew folder with this PHP formula.
|
/// The associated Homebrew folder with this PHP formula.
|
||||||
var homebrewFolder: String {
|
var homebrewFolder: String {
|
||||||
let resolved = name
|
let resolved = name
|
||||||
@ -60,7 +81,7 @@ struct BrewFormula {
|
|||||||
/// The short version associated with this formula, if installed.
|
/// The short version associated with this formula, if installed.
|
||||||
var shortVersion: String? {
|
var shortVersion: String? {
|
||||||
guard let version = self.installedVersion else {
|
guard let version = self.installedVersion else {
|
||||||
return nil
|
return self.displayName.replacingOccurrences(of: "PHP ", with: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return VersionNumber.make(from: version)?.short ?? nil
|
return VersionNumber.make(from: version)?.short ?? nil
|
||||||
@ -81,6 +102,7 @@ struct BrewFormula {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?.isHealthy ?? nil
|
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?
|
||||||
|
.isHealthy ?? nil
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,22 +8,23 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol HandlesBrewFormulae {
|
protocol HandlesBrewPhpFormulae {
|
||||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula]
|
func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula]
|
||||||
func refreshPhpVersions(loadOutdated: Bool) async
|
func refreshPhpVersions(loadOutdated: Bool) async
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HandlesBrewFormulae {
|
extension HandlesBrewPhpFormulae {
|
||||||
public func refreshPhpVersions(loadOutdated: Bool) async {
|
public func refreshPhpVersions(loadOutdated: Bool) async {
|
||||||
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
Brew.shared.formulae.phpVersions = items
|
Brew.shared.formulae.phpVersions = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BrewFormulaeHandler: HandlesBrewFormulae {
|
class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] {
|
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
|
||||||
var outdated: [OutdatedFormula]?
|
var outdated: [OutdatedFormula]?
|
||||||
|
|
||||||
if loadOutdated {
|
if loadOutdated {
|
||||||
@ -43,17 +44,17 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Brew.phpVersionFormulae.map { (version, formula) in
|
return Brew.phpVersionFormulae.map { (version, formula) in
|
||||||
let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?.versionNumber.text
|
var fullVersion: String?
|
||||||
|
|
||||||
var upgradeVersion: String?
|
var upgradeVersion: String?
|
||||||
|
|
||||||
if let version = fullVersion {
|
if let install = PhpEnvironments.shared.cachedPhpInstallations[version] {
|
||||||
|
fullVersion = install.versionNumber.text
|
||||||
upgradeVersion = outdated?.first(where: { formula in
|
upgradeVersion = outdated?.first(where: { formula in
|
||||||
return formula.installed_versions.contains(version)
|
return formula.name == install.formulaName
|
||||||
})?.current_version
|
})?.current_version
|
||||||
}
|
}
|
||||||
|
|
||||||
return BrewFormula(
|
return BrewPhpFormula(
|
||||||
name: formula,
|
name: formula,
|
||||||
displayName: "PHP \(version)",
|
displayName: "PHP \(version)",
|
||||||
installedVersion: fullVersion,
|
installedVersion: fullVersion,
|
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// BrewTapFormulae.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 01/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class BrewTapFormulae {
|
||||||
|
public static func from(tap: String) -> [String: [BrewPhpExtension]] {
|
||||||
|
let directory = "\(Paths.tapPath)/\(tap)/Formula"
|
||||||
|
|
||||||
|
let files = try? FileSystem.getShallowContentsOfDirectory(directory)
|
||||||
|
|
||||||
|
var availableExtensions = [String: [BrewPhpExtension]]()
|
||||||
|
|
||||||
|
guard let files = files else {
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
|
||||||
|
let regex = try! NSRegularExpression(pattern: "(\\w+)@(\\d+\\.\\d+)\\.rb")
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
let matches = regex.matches(in: file, range: NSRange(file.startIndex..., in: file))
|
||||||
|
if let match = matches.first {
|
||||||
|
if let phpExtensionRange = Range(match.range(at: 1), in: file),
|
||||||
|
let versionRange = Range(match.range(at: 2), in: file) {
|
||||||
|
// Determine what the extension's name is
|
||||||
|
let phpExtensionName = String(file[phpExtensionRange])
|
||||||
|
|
||||||
|
// Determine what PHP version this is for
|
||||||
|
let phpVersion = String(file[versionRange])
|
||||||
|
|
||||||
|
// Create a new BrewPhpExtension object (determines if installed)
|
||||||
|
let phpExtension = BrewPhpExtension(
|
||||||
|
path: "\(Paths.tapPath)/\(tap)/Formula/\(file)",
|
||||||
|
name: phpExtensionName,
|
||||||
|
phpVersion: phpVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
// Append the extension to the list
|
||||||
|
var extensions = availableExtensions[phpVersion, default: []]
|
||||||
|
extensions.append(phpExtension)
|
||||||
|
availableExtensions[phpVersion] = extensions.sorted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,8 @@ import Foundation
|
|||||||
|
|
||||||
protocol BrewCommand {
|
protocol BrewCommand {
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
||||||
|
|
||||||
|
func getCommandTitle() -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BrewCommand {
|
extension BrewCommand {
|
||||||
@ -31,6 +33,44 @@ extension BrewCommand {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
var loggedMessages: [String] = []
|
||||||
|
|
||||||
|
let (process, _) = try! await Shell.attach(
|
||||||
|
command,
|
||||||
|
didReceiveOutput: { text, _ in
|
||||||
|
if !text.isEmpty {
|
||||||
|
Log.perf(text)
|
||||||
|
loggedMessages.append(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (number, text) = self.reportInstallationProgress(text) {
|
||||||
|
onProgress(.create(value: number, title: getCommandTitle(), description: text))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withTimeout: .minutes(15)
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.terminationStatus <= 0 {
|
||||||
|
loggedMessages = []
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/php") {
|
||||||
|
let command = "brew tap shivammathur/php"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") {
|
||||||
|
let command = "brew tap shivammathur/extensions"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BrewCommandProgress {
|
struct BrewCommandProgress {
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// InstallPhpExtensionCommand.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 21/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class InstallPhpExtensionCommand: BrewCommand {
|
||||||
|
let installing: [BrewPhpExtension]
|
||||||
|
|
||||||
|
func getExtensionNames() -> String {
|
||||||
|
return installing.map { $0.name }.joined(separator: ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "phpman.steps.installing".localized(getExtensionNames())
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(install extensions: [BrewPhpExtension]) {
|
||||||
|
self.installing = extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
let progressTitle = "phpman.steps.wait".localized
|
||||||
|
|
||||||
|
onProgress(.create(
|
||||||
|
value: 0.2,
|
||||||
|
title: progressTitle,
|
||||||
|
description: "phpman.steps.preparing".localized
|
||||||
|
))
|
||||||
|
|
||||||
|
// Make sure the tap is installed
|
||||||
|
try await self.checkPhpTap(onProgress)
|
||||||
|
|
||||||
|
// Make sure that the extension(s) are installed
|
||||||
|
try await self.installPackages(onProgress)
|
||||||
|
|
||||||
|
// Finally, complete all operations
|
||||||
|
await self.completedOperations(onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installPackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
// If no installations are needed, early exit
|
||||||
|
if self.installing.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) install \(self.installing.map { $0.formulaName }.joined(separator: " ")) --force
|
||||||
|
"""
|
||||||
|
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||||
|
// Reload and restart PHP versions
|
||||||
|
onProgress(.create(value: 0.95, title: self.getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
// Restart PHP-FPM
|
||||||
|
if let installed = self.installing.first {
|
||||||
|
await Actions.restartPhpFpm(version: installed.phpVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check which version of PHP are now installed
|
||||||
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
|
// Keep track of the currently installed version
|
||||||
|
await MainMenu.shared.refreshActiveInstallation()
|
||||||
|
|
||||||
|
// Also rebuild the content of the main menu
|
||||||
|
await MainMenu.shared.rebuild()
|
||||||
|
|
||||||
|
// Let the UI know that the installation has been completed
|
||||||
|
onProgress(.create(
|
||||||
|
value: 1,
|
||||||
|
title: "phpman.steps.completed".localized,
|
||||||
|
description: "phpman.steps.success".localized
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// RemovePhpExtensionCommand.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 21/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class RemovePhpExtensionCommand: BrewCommand {
|
||||||
|
public let phpExtension: BrewPhpExtension
|
||||||
|
|
||||||
|
public init(remove formula: BrewPhpExtension) {
|
||||||
|
self.phpExtension = formula
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "phpman.steps.removing".localized(phpExtension.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
onProgress(.create(
|
||||||
|
value: 0.2,
|
||||||
|
title: getCommandTitle(),
|
||||||
|
description: "phpman.steps.removing".localized("`\(phpExtension.name)`...")
|
||||||
|
))
|
||||||
|
|
||||||
|
// Keep track of the file that contains the information about the extension
|
||||||
|
let existing = PhpEnvironments.shared
|
||||||
|
.cachedPhpInstallations[phpExtension.phpVersion]?
|
||||||
|
.extensions.first(where: { ext in
|
||||||
|
ext.name == phpExtension.name
|
||||||
|
})
|
||||||
|
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) remove \(phpExtension.formulaName) --force --ignore-dependencies
|
||||||
|
"""
|
||||||
|
|
||||||
|
var loggedMessages: [String] = []
|
||||||
|
|
||||||
|
let (process, _) = try! await Shell.attach(
|
||||||
|
command,
|
||||||
|
didReceiveOutput: { text, _ in
|
||||||
|
if !text.isEmpty {
|
||||||
|
Log.perf(text)
|
||||||
|
loggedMessages.append(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withTimeout: .minutes(5)
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.terminationStatus <= 0 {
|
||||||
|
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
if let ext = existing {
|
||||||
|
await performExtensionCleanup(for: ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
|
await Actions.restartPhpFpm(version: phpExtension.phpVersion)
|
||||||
|
|
||||||
|
await MainMenu.shared.refreshActiveInstallation()
|
||||||
|
|
||||||
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "phpman.steps.failure".localized, log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performExtensionCleanup(for ext: PhpExtension) async {
|
||||||
|
if ext.file.hasSuffix("20-\(ext.name).ini") {
|
||||||
|
// The extension's default configuration file can be removed
|
||||||
|
Log.info("The extension was found in a default extension .ini location. Purging that .ini file.")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove(ext.file)
|
||||||
|
} catch {
|
||||||
|
Log.err("The file `\(ext.file)` could not be removed.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The extension's default configuration file cannot be removed, it should be disabled instead
|
||||||
|
Log.info("The extension was not found in a default location. Disabling the extension only.")
|
||||||
|
if ext.enabled {
|
||||||
|
await ext.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,23 +8,33 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class InstallAndUpgradeCommand: BrewCommand {
|
class ModifyPhpVersionCommand: BrewCommand {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let installing: [BrewFormula]
|
let installing: [BrewPhpFormula]
|
||||||
let upgrading: [BrewFormula]
|
let upgrading: [BrewPhpFormula]
|
||||||
let phpGuard: PhpGuard
|
let phpGuard: PhpGuard
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
||||||
The process will be executed in two steps: first upgrades, then installations.
|
The process will be executed in two steps: first upgrades, then installations.
|
||||||
|
|
||||||
Upgrades come first because... well, otherwise installations may very well break.
|
Upgrades come first because... well, otherwise installations may very well break.
|
||||||
Each version that is installed will need to be checked afterwards (if it is OK).
|
Each version that is installed will need to be checked afterwards. Installing a
|
||||||
|
newer formula may break other PHP installations, which in turn need to be fixed.
|
||||||
|
|
||||||
|
- Important: If any PHP formula is a major upgrade that causes a PHP "version" to be
|
||||||
|
uninstalled, this is remedied by running `upgradeMainPhpFormula()`. This process
|
||||||
|
will ensure that the upgrade is applied, but the also that old version is
|
||||||
|
re-installed and linked again.
|
||||||
*/
|
*/
|
||||||
public init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
upgrading: [BrewFormula],
|
upgrading: [BrewPhpFormula],
|
||||||
installing: [BrewFormula]
|
installing: [BrewPhpFormula]
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.installing = installing
|
self.installing = installing
|
||||||
@ -33,17 +43,32 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
let progressTitle = "Please wait..."
|
let progressTitle = "phpman.steps.wait".localized
|
||||||
|
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: progressTitle,
|
||||||
description: "PHP Monitor is preparing Homebrew..."
|
description: "phpman.steps.preparing".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
// Try to run all upgrade and installation operations
|
// Determine if a formula will become unavailable
|
||||||
try await self.upgradePackages(onProgress)
|
// This is the case when `php` will be bumped to a new version
|
||||||
try await self.installPackages(onProgress)
|
let unavailable = upgrading.first(where: { formula in
|
||||||
|
formula.unavailableAfterUpgrade
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make sure the tap is installed
|
||||||
|
try await self.checkPhpTap(onProgress)
|
||||||
|
|
||||||
|
if unavailable == nil {
|
||||||
|
// Try to run all upgrade and installation operations
|
||||||
|
try await self.upgradePackages(onProgress)
|
||||||
|
try await self.installPackages(onProgress)
|
||||||
|
} else {
|
||||||
|
// Simply upgrade `php` to the latest version
|
||||||
|
try await self.upgradeMainPhpFormula(unavailable!, onProgress)
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
}
|
||||||
|
|
||||||
// Re-check the installed versions
|
// Re-check the installed versions
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@ -55,6 +80,27 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
await self.completedOperations(onProgress)
|
await self.completedOperations(onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func upgradeMainPhpFormula(
|
||||||
|
_ unavailable: BrewPhpFormula,
|
||||||
|
_ onProgress: @escaping (BrewCommandProgress) -> Void
|
||||||
|
) async throws {
|
||||||
|
// Determine which version was previously available (that will become unavailable)
|
||||||
|
guard let short = try? VersionNumber
|
||||||
|
.parse(unavailable.installedVersion!).short else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade the main formula
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) upgrade php;
|
||||||
|
\(Paths.brew) install php@\(short);
|
||||||
|
"""
|
||||||
|
|
||||||
|
// Run the upgrade command
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
// If no upgrades are needed, early exit
|
// If no upgrades are needed, early exit
|
||||||
if self.upgrading.isEmpty {
|
if self.upgrading.isEmpty {
|
||||||
@ -117,35 +163,12 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
try await run(command, onProgress)
|
try await run(command, onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
|
||||||
var loggedMessages: [String] = []
|
|
||||||
|
|
||||||
let (process, _) = try! await Shell.attach(
|
|
||||||
command,
|
|
||||||
didReceiveOutput: { text, _ in
|
|
||||||
if !text.isEmpty {
|
|
||||||
Log.perf(text)
|
|
||||||
loggedMessages.append(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (number, text) = self.reportInstallationProgress(text) {
|
|
||||||
onProgress(.create(value: number, title: self.title, description: text))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
withTimeout: .minutes(15)
|
|
||||||
)
|
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
|
||||||
loggedMessages = []
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||||
// Reload and restart PHP versions
|
// Reload and restart PHP versions
|
||||||
onProgress(.create(value: 0.95, title: self.title, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: self.title, description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
// Ensure all symlinks are correctly linked
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Check which version of PHP are now installed
|
// Check which version of PHP are now installed
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@ -164,9 +187,8 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
// Let the UI know that the installation has been completed
|
// Let the UI know that the installation has been completed
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 1,
|
value: 1,
|
||||||
title: "Operation completed!",
|
title: "phpman.steps.completed".localized,
|
||||||
description: "The installation has succeeded."
|
description: "phpman.steps.success".localized
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -21,13 +21,15 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
self.phpGuard = PhpGuard()
|
self.phpGuard = PhpGuard()
|
||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func getCommandTitle() -> String {
|
||||||
let progressTitle = "Removing PHP \(version)..."
|
return "phpman.steps.removing".localized("PHP \(version)...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: getCommandTitle(),
|
||||||
description: "Please wait while Homebrew removes PHP \(version)..."
|
description: "phpman.steps.wait".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
let command = """
|
let command = """
|
||||||
@ -56,7 +58,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
if process.terminationStatus <= 0 {
|
||||||
onProgress(.create(value: 0.95, title: progressTitle, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
@ -66,7 +68,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(.create(value: 1, title: progressTitle, description: "The operation has succeeded."))
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
} else {
|
} else {
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
}
|
}
|
@ -9,6 +9,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class FakeCommand: BrewCommand {
|
class FakeCommand: BrewCommand {
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "Hello"
|
||||||
|
}
|
||||||
|
|
||||||
let version: String
|
let version: String
|
||||||
|
|
||||||
init(version: String) {
|
init(version: String) {
|
||||||
|
@ -24,4 +24,6 @@ protocol ValetListable {
|
|||||||
|
|
||||||
func getListableUrl() -> URL?
|
func getListableUrl() -> URL?
|
||||||
|
|
||||||
|
func getListableFavorited() -> Bool
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,11 @@ class ValetProxy: ValetListable {
|
|||||||
var target: String
|
var target: String
|
||||||
var secured: Bool = false
|
var secured: Bool = false
|
||||||
|
|
||||||
|
var favorited: Bool = false
|
||||||
|
var favoriteSignature: String {
|
||||||
|
"proxy:domain:\(domain).\(tld)|target:\(target)"
|
||||||
|
}
|
||||||
|
|
||||||
init(domain: String, target: String, secure: Bool, tld: String) {
|
init(domain: String, target: String, secure: Bool, tld: String) {
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
self.tld = tld
|
self.tld = tld
|
||||||
@ -28,6 +33,8 @@ class ValetProxy: ValetListable {
|
|||||||
secure: false,
|
secure: false,
|
||||||
tld: configuration.tld
|
tld: configuration.tld
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.favorited = Favorites.shared.contains(domain: self.domain)
|
||||||
self.determineSecured()
|
self.determineSecured()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,12 +68,21 @@ class ValetProxy: ValetListable {
|
|||||||
return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)")
|
return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getListableFavorited() -> Bool {
|
||||||
|
return self.favorited
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interactions
|
// MARK: - Interactions
|
||||||
|
|
||||||
func determineSecured() {
|
func determineSecured() {
|
||||||
self.secured = FileSystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
|
self.secured = FileSystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toggleFavorite() {
|
||||||
|
self.favorited.toggle()
|
||||||
|
Favorites.shared.toggle(domain: self.favoriteSignature)
|
||||||
|
}
|
||||||
|
|
||||||
func toggleSecure() async throws {
|
func toggleSecure() async throws {
|
||||||
try await ValetInteractor.shared.toggleSecure(proxy: self)
|
try await ValetInteractor.shared.toggleSecure(proxy: self)
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,11 @@ class ValetSite: ValetListable {
|
|||||||
?? "???"
|
?? "???"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var favorited: Bool = false
|
||||||
|
var favoriteSignature: String {
|
||||||
|
"site:domain:\(name).\(tld)|path:\(absolutePath)"
|
||||||
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
name: String,
|
name: String,
|
||||||
tld: String,
|
tld: String,
|
||||||
@ -75,6 +80,7 @@ class ValetSite: ValetListable {
|
|||||||
self.secured = false
|
self.secured = false
|
||||||
|
|
||||||
if makeDeterminations {
|
if makeDeterminations {
|
||||||
|
self.favorited = Favorites.shared.contains(domain: favoriteSignature)
|
||||||
determineSecured()
|
determineSecured()
|
||||||
determineIsolated()
|
determineIsolated()
|
||||||
determineComposerPhpVersion()
|
determineComposerPhpVersion()
|
||||||
@ -305,12 +311,21 @@ class ValetSite: ValetListable {
|
|||||||
return URL(string: "\(self.secured ? "https://" : "http://")\(self.name).\(Valet.shared.config.tld)")
|
return URL(string: "\(self.secured ? "https://" : "http://")\(self.name).\(Valet.shared.config.tld)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getListableFavorited() -> Bool {
|
||||||
|
return self.favorited
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interactions
|
// MARK: - Interactions
|
||||||
|
|
||||||
func toggleSecure() async throws {
|
func toggleSecure() async throws {
|
||||||
try await ValetInteractor.shared.toggleSecure(site: self)
|
try await ValetInteractor.shared.toggleSecure(site: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toggleFavorite() {
|
||||||
|
self.favorited.toggle()
|
||||||
|
Favorites.shared.toggle(domain: self.favoriteSignature)
|
||||||
|
}
|
||||||
|
|
||||||
func isolate(version: String) async throws {
|
func isolate(version: String) async throws {
|
||||||
try await ValetInteractor.shared.isolate(site: self, version: version)
|
try await ValetInteractor.shared.isolate(site: self, version: version)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension Valet {
|
extension Valet {
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ extension Valet {
|
|||||||
public func notifyAboutUnsupportedTLD() {
|
public func notifyAboutUnsupportedTLD() {
|
||||||
if Valet.shared.config.tld != "test" && Preferences.isEnabled(.warnAboutNonStandardTLD) {
|
if Valet.shared.config.tld != "test" && Preferences.isEnabled(.warnAboutNonStandardTLD) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.warnings.tld_issue.title".localized,
|
title: "alert.warnings.tld_issue.title".localized,
|
||||||
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
||||||
description: "alert.warnings.tld_issue.description".localized
|
description: "alert.warnings.tld_issue.description".localized
|
||||||
@ -33,7 +34,7 @@ extension Valet {
|
|||||||
|
|
||||||
public func notifyAboutOutdatedValetVersion(_ version: VersionNumber) {
|
public func notifyAboutOutdatedValetVersion(_ version: VersionNumber) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.min_valet_version.title".localized,
|
title: "alert.min_valet_version.title".localized,
|
||||||
subtitle: "alert.min_valet_version.info".localized(
|
subtitle: "alert.min_valet_version.info".localized(
|
||||||
@ -60,7 +61,7 @@ extension Valet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.php_fpm_broken.title".localized,
|
title: "alert.php_fpm_broken.title".localized,
|
||||||
subtitle: "alert.php_fpm_broken.info".localized,
|
subtitle: "alert.php_fpm_broken.info".localized,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ extension MainMenu {
|
|||||||
|
|
||||||
@MainActor @objc func displayUnlinkedInfo() {
|
@MainActor @objc func displayUnlinkedInfo() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "phpman.unlinked.title".localized,
|
title: "phpman.unlinked.title".localized,
|
||||||
subtitle: "phpman.unlinked.desc".localized,
|
subtitle: "phpman.unlinked.desc".localized,
|
||||||
@ -32,7 +33,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor @objc func fixHomebrewPermissions() {
|
@MainActor @objc func fixHomebrewPermissions() {
|
||||||
if !BetterAlert()
|
if !NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_homebrew_permissions.title".localized,
|
title: "alert.fix_homebrew_permissions.title".localized,
|
||||||
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
|
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
|
||||||
@ -47,7 +48,7 @@ extension MainMenu {
|
|||||||
asyncExecution {
|
asyncExecution {
|
||||||
try Actions.fixHomebrewPermissions()
|
try Actions.fixHomebrewPermissions()
|
||||||
} success: {
|
} success: {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_homebrew_permissions_done.title".localized,
|
title: "alert.fix_homebrew_permissions_done.title".localized,
|
||||||
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
|
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
|
||||||
@ -56,7 +57,7 @@ extension MainMenu {
|
|||||||
.withPrimary(text: "generic.ok".localized)
|
.withPrimary(text: "generic.ok".localized)
|
||||||
.show()
|
.show()
|
||||||
} failure: { error in
|
} failure: { error in
|
||||||
BetterAlert.show(for: error as! HomebrewPermissionError)
|
NVAlert.show(for: error as! HomebrewPermissionError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +176,7 @@ extension MainMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.revert_description.title".localized,
|
title: "alert.revert_description.title".localized,
|
||||||
subtitle: "alert.revert_description.subtitle".localized(
|
subtitle: "alert.revert_description.subtitle".localized(
|
||||||
preset.textDescription
|
preset.textDescription
|
||||||
@ -196,7 +197,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor @objc func showPresetHelp() {
|
@MainActor @objc func showPresetHelp() {
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "preset_help_title".localized,
|
title: "preset_help_title".localized,
|
||||||
subtitle: "preset_help_info".localized,
|
subtitle: "preset_help_info".localized,
|
||||||
description: "preset_help_desc".localized
|
description: "preset_help_desc".localized
|
||||||
@ -263,7 +264,7 @@ extension MainMenu {
|
|||||||
Task { MainMenu.shared.switchToPhpVersion(version) }
|
Task { MainMenu.shared.switchToPhpVersion(version) }
|
||||||
} else {
|
} else {
|
||||||
Task {
|
Task {
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.php_switch_unavailable.title".localized,
|
title: "alert.php_switch_unavailable.title".localized,
|
||||||
subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
|
subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
|
||||||
).withPrimary(
|
).withPrimary(
|
||||||
@ -283,12 +284,11 @@ extension MainMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@ -298,13 +298,12 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func switchToPhpVersion(_ version: String) {
|
@objc func switchToPhpVersion(_ version: String) {
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@ -325,13 +324,12 @@ extension MainMenu {
|
|||||||
*/
|
*/
|
||||||
func switchToPhp(_ version: String) async {
|
func switchToPhp(_ version: String) async {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
|
@ -45,21 +45,16 @@ extension MainMenu {
|
|||||||
.broadcastServicesUpdate
|
.broadcastServicesUpdate
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
if behaviours.contains(.reloadsPhpInstallation) {
|
if behaviours.contains(.reloadsPhpInstallation) || behaviours.contains(.setsBusyUI) {
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.setsBusyUI) {
|
|
||||||
setBusyImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
var error: Error?
|
var error: Error?
|
||||||
|
|
||||||
do { try execute() } catch let e { error = e }
|
do { try execute() } catch let e {
|
||||||
|
error = e
|
||||||
if behaviours.contains(.setsBusyUI) {
|
Log.err(e)
|
||||||
PhpEnvironments.shared.isBusy = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor [self, error] in
|
Task { @MainActor [self, error] in
|
||||||
@ -68,15 +63,18 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.updatesMenuBarContents) {
|
if behaviours.contains(.updatesMenuBarContents) {
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
} else if behaviours.contains(.setsBusyUI) {
|
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.broadcastServicesUpdate) {
|
if behaviours.contains(.broadcastServicesUpdate) {
|
||||||
Task { await ServicesManager.shared.reloadServicesStatus() }
|
Task { await ServicesManager.shared.reloadServicesStatus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if behaviours.contains(.setsBusyUI) {
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
if error != nil {
|
if error != nil {
|
||||||
return failure(error!)
|
return failure(error!)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ extension MainMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !BetterAlert()
|
if !NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_my_valet.title".localized,
|
title: "alert.fix_my_valet.title".localized,
|
||||||
subtitle: "alert.fix_my_valet.info".localized(PhpEnvironments.brewPhpAlias)
|
subtitle: "alert.fix_my_valet.info".localized(PhpEnvironments.brewPhpAlias)
|
||||||
@ -43,7 +44,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func presentAlertForMissingFormula() {
|
@MainActor private func presentAlertForMissingFormula() {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.php_formula_missing.title".localized,
|
title: "alert.php_formula_missing.title".localized,
|
||||||
subtitle: "alert.php_formula_missing.info".localized
|
subtitle: "alert.php_formula_missing.info".localized
|
||||||
@ -53,7 +54,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func presentAlertForSameVersion() {
|
@MainActor private func presentAlertForSameVersion() {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_my_valet_done.title".localized,
|
title: "alert.fix_my_valet_done.title".localized,
|
||||||
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
||||||
@ -64,7 +65,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func presentAlertForDifferentVersion(version: String) {
|
@MainActor private func presentAlertForDifferentVersion(version: String) {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_my_valet_done.title".localized,
|
title: "alert.fix_my_valet_done.title".localized,
|
||||||
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
/**
|
/**
|
||||||
@ -15,7 +16,7 @@ extension MainMenu {
|
|||||||
func startup() async {
|
func startup() async {
|
||||||
// Start with the icon
|
// Start with the icon
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
self.setStatusBar(image: NSImage.statusBarIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
if await Startup().checkEnvironment() {
|
if await Startup().checkEnvironment() {
|
||||||
@ -32,19 +33,14 @@ extension MainMenu {
|
|||||||
// Determine what the `php` formula is aliased to
|
// Determine what the `php` formula is aliased to
|
||||||
await PhpEnvironments.shared.determinePhpAlias()
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
|
||||||
|
// Make sure that broken symlinks are removed ASAP
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Initialize preferences
|
// Initialize preferences
|
||||||
_ = Preferences.shared
|
_ = Preferences.shared
|
||||||
|
|
||||||
// Determine install method
|
// Put some useful diagnostics information in log
|
||||||
Log.info(BrewDiagnostics.customCaskInstalled
|
BrewDiagnostics.logBootInformation()
|
||||||
? "[BREW] The app has been installed via Homebrew Cask."
|
|
||||||
: "[BREW] The app has been installed directly (optimal)."
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.info(BrewDiagnostics.usesNginxFullFormula
|
|
||||||
? "[BREW] The app will be using the `nginx-full` formula."
|
|
||||||
: "[BREW] The app will be using the `nginx` formula."
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attempt to find out more info about Valet
|
// Attempt to find out more info about Valet
|
||||||
if Valet.shared.version != nil {
|
if Valet.shared.version != nil {
|
||||||
@ -63,9 +59,6 @@ extension MainMenu {
|
|||||||
// Check for an alias conflict
|
// Check for an alias conflict
|
||||||
await BrewDiagnostics.checkForCaskConflict()
|
await BrewDiagnostics.checkForCaskConflict()
|
||||||
|
|
||||||
// Update the icon
|
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
|
|
||||||
// Attempt to find out if PHP-FPM is broken
|
// Attempt to find out if PHP-FPM is broken
|
||||||
PhpEnvironments.prepare()
|
PhpEnvironments.prepare()
|
||||||
|
|
||||||
@ -76,7 +69,6 @@ extension MainMenu {
|
|||||||
WarningManager.shared.evaluateWarnings()
|
WarningManager.shared.evaluateWarnings()
|
||||||
|
|
||||||
// Set up the config watchers on launch (updated automatically when switching)
|
// Set up the config watchers on launch (updated automatically when switching)
|
||||||
Log.info("Setting up watchers...")
|
|
||||||
App.shared.handlePhpConfigWatcher()
|
App.shared.handlePhpConfigWatcher()
|
||||||
|
|
||||||
// Detect built-in and custom applications
|
// Detect built-in and custom applications
|
||||||
@ -105,9 +97,33 @@ extension MainMenu {
|
|||||||
Valet.shared.notifyAboutUnsupportedTLD()
|
Valet.shared.notifyAboutUnsupportedTLD()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep track of which PHP versions are currently about to release
|
||||||
|
Log.info("Experimental PHP versions are: \(Constants.ExperimentalPhpVersions)")
|
||||||
|
|
||||||
// Find out which services are active
|
// Find out which services are active
|
||||||
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
||||||
|
|
||||||
|
// Post-launch stats and update check, but only if not running tests
|
||||||
|
await performPostLaunchActions()
|
||||||
|
|
||||||
|
// Check if the linked version has changed between launches of phpmon
|
||||||
|
PhpGuard().compareToLastGlobalVersion()
|
||||||
|
|
||||||
|
// We are ready!
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
|
||||||
|
// Finally!
|
||||||
|
Log.info("PHP Monitor is ready to serve!")
|
||||||
|
|
||||||
|
// Check if we upgraded from a previous version
|
||||||
|
AppUpdater.checkIfUpdateWasPerformed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Performs a set of post-launch actions, like incrementing stats and checking for updates.
|
||||||
|
(This code is skipped when running SwiftUI previews.)
|
||||||
|
*/
|
||||||
|
private func performPostLaunchActions() async {
|
||||||
if !isRunningSwiftUIPreview {
|
if !isRunningSwiftUIPreview {
|
||||||
Stats.incrementSuccessfulLaunchCount()
|
Stats.incrementSuccessfulLaunchCount()
|
||||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||||
@ -121,15 +137,6 @@ extension MainMenu {
|
|||||||
await AppUpdater().checkForUpdates(userInitiated: false)
|
await AppUpdater().checkForUpdates(userInitiated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the linked version has changed between launches of phpmon
|
|
||||||
PhpGuard().compareToLastGlobalVersion()
|
|
||||||
|
|
||||||
// We are ready!
|
|
||||||
Log.info("PHP Monitor is ready to serve!")
|
|
||||||
|
|
||||||
// Check if we upgraded just now
|
|
||||||
AppUpdater.checkIfUpdateWasPerformed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -137,7 +144,7 @@ extension MainMenu {
|
|||||||
*/
|
*/
|
||||||
private func onEnvironmentFail() async {
|
private func onEnvironmentFail() async {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.cannot_start.title".localized,
|
title: "alert.cannot_start.title".localized,
|
||||||
subtitle: "alert.cannot_start.subtitle".localized,
|
subtitle: "alert.cannot_start.subtitle".localized,
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
|
|
||||||
@ -16,7 +17,9 @@ extension MainMenu {
|
|||||||
|
|
||||||
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
||||||
// Mark as no longer busy
|
// Mark as no longer busy
|
||||||
PhpEnvironments.shared.isBusy = false
|
Task { @MainActor in
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
Task { // Things to do after reloading domain list data
|
Task { // Things to do after reloading domain list data
|
||||||
if Valet.installed {
|
if Valet.installed {
|
||||||
@ -25,7 +28,7 @@ extension MainMenu {
|
|||||||
|
|
||||||
// Perform UI updates on main thread
|
// Perform UI updates on main thread
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
|
|
||||||
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
||||||
@ -73,7 +76,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func suggestFixMyValet(failed version: String) {
|
@MainActor private func suggestFixMyValet(failed version: String) {
|
||||||
let outcome = BetterAlert()
|
let outcome = NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.php_switch_failed.title".localized(version),
|
title: "alert.php_switch_failed.title".localized(version),
|
||||||
subtitle: "alert.php_switch_failed.info".localized(version),
|
subtitle: "alert.php_switch_failed.info".localized(version),
|
||||||
@ -88,7 +91,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func suggestFixMyComposer() {
|
@MainActor private func suggestFixMyComposer() {
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.global_composer_platform_issues.title".localized,
|
title: "alert.global_composer_platform_issues.title".localized,
|
||||||
subtitle: "alert.global_composer_platform_issues.subtitle".localized,
|
subtitle: "alert.global_composer_platform_issues.subtitle".localized,
|
||||||
description: "alert.global_composer_platform_issues.desc".localized
|
description: "alert.global_composer_platform_issues.desc".localized
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
|
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
|
||||||
@ -37,8 +38,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
// MARK: - UI related
|
// MARK: - UI related
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Rebuilds the menu (either asynchronously or synchronously).
|
Rebuilds the menu on the main thread.
|
||||||
Defaults to rebuilding the menu asynchronously.
|
|
||||||
*/
|
*/
|
||||||
func rebuild() {
|
func rebuild() {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
@ -80,13 +80,15 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func refreshActiveInstallation() {
|
@objc func refreshActiveInstallation() {
|
||||||
if !PhpEnvironments.shared.isBusy {
|
if !PhpEnvironments.shared.isBusy {
|
||||||
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
} else {
|
} else {
|
||||||
Log.perf("Skipping version refresh due to busy status!")
|
Log.perf("Skipping version refresh due to busy status!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||||
|
@available(*, deprecated, message: "Use the busy status instead")
|
||||||
@objc func updatePhpVersionInStatusBar() {
|
@objc func updatePhpVersionInStatusBar() {
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
@ -119,7 +121,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
|
|
||||||
@objc func showIncompatiblePhpVersionsAlert() {
|
@objc func showIncompatiblePhpVersionsAlert() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "startup.unsupported_versions_explanation.title".localized,
|
title: "startup.unsupported_versions_explanation.title".localized,
|
||||||
subtitle: "startup.unsupported_versions_explanation.subtitle".localized(
|
subtitle: "startup.unsupported_versions_explanation.subtitle".localized(
|
||||||
PhpEnvironments.shared.incompatiblePhpVersions
|
PhpEnvironments.shared.incompatiblePhpVersions
|
||||||
@ -139,7 +141,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func reloadPhpMonitorMenuInBackground() {
|
@objc func reloadPhpMonitorMenuInBackground() {
|
||||||
asyncExecution({
|
asyncExecution({
|
||||||
// This automatically reloads the menu
|
// This automatically reloads the menu
|
||||||
Log.info("Reloading information about the PHP installation (in the background)...")
|
Log.perf("Reloading information about the PHP installation (in the background)...")
|
||||||
}, behaviours: [
|
}, behaviours: [
|
||||||
.setsBusyUI,
|
.setsBusyUI,
|
||||||
.reloadsPhpInstallation,
|
.reloadsPhpInstallation,
|
||||||
@ -150,13 +152,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
|
|
||||||
/** Refreshes the icon with the PHP version. */
|
/** Refreshes the icon with the PHP version. */
|
||||||
@objc func refreshIcon() {
|
@objc func refreshIcon() {
|
||||||
|
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
Log.perf("Refreshing icon: currently busy")
|
||||||
|
setStatusBar(image: NSImage.statusBarIcon)
|
||||||
} else {
|
} else {
|
||||||
|
Log.perf("Refreshing icon: no longer busy")
|
||||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||||
// Static icon has been requested
|
// Static icon has been requested
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
setStatusBar(image: NSImage.statusBarIconStatic)
|
||||||
} else {
|
} else {
|
||||||
// The dynamic icon has been requested
|
// The dynamic icon has been requested
|
||||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||||
@ -172,13 +177,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon to be displayed as busy. */
|
|
||||||
@objc func setBusyImage() {
|
|
||||||
Task { @MainActor [self] in
|
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Menu Item Functionality
|
// MARK: - Menu Item Functionality
|
||||||
|
|
||||||
@objc func openAbout() {
|
@objc func openAbout() {
|
||||||
@ -188,7 +186,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
|
|
||||||
@objc func openLiteModeInfo() {
|
@objc func openLiteModeInfo() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "lite_mode_explanation.title".localized,
|
title: "lite_mode_explanation.title".localized,
|
||||||
subtitle: "lite_mode_explanation.subtitle".localized,
|
subtitle: "lite_mode_explanation.subtitle".localized,
|
||||||
description: "lite_mode_explanation.description".localized
|
description: "lite_mode_explanation.description".localized
|
||||||
@ -218,6 +216,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
PhpVersionManagerWindowController.show()
|
PhpVersionManagerWindowController.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openPhpExtensionManager() {
|
||||||
|
PhpExtensionManagerWindowController.show()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func openDonate() {
|
@objc func openDonate() {
|
||||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import Cocoa
|
|||||||
|
|
||||||
extension StatusMenu {
|
extension StatusMenu {
|
||||||
|
|
||||||
func addPhpVersionMenuItems() {
|
@MainActor func addPhpVersionMenuItems() {
|
||||||
if PhpEnvironments.phpInstall == nil {
|
if PhpEnvironments.phpInstall == nil {
|
||||||
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
||||||
addItems([
|
addItems([
|
||||||
@ -34,7 +34,7 @@ extension StatusMenu {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPhpActionMenuItems() {
|
@MainActor func addPhpActionMenuItems() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
addItem(NSMenuItem(title: "mi_busy".localized))
|
addItem(NSMenuItem(title: "mi_busy".localized))
|
||||||
return
|
return
|
||||||
@ -54,7 +54,7 @@ extension StatusMenu {
|
|||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
func addServicesManagerMenuItem() {
|
@MainActor func addServicesManagerMenuItem() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSwitchToPhpMenuItems() {
|
@MainActor func addSwitchToPhpMenuItems() {
|
||||||
var shortcutKey = 1
|
var shortcutKey = 1
|
||||||
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
||||||
// Get the short and long version
|
// Get the short and long version
|
||||||
@ -102,14 +102,14 @@ extension StatusMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addLiteModeMenuItem() {
|
@MainActor func addLiteModeMenuItem() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPreferencesMenuItems() {
|
@MainActor func addPreferencesMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_preferences".localized,
|
NSMenuItem(title: "mi_preferences".localized,
|
||||||
@ -119,7 +119,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addCoreMenuItems() {
|
@MainActor func addCoreMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_about".localized,
|
NSMenuItem(title: "mi_about".localized,
|
||||||
@ -131,7 +131,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Valet
|
// MARK: - Valet
|
||||||
|
|
||||||
func addValetMenuItems() {
|
@MainActor func addValetMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_valet".localized),
|
HeaderView.asMenuItem(text: "mi_valet".localized),
|
||||||
NSMenuItem(title: "mi_valet_config".localized,
|
NSMenuItem(title: "mi_valet_config".localized,
|
||||||
@ -146,12 +146,15 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - PHP Configuration
|
// MARK: - PHP Configuration
|
||||||
|
|
||||||
func addConfigurationMenuItems() {
|
@MainActor func addConfigurationMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
||||||
NSMenuItem(title: "mi_php_version_manager".localized,
|
NSMenuItem(title: "mi_php_version_manager".localized,
|
||||||
action: #selector(MainMenu.openPhpVersionManager),
|
action: #selector(MainMenu.openPhpVersionManager),
|
||||||
keyEquivalent: "m"),
|
keyEquivalent: "m"),
|
||||||
|
NSMenuItem(title: "mi_php_ext_manager".localized,
|
||||||
|
action: #selector(MainMenu.openPhpExtensionManager),
|
||||||
|
keyEquivalent: "e"),
|
||||||
NSMenuItem(title: "mi_php_config".localized,
|
NSMenuItem(title: "mi_php_config".localized,
|
||||||
action: #selector(MainMenu.openActiveConfigFolder),
|
action: #selector(MainMenu.openActiveConfigFolder),
|
||||||
keyEquivalent: "c"),
|
keyEquivalent: "c"),
|
||||||
@ -166,7 +169,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Composer
|
// MARK: - Composer
|
||||||
|
|
||||||
func addComposerMenuItems() {
|
@MainActor func addComposerMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_composer".localized),
|
HeaderView.asMenuItem(text: "mi_composer".localized),
|
||||||
NSMenuItem(
|
NSMenuItem(
|
||||||
@ -187,7 +190,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Stats
|
// MARK: - Stats
|
||||||
|
|
||||||
func addStatsMenuItem() {
|
@MainActor func addStatsMenuItem() {
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("Not showing stats menu item if no PHP version is linked.")
|
Log.info("Not showing stats menu item if no PHP version is linked.")
|
||||||
return
|
return
|
||||||
@ -200,21 +203,11 @@ extension StatusMenu {
|
|||||||
post: stats.post_max_size,
|
post: stats.post_max_size,
|
||||||
upload: stats.upload_max_filesize)
|
upload: stats.upload_max_filesize)
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: As soon as this does more than just edit memory limits, move this
|
|
||||||
/*
|
|
||||||
addItem(NSMenuItem.separator())
|
|
||||||
addItem(NSMenuItem(
|
|
||||||
title: "mi_manage_limits".localized,
|
|
||||||
action: #selector(MainMenu.openConfigGUI),
|
|
||||||
keyEquivalent: "l")
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Extensions
|
// MARK: - Extensions
|
||||||
|
|
||||||
func addExtensionsMenuItems() {
|
@MainActor func addExtensionsMenuItems() {
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
||||||
return
|
return
|
||||||
@ -235,7 +228,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Presets
|
// MARK: - Presets
|
||||||
|
|
||||||
func addPresetsMenuItem() {
|
@MainActor func addPresetsMenuItem() {
|
||||||
guard let presets = Preferences.custom.presets else {
|
guard let presets = Preferences.custom.presets else {
|
||||||
addEmptyPresetHelp()
|
addEmptyPresetHelp()
|
||||||
return
|
return
|
||||||
@ -249,7 +242,7 @@ extension StatusMenu {
|
|||||||
addLoadedPresets()
|
addLoadedPresets()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addEmptyPresetHelp() {
|
@MainActor private func addEmptyPresetHelp() {
|
||||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||||
NSMenuItem(title: "mi_no_presets".localized),
|
NSMenuItem(title: "mi_no_presets".localized),
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
@ -258,7 +251,7 @@ extension StatusMenu {
|
|||||||
], target: MainMenu.shared))
|
], target: MainMenu.shared))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addLoadedPresets() {
|
@MainActor private func addLoadedPresets() {
|
||||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)
|
HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)
|
||||||
@ -273,7 +266,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Xdebug
|
// MARK: - Xdebug
|
||||||
|
|
||||||
func addXdebugMenuItem() {
|
@MainActor func addXdebugMenuItem() {
|
||||||
if !Xdebug.enabled {
|
if !Xdebug.enabled {
|
||||||
addItem(NSMenuItem.separator())
|
addItem(NSMenuItem.separator())
|
||||||
return
|
return
|
||||||
@ -293,7 +286,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - PHP Doctor
|
// MARK: - PHP Doctor
|
||||||
|
|
||||||
func addPhpDoctorMenuItem() {
|
@MainActor func addPhpDoctorMenuItem() {
|
||||||
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
|
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
|
||||||
!WarningManager.shared.hasWarnings() {
|
!WarningManager.shared.hasWarnings() {
|
||||||
return
|
return
|
||||||
@ -309,7 +302,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - First Aid & Services
|
// MARK: - First Aid & Services
|
||||||
|
|
||||||
func addFirstAidAndServicesMenuItems() {
|
@MainActor func addFirstAidAndServicesMenuItems() {
|
||||||
let services = NSMenuItem(title: "mi_other".localized)
|
let services = NSMenuItem(title: "mi_other".localized)
|
||||||
|
|
||||||
var items: [NSMenuItem] = [
|
var items: [NSMenuItem] = [
|
||||||
@ -366,7 +359,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Other helper methods to generate menu items
|
// MARK: - Other helper methods to generate menu items
|
||||||
|
|
||||||
func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
@MainActor func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
||||||
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
||||||
|
|
||||||
let menuItem = ExtensionMenuItem(
|
let menuItem = ExtensionMenuItem(
|
||||||
|
@ -9,7 +9,7 @@ import Cocoa
|
|||||||
|
|
||||||
class StatusMenu: NSMenu {
|
class StatusMenu: NSMenu {
|
||||||
// swiftlint:disable cyclomatic_complexity
|
// swiftlint:disable cyclomatic_complexity
|
||||||
func addMenuItems() {
|
@MainActor func addMenuItems() {
|
||||||
addPhpVersionMenuItems()
|
addPhpVersionMenuItems()
|
||||||
addItem(NSMenuItem.separator())
|
addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
//
|
|
||||||
// Notice.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 16/02/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class BetterAlert {
|
|
||||||
|
|
||||||
var windowController: NSWindowController!
|
|
||||||
|
|
||||||
var noticeVC: BetterAlertVC {
|
|
||||||
return self.windowController.contentViewController as! BetterAlertVC
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
|
||||||
|
|
||||||
self.windowController = storyboard.instantiateController(
|
|
||||||
withIdentifier: "noticeWindow"
|
|
||||||
) as? NSWindowController
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func make() -> BetterAlert {
|
|
||||||
return BetterAlert()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func withPrimary(
|
|
||||||
text: String,
|
|
||||||
action: @MainActor @escaping (BetterAlertVC) -> Void = { vc in
|
|
||||||
vc.close(with: .alertFirstButtonReturn)
|
|
||||||
}
|
|
||||||
) -> Self {
|
|
||||||
self.noticeVC.buttonPrimary.title = text
|
|
||||||
self.noticeVC.actionPrimary = action
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
public func withSecondary(
|
|
||||||
text: String,
|
|
||||||
action: (@MainActor (BetterAlertVC) -> Void)? = { vc in
|
|
||||||
vc.close(with: .alertSecondButtonReturn)
|
|
||||||
}
|
|
||||||
) -> Self {
|
|
||||||
self.noticeVC.buttonSecondary.title = text
|
|
||||||
self.noticeVC.actionSecondary = action
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
public func withTertiary(
|
|
||||||
text: String = "",
|
|
||||||
action: (@MainActor (BetterAlertVC) -> Void)? = nil
|
|
||||||
) -> Self {
|
|
||||||
if text == "" {
|
|
||||||
self.noticeVC.buttonTertiary.bezelStyle = .helpButton
|
|
||||||
}
|
|
||||||
self.noticeVC.buttonTertiary.title = text
|
|
||||||
self.noticeVC.actionTertiary = action
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
public func withInformation(
|
|
||||||
title: String,
|
|
||||||
subtitle: String,
|
|
||||||
description: String = ""
|
|
||||||
) -> Self {
|
|
||||||
self.noticeVC.labelTitle.stringValue = title
|
|
||||||
self.noticeVC.labelSubtitle.stringValue = subtitle
|
|
||||||
self.noticeVC.labelDescription.stringValue = description
|
|
||||||
|
|
||||||
// If the description is missing, handle the excess space and change the top margin
|
|
||||||
if description == "" {
|
|
||||||
self.noticeVC.labelDescription.isHidden = true
|
|
||||||
self.noticeVC.primaryButtonTopMargin.constant = 0
|
|
||||||
}
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Shows the modal and returns a ModalResponse.
|
|
||||||
If you wish to simply show the alert and disregard the outcome, use `show`.
|
|
||||||
*/
|
|
||||||
@MainActor public func runModal() -> NSApplication.ModalResponse {
|
|
||||||
if !Thread.isMainThread {
|
|
||||||
fatalError("You should always present alerts on the main thread!")
|
|
||||||
}
|
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
|
|
||||||
windowController.window?.makeKeyAndOrderFront(nil)
|
|
||||||
windowController.window?.setCenterPosition(offsetY: 70)
|
|
||||||
return NSApplication.shared.runModal(for: windowController.window!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Shows the modal and returns true if the user pressed the primary button. */
|
|
||||||
@MainActor public func didSelectPrimary() -> Bool {
|
|
||||||
return self.runModal() == .alertFirstButtonReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Shows the modal and does not return anything.
|
|
||||||
*/
|
|
||||||
@MainActor public func show() {
|
|
||||||
_ = self.runModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Shows the modal for a particular error.
|
|
||||||
*/
|
|
||||||
@MainActor public static func show(for error: Error & AlertableError) {
|
|
||||||
let key = error.getErrorMessageKey()
|
|
||||||
return BetterAlert().withInformation(
|
|
||||||
title: "\(key).title".localized,
|
|
||||||
subtitle: "\(key).description".localized
|
|
||||||
).withPrimary(text: "generic.ok".localized).show()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
//
|
|
||||||
// NoticeVC.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 16/02/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
class BetterAlertVC: NSViewController {
|
|
||||||
|
|
||||||
// MARK: - Outlets
|
|
||||||
|
|
||||||
@IBOutlet weak var labelTitle: NSTextField!
|
|
||||||
@IBOutlet weak var labelSubtitle: NSTextField!
|
|
||||||
@IBOutlet weak var labelDescription: NSTextField!
|
|
||||||
|
|
||||||
@IBOutlet weak var buttonPrimary: NSButton!
|
|
||||||
@IBOutlet weak var buttonSecondary: NSButton!
|
|
||||||
@IBOutlet weak var buttonTertiary: NSButton!
|
|
||||||
|
|
||||||
var actionPrimary: (@MainActor (BetterAlertVC) -> Void) = { _ in }
|
|
||||||
var actionSecondary: (@MainActor (BetterAlertVC) -> Void)?
|
|
||||||
var actionTertiary: (@MainActor (BetterAlertVC) -> Void)?
|
|
||||||
|
|
||||||
@IBOutlet weak var imageView: NSImageView!
|
|
||||||
|
|
||||||
@IBOutlet weak var primaryButtonTopMargin: NSLayoutConstraint!
|
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
|
||||||
|
|
||||||
override func viewWillAppear() {
|
|
||||||
imageView.image = NSApp.applicationIconImage
|
|
||||||
|
|
||||||
if actionSecondary == nil {
|
|
||||||
buttonSecondary.isHidden = true
|
|
||||||
}
|
|
||||||
if actionTertiary == nil {
|
|
||||||
buttonTertiary.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear() {
|
|
||||||
view.window?.makeFirstResponder(buttonPrimary)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
// Log.perf("deinit: \(String(describing: self)).\(#function)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Outlet Actions
|
|
||||||
|
|
||||||
@IBAction func primaryButtonAction(_ sender: Any) {
|
|
||||||
self.actionPrimary(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func secondaryButtonAction(_ sender: Any) {
|
|
||||||
if self.actionSecondary != nil {
|
|
||||||
self.actionSecondary!(self)
|
|
||||||
} else {
|
|
||||||
self.close(with: .alertSecondButtonReturn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func tertiaryButtonAction(_ sender: Any) {
|
|
||||||
if self.actionTertiary != nil {
|
|
||||||
self.actionTertiary!(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor public func close(with code: NSApplication.ModalResponse) {
|
|
||||||
self.view.window?.close()
|
|
||||||
NSApplication.shared.stopModal(withCode: code)
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class PhpGuard {
|
class PhpGuard {
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ class PhpGuard {
|
|||||||
Log.info("PHP Guard noticed a different PHP version. An alert will be displayed!")
|
Log.info("PHP Guard noticed a different PHP version. An alert will be displayed!")
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "startup.version_mismatch.title".localized,
|
title: "startup.version_mismatch.title".localized,
|
||||||
subtitle: "startup.version_mismatch.subtitle".localized(
|
subtitle: "startup.version_mismatch.subtitle".localized(
|
||||||
|
@ -20,6 +20,7 @@ enum PreferenceName: String, Codable {
|
|||||||
case globalHotkey = "global_hotkey"
|
case globalHotkey = "global_hotkey"
|
||||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||||
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
||||||
|
case languageOverride = "language_override"
|
||||||
|
|
||||||
// APPEARANCE
|
// APPEARANCE
|
||||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||||
@ -84,7 +85,8 @@ enum PreferenceName: String, Codable {
|
|||||||
],
|
],
|
||||||
.string: [
|
.string: [
|
||||||
.globalHotkey,
|
.globalHotkey,
|
||||||
.iconTypeToDisplay
|
.iconTypeToDisplay,
|
||||||
|
.languageOverride
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,10 @@ class Preferences {
|
|||||||
environmentVariables: [:]
|
environmentVariables: [:]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if isRunningSwiftUIPreview {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Task { await loadCustomPreferences() }
|
Task { await loadCustomPreferences() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +55,7 @@ class Preferences {
|
|||||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||||
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
||||||
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
||||||
|
PreferenceName.languageOverride.rawValue: "",
|
||||||
|
|
||||||
/// Preferences: Appearance
|
/// Preferences: Appearance
|
||||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||||
|
@ -17,7 +17,9 @@ class GeneralPreferencesVC: GenericPreferenceVC {
|
|||||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||||
|
|
||||||
_ = vc.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
_ = vc
|
||||||
|
.addView(when: true, vc.getLanguageOptionsPV())
|
||||||
|
.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
||||||
.addView(when: true, vc.getAutoRestartServicesPV())
|
.addView(when: true, vc.getAutoRestartServicesPV())
|
||||||
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
||||||
.addView(when: true, vc.getShortcutPV())
|
.addView(when: true, vc.getShortcutPV())
|
||||||
|
@ -48,11 +48,44 @@ class GenericPreferenceVC: NSViewController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLanguageOptionsPV() -> NSView {
|
||||||
|
var options = Bundle.main.localizations
|
||||||
|
.filter({ $0 != "Base"})
|
||||||
|
.map({ lang in
|
||||||
|
return PreferenceDropdownOption(
|
||||||
|
label: Locale.current.localizedString(forLanguageCode: lang)!,
|
||||||
|
value: lang
|
||||||
|
)
|
||||||
|
})
|
||||||
|
options.insert(PreferenceDropdownOption(label: "System Default", value: ""), at: 0)
|
||||||
|
|
||||||
|
return SelectPreferenceView.make(
|
||||||
|
sectionText: "prefs.language".localized,
|
||||||
|
descriptionText: "prefs.language_options_desc".localized,
|
||||||
|
options: options,
|
||||||
|
preference: .languageOverride,
|
||||||
|
action: {
|
||||||
|
MainMenu.shared.refreshIcon()
|
||||||
|
MainMenu.shared.rebuild()
|
||||||
|
|
||||||
|
if let window = App.shared.preferencesWindowController?.window {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "alert.language_changed.title".localized
|
||||||
|
alert.informativeText = "alert.language_changed.subtitle".localized
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.addButton(withTitle: "generic.ok".localized)
|
||||||
|
alert.beginSheetModal(for: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func getIconOptionsPV() -> NSView {
|
func getIconOptionsPV() -> NSView {
|
||||||
return SelectPreferenceView.make(
|
return SelectPreferenceView.make(
|
||||||
sectionText: "",
|
sectionText: "",
|
||||||
descriptionText: "prefs.icon_options_desc".localized,
|
descriptionText: "prefs.icon_options_desc".localized,
|
||||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
options: MenuBarIcon.allCases
|
||||||
|
.map({ return PreferenceDropdownOption(label: $0.rawValue, value: $0.rawValue) }),
|
||||||
localizationPrefix: "prefs.icon_options",
|
localizationPrefix: "prefs.icon_options",
|
||||||
preference: .iconTypeToDisplay,
|
preference: .iconTypeToDisplay,
|
||||||
action: {
|
action: {
|
||||||
|
@ -68,6 +68,8 @@ class PreferencesWindowController: PMWindowController {
|
|||||||
App.shared.preferencesWindowController?.positionWindowInTopRightCorner()
|
App.shared.preferencesWindowController?.positionWindowInTopRightCorner()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App.shared.preferencesWindowController?.window?.orderFrontRegardless()
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class Stats {
|
class Stats {
|
||||||
|
|
||||||
@ -84,6 +85,10 @@ class Stats {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func clearCurrentGlobalPhpVersion() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: InternalStats.lastGlobalPhpVersion.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine if the sponsor message should be displayed.
|
Determine if the sponsor message should be displayed.
|
||||||
|
|
||||||
@ -119,7 +124,7 @@ class Stats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
let donate = BetterAlert()
|
let donate = NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "startup.sponsor_encouragement.title".localized,
|
title: "startup.sponsor_encouragement.title".localized,
|
||||||
subtitle: "startup.sponsor_encouragement.subtitle".localized,
|
subtitle: "startup.sponsor_encouragement.subtitle".localized,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
|
@ -9,30 +9,34 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class SelectPreferenceView: NSView, XibLoadable {
|
struct PreferenceDropdownOption {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectPreferenceView: NSView, XibLoadable {
|
||||||
@IBOutlet weak var labelSection: NSTextField!
|
@IBOutlet weak var labelSection: NSTextField!
|
||||||
@IBOutlet weak var labelDescription: NSTextField!
|
@IBOutlet weak var labelDescription: NSTextField!
|
||||||
@IBOutlet weak var popupButton: NSPopUpButton!
|
@IBOutlet weak var popupButton: NSPopUpButton!
|
||||||
|
|
||||||
var localizationPrefix: String = ""
|
var localizationPrefix: String?
|
||||||
var imagePrefix: String?
|
var imagePrefix: String?
|
||||||
|
|
||||||
var options: [String] = [] {
|
var options: [PreferenceDropdownOption] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
self.popupButton.removeAllItems()
|
self.popupButton.removeAllItems()
|
||||||
self.options.forEach { value in
|
self.options.forEach { option in
|
||||||
self.popupButton.addItem(
|
if let prefix = localizationPrefix {
|
||||||
withTitle: "\(localizationPrefix).\(value)".localized
|
self.popupButton.addItem(withTitle: "\(prefix).\(option.label)".localized)
|
||||||
)
|
} else {
|
||||||
|
self.popupButton.addItem(withTitle: option.label)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if imagePrefix == nil {
|
if let prefix = imagePrefix {
|
||||||
return
|
self.popupButton.itemArray.enumerated().forEach { item in
|
||||||
}
|
item.element.image = NSImage(named: "\(prefix)_\(self.options[item.offset].value)")
|
||||||
|
}
|
||||||
self.popupButton.itemArray.enumerated().forEach { item in
|
|
||||||
item.element.image = NSImage(named: "\(imagePrefix!)_\(self.options[item.offset])")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,19 +47,18 @@ class SelectPreferenceView: NSView, XibLoadable {
|
|||||||
didSet {
|
didSet {
|
||||||
let value = Preferences.preferences[preference] as! String
|
let value = Preferences.preferences[preference] as! String
|
||||||
self.options.enumerated().forEach { option in
|
self.options.enumerated().forEach { option in
|
||||||
if option.element == value {
|
if option.element.value == value {
|
||||||
self.popupButton.selectItem(at: option.offset)
|
self.popupButton.selectItem(at: option.offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable function_parameter_count
|
|
||||||
static func make(
|
static func make(
|
||||||
sectionText: String,
|
sectionText: String,
|
||||||
descriptionText: String,
|
descriptionText: String,
|
||||||
options: [String],
|
options: [PreferenceDropdownOption],
|
||||||
localizationPrefix: String,
|
localizationPrefix: String? = nil,
|
||||||
imagePrefix: String? = nil,
|
imagePrefix: String? = nil,
|
||||||
preference: PreferenceName,
|
preference: PreferenceName,
|
||||||
action: @escaping () -> Void) -> NSView {
|
action: @escaping () -> Void) -> NSView {
|
||||||
@ -72,11 +75,10 @@ class SelectPreferenceView: NSView, XibLoadable {
|
|||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
// swiftlint:enable function_parameter_count
|
|
||||||
|
|
||||||
@IBAction func valueChanged(_ sender: Any) {
|
@IBAction func valueChanged(_ sender: Any) {
|
||||||
let index = self.popupButton.indexOfSelectedItem
|
let index = self.popupButton.indexOfSelectedItem
|
||||||
Preferences.update(.iconTypeToDisplay, value: self.options[index])
|
Preferences.update(self.preference, value: self.options[index].value)
|
||||||
self.action()
|
self.action()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@ -13,16 +13,16 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="183" y="5" width="395" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="13" y="29" width="154" height="16"/>
|
<rect key="frame" x="-2" y="29" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
@ -33,7 +33,7 @@
|
|||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YaB-Tg-Ir3">
|
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YaB-Tg-Ir3">
|
||||||
<rect key="frame" x="182" y="23" width="110" height="25"/>
|
<rect key="frame" x="167" y="23" width="110" height="25"/>
|
||||||
<popUpButtonCell key="cell" type="push" title="Icon Option" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SaA-mm-HBo" id="Su6-C4-aGo">
|
<popUpButtonCell key="cell" type="push" title="Icon Option" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SaA-mm-HBo" id="Su6-C4-aGo">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="menu"/>
|
<font key="font" metaFont="menu"/>
|
||||||
@ -58,7 +58,7 @@
|
|||||||
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
||||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
||||||
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="15" id="Ztd-uk-4aw"/>
|
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" id="aBU-J8-gRK"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
struct Preset: Codable, Equatable {
|
struct Preset: Codable, Equatable {
|
||||||
let name: String
|
let name: String
|
||||||
@ -139,7 +140,7 @@ struct Preset: Codable, Equatable {
|
|||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.php_switch_unavailable.title".localized,
|
title: "alert.php_switch_unavailable.title".localized,
|
||||||
subtitle: "alert.php_switch_unavailable.subtitle".localized(version!),
|
subtitle: "alert.php_switch_unavailable.subtitle".localized(version!),
|
||||||
description: "alert.php_switch_unavailable.info".localized(
|
description: "alert.php_switch_unavailable.info".localized(
|
||||||
|
@ -29,7 +29,7 @@ struct BlockingOverlayView<Content: View>: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
content().opacity(isBlocking ? 0.2 : 1)
|
content().opacity(isBlocking ? 0 : 1)
|
||||||
if isBlocking {
|
if isBlocking {
|
||||||
VStack {
|
VStack {
|
||||||
ActivityIndicator()
|
ActivityIndicator()
|
||||||
@ -44,7 +44,8 @@ struct BlockingOverlayView<Content: View>: View {
|
|||||||
.padding(.top, -4)
|
.padding(.top, -4)
|
||||||
}.padding(60)
|
}.padding(60)
|
||||||
}
|
}
|
||||||
}.background(Color.white)
|
}
|
||||||
|
.background(Color.spinnerBackground)
|
||||||
.disabled(isBlocking)
|
.disabled(isBlocking)
|
||||||
}
|
}
|
||||||
}
|
}
|
36
phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift
Normal file
36
phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// CustomButtonStyles.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 15/03/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct CustomButtonStyle: ButtonStyle {
|
||||||
|
@Environment(\.isEnabled) var isEnabled
|
||||||
|
|
||||||
|
public func makeBody(configuration: Self.Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.background(.statusColorBlue, in: .rect(cornerRadius: 8, style: .continuous))
|
||||||
|
.opacity({
|
||||||
|
if configuration.isPressed {
|
||||||
|
return 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isEnabled {
|
||||||
|
return 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1.0
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ButtonStyle where Self == CustomButtonStyle {
|
||||||
|
static var custom: CustomButtonStyle { .init() }
|
||||||
|
}
|
@ -27,15 +27,15 @@ struct HelpButton: View {
|
|||||||
.buttonStyle(BorderlessButtonStyle())
|
.buttonStyle(BorderlessButtonStyle())
|
||||||
.focusable(false)
|
.focusable(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
struct HelpButton_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
#Preview("Light Mode") {
|
||||||
Group {
|
HelpButton(action: {})
|
||||||
HelpButton(action: {}).padding()
|
.padding(100)
|
||||||
.previewDisplayName("Light Mode")
|
}
|
||||||
HelpButton(action: {}).padding().preferredColorScheme(.dark)
|
|
||||||
.previewDisplayName("Dark Mode")
|
#Preview("Dark Mode") {
|
||||||
}
|
HelpButton(action: {})
|
||||||
}
|
.padding(100)
|
||||||
}
|
.preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,9 @@ var isRunningSwiftUIPreview: Bool {
|
|||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
public static var appPrimary: Color = Color("AppColor")
|
public static var appPrimary: Color = Color("AppColor")
|
||||||
public static var appSecondary: Color = Color("AppSecondary")
|
|
||||||
|
// This next one is generated automatically via asset catalogs now
|
||||||
|
// public static var appSecondary: Color = Color("AppSecondary")
|
||||||
|
|
||||||
public static var debug: Color = {
|
public static var debug: Color = {
|
||||||
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
||||||
|
67
phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift
Normal file
67
phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
//
|
||||||
|
// NoDomainsView.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 19/03/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UnavailableContentView: View {
|
||||||
|
var title: String
|
||||||
|
var description: String
|
||||||
|
var icon: String
|
||||||
|
var button: String?
|
||||||
|
var action: (() -> Void)?
|
||||||
|
|
||||||
|
init(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
icon: String,
|
||||||
|
button: String? = nil,
|
||||||
|
action: (() -> Void)? = nil
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.icon = icon
|
||||||
|
self.button = button
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
Image(systemName: self.icon)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 48, height: 48)
|
||||||
|
.foregroundColor(Color.appPrimary)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
Text(self.title)
|
||||||
|
.font(.system(size: 18, weight: .bold))
|
||||||
|
|
||||||
|
Text(self.description)
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
if self.button != nil {
|
||||||
|
Button(self.button!) {
|
||||||
|
self.action!()
|
||||||
|
}.buttonStyle(.custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(30)
|
||||||
|
.frame(maxWidth: 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
UnavailableContentView(
|
||||||
|
title: "domain_list.domains_empty.title".localized,
|
||||||
|
description: "domain_list.domains_empty.desc".localized,
|
||||||
|
icon: "globe",
|
||||||
|
button: "domain_list.domains_empty.button".localized,
|
||||||
|
action: {}
|
||||||
|
)
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// NoDomainResults.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 15/08/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct NoDomainResults: View {
|
|
||||||
@State var searching: Bool = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .center, spacing: 15) {
|
|
||||||
Image(systemName: searching ? "magnifyingglass.circle.fill" : "questionmark.circle.fill")
|
|
||||||
.resizable()
|
|
||||||
.renderingMode(.template)
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
VStack(alignment: .center) {
|
|
||||||
Text(
|
|
||||||
searching
|
|
||||||
? "domain_list.no_domains_for_search_query".localizedForSwiftUI
|
|
||||||
: "domain_list.no_domains".localizedForSwiftUI
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minWidth: 0, maxWidth: .infinity)
|
|
||||||
.padding(25)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NoDomainResults_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
NoDomainResults()
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,8 +14,16 @@ struct VersionPopoverView: View {
|
|||||||
|
|
||||||
@State var validPhpVersions: [VersionNumber]
|
@State var validPhpVersions: [VersionNumber]
|
||||||
|
|
||||||
|
@State var prefersIsolationSuggestions: Bool
|
||||||
|
|
||||||
@State var parent: NSPopover!
|
@State var parent: NSPopover!
|
||||||
|
|
||||||
|
let rows = [
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible()),
|
||||||
|
GridItem(.flexible())
|
||||||
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text(getTitleText())
|
Text(getTitleText())
|
||||||
@ -32,14 +40,29 @@ struct VersionPopoverView: View {
|
|||||||
message: "alert.php_suggestions".localized,
|
message: "alert.php_suggestions".localized,
|
||||||
color: Color("AppColor")
|
color: Color("AppColor")
|
||||||
)
|
)
|
||||||
HStack {
|
if prefersIsolationSuggestions {
|
||||||
ForEach(validPhpVersions, id: \.self) { version in
|
// SITE ISOLATION (preferred)
|
||||||
Button("site_link.switch_to_php".localized(version.short), action: {
|
LazyVGrid(columns: self.rows, alignment: .leading, spacing: 5, content: {
|
||||||
MainMenu.shared.switchToPhpVersion(version.short)
|
ForEach(validPhpVersions, id: \.self) { version in
|
||||||
parent?.close()
|
Button("site_link.isolate_php".localized(version.short), action: {
|
||||||
})
|
App.shared.domainListWindowController?.contentVC
|
||||||
}
|
.isolateSite(site: site, version: version.short)
|
||||||
}.padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0))
|
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 {
|
} else {
|
||||||
if site.preferredPhpVersionSource == .unknown {
|
if site.preferredPhpVersionSource == .unknown {
|
||||||
@ -126,78 +149,90 @@ struct DisclaimerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VersionPopoverView_Previews: PreviewProvider {
|
#Preview("Unknown Requirement") {
|
||||||
static var previews: some View {
|
VersionPopoverView(
|
||||||
VersionPopoverView(
|
site: FakeValetSite(
|
||||||
site: FakeValetSite(
|
fakeWithName: "amazingwebsite",
|
||||||
fakeWithName: "amazingwebsite",
|
tld: "test",
|
||||||
tld: "test",
|
secure: true,
|
||||||
secure: true,
|
path: "/path/to/site",
|
||||||
path: "/path/to/site",
|
linked: true,
|
||||||
linked: true,
|
constraint: ""
|
||||||
constraint: ""
|
),
|
||||||
),
|
validPhpVersions: [],
|
||||||
validPhpVersions: [],
|
prefersIsolationSuggestions: false,
|
||||||
parent: nil
|
parent: nil
|
||||||
)
|
)
|
||||||
.previewDisplayName("Unknown Requirement")
|
}
|
||||||
|
|
||||||
VersionPopoverView(
|
#Preview("Requirement Matches") {
|
||||||
site: FakeValetSite(
|
VersionPopoverView(
|
||||||
fakeWithName: "amazingwebsite",
|
site: FakeValetSite(
|
||||||
tld: "test",
|
fakeWithName: "amazingwebsite",
|
||||||
secure: true,
|
tld: "test",
|
||||||
path: "/path/to/site",
|
secure: true,
|
||||||
linked: true,
|
path: "/path/to/site",
|
||||||
constraint: "^8.1"
|
linked: true,
|
||||||
),
|
constraint: "^8.1"
|
||||||
validPhpVersions: [],
|
),
|
||||||
parent: nil
|
validPhpVersions: [],
|
||||||
)
|
prefersIsolationSuggestions: false,
|
||||||
.previewDisplayName("Requirement Matches")
|
parent: nil
|
||||||
VersionPopoverView(
|
)
|
||||||
site: FakeValetSite(
|
}
|
||||||
fakeWithName: "anothersite",
|
|
||||||
tld: "test",
|
#Preview("Isolated") {
|
||||||
secure: true,
|
VersionPopoverView(
|
||||||
path: "/path/to/site",
|
site: FakeValetSite(
|
||||||
linked: true,
|
fakeWithName: "anothersite",
|
||||||
constraint: "^8.0",
|
tld: "test",
|
||||||
isolated: "8.0"
|
secure: true,
|
||||||
),
|
path: "/path/to/site",
|
||||||
validPhpVersions: [],
|
linked: true,
|
||||||
parent: nil
|
constraint: "^8.0",
|
||||||
)
|
isolated: "8.0"
|
||||||
.previewDisplayName("Isolated")
|
),
|
||||||
VersionPopoverView(
|
validPhpVersions: [],
|
||||||
site: FakeValetSite(
|
prefersIsolationSuggestions: false,
|
||||||
fakeWithName: "anothersite",
|
parent: nil
|
||||||
tld: "test",
|
)
|
||||||
secure: true,
|
}
|
||||||
path: "/path/to/site",
|
|
||||||
linked: true,
|
#Preview("Isolated Mismatch") {
|
||||||
constraint: "^8.0",
|
VersionPopoverView(
|
||||||
isolated: "7.4"
|
site: FakeValetSite(
|
||||||
),
|
fakeWithName: "anothersite",
|
||||||
validPhpVersions: [],
|
tld: "test",
|
||||||
parent: nil
|
secure: true,
|
||||||
)
|
path: "/path/to/site",
|
||||||
.previewDisplayName("Isolated Mismatch")
|
linked: true,
|
||||||
VersionPopoverView(
|
constraint: "^8.0",
|
||||||
site: FakeValetSite(
|
isolated: "7.4"
|
||||||
fakeWithName: "anothersite",
|
),
|
||||||
tld: "test",
|
validPhpVersions: [],
|
||||||
secure: true,
|
prefersIsolationSuggestions: false,
|
||||||
path: "/path/to/site",
|
parent: nil
|
||||||
linked: true,
|
)
|
||||||
constraint: "^8.0"
|
}
|
||||||
),
|
|
||||||
validPhpVersions: [
|
#Preview("Recommend Alternatives") {
|
||||||
VersionNumber(major: 8, minor: 0, patch: 0),
|
VersionPopoverView(
|
||||||
VersionNumber(major: 8, minor: 1, patch: 0)
|
site: FakeValetSite(
|
||||||
],
|
fakeWithName: "anothersite",
|
||||||
parent: nil
|
tld: "test",
|
||||||
)
|
secure: true,
|
||||||
.previewDisplayName("Recommend Alternatives")
|
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 {
|
#Preview {
|
||||||
static var previews: some View {
|
HeaderView(text: "Hello world")
|
||||||
HeaderView(text: "Hello world")
|
.frame(width: 330.0)
|
||||||
.frame(width: 330.0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
struct ServicesView: View {
|
struct ServicesView: View {
|
||||||
|
|
||||||
@ -81,7 +82,7 @@ struct ServicesView: View {
|
|||||||
: "key_service_not_running"
|
: "key_service_not_running"
|
||||||
|
|
||||||
// Show an alert with more information
|
// Show an alert with more information
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.\(type).title".localized,
|
title: "alert.\(type).title".localized,
|
||||||
subtitle: "alert.\(type).subtitle".localized,
|
subtitle: "alert.\(type).subtitle".localized,
|
||||||
description: "alert.\(type).desc".localized
|
description: "alert.\(type).desc".localized
|
||||||
@ -116,7 +117,7 @@ struct ServiceView: View {
|
|||||||
if service.status == .missing {
|
if service.status == .missing {
|
||||||
Button {
|
Button {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.warnings.service_missing.title".localized,
|
title: "alert.warnings.service_missing.title".localized,
|
||||||
subtitle: "alert.warnings.service_missing.subtitle".localized,
|
subtitle: "alert.warnings.service_missing.subtitle".localized,
|
||||||
description: "alert.warnings.service_missing.description".localized
|
description: "alert.warnings.service_missing.description".localized
|
||||||
@ -172,23 +173,21 @@ struct ServiceView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ServicesView_Previews: PreviewProvider {
|
#Preview("Active 1") {
|
||||||
static var previews: some View {
|
ServicesView(manager: FakeServicesManager(
|
||||||
ServicesView(manager: FakeServicesManager(
|
formulae: ["php", "nginx", "dnsmasq"],
|
||||||
formulae: ["php", "nginx", "dnsmasq"],
|
status: .active
|
||||||
status: .active
|
), perRow: 4)
|
||||||
), perRow: 4)
|
.frame(width: 330.0)
|
||||||
.frame(width: 330.0)
|
}
|
||||||
.previewDisplayName("Active 1")
|
|
||||||
|
#Preview("Active 2") {
|
||||||
ServicesView(manager: FakeServicesManager(
|
ServicesView(manager: FakeServicesManager(
|
||||||
formulae: [
|
formulae: [
|
||||||
"php", "nginx", "dnsmasq", "thing1",
|
"php", "nginx", "dnsmasq", "thing1",
|
||||||
"thing2", "thing3", "thing4", "thing5"
|
"thing2", "thing3", "thing4", "thing5"
|
||||||
],
|
],
|
||||||
status: .inactive
|
status: .inactive
|
||||||
), perRow: 4)
|
), perRow: 4)
|
||||||
.frame(width: 330.0)
|
.frame(width: 330.0)
|
||||||
.previewDisplayName("Active 2")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,7 @@ struct StatsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "gearshape.fill")
|
Image(systemName: "gearshape.fill")
|
||||||
}
|
}
|
||||||
|
.accessibility(identifier: "phpConfigButton")
|
||||||
.focusable(false)
|
.focusable(false)
|
||||||
.frame(minWidth: 30, alignment: .center)
|
.frame(minWidth: 30, alignment: .center)
|
||||||
}
|
}
|
||||||
@ -98,12 +99,10 @@ struct StatsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StatsView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
StatsView(
|
||||||
StatsView(
|
memoryLimit: "1024 MB",
|
||||||
memoryLimit: "1024 MB",
|
maxPostSize: "1024 MB",
|
||||||
maxPostSize: "1024 MB",
|
maxUploadSize: "1024 MB"
|
||||||
maxUploadSize: "1024 MB"
|
).frame(height: 100)
|
||||||
).frame(height: 100)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
//
|
|
||||||
// ProgressViewSubject.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 11/03/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class ProgressViewSubject: ObservableObject {
|
|
||||||
@Published var title: String
|
|
||||||
@Published var description: String?
|
|
||||||
@Published var progress: Double
|
|
||||||
|
|
||||||
init(title: String, description: String) {
|
|
||||||
self.title = title
|
|
||||||
self.description = description
|
|
||||||
self.progress = 0
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user