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