mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-09 12:43:01 +02:00
Compare commits
156 Commits
Author | SHA1 | Date | |
---|---|---|---|
3e319cd50f | |||
595dc8c028 | |||
f7b1679e97 | |||
9f1761d68e | |||
871480d70c | |||
2b1c1c12f8 | |||
a22346ed35 | |||
e3fa34d4f9 | |||
3d225ea79f | |||
d2cd387c18 | |||
48bb782e33 | |||
9710ffa8da | |||
46408f5ee5 | |||
2c39f1db8b | |||
f20286cbd9 | |||
f1fe42e563 | |||
94abfe4b49 | |||
9778fd5c7b | |||
dba2ce5bf3 | |||
4644c1ada4 | |||
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 | |||
20d19f2f92 | |||
91bc347e57 | |||
e05300b25b | |||
1ae7a20870 | |||
5594130ccd | |||
b9c7cdb3cc | |||
00b4760b85 | |||
9a35014d2a | |||
7cba25b52e | |||
c6c3996c7b | |||
03c96a1d16 | |||
a6fa4b240f | |||
7e78026d06 |
21
DEVELOPER.md
21
DEVELOPER.md
@@ -14,6 +14,17 @@ It also automatically runs when you try to build the project. You'll get a warni
|
|||||||
swiftlint --fix
|
swiftlint --fix
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📦 Swift Packages
|
||||||
|
|
||||||
|
Starting from PHP Monitor 7.1, the app now uses various first-party package dependencies.
|
||||||
|
|
||||||
|
The following package dependencies are in use:
|
||||||
|
|
||||||
|
* [`NVAppUpdater`](https://github.com/nicoverbruggen/NVAppUpdater)
|
||||||
|
* [`NVAlert`](https://github.com/nicoverbruggen/NVAlert)
|
||||||
|
|
||||||
|
You may need an internet connection to download these dependencies, or you can also clone the dependencies and include them manually.
|
||||||
|
|
||||||
## ⚙️ Preferences
|
## ⚙️ Preferences
|
||||||
|
|
||||||
You can find the persisted configuration file in `~/Library/Preferences/com.nicoverbruggen.phpmon.plist`
|
You can find the persisted configuration file in `~/Library/Preferences/com.nicoverbruggen.phpmon.plist`
|
||||||
@@ -33,16 +44,18 @@ defaults delete com.nicoverbruggen.phpmon && killall cfprefsd
|
|||||||
If you'd like to build PHP Monitor yourself, you need:
|
If you'd like to build PHP Monitor yourself, you need:
|
||||||
|
|
||||||
* Xcode (usually the latest version)
|
* Xcode (usually the latest version)
|
||||||
* *PHP Monitor Self-Updater.app* in the `phpmon-updater` directory (You can build it yourself, it is included as a target OR copy the signed app so it is included w/ PHP Monitor)
|
|
||||||
* The contents of this repository
|
|
||||||
|
|
||||||
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
|
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
|
||||||
|
|
||||||
|
**Important**: The updater now gets automatically built and included as part of the main target.
|
||||||
|
|
||||||
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
||||||
|
|
||||||
### PHP Monitor Updater
|
## ✅ Testing
|
||||||
|
|
||||||
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.
|
In order to properly test everything, you will want to use the _PHP Monitor DEV_ target. There are unit and UI tests both.
|
||||||
|
|
||||||
|
You may sporadically see failures in UI tests due to the following error: `Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY`. This seems to be an issue with Xcode that Apple may need to resolve?
|
||||||
|
|
||||||
## 🚀 Release procedure
|
## 🚀 Release procedure
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1610"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1610"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1610"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1610"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1610"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@@ -22,7 +22,7 @@ You can also add new domains as links, isolate sites, manage various services, a
|
|||||||
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
|
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
|
||||||
|
|
||||||
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
|
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
|
||||||
* macOS 12.4 or later (Monterey, Ventura and Sonoma are supported)
|
* macOS 12.4 or later
|
||||||
* Homebrew is installed in the default location (`/usr/local/homebrew` or `/opt/homebrew`)
|
* Homebrew is installed in the default location (`/usr/local/homebrew` or `/opt/homebrew`)
|
||||||
* Homebrew `php` formula is installed
|
* Homebrew `php` formula is installed
|
||||||
* Optional but recommended: Laravel Valet
|
* Optional but recommended: Laravel Valet
|
||||||
@@ -112,13 +112,15 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
|
|||||||
|
|
||||||
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
||||||
|
|
||||||
|
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
|
||||||
|
|
||||||
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
||||||
|
|
||||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
|
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.3.
|
||||||
|
|
||||||
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
||||||
|
|
||||||
|
13
SECURITY.md
13
SECURITY.md
@@ -4,18 +4,21 @@
|
|||||||
|
|
||||||
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
|
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
|
||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 6.1 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 7.1 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0+)<br/>Sequoia (15.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.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
|
||||||
## Legacy versions
|
## Legacy versions
|
||||||
|
|
||||||
These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
|
These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
|
||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 7.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 6.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.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
||||||
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 674 KiB After Width: | Height: | Size: 723 KiB |
@@ -1,46 +0,0 @@
|
|||||||
//
|
|
||||||
// LaunchControl.swift
|
|
||||||
// PHP Monitor Self-Updater
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 02/02/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
class LaunchControl {
|
|
||||||
public static func smartRestart(priority: [String]) async {
|
|
||||||
for appPath in priority {
|
|
||||||
if FileManager.default.fileExists(atPath: appPath) {
|
|
||||||
let app = await LaunchControl.startApplication(at: appPath)
|
|
||||||
if app != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func terminateApplications(bundleIds: [String]) async {
|
|
||||||
let runningApplications = NSWorkspace.shared.runningApplications
|
|
||||||
|
|
||||||
// Terminate all instances found
|
|
||||||
for id in bundleIds {
|
|
||||||
if let phpmon = runningApplications.first(where: {
|
|
||||||
(application) in return application.bundleIdentifier == id
|
|
||||||
}) {
|
|
||||||
phpmon.terminate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func startApplication(at path: String) async -> NSRunningApplication? {
|
|
||||||
await withCheckedContinuation { continuation in
|
|
||||||
let url = NSURL(fileURLWithPath: path, isDirectory: true) as URL
|
|
||||||
let configuration = NSWorkspace.OpenConfiguration()
|
|
||||||
NSWorkspace.shared.openApplication(at: url, configuration: configuration) { phpmon, error in
|
|
||||||
continuation.resume(returning: phpmon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,162 +0,0 @@
|
|||||||
//
|
|
||||||
// Updater.swift
|
|
||||||
// PHP Monitor Updater
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 01/02/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
class Updater: NSObject, NSApplicationDelegate {
|
|
||||||
|
|
||||||
var updaterDirectory: String = ""
|
|
||||||
var manifestPath: String = ""
|
|
||||||
var manifest: ReleaseManifest! = nil
|
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
||||||
Task { await self.installUpdate() }
|
|
||||||
}
|
|
||||||
|
|
||||||
func installUpdate() async {
|
|
||||||
print("PHP MONITOR SELF-UPDATER by Nico Verbruggen")
|
|
||||||
print("===========================================")
|
|
||||||
|
|
||||||
self.updaterDirectory = "~/.config/phpmon/updater"
|
|
||||||
.replacingOccurrences(of: "~", with: NSHomeDirectory())
|
|
||||||
|
|
||||||
print("Updater directory set to: \(self.updaterDirectory)")
|
|
||||||
|
|
||||||
self.manifestPath = "\(updaterDirectory)/update.json"
|
|
||||||
|
|
||||||
// Fetch the manifest on the local filesystem
|
|
||||||
let manifest = await parseManifest()!
|
|
||||||
|
|
||||||
// Download the latest file
|
|
||||||
let zipPath = await download(manifest)
|
|
||||||
|
|
||||||
// Terminate all instances of PHP Monitor first
|
|
||||||
await LaunchControl.terminateApplications(bundleIds: [
|
|
||||||
"com.nicoverbruggen.phpmon.eap",
|
|
||||||
"com.nicoverbruggen.phpmon.dev",
|
|
||||||
"com.nicoverbruggen.phpmon"
|
|
||||||
])
|
|
||||||
|
|
||||||
// Install the app based on the zip
|
|
||||||
let appPath = await extractAndInstall(zipPath: zipPath)
|
|
||||||
|
|
||||||
// Restart PHP Monitor, this will also close the updater
|
|
||||||
_ = await LaunchControl.startApplication(at: appPath)
|
|
||||||
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applicationWillTerminate(_ aNotification: Notification) {
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseManifest() async -> ReleaseManifest? {
|
|
||||||
// Read out the correct information from the manifest JSON
|
|
||||||
print("Checking manifest file at \(manifestPath)...")
|
|
||||||
|
|
||||||
do {
|
|
||||||
let manifestText = try String(contentsOfFile: manifestPath)
|
|
||||||
manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestText.data(using: .utf8)!)
|
|
||||||
return manifest
|
|
||||||
} catch {
|
|
||||||
print("Parsing the manifest failed (or the manifest file doesn't exist)!")
|
|
||||||
await Alert.show(description: "The manifest file for a potential update was not found. Please try searching for updates again in PHP Monitor.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func download(_ manifest: ReleaseManifest) async -> String {
|
|
||||||
// Remove all zips
|
|
||||||
system_quiet("rm -rf \(updaterDirectory)/*.zip")
|
|
||||||
|
|
||||||
// Download the file (and follow redirects + no output on failure)
|
|
||||||
system_quiet("cd \"\(updaterDirectory)\" && curl \(manifest.url) -fLO --max-time 20")
|
|
||||||
|
|
||||||
// Identify the downloaded file
|
|
||||||
let filename = system("cd \"\(updaterDirectory)\" && ls | grep .zip")
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
|
|
||||||
// Ensure the zip exists
|
|
||||||
if filename.isEmpty {
|
|
||||||
print("The update has not been downloaded. Sadly, that means that PHP Monitor cannot not updated!")
|
|
||||||
await Alert.show(description: "The update could not be downloaded, or the file was not correctly written to disk. \n\nPlease try again. \n\n(Note that the download will time-out after 20 seconds, so for slow connections it is recommended to manually download the update.)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the checksum for the downloaded file
|
|
||||||
let checksum = system("openssl dgst -sha256 \"\(updaterDirectory)/\(filename)\" | awk '{print $NF}'")
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
|
|
||||||
// Compare the checksums
|
|
||||||
print("""
|
|
||||||
Comparing checksums...
|
|
||||||
Expected SHA256: \(manifest.sha256)
|
|
||||||
Actual SHA256: \(checksum)
|
|
||||||
""")
|
|
||||||
|
|
||||||
// Make sure the checksum matches before we do anything with the file
|
|
||||||
if checksum != manifest.sha256 {
|
|
||||||
print("The checksums failed to match. Cancelling!")
|
|
||||||
await Alert.show(description: "The downloaded update failed checksum validation. Please try again. If this issue persists, there may be an issue with the server and I do not recommend upgrading.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the path to the zip
|
|
||||||
return "\(updaterDirectory)/\(filename)"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func extractAndInstall(zipPath: String) async -> String {
|
|
||||||
// Remove the directory that will contain the extracted update
|
|
||||||
system_quiet("rm -rf \"\(updaterDirectory)/extracted\"")
|
|
||||||
|
|
||||||
// Recreate the directory where we will unzip the .app file
|
|
||||||
system_quiet("mkdir -p \"\(updaterDirectory)/extracted\"")
|
|
||||||
|
|
||||||
// Make sure the updater directory exists
|
|
||||||
var isDirectory: ObjCBool = true
|
|
||||||
if !FileManager.default.fileExists(atPath: "\(updaterDirectory)/extracted", isDirectory: &isDirectory) {
|
|
||||||
await Alert.show(description: "The updater directory is missing. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unzip the file
|
|
||||||
system_quiet("unzip \"\(zipPath)\" -d \"\(updaterDirectory)/extracted\"")
|
|
||||||
|
|
||||||
// Find the .app file
|
|
||||||
let app = system("ls \"\(updaterDirectory)/extracted\" | grep .app")
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
|
|
||||||
print("Finished extracting: \(updaterDirectory)/extracted/\(app)")
|
|
||||||
|
|
||||||
// Make sure the file was extracted
|
|
||||||
if app.isEmpty {
|
|
||||||
await Alert.show(description: "The downloaded file could not be extracted. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the original app
|
|
||||||
print("Removing \(app) before replacing...")
|
|
||||||
system_quiet("rm -rf \"/Applications/\(app)\"")
|
|
||||||
|
|
||||||
// Move the new app in place
|
|
||||||
system_quiet("mv \"\(updaterDirectory)/extracted/\(app)\" \"/Applications/\(app)\"")
|
|
||||||
|
|
||||||
// Remove the zip
|
|
||||||
system_quiet("rm \"\(zipPath)\"")
|
|
||||||
|
|
||||||
// Remove the manifest
|
|
||||||
system_quiet("rm \"\(manifestPath)\"")
|
|
||||||
|
|
||||||
// Write a file that is only written when we upgraded successfully
|
|
||||||
system_quiet("touch \"\(updaterDirectory)/upgrade.success\"")
|
|
||||||
|
|
||||||
// Return the new location of the app
|
|
||||||
return "/Applications/\(app)"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,34 +0,0 @@
|
|||||||
//
|
|
||||||
// Utility.swift
|
|
||||||
// PHP Monitor Self-Updater
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 02/02/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
class Alert {
|
|
||||||
public static func show(description: String, shouldExit: Bool = true) async {
|
|
||||||
await withUnsafeContinuation { continuation in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = "The app could not be updated."
|
|
||||||
alert.informativeText = description
|
|
||||||
alert.addButton(withTitle: "OK")
|
|
||||||
alert.alertStyle = .critical
|
|
||||||
alert.runModal()
|
|
||||||
if shouldExit {
|
|
||||||
exit(0)
|
|
||||||
}
|
|
||||||
continuation.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ReleaseManifest: Codable {
|
|
||||||
let url: String
|
|
||||||
let sha256: String
|
|
||||||
}
|
|
@@ -7,8 +7,17 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAppUpdater
|
||||||
|
|
||||||
let app = NSApplication.shared
|
let delegate = SelfUpdater(
|
||||||
let delegate = Updater()
|
appName: "PHP Monitor",
|
||||||
app.delegate = delegate
|
bundleIdentifiers: [
|
||||||
|
"com.nicoverbruggen.phpmon.eap",
|
||||||
|
"com.nicoverbruggen.phpmon.dev",
|
||||||
|
"com.nicoverbruggen.phpmon"
|
||||||
|
],
|
||||||
|
selfUpdaterPath: "~/.config/phpmon/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
NSApplication.shared.delegate = delegate
|
||||||
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "1.000",
|
||||||
|
"green" : "1.000",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.172",
|
||||||
|
"green" : "0.182",
|
||||||
|
"red" : "0.182"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@@ -24,7 +24,7 @@
|
|||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.988",
|
"blue" : "0.988",
|
||||||
"green" : "0.723",
|
"green" : "0.444",
|
||||||
"red" : "0.277"
|
"red" : "0.277"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.300",
|
||||||
|
"blue" : "0.180",
|
||||||
|
"green" : "0.841",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.300",
|
||||||
|
"blue" : "0.426",
|
||||||
|
"green" : "0.809",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
25
phpmon/Assets.xcassets/php.imageset/Contents.json
vendored
Normal file
25
phpmon/Assets.xcassets/php.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "php.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true,
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
|
}
|
||||||
|
}
|
1
phpmon/Assets.xcassets/php.imageset/php.svg
vendored
Normal file
1
phpmon/Assets.xcassets/php.imageset/php.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 24 24"><path d="M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zm-2.595-1.382h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -14,14 +14,17 @@ class Actions {
|
|||||||
|
|
||||||
public static func linkPhp() async {
|
public static func linkPhp() async {
|
||||||
await brew("link php --overwrite --force")
|
await brew("link php --overwrite --force")
|
||||||
|
|
||||||
// TODO: Verify that this worked, if not, notify the user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func restartPhpFpm() async {
|
public static func restartPhpFpm() async {
|
||||||
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
|
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func restartPhpFpm(version: String) async {
|
||||||
|
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
|
||||||
|
await brew("services restart \(formula)", sudo: HomebrewFormulae.php.elevated)
|
||||||
|
}
|
||||||
|
|
||||||
public static func restartNginx() async {
|
public static func restartNginx() async {
|
||||||
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
|
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
|
||||||
}
|
}
|
||||||
|
@@ -19,10 +19,51 @@ struct Constants {
|
|||||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The PHP versions that are considered pre-release versions.
|
PHP Monitor supplies a hardcoded list of PHP packages in its own
|
||||||
|
PHP Version Manager.
|
||||||
|
|
||||||
|
This hardcoded list will expire and will need to be modified when
|
||||||
|
the cutoff date occurs, which is when the `php` formula will
|
||||||
|
become PHP 8.5, and a new build will need to be made.
|
||||||
|
|
||||||
|
If users launch an older version of the app, then a warning
|
||||||
|
will be displayed to let them know that certain operations
|
||||||
|
will not work correctly and that they need to update their app.
|
||||||
*/
|
*/
|
||||||
static let ExperimentalPhpVersions: Set = [
|
static let PhpFormulaeCutoffDate = "2025-11-30" // YYYY-MM-DD
|
||||||
"8.3", "8.4"
|
|
||||||
|
/**
|
||||||
|
* The PHP versions that are considered pre-release versions.
|
||||||
|
* Past a certain date, an experimental version "graduates"
|
||||||
|
* to a release version and is no longer marked as experimental.
|
||||||
|
*/
|
||||||
|
static var ExperimentalPhpVersions: Set<String> {
|
||||||
|
let releaseDates = [
|
||||||
|
"8.5": Date.fromString(Self.PhpFormulaeCutoffDate),
|
||||||
|
"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,8 @@ struct Constants {
|
|||||||
static let DetectedPhpVersions: Set = [
|
static let DetectedPhpVersions: Set = [
|
||||||
"5.6",
|
"5.6",
|
||||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3", "8.4",
|
||||||
"8.3",
|
"8.5" // DEV
|
||||||
"8.4"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,14 +90,13 @@ struct Constants {
|
|||||||
3: // Valet v3 dropped support for v5.6
|
3: // Valet v3 dropped support for v5.6
|
||||||
[
|
[
|
||||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3", "8.4"
|
||||||
"8.3", "8.4" // dev
|
|
||||||
],
|
],
|
||||||
4: // Valet v4 dropped support for v7.0
|
4: // Valet v4 dropped support for v7.0
|
||||||
[
|
[
|
||||||
"7.1", "7.2", "7.3", "7.4",
|
"7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3", "8.4",
|
||||||
"8.3", "8.4" // dev
|
"8.5" // DEV
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -73,6 +112,14 @@ struct Constants {
|
|||||||
string: "https://phpmon.app/faq"
|
string: "https://phpmon.app/faq"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
|
static let WikiPhpUnavailable = URL(
|
||||||
|
string: "https://phpmon.app/php-unavailable"
|
||||||
|
)!
|
||||||
|
|
||||||
|
static let WikiPhpUpgrade = URL(
|
||||||
|
string: "https://phpmon.app/php-upgrade"
|
||||||
|
)!
|
||||||
|
|
||||||
static let DonationPayment = URL(
|
static let DonationPayment = URL(
|
||||||
string: "https://phpmon.app/sponsor/now"
|
string: "https://phpmon.app/sponsor/now"
|
||||||
)!
|
)!
|
||||||
@@ -91,6 +138,8 @@ struct Constants {
|
|||||||
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
|
// EAP URLs
|
||||||
|
|
||||||
static let EarlyAccessCaskFile = URL(
|
static let EarlyAccessCaskFile = URL(
|
||||||
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
||||||
)!
|
)!
|
||||||
|
@@ -45,7 +45,6 @@ func grepContains(file: String, query: String) async -> Bool {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
Attempts to introduce sleep for a particular duration. Use with caution.
|
Attempts to introduce sleep for a particular duration. Use with caution.
|
||||||
Only intended for testing purposes.
|
|
||||||
*/
|
*/
|
||||||
func delay(seconds: Double) async {
|
func delay(seconds: Double) async {
|
||||||
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
||||||
|
@@ -17,6 +17,7 @@ public class Paths {
|
|||||||
|
|
||||||
internal var baseDir: Paths.HomebrewDir
|
internal var baseDir: Paths.HomebrewDir
|
||||||
private var userName: String
|
private var userName: String
|
||||||
|
private var preferredShell: String
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Assume the default directory is correct
|
// Assume the default directory is correct
|
||||||
@@ -31,9 +32,11 @@ public class Paths {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userName = identity()
|
userName = identity()
|
||||||
|
preferredShell = preferred_shell()
|
||||||
|
|
||||||
if !isRunningSwiftUIPreview {
|
if !isRunningSwiftUIPreview {
|
||||||
Log.info("The current username is `\(userName)`.")
|
Log.info("The current username is `\(userName)`.")
|
||||||
|
Log.info("The user's shell is `\(preferredShell)`.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,11 +102,23 @@ public class Paths {
|
|||||||
return "\(shared.baseDir.rawValue)/etc"
|
return "\(shared.baseDir.rawValue)/etc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var tapPath: String {
|
||||||
|
if shared.baseDir == .usr {
|
||||||
|
return "\(shared.baseDir.rawValue)/homebrew/Library/Taps"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(shared.baseDir.rawValue)/Library/Taps"
|
||||||
|
}
|
||||||
|
|
||||||
public static var caskroomPath: String {
|
public static var caskroomPath: String {
|
||||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var shell: String {
|
||||||
|
return shared.preferredShell
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Flexible Binaries
|
// MARK: - Flexible Binaries
|
||||||
// (these can be in multiple locations, so we scan common places because)
|
// (these can be in multiple locations, so we scan common places because)
|
||||||
// (PHP Monitor will not use the user's own PATH)
|
// (PHP Monitor will not use the user's own PATH)
|
||||||
|
@@ -8,6 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol AlertableError {
|
public protocol AlertableError {
|
||||||
func getErrorMessageKey() -> String
|
func getErrorMessageKey() -> String
|
||||||
}
|
}
|
||||||
|
@@ -15,4 +15,10 @@ extension Date {
|
|||||||
return dateFormatter.string(from: self)
|
return dateFormatter.string(from: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func fromString(_ string: String) -> Date? {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
return dateFormatter.date(from: string)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
23
phpmon/Common/Extensions/NVAlertExtension.swift
Normal file
23
phpmon/Common/Extensions/NVAlertExtension.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// NVAlertExtension.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 16/07/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
|
extension NVAlert {
|
||||||
|
/**
|
||||||
|
Shows the modal for a particular error.
|
||||||
|
*/
|
||||||
|
@MainActor public static func show(for error: Error & AlertableError) {
|
||||||
|
let key = error.getErrorMessageKey()
|
||||||
|
return NVAlert().withInformation(
|
||||||
|
title: "\(key).title".localized,
|
||||||
|
subtitle: "\(key).description".localized
|
||||||
|
).withPrimary(text: "generic.ok".localized).show()
|
||||||
|
}
|
||||||
|
}
|
@@ -8,6 +8,18 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct Localization {
|
struct Localization {
|
||||||
|
static var preferredLanguage: String? {
|
||||||
|
guard let language = Preferences.preferences[.languageOverride] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if language.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
|
||||||
static var bundle: Bundle = {
|
static var bundle: Bundle = {
|
||||||
if !isRunningTests {
|
if !isRunningTests {
|
||||||
return Bundle.main
|
return Bundle.main
|
||||||
@@ -32,7 +44,15 @@ struct Localization {
|
|||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
var localized: String {
|
var localized: String {
|
||||||
let string = NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
|
var preferredBundle: Bundle = Localization.bundle
|
||||||
|
|
||||||
|
if let preferred = Localization.preferredLanguage,
|
||||||
|
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
|
||||||
|
let bundle = Bundle(path: path) {
|
||||||
|
preferredBundle = bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
let string = NSLocalizedString(self, tableName: nil, bundle: preferredBundle, value: "", comment: "")
|
||||||
|
|
||||||
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
||||||
if string == self {
|
if string == self {
|
||||||
|
@@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
|
|||||||
// MARK: — FS Attributes
|
// MARK: — FS Attributes
|
||||||
|
|
||||||
func makeExecutable(_ path: String) throws {
|
func makeExecutable(_ path: String) throws {
|
||||||
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Checks
|
// MARK: - Checks
|
||||||
|
@@ -75,7 +75,7 @@ class MenuBarImageGenerator {
|
|||||||
|
|
||||||
// Then we'll fetch the image we want on the left
|
// Then we'll fetch the image we want on the left
|
||||||
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
||||||
if iconType == nil {
|
if iconType == nil || !MenuBarIcon.allCases.map({ $0.rawValue }).contains(iconType) {
|
||||||
Log.warn("Invalid icon type found, using the default")
|
Log.warn("Invalid icon type found, using the default")
|
||||||
iconType = MenuBarIcon.iconPhp.rawValue
|
iconType = MenuBarIcon.iconPhp.rawValue
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,8 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
App.shared.remove(window: windowName)
|
App.shared.remove(window: windowName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func windowDidResize(_ notification: Notification) {}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||||
}
|
}
|
||||||
@@ -37,7 +39,7 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
|
|
||||||
extension NSWindowController {
|
extension NSWindowController {
|
||||||
|
|
||||||
public func positionWindowInTopLeftCorner(offsetY: CGFloat = 0, offsetX: CGFloat = 0) {
|
public func positionWindowInTopRightCorner(offsetY: CGFloat = 0, offsetX: CGFloat = 0) {
|
||||||
guard let frame = NSScreen.main?.frame else { return }
|
guard let frame = NSScreen.main?.frame else { return }
|
||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
|
|
||||||
|
@@ -10,7 +10,6 @@ import Foundation
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
Run a simple blocking Shell command on the user's own system.
|
Run a simple blocking Shell command on the user's own system.
|
||||||
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
|
|
||||||
*/
|
*/
|
||||||
public func system(_ command: String) -> String {
|
public func system(_ command: String) -> String {
|
||||||
let task = Process()
|
let task = Process()
|
||||||
@@ -65,3 +64,11 @@ public func identity() -> String {
|
|||||||
|
|
||||||
return output.trimmingCharacters(in: .whitespacesAndNewlines)
|
return output.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Retrieves the user's preferred shell.
|
||||||
|
*/
|
||||||
|
public func preferred_shell() -> String {
|
||||||
|
return system("dscl . -read ~/ UserShell | sed 's/UserShell: //'")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// WIP.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 01/11/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
func todo(_ context: String = "") {
|
|
||||||
if !context.isEmpty {
|
|
||||||
fatalError("To be implemented: \(context)")
|
|
||||||
}
|
|
||||||
|
|
||||||
fatalError("To be implemented")
|
|
||||||
}
|
|
@@ -62,13 +62,6 @@ class ActivePhpInstallation {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load extension information
|
|
||||||
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
|
||||||
|
|
||||||
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
|
||||||
iniFiles.append(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get configuration values
|
// Get configuration values
|
||||||
limits = Limits(
|
limits = Limits(
|
||||||
memory_limit: getByteCount(key: "memory_limit"),
|
memory_limit: getByteCount(key: "memory_limit"),
|
||||||
@@ -76,14 +69,9 @@ class ActivePhpInstallation {
|
|||||||
post_max_size: getByteCount(key: "post_max_size")
|
post_max_size: getByteCount(key: "post_max_size")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Return a list of .ini files parsed after php.ini
|
let paths = ActiveShell.shared
|
||||||
let paths = Command.execute(
|
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
path: Paths.php,
|
.split(separator: "\n")
|
||||||
arguments: ["-r", "echo php_ini_scanned_files();"],
|
|
||||||
trimNewlines: false
|
|
||||||
)
|
|
||||||
.replacingOccurrences(of: "\n", with: "")
|
|
||||||
.split(separator: ",")
|
|
||||||
.map { String($0) }
|
.map { String($0) }
|
||||||
|
|
||||||
// See if any extensions are present in said .ini files
|
// See if any extensions are present in said .ini files
|
||||||
|
@@ -13,7 +13,7 @@ class PhpEnvironments {
|
|||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loads the currently active PHP installation upon startup. May be empty.
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
self.currentInstall = ActivePhpInstallation.load()
|
self.currentInstall = ActivePhpInstallation.load()
|
||||||
@@ -29,7 +29,7 @@ class PhpEnvironments {
|
|||||||
/**
|
/**
|
||||||
Determine which PHP version the `php` formula is aliased to.
|
Determine which PHP version the `php` formula is aliased to.
|
||||||
*/
|
*/
|
||||||
func determinePhpAlias() async {
|
@MainActor func determinePhpAlias() async {
|
||||||
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
||||||
|
|
||||||
self.homebrewPackage = try! JSONDecoder().decode(
|
self.homebrewPackage = try! JSONDecoder().decode(
|
||||||
@@ -37,7 +37,28 @@ class PhpEnvironments {
|
|||||||
from: brewPhpAlias.data(using: .utf8)!
|
from: brewPhpAlias.data(using: .utf8)!
|
||||||
).first!
|
).first!
|
||||||
|
|
||||||
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!")
|
PhpEnvironments.brewPhpAlias = self.homebrewPackage.version
|
||||||
|
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).")
|
||||||
|
|
||||||
|
// Check if that version actually corresponds to an older version
|
||||||
|
let phpConfigExecutablePath = "\(Paths.optPath)/php/bin/php-config"
|
||||||
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
|
let longVersionString = Command.execute(
|
||||||
|
path: phpConfigExecutablePath,
|
||||||
|
arguments: ["--version"],
|
||||||
|
trimNewlines: false
|
||||||
|
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
if let version = try? VersionNumber.parse(longVersionString) {
|
||||||
|
PhpEnvironments.brewPhpAlias = version.short
|
||||||
|
if version.short != homebrewPackage.version {
|
||||||
|
Log.info("[BREW] An older version of `php` is actually installed (\(version.short)).")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not determine the actual version of the php binary; assuming Homebrew is correct.")
|
||||||
|
PhpEnvironments.brewPhpAlias = homebrewPackage.version
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
@@ -49,14 +70,12 @@ class PhpEnvironments {
|
|||||||
static let shared = PhpEnvironments()
|
static let shared = PhpEnvironments()
|
||||||
|
|
||||||
/** Whether the switcher is busy performing any actions. */
|
/** Whether the switcher is busy performing any actions. */
|
||||||
var isBusy: Bool = false {
|
@MainActor var isBusy: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
Task { @MainActor in
|
MainMenu.shared.refreshIcon()
|
||||||
MainMenu.shared.setBusyImage()
|
|
||||||
MainMenu.shared.rebuild()
|
MainMenu.shared.rebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** All versions of PHP that are currently supported. */
|
/** All versions of PHP that are currently supported. */
|
||||||
var availablePhpVersions: [String] = []
|
var availablePhpVersions: [String] = []
|
||||||
@@ -68,7 +87,14 @@ class PhpEnvironments {
|
|||||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||||
|
|
||||||
/** Information about the currently linked PHP installation. */
|
/** Information about the currently linked PHP installation. */
|
||||||
var currentInstall: ActivePhpInstallation?
|
var currentInstall: ActivePhpInstallation? {
|
||||||
|
didSet {
|
||||||
|
// Let the PHP extension manager, if it exists, know the version changed
|
||||||
|
if let version = currentInstall?.version.short {
|
||||||
|
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The version that the `php` formula via Brew is aliased to on the current system.
|
The version that the `php` formula via Brew is aliased to on the current system.
|
||||||
@@ -79,7 +105,12 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
As such, we take that information from Homebrew.
|
As such, we take that information from Homebrew.
|
||||||
*/
|
*/
|
||||||
static var brewPhpAlias: String {
|
static var brewPhpAlias: String = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
It's possible for the alias to be newer than the actual installed version of PHP.
|
||||||
|
*/
|
||||||
|
static var homebrewBrewPhpAlias: String {
|
||||||
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
||||||
|
|
||||||
return PhpEnvironments.shared.homebrewPackage.version
|
return PhpEnvironments.shared.homebrewPackage.version
|
||||||
@@ -146,8 +177,13 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
// Avoid inserting a duplicate
|
// Avoid inserting a duplicate
|
||||||
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||||
|
let phpAliasInstall = PhpInstallation(phpAlias)
|
||||||
|
// Before inserting, ensure that the actual output matches the alias
|
||||||
|
// if that isn't the case, our formula remains out-of-date
|
||||||
|
if !phpAliasInstall.isMissingBinary {
|
||||||
supportedVersions.insert(phpAlias)
|
supportedVersions.insert(phpAlias)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
availablePhpVersions = Array(supportedVersions)
|
availablePhpVersions = Array(supportedVersions)
|
||||||
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
|
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
|
||||||
|
@@ -49,8 +49,10 @@ class PhpHelper {
|
|||||||
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
|
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
|
||||||
.resolvingSymlinksInPath().path
|
.resolvingSymlinksInPath().path
|
||||||
|
|
||||||
// The contents of the script!
|
// Check if the user uses Fish
|
||||||
let script = script(path, keyPhrase, version, dotless)
|
let script = Paths.shell.contains("/fish")
|
||||||
|
? fishScript(path, keyPhrase, version, dotless)
|
||||||
|
: zshScript(path, keyPhrase, version, dotless)
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try FileSystem.writeAtomicallyToFile(destination, content: script)
|
try FileSystem.writeAtomicallyToFile(destination, content: script)
|
||||||
@@ -78,7 +80,7 @@ class PhpHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func script(
|
private static func zshScript(
|
||||||
_ path: String,
|
_ path: String,
|
||||||
_ keyPhrase: String,
|
_ keyPhrase: String,
|
||||||
_ version: String,
|
_ version: String,
|
||||||
@@ -96,6 +98,22 @@ class PhpHelper {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func fishScript(
|
||||||
|
_ path: String,
|
||||||
|
_ keyPhrase: String,
|
||||||
|
_ version: String,
|
||||||
|
_ dotless: String
|
||||||
|
) -> String {
|
||||||
|
return """
|
||||||
|
#!\(Paths.binPath)/fish
|
||||||
|
# \(keyPhrase)
|
||||||
|
# It reflects the location of PHP \(version)'s binaries on your system.
|
||||||
|
# Usage: . pm\(dotless)
|
||||||
|
echo "PHP Monitor has enabled this terminal to use PHP \(version)."; \\
|
||||||
|
set -x PATH \(path) $PATH
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
private static func createSymlink(_ dotless: String) async {
|
private static func createSymlink(_ dotless: String) async {
|
||||||
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
||||||
let destination = "/usr/local/bin/pm\(dotless)"
|
let destination = "/usr/local/bin/pm\(dotless)"
|
||||||
|
@@ -69,8 +69,9 @@ class PhpConfigurationFile: CreatedFromFile {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReplacementErrors: Error {
|
public enum ReplacementErrors: Error {
|
||||||
case missingKey
|
case missingKey
|
||||||
|
case missingFile
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,10 +96,16 @@ class PhpConfigurationFile: CreatedFromFile {
|
|||||||
// Replace the specific line
|
// Replace the specific line
|
||||||
self.lines[item.lineIndex] = components.joined(separator: "=")
|
self.lines[item.lineIndex] = components.joined(separator: "=")
|
||||||
|
|
||||||
|
// Ensure the watchers aren't tripped up by config changes
|
||||||
|
ConfigWatchManager.ignoresModificationsToConfigValues = true
|
||||||
|
|
||||||
// Finally, join the string and save the file atomatically again
|
// Finally, join the string and save the file atomatically again
|
||||||
try self.lines.joined(separator: "\n")
|
try self.lines.joined(separator: "\n")
|
||||||
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
|
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
// Ensure watcher behaviour is reverted
|
||||||
|
ConfigWatchManager.ignoresModificationsToConfigValues = false
|
||||||
|
|
||||||
// Reload the original file
|
// Reload the original file
|
||||||
self.reload()
|
self.reload()
|
||||||
}
|
}
|
||||||
|
@@ -67,7 +67,7 @@ class PhpExtension {
|
|||||||
|
|
||||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||||
|
|
||||||
self.enabled = !line.contains(";")
|
self.enabled = !line.starts(with: ";")
|
||||||
self.file = file
|
self.file = file
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ class PhpExtension {
|
|||||||
You may need to restart the other services in order for this change to apply.
|
You may need to restart the other services in order for this change to apply.
|
||||||
*/
|
*/
|
||||||
func toggle() async {
|
func toggle() async {
|
||||||
let newLine = enabled
|
let newLine = !line.starts(with: ";")
|
||||||
// DISABLED: Commented out line
|
// DISABLED: Commented out line
|
||||||
? "; \(line)"
|
? "; \(line)"
|
||||||
// ENABLED: Line where the comment delimiter (;) is removed
|
// ENABLED: Line where the comment delimiter (;) is removed
|
||||||
@@ -84,14 +84,14 @@ class PhpExtension {
|
|||||||
|
|
||||||
await sed(file: file, original: line, replacement: newLine)
|
await sed(file: file, original: line, replacement: newLine)
|
||||||
|
|
||||||
enabled.toggle()
|
self.enabled = !newLine.starts(with: ";")
|
||||||
|
self.line = newLine
|
||||||
|
|
||||||
if !isRunningTests {
|
if !isRunningTests {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
MainMenu.shared.rebuild()
|
MainMenu.shared.rebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Static Methods
|
// MARK: - Static Methods
|
||||||
|
@@ -12,19 +12,48 @@ class PhpInstallation {
|
|||||||
|
|
||||||
var versionNumber: VersionNumber
|
var versionNumber: VersionNumber
|
||||||
|
|
||||||
|
var iniFiles: [PhpConfigurationFile] = []
|
||||||
|
|
||||||
|
var isPreRelease: Bool = false
|
||||||
|
|
||||||
|
var isMissingBinary: Bool = false
|
||||||
|
|
||||||
var isHealthy: Bool = true
|
var isHealthy: Bool = true
|
||||||
|
|
||||||
|
var extensions: [PhpExtension] {
|
||||||
|
return self.iniFiles.flatMap({ $0.extensions })
|
||||||
|
}
|
||||||
|
|
||||||
|
var formulaName: String {
|
||||||
|
let version = self.versionNumber.short
|
||||||
|
|
||||||
|
if version == PhpEnvironments.brewPhpAlias {
|
||||||
|
return "php"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "php@\(self.versionNumber.short)"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
In order to determine details about a PHP installation,
|
In order to determine details about a PHP installation,
|
||||||
we’ll simply run `php-config --version` in the relevant directory.
|
we’ll simply run `php-config --version` in the relevant directory.
|
||||||
*/
|
*/
|
||||||
init(_ version: String) {
|
init(_ version: String) {
|
||||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
|
||||||
|
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||||
|
|
||||||
let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
versionNumber = VersionNumber.make(from: version)!
|
||||||
|
|
||||||
self.versionNumber = VersionNumber.make(from: version)!
|
determineVersion(phpConfigExecutablePath, phpExecutablePath)
|
||||||
|
determineHealth(phpExecutablePath)
|
||||||
|
determineIniFiles(phpExecutablePath)
|
||||||
|
|
||||||
|
// Find all enabled extensions
|
||||||
|
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
|
||||||
|
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpConfigExecutablePath) {
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
let longVersionString = Command.execute(
|
let longVersionString = Command.execute(
|
||||||
path: phpConfigExecutablePath,
|
path: phpConfigExecutablePath,
|
||||||
@@ -32,11 +61,21 @@ class PhpInstallation {
|
|||||||
trimNewlines: false
|
trimNewlines: false
|
||||||
).trimmingCharacters(in: .whitespacesAndNewlines)
|
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
// The parser should always work, or the string has to be very unusual.
|
if longVersionString.contains("-dev") {
|
||||||
// If so, the app SHOULD crash, so that the users report what's up.
|
isPreRelease = true
|
||||||
self.versionNumber = try! VersionNumber.parse(longVersionString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
versionNumber = try! VersionNumber.parse(longVersionString)
|
||||||
|
} else {
|
||||||
|
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
|
||||||
|
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
|
||||||
|
isMissingBinary = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineHealth(_ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpExecutablePath) {
|
if FileSystem.fileExists(phpExecutablePath) {
|
||||||
let testCommand = Command.execute(
|
let testCommand = Command.execute(
|
||||||
path: phpExecutablePath,
|
path: phpExecutablePath,
|
||||||
@@ -53,4 +92,18 @@ class PhpInstallation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func determineIniFiles(_ phpExecutablePath: String) {
|
||||||
|
let paths = ActiveShell.shared
|
||||||
|
.sync("\(phpExecutablePath) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
|
.split(separator: "\n")
|
||||||
|
.map { String($0) }
|
||||||
|
|
||||||
|
// See if any extensions are present in said .ini files
|
||||||
|
paths.forEach { (iniFilePath) in
|
||||||
|
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
|
||||||
|
iniFiles.append(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@ extension InternalSwitcher {
|
|||||||
return corrections.contains(true)
|
return corrections.contains(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - PHP FPM pool
|
// MARK: - Corrections
|
||||||
|
|
||||||
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
||||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||||
@@ -54,37 +54,7 @@ extension InternalSwitcher {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||||
return [
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/php-fpm.d/valet-fpm.conf",
|
|
||||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
|
||||||
replacements: [
|
|
||||||
"VALET_USER": Paths.whoami,
|
|
||||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
|
||||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
|
||||||
],
|
|
||||||
applies: { Valet.shared.version!.major > 2 }
|
|
||||||
),
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/conf.d/error_log.ini",
|
|
||||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
|
||||||
replacements: [
|
|
||||||
"VALET_USER": Paths.whoami,
|
|
||||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
|
||||||
],
|
|
||||||
applies: { return true }
|
|
||||||
),
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/conf.d/php-memory-limits.ini",
|
|
||||||
source: "/cli/stubs/php-memory-limits.ini",
|
|
||||||
replacements: [:],
|
|
||||||
applies: { return true }
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
|
||||||
let files = self.getExpectedConfigurationFiles(for: version)
|
let files = self.getExpectedConfigurationFiles(for: version)
|
||||||
|
|
||||||
// For each of the files, attempt to fix anything that is wrong
|
// For each of the files, attempt to fix anything that is wrong
|
||||||
@@ -124,6 +94,38 @@ extension InternalSwitcher {
|
|||||||
return outcomes.contains(true)
|
return outcomes.contains(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Internals
|
||||||
|
|
||||||
|
private func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||||
|
return [
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/php-fpm.d/valet-fpm.conf",
|
||||||
|
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||||
|
replacements: [
|
||||||
|
"VALET_USER": Paths.whoami,
|
||||||
|
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||||
|
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||||
|
],
|
||||||
|
applies: { Valet.shared.version!.major > 2 }
|
||||||
|
),
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/conf.d/error_log.ini",
|
||||||
|
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||||
|
replacements: [
|
||||||
|
"VALET_USER": Paths.whoami,
|
||||||
|
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||||
|
],
|
||||||
|
applies: { return true }
|
||||||
|
),
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/conf.d/php-memory-limits.ini",
|
||||||
|
source: "/cli/stubs/php-memory-limits.ini",
|
||||||
|
replacements: [:],
|
||||||
|
applies: { return true }
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ExpectedConfigurationFile {
|
public struct ExpectedConfigurationFile {
|
||||||
|
@@ -8,9 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Process: @unchecked Sendable {}
|
|
||||||
extension Timer: @unchecked Sendable {}
|
|
||||||
|
|
||||||
class RealShell: ShellProtocol {
|
class RealShell: ShellProtocol {
|
||||||
/**
|
/**
|
||||||
The launch path of the terminal in question that is used.
|
The launch path of the terminal in question that is used.
|
||||||
@@ -86,14 +83,37 @@ class RealShell: ShellProtocol {
|
|||||||
|
|
||||||
// MARK: - Shellable Protocol
|
// MARK: - Shellable Protocol
|
||||||
|
|
||||||
|
func sync(_ command: String) -> ShellOutput {
|
||||||
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
|
let outputPipe = Pipe()
|
||||||
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
|
sleep(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
task.standardOutput = outputPipe
|
||||||
|
task.standardError = errorPipe
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
|
||||||
|
if Log.shared.verbosity == .cli {
|
||||||
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
func pipe(_ command: String) async -> ShellOutput {
|
func pipe(_ command: String) async -> ShellOutput {
|
||||||
let task = getShellProcess(for: command)
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
let outputPipe = Pipe()
|
let outputPipe = Pipe()
|
||||||
let errorPipe = Pipe()
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
// Seriously slow down how long it takes for the shell to return output
|
|
||||||
// (in order to debug or identify async issues)
|
|
||||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
Log.info("[SLOW SHELL] \(command)")
|
Log.info("[SLOW SHELL] \(command)")
|
||||||
await delay(seconds: 3.0)
|
await delay(seconds: 3.0)
|
||||||
@@ -104,17 +124,17 @@ class RealShell: ShellProtocol {
|
|||||||
task.launch()
|
task.launch()
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
|
|
||||||
let stdOut = String(
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
let stdErr = String(
|
|
||||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
if Log.shared.verbosity == .cli {
|
if Log.shared.verbosity == .cli {
|
||||||
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func log(task: Process, stdOut: String, stdErr: String) {
|
||||||
var args = task.arguments ?? []
|
var args = task.arguments ?? []
|
||||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||||
var log = """
|
var log = """
|
||||||
@@ -141,9 +161,6 @@ class RealShell: ShellProtocol {
|
|||||||
Log.info(log)
|
Log.info(log)
|
||||||
}
|
}
|
||||||
|
|
||||||
return .out(stdOut, stdErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func quiet(_ command: String) async {
|
func quiet(_ command: String) async {
|
||||||
_ = await self.pipe(command)
|
_ = await self.pipe(command)
|
||||||
}
|
}
|
||||||
@@ -164,25 +181,26 @@ class RealShell: ShellProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return try await withCheckedThrowingContinuation({ continuation in
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
let timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in
|
let task = Task {
|
||||||
|
try await Task.sleep(nanoseconds: timeout.nanoseconds)
|
||||||
// Only terminate if the process is still running
|
// Only terminate if the process is still running
|
||||||
if process.isRunning {
|
if process.isRunning {
|
||||||
process.terminationHandler = nil
|
process.terminationHandler = nil
|
||||||
process.terminate()
|
process.terminate()
|
||||||
return continuation.resume(throwing: ShellError.timedOut)
|
continuation.resume(throwing: ShellError.timedOut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.terminationHandler = { [timer, output] process in
|
process.terminationHandler = { [output] process in
|
||||||
timer.invalidate()
|
task.cancel()
|
||||||
|
|
||||||
process.haltListening()
|
process.haltListening()
|
||||||
|
|
||||||
if !output.err.isEmpty {
|
if !output.err.isEmpty {
|
||||||
return continuation.resume(returning: (process, .err(output.err)))
|
continuation.resume(returning: (process, .err(output.err)))
|
||||||
|
} else {
|
||||||
|
continuation.resume(returning: (process, .out(output.out)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return continuation.resume(returning: (process, .out(output.out)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.launch()
|
process.launch()
|
||||||
@@ -190,3 +208,9 @@ class RealShell: ShellProtocol {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimeInterval {
|
||||||
|
var nanoseconds: UInt64 {
|
||||||
|
return UInt64(self * 1_000_000_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -14,6 +14,16 @@ protocol ShellProtocol {
|
|||||||
*/
|
*/
|
||||||
var PATH: String { get }
|
var PATH: String { get }
|
||||||
|
|
||||||
|
/**
|
||||||
|
Run a command synchronously. Use with caution.
|
||||||
|
|
||||||
|
Common usage:
|
||||||
|
```
|
||||||
|
let output = Shell.sync("php -v")
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
func sync(_ command: String) -> ShellOutput
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Run a command asynchronously.
|
Run a command asynchronously.
|
||||||
Returns the most relevant output (prefers error output if it exists).
|
Returns the most relevant output (prefers error output if it exists).
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// PhpFormulaeStatus.swift
|
// BusyStatus.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 02/05/2023.
|
// Created by Nico Verbruggen on 02/05/2023.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PhpFormulaeStatus: ObservableObject {
|
class BusyStatus: ObservableObject {
|
||||||
@Published var busy: Bool
|
@Published var busy: Bool
|
||||||
@Published var title: String
|
@Published var title: String
|
||||||
@Published var description: String
|
@Published var description: String
|
||||||
@@ -18,4 +18,12 @@ class PhpFormulaeStatus: ObservableObject {
|
|||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func notBusy() -> BusyStatus {
|
||||||
|
return BusyStatus(busy: false, title: "", description: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func busy() -> BusyStatus {
|
||||||
|
return BusyStatus(busy: false, title: "", description: "")
|
||||||
|
}
|
||||||
}
|
}
|
@@ -43,6 +43,7 @@ public struct TestableConfiguration: Codable {
|
|||||||
private var primaryPhpVersion: VersionNumber?
|
private var primaryPhpVersion: VersionNumber?
|
||||||
private var secondaryPhpVersions: [VersionNumber] = []
|
private var secondaryPhpVersions: [VersionNumber] = []
|
||||||
|
|
||||||
|
// swiftlint:disable function_body_length
|
||||||
mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) {
|
mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) {
|
||||||
if primary {
|
if primary {
|
||||||
if primaryPhpVersion != nil {
|
if primaryPhpVersion != nil {
|
||||||
@@ -72,12 +73,26 @@ public struct TestableConfiguration: Codable {
|
|||||||
: .fake(.text)
|
: .fake(.text)
|
||||||
]) { (_, new) in new }
|
]) { (_, new) in new }
|
||||||
|
|
||||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"]
|
// PHP configuration files
|
||||||
= version.long
|
self.shellOutput["/opt/homebrew/opt/php@\(version.short)/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||||
|
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||||
|
|
||||||
|
// PHP Homebrew operations
|
||||||
|
self.shellOutput["/opt/homebrew/bin/brew unlink php@\(version.short)"] = .delayed(0.2, "OK")
|
||||||
|
self.shellOutput["sudo /opt/homebrew/bin/brew services stop php@\(version.short)"] = .delayed(0.2, "OK")
|
||||||
|
self.shellOutput["sudo /opt/homebrew/bin/brew services start php@\(version.short)"] = .delayed(0.2, "OK")
|
||||||
|
self.shellOutput["/opt/homebrew/bin/brew link php@\(version.short) --overwrite --force"] = .delayed(0.2, "OK")
|
||||||
|
|
||||||
|
// PHP version output
|
||||||
|
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"] = version.long
|
||||||
|
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php -v"] = "OK"
|
||||||
|
|
||||||
if primary {
|
if primary {
|
||||||
self.shellOutput["ls /opt/homebrew/opt | grep php"]
|
// Files expected to be present for currently linked PHP version
|
||||||
= .instant("php")
|
self.shellOutput["ls /opt/homebrew/opt | grep php"] =
|
||||||
|
.instant("php")
|
||||||
|
self.shellOutput["/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||||
|
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||||
self.filesystem["/opt/homebrew/opt/php"]
|
self.filesystem["/opt/homebrew/opt/php"]
|
||||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
||||||
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
||||||
@@ -88,12 +103,8 @@ public struct TestableConfiguration: Codable {
|
|||||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
|
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
|
||||||
self.commandOutput["/opt/homebrew/bin/php-config --version"]
|
self.commandOutput["/opt/homebrew/bin/php-config --version"]
|
||||||
= version.long
|
= version.long
|
||||||
self.commandOutput["/opt/homebrew/bin/php -r echo php_ini_scanned_files();"] =
|
|
||||||
"""
|
|
||||||
/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini,
|
|
||||||
"""
|
|
||||||
} else {
|
} else {
|
||||||
|
// Output expected to be present for non-linked PHP versions
|
||||||
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
||||||
BatchFakeShellOutput.instant(
|
BatchFakeShellOutput.instant(
|
||||||
self.secondaryPhpVersions
|
self.secondaryPhpVersions
|
||||||
@@ -102,6 +113,7 @@ public struct TestableConfiguration: Codable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// swiftlint:enable function_body_length
|
||||||
|
|
||||||
// MARK: Interactions
|
// MARK: Interactions
|
||||||
|
|
||||||
|
@@ -18,11 +18,11 @@ class TestableFileSystem: FileSystemProtocol {
|
|||||||
self.files = files
|
self.files = files
|
||||||
|
|
||||||
// Ensure that each of the ~ characters are replaced with the home directory path
|
// Ensure that each of the ~ characters are replaced with the home directory path
|
||||||
for key in self.files.keys where key.contains("~") {
|
accessQueue.sync {
|
||||||
self.files.renameKey(
|
for (key, value) in files {
|
||||||
fromKey: key,
|
let adjustedKey = key.contains("~") ? key.replacingOccurrences(of: "~", with: self.homeDirectory) : key
|
||||||
toKey: key.replacingOccurrences(of: "~", with: self.homeDirectory)
|
self.files[adjustedKey] = value
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that intermediate directories are created
|
// Ensure that intermediate directories are created
|
||||||
@@ -46,11 +46,17 @@ class TestableFileSystem: FileSystemProtocol {
|
|||||||
*/
|
*/
|
||||||
private(set) var homeDirectory = "/Users/fake"
|
private(set) var homeDirectory = "/Users/fake"
|
||||||
|
|
||||||
|
/**
|
||||||
|
Serial dispatch queue for ensuring thread-safe access to the `files` dictionary.
|
||||||
|
*/
|
||||||
|
private let accessQueue = DispatchQueue(label: "com.testablefilesystem.accessQueue")
|
||||||
|
|
||||||
// MARK: - Basics
|
// MARK: - Basics
|
||||||
|
|
||||||
func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws {
|
func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
try accessQueue.sync {
|
||||||
if files[path] != nil {
|
if files[path] != nil {
|
||||||
throw TestableFileSystemError.alreadyExists
|
throw TestableFileSystemError.alreadyExists
|
||||||
}
|
}
|
||||||
@@ -59,26 +65,31 @@ class TestableFileSystem: FileSystemProtocol {
|
|||||||
|
|
||||||
self.files[path] = .fake(.directory)
|
self.files[path] = .fake(.directory)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writeAtomicallyToFile(_ path: String, content: String) throws {
|
func writeAtomicallyToFile(_ path: String, content: String) throws {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
try accessQueue.sync {
|
||||||
if files[path] != nil {
|
if files[path] != nil {
|
||||||
throw TestableFileSystemError.alreadyExists
|
throw TestableFileSystemError.alreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
self.files[path] = .fake(.text, content)
|
self.files[path] = .fake(.text, content)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getStringFromFile(_ path: String) throws -> String {
|
func getStringFromFile(_ path: String) throws -> String {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
return try accessQueue.sync {
|
||||||
guard let file = files[path] else {
|
guard let file = files[path] else {
|
||||||
throw TestableFileSystemError.fileMissing
|
throw TestableFileSystemError.fileMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
return file.content ?? ""
|
return file.content ?? ""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
|
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
@@ -88,15 +99,18 @@ class TestableFileSystem: FileSystemProtocol {
|
|||||||
seek = "\(seek)/"
|
seek = "\(seek)/"
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.files.keys
|
return accessQueue.sync {
|
||||||
|
self.files.keys
|
||||||
.filter { $0.hasPrefix(seek) }
|
.filter { $0.hasPrefix(seek) }
|
||||||
.map { $0.replacingOccurrences(of: seek, with: "") }
|
.map { $0.replacingOccurrences(of: seek, with: "") }
|
||||||
.filter { !$0.contains("/") }
|
.filter { !$0.contains("/") }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getDestinationOfSymlink(_ path: String) throws -> String {
|
func getDestinationOfSymlink(_ path: String) throws -> String {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
return try accessQueue.sync {
|
||||||
guard let file = files[path] else {
|
guard let file = files[path] else {
|
||||||
throw TestableFileSystemError.fileMissing
|
throw TestableFileSystemError.fileMissing
|
||||||
}
|
}
|
||||||
@@ -115,6 +129,7 @@ class TestableFileSystem: FileSystemProtocol {
|
|||||||
|
|
||||||
return pathToSymlink
|
return pathToSymlink
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Move & Delete Files
|
// MARK: - Move & Delete Files
|
||||||
|
|
||||||
@@ -122,6 +137,7 @@ class TestableFileSystem: FileSystemProtocol {
|
|||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
let newPath = newPath.replacingTildeWithHomeDirectory
|
let newPath = newPath.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
accessQueue.sync {
|
||||||
self.files.keys.forEach { key in
|
self.files.keys.forEach { key in
|
||||||
if key.hasPrefix(path) {
|
if key.hasPrefix(path) {
|
||||||
self.files.renameKey(
|
self.files.renameKey(
|
||||||
@@ -133,8 +149,10 @@ class TestableFileSystem: FileSystemProtocol {
|
|||||||
|
|
||||||
self.files.renameKey(fromKey: path, toKey: newPath)
|
self.files.renameKey(fromKey: path, toKey: newPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func remove(_ path: String) throws {
|
func remove(_ path: String) throws {
|
||||||
|
accessQueue.sync {
|
||||||
// Remove recursively
|
// Remove recursively
|
||||||
self.files.keys.forEach { key in
|
self.files.keys.forEach { key in
|
||||||
if key.hasPrefix(path) {
|
if key.hasPrefix(path) {
|
||||||
@@ -144,110 +162,127 @@ class TestableFileSystem: FileSystemProtocol {
|
|||||||
|
|
||||||
self.files.removeValue(forKey: path)
|
self.files.removeValue(forKey: path)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: — Attributes
|
// MARK: — Attributes
|
||||||
|
|
||||||
func makeExecutable(_ path: String) throws {
|
func makeExecutable(_ path: String) throws {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
try accessQueue.sync {
|
||||||
guard let file = files[path] else {
|
guard let file = files[path] else {
|
||||||
throw TestableFileSystemError.fileMissing
|
throw TestableFileSystemError.fileMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
file.type = .binary
|
file.type = .binary
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Checks
|
// MARK: - Checks
|
||||||
|
|
||||||
func isExecutableFile(_ path: String) -> Bool {
|
func isExecutableFile(_ path: String) -> Bool {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
return accessQueue.sync {
|
||||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return file.type == .binary
|
return file.type == .binary
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isWriteableFile(_ path: String) -> Bool {
|
func isWriteableFile(_ path: String) -> Bool {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
return accessQueue.sync {
|
||||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return !file.readOnly
|
return !file.readOnly
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func anyExists(_ path: String) -> Bool {
|
func anyExists(_ path: String) -> Bool {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
return files.keys.contains(path)
|
return accessQueue.sync {
|
||||||
|
files.keys.contains(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileExists(_ path: String) -> Bool {
|
func fileExists(_ path: String) -> Bool {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
return accessQueue.sync {
|
||||||
guard let file = files[path] else {
|
guard let file = files[path] else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.binary, .symlink, .text].contains(file.type)
|
return [.binary, .symlink, .text].contains(file.type)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func directoryExists(_ path: String) -> Bool {
|
func directoryExists(_ path: String) -> Bool {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
return accessQueue.sync {
|
||||||
guard let file = files[path] else {
|
guard let file = files[path] else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return [.directory].contains(file.type)
|
return [.directory].contains(file.type)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isSymlink(_ path: String) -> Bool {
|
func isSymlink(_ path: String) -> Bool {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
return accessQueue.sync {
|
||||||
guard let file = files[path] else {
|
guard let file = files[path] else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return file.type == .symlink
|
return file.type == .symlink
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isDirectory(_ path: String) -> Bool {
|
func isDirectory(_ path: String) -> Bool {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
|
return accessQueue.sync {
|
||||||
guard let file = files[path] else {
|
guard let file = files[path] else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return file.type == .directory
|
return file.type == .directory
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func printContents() {
|
public func printContents() {
|
||||||
|
accessQueue.sync {
|
||||||
for key in self.files.keys.sorted() {
|
for key in self.files.keys.sorted() {
|
||||||
print("\(key) -> \(self.files[key]!.type)")
|
print("\(key) -> \(self.files[key]!.type)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func createIntermediateDirectories(_ path: String) {
|
private func createIntermediateDirectories(_ path: String) {
|
||||||
let path = path.replacingTildeWithHomeDirectory
|
let path = path.replacingTildeWithHomeDirectory
|
||||||
|
|
||||||
let items = path.components(separatedBy: "/")
|
let items = path.components(separatedBy: "/")
|
||||||
|
|
||||||
var preceding = ""
|
var preceding = ""
|
||||||
|
|
||||||
for item in items {
|
var directoriesToCreate: [String] = []
|
||||||
let key = preceding == "/"
|
|
||||||
? "/\(item)"
|
|
||||||
: "\(preceding)/\(item)"
|
|
||||||
|
|
||||||
if !self.files.keys.contains(key) {
|
for item in items {
|
||||||
self.files[key] = .fake(.directory)
|
let key = preceding == "/" ? "/\(item)" : "\(preceding)/\(item)"
|
||||||
|
directoriesToCreate.append(key)
|
||||||
|
preceding = key
|
||||||
}
|
}
|
||||||
|
|
||||||
preceding = key
|
for key in directoriesToCreate where !self.files.keys.contains(key) {
|
||||||
|
self.files[key] = .fake(.directory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,17 @@ public class TestableShell: ShellProtocol {
|
|||||||
|
|
||||||
var expectations: [String: BatchFakeShellOutput] = [:]
|
var expectations: [String: BatchFakeShellOutput] = [:]
|
||||||
|
|
||||||
|
func sync(_ command: String) -> ShellOutput {
|
||||||
|
// This assertion will only fire during test builds
|
||||||
|
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
|
||||||
|
|
||||||
|
guard let expectation = expectations[command] else {
|
||||||
|
return .err("No Expected Output")
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectation.syncOutput()
|
||||||
|
}
|
||||||
|
|
||||||
func quiet(_ command: String) async {
|
func quiet(_ command: String) async {
|
||||||
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
||||||
}
|
}
|
||||||
@@ -112,6 +123,29 @@ struct BatchFakeShellOutput: Codable {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Outputs the fake shell output as expected, but does this synchronously.
|
||||||
|
*/
|
||||||
|
public func syncOutput(
|
||||||
|
ignoreDelay: Bool = false
|
||||||
|
) -> ShellOutput {
|
||||||
|
let output = ShellOutput.empty()
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
if !ignoreDelay {
|
||||||
|
Thread.sleep(forTimeInterval: item.delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.stream == .stdErr {
|
||||||
|
output.err += item.output
|
||||||
|
} else if item.stream == .stdOut {
|
||||||
|
output.out += item.output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
||||||
*/
|
*/
|
||||||
|
@@ -18,8 +18,15 @@
|
|||||||
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
||||||
<p><b>Get the latest on Twitter or Mastodon.</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
<p><b>Get the latest on Twitter or Mastodon.</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
||||||
<p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p>
|
<p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p>
|
||||||
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(Some names have been omitted due to their sponsorships being private. Thank you all!)
|
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(Some names have been omitted due to their sponsorships being private. Thank you all!)</p>
|
||||||
|
<p><b>Localization credits:</b></br>
|
||||||
|
‐ English, Dutch</b> by @nicoverbruggen</br>
|
||||||
|
‐ Vietnamese</b> by @xuandung38</br>
|
||||||
|
‐ German</b> by @dsturm</br>
|
||||||
|
‐ Portuguese</b> by @joseborges</br>
|
||||||
|
‐ French</b> by @nhedger, @tplesnar</br>
|
||||||
|
‐ Chinese</b> by @guanguans</br>
|
||||||
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@@ -46,9 +46,11 @@ extension App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hotkey.keyDownHandler = {
|
hotkey.keyDownHandler = {
|
||||||
|
Task { @MainActor in
|
||||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -74,21 +74,27 @@ class App {
|
|||||||
/** The window controller of the onboarding window. */
|
/** The window controller of the onboarding window. */
|
||||||
var onboardingWindowController: OnboardingWindowController?
|
var onboardingWindowController: OnboardingWindowController?
|
||||||
|
|
||||||
|
/** The window controller of the config manager window. */
|
||||||
|
var phpConfigManagerWindowController: PhpConfigManagerWindowController?
|
||||||
|
|
||||||
/** The window controller of the warnings window. */
|
/** The window controller of the warnings window. */
|
||||||
var phpDoctorWindowController: PhpDoctorWindowController?
|
var phpDoctorWindowController: PhpDoctorWindowController?
|
||||||
|
|
||||||
/** The window controller of the warnings window. */
|
/** The window controller of the PHP version manager window. */
|
||||||
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
||||||
|
|
||||||
|
/** The window controller of the PHP extension manager window. */
|
||||||
|
var phpExtensionManagerWindowController: PhpExtensionManagerWindowController?
|
||||||
|
|
||||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||||
var detectedApplications: [Application] = []
|
var detectedApplications: [Application] = []
|
||||||
|
|
||||||
|
/** Favorites storage, which keeps track of favorited domains. */
|
||||||
|
var favorites = Favorites.shared
|
||||||
|
|
||||||
/** The warning manager, responsible for keeping track of warnings. */
|
/** The warning manager, responsible for keeping track of warnings. */
|
||||||
var warnings = WarningManager.shared
|
var warnings = WarningManager.shared
|
||||||
|
|
||||||
/** The filesystem watchers, responsible for keeping track of changes to the PHP installation. */
|
|
||||||
var watchers: [FSNotifier.Kind: FSNotifier] = [:]
|
|
||||||
|
|
||||||
/** Timer that will periodically reload info about the user's PHP installation. */
|
/** Timer that will periodically reload info about the user's PHP installation. */
|
||||||
var timer: Timer?
|
var timer: Timer?
|
||||||
|
|
||||||
@@ -117,8 +123,12 @@ class App {
|
|||||||
|
|
||||||
// MARK: - App Watchers
|
// MARK: - App Watchers
|
||||||
|
|
||||||
|
/** Individual filesystem watchers, which are, i.e. responsible for watching the Homebrew folders. */
|
||||||
|
var watchers: [String: FSNotifier] = [:]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
The `ConfigWatchManager` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
||||||
|
This manager object can immediately start or stop all watchers (or pause them) all at once.
|
||||||
*/
|
*/
|
||||||
var watcher: PhpConfigWatcher!
|
var watchManager: ConfigWatchManager!
|
||||||
}
|
}
|
||||||
|
@@ -23,12 +23,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
*/
|
*/
|
||||||
let state: App
|
let state: App
|
||||||
|
|
||||||
/**
|
|
||||||
The MainMenu singleton is responsible for rendering the
|
|
||||||
menu bar item and its menu, as well as its actions.
|
|
||||||
*/
|
|
||||||
let menu: MainMenu
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The paths singleton that determines where Homebrew is installed,
|
The paths singleton that determines where Homebrew is installed,
|
||||||
and where to look for binaries.
|
and where to look for binaries.
|
||||||
@@ -96,7 +90,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.state = App.shared
|
self.state = App.shared
|
||||||
self.menu = MainMenu.shared
|
|
||||||
self.paths = Paths.shared
|
self.paths = Paths.shared
|
||||||
self.valet = Valet.shared
|
self.valet = Valet.shared
|
||||||
self.brew = Brew.shared
|
self.brew = Brew.shared
|
||||||
@@ -109,6 +102,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
static func initializeTestingProfile(_ path: String) {
|
static func initializeTestingProfile(_ path: String) {
|
||||||
Log.info("The configuration with path `\(path)` is being requested...")
|
Log.info("The configuration with path `\(path)` is being requested...")
|
||||||
|
// Clear for PHP Guard
|
||||||
|
Stats.clearCurrentGlobalPhpVersion()
|
||||||
|
// Load the configuration file
|
||||||
TestableConfiguration.loadFrom(path: path).apply()
|
TestableConfiguration.loadFrom(path: path).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +125,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
setupNotifications()
|
setupNotifications()
|
||||||
|
|
||||||
Task { // Make sure the menu performs its initial checks
|
Task { // Make sure the menu performs its initial checks
|
||||||
await menu.startup()
|
await MainMenu.shared.startup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class AppUpdater {
|
class AppUpdater {
|
||||||
var caskFile: CaskFile!
|
var caskFile: CaskFile!
|
||||||
@@ -72,7 +73,7 @@ class AppUpdater {
|
|||||||
: "brew upgrade phpmon"
|
: "brew upgrade phpmon"
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "updater.alerts.newer_version_available.title"
|
title: "updater.alerts.newer_version_available.title"
|
||||||
.localized(latestVersionOnline.humanReadable),
|
.localized(latestVersionOnline.humanReadable),
|
||||||
subtitle: "updater.alerts.newer_version_available.subtitle"
|
subtitle: "updater.alerts.newer_version_available.subtitle"
|
||||||
@@ -112,7 +113,7 @@ class AppUpdater {
|
|||||||
|
|
||||||
public func presentNoNewerVersionAvailableAlert() {
|
public func presentNoNewerVersionAvailableAlert() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "updater.alerts.is_latest_version.title".localized,
|
title: "updater.alerts.is_latest_version.title".localized,
|
||||||
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
|
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
|
||||||
description: ""
|
description: ""
|
||||||
@@ -124,7 +125,7 @@ class AppUpdater {
|
|||||||
|
|
||||||
public func presentCouldNotRetrieveUpdate() {
|
public func presentCouldNotRetrieveUpdate() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "updater.alerts.cannot_check_for_update.title".localized,
|
title: "updater.alerts.cannot_check_for_update.title".localized,
|
||||||
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
|
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
|
||||||
description: "updater.alerts.cannot_check_for_update.description".localized(
|
description: "updater.alerts.cannot_check_for_update.description".localized(
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/>
|
||||||
<capability name="Image references" minToolsVersion="12.0"/>
|
<capability name="Image references" minToolsVersion="12.0"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
||||||
@@ -63,6 +63,13 @@
|
|||||||
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="bPr-YU-lg4"/>
|
||||||
|
<menuItem title="actions" enabled="NO" id="cAS-FU-WUA" userLabel="actions" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_actions"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</menuItem>
|
||||||
</items>
|
</items>
|
||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
@@ -429,7 +436,7 @@
|
|||||||
</toolbarItem>
|
</toolbarItem>
|
||||||
<searchToolbarItem implicitItemIdentifier="7C834FBE-7118-4082-A09F-7CBECEC1356A" label="Search" paletteLabel="Search" visibilityPriority="1001" id="G2g-jS-RVc">
|
<searchToolbarItem implicitItemIdentifier="7C834FBE-7118-4082-A09F-7CBECEC1356A" label="Search" paletteLabel="Search" visibilityPriority="1001" id="G2g-jS-RVc">
|
||||||
<nil key="toolTip"/>
|
<nil key="toolTip"/>
|
||||||
<searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
|
<searchField key="view" focusRingType="none" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
|
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="vp9-vH-goQ">
|
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="vp9-vH-goQ">
|
||||||
@@ -508,10 +515,10 @@
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="-374" y="2267"/>
|
<point key="canvasLocation" x="-374" y="2267"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Better AlertVC-->
|
<!--AlertVC-->
|
||||||
<scene sceneID="y9E-bB-wIG">
|
<scene sceneID="y9E-bB-wIG">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="noticeVC" id="hkw-9V-NxP" customClass="BetterAlertVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="noticeVC" id="hkw-9V-NxP" customClass="NVAlertVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" id="UPH-hV-Naz">
|
<view key="view" id="UPH-hV-Naz">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
|
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
@@ -521,9 +528,6 @@
|
|||||||
<subviews>
|
<subviews>
|
||||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8zu-cF-KCX">
|
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8zu-cF-KCX">
|
||||||
<rect key="frame" x="383" y="13" width="104" height="32"/>
|
<rect key="frame" x="383" y="13" width="104" height="32"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="4Uf-fh-jWJ"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="Primary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="F26-vf-hFH">
|
<buttonCell key="cell" type="push" title="Primary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="F26-vf-hFH">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -531,15 +535,15 @@
|
|||||||
DQ
|
DQ
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="4Uf-fh-jWJ"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="primaryButtonAction:" target="hkw-9V-NxP" id="W7d-3b-pZT"/>
|
<action selector="primaryButtonAction:" target="hkw-9V-NxP" id="W7d-3b-pZT"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TCp-nS-HN2">
|
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TCp-nS-HN2">
|
||||||
<rect key="frame" x="281" y="13" width="104" height="32"/>
|
<rect key="frame" x="281" y="13" width="104" height="32"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="QWZ-BA-0g9"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="Secondary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="eCk-FC-9Zr">
|
<buttonCell key="cell" type="push" title="Secondary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="eCk-FC-9Zr">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -547,6 +551,9 @@ DQ
|
|||||||
Gw
|
Gw
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="QWZ-BA-0g9"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="secondaryButtonAction:" target="hkw-9V-NxP" id="YJs-Hu-lFP"/>
|
<action selector="secondaryButtonAction:" target="hkw-9V-NxP" id="YJs-Hu-lFP"/>
|
||||||
</connections>
|
</connections>
|
||||||
@@ -575,7 +582,7 @@ Gw
|
|||||||
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="wIl-uw-y3p"/>
|
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="wIl-uw-y3p"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</visualEffectView>
|
</visualEffectView>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
|
||||||
<rect key="frame" x="98" y="153" width="384" height="19"/>
|
<rect key="frame" x="98" y="153" width="384" height="19"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="380" id="WgB-hj-d4P"/>
|
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="380" id="WgB-hj-d4P"/>
|
||||||
@@ -586,7 +593,7 @@ Gw
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
|
||||||
<rect key="frame" x="98" y="127" width="384" height="16"/>
|
<rect key="frame" x="98" y="127" width="384" height="16"/>
|
||||||
<textFieldCell key="cell" selectable="YES" title="This is a slightly more expanded explanation." id="rY3-Nd-Iit">
|
<textFieldCell key="cell" selectable="YES" title="This is a slightly more expanded explanation." id="rY3-Nd-Iit">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -610,7 +617,7 @@ Gw
|
|||||||
</constraints>
|
</constraints>
|
||||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="7eT-Hw-EL9"/>
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="7eT-Hw-EL9"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
|
||||||
<rect key="frame" x="98" y="70" width="384" height="42"/>
|
<rect key="frame" x="98" y="70" width="384" height="42"/>
|
||||||
<textFieldCell key="cell" selectable="YES" id="7iW-Lc-DqO">
|
<textFieldCell key="cell" selectable="YES" id="7iW-Lc-DqO">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -685,9 +692,6 @@ DQ
|
|||||||
</button>
|
</button>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
||||||
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
|
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -695,11 +699,14 @@ DQ
|
|||||||
Gw
|
Gw
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
|
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
|
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
|
||||||
<rect key="frame" x="20" y="150" width="440" height="21"/>
|
<rect key="frame" x="20" y="150" width="440" height="21"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -710,7 +717,7 @@ Gw
|
|||||||
<outlet property="delegate" destination="glS-wF-sEU" id="Dyf-0M-Gwj"/>
|
<outlet property="delegate" destination="glS-wF-sEU" id="Dyf-0M-Gwj"/>
|
||||||
</connections>
|
</connections>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
|
||||||
<rect key="frame" x="18" y="128" width="444" height="14"/>
|
<rect key="frame" x="18" y="128" width="444" height="14"/>
|
||||||
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
|
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -728,7 +735,7 @@ Gw
|
|||||||
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
|
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
|
||||||
<rect key="frame" x="18" y="60" width="444" height="28"/>
|
<rect key="frame" x="18" y="60" width="444" height="28"/>
|
||||||
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="4gd-KM-5Fu">
|
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="4gd-KM-5Fu">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -743,7 +750,7 @@ Gw
|
|||||||
<url key="url" string="file:///Users/"/>
|
<url key="url" string="file:///Users/"/>
|
||||||
</pathCell>
|
</pathCell>
|
||||||
</pathControl>
|
</pathControl>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
|
||||||
<rect key="frame" x="18" y="209" width="128" height="16"/>
|
<rect key="frame" x="18" y="209" width="128" height="16"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
|
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
|
||||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||||
@@ -751,7 +758,7 @@ Gw
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
||||||
<rect key="frame" x="140" y="23" width="180" height="14"/>
|
<rect key="frame" x="140" y="23" width="180" height="14"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
|
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -819,14 +826,21 @@ Gw
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
|
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
|
||||||
|
<rect key="frame" x="298" y="150" width="30" height="30"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
|
||||||
|
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
||||||
|
</constraints>
|
||||||
|
</progressIndicator>
|
||||||
<scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
|
<scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
||||||
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w">
|
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="611" height="294"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="281"/>
|
<rect key="frame" x="0.0" y="0.0" width="611" height="266"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<size key="intercellSpacing" width="17" height="0.0"/>
|
<size key="intercellSpacing" width="17" height="0.0"/>
|
||||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
@@ -886,7 +900,7 @@ Gw
|
|||||||
<rect key="frame" x="69" y="0.0" width="200" height="54"/>
|
<rect key="frame" x="69" y="0.0" width="200" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
||||||
<rect key="frame" x="3" y="26" width="145" height="16"/>
|
<rect key="frame" x="3" y="26" width="145" height="16"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
|
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
|
||||||
<font key="font" metaFont="systemSemibold" size="13"/>
|
<font key="font" metaFont="systemSemibold" size="13"/>
|
||||||
@@ -894,7 +908,7 @@ Gw
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
|
||||||
<rect key="frame" x="3" y="12" width="75" height="14"/>
|
<rect key="frame" x="3" y="12" width="75" height="14"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
|
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -916,6 +930,50 @@ Gw
|
|||||||
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
|
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tableCellView>
|
</tableCellView>
|
||||||
|
<tableCellView identifier="domainListNameCellFavorited" wantsLayer="YES" id="Byb-te-u65" customClass="DomainListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="69" y="54" width="200" height="54"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="aot-FJ-HIk">
|
||||||
|
<rect key="frame" x="33" y="26" width="145" height="16"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="LHu-UF-QlC">
|
||||||
|
<font key="font" metaFont="systemSemibold" size="13"/>
|
||||||
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="GNH-l8-oki">
|
||||||
|
<rect key="frame" x="33" y="12" width="75" height="14"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="LNw-Ju-0Ot">
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="3Wp-DX-An9">
|
||||||
|
<rect key="frame" x="5" y="4" width="20" height="47"/>
|
||||||
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="Q76-fI-lkW">
|
||||||
|
<imageReference key="image" image="star.circle.fill" catalog="system" symbolScale="large"/>
|
||||||
|
</imageCell>
|
||||||
|
<color key="contentTintColor" name="AccentColor"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="3Wp-DX-An9" firstAttribute="leading" secondItem="Byb-te-u65" secondAttribute="leading" constant="5" id="CTd-ON-loK"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="aot-FJ-HIk" secondAttribute="trailing" constant="20" symbolic="YES" id="Csc-Dy-H4K"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="GNH-l8-oki" secondAttribute="trailing" constant="20" symbolic="YES" id="H10-MG-hCG"/>
|
||||||
|
<constraint firstItem="GNH-l8-oki" firstAttribute="leading" secondItem="aot-FJ-HIk" secondAttribute="leading" id="Hk0-x3-RyN"/>
|
||||||
|
<constraint firstItem="3Wp-DX-An9" firstAttribute="top" secondItem="Byb-te-u65" secondAttribute="top" constant="9" id="erH-dR-K7S"/>
|
||||||
|
<constraint firstItem="aot-FJ-HIk" firstAttribute="top" secondItem="Byb-te-u65" secondAttribute="top" constant="12" id="ktI-fg-qaX"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="3Wp-DX-An9" secondAttribute="bottom" constant="9" id="uyc-26-gZb"/>
|
||||||
|
<constraint firstItem="aot-FJ-HIk" firstAttribute="leading" secondItem="Byb-te-u65" secondAttribute="leading" constant="35" id="vXE-jj-lLF"/>
|
||||||
|
<constraint firstItem="GNH-l8-oki" firstAttribute="top" secondItem="aot-FJ-HIk" secondAttribute="bottom" id="wSX-fR-O7a"/>
|
||||||
|
</constraints>
|
||||||
|
<connections>
|
||||||
|
<outlet property="labelPathName" destination="GNH-l8-oki" id="GC1-TA-lIk"/>
|
||||||
|
<outlet property="labelSiteName" destination="aot-FJ-HIk" id="HdZ-Rh-ua6"/>
|
||||||
|
</connections>
|
||||||
|
</tableCellView>
|
||||||
</prototypeCellViews>
|
</prototypeCellViews>
|
||||||
</tableColumn>
|
</tableColumn>
|
||||||
<tableColumn identifier="ENVIRONMENT" width="100" minWidth="100" maxWidth="150" id="hzb-XI-Out">
|
<tableColumn identifier="ENVIRONMENT" width="100" minWidth="100" maxWidth="150" id="hzb-XI-Out">
|
||||||
@@ -937,13 +995,13 @@ Gw
|
|||||||
<subviews>
|
<subviews>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
|
||||||
<rect key="frame" x="27" y="18" width="70" height="18"/>
|
<rect key="frame" x="27" y="18" width="70" height="18"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="70" id="MBa-bB-DTB"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="inline" title="PHP X.X" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="Tfk-YR-L4B">
|
<buttonCell key="cell" type="inline" title="PHP X.X" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="Tfk-YR-L4B">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="smallSystemBold"/>
|
<font key="font" metaFont="smallSystemBold"/>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="70" id="MBa-bB-DTB"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="pressedPhpVersion:" target="T49-0U-d58" id="jVO-TS-F6d"/>
|
<action selector="pressedPhpVersion:" target="T49-0U-d58" id="jVO-TS-F6d"/>
|
||||||
</connections>
|
</connections>
|
||||||
@@ -987,11 +1045,11 @@ Gw
|
|||||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||||
<prototypeCellViews>
|
<prototypeCellViews>
|
||||||
<tableCellView identifier="domainListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="DomainListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableCellView identifier="domainListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="DomainListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="403" y="0.0" width="48" height="54"/>
|
<rect key="frame" x="403" y="0.0" width="50" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
|
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
|
||||||
<rect key="frame" x="15" y="18" width="18" height="18"/>
|
<rect key="frame" x="16" y="18" width="18" height="18"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="18" id="XcB-uw-szU"/>
|
<constraint firstAttribute="width" constant="18" id="XcB-uw-szU"/>
|
||||||
<constraint firstAttribute="height" constant="18" id="bGN-Vh-Sh0"/>
|
<constraint firstAttribute="height" constant="18" id="bGN-Vh-Sh0"/>
|
||||||
@@ -1024,10 +1082,10 @@ Gw
|
|||||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||||
<prototypeCellViews>
|
<prototypeCellViews>
|
||||||
<tableCellView identifier="domainListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="DomainListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableCellView identifier="domainListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="DomainListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="468" y="0.0" width="97" height="54"/>
|
<rect key="frame" x="470" y="0.0" width="97" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
||||||
<rect key="frame" x="6" y="26" width="93" height="14"/>
|
<rect key="frame" x="6" y="26" width="93" height="14"/>
|
||||||
<textFieldCell key="cell" alignment="left" title="Laravel" id="0lu-L6-oKr">
|
<textFieldCell key="cell" alignment="left" title="Laravel" id="0lu-L6-oKr">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -1035,7 +1093,7 @@ Gw
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
|
||||||
<rect key="frame" x="6" y="15" width="93" height="11"/>
|
<rect key="frame" x="6" y="15" width="93" height="11"/>
|
||||||
<textFieldCell key="cell" alignment="left" title="PHP 8.0" id="puf-Jh-ham">
|
<textFieldCell key="cell" alignment="left" title="PHP 8.0" id="puf-Jh-ham">
|
||||||
<font key="font" metaFont="miniSystem"/>
|
<font key="font" metaFont="miniSystem"/>
|
||||||
@@ -1073,28 +1131,30 @@ Gw
|
|||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/>
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
|
<scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
|
||||||
<rect key="frame" x="0.0" y="293" width="626" height="16"/>
|
<rect key="frame" x="0.0" y="294" width="611" height="15"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
|
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
|
||||||
<rect key="frame" x="610" y="28" width="16" height="281"/>
|
<rect key="frame" x="611" y="28" width="15" height="266"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
|
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="28"/>
|
<rect key="frame" x="0.0" y="0.0" width="611" height="28"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</tableHeaderView>
|
</tableHeaderView>
|
||||||
</scrollView>
|
</scrollView>
|
||||||
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
|
<customView translatesAutoresizingMaskIntoConstraints="NO" id="wcV-ed-8Bv">
|
||||||
<rect key="frame" x="298" y="150" width="30" height="30"/>
|
<rect key="frame" x="113" y="5" width="400" height="300"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
|
<constraint firstAttribute="width" constant="400" id="HCo-LG-x3N"/>
|
||||||
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
<constraint firstAttribute="height" constant="300" id="Xpi-Rl-xmb"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</progressIndicator>
|
</customView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/>
|
<constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/>
|
||||||
|
<constraint firstItem="wcV-ed-8Bv" firstAttribute="centerX" secondItem="rIZ-4U-bhj" secondAttribute="centerX" id="DPz-kQ-aP0"/>
|
||||||
|
<constraint firstItem="wcV-ed-8Bv" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" id="HCW-zJ-gSY"/>
|
||||||
<constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/>
|
<constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/>
|
<constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/>
|
||||||
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/>
|
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/>
|
||||||
@@ -1103,13 +1163,14 @@ Gw
|
|||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<connections>
|
<connections>
|
||||||
|
<outlet property="noResultsView" destination="wcV-ed-8Bv" id="K3s-fb-1aN"/>
|
||||||
<outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/>
|
<outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/>
|
||||||
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
||||||
</connections>
|
</connections>
|
||||||
</viewController>
|
</viewController>
|
||||||
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="323" y="723"/>
|
<point key="canvasLocation" x="323" y="722.5"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Add ProxyVC-->
|
<!--Add ProxyVC-->
|
||||||
<scene sceneID="g8z-pE-RL9">
|
<scene sceneID="g8z-pE-RL9">
|
||||||
@@ -1125,7 +1186,7 @@ Gw
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
|
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
|
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
|
||||||
<rect key="frame" x="20" y="196" width="500" height="21"/>
|
<rect key="frame" x="20" y="196" width="500" height="21"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -1136,7 +1197,7 @@ Gw
|
|||||||
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
|
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
|
||||||
</connections>
|
</connections>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
|
||||||
<rect key="frame" x="18" y="221" width="325" height="14"/>
|
<rect key="frame" x="18" y="221" width="325" height="14"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Proxy subject (usually: protocol, IP address and port)" id="G1Z-3f-BhL">
|
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Proxy subject (usually: protocol, IP address and port)" id="G1Z-3f-BhL">
|
||||||
<font key="font" metaFont="systemMedium" size="11"/>
|
<font key="font" metaFont="systemMedium" size="11"/>
|
||||||
@@ -1144,7 +1205,7 @@ Gw
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
|
||||||
<rect key="frame" x="18" y="172" width="112" height="14"/>
|
<rect key="frame" x="18" y="172" width="112" height="14"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
|
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
|
||||||
<font key="font" metaFont="systemMedium" size="11"/>
|
<font key="font" metaFont="systemMedium" size="11"/>
|
||||||
@@ -1152,7 +1213,7 @@ Gw
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
|
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
|
||||||
<rect key="frame" x="20" y="147" width="500" height="21"/>
|
<rect key="frame" x="20" y="147" width="500" height="21"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -1194,9 +1255,6 @@ DQ
|
|||||||
</button>
|
</button>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
|
||||||
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="uCc-fF-wS2"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="D8g-GE-7TU">
|
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="D8g-GE-7TU">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -1204,11 +1262,14 @@ DQ
|
|||||||
Gw
|
Gw
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="uCc-fF-wS2"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
|
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
|
||||||
<rect key="frame" x="18" y="128" width="504" height="14"/>
|
<rect key="frame" x="18" y="128" width="504" height="14"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="sF1-RG-URI"/>
|
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="sF1-RG-URI"/>
|
||||||
@@ -1229,7 +1290,7 @@ Gw
|
|||||||
<action selector="pressedSecure:" target="dwh-CF-6iv" id="b74-8T-AzO"/>
|
<action selector="pressedSecure:" target="dwh-CF-6iv" id="b74-8T-AzO"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
|
||||||
<rect key="frame" x="18" y="60" width="504" height="28"/>
|
<rect key="frame" x="18" y="60" width="504" height="28"/>
|
||||||
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
|
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -1237,7 +1298,7 @@ Gw
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
|
||||||
<rect key="frame" x="18" y="250" width="123" height="16"/>
|
<rect key="frame" x="18" y="250" width="123" height="16"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
|
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
|
||||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||||
@@ -1245,7 +1306,7 @@ Gw
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
|
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
|
||||||
<rect key="frame" x="191" y="23" width="180" height="14"/>
|
<rect key="frame" x="191" y="23" width="180" height="14"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
|
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -1340,9 +1401,6 @@ Gw
|
|||||||
<subviews>
|
<subviews>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
|
||||||
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="Zhu-D8-cLK"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="LxP-t4-H2W">
|
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="LxP-t4-H2W">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -1350,6 +1408,9 @@ Gw
|
|||||||
Gw
|
Gw
|
||||||
</string>
|
</string>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="Zhu-D8-cLK"/>
|
||||||
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="pressedCancel:" target="gOD-Gu-zDG" id="wMp-sM-0A4"/>
|
<action selector="pressedCancel:" target="gOD-Gu-zDG" id="wMp-sM-0A4"/>
|
||||||
</connections>
|
</connections>
|
||||||
@@ -1389,7 +1450,7 @@ Gw
|
|||||||
<real value="3.4028234663852886e+38"/>
|
<real value="3.4028234663852886e+38"/>
|
||||||
</customSpacing>
|
</customSpacing>
|
||||||
</stackView>
|
</stackView>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
|
||||||
<rect key="frame" x="18" y="138" width="504" height="19"/>
|
<rect key="frame" x="18" y="138" width="504" height="19"/>
|
||||||
<textFieldCell key="cell" selectable="YES" alignment="left" title="[i18n] What kind of domain would you like to set up?" id="agk-Nj-FLd">
|
<textFieldCell key="cell" selectable="YES" alignment="left" title="[i18n] What kind of domain would you like to set up?" id="agk-Nj-FLd">
|
||||||
<font key="font" metaFont="systemBold" size="15"/>
|
<font key="font" metaFont="systemBold" size="15"/>
|
||||||
@@ -1397,7 +1458,7 @@ Gw
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
|
<textField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
|
||||||
<rect key="frame" x="18" y="60" width="504" height="70"/>
|
<rect key="frame" x="18" y="60" width="504" height="70"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="tbl-AV-4qB"/>
|
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="tbl-AV-4qB"/>
|
||||||
@@ -1486,6 +1547,10 @@ Gw
|
|||||||
<image name="Lock" width="30" height="30"/>
|
<image name="Lock" width="30" height="30"/>
|
||||||
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
|
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
|
||||||
<image name="plus" catalog="system" width="14" height="13"/>
|
<image name="plus" catalog="system" width="14" height="13"/>
|
||||||
|
<image name="star.circle.fill" catalog="system" width="20" height="20"/>
|
||||||
|
<namedColor name="AccentColor">
|
||||||
|
<color red="0.0" green="0.46000000000000002" blue="0.89000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
<namedColor name="IconColorGreen">
|
<namedColor name="IconColorGreen">
|
||||||
<color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class ValetServicesManager: ServicesManager {
|
class ValetServicesManager: ServicesManager {
|
||||||
override init() {
|
override init() {
|
||||||
@@ -131,7 +132,7 @@ class ValetServicesManager: ServicesManager {
|
|||||||
Log.err("The service '\(named)' is now reporting an error.")
|
Log.err("The service '\(named)' is now reporting an error.")
|
||||||
|
|
||||||
guard let errorLogPath = after.error_log_path else {
|
guard let errorLogPath = after.error_log_path else {
|
||||||
return BetterAlert().withInformation(
|
return NVAlert().withInformation(
|
||||||
title: "alert.service_error.title".localized(named),
|
title: "alert.service_error.title".localized(named),
|
||||||
subtitle: "alert.service_error.subtitle.no_error_log".localized(named),
|
subtitle: "alert.service_error.subtitle.no_error_log".localized(named),
|
||||||
description: "alert.service_error.extra".localized
|
description: "alert.service_error.extra".localized
|
||||||
@@ -140,7 +141,7 @@ class ValetServicesManager: ServicesManager {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.service_error.title".localized(named),
|
title: "alert.service_error.title".localized(named),
|
||||||
subtitle: "alert.service_error.subtitle.error_log".localized(named),
|
subtitle: "alert.service_error.subtitle.error_log".localized(named),
|
||||||
description: "alert.service_error.extra".localized
|
description: "alert.service_error.extra".localized
|
||||||
|
@@ -7,9 +7,24 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class Startup {
|
class Startup {
|
||||||
|
|
||||||
|
@MainActor static var startupTimer: Timer?
|
||||||
|
|
||||||
|
@MainActor func startTimeoutTimer() {
|
||||||
|
Self.startupTimer = Timer.scheduledTimer(
|
||||||
|
timeInterval: 30.0, target: self, selector: #selector(startupTimeout),
|
||||||
|
userInfo: nil, repeats: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor static func invalidateTimeoutTimer() {
|
||||||
|
Self.startupTimer?.invalidate()
|
||||||
|
Self.startupTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks the user's environment and checks if PHP Monitor can be used properly.
|
Checks the user's environment and checks if PHP Monitor can be used properly.
|
||||||
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
|
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
|
||||||
@@ -21,6 +36,11 @@ class Startup {
|
|||||||
// Do the important system setup checks
|
// Do the important system setup checks
|
||||||
Log.info("The user is running PHP Monitor with the architecture: \(App.architecture)")
|
Log.info("The user is running PHP Monitor with the architecture: \(App.architecture)")
|
||||||
|
|
||||||
|
// Set up a "background" timer on the main thread
|
||||||
|
Task { @MainActor in
|
||||||
|
startTimeoutTimer()
|
||||||
|
}
|
||||||
|
|
||||||
for group in self.groups {
|
for group in self.groups {
|
||||||
if group.condition() {
|
if group.condition() {
|
||||||
Log.info("Now running \(group.checks.count) \(group.name) checks!")
|
Log.info("Now running \(group.checks.count) \(group.name) checks!")
|
||||||
@@ -44,10 +64,34 @@ class Startup {
|
|||||||
// If we get here, nothing has gone wrong. That's what we want!
|
// If we get here, nothing has gone wrong. That's what we want!
|
||||||
initializeSwitcher()
|
initializeSwitcher()
|
||||||
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
|
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
|
||||||
|
|
||||||
Log.separator(as: .info)
|
Log.separator(as: .info)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Displays an alert for when the application startup process takes too long.
|
||||||
|
*/
|
||||||
|
@MainActor @objc func startupTimeout() {
|
||||||
|
NVAlert()
|
||||||
|
.withInformation(
|
||||||
|
title: "startup.timeout.title".localized,
|
||||||
|
subtitle: "startup.timeout.subtitle".localized,
|
||||||
|
description: "startup.timeout.description".localized
|
||||||
|
)
|
||||||
|
.withPrimary(text: "alert.cannot_start.close".localized, action: { vc in
|
||||||
|
vc.close(with: .alertFirstButtonReturn)
|
||||||
|
exit(1)
|
||||||
|
})
|
||||||
|
.withSecondary(text: "startup.timeout.ignore".localized, action: { vc in
|
||||||
|
vc.close(with: .alertSecondButtonReturn)
|
||||||
|
})
|
||||||
|
.withTertiary(text: "", action: { _ in
|
||||||
|
NSWorkspace.shared.open(URL(string: "https://github.com/nicoverbruggen/phpmon/issues/294")!)
|
||||||
|
})
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Displays an alert for a particular check. There are two types of alerts:
|
Displays an alert for a particular check. There are two types of alerts:
|
||||||
- ones that require an app restart, which prompt the user to exit the app
|
- ones that require an app restart, which prompt the user to exit the app
|
||||||
@@ -55,7 +99,7 @@ class Startup {
|
|||||||
*/
|
*/
|
||||||
@MainActor private func showAlert(for check: EnvironmentCheck) {
|
@MainActor private func showAlert(for check: EnvironmentCheck) {
|
||||||
if check.requiresAppRestart {
|
if check.requiresAppRestart {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: check.titleText,
|
title: check.titleText,
|
||||||
subtitle: check.subtitleText,
|
subtitle: check.subtitleText,
|
||||||
@@ -66,7 +110,7 @@ class Startup {
|
|||||||
}).show()
|
}).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: check.titleText,
|
title: check.titleText,
|
||||||
subtitle: check.subtitleText,
|
subtitle: check.subtitleText,
|
||||||
@@ -142,7 +186,7 @@ class Startup {
|
|||||||
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
||||||
.contains("Library not loaded")
|
.contains("Library not loaded")
|
||||||
},
|
},
|
||||||
name: "`no dyld issue detected",
|
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||||
titleText: "startup.errors.dyld_library.title".localized,
|
titleText: "startup.errors.dyld_library.title".localized,
|
||||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||||
Paths.optPath
|
Paths.optPath
|
||||||
@@ -241,6 +285,20 @@ class Startup {
|
|||||||
descriptionText: "startup.errors.which_alias_issue.desc".localized
|
descriptionText: "startup.errors.which_alias_issue.desc".localized
|
||||||
),
|
),
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
|
// Determine that Laravel Herd is not running (may cause conflicts)
|
||||||
|
// =================================================================================
|
||||||
|
EnvironmentCheck(
|
||||||
|
command: {
|
||||||
|
return NSWorkspace.shared.runningApplications.contains(where: { app in
|
||||||
|
return app.bundleIdentifier == "de.beyondco.herd"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
name: "Herd is not running",
|
||||||
|
titleText: "startup.errors.herd_running.title".localized,
|
||||||
|
subtitleText: "startup.errors.herd_running.subtitle".localized,
|
||||||
|
descriptionText: "startup.errors.herd_running.desc".localized
|
||||||
|
),
|
||||||
|
// =================================================================================
|
||||||
// Determine that Valet works correctly (no issues in platform detected)
|
// Determine that Valet works correctly (no issues in platform detected)
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
EnvironmentCheck(
|
EnvironmentCheck(
|
||||||
|
@@ -62,12 +62,13 @@ struct ComposerJson: Decodable {
|
|||||||
public func getNotableDependencies() -> [String: String] {
|
public func getNotableDependencies() -> [String: String] {
|
||||||
var notable: [String: String] = [:]
|
var notable: [String: String] = [:]
|
||||||
|
|
||||||
var scan = Array(PhpFrameworks.DependencyList.keys)
|
let scan = Array(ProjectTypeDetection.CommonDependencyList.keys) +
|
||||||
scan.append("php")
|
Array(ProjectTypeDetection.SpecificDependencyList.keys) +
|
||||||
|
["php"]
|
||||||
|
|
||||||
scan.forEach { dependency in
|
scan.forEach { dependency in
|
||||||
if dependencies?[dependency] != nil {
|
if let resolvedDependency = dependencies?[dependency] {
|
||||||
notable[dependency] = dependencies![dependency]
|
notable[dependency] = resolvedDependency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
@MainActor class ComposerWindow {
|
@MainActor class ComposerWindow {
|
||||||
private var shouldNotify: Bool! = nil
|
private var shouldNotify: Bool! = nil
|
||||||
@@ -28,8 +29,6 @@ import Foundation
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
MainMenu.shared.setBusyImage()
|
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
|
|
||||||
window = TerminalProgressWindowController.display(
|
window = TerminalProgressWindowController.display(
|
||||||
title: "alert.composer_progress.title".localized,
|
title: "alert.composer_progress.title".localized,
|
||||||
@@ -106,15 +105,12 @@ import Foundation
|
|||||||
|
|
||||||
private func removeBusyStatus() {
|
private func removeBusyStatus() {
|
||||||
PhpEnvironments.shared.isBusy = false
|
PhpEnvironments.shared.isBusy = false
|
||||||
Task { @MainActor in
|
|
||||||
MainMenu.shared.updatePhpVersionInStatusBar()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Alert
|
// MARK: Alert
|
||||||
|
|
||||||
@MainActor private func presentMissingAlert() {
|
private func presentMissingAlert() {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.composer_missing.title".localized,
|
title: "alert.composer_missing.title".localized,
|
||||||
subtitle: "alert.composer_missing.subtitle".localized,
|
subtitle: "alert.composer_missing.subtitle".localized,
|
||||||
|
@@ -8,21 +8,21 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct PhpFrameworks {
|
struct ProjectTypeDetection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This list should probably be reversed when checked, because some of these
|
This list is only checked if the specific dependency list doesn't report a match.
|
||||||
will also require either `laravel/framework` or `symfony/symfony`.
|
|
||||||
*/
|
*/
|
||||||
public static let DependencyList = [
|
public static let CommonDependencyList = [
|
||||||
|
|
||||||
// COMMON FRAMEWORKS
|
|
||||||
"laravel/framework": "Laravel",
|
"laravel/framework": "Laravel",
|
||||||
"symfony/symfony": "Symfony",
|
"symfony/symfony": "Symfony",
|
||||||
"laravel/lumen": "Lumen",
|
"laravel/lumen": "Lumen"
|
||||||
|
]
|
||||||
|
|
||||||
// VARIOUS CMS
|
/**
|
||||||
"roots/bedrock": "Bedrock",
|
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-autoloader": "Bedrock",
|
||||||
"cakephp/app": "CakePHP",
|
"cakephp/app": "CakePHP",
|
||||||
"craftcms/craft": "Craft",
|
"craftcms/craft": "Craft",
|
||||||
"drupal/core": "Drupal",
|
"drupal/core": "Drupal",
|
||||||
@@ -37,30 +37,8 @@ struct PhpFrameworks {
|
|||||||
"johnpbloch/wordpress-core": "WordPress",
|
"johnpbloch/wordpress-core": "WordPress",
|
||||||
"zendframework/zendframework": "Zend",
|
"zendframework/zendframework": "Zend",
|
||||||
"zendframework/zend-mvc": "Zend",
|
"zendframework/zend-mvc": "Zend",
|
||||||
"typo3/cms-core": "Typo3"
|
"typo3/cms-core": "Typo3",
|
||||||
// "magento/*": "Magento",
|
"slim/slim": "Slim"
|
||||||
// "concrete5/*": "Concrete5",
|
|
||||||
// "contao/*": "Contao",
|
|
||||||
// "slim/*": "Slim",
|
|
||||||
]
|
|
||||||
|
|
||||||
public static let FileMapping: [String: [String]] = [
|
|
||||||
"Drupal": [
|
|
||||||
// Legacy installations
|
|
||||||
"/misc/drupal.js",
|
|
||||||
"/core/lib/Drupal.php",
|
|
||||||
// The default (new) installation w/ Composer puts the modules in /web
|
|
||||||
"/web/misc/drupal.js",
|
|
||||||
"/web/core/lib/Drupal.php"
|
|
||||||
],
|
|
||||||
"WordPress": [
|
|
||||||
"/wp-config.php",
|
|
||||||
"/wp-config-sample.php"
|
|
||||||
],
|
|
||||||
"Typo3": [
|
|
||||||
"/typo3",
|
|
||||||
"/public/typo3"
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,4 +60,25 @@ struct PhpFrameworks {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
File mapping is used as a fallback if neither specific nor framework matches could be done.
|
||||||
|
*/
|
||||||
|
public static let FileMapping: [String: [String]] = [
|
||||||
|
"Drupal": [
|
||||||
|
// Legacy installations
|
||||||
|
"/misc/drupal.js",
|
||||||
|
"/core/lib/Drupal.php",
|
||||||
|
// The default (new) installation w/ Composer puts the modules in /web
|
||||||
|
"/web/misc/drupal.js",
|
||||||
|
"/web/core/lib/Drupal.php"
|
||||||
|
],
|
||||||
|
"WordPress": [
|
||||||
|
"/wp-config.php",
|
||||||
|
"/wp-config-sample.php"
|
||||||
|
],
|
||||||
|
"Typo3": [
|
||||||
|
"/typo3",
|
||||||
|
"/public/typo3"
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
@@ -8,16 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class BrewFormulaeObservable: ObservableObject {
|
|
||||||
@Published var phpVersions: [BrewFormula] = []
|
|
||||||
|
|
||||||
var upgradeable: [BrewFormula] {
|
|
||||||
return phpVersions.filter { formula in
|
|
||||||
formula.hasUpgrade
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Brew {
|
class Brew {
|
||||||
static let shared = Brew()
|
static let shared = Brew()
|
||||||
|
|
||||||
@@ -45,10 +35,12 @@ class Brew {
|
|||||||
|
|
||||||
/// Each formula for each PHP version that can be installed.
|
/// Each formula for each PHP version that can be installed.
|
||||||
public static let phpVersionFormulae = [
|
public static let phpVersionFormulae = [
|
||||||
|
"8.5": "shivammathur/php/php@8.5",
|
||||||
|
"8.4": "shivammathur/php/php@8.4",
|
||||||
"8.3": "shivammathur/php/php@8.3",
|
"8.3": "shivammathur/php/php@8.3",
|
||||||
"8.2": "php@8.2",
|
"8.2": "shivammathur/php/php@8.2",
|
||||||
"8.1": "php@8.1",
|
"8.1": "shivammathur/php/php@8.1",
|
||||||
"8.0": "php@8.0",
|
"8.0": "shivammathur/php/php@8.0",
|
||||||
"7.4": "shivammathur/php/php@7.4",
|
"7.4": "shivammathur/php/php@7.4",
|
||||||
"7.3": "shivammathur/php/php@7.3",
|
"7.3": "shivammathur/php/php@7.3",
|
||||||
"7.2": "shivammathur/php/php@7.2",
|
"7.2": "shivammathur/php/php@7.2",
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class BrewDiagnostics {
|
class BrewDiagnostics {
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +28,21 @@ class BrewDiagnostics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Logs a bunch of useful information during startup.
|
||||||
|
*/
|
||||||
|
public static func logBootInformation() {
|
||||||
|
Log.info(BrewDiagnostics.customCaskInstalled
|
||||||
|
? "[BREW] The app has been installed via Homebrew Cask."
|
||||||
|
: "[BREW] The app has been installed directly (optimal)."
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.info(BrewDiagnostics.usesNginxFullFormula
|
||||||
|
? "[BREW] The app will be using the `nginx-full` formula."
|
||||||
|
: "[BREW] The app will be using the `nginx` formula."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determines whether the PHP Monitor Cask is installed.
|
Determines whether the PHP Monitor Cask is installed.
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +62,43 @@ class BrewDiagnostics {
|
|||||||
return destination.contains("/nginx-full/")
|
return destination.contains("/nginx-full/")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
/**
|
||||||
|
It is possible to have outdated symlinks for PHP installations. This can mean that certain PHP installations
|
||||||
|
are going to be reported incorrectly (e.g. `php@8.2` links to an installation in a `8.3` folder after an upgrade).
|
||||||
|
|
||||||
|
To ensure this does not cause issues, PHP Monitor will automatically remove all incorrect PHP symlinks.
|
||||||
|
*/
|
||||||
|
public static func checkForOutdatedPhpInstallationSymlinks() async {
|
||||||
|
// Set up a regular expression
|
||||||
|
let regex = try! NSRegularExpression(pattern: "^php@[0-9]+\\.[0-9]+$", options: .caseInsensitive)
|
||||||
|
|
||||||
|
// Check for incorrect versions
|
||||||
|
if let contents = try? FileSystem.getShallowContentsOfDirectory("\(Paths.optPath)")
|
||||||
|
.filter({
|
||||||
|
let range = NSRange($0.startIndex..., in: $0)
|
||||||
|
return regex.firstMatch(in: $0, options: [], range: range) != nil
|
||||||
|
}) {
|
||||||
|
|
||||||
|
for symlink in contents {
|
||||||
|
let version = symlink.replacingOccurrences(of: "php@", with: "")
|
||||||
|
if let destination = try? FileSystem.getDestinationOfSymlink("\(Paths.optPath)/\(symlink)") {
|
||||||
|
if !destination.contains("Cellar/php/\(version)")
|
||||||
|
&& !destination.contains("Cellar/php@\(version)") {
|
||||||
|
Log.err("Symlink for \(symlink) is incorrect. Removing...")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove("\(Paths.optPath)/\(symlink)")
|
||||||
|
Log.info("Incorrect symlink for \(symlink) has been successfully removed.")
|
||||||
|
} catch {
|
||||||
|
Log.err("Symlink for \(symlink) was incorrect but could not be removed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not read symlink at: \(Paths.optPath)/\(symlink)! Symlink check skipped.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||||
This will then result in two different aliases claiming to point to the same formula (`php`).
|
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||||
@@ -131,7 +184,7 @@ class BrewDiagnostics {
|
|||||||
*/
|
*/
|
||||||
private static func presentAlertAboutConflict() {
|
private static func presentAlertAboutConflict() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.php_alias_conflict.title".localized,
|
title: "alert.php_alias_conflict.title".localized,
|
||||||
subtitle: "alert.php_alias_conflict.info".localized
|
subtitle: "alert.php_alias_conflict.info".localized
|
||||||
|
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// BrewPhpExtension.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 27/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct BrewPhpExtension: Hashable, Comparable {
|
||||||
|
let name: String
|
||||||
|
let phpVersion: String
|
||||||
|
let isInstalled: Bool
|
||||||
|
let path: String
|
||||||
|
let dependencies: [String]
|
||||||
|
|
||||||
|
var extensionDependencies: [String] {
|
||||||
|
return dependencies
|
||||||
|
.filter {
|
||||||
|
$0.contains("shivammathur/extensions/") && $0.contains("@\(phpVersion)")
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
$0.replacingOccurrences(of: "shivammathur/extensions/", with: "")
|
||||||
|
.replacingOccurrences(of: "@\(phpVersion)", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var formulaName: String {
|
||||||
|
return "\(name)@\(phpVersion)"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(path: String, name: String, phpVersion: String) {
|
||||||
|
self.path = path
|
||||||
|
self.name = name
|
||||||
|
self.phpVersion = phpVersion
|
||||||
|
|
||||||
|
self.isInstalled = BrewPhpExtension.hasInstallationReceipt(
|
||||||
|
for: "\(name)@\(phpVersion)"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.dependencies = BrewPhpExtension.extractDependencies(from: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAlternativeInstall: Bool {
|
||||||
|
guard let php = PhpEnvironments.shared.cachedPhpInstallations[self.phpVersion] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let alreadyDiscovered = php.extensions.contains(where: { $0.name == self.name })
|
||||||
|
|
||||||
|
return alreadyDiscovered && !isInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func firstDependent(in exts: [BrewPhpExtension]) -> BrewPhpExtension? {
|
||||||
|
return exts
|
||||||
|
.filter({ $0.isInstalled })
|
||||||
|
.first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasInstallationReceipt(for formulaName: String) -> Bool {
|
||||||
|
return FileSystem.fileExists("\(Paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name < rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name == rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractDependencies(from path: String) -> [String] {
|
||||||
|
let regexPattern = #"depends_on "(.*)""#
|
||||||
|
var dependencies: [String] = []
|
||||||
|
|
||||||
|
guard let content = try? FileSystem.getStringFromFile(path) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let regex = try NSRegularExpression(pattern: regexPattern, options: [])
|
||||||
|
let range = NSRange(content.startIndex..<content.endIndex, in: content)
|
||||||
|
let matches = regex.matches(in: content, options: [], range: range)
|
||||||
|
|
||||||
|
for match in matches {
|
||||||
|
if let range = Range(match.range(at: 1), in: content) {
|
||||||
|
let dependencyName = String(content[range])
|
||||||
|
dependencies.append(dependencyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// BrewFormula.swift
|
// BrewPhpFormula.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 17/03/2023.
|
// Created by Nico Verbruggen on 17/03/2023.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct BrewFormula {
|
struct BrewPhpFormula: Equatable {
|
||||||
/// Name of the formula.
|
/// Name of the formula.
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
@@ -24,6 +24,9 @@ struct BrewFormula {
|
|||||||
/// Whether this formula is a stable version of PHP.
|
/// Whether this formula is a stable version of PHP.
|
||||||
let prerelease: Bool
|
let prerelease: Bool
|
||||||
|
|
||||||
|
/// Whether this formula's associated Homebrew file exists.
|
||||||
|
var hasFormulaFile: Bool = false
|
||||||
|
|
||||||
/// Whether the formula is currently installed.
|
/// Whether the formula is currently installed.
|
||||||
var isInstalled: Bool {
|
var isInstalled: Bool {
|
||||||
return installedVersion != nil
|
return installedVersion != nil
|
||||||
@@ -41,6 +44,7 @@ struct BrewFormula {
|
|||||||
self.installedVersion = installedVersion
|
self.installedVersion = installedVersion
|
||||||
self.upgradeVersion = upgradeVersion
|
self.upgradeVersion = upgradeVersion
|
||||||
self.prerelease = prerelease
|
self.prerelease = prerelease
|
||||||
|
self.hasFormulaFile = checkFormulaFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the formula can be upgraded.
|
/// Whether the formula can be upgraded.
|
||||||
@@ -48,6 +52,25 @@ struct BrewFormula {
|
|||||||
return upgradeVersion != nil
|
return upgradeVersion != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this formula alias is different.
|
||||||
|
var hasUpgradedFormulaAlias: Bool {
|
||||||
|
return self.shortVersion == PhpEnvironments.homebrewBrewPhpAlias
|
||||||
|
&& PhpEnvironments.homebrewBrewPhpAlias != PhpEnvironments.brewPhpAlias
|
||||||
|
}
|
||||||
|
|
||||||
|
var unavailableAfterUpgrade: Bool {
|
||||||
|
if installedVersion == nil || upgradeVersion == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let installed = try? VersionNumber.parse(self.installedVersion!),
|
||||||
|
let upgrade = try? VersionNumber.parse(self.upgradeVersion!) {
|
||||||
|
return upgrade.short != installed.short
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/// The associated Homebrew folder with this PHP formula.
|
/// The associated Homebrew folder with this PHP formula.
|
||||||
var homebrewFolder: String {
|
var homebrewFolder: String {
|
||||||
let resolved = name
|
let resolved = name
|
||||||
@@ -60,7 +83,7 @@ struct BrewFormula {
|
|||||||
/// The short version associated with this formula, if installed.
|
/// The short version associated with this formula, if installed.
|
||||||
var shortVersion: String? {
|
var shortVersion: String? {
|
||||||
guard let version = self.installedVersion else {
|
guard let version = self.installedVersion else {
|
||||||
return nil
|
return self.displayName.replacingOccurrences(of: "PHP ", with: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return VersionNumber.make(from: version)?.short ?? nil
|
return VersionNumber.make(from: version)?.short ?? nil
|
||||||
@@ -72,6 +95,21 @@ struct BrewFormula {
|
|||||||
return isHealthy() ?? true
|
return isHealthy() ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Verify whether the formula file exists (sourced via `shivammathur/homebrew-php`).
|
||||||
|
If it does not exist, the formula cannot be installed.
|
||||||
|
*/
|
||||||
|
private func checkFormulaFile() -> Bool {
|
||||||
|
guard let version = shortVersion else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileSystem.fileExists(
|
||||||
|
"\(Paths.tapPath)/shivammathur/homebrew-php/Formula/php@\(version).rb"
|
||||||
|
.replacingOccurrences(of: "php@" + PhpEnvironments.brewPhpAlias, with: "php")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if this PHP installation is healthy.
|
* Determines if this PHP installation is healthy.
|
||||||
* Uses the cached installation health check as basis.
|
* Uses the cached installation health check as basis.
|
||||||
@@ -81,6 +119,7 @@ struct BrewFormula {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?.isHealthy ?? nil
|
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?
|
||||||
|
.isHealthy ?? nil
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -8,22 +8,23 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol HandlesBrewFormulae {
|
protocol HandlesBrewPhpFormulae {
|
||||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula]
|
func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula]
|
||||||
func refreshPhpVersions(loadOutdated: Bool) async
|
func refreshPhpVersions(loadOutdated: Bool) async
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HandlesBrewFormulae {
|
extension HandlesBrewPhpFormulae {
|
||||||
public func refreshPhpVersions(loadOutdated: Bool) async {
|
public func refreshPhpVersions(loadOutdated: Bool) async {
|
||||||
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
Brew.shared.formulae.phpVersions = items
|
Brew.shared.formulae.phpVersions = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BrewFormulaeHandler: HandlesBrewFormulae {
|
class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] {
|
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
|
||||||
var outdated: [OutdatedFormula]?
|
var outdated: [OutdatedFormula]?
|
||||||
|
|
||||||
if loadOutdated {
|
if loadOutdated {
|
||||||
@@ -38,27 +39,33 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
|||||||
OutdatedFormulae.self,
|
OutdatedFormulae.self,
|
||||||
from: rawJsonText
|
from: rawJsonText
|
||||||
).formulae.filter({ formula in
|
).formulae.filter({ formula in
|
||||||
formula.name.starts(with: "php")
|
formula.name.starts(with: "shivammathur/php/php") || formula.name.starts(with: "php")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return Brew.phpVersionFormulae.map { (version, formula) in
|
return Brew.phpVersionFormulae.map { (version, formula) in
|
||||||
let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?.versionNumber.text
|
var fullVersion: String?
|
||||||
|
|
||||||
var upgradeVersion: String?
|
var upgradeVersion: String?
|
||||||
|
var isPrerelease: Bool = Constants.ExperimentalPhpVersions.contains(version)
|
||||||
|
|
||||||
|
if let install = PhpEnvironments.shared.cachedPhpInstallations[version] {
|
||||||
|
fullVersion = install.versionNumber.text
|
||||||
|
fullVersion = install.isPreRelease ? "\(fullVersion!)-dev" : fullVersion
|
||||||
|
|
||||||
if let version = fullVersion {
|
|
||||||
upgradeVersion = outdated?.first(where: { formula in
|
upgradeVersion = outdated?.first(where: { formula in
|
||||||
return formula.installed_versions.contains(version)
|
return formula.name.replacingOccurrences(of: "shivammathur/php/", with: "")
|
||||||
|
== install.formulaName.replacingOccurrences(of: "shivammathur/php/", with: "")
|
||||||
})?.current_version
|
})?.current_version
|
||||||
|
|
||||||
|
isPrerelease = install.isPreRelease
|
||||||
}
|
}
|
||||||
|
|
||||||
return BrewFormula(
|
return BrewPhpFormula(
|
||||||
name: formula,
|
name: formula,
|
||||||
displayName: "PHP \(version)",
|
displayName: "PHP \(version)",
|
||||||
installedVersion: fullVersion,
|
installedVersion: fullVersion,
|
||||||
upgradeVersion: upgradeVersion,
|
upgradeVersion: upgradeVersion,
|
||||||
prerelease: Constants.ExperimentalPhpVersions.contains(version)
|
prerelease: isPrerelease
|
||||||
)
|
)
|
||||||
}.sorted { $0.displayName > $1.displayName }
|
}.sorted { $0.displayName > $1.displayName }
|
||||||
}
|
}
|
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// BrewTapFormulae.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 01/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class BrewTapFormulae {
|
||||||
|
public static func from(tap: String) -> [String: [BrewPhpExtension]] {
|
||||||
|
let directory = "\(Paths.tapPath)/\(tap)/Formula"
|
||||||
|
|
||||||
|
let files = try? FileSystem.getShallowContentsOfDirectory(directory)
|
||||||
|
|
||||||
|
var availableExtensions = [String: [BrewPhpExtension]]()
|
||||||
|
|
||||||
|
guard let files = files else {
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
|
||||||
|
let regex = try! NSRegularExpression(pattern: "(\\w+)@(\\d+\\.\\d+)\\.rb")
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
let matches = regex.matches(in: file, range: NSRange(file.startIndex..., in: file))
|
||||||
|
if let match = matches.first {
|
||||||
|
if let phpExtensionRange = Range(match.range(at: 1), in: file),
|
||||||
|
let versionRange = Range(match.range(at: 2), in: file) {
|
||||||
|
// Determine what the extension's name is
|
||||||
|
let phpExtensionName = String(file[phpExtensionRange])
|
||||||
|
|
||||||
|
// Determine what PHP version this is for
|
||||||
|
let phpVersion = String(file[versionRange])
|
||||||
|
|
||||||
|
// Create a new BrewPhpExtension object (determines if installed)
|
||||||
|
let phpExtension = BrewPhpExtension(
|
||||||
|
path: "\(Paths.tapPath)/\(tap)/Formula/\(file)",
|
||||||
|
name: phpExtensionName,
|
||||||
|
phpVersion: phpVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
// Append the extension to the list
|
||||||
|
var extensions = availableExtensions[phpVersion, default: []]
|
||||||
|
extensions.append(phpExtension)
|
||||||
|
availableExtensions[phpVersion] = extensions.sorted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,8 @@ import Foundation
|
|||||||
|
|
||||||
protocol BrewCommand {
|
protocol BrewCommand {
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
||||||
|
|
||||||
|
func getCommandTitle() -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BrewCommand {
|
extension BrewCommand {
|
||||||
@@ -31,6 +33,44 @@ extension BrewCommand {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
var loggedMessages: [String] = []
|
||||||
|
|
||||||
|
let (process, _) = try! await Shell.attach(
|
||||||
|
command,
|
||||||
|
didReceiveOutput: { text, _ in
|
||||||
|
if !text.isEmpty {
|
||||||
|
Log.perf(text)
|
||||||
|
loggedMessages.append(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (number, text) = self.reportInstallationProgress(text) {
|
||||||
|
onProgress(.create(value: number, title: getCommandTitle(), description: text))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withTimeout: .minutes(15)
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.terminationStatus <= 0 {
|
||||||
|
loggedMessages = []
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/php") {
|
||||||
|
let command = "brew tap shivammathur/php"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") {
|
||||||
|
let command = "brew tap shivammathur/extensions"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BrewCommandProgress {
|
struct BrewCommandProgress {
|
||||||
|
@@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// InstallPhpExtensionCommand.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 21/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class InstallPhpExtensionCommand: BrewCommand {
|
||||||
|
let installing: [BrewPhpExtension]
|
||||||
|
|
||||||
|
func getExtensionNames() -> String {
|
||||||
|
return installing.map { $0.name }.joined(separator: ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "phpman.steps.installing".localized(getExtensionNames())
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(install extensions: [BrewPhpExtension]) {
|
||||||
|
self.installing = extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
let progressTitle = "phpman.steps.wait".localized
|
||||||
|
|
||||||
|
onProgress(.create(
|
||||||
|
value: 0.2,
|
||||||
|
title: progressTitle,
|
||||||
|
description: "phpman.steps.preparing".localized
|
||||||
|
))
|
||||||
|
|
||||||
|
// Make sure the tap is installed
|
||||||
|
try await self.checkPhpTap(onProgress)
|
||||||
|
|
||||||
|
// Make sure that the extension(s) are installed
|
||||||
|
try await self.installPackages(onProgress)
|
||||||
|
|
||||||
|
// Finally, complete all operations
|
||||||
|
await self.completedOperations(onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installPackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
// If no installations are needed, early exit
|
||||||
|
if self.installing.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) install \(self.installing.map { $0.formulaName }.joined(separator: " ")) --force
|
||||||
|
"""
|
||||||
|
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||||
|
// Reload and restart PHP versions
|
||||||
|
onProgress(.create(value: 0.95, title: self.getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
// Restart PHP-FPM
|
||||||
|
if let installed = self.installing.first {
|
||||||
|
await Actions.restartPhpFpm(version: installed.phpVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check which version of PHP are now installed
|
||||||
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
|
// Keep track of the currently installed version
|
||||||
|
await MainMenu.shared.refreshActiveInstallation()
|
||||||
|
|
||||||
|
// Also rebuild the content of the main menu
|
||||||
|
await MainMenu.shared.rebuild()
|
||||||
|
|
||||||
|
// Let the UI know that the installation has been completed
|
||||||
|
onProgress(.create(
|
||||||
|
value: 1,
|
||||||
|
title: "phpman.steps.completed".localized,
|
||||||
|
description: "phpman.steps.success".localized
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// RemovePhpExtensionCommand.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 21/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class RemovePhpExtensionCommand: BrewCommand {
|
||||||
|
public let phpExtension: BrewPhpExtension
|
||||||
|
|
||||||
|
public init(remove formula: BrewPhpExtension) {
|
||||||
|
self.phpExtension = formula
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "phpman.steps.removing".localized(phpExtension.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
onProgress(.create(
|
||||||
|
value: 0.2,
|
||||||
|
title: getCommandTitle(),
|
||||||
|
description: "phpman.steps.removing".localized("`\(phpExtension.name)`...")
|
||||||
|
))
|
||||||
|
|
||||||
|
// Keep track of the file that contains the information about the extension
|
||||||
|
let existing = PhpEnvironments.shared
|
||||||
|
.cachedPhpInstallations[phpExtension.phpVersion]?
|
||||||
|
.extensions.first(where: { ext in
|
||||||
|
ext.name == phpExtension.name
|
||||||
|
})
|
||||||
|
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) remove \(phpExtension.formulaName) --force --ignore-dependencies
|
||||||
|
"""
|
||||||
|
|
||||||
|
var loggedMessages: [String] = []
|
||||||
|
|
||||||
|
let (process, _) = try! await Shell.attach(
|
||||||
|
command,
|
||||||
|
didReceiveOutput: { text, _ in
|
||||||
|
if !text.isEmpty {
|
||||||
|
Log.perf(text)
|
||||||
|
loggedMessages.append(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withTimeout: .minutes(5)
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.terminationStatus <= 0 {
|
||||||
|
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
if let ext = existing {
|
||||||
|
await performExtensionCleanup(for: ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
|
await Actions.restartPhpFpm(version: phpExtension.phpVersion)
|
||||||
|
|
||||||
|
await MainMenu.shared.refreshActiveInstallation()
|
||||||
|
|
||||||
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "phpman.steps.failure".localized, log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performExtensionCleanup(for ext: PhpExtension) async {
|
||||||
|
if ext.file.hasSuffix("20-\(ext.name).ini") {
|
||||||
|
// The extension's default configuration file can be removed
|
||||||
|
Log.info("The extension was found in a default extension .ini location. Purging that .ini file.")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove(ext.file)
|
||||||
|
} catch {
|
||||||
|
Log.err("The file `\(ext.file)` could not be removed.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The extension's default configuration file cannot be removed, it should be disabled instead
|
||||||
|
Log.info("The extension was not found in a default location. Disabling the extension only.")
|
||||||
|
if ext.enabled {
|
||||||
|
await ext.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -8,23 +8,33 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class InstallAndUpgradeCommand: BrewCommand {
|
class ModifyPhpVersionCommand: BrewCommand {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let installing: [BrewFormula]
|
let installing: [BrewPhpFormula]
|
||||||
let upgrading: [BrewFormula]
|
let upgrading: [BrewPhpFormula]
|
||||||
let phpGuard: PhpGuard
|
let phpGuard: PhpGuard
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
||||||
The process will be executed in two steps: first upgrades, then installations.
|
The process will be executed in two steps: first upgrades, then installations.
|
||||||
|
|
||||||
Upgrades come first because... well, otherwise installations may very well break.
|
Upgrades come first because... well, otherwise installations may very well break.
|
||||||
Each version that is installed will need to be checked afterwards (if it is OK).
|
Each version that is installed will need to be checked afterwards. Installing a
|
||||||
|
newer formula may break other PHP installations, which in turn need to be fixed.
|
||||||
|
|
||||||
|
- Important: If any PHP formula is a major upgrade that causes a PHP "version" to be
|
||||||
|
uninstalled, this is remedied by running `upgradeMainPhpFormula()`. This process
|
||||||
|
will ensure that the upgrade is applied, but the also that old version is
|
||||||
|
re-installed and linked again.
|
||||||
*/
|
*/
|
||||||
public init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
upgrading: [BrewFormula],
|
upgrading: [BrewPhpFormula],
|
||||||
installing: [BrewFormula]
|
installing: [BrewPhpFormula]
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.installing = installing
|
self.installing = installing
|
||||||
@@ -33,17 +43,32 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
let progressTitle = "Please wait..."
|
let progressTitle = "phpman.steps.wait".localized
|
||||||
|
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: progressTitle,
|
||||||
description: "PHP Monitor is preparing Homebrew..."
|
description: "phpman.steps.preparing".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
|
// Determine if a formula will become unavailable
|
||||||
|
// This is the case when `php` will be bumped to a new version
|
||||||
|
let unavailable = upgrading.first(where: { formula in
|
||||||
|
formula.unavailableAfterUpgrade
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make sure the tap is installed
|
||||||
|
try await self.checkPhpTap(onProgress)
|
||||||
|
|
||||||
|
if unavailable == nil {
|
||||||
// Try to run all upgrade and installation operations
|
// Try to run all upgrade and installation operations
|
||||||
try await self.upgradePackages(onProgress)
|
try await self.upgradePackages(onProgress)
|
||||||
try await self.installPackages(onProgress)
|
try await self.installPackages(onProgress)
|
||||||
|
} else {
|
||||||
|
// Simply upgrade `php` to the latest version
|
||||||
|
try await self.upgradeMainPhpFormula(unavailable!, onProgress)
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
}
|
||||||
|
|
||||||
// Re-check the installed versions
|
// Re-check the installed versions
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@@ -55,6 +80,27 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
await self.completedOperations(onProgress)
|
await self.completedOperations(onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func upgradeMainPhpFormula(
|
||||||
|
_ unavailable: BrewPhpFormula,
|
||||||
|
_ onProgress: @escaping (BrewCommandProgress) -> Void
|
||||||
|
) async throws {
|
||||||
|
// Determine which version was previously available (that will become unavailable)
|
||||||
|
guard let short = try? VersionNumber
|
||||||
|
.parse(unavailable.installedVersion!).short else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade the main formula
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) upgrade php;
|
||||||
|
\(Paths.brew) install php@\(short);
|
||||||
|
"""
|
||||||
|
|
||||||
|
// Run the upgrade command
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
// If no upgrades are needed, early exit
|
// If no upgrades are needed, early exit
|
||||||
if self.upgrading.isEmpty {
|
if self.upgrading.isEmpty {
|
||||||
@@ -117,35 +163,12 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
try await run(command, onProgress)
|
try await run(command, onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
|
||||||
var loggedMessages: [String] = []
|
|
||||||
|
|
||||||
let (process, _) = try! await Shell.attach(
|
|
||||||
command,
|
|
||||||
didReceiveOutput: { text, _ in
|
|
||||||
if !text.isEmpty {
|
|
||||||
Log.perf(text)
|
|
||||||
loggedMessages.append(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (number, text) = self.reportInstallationProgress(text) {
|
|
||||||
onProgress(.create(value: number, title: self.title, description: text))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
withTimeout: .minutes(15)
|
|
||||||
)
|
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
|
||||||
loggedMessages = []
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||||
// Reload and restart PHP versions
|
// Reload and restart PHP versions
|
||||||
onProgress(.create(value: 0.95, title: self.title, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: self.title, description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
// Ensure all symlinks are correctly linked
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Check which version of PHP are now installed
|
// Check which version of PHP are now installed
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@@ -164,9 +187,8 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
// Let the UI know that the installation has been completed
|
// Let the UI know that the installation has been completed
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 1,
|
value: 1,
|
||||||
title: "Operation completed!",
|
title: "phpman.steps.completed".localized,
|
||||||
description: "The installation has succeeded."
|
description: "phpman.steps.success".localized
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -21,13 +21,15 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
self.phpGuard = PhpGuard()
|
self.phpGuard = PhpGuard()
|
||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func getCommandTitle() -> String {
|
||||||
let progressTitle = "Removing PHP \(version)..."
|
return "phpman.steps.removing".localized("PHP \(version)...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: getCommandTitle(),
|
||||||
description: "Please wait while Homebrew removes PHP \(version)..."
|
description: "phpman.steps.wait".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
let command = """
|
let command = """
|
||||||
@@ -56,7 +58,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
if process.terminationStatus <= 0 {
|
||||||
onProgress(.create(value: 0.95, title: progressTitle, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(.create(value: 1, title: progressTitle, description: "The operation has succeeded."))
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
} else {
|
} else {
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
}
|
}
|
@@ -9,6 +9,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class FakeCommand: BrewCommand {
|
class FakeCommand: BrewCommand {
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "Hello"
|
||||||
|
}
|
||||||
|
|
||||||
let version: String
|
let version: String
|
||||||
|
|
||||||
init(version: String) {
|
init(version: String) {
|
||||||
|
@@ -24,4 +24,6 @@ protocol ValetListable {
|
|||||||
|
|
||||||
func getListableUrl() -> URL?
|
func getListableUrl() -> URL?
|
||||||
|
|
||||||
|
func getListableFavorited() -> Bool
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,11 @@ class ValetProxy: ValetListable {
|
|||||||
var target: String
|
var target: String
|
||||||
var secured: Bool = false
|
var secured: Bool = false
|
||||||
|
|
||||||
|
var favorited: Bool = false
|
||||||
|
var favoriteSignature: String {
|
||||||
|
"proxy:domain:\(domain).\(tld)|target:\(target)"
|
||||||
|
}
|
||||||
|
|
||||||
init(domain: String, target: String, secure: Bool, tld: String) {
|
init(domain: String, target: String, secure: Bool, tld: String) {
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
self.tld = tld
|
self.tld = tld
|
||||||
@@ -28,6 +33,8 @@ class ValetProxy: ValetListable {
|
|||||||
secure: false,
|
secure: false,
|
||||||
tld: configuration.tld
|
tld: configuration.tld
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.favorited = Favorites.shared.contains(domain: self.domain)
|
||||||
self.determineSecured()
|
self.determineSecured()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,12 +68,21 @@ class ValetProxy: ValetListable {
|
|||||||
return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)")
|
return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getListableFavorited() -> Bool {
|
||||||
|
return self.favorited
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interactions
|
// MARK: - Interactions
|
||||||
|
|
||||||
func determineSecured() {
|
func determineSecured() {
|
||||||
self.secured = FileSystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
|
self.secured = FileSystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toggleFavorite() {
|
||||||
|
self.favorited.toggle()
|
||||||
|
Favorites.shared.toggle(domain: self.favoriteSignature)
|
||||||
|
}
|
||||||
|
|
||||||
func toggleSecure() async throws {
|
func toggleSecure() async throws {
|
||||||
try await ValetInteractor.shared.toggleSecure(proxy: self)
|
try await ValetInteractor.shared.toggleSecure(proxy: self)
|
||||||
}
|
}
|
||||||
|
@@ -61,6 +61,11 @@ class ValetSite: ValetListable {
|
|||||||
?? "???"
|
?? "???"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var favorited: Bool = false
|
||||||
|
var favoriteSignature: String {
|
||||||
|
"site:domain:\(name).\(tld)|path:\(absolutePath)"
|
||||||
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
name: String,
|
name: String,
|
||||||
tld: String,
|
tld: String,
|
||||||
@@ -75,6 +80,7 @@ class ValetSite: ValetListable {
|
|||||||
self.secured = false
|
self.secured = false
|
||||||
|
|
||||||
if makeDeterminations {
|
if makeDeterminations {
|
||||||
|
self.favorited = Favorites.shared.contains(domain: favoriteSignature)
|
||||||
determineSecured()
|
determineSecured()
|
||||||
determineIsolated()
|
determineIsolated()
|
||||||
determineComposerPhpVersion()
|
determineComposerPhpVersion()
|
||||||
@@ -141,7 +147,7 @@ class ValetSite: ValetListable {
|
|||||||
self.determineDriverViaComposer()
|
self.determineDriverViaComposer()
|
||||||
|
|
||||||
if self.driver == nil {
|
if self.driver == nil {
|
||||||
self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath)
|
self.driver = ProjectTypeDetection.detectFallbackDependency(self.absolutePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,10 +161,16 @@ class ValetSite: ValetListable {
|
|||||||
private func determineDriverViaComposer() {
|
private func determineDriverViaComposer() {
|
||||||
self.driverDeterminedByComposer = true
|
self.driverDeterminedByComposer = true
|
||||||
|
|
||||||
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
|
for (key, value) in ProjectTypeDetection.SpecificDependencyList
|
||||||
if self.notableComposerDependencies.keys.contains(key) {
|
where notableComposerDependencies.keys.contains(key) {
|
||||||
self.driver = value
|
self.driver = value
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (key, value) in ProjectTypeDetection.CommonDependencyList
|
||||||
|
where notableComposerDependencies.keys.contains(key) {
|
||||||
|
self.driver = value
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,12 +311,21 @@ class ValetSite: ValetListable {
|
|||||||
return URL(string: "\(self.secured ? "https://" : "http://")\(self.name).\(Valet.shared.config.tld)")
|
return URL(string: "\(self.secured ? "https://" : "http://")\(self.name).\(Valet.shared.config.tld)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getListableFavorited() -> Bool {
|
||||||
|
return self.favorited
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interactions
|
// MARK: - Interactions
|
||||||
|
|
||||||
func toggleSecure() async throws {
|
func toggleSecure() async throws {
|
||||||
try await ValetInteractor.shared.toggleSecure(site: self)
|
try await ValetInteractor.shared.toggleSecure(site: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toggleFavorite() {
|
||||||
|
self.favorited.toggle()
|
||||||
|
Favorites.shared.toggle(domain: self.favoriteSignature)
|
||||||
|
}
|
||||||
|
|
||||||
func isolate(version: String) async throws {
|
func isolate(version: String) async throws {
|
||||||
try await ValetInteractor.shared.isolate(site: self, version: version)
|
try await ValetInteractor.shared.isolate(site: self, version: version)
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension Valet {
|
extension Valet {
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ extension Valet {
|
|||||||
public func notifyAboutUnsupportedTLD() {
|
public func notifyAboutUnsupportedTLD() {
|
||||||
if Valet.shared.config.tld != "test" && Preferences.isEnabled(.warnAboutNonStandardTLD) {
|
if Valet.shared.config.tld != "test" && Preferences.isEnabled(.warnAboutNonStandardTLD) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.warnings.tld_issue.title".localized,
|
title: "alert.warnings.tld_issue.title".localized,
|
||||||
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
||||||
description: "alert.warnings.tld_issue.description".localized
|
description: "alert.warnings.tld_issue.description".localized
|
||||||
@@ -33,7 +34,7 @@ extension Valet {
|
|||||||
|
|
||||||
public func notifyAboutOutdatedValetVersion(_ version: VersionNumber) {
|
public func notifyAboutOutdatedValetVersion(_ version: VersionNumber) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.min_valet_version.title".localized,
|
title: "alert.min_valet_version.title".localized,
|
||||||
subtitle: "alert.min_valet_version.info".localized(
|
subtitle: "alert.min_valet_version.info".localized(
|
||||||
@@ -60,7 +61,7 @@ extension Valet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.php_fpm_broken.title".localized,
|
title: "alert.php_fpm_broken.title".localized,
|
||||||
subtitle: "alert.php_fpm_broken.info".localized,
|
subtitle: "alert.php_fpm_broken.info".localized,
|
||||||
|
41
phpmon/Domain/Menu/AppMenu.swift
Normal file
41
phpmon/Domain/Menu/AppMenu.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// AppMenu.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 27/06/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
class AppMenu {
|
||||||
|
|
||||||
|
// MARK: - Main Menu
|
||||||
|
|
||||||
|
static var appMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[0].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
static var sitesMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[1].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
static var editMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[2].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
static var windowMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[3].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
static var helpMenu: NSMenu? {
|
||||||
|
return NSApplication.shared.mainMenu?.items[4].submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Submenu
|
||||||
|
|
||||||
|
static var actionsMenu: NSMenuItem? {
|
||||||
|
return sitesMenu?.items.last
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ extension MainMenu {
|
|||||||
|
|
||||||
@MainActor @objc func displayUnlinkedInfo() {
|
@MainActor @objc func displayUnlinkedInfo() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "phpman.unlinked.title".localized,
|
title: "phpman.unlinked.title".localized,
|
||||||
subtitle: "phpman.unlinked.desc".localized,
|
subtitle: "phpman.unlinked.desc".localized,
|
||||||
@@ -32,7 +33,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor @objc func fixHomebrewPermissions() {
|
@MainActor @objc func fixHomebrewPermissions() {
|
||||||
if !BetterAlert()
|
if !NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_homebrew_permissions.title".localized,
|
title: "alert.fix_homebrew_permissions.title".localized,
|
||||||
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
|
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
|
||||||
@@ -47,7 +48,7 @@ extension MainMenu {
|
|||||||
asyncExecution {
|
asyncExecution {
|
||||||
try Actions.fixHomebrewPermissions()
|
try Actions.fixHomebrewPermissions()
|
||||||
} success: {
|
} success: {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_homebrew_permissions_done.title".localized,
|
title: "alert.fix_homebrew_permissions_done.title".localized,
|
||||||
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
|
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
|
||||||
@@ -56,7 +57,7 @@ extension MainMenu {
|
|||||||
.withPrimary(text: "generic.ok".localized)
|
.withPrimary(text: "generic.ok".localized)
|
||||||
.show()
|
.show()
|
||||||
} failure: { error in
|
} failure: { error in
|
||||||
BetterAlert.show(for: error as! HomebrewPermissionError)
|
NVAlert.show(for: error as! HomebrewPermissionError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ extension MainMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.revert_description.title".localized,
|
title: "alert.revert_description.title".localized,
|
||||||
subtitle: "alert.revert_description.subtitle".localized(
|
subtitle: "alert.revert_description.subtitle".localized(
|
||||||
preset.textDescription
|
preset.textDescription
|
||||||
@@ -196,7 +197,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor @objc func showPresetHelp() {
|
@MainActor @objc func showPresetHelp() {
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "preset_help_title".localized,
|
title: "preset_help_title".localized,
|
||||||
subtitle: "preset_help_info".localized,
|
subtitle: "preset_help_info".localized,
|
||||||
description: "preset_help_desc".localized
|
description: "preset_help_desc".localized
|
||||||
@@ -263,7 +264,7 @@ extension MainMenu {
|
|||||||
Task { MainMenu.shared.switchToPhpVersion(version) }
|
Task { MainMenu.shared.switchToPhpVersion(version) }
|
||||||
} else {
|
} else {
|
||||||
Task {
|
Task {
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.php_switch_unavailable.title".localized,
|
title: "alert.php_switch_unavailable.title".localized,
|
||||||
subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
|
subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
|
||||||
).withPrimary(
|
).withPrimary(
|
||||||
@@ -283,12 +284,11 @@ extension MainMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@@ -298,13 +298,12 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func switchToPhpVersion(_ version: String) {
|
@objc func switchToPhpVersion(_ version: String) {
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@@ -325,13 +324,12 @@ extension MainMenu {
|
|||||||
*/
|
*/
|
||||||
func switchToPhp(_ version: String) async {
|
func switchToPhp(_ version: String) async {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
|
@@ -45,21 +45,16 @@ extension MainMenu {
|
|||||||
.broadcastServicesUpdate
|
.broadcastServicesUpdate
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
if behaviours.contains(.reloadsPhpInstallation) {
|
if behaviours.contains(.reloadsPhpInstallation) || behaviours.contains(.setsBusyUI) {
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.setsBusyUI) {
|
|
||||||
setBusyImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
var error: Error?
|
var error: Error?
|
||||||
|
|
||||||
do { try execute() } catch let e { error = e }
|
do { try execute() } catch let e {
|
||||||
|
error = e
|
||||||
if behaviours.contains(.setsBusyUI) {
|
Log.err(e)
|
||||||
PhpEnvironments.shared.isBusy = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor [self, error] in
|
Task { @MainActor [self, error] in
|
||||||
@@ -68,15 +63,18 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.updatesMenuBarContents) {
|
if behaviours.contains(.updatesMenuBarContents) {
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
} else if behaviours.contains(.setsBusyUI) {
|
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.broadcastServicesUpdate) {
|
if behaviours.contains(.broadcastServicesUpdate) {
|
||||||
Task { await ServicesManager.shared.reloadServicesStatus() }
|
Task { await ServicesManager.shared.reloadServicesStatus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if behaviours.contains(.setsBusyUI) {
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
if error != nil {
|
if error != nil {
|
||||||
return failure(error!)
|
return failure(error!)
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ extension MainMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !BetterAlert()
|
if !NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_my_valet.title".localized,
|
title: "alert.fix_my_valet.title".localized,
|
||||||
subtitle: "alert.fix_my_valet.info".localized(PhpEnvironments.brewPhpAlias)
|
subtitle: "alert.fix_my_valet.info".localized(PhpEnvironments.brewPhpAlias)
|
||||||
@@ -43,7 +44,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func presentAlertForMissingFormula() {
|
@MainActor private func presentAlertForMissingFormula() {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.php_formula_missing.title".localized,
|
title: "alert.php_formula_missing.title".localized,
|
||||||
subtitle: "alert.php_formula_missing.info".localized
|
subtitle: "alert.php_formula_missing.info".localized
|
||||||
@@ -53,7 +54,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func presentAlertForSameVersion() {
|
@MainActor private func presentAlertForSameVersion() {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_my_valet_done.title".localized,
|
title: "alert.fix_my_valet_done.title".localized,
|
||||||
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
||||||
@@ -64,7 +65,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func presentAlertForDifferentVersion(version: String) {
|
@MainActor private func presentAlertForDifferentVersion(version: String) {
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_my_valet_done.title".localized,
|
title: "alert.fix_my_valet_done.title".localized,
|
||||||
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
/**
|
/**
|
||||||
@@ -15,7 +16,7 @@ extension MainMenu {
|
|||||||
func startup() async {
|
func startup() async {
|
||||||
// Start with the icon
|
// Start with the icon
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
self.setStatusBar(image: NSImage.statusBarIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
if await Startup().checkEnvironment() {
|
if await Startup().checkEnvironment() {
|
||||||
@@ -32,19 +33,14 @@ extension MainMenu {
|
|||||||
// Determine what the `php` formula is aliased to
|
// Determine what the `php` formula is aliased to
|
||||||
await PhpEnvironments.shared.determinePhpAlias()
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
|
||||||
|
// Make sure that broken symlinks are removed ASAP
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Initialize preferences
|
// Initialize preferences
|
||||||
_ = Preferences.shared
|
_ = Preferences.shared
|
||||||
|
|
||||||
// Determine install method
|
// Put some useful diagnostics information in log
|
||||||
Log.info(BrewDiagnostics.customCaskInstalled
|
BrewDiagnostics.logBootInformation()
|
||||||
? "[BREW] The app has been installed via Homebrew Cask."
|
|
||||||
: "[BREW] The app has been installed directly (optimal)."
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.info(BrewDiagnostics.usesNginxFullFormula
|
|
||||||
? "[BREW] The app will be using the `nginx-full` formula."
|
|
||||||
: "[BREW] The app will be using the `nginx` formula."
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attempt to find out more info about Valet
|
// Attempt to find out more info about Valet
|
||||||
if Valet.shared.version != nil {
|
if Valet.shared.version != nil {
|
||||||
@@ -63,9 +59,6 @@ extension MainMenu {
|
|||||||
// Check for an alias conflict
|
// Check for an alias conflict
|
||||||
await BrewDiagnostics.checkForCaskConflict()
|
await BrewDiagnostics.checkForCaskConflict()
|
||||||
|
|
||||||
// Update the icon
|
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
|
|
||||||
// Attempt to find out if PHP-FPM is broken
|
// Attempt to find out if PHP-FPM is broken
|
||||||
PhpEnvironments.prepare()
|
PhpEnvironments.prepare()
|
||||||
|
|
||||||
@@ -76,7 +69,6 @@ extension MainMenu {
|
|||||||
WarningManager.shared.evaluateWarnings()
|
WarningManager.shared.evaluateWarnings()
|
||||||
|
|
||||||
// Set up the config watchers on launch (updated automatically when switching)
|
// Set up the config watchers on launch (updated automatically when switching)
|
||||||
Log.info("Setting up watchers...")
|
|
||||||
App.shared.handlePhpConfigWatcher()
|
App.shared.handlePhpConfigWatcher()
|
||||||
|
|
||||||
// Detect built-in and custom applications
|
// Detect built-in and custom applications
|
||||||
@@ -105,9 +97,36 @@ extension MainMenu {
|
|||||||
Valet.shared.notifyAboutUnsupportedTLD()
|
Valet.shared.notifyAboutUnsupportedTLD()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep track of which PHP versions are currently about to release
|
||||||
|
Log.info("Experimental PHP versions are: \(Constants.ExperimentalPhpVersions)")
|
||||||
|
|
||||||
// Find out which services are active
|
// Find out which services are active
|
||||||
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
||||||
|
|
||||||
|
// Post-launch stats and update check, but only if not running tests
|
||||||
|
await performPostLaunchActions()
|
||||||
|
|
||||||
|
// Check if the linked version has changed between launches of phpmon
|
||||||
|
PhpGuard().compareToLastGlobalVersion()
|
||||||
|
|
||||||
|
// We are ready!
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
|
||||||
|
// Finally!
|
||||||
|
Log.info("PHP Monitor is ready to serve!")
|
||||||
|
|
||||||
|
// Avoid showing the "startup timeout" alert
|
||||||
|
Startup.invalidateTimeoutTimer()
|
||||||
|
|
||||||
|
// Check if we upgraded from a previous version
|
||||||
|
AppUpdater.checkIfUpdateWasPerformed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Performs a set of post-launch actions, like incrementing stats and checking for updates.
|
||||||
|
(This code is skipped when running SwiftUI previews.)
|
||||||
|
*/
|
||||||
|
private func performPostLaunchActions() async {
|
||||||
if !isRunningSwiftUIPreview {
|
if !isRunningSwiftUIPreview {
|
||||||
Stats.incrementSuccessfulLaunchCount()
|
Stats.incrementSuccessfulLaunchCount()
|
||||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||||
@@ -121,15 +140,6 @@ extension MainMenu {
|
|||||||
await AppUpdater().checkForUpdates(userInitiated: false)
|
await AppUpdater().checkForUpdates(userInitiated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the linked version has changed between launches of phpmon
|
|
||||||
PhpGuard().compareToLastGlobalVersion()
|
|
||||||
|
|
||||||
// We are ready!
|
|
||||||
Log.info("PHP Monitor is ready to serve!")
|
|
||||||
|
|
||||||
// Check if we upgraded just now
|
|
||||||
AppUpdater.checkIfUpdateWasPerformed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,7 +147,7 @@ extension MainMenu {
|
|||||||
*/
|
*/
|
||||||
private func onEnvironmentFail() async {
|
private func onEnvironmentFail() async {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.cannot_start.title".localized,
|
title: "alert.cannot_start.title".localized,
|
||||||
subtitle: "alert.cannot_start.subtitle".localized,
|
subtitle: "alert.cannot_start.subtitle".localized,
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
|
|
||||||
@@ -16,7 +17,9 @@ extension MainMenu {
|
|||||||
|
|
||||||
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
||||||
// Mark as no longer busy
|
// Mark as no longer busy
|
||||||
|
Task { @MainActor in
|
||||||
PhpEnvironments.shared.isBusy = false
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
Task { // Things to do after reloading domain list data
|
Task { // Things to do after reloading domain list data
|
||||||
if Valet.installed {
|
if Valet.installed {
|
||||||
@@ -25,7 +28,7 @@ extension MainMenu {
|
|||||||
|
|
||||||
// Perform UI updates on main thread
|
// Perform UI updates on main thread
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
|
|
||||||
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
||||||
@@ -73,7 +76,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func suggestFixMyValet(failed version: String) {
|
@MainActor private func suggestFixMyValet(failed version: String) {
|
||||||
let outcome = BetterAlert()
|
let outcome = NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.php_switch_failed.title".localized(version),
|
title: "alert.php_switch_failed.title".localized(version),
|
||||||
subtitle: "alert.php_switch_failed.info".localized(version),
|
subtitle: "alert.php_switch_failed.info".localized(version),
|
||||||
@@ -88,7 +91,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func suggestFixMyComposer() {
|
@MainActor private func suggestFixMyComposer() {
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.global_composer_platform_issues.title".localized,
|
title: "alert.global_composer_platform_issues.title".localized,
|
||||||
subtitle: "alert.global_composer_platform_issues.subtitle".localized,
|
subtitle: "alert.global_composer_platform_issues.subtitle".localized,
|
||||||
description: "alert.global_composer_platform_issues.desc".localized
|
description: "alert.global_composer_platform_issues.desc".localized
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
|
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
|
||||||
@@ -37,8 +38,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
// MARK: - UI related
|
// MARK: - UI related
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Rebuilds the menu (either asynchronously or synchronously).
|
Rebuilds the menu on the main thread.
|
||||||
Defaults to rebuilding the menu asynchronously.
|
|
||||||
*/
|
*/
|
||||||
func rebuild() {
|
func rebuild() {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
@@ -80,13 +80,15 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func refreshActiveInstallation() {
|
@objc func refreshActiveInstallation() {
|
||||||
if !PhpEnvironments.shared.isBusy {
|
if !PhpEnvironments.shared.isBusy {
|
||||||
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
} else {
|
} else {
|
||||||
Log.perf("Skipping version refresh due to busy status!")
|
Log.perf("Skipping version refresh due to busy status!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||||
|
@available(*, deprecated, message: "Use the busy status instead")
|
||||||
@objc func updatePhpVersionInStatusBar() {
|
@objc func updatePhpVersionInStatusBar() {
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
@@ -119,7 +121,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
|
|
||||||
@objc func showIncompatiblePhpVersionsAlert() {
|
@objc func showIncompatiblePhpVersionsAlert() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "startup.unsupported_versions_explanation.title".localized,
|
title: "startup.unsupported_versions_explanation.title".localized,
|
||||||
subtitle: "startup.unsupported_versions_explanation.subtitle".localized(
|
subtitle: "startup.unsupported_versions_explanation.subtitle".localized(
|
||||||
PhpEnvironments.shared.incompatiblePhpVersions
|
PhpEnvironments.shared.incompatiblePhpVersions
|
||||||
@@ -139,7 +141,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func reloadPhpMonitorMenuInBackground() {
|
@objc func reloadPhpMonitorMenuInBackground() {
|
||||||
asyncExecution({
|
asyncExecution({
|
||||||
// This automatically reloads the menu
|
// This automatically reloads the menu
|
||||||
Log.info("Reloading information about the PHP installation (in the background)...")
|
Log.perf("Reloading information about the PHP installation (in the background)...")
|
||||||
}, behaviours: [
|
}, behaviours: [
|
||||||
.setsBusyUI,
|
.setsBusyUI,
|
||||||
.reloadsPhpInstallation,
|
.reloadsPhpInstallation,
|
||||||
@@ -150,13 +152,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
|
|
||||||
/** Refreshes the icon with the PHP version. */
|
/** Refreshes the icon with the PHP version. */
|
||||||
@objc func refreshIcon() {
|
@objc func refreshIcon() {
|
||||||
|
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
Log.perf("Refreshing icon: currently busy")
|
||||||
|
setStatusBar(image: NSImage.statusBarIcon)
|
||||||
} else {
|
} else {
|
||||||
|
Log.perf("Refreshing icon: no longer busy")
|
||||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||||
// Static icon has been requested
|
// Static icon has been requested
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
setStatusBar(image: NSImage.statusBarIconStatic)
|
||||||
} else {
|
} else {
|
||||||
// The dynamic icon has been requested
|
// The dynamic icon has been requested
|
||||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||||
@@ -172,13 +177,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon to be displayed as busy. */
|
|
||||||
@objc func setBusyImage() {
|
|
||||||
Task { @MainActor [self] in
|
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Menu Item Functionality
|
// MARK: - Menu Item Functionality
|
||||||
|
|
||||||
@objc func openAbout() {
|
@objc func openAbout() {
|
||||||
@@ -188,7 +186,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
|
|
||||||
@objc func openLiteModeInfo() {
|
@objc func openLiteModeInfo() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "lite_mode_explanation.title".localized,
|
title: "lite_mode_explanation.title".localized,
|
||||||
subtitle: "lite_mode_explanation.subtitle".localized,
|
subtitle: "lite_mode_explanation.subtitle".localized,
|
||||||
description: "lite_mode_explanation.description".localized
|
description: "lite_mode_explanation.description".localized
|
||||||
@@ -206,6 +204,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
PhpDoctorWindowController.show()
|
PhpDoctorWindowController.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openConfigGUI() {
|
||||||
|
PhpConfigManagerWindowController.show()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func openDomainList() {
|
@objc func openDomainList() {
|
||||||
DomainListVC.show()
|
DomainListVC.show()
|
||||||
}
|
}
|
||||||
@@ -214,6 +216,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
PhpVersionManagerWindowController.show()
|
PhpVersionManagerWindowController.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openPhpExtensionManager() {
|
||||||
|
PhpExtensionManagerWindowController.show()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func openDonate() {
|
@objc func openDonate() {
|
||||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ import Cocoa
|
|||||||
|
|
||||||
extension StatusMenu {
|
extension StatusMenu {
|
||||||
|
|
||||||
func addPhpVersionMenuItems() {
|
@MainActor func addPhpVersionMenuItems() {
|
||||||
if PhpEnvironments.phpInstall == nil {
|
if PhpEnvironments.phpInstall == nil {
|
||||||
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
||||||
addItems([
|
addItems([
|
||||||
@@ -34,7 +34,7 @@ extension StatusMenu {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPhpActionMenuItems() {
|
@MainActor func addPhpActionMenuItems() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
addItem(NSMenuItem(title: "mi_busy".localized))
|
addItem(NSMenuItem(title: "mi_busy".localized))
|
||||||
return
|
return
|
||||||
@@ -54,7 +54,7 @@ extension StatusMenu {
|
|||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
func addServicesManagerMenuItem() {
|
@MainActor func addServicesManagerMenuItem() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSwitchToPhpMenuItems() {
|
@MainActor func addSwitchToPhpMenuItems() {
|
||||||
var shortcutKey = 1
|
var shortcutKey = 1
|
||||||
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
||||||
// Get the short and long version
|
// Get the short and long version
|
||||||
@@ -102,14 +102,14 @@ extension StatusMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addLiteModeMenuItem() {
|
@MainActor func addLiteModeMenuItem() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPreferencesMenuItems() {
|
@MainActor func addPreferencesMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_preferences".localized,
|
NSMenuItem(title: "mi_preferences".localized,
|
||||||
@@ -119,7 +119,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addCoreMenuItems() {
|
@MainActor func addCoreMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_about".localized,
|
NSMenuItem(title: "mi_about".localized,
|
||||||
@@ -131,7 +131,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Valet
|
// MARK: - Valet
|
||||||
|
|
||||||
func addValetMenuItems() {
|
@MainActor func addValetMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_valet".localized),
|
HeaderView.asMenuItem(text: "mi_valet".localized),
|
||||||
NSMenuItem(title: "mi_valet_config".localized,
|
NSMenuItem(title: "mi_valet_config".localized,
|
||||||
@@ -146,12 +146,15 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - PHP Configuration
|
// MARK: - PHP Configuration
|
||||||
|
|
||||||
func addConfigurationMenuItems() {
|
@MainActor func addConfigurationMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
||||||
NSMenuItem(title: "mi_php_version_manager".localized,
|
NSMenuItem(title: "mi_php_version_manager".localized,
|
||||||
action: #selector(MainMenu.openPhpVersionManager),
|
action: #selector(MainMenu.openPhpVersionManager),
|
||||||
keyEquivalent: "m"),
|
keyEquivalent: "m"),
|
||||||
|
NSMenuItem(title: "mi_php_ext_manager".localized,
|
||||||
|
action: #selector(MainMenu.openPhpExtensionManager),
|
||||||
|
keyEquivalent: "e"),
|
||||||
NSMenuItem(title: "mi_php_config".localized,
|
NSMenuItem(title: "mi_php_config".localized,
|
||||||
action: #selector(MainMenu.openActiveConfigFolder),
|
action: #selector(MainMenu.openActiveConfigFolder),
|
||||||
keyEquivalent: "c"),
|
keyEquivalent: "c"),
|
||||||
@@ -166,7 +169,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Composer
|
// MARK: - Composer
|
||||||
|
|
||||||
func addComposerMenuItems() {
|
@MainActor func addComposerMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_composer".localized),
|
HeaderView.asMenuItem(text: "mi_composer".localized),
|
||||||
NSMenuItem(
|
NSMenuItem(
|
||||||
@@ -187,7 +190,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Stats
|
// MARK: - Stats
|
||||||
|
|
||||||
func addStatsMenuItem() {
|
@MainActor func addStatsMenuItem() {
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("Not showing stats menu item if no PHP version is linked.")
|
Log.info("Not showing stats menu item if no PHP version is linked.")
|
||||||
return
|
return
|
||||||
@@ -204,7 +207,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Extensions
|
// MARK: - Extensions
|
||||||
|
|
||||||
func addExtensionsMenuItems() {
|
@MainActor func addExtensionsMenuItems() {
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
||||||
return
|
return
|
||||||
@@ -225,7 +228,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Presets
|
// MARK: - Presets
|
||||||
|
|
||||||
func addPresetsMenuItem() {
|
@MainActor func addPresetsMenuItem() {
|
||||||
guard let presets = Preferences.custom.presets else {
|
guard let presets = Preferences.custom.presets else {
|
||||||
addEmptyPresetHelp()
|
addEmptyPresetHelp()
|
||||||
return
|
return
|
||||||
@@ -239,7 +242,7 @@ extension StatusMenu {
|
|||||||
addLoadedPresets()
|
addLoadedPresets()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addEmptyPresetHelp() {
|
@MainActor private func addEmptyPresetHelp() {
|
||||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||||
NSMenuItem(title: "mi_no_presets".localized),
|
NSMenuItem(title: "mi_no_presets".localized),
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
@@ -248,7 +251,7 @@ extension StatusMenu {
|
|||||||
], target: MainMenu.shared))
|
], target: MainMenu.shared))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addLoadedPresets() {
|
@MainActor private func addLoadedPresets() {
|
||||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)
|
HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)
|
||||||
@@ -263,7 +266,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Xdebug
|
// MARK: - Xdebug
|
||||||
|
|
||||||
func addXdebugMenuItem() {
|
@MainActor func addXdebugMenuItem() {
|
||||||
if !Xdebug.enabled {
|
if !Xdebug.enabled {
|
||||||
addItem(NSMenuItem.separator())
|
addItem(NSMenuItem.separator())
|
||||||
return
|
return
|
||||||
@@ -283,7 +286,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - PHP Doctor
|
// MARK: - PHP Doctor
|
||||||
|
|
||||||
func addPhpDoctorMenuItem() {
|
@MainActor func addPhpDoctorMenuItem() {
|
||||||
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
|
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
|
||||||
!WarningManager.shared.hasWarnings() {
|
!WarningManager.shared.hasWarnings() {
|
||||||
return
|
return
|
||||||
@@ -299,7 +302,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - First Aid & Services
|
// MARK: - First Aid & Services
|
||||||
|
|
||||||
func addFirstAidAndServicesMenuItems() {
|
@MainActor func addFirstAidAndServicesMenuItems() {
|
||||||
let services = NSMenuItem(title: "mi_other".localized)
|
let services = NSMenuItem(title: "mi_other".localized)
|
||||||
|
|
||||||
var items: [NSMenuItem] = [
|
var items: [NSMenuItem] = [
|
||||||
@@ -356,7 +359,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Other helper methods to generate menu items
|
// MARK: - Other helper methods to generate menu items
|
||||||
|
|
||||||
func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
@MainActor func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
||||||
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
||||||
|
|
||||||
let menuItem = ExtensionMenuItem(
|
let menuItem = ExtensionMenuItem(
|
||||||
|
@@ -9,7 +9,7 @@ import Cocoa
|
|||||||
|
|
||||||
class StatusMenu: NSMenu {
|
class StatusMenu: NSMenu {
|
||||||
// swiftlint:disable cyclomatic_complexity
|
// swiftlint:disable cyclomatic_complexity
|
||||||
func addMenuItems() {
|
@MainActor func addMenuItems() {
|
||||||
addPhpVersionMenuItems()
|
addPhpVersionMenuItems()
|
||||||
addItem(NSMenuItem.separator())
|
addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
|
@@ -1,121 +0,0 @@
|
|||||||
//
|
|
||||||
// Notice.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 16/02/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class BetterAlert {
|
|
||||||
|
|
||||||
var windowController: NSWindowController!
|
|
||||||
|
|
||||||
var noticeVC: BetterAlertVC {
|
|
||||||
return self.windowController.contentViewController as! BetterAlertVC
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
|
||||||
|
|
||||||
self.windowController = storyboard.instantiateController(
|
|
||||||
withIdentifier: "noticeWindow"
|
|
||||||
) as? NSWindowController
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func make() -> BetterAlert {
|
|
||||||
return BetterAlert()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func withPrimary(
|
|
||||||
text: String,
|
|
||||||
action: @MainActor @escaping (BetterAlertVC) -> Void = { vc in
|
|
||||||
vc.close(with: .alertFirstButtonReturn)
|
|
||||||
}
|
|
||||||
) -> Self {
|
|
||||||
self.noticeVC.buttonPrimary.title = text
|
|
||||||
self.noticeVC.actionPrimary = action
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
public func withSecondary(
|
|
||||||
text: String,
|
|
||||||
action: (@MainActor (BetterAlertVC) -> Void)? = { vc in
|
|
||||||
vc.close(with: .alertSecondButtonReturn)
|
|
||||||
}
|
|
||||||
) -> Self {
|
|
||||||
self.noticeVC.buttonSecondary.title = text
|
|
||||||
self.noticeVC.actionSecondary = action
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
public func withTertiary(
|
|
||||||
text: String = "",
|
|
||||||
action: (@MainActor (BetterAlertVC) -> Void)? = nil
|
|
||||||
) -> Self {
|
|
||||||
if text == "" {
|
|
||||||
self.noticeVC.buttonTertiary.bezelStyle = .helpButton
|
|
||||||
}
|
|
||||||
self.noticeVC.buttonTertiary.title = text
|
|
||||||
self.noticeVC.actionTertiary = action
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
public func withInformation(
|
|
||||||
title: String,
|
|
||||||
subtitle: String,
|
|
||||||
description: String = ""
|
|
||||||
) -> Self {
|
|
||||||
self.noticeVC.labelTitle.stringValue = title
|
|
||||||
self.noticeVC.labelSubtitle.stringValue = subtitle
|
|
||||||
self.noticeVC.labelDescription.stringValue = description
|
|
||||||
|
|
||||||
// If the description is missing, handle the excess space and change the top margin
|
|
||||||
if description == "" {
|
|
||||||
self.noticeVC.labelDescription.isHidden = true
|
|
||||||
self.noticeVC.primaryButtonTopMargin.constant = 0
|
|
||||||
}
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Shows the modal and returns a ModalResponse.
|
|
||||||
If you wish to simply show the alert and disregard the outcome, use `show`.
|
|
||||||
*/
|
|
||||||
@MainActor public func runModal() -> NSApplication.ModalResponse {
|
|
||||||
if !Thread.isMainThread {
|
|
||||||
fatalError("You should always present alerts on the main thread!")
|
|
||||||
}
|
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
windowController.window?.makeKeyAndOrderFront(nil)
|
|
||||||
windowController.window?.setCenterPosition(offsetY: 70)
|
|
||||||
return NSApplication.shared.runModal(for: windowController.window!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Shows the modal and returns true if the user pressed the primary button. */
|
|
||||||
@MainActor public func didSelectPrimary() -> Bool {
|
|
||||||
return self.runModal() == .alertFirstButtonReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Shows the modal and does not return anything.
|
|
||||||
*/
|
|
||||||
@MainActor public func show() {
|
|
||||||
_ = self.runModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Shows the modal for a particular error.
|
|
||||||
*/
|
|
||||||
@MainActor public static func show(for error: Error & AlertableError) {
|
|
||||||
let key = error.getErrorMessageKey()
|
|
||||||
return BetterAlert().withInformation(
|
|
||||||
title: "\(key).title".localized,
|
|
||||||
subtitle: "\(key).description".localized
|
|
||||||
).withPrimary(text: "generic.ok".localized).show()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,77 +0,0 @@
|
|||||||
//
|
|
||||||
// NoticeVC.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 16/02/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Cocoa
|
|
||||||
|
|
||||||
class BetterAlertVC: NSViewController {
|
|
||||||
|
|
||||||
// MARK: - Outlets
|
|
||||||
|
|
||||||
@IBOutlet weak var labelTitle: NSTextField!
|
|
||||||
@IBOutlet weak var labelSubtitle: NSTextField!
|
|
||||||
@IBOutlet weak var labelDescription: NSTextField!
|
|
||||||
|
|
||||||
@IBOutlet weak var buttonPrimary: NSButton!
|
|
||||||
@IBOutlet weak var buttonSecondary: NSButton!
|
|
||||||
@IBOutlet weak var buttonTertiary: NSButton!
|
|
||||||
|
|
||||||
var actionPrimary: (@MainActor (BetterAlertVC) -> Void) = { _ in }
|
|
||||||
var actionSecondary: (@MainActor (BetterAlertVC) -> Void)?
|
|
||||||
var actionTertiary: (@MainActor (BetterAlertVC) -> Void)?
|
|
||||||
|
|
||||||
@IBOutlet weak var imageView: NSImageView!
|
|
||||||
|
|
||||||
@IBOutlet weak var primaryButtonTopMargin: NSLayoutConstraint!
|
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
|
||||||
|
|
||||||
override func viewWillAppear() {
|
|
||||||
imageView.image = NSApp.applicationIconImage
|
|
||||||
|
|
||||||
if actionSecondary == nil {
|
|
||||||
buttonSecondary.isHidden = true
|
|
||||||
}
|
|
||||||
if actionTertiary == nil {
|
|
||||||
buttonTertiary.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear() {
|
|
||||||
view.window?.makeFirstResponder(buttonPrimary)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
// Log.perf("deinit: \(String(describing: self)).\(#function)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Outlet Actions
|
|
||||||
|
|
||||||
@IBAction func primaryButtonAction(_ sender: Any) {
|
|
||||||
self.actionPrimary(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func secondaryButtonAction(_ sender: Any) {
|
|
||||||
if self.actionSecondary != nil {
|
|
||||||
self.actionSecondary!(self)
|
|
||||||
} else {
|
|
||||||
self.close(with: .alertSecondButtonReturn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func tertiaryButtonAction(_ sender: Any) {
|
|
||||||
if self.actionTertiary != nil {
|
|
||||||
self.actionTertiary!(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor public func close(with code: NSApplication.ModalResponse) {
|
|
||||||
self.view.window?.close()
|
|
||||||
NSApplication.shared.stopModal(withCode: code)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class PhpGuard {
|
class PhpGuard {
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ class PhpGuard {
|
|||||||
Log.info("PHP Guard noticed a different PHP version. An alert will be displayed!")
|
Log.info("PHP Guard noticed a different PHP version. An alert will be displayed!")
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert()
|
NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "startup.version_mismatch.title".localized,
|
title: "startup.version_mismatch.title".localized,
|
||||||
subtitle: "startup.version_mismatch.subtitle".localized(
|
subtitle: "startup.version_mismatch.subtitle".localized(
|
||||||
|
@@ -20,6 +20,7 @@ enum PreferenceName: String, Codable {
|
|||||||
case globalHotkey = "global_hotkey"
|
case globalHotkey = "global_hotkey"
|
||||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||||
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
||||||
|
case languageOverride = "language_override"
|
||||||
|
|
||||||
// APPEARANCE
|
// APPEARANCE
|
||||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||||
@@ -84,7 +85,8 @@ enum PreferenceName: String, Codable {
|
|||||||
],
|
],
|
||||||
.string: [
|
.string: [
|
||||||
.globalHotkey,
|
.globalHotkey,
|
||||||
.iconTypeToDisplay
|
.iconTypeToDisplay,
|
||||||
|
.languageOverride
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,10 @@ class Preferences {
|
|||||||
environmentVariables: [:]
|
environmentVariables: [:]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if isRunningSwiftUIPreview {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Task { await loadCustomPreferences() }
|
Task { await loadCustomPreferences() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +55,7 @@ class Preferences {
|
|||||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||||
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
||||||
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
||||||
|
PreferenceName.languageOverride.rawValue: "",
|
||||||
|
|
||||||
/// Preferences: Appearance
|
/// Preferences: Appearance
|
||||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||||
|
@@ -17,7 +17,9 @@ class GeneralPreferencesVC: GenericPreferenceVC {
|
|||||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||||
|
|
||||||
_ = vc.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
_ = vc
|
||||||
|
.addView(when: true, vc.getLanguageOptionsPV())
|
||||||
|
.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
||||||
.addView(when: true, vc.getAutoRestartServicesPV())
|
.addView(when: true, vc.getAutoRestartServicesPV())
|
||||||
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
||||||
.addView(when: true, vc.getShortcutPV())
|
.addView(when: true, vc.getShortcutPV())
|
||||||
|
@@ -48,11 +48,44 @@ class GenericPreferenceVC: NSViewController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLanguageOptionsPV() -> NSView {
|
||||||
|
var options = Bundle.main.localizations
|
||||||
|
.filter({ $0 != "Base"})
|
||||||
|
.map({ lang in
|
||||||
|
return PreferenceDropdownOption(
|
||||||
|
label: Locale.current.localizedString(forLanguageCode: lang)!,
|
||||||
|
value: lang
|
||||||
|
)
|
||||||
|
})
|
||||||
|
options.insert(PreferenceDropdownOption(label: "System Default", value: ""), at: 0)
|
||||||
|
|
||||||
|
return SelectPreferenceView.make(
|
||||||
|
sectionText: "prefs.language".localized,
|
||||||
|
descriptionText: "prefs.language_options_desc".localized,
|
||||||
|
options: options,
|
||||||
|
preference: .languageOverride,
|
||||||
|
action: {
|
||||||
|
MainMenu.shared.refreshIcon()
|
||||||
|
MainMenu.shared.rebuild()
|
||||||
|
|
||||||
|
if let window = App.shared.preferencesWindowController?.window {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "alert.language_changed.title".localized
|
||||||
|
alert.informativeText = "alert.language_changed.subtitle".localized
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.addButton(withTitle: "generic.ok".localized)
|
||||||
|
alert.beginSheetModal(for: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func getIconOptionsPV() -> NSView {
|
func getIconOptionsPV() -> NSView {
|
||||||
return SelectPreferenceView.make(
|
return SelectPreferenceView.make(
|
||||||
sectionText: "",
|
sectionText: "",
|
||||||
descriptionText: "prefs.icon_options_desc".localized,
|
descriptionText: "prefs.icon_options_desc".localized,
|
||||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
options: MenuBarIcon.allCases
|
||||||
|
.map({ return PreferenceDropdownOption(label: $0.rawValue, value: $0.rawValue) }),
|
||||||
localizationPrefix: "prefs.icon_options",
|
localizationPrefix: "prefs.icon_options",
|
||||||
preference: .iconTypeToDisplay,
|
preference: .iconTypeToDisplay,
|
||||||
action: {
|
action: {
|
||||||
|
@@ -65,9 +65,11 @@ class PreferencesWindowController: PMWindowController {
|
|||||||
App.shared.preferencesWindowController?.showWindow(self)
|
App.shared.preferencesWindowController?.showWindow(self)
|
||||||
|
|
||||||
if justCreated {
|
if justCreated {
|
||||||
App.shared.preferencesWindowController?.positionWindowInTopLeftCorner()
|
App.shared.preferencesWindowController?.positionWindowInTopRightCorner()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App.shared.preferencesWindowController?.window?.orderFrontRegardless()
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
class Stats {
|
class Stats {
|
||||||
|
|
||||||
@@ -84,6 +85,10 @@ class Stats {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func clearCurrentGlobalPhpVersion() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: InternalStats.lastGlobalPhpVersion.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine if the sponsor message should be displayed.
|
Determine if the sponsor message should be displayed.
|
||||||
|
|
||||||
@@ -119,7 +124,7 @@ class Stats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
let donate = BetterAlert()
|
let donate = NVAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "startup.sponsor_encouragement.title".localized,
|
title: "startup.sponsor_encouragement.title".localized,
|
||||||
subtitle: "startup.sponsor_encouragement.subtitle".localized,
|
subtitle: "startup.sponsor_encouragement.subtitle".localized,
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
|
@@ -9,30 +9,34 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class SelectPreferenceView: NSView, XibLoadable {
|
struct PreferenceDropdownOption {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectPreferenceView: NSView, XibLoadable {
|
||||||
@IBOutlet weak var labelSection: NSTextField!
|
@IBOutlet weak var labelSection: NSTextField!
|
||||||
@IBOutlet weak var labelDescription: NSTextField!
|
@IBOutlet weak var labelDescription: NSTextField!
|
||||||
@IBOutlet weak var popupButton: NSPopUpButton!
|
@IBOutlet weak var popupButton: NSPopUpButton!
|
||||||
|
|
||||||
var localizationPrefix: String = ""
|
var localizationPrefix: String?
|
||||||
var imagePrefix: String?
|
var imagePrefix: String?
|
||||||
|
|
||||||
var options: [String] = [] {
|
var options: [PreferenceDropdownOption] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
self.popupButton.removeAllItems()
|
self.popupButton.removeAllItems()
|
||||||
self.options.forEach { value in
|
self.options.forEach { option in
|
||||||
self.popupButton.addItem(
|
if let prefix = localizationPrefix {
|
||||||
withTitle: "\(localizationPrefix).\(value)".localized
|
self.popupButton.addItem(withTitle: "\(prefix).\(option.label)".localized)
|
||||||
)
|
} else {
|
||||||
}
|
self.popupButton.addItem(withTitle: option.label)
|
||||||
|
}
|
||||||
if imagePrefix == nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let prefix = imagePrefix {
|
||||||
self.popupButton.itemArray.enumerated().forEach { item in
|
self.popupButton.itemArray.enumerated().forEach { item in
|
||||||
item.element.image = NSImage(named: "\(imagePrefix!)_\(self.options[item.offset])")
|
item.element.image = NSImage(named: "\(prefix)_\(self.options[item.offset].value)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,19 +47,18 @@ class SelectPreferenceView: NSView, XibLoadable {
|
|||||||
didSet {
|
didSet {
|
||||||
let value = Preferences.preferences[preference] as! String
|
let value = Preferences.preferences[preference] as! String
|
||||||
self.options.enumerated().forEach { option in
|
self.options.enumerated().forEach { option in
|
||||||
if option.element == value {
|
if option.element.value == value {
|
||||||
self.popupButton.selectItem(at: option.offset)
|
self.popupButton.selectItem(at: option.offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable function_parameter_count
|
|
||||||
static func make(
|
static func make(
|
||||||
sectionText: String,
|
sectionText: String,
|
||||||
descriptionText: String,
|
descriptionText: String,
|
||||||
options: [String],
|
options: [PreferenceDropdownOption],
|
||||||
localizationPrefix: String,
|
localizationPrefix: String? = nil,
|
||||||
imagePrefix: String? = nil,
|
imagePrefix: String? = nil,
|
||||||
preference: PreferenceName,
|
preference: PreferenceName,
|
||||||
action: @escaping () -> Void) -> NSView {
|
action: @escaping () -> Void) -> NSView {
|
||||||
@@ -72,11 +75,10 @@ class SelectPreferenceView: NSView, XibLoadable {
|
|||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
// swiftlint:enable function_parameter_count
|
|
||||||
|
|
||||||
@IBAction func valueChanged(_ sender: Any) {
|
@IBAction func valueChanged(_ sender: Any) {
|
||||||
let index = self.popupButton.indexOfSelectedItem
|
let index = self.popupButton.indexOfSelectedItem
|
||||||
Preferences.update(.iconTypeToDisplay, value: self.options[index])
|
Preferences.update(self.preference, value: self.options[index].value)
|
||||||
self.action()
|
self.action()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@@ -13,16 +13,16 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="183" y="5" width="395" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="13" y="29" width="154" height="16"/>
|
<rect key="frame" x="-2" y="29" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YaB-Tg-Ir3">
|
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YaB-Tg-Ir3">
|
||||||
<rect key="frame" x="182" y="23" width="110" height="25"/>
|
<rect key="frame" x="167" y="23" width="110" height="25"/>
|
||||||
<popUpButtonCell key="cell" type="push" title="Icon Option" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SaA-mm-HBo" id="Su6-C4-aGo">
|
<popUpButtonCell key="cell" type="push" title="Icon Option" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SaA-mm-HBo" id="Su6-C4-aGo">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="menu"/>
|
<font key="font" metaFont="menu"/>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
||||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
||||||
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="15" id="Ztd-uk-4aw"/>
|
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" id="aBU-J8-gRK"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
struct Preset: Codable, Equatable {
|
struct Preset: Codable, Equatable {
|
||||||
let name: String
|
let name: String
|
||||||
@@ -139,7 +140,7 @@ struct Preset: Codable, Equatable {
|
|||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
BetterAlert().withInformation(
|
NVAlert().withInformation(
|
||||||
title: "alert.php_switch_unavailable.title".localized,
|
title: "alert.php_switch_unavailable.title".localized,
|
||||||
subtitle: "alert.php_switch_unavailable.subtitle".localized(version!),
|
subtitle: "alert.php_switch_unavailable.subtitle".localized(version!),
|
||||||
description: "alert.php_switch_unavailable.info".localized(
|
description: "alert.php_switch_unavailable.info".localized(
|
||||||
|
@@ -29,7 +29,7 @@ struct BlockingOverlayView<Content: View>: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
content().opacity(isBlocking ? 0.2 : 1)
|
content().opacity(isBlocking ? 0 : 1)
|
||||||
if isBlocking {
|
if isBlocking {
|
||||||
VStack {
|
VStack {
|
||||||
ActivityIndicator()
|
ActivityIndicator()
|
||||||
@@ -44,7 +44,8 @@ struct BlockingOverlayView<Content: View>: View {
|
|||||||
.padding(.top, -4)
|
.padding(.top, -4)
|
||||||
}.padding(60)
|
}.padding(60)
|
||||||
}
|
}
|
||||||
}.background(Color.white)
|
}
|
||||||
|
.background(Color.spinnerBackground)
|
||||||
.disabled(isBlocking)
|
.disabled(isBlocking)
|
||||||
}
|
}
|
||||||
}
|
}
|
36
phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift
Normal file
36
phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// CustomButtonStyles.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 15/03/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct CustomButtonStyle: ButtonStyle {
|
||||||
|
@Environment(\.isEnabled) var isEnabled
|
||||||
|
|
||||||
|
public func makeBody(configuration: Self.Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.background(.statusColorBlue, in: .rect(cornerRadius: 8, style: .continuous))
|
||||||
|
.opacity({
|
||||||
|
if configuration.isPressed {
|
||||||
|
return 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isEnabled {
|
||||||
|
return 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1.0
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ButtonStyle where Self == CustomButtonStyle {
|
||||||
|
static var custom: CustomButtonStyle { .init() }
|
||||||
|
}
|
@@ -10,6 +10,11 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HelpButton: View {
|
struct HelpButton: View {
|
||||||
|
@State var frameSize: CGFloat = 14
|
||||||
|
@State var textSize: CGFloat = 12
|
||||||
|
@State var shadowOpacity: CGFloat = 0.3
|
||||||
|
@State var shadowRadius: CGFloat = 1
|
||||||
|
|
||||||
var action: () -> Void
|
var action: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -18,24 +23,25 @@ struct HelpButton: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.strokeBorder(Color(NSColor.separatorColor), lineWidth: 0.5)
|
.strokeBorder(Color(NSColor.separatorColor), lineWidth: 0.5)
|
||||||
.background(Circle().foregroundColor(Color(NSColor.controlColor)).opacity(0.7))
|
.background(Circle().foregroundColor(Color(NSColor.controlColor)).opacity(0.7))
|
||||||
.shadow(color: Color(NSColor.separatorColor).opacity(0.3), radius: 1)
|
.shadow(color: Color(NSColor.separatorColor)
|
||||||
.frame(width: 14, height: 14)
|
.opacity(shadowOpacity), radius: shadowRadius)
|
||||||
Text("?").font(.system(size: 12, weight: .medium))
|
.frame(width: frameSize, height: frameSize)
|
||||||
|
Text("?").font(.system(size: textSize, weight: .medium))
|
||||||
.foregroundColor(Color(NSColor.labelColor))
|
.foregroundColor(Color(NSColor.labelColor))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.buttonStyle(BorderlessButtonStyle())
|
.buttonStyle(BorderlessButtonStyle())
|
||||||
.focusable(false)
|
.focusable(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct HelpButton_Previews: PreviewProvider {
|
#Preview("Light Mode") {
|
||||||
static var previews: some View {
|
HelpButton(action: {})
|
||||||
Group {
|
.padding(100)
|
||||||
HelpButton(action: {}).padding()
|
|
||||||
.previewDisplayName("Light Mode")
|
|
||||||
HelpButton(action: {}).padding().preferredColorScheme(.dark)
|
|
||||||
.previewDisplayName("Dark Mode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview("Dark Mode") {
|
||||||
|
HelpButton(action: {})
|
||||||
|
.padding(100)
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,9 @@ var isRunningSwiftUIPreview: Bool {
|
|||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
public static var appPrimary: Color = Color("AppColor")
|
public static var appPrimary: Color = Color("AppColor")
|
||||||
public static var appSecondary: Color = Color("AppSecondary")
|
|
||||||
|
// This next one is generated automatically via asset catalogs now
|
||||||
|
// public static var appSecondary: Color = Color("AppSecondary")
|
||||||
|
|
||||||
public static var debug: Color = {
|
public static var debug: Color = {
|
||||||
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
||||||
|
67
phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift
Normal file
67
phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
//
|
||||||
|
// NoDomainsView.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 19/03/2024.
|
||||||
|
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UnavailableContentView: View {
|
||||||
|
var title: String
|
||||||
|
var description: String
|
||||||
|
var icon: String
|
||||||
|
var button: String?
|
||||||
|
var action: (() -> Void)?
|
||||||
|
|
||||||
|
init(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
icon: String,
|
||||||
|
button: String? = nil,
|
||||||
|
action: (() -> Void)? = nil
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.icon = icon
|
||||||
|
self.button = button
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
Image(systemName: self.icon)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 48, height: 48)
|
||||||
|
.foregroundColor(Color.appPrimary)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
Text(self.title)
|
||||||
|
.font(.system(size: 18, weight: .bold))
|
||||||
|
|
||||||
|
Text(self.description)
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
if self.button != nil {
|
||||||
|
Button(self.button!) {
|
||||||
|
self.action!()
|
||||||
|
}.buttonStyle(.custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(30)
|
||||||
|
.frame(maxWidth: 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
UnavailableContentView(
|
||||||
|
title: "domain_list.domains_empty.title".localized,
|
||||||
|
description: "domain_list.domains_empty.desc".localized,
|
||||||
|
icon: "globe",
|
||||||
|
button: "domain_list.domains_empty.button".localized,
|
||||||
|
action: {}
|
||||||
|
)
|
||||||
|
}
|
@@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// NoDomainResults.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 15/08/2022.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct NoDomainResults: View {
|
|
||||||
@State var searching: Bool = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .center, spacing: 15) {
|
|
||||||
Image(systemName: searching ? "magnifyingglass.circle.fill" : "questionmark.circle.fill")
|
|
||||||
.resizable()
|
|
||||||
.renderingMode(.template)
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
VStack(alignment: .center) {
|
|
||||||
Text(
|
|
||||||
searching
|
|
||||||
? "domain_list.no_domains_for_search_query".localizedForSwiftUI
|
|
||||||
: "domain_list.no_domains".localizedForSwiftUI
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minWidth: 0, maxWidth: .infinity)
|
|
||||||
.padding(25)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NoDomainResults_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
NoDomainResults()
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user