mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-08 04:20:07 +02:00
Compare commits
85 Commits
Author | SHA1 | Date | |
---|---|---|---|
70c5aadb7f | |||
a731f15cf7 | |||
ab4c436202 | |||
c0231690d4 | |||
988e9d3351 | |||
2f119d4332 | |||
d83c629a7b | |||
e7d98dbeae | |||
f3d5946743 | |||
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 | |||
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 | |||
c42188b717 | |||
cc251686f9 | |||
6fd6241567 | |||
c8ab2e67f6 | |||
f82ab913c6 | |||
58943148fa | |||
8a46b9d374 | |||
a62ebcff92 | |||
541378f3f9 | |||
e6f1d7e834 | |||
20d19f2f92 | |||
91bc347e57 | |||
e05300b25b | |||
1ae7a20870 | |||
5594130ccd | |||
b9c7cdb3cc | |||
00b4760b85 | |||
9a35014d2a | |||
7cba25b52e | |||
c6c3996c7b | |||
03c96a1d16 | |||
a6fa4b240f | |||
7e78026d06 |
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 = "1500"
|
||||||
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 = "1500"
|
||||||
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 = "1500"
|
||||||
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 = "1500"
|
||||||
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 = "1500"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -112,13 +112,15 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
|
|||||||
|
|
||||||
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
||||||
|
|
||||||
|
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
|
||||||
|
|
||||||
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
||||||
|
|
||||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
|
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.3.
|
||||||
|
|
||||||
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
|
|||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | 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 |
|
| 6.2 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
|
||||||
## Legacy versions
|
## Legacy versions
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ These versions of PHP Monitor are no longer supported, but if you’re using an
|
|||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
|
| 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 |
|
| 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.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 |
|
||||||
|
@ -14,8 +14,6 @@ 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 {
|
||||||
|
@ -19,11 +19,40 @@ struct Constants {
|
|||||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The PHP versions that are considered pre-release versions.
|
PHP Monitor supplies a hardcoded list of PHP packages in its own
|
||||||
|
PHP Version Manager.
|
||||||
|
|
||||||
|
This hardcoded list will expire and will need to be modified when
|
||||||
|
the cutoff date occurs, which is when the `php` formula will
|
||||||
|
become PHP 8.4, and a new build will need to be made.
|
||||||
|
|
||||||
|
If users launch an older version of the app, then a warning
|
||||||
|
will be displayed to let them know that certain operations
|
||||||
|
will not work correctly and that they need to update their app.
|
||||||
*/
|
*/
|
||||||
static let ExperimentalPhpVersions: Set = [
|
static let PhpFormulaeCutoffDate = "2024-11-01"
|
||||||
"8.3", "8.4"
|
|
||||||
]
|
/**
|
||||||
|
* The PHP versions that are considered pre-release versions.
|
||||||
|
* Past a certain date, an experimental version "graduates"
|
||||||
|
* to a release version and is no longer marked as experimental.
|
||||||
|
*/
|
||||||
|
static var ExperimentalPhpVersions: Set<String> {
|
||||||
|
let releaseDates = [
|
||||||
|
"8.4": Date.fromString("2024-12-01") // PLACEHOLDER DATE
|
||||||
|
]
|
||||||
|
|
||||||
|
return Set(releaseDates
|
||||||
|
.filter { (_: String, date: Date?) in
|
||||||
|
guard let date else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return date > Date.now
|
||||||
|
}.map { (version: String, _: Date?) in
|
||||||
|
return version
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The PHP versions supported by this application.
|
* The PHP versions supported by this application.
|
||||||
@ -32,8 +61,7 @@ struct Constants {
|
|||||||
static let DetectedPhpVersions: Set = [
|
static let DetectedPhpVersions: Set = [
|
||||||
"5.6",
|
"5.6",
|
||||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||||
"8.0", "8.1", "8.2",
|
"8.0", "8.1", "8.2", "8.3",
|
||||||
"8.3",
|
|
||||||
"8.4"
|
"8.4"
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -91,6 +119,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"
|
||||||
)!
|
)!
|
||||||
|
@ -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,19 @@ public class Paths {
|
|||||||
return "\(shared.baseDir.rawValue)/etc"
|
return "\(shared.baseDir.rawValue)/etc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var tapPath: String {
|
||||||
|
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)
|
||||||
|
@ -15,4 +15,10 @@ extension Date {
|
|||||||
return dateFormatter.string(from: self)
|
return dateFormatter.string(from: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func fromString(_ string: String) -> Date? {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
return dateFormatter.date(from: string)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,18 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct Localization {
|
struct Localization {
|
||||||
|
static var preferredLanguage: String? {
|
||||||
|
guard let language = Preferences.preferences[.languageOverride] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if language.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
|
||||||
static var bundle: Bundle = {
|
static var bundle: Bundle = {
|
||||||
if !isRunningTests {
|
if !isRunningTests {
|
||||||
return Bundle.main
|
return Bundle.main
|
||||||
@ -32,7 +44,15 @@ struct Localization {
|
|||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
var localized: String {
|
var localized: String {
|
||||||
let string = NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
|
var preferredBundle: Bundle = Localization.bundle
|
||||||
|
|
||||||
|
if let preferred = Localization.preferredLanguage,
|
||||||
|
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
|
||||||
|
let bundle = Bundle(path: path) {
|
||||||
|
preferredBundle = bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
let string = NSLocalizedString(self, tableName: nil, bundle: preferredBundle, value: "", comment: "")
|
||||||
|
|
||||||
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
||||||
if string == self {
|
if string == self {
|
||||||
|
@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
|
|||||||
// MARK: — FS Attributes
|
// MARK: — FS Attributes
|
||||||
|
|
||||||
func makeExecutable(_ path: String) throws {
|
func makeExecutable(_ path: String) throws {
|
||||||
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Checks
|
// MARK: - Checks
|
||||||
|
@ -75,7 +75,7 @@ class MenuBarImageGenerator {
|
|||||||
|
|
||||||
// Then we'll fetch the image we want on the left
|
// Then we'll fetch the image we want on the left
|
||||||
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
||||||
if iconType == nil {
|
if iconType == nil || !MenuBarIcon.allCases.map({ $0.rawValue }).contains(iconType) {
|
||||||
Log.warn("Invalid icon type found, using the default")
|
Log.warn("Invalid icon type found, using the default")
|
||||||
iconType = MenuBarIcon.iconPhp.rawValue
|
iconType = MenuBarIcon.iconPhp.rawValue
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,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)
|
||||||
|
}
|
||||||
|
@ -62,13 +62,6 @@ class ActivePhpInstallation {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load extension information
|
|
||||||
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
|
||||||
|
|
||||||
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
|
||||||
iniFiles.append(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get configuration values
|
// Get configuration values
|
||||||
limits = Limits(
|
limits = Limits(
|
||||||
memory_limit: getByteCount(key: "memory_limit"),
|
memory_limit: getByteCount(key: "memory_limit"),
|
||||||
@ -76,15 +69,10 @@ class ActivePhpInstallation {
|
|||||||
post_max_size: getByteCount(key: "post_max_size")
|
post_max_size: getByteCount(key: "post_max_size")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Return a list of .ini files parsed after php.ini
|
let paths = ActiveShell.shared
|
||||||
let paths = Command.execute(
|
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
path: Paths.php,
|
.split(separator: "\n")
|
||||||
arguments: ["-r", "echo php_ini_scanned_files();"],
|
.map { String($0) }
|
||||||
trimNewlines: false
|
|
||||||
)
|
|
||||||
.replacingOccurrences(of: "\n", with: "")
|
|
||||||
.split(separator: ",")
|
|
||||||
.map { String($0) }
|
|
||||||
|
|
||||||
// See if any extensions are present in said .ini files
|
// See if any extensions are present in said .ini files
|
||||||
paths.forEach { (iniFilePath) in
|
paths.forEach { (iniFilePath) in
|
||||||
|
@ -13,7 +13,7 @@ class PhpEnvironments {
|
|||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loads the currently active PHP installation upon startup. May be empty.
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
self.currentInstall = ActivePhpInstallation.load()
|
self.currentInstall = ActivePhpInstallation.load()
|
||||||
@ -29,7 +29,7 @@ class PhpEnvironments {
|
|||||||
/**
|
/**
|
||||||
Determine which PHP version the `php` formula is aliased to.
|
Determine which PHP version the `php` formula is aliased to.
|
||||||
*/
|
*/
|
||||||
func determinePhpAlias() async {
|
@MainActor func determinePhpAlias() async {
|
||||||
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
||||||
|
|
||||||
self.homebrewPackage = try! JSONDecoder().decode(
|
self.homebrewPackage = try! JSONDecoder().decode(
|
||||||
@ -37,7 +37,27 @@ 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)!")
|
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).")
|
||||||
|
|
||||||
|
// Check if that version actually corresponds to an older version
|
||||||
|
let phpConfigExecutablePath = "\(Paths.optPath)/php/bin/php-config"
|
||||||
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
|
let longVersionString = Command.execute(
|
||||||
|
path: phpConfigExecutablePath,
|
||||||
|
arguments: ["--version"],
|
||||||
|
trimNewlines: false
|
||||||
|
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
if let version = try? VersionNumber.parse(longVersionString) {
|
||||||
|
PhpEnvironments.brewPhpAlias = version.short
|
||||||
|
if version.short != homebrewPackage.version {
|
||||||
|
Log.info("[BREW] An older version of `php` is actually installed (\(version.short)).")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not determine the actual version of the php binary; assuming Homebrew is correct.")
|
||||||
|
PhpEnvironments.brewPhpAlias = homebrewPackage.version
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
@ -49,12 +69,10 @@ class PhpEnvironments {
|
|||||||
static let shared = PhpEnvironments()
|
static let shared = PhpEnvironments()
|
||||||
|
|
||||||
/** Whether the switcher is busy performing any actions. */
|
/** Whether the switcher is busy performing any actions. */
|
||||||
var isBusy: Bool = false {
|
@MainActor var isBusy: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
Task { @MainActor in
|
MainMenu.shared.refreshIcon()
|
||||||
MainMenu.shared.setBusyImage()
|
MainMenu.shared.rebuild()
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +86,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 +104,12 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
As such, we take that information from Homebrew.
|
As such, we take that information from Homebrew.
|
||||||
*/
|
*/
|
||||||
static var brewPhpAlias: String {
|
static var brewPhpAlias: String = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
It's possible for the alias to be newer than the actual installed version of PHP.
|
||||||
|
*/
|
||||||
|
static var homebrewBrewPhpAlias: String {
|
||||||
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
||||||
|
|
||||||
return PhpEnvironments.shared.homebrewPackage.version
|
return PhpEnvironments.shared.homebrewPackage.version
|
||||||
@ -146,7 +176,12 @@ class PhpEnvironments {
|
|||||||
|
|
||||||
// Avoid inserting a duplicate
|
// Avoid inserting a duplicate
|
||||||
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||||
supportedVersions.insert(phpAlias)
|
let phpAliasInstall = PhpInstallation(phpAlias)
|
||||||
|
// Before inserting, ensure that the actual output matches the alias
|
||||||
|
// if that isn't the case, our formula remains out-of-date
|
||||||
|
if !phpAliasInstall.isMissingBinary {
|
||||||
|
supportedVersions.insert(phpAlias)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
availablePhpVersions = Array(supportedVersions)
|
availablePhpVersions = Array(supportedVersions)
|
||||||
|
@ -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,36 @@ class PhpInstallation {
|
|||||||
|
|
||||||
var versionNumber: VersionNumber
|
var versionNumber: VersionNumber
|
||||||
|
|
||||||
|
var iniFiles: [PhpConfigurationFile] = []
|
||||||
|
|
||||||
|
var isMissingBinary: Bool = false
|
||||||
|
|
||||||
var isHealthy: Bool = true
|
var isHealthy: Bool = true
|
||||||
|
|
||||||
|
var extensions: [PhpExtension] {
|
||||||
|
return self.iniFiles.flatMap({ $0.extensions })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
In order to determine details about a PHP installation,
|
In order to determine details about a PHP installation,
|
||||||
we’ll simply run `php-config --version` in the relevant directory.
|
we’ll simply run `php-config --version` in the relevant directory.
|
||||||
*/
|
*/
|
||||||
init(_ version: String) {
|
init(_ version: String) {
|
||||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
|
||||||
|
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||||
|
|
||||||
let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
versionNumber = VersionNumber.make(from: version)!
|
||||||
|
|
||||||
self.versionNumber = VersionNumber.make(from: version)!
|
determineVersion(phpConfigExecutablePath, phpExecutablePath)
|
||||||
|
determineHealth(phpExecutablePath)
|
||||||
|
determineIniFiles(phpExecutablePath)
|
||||||
|
|
||||||
|
// Find all enabled extensions
|
||||||
|
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
|
||||||
|
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpConfigExecutablePath) {
|
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||||
let longVersionString = Command.execute(
|
let longVersionString = Command.execute(
|
||||||
path: phpConfigExecutablePath,
|
path: phpConfigExecutablePath,
|
||||||
@ -34,9 +51,15 @@ class PhpInstallation {
|
|||||||
|
|
||||||
// The parser should always work, or the string has to be very unusual.
|
// The parser should always work, or the string has to be very unusual.
|
||||||
// If so, the app SHOULD crash, so that the users report what's up.
|
// If so, the app SHOULD crash, so that the users report what's up.
|
||||||
self.versionNumber = try! VersionNumber.parse(longVersionString)
|
versionNumber = try! VersionNumber.parse(longVersionString)
|
||||||
|
} else {
|
||||||
|
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
|
||||||
|
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
|
||||||
|
isMissingBinary = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineHealth(_ phpExecutablePath: String) {
|
||||||
if FileSystem.fileExists(phpExecutablePath) {
|
if FileSystem.fileExists(phpExecutablePath) {
|
||||||
let testCommand = Command.execute(
|
let testCommand = Command.execute(
|
||||||
path: phpExecutablePath,
|
path: phpExecutablePath,
|
||||||
@ -53,4 +76,18 @@ class PhpInstallation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func determineIniFiles(_ phpExecutablePath: String) {
|
||||||
|
let paths = ActiveShell.shared
|
||||||
|
.sync("\(phpExecutablePath) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||||
|
.split(separator: "\n")
|
||||||
|
.map { String($0) }
|
||||||
|
|
||||||
|
// See if any extensions are present in said .ini files
|
||||||
|
paths.forEach { (iniFilePath) in
|
||||||
|
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
|
||||||
|
iniFiles.append(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ extension InternalSwitcher {
|
|||||||
return corrections.contains(true)
|
return corrections.contains(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - PHP FPM pool
|
// MARK: - Corrections
|
||||||
|
|
||||||
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
||||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||||
@ -54,37 +54,7 @@ extension InternalSwitcher {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||||
return [
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/php-fpm.d/valet-fpm.conf",
|
|
||||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
|
||||||
replacements: [
|
|
||||||
"VALET_USER": Paths.whoami,
|
|
||||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
|
||||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
|
||||||
],
|
|
||||||
applies: { Valet.shared.version!.major > 2 }
|
|
||||||
),
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/conf.d/error_log.ini",
|
|
||||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
|
||||||
replacements: [
|
|
||||||
"VALET_USER": Paths.whoami,
|
|
||||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
|
||||||
],
|
|
||||||
applies: { return true }
|
|
||||||
),
|
|
||||||
ExpectedConfigurationFile(
|
|
||||||
destination: "/conf.d/php-memory-limits.ini",
|
|
||||||
source: "/cli/stubs/php-memory-limits.ini",
|
|
||||||
replacements: [:],
|
|
||||||
applies: { return true }
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
|
||||||
let files = self.getExpectedConfigurationFiles(for: version)
|
let files = self.getExpectedConfigurationFiles(for: version)
|
||||||
|
|
||||||
// For each of the files, attempt to fix anything that is wrong
|
// For each of the files, attempt to fix anything that is wrong
|
||||||
@ -124,6 +94,38 @@ extension InternalSwitcher {
|
|||||||
return outcomes.contains(true)
|
return outcomes.contains(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Internals
|
||||||
|
|
||||||
|
private func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||||
|
return [
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/php-fpm.d/valet-fpm.conf",
|
||||||
|
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||||
|
replacements: [
|
||||||
|
"VALET_USER": Paths.whoami,
|
||||||
|
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||||
|
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||||
|
],
|
||||||
|
applies: { Valet.shared.version!.major > 2 }
|
||||||
|
),
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/conf.d/error_log.ini",
|
||||||
|
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||||
|
replacements: [
|
||||||
|
"VALET_USER": Paths.whoami,
|
||||||
|
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||||
|
],
|
||||||
|
applies: { return true }
|
||||||
|
),
|
||||||
|
ExpectedConfigurationFile(
|
||||||
|
destination: "/conf.d/php-memory-limits.ini",
|
||||||
|
source: "/cli/stubs/php-memory-limits.ini",
|
||||||
|
replacements: [:],
|
||||||
|
applies: { return true }
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ExpectedConfigurationFile {
|
public struct ExpectedConfigurationFile {
|
||||||
|
@ -86,14 +86,37 @@ class RealShell: ShellProtocol {
|
|||||||
|
|
||||||
// MARK: - Shellable Protocol
|
// MARK: - Shellable Protocol
|
||||||
|
|
||||||
|
func sync(_ command: String) -> ShellOutput {
|
||||||
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
|
let outputPipe = Pipe()
|
||||||
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
|
sleep(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
task.standardOutput = outputPipe
|
||||||
|
task.standardError = errorPipe
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
|
|
||||||
|
if Log.shared.verbosity == .cli {
|
||||||
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
func pipe(_ command: String) async -> ShellOutput {
|
func pipe(_ command: String) async -> ShellOutput {
|
||||||
let task = getShellProcess(for: command)
|
let task = getShellProcess(for: command)
|
||||||
|
|
||||||
let outputPipe = Pipe()
|
let outputPipe = Pipe()
|
||||||
let errorPipe = Pipe()
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
// Seriously slow down how long it takes for the shell to return output
|
|
||||||
// (in order to debug or identify async issues)
|
|
||||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||||
Log.info("[SLOW SHELL] \(command)")
|
Log.info("[SLOW SHELL] \(command)")
|
||||||
await delay(seconds: 3.0)
|
await delay(seconds: 3.0)
|
||||||
@ -104,20 +127,20 @@ class RealShell: ShellProtocol {
|
|||||||
task.launch()
|
task.launch()
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
|
|
||||||
let stdOut = String(
|
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
let stdErr = String(
|
|
||||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
|
||||||
encoding: .utf8
|
|
||||||
)!
|
|
||||||
|
|
||||||
if Log.shared.verbosity == .cli {
|
if Log.shared.verbosity == .cli {
|
||||||
var args = task.arguments ?? []
|
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
}
|
||||||
var log = """
|
|
||||||
|
return .out(stdOut, stdErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func log(task: Process, stdOut: String, stdErr: String) {
|
||||||
|
var args = task.arguments ?? []
|
||||||
|
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||||
|
var log = """
|
||||||
|
|
||||||
<~~~~~~~~~~~~~~~~~~~~~~~
|
<~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
||||||
@ -126,22 +149,19 @@ class RealShell: ShellProtocol {
|
|||||||
\(stdOut)
|
\(stdOut)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if !stdErr.isEmpty {
|
if !stdErr.isEmpty {
|
||||||
log.append("""
|
log.append("""
|
||||||
[ERR]:
|
[ERR]:
|
||||||
\(stdErr)
|
\(stdErr)
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.append("""
|
log.append("""
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~>
|
~~~~~~~~~~~~~~~~~~~~~~~~>
|
||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
Log.info(log)
|
Log.info(log)
|
||||||
}
|
|
||||||
|
|
||||||
return .out(stdOut, stdErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func quiet(_ command: String) async {
|
func quiet(_ command: String) async {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -46,8 +46,10 @@ extension App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hotkey.keyDownHandler = {
|
hotkey.keyDownHandler = {
|
||||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
Task { @MainActor in
|
||||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||||
|
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,21 +74,24 @@ 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] = []
|
||||||
|
|
||||||
/** 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 +120,12 @@ class App {
|
|||||||
|
|
||||||
// MARK: - App Watchers
|
// MARK: - App Watchers
|
||||||
|
|
||||||
/**
|
/** Individual filesystem watchers, which are, i.e. responsible for watching the Homebrew folders. */
|
||||||
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
var watchers: [String: FSNotifier] = [:]
|
||||||
|
|
||||||
|
/**
|
||||||
|
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!
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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="22155" 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="22155"/>
|
||||||
<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"/>
|
||||||
@ -429,7 +429,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">
|
||||||
@ -521,9 +521,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 +528,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 +544,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 +575,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 +586,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 +610,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 +685,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 +692,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 +710,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 +728,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 +743,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 +751,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"/>
|
||||||
@ -886,7 +886,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 +894,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"/>
|
||||||
@ -937,13 +937,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 +987,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 +1024,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 +1035,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"/>
|
||||||
@ -1125,7 +1125,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 +1136,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 +1144,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 +1152,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 +1194,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 +1201,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 +1229,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 +1237,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 +1245,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 +1340,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 +1347,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 +1389,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 +1397,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"/>
|
||||||
|
@ -142,7 +142,7 @@ class Startup {
|
|||||||
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
return await Shell.pipe("\(Paths.binPath)/php -v").err
|
||||||
.contains("Library not loaded")
|
.contains("Library not loaded")
|
||||||
},
|
},
|
||||||
name: "`no dyld issue detected",
|
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||||
titleText: "startup.errors.dyld_library.title".localized,
|
titleText: "startup.errors.dyld_library.title".localized,
|
||||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||||
Paths.optPath
|
Paths.optPath
|
||||||
@ -241,6 +241,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +28,6 @@ import Foundation
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
MainMenu.shared.setBusyImage()
|
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
|
|
||||||
window = TerminalProgressWindowController.display(
|
window = TerminalProgressWindowController.display(
|
||||||
title: "alert.composer_progress.title".localized,
|
title: "alert.composer_progress.title".localized,
|
||||||
@ -106,14 +104,11 @@ import Foundation
|
|||||||
|
|
||||||
private func removeBusyStatus() {
|
private func removeBusyStatus() {
|
||||||
PhpEnvironments.shared.isBusy = false
|
PhpEnvironments.shared.isBusy = false
|
||||||
Task { @MainActor in
|
|
||||||
MainMenu.shared.updatePhpVersionInStatusBar()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Alert
|
// MARK: Alert
|
||||||
|
|
||||||
@MainActor private func presentMissingAlert() {
|
private func presentMissingAlert() {
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.composer_missing.title".localized,
|
title: "alert.composer_missing.title".localized,
|
||||||
|
@ -8,20 +8,20 @@
|
|||||||
|
|
||||||
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
|
/**
|
||||||
|
This list is checked first to see if a project dependency can be mapped to a certain project type.
|
||||||
|
*/
|
||||||
|
public static let SpecificDependencyList = [
|
||||||
"roots/bedrock": "Bedrock",
|
"roots/bedrock": "Bedrock",
|
||||||
"cakephp/app": "CakePHP",
|
"cakephp/app": "CakePHP",
|
||||||
"craftcms/craft": "Craft",
|
"craftcms/craft": "Craft",
|
||||||
@ -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,11 @@ class Brew {
|
|||||||
|
|
||||||
/// Each formula for each PHP version that can be installed.
|
/// Each formula for each PHP version that can be installed.
|
||||||
public static let phpVersionFormulae = [
|
public static let phpVersionFormulae = [
|
||||||
"8.3": "shivammathur/php/php@8.3",
|
"8.4": "shivammathur/php/php@8.4",
|
||||||
|
"8.3": "php@8.3",
|
||||||
"8.2": "php@8.2",
|
"8.2": "php@8.2",
|
||||||
"8.1": "php@8.1",
|
"8.1": "php@8.1",
|
||||||
"8.0": "php@8.0",
|
"8.0": "shivammathur/php/php@8.0",
|
||||||
"7.4": "shivammathur/php/php@7.4",
|
"7.4": "shivammathur/php/php@7.4",
|
||||||
"7.3": "shivammathur/php/php@7.3",
|
"7.3": "shivammathur/php/php@7.3",
|
||||||
"7.2": "shivammathur/php/php@7.2",
|
"7.2": "shivammathur/php/php@7.2",
|
||||||
|
@ -27,6 +27,21 @@ class BrewDiagnostics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Logs a bunch of useful information during startup.
|
||||||
|
*/
|
||||||
|
public static func logBootInformation() {
|
||||||
|
Log.info(BrewDiagnostics.customCaskInstalled
|
||||||
|
? "[BREW] The app has been installed via Homebrew Cask."
|
||||||
|
: "[BREW] The app has been installed directly (optimal)."
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.info(BrewDiagnostics.usesNginxFullFormula
|
||||||
|
? "[BREW] The app will be using the `nginx-full` formula."
|
||||||
|
: "[BREW] The app will be using the `nginx` formula."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determines whether the PHP Monitor Cask is installed.
|
Determines whether the PHP Monitor Cask is installed.
|
||||||
*/
|
*/
|
||||||
@ -46,6 +61,43 @@ class BrewDiagnostics {
|
|||||||
return destination.contains("/nginx-full/")
|
return destination.contains("/nginx-full/")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
/**
|
||||||
|
It is possible to have outdated symlinks for PHP installations. This can mean that certain PHP installations
|
||||||
|
are going to be reported incorrectly (e.g. `php@8.2` links to an installation in a `8.3` folder after an upgrade).
|
||||||
|
|
||||||
|
To ensure this does not cause issues, PHP Monitor will automatically remove all incorrect PHP symlinks.
|
||||||
|
*/
|
||||||
|
public static func checkForOutdatedPhpInstallationSymlinks() async {
|
||||||
|
// Set up a regular expression
|
||||||
|
let regex = try! NSRegularExpression(pattern: "^php@[0-9]+\\.[0-9]+$", options: .caseInsensitive)
|
||||||
|
|
||||||
|
// Check for incorrect versions
|
||||||
|
if let contents = try? FileSystem.getShallowContentsOfDirectory("\(Paths.optPath)")
|
||||||
|
.filter({
|
||||||
|
let range = NSRange($0.startIndex..., in: $0)
|
||||||
|
return regex.firstMatch(in: $0, options: [], range: range) != nil
|
||||||
|
}) {
|
||||||
|
|
||||||
|
for symlink in contents {
|
||||||
|
let version = symlink.replacingOccurrences(of: "php@", with: "")
|
||||||
|
if let destination = try? FileSystem.getDestinationOfSymlink("\(Paths.optPath)/\(symlink)") {
|
||||||
|
if !destination.contains("Cellar/php/\(version)")
|
||||||
|
&& !destination.contains("Cellar/php@\(version)") {
|
||||||
|
Log.err("Symlink for \(symlink) is incorrect. Removing...")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove("\(Paths.optPath)/\(symlink)")
|
||||||
|
Log.info("Incorrect symlink for \(symlink) has been successfully removed.")
|
||||||
|
} catch {
|
||||||
|
Log.err("Symlink for \(symlink) was incorrect but could not be removed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.warn("Could not read symlink at: \(Paths.optPath)/\(symlink)! Symlink check skipped.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||||
This will then result in two different aliases claiming to point to the same formula (`php`).
|
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||||
|
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// BrewPhpExtension.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 27/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct BrewPhpExtension: Hashable, Comparable {
|
||||||
|
let name: String
|
||||||
|
let phpVersion: String
|
||||||
|
let isInstalled: Bool
|
||||||
|
let path: String
|
||||||
|
let dependencies: [String]
|
||||||
|
|
||||||
|
var extensionDependencies: [String] {
|
||||||
|
return dependencies
|
||||||
|
.filter {
|
||||||
|
$0.contains("shivammathur/extensions/") && $0.contains("@\(phpVersion)")
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
$0.replacingOccurrences(of: "shivammathur/extensions/", with: "")
|
||||||
|
.replacingOccurrences(of: "@\(phpVersion)", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var formulaName: String {
|
||||||
|
return "\(name)@\(phpVersion)"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(path: String, name: String, phpVersion: String) {
|
||||||
|
self.path = path
|
||||||
|
self.name = name
|
||||||
|
self.phpVersion = phpVersion
|
||||||
|
|
||||||
|
self.isInstalled = BrewPhpExtension.hasInstallationReceipt(
|
||||||
|
for: "\(name)@\(phpVersion)"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.dependencies = BrewPhpExtension.extractDependencies(from: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAlternativeInstall: Bool {
|
||||||
|
guard let php = PhpEnvironments.shared.cachedPhpInstallations[self.phpVersion] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let alreadyDiscovered = php.extensions.contains(where: { $0.name == self.name })
|
||||||
|
|
||||||
|
return alreadyDiscovered && !isInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func firstDependent(in exts: [BrewPhpExtension]) -> BrewPhpExtension? {
|
||||||
|
return exts
|
||||||
|
.filter({ $0.isInstalled })
|
||||||
|
.first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasInstallationReceipt(for formulaName: String) -> Bool {
|
||||||
|
return FileSystem.fileExists("\(Paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name < rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||||
|
return lhs.name == rhs.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractDependencies(from path: String) -> [String] {
|
||||||
|
let regexPattern = #"depends_on "(.*)""#
|
||||||
|
var dependencies: [String] = []
|
||||||
|
|
||||||
|
guard let content = try? FileSystem.getStringFromFile(path) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let regex = try NSRegularExpression(pattern: regexPattern, options: [])
|
||||||
|
let range = NSRange(content.startIndex..<content.endIndex, in: content)
|
||||||
|
let matches = regex.matches(in: content, options: [], range: range)
|
||||||
|
|
||||||
|
for match in matches {
|
||||||
|
if let range = Range(match.range(at: 1), in: content) {
|
||||||
|
let dependencyName = String(content[range])
|
||||||
|
dependencies.append(dependencyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// BrewFormula.swift
|
// BrewPhpFormula.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 17/03/2023.
|
// Created by Nico Verbruggen on 17/03/2023.
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct BrewFormula {
|
struct BrewPhpFormula: Equatable {
|
||||||
/// Name of the formula.
|
/// Name of the formula.
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
@ -21,6 +21,8 @@ struct BrewFormula {
|
|||||||
/// The upgrade that is currently available, if it exists.
|
/// The upgrade that is currently available, if it exists.
|
||||||
let upgradeVersion: String?
|
let upgradeVersion: String?
|
||||||
|
|
||||||
|
// TODO: A rebuild attribute could be checked, to check if a Tap update exists for a pre-release version
|
||||||
|
|
||||||
/// Whether this formula is a stable version of PHP.
|
/// Whether this formula is a stable version of PHP.
|
||||||
let prerelease: Bool
|
let prerelease: Bool
|
||||||
|
|
||||||
@ -48,6 +50,25 @@ struct BrewFormula {
|
|||||||
return upgradeVersion != nil
|
return upgradeVersion != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this formula alias is different.
|
||||||
|
var hasUpgradedFormulaAlias: Bool {
|
||||||
|
return self.shortVersion == PhpEnvironments.homebrewBrewPhpAlias
|
||||||
|
&& PhpEnvironments.homebrewBrewPhpAlias != PhpEnvironments.brewPhpAlias
|
||||||
|
}
|
||||||
|
|
||||||
|
var unavailableAfterUpgrade: Bool {
|
||||||
|
if installedVersion == nil || upgradeVersion == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let installed = try? VersionNumber.parse(self.installedVersion!),
|
||||||
|
let upgrade = try? VersionNumber.parse(self.upgradeVersion!) {
|
||||||
|
return upgrade.short != installed.short
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/// The associated Homebrew folder with this PHP formula.
|
/// The associated Homebrew folder with this PHP formula.
|
||||||
var homebrewFolder: String {
|
var homebrewFolder: String {
|
||||||
let resolved = name
|
let resolved = name
|
||||||
@ -60,7 +81,7 @@ struct BrewFormula {
|
|||||||
/// The short version associated with this formula, if installed.
|
/// The short version associated with this formula, if installed.
|
||||||
var shortVersion: String? {
|
var shortVersion: String? {
|
||||||
guard let version = self.installedVersion else {
|
guard let version = self.installedVersion else {
|
||||||
return nil
|
return self.displayName.replacingOccurrences(of: "PHP ", with: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return VersionNumber.make(from: version)?.short ?? nil
|
return VersionNumber.make(from: version)?.short ?? nil
|
||||||
@ -81,6 +102,7 @@ struct BrewFormula {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?.isHealthy ?? nil
|
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?
|
||||||
|
.isHealthy ?? nil
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,22 +8,23 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol HandlesBrewFormulae {
|
protocol HandlesBrewPhpFormulae {
|
||||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula]
|
func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula]
|
||||||
func refreshPhpVersions(loadOutdated: Bool) async
|
func refreshPhpVersions(loadOutdated: Bool) async
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HandlesBrewFormulae {
|
extension HandlesBrewPhpFormulae {
|
||||||
public func refreshPhpVersions(loadOutdated: Bool) async {
|
public func refreshPhpVersions(loadOutdated: Bool) async {
|
||||||
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
Brew.shared.formulae.phpVersions = items
|
Brew.shared.formulae.phpVersions = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BrewFormulaeHandler: HandlesBrewFormulae {
|
class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] {
|
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
|
||||||
var outdated: [OutdatedFormula]?
|
var outdated: [OutdatedFormula]?
|
||||||
|
|
||||||
if loadOutdated {
|
if loadOutdated {
|
||||||
@ -43,7 +44,8 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Brew.phpVersionFormulae.map { (version, formula) in
|
return Brew.phpVersionFormulae.map { (version, formula) in
|
||||||
let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?.versionNumber.text
|
let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?
|
||||||
|
.versionNumber.text
|
||||||
|
|
||||||
var upgradeVersion: String?
|
var upgradeVersion: String?
|
||||||
|
|
||||||
@ -53,7 +55,7 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
|||||||
})?.current_version
|
})?.current_version
|
||||||
}
|
}
|
||||||
|
|
||||||
return BrewFormula(
|
return BrewPhpFormula(
|
||||||
name: formula,
|
name: formula,
|
||||||
displayName: "PHP \(version)",
|
displayName: "PHP \(version)",
|
||||||
installedVersion: fullVersion,
|
installedVersion: fullVersion,
|
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// BrewTapFormulae.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 01/11/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class BrewTapFormulae {
|
||||||
|
public static func from(tap: String) -> [String: [BrewPhpExtension]] {
|
||||||
|
let directory = "\(Paths.tapPath)/\(tap)/Formula"
|
||||||
|
|
||||||
|
let files = try? FileSystem.getShallowContentsOfDirectory(directory)
|
||||||
|
|
||||||
|
var availableExtensions = [String: [BrewPhpExtension]]()
|
||||||
|
|
||||||
|
guard let files = files else {
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
|
||||||
|
let regex = try! NSRegularExpression(pattern: "(\\w+)@(\\d+\\.\\d+)\\.rb")
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
let matches = regex.matches(in: file, range: NSRange(file.startIndex..., in: file))
|
||||||
|
if let match = matches.first {
|
||||||
|
if let phpExtensionRange = Range(match.range(at: 1), in: file),
|
||||||
|
let versionRange = Range(match.range(at: 2), in: file) {
|
||||||
|
// Determine what the extension's name is
|
||||||
|
let phpExtensionName = String(file[phpExtensionRange])
|
||||||
|
|
||||||
|
// Determine what PHP version this is for
|
||||||
|
let phpVersion = String(file[versionRange])
|
||||||
|
|
||||||
|
// Create a new BrewPhpExtension object (determines if installed)
|
||||||
|
let phpExtension = BrewPhpExtension(
|
||||||
|
path: "\(Paths.tapPath)/\(tap)/Formula/\(file)",
|
||||||
|
name: phpExtensionName,
|
||||||
|
phpVersion: phpVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
// Append the extension to the list
|
||||||
|
var extensions = availableExtensions[phpVersion, default: []]
|
||||||
|
extensions.append(phpExtension)
|
||||||
|
availableExtensions[phpVersion] = extensions.sorted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableExtensions
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,8 @@ import Foundation
|
|||||||
|
|
||||||
protocol BrewCommand {
|
protocol BrewCommand {
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
||||||
|
|
||||||
|
func getCommandTitle() -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BrewCommand {
|
extension BrewCommand {
|
||||||
@ -31,6 +33,44 @@ extension BrewCommand {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
var loggedMessages: [String] = []
|
||||||
|
|
||||||
|
let (process, _) = try! await Shell.attach(
|
||||||
|
command,
|
||||||
|
didReceiveOutput: { text, _ in
|
||||||
|
if !text.isEmpty {
|
||||||
|
Log.perf(text)
|
||||||
|
loggedMessages.append(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (number, text) = self.reportInstallationProgress(text) {
|
||||||
|
onProgress(.create(value: number, title: getCommandTitle(), description: text))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withTimeout: .minutes(15)
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.terminationStatus <= 0 {
|
||||||
|
loggedMessages = []
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/php") {
|
||||||
|
let command = "brew tap shivammathur/php"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") {
|
||||||
|
let command = "brew tap shivammathur/extensions"
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BrewCommandProgress {
|
struct BrewCommandProgress {
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
// 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,89 @@
|
|||||||
|
//
|
||||||
|
// 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 MainMenu.shared.refreshActiveInstallation()
|
||||||
|
|
||||||
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
|
} else {
|
||||||
|
throw BrewCommandError(error: "phpman.steps.failure".localized, log: loggedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performExtensionCleanup(for ext: PhpExtension) async {
|
||||||
|
if ext.file.hasSuffix("20-\(ext.name).ini") {
|
||||||
|
// The extension's default configuration file can be removed
|
||||||
|
Log.info("The extension was found in a default extension .ini location. Purging that .ini file.")
|
||||||
|
do {
|
||||||
|
try FileSystem.remove(ext.file)
|
||||||
|
} catch {
|
||||||
|
Log.err("The file `\(ext.file)` could not be removed.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The extension's default configuration file cannot be removed, it should be disabled instead
|
||||||
|
Log.info("The extension was not found in a default location. Disabling the extension only.")
|
||||||
|
if ext.enabled {
|
||||||
|
await ext.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,23 +8,33 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class InstallAndUpgradeCommand: BrewCommand {
|
class ModifyPhpVersionCommand: BrewCommand {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let installing: [BrewFormula]
|
let installing: [BrewPhpFormula]
|
||||||
let upgrading: [BrewFormula]
|
let upgrading: [BrewPhpFormula]
|
||||||
let phpGuard: PhpGuard
|
let phpGuard: PhpGuard
|
||||||
|
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
||||||
The process will be executed in two steps: first upgrades, then installations.
|
The process will be executed in two steps: first upgrades, then installations.
|
||||||
|
|
||||||
Upgrades come first because... well, otherwise installations may very well break.
|
Upgrades come first because... well, otherwise installations may very well break.
|
||||||
Each version that is installed will need to be checked afterwards (if it is OK).
|
Each version that is installed will need to be checked afterwards. Installing a
|
||||||
|
newer formula may break other PHP installations, which in turn need to be fixed.
|
||||||
|
|
||||||
|
- Important: If any PHP formula is a major upgrade that causes a PHP "version" to be
|
||||||
|
uninstalled, this is remedied by running `upgradeMainPhpFormula()`. This process
|
||||||
|
will ensure that the upgrade is applied, but the also that old version is
|
||||||
|
re-installed and linked again.
|
||||||
*/
|
*/
|
||||||
public init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
upgrading: [BrewFormula],
|
upgrading: [BrewPhpFormula],
|
||||||
installing: [BrewFormula]
|
installing: [BrewPhpFormula]
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.installing = installing
|
self.installing = installing
|
||||||
@ -33,17 +43,32 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
let progressTitle = "Please wait..."
|
let progressTitle = "phpman.steps.wait".localized
|
||||||
|
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: progressTitle,
|
||||||
description: "PHP Monitor is preparing Homebrew..."
|
description: "phpman.steps.preparing".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
// Try to run all upgrade and installation operations
|
// Determine if a formula will become unavailable
|
||||||
try await self.upgradePackages(onProgress)
|
// This is the case when `php` will be bumped to a new version
|
||||||
try await self.installPackages(onProgress)
|
let unavailable = upgrading.first(where: { formula in
|
||||||
|
formula.unavailableAfterUpgrade
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make sure the tap is installed
|
||||||
|
try await self.checkPhpTap(onProgress)
|
||||||
|
|
||||||
|
if unavailable == nil {
|
||||||
|
// Try to run all upgrade and installation operations
|
||||||
|
try await self.upgradePackages(onProgress)
|
||||||
|
try await self.installPackages(onProgress)
|
||||||
|
} else {
|
||||||
|
// Simply upgrade `php` to the latest version
|
||||||
|
try await self.upgradeMainPhpFormula(unavailable!, onProgress)
|
||||||
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
}
|
||||||
|
|
||||||
// Re-check the installed versions
|
// Re-check the installed versions
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@ -55,6 +80,27 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
await self.completedOperations(onProgress)
|
await self.completedOperations(onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func upgradeMainPhpFormula(
|
||||||
|
_ unavailable: BrewPhpFormula,
|
||||||
|
_ onProgress: @escaping (BrewCommandProgress) -> Void
|
||||||
|
) async throws {
|
||||||
|
// Determine which version was previously available (that will become unavailable)
|
||||||
|
guard let short = try? VersionNumber
|
||||||
|
.parse(unavailable.installedVersion!).short else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade the main formula
|
||||||
|
let command = """
|
||||||
|
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||||
|
\(Paths.brew) upgrade php;
|
||||||
|
\(Paths.brew) install php@\(short);
|
||||||
|
"""
|
||||||
|
|
||||||
|
// Run the upgrade command
|
||||||
|
try await run(command, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
// If no upgrades are needed, early exit
|
// If no upgrades are needed, early exit
|
||||||
if self.upgrading.isEmpty {
|
if self.upgrading.isEmpty {
|
||||||
@ -117,35 +163,12 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
try await run(command, onProgress)
|
try await run(command, onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
|
||||||
var loggedMessages: [String] = []
|
|
||||||
|
|
||||||
let (process, _) = try! await Shell.attach(
|
|
||||||
command,
|
|
||||||
didReceiveOutput: { text, _ in
|
|
||||||
if !text.isEmpty {
|
|
||||||
Log.perf(text)
|
|
||||||
loggedMessages.append(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (number, text) = self.reportInstallationProgress(text) {
|
|
||||||
onProgress(.create(value: number, title: self.title, description: text))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
withTimeout: .minutes(15)
|
|
||||||
)
|
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
|
||||||
loggedMessages = []
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||||
// Reload and restart PHP versions
|
// Reload and restart PHP versions
|
||||||
onProgress(.create(value: 0.95, title: self.title, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: self.title, description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
|
// Ensure all symlinks are correctly linked
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Check which version of PHP are now installed
|
// Check which version of PHP are now installed
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
@ -164,9 +187,8 @@ class InstallAndUpgradeCommand: BrewCommand {
|
|||||||
// Let the UI know that the installation has been completed
|
// Let the UI know that the installation has been completed
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 1,
|
value: 1,
|
||||||
title: "Operation completed!",
|
title: "phpman.steps.completed".localized,
|
||||||
description: "The installation has succeeded."
|
description: "phpman.steps.success".localized
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -21,13 +21,15 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
self.phpGuard = PhpGuard()
|
self.phpGuard = PhpGuard()
|
||||||
}
|
}
|
||||||
|
|
||||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
func getCommandTitle() -> String {
|
||||||
let progressTitle = "Removing PHP \(version)..."
|
return "phpman.steps.removing".localized("PHP \(version)...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||||
onProgress(.create(
|
onProgress(.create(
|
||||||
value: 0.2,
|
value: 0.2,
|
||||||
title: progressTitle,
|
title: getCommandTitle(),
|
||||||
description: "Please wait while Homebrew removes PHP \(version)..."
|
description: "phpman.steps.wait".localized
|
||||||
))
|
))
|
||||||
|
|
||||||
let command = """
|
let command = """
|
||||||
@ -56,7 +58,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.terminationStatus <= 0 {
|
if process.terminationStatus <= 0 {
|
||||||
onProgress(.create(value: 0.95, title: progressTitle, description: "Reloading PHP versions..."))
|
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||||
|
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
|
|
||||||
@ -66,7 +68,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
|||||||
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(.create(value: 1, title: progressTitle, description: "The operation has succeeded."))
|
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||||
} else {
|
} else {
|
||||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||||
}
|
}
|
@ -9,6 +9,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class FakeCommand: BrewCommand {
|
class FakeCommand: BrewCommand {
|
||||||
|
func getCommandTitle() -> String {
|
||||||
|
return "Hello"
|
||||||
|
}
|
||||||
|
|
||||||
let version: String
|
let version: String
|
||||||
|
|
||||||
init(version: String) {
|
init(version: String) {
|
||||||
|
@ -141,7 +141,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 +155,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,12 +283,11 @@ extension MainMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@ -298,13 +297,12 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func switchToPhpVersion(_ version: String) {
|
@objc func switchToPhpVersion(_ version: String) {
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
@ -325,13 +323,12 @@ extension MainMenu {
|
|||||||
*/
|
*/
|
||||||
func switchToPhp(_ version: String) async {
|
func switchToPhp(_ version: String) async {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
setBusyImage()
|
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
PhpEnvironments.shared.delegate = self
|
PhpEnvironments.shared.delegate = self
|
||||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||||
|
|
||||||
|
@ -45,21 +45,16 @@ extension MainMenu {
|
|||||||
.broadcastServicesUpdate
|
.broadcastServicesUpdate
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
if behaviours.contains(.reloadsPhpInstallation) {
|
if behaviours.contains(.reloadsPhpInstallation) || behaviours.contains(.setsBusyUI) {
|
||||||
PhpEnvironments.shared.isBusy = true
|
PhpEnvironments.shared.isBusy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.setsBusyUI) {
|
|
||||||
setBusyImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
Task(priority: .userInitiated) { [unowned self] in
|
Task(priority: .userInitiated) { [unowned self] in
|
||||||
var error: Error?
|
var error: Error?
|
||||||
|
|
||||||
do { try execute() } catch let e { error = e }
|
do { try execute() } catch let e {
|
||||||
|
error = e
|
||||||
if behaviours.contains(.setsBusyUI) {
|
Log.err(e)
|
||||||
PhpEnvironments.shared.isBusy = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor [self, error] in
|
Task { @MainActor [self, error] in
|
||||||
@ -68,15 +63,18 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.updatesMenuBarContents) {
|
if behaviours.contains(.updatesMenuBarContents) {
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
} else if behaviours.contains(.setsBusyUI) {
|
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.broadcastServicesUpdate) {
|
if behaviours.contains(.broadcastServicesUpdate) {
|
||||||
Task { await ServicesManager.shared.reloadServicesStatus() }
|
Task { await ServicesManager.shared.reloadServicesStatus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if behaviours.contains(.setsBusyUI) {
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
if error != nil {
|
if error != nil {
|
||||||
return failure(error!)
|
return failure(error!)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ extension MainMenu {
|
|||||||
func startup() async {
|
func startup() async {
|
||||||
// Start with the icon
|
// Start with the icon
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
self.setStatusBar(image: NSImage.statusBarIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
if await Startup().checkEnvironment() {
|
if await Startup().checkEnvironment() {
|
||||||
@ -32,19 +32,14 @@ extension MainMenu {
|
|||||||
// Determine what the `php` formula is aliased to
|
// Determine what the `php` formula is aliased to
|
||||||
await PhpEnvironments.shared.determinePhpAlias()
|
await PhpEnvironments.shared.determinePhpAlias()
|
||||||
|
|
||||||
|
// Make sure that broken symlinks are removed ASAP
|
||||||
|
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||||
|
|
||||||
// Initialize preferences
|
// Initialize preferences
|
||||||
_ = Preferences.shared
|
_ = Preferences.shared
|
||||||
|
|
||||||
// Determine install method
|
// Put some useful diagnostics information in log
|
||||||
Log.info(BrewDiagnostics.customCaskInstalled
|
BrewDiagnostics.logBootInformation()
|
||||||
? "[BREW] The app has been installed via Homebrew Cask."
|
|
||||||
: "[BREW] The app has been installed directly (optimal)."
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.info(BrewDiagnostics.usesNginxFullFormula
|
|
||||||
? "[BREW] The app will be using the `nginx-full` formula."
|
|
||||||
: "[BREW] The app will be using the `nginx` formula."
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attempt to find out more info about Valet
|
// Attempt to find out more info about Valet
|
||||||
if Valet.shared.version != nil {
|
if Valet.shared.version != nil {
|
||||||
@ -63,9 +58,6 @@ extension MainMenu {
|
|||||||
// Check for an alias conflict
|
// Check for an alias conflict
|
||||||
await BrewDiagnostics.checkForCaskConflict()
|
await BrewDiagnostics.checkForCaskConflict()
|
||||||
|
|
||||||
// Update the icon
|
|
||||||
updatePhpVersionInStatusBar()
|
|
||||||
|
|
||||||
// Attempt to find out if PHP-FPM is broken
|
// Attempt to find out if PHP-FPM is broken
|
||||||
PhpEnvironments.prepare()
|
PhpEnvironments.prepare()
|
||||||
|
|
||||||
@ -76,7 +68,6 @@ extension MainMenu {
|
|||||||
WarningManager.shared.evaluateWarnings()
|
WarningManager.shared.evaluateWarnings()
|
||||||
|
|
||||||
// Set up the config watchers on launch (updated automatically when switching)
|
// Set up the config watchers on launch (updated automatically when switching)
|
||||||
Log.info("Setting up watchers...")
|
|
||||||
App.shared.handlePhpConfigWatcher()
|
App.shared.handlePhpConfigWatcher()
|
||||||
|
|
||||||
// Detect built-in and custom applications
|
// Detect built-in and custom applications
|
||||||
@ -105,9 +96,33 @@ extension MainMenu {
|
|||||||
Valet.shared.notifyAboutUnsupportedTLD()
|
Valet.shared.notifyAboutUnsupportedTLD()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep track of which PHP versions are currently about to release
|
||||||
|
Log.info("Experimental PHP versions are: \(Constants.ExperimentalPhpVersions)")
|
||||||
|
|
||||||
// Find out which services are active
|
// Find out which services are active
|
||||||
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
||||||
|
|
||||||
|
// Post-launch stats and update check, but only if not running tests
|
||||||
|
await performPostLaunchActions()
|
||||||
|
|
||||||
|
// Check if the linked version has changed between launches of phpmon
|
||||||
|
PhpGuard().compareToLastGlobalVersion()
|
||||||
|
|
||||||
|
// We are ready!
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
|
||||||
|
// Finally!
|
||||||
|
Log.info("PHP Monitor is ready to serve!")
|
||||||
|
|
||||||
|
// Check if we upgraded from a previous version
|
||||||
|
AppUpdater.checkIfUpdateWasPerformed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Performs a set of post-launch actions, like incrementing stats and checking for updates.
|
||||||
|
(This code is skipped when running SwiftUI previews.)
|
||||||
|
*/
|
||||||
|
private func performPostLaunchActions() async {
|
||||||
if !isRunningSwiftUIPreview {
|
if !isRunningSwiftUIPreview {
|
||||||
Stats.incrementSuccessfulLaunchCount()
|
Stats.incrementSuccessfulLaunchCount()
|
||||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||||
@ -121,15 +136,6 @@ extension MainMenu {
|
|||||||
await AppUpdater().checkForUpdates(userInitiated: false)
|
await AppUpdater().checkForUpdates(userInitiated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the linked version has changed between launches of phpmon
|
|
||||||
PhpGuard().compareToLastGlobalVersion()
|
|
||||||
|
|
||||||
// We are ready!
|
|
||||||
Log.info("PHP Monitor is ready to serve!")
|
|
||||||
|
|
||||||
// Check if we upgraded just now
|
|
||||||
AppUpdater.checkIfUpdateWasPerformed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +16,9 @@ extension MainMenu {
|
|||||||
|
|
||||||
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
||||||
// Mark as no longer busy
|
// Mark as no longer busy
|
||||||
PhpEnvironments.shared.isBusy = false
|
Task { @MainActor in
|
||||||
|
PhpEnvironments.shared.isBusy = false
|
||||||
|
}
|
||||||
|
|
||||||
Task { // Things to do after reloading domain list data
|
Task { // Things to do after reloading domain list data
|
||||||
if Valet.installed {
|
if Valet.installed {
|
||||||
@ -25,7 +27,7 @@ extension MainMenu {
|
|||||||
|
|
||||||
// Perform UI updates on main thread
|
// Perform UI updates on main thread
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
|
|
||||||
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
||||||
|
@ -37,8 +37,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
// MARK: - UI related
|
// MARK: - UI related
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Rebuilds the menu (either asynchronously or synchronously).
|
Rebuilds the menu on the main thread.
|
||||||
Defaults to rebuilding the menu asynchronously.
|
|
||||||
*/
|
*/
|
||||||
func rebuild() {
|
func rebuild() {
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
@ -80,13 +79,15 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func refreshActiveInstallation() {
|
@objc func refreshActiveInstallation() {
|
||||||
if !PhpEnvironments.shared.isBusy {
|
if !PhpEnvironments.shared.isBusy {
|
||||||
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
||||||
updatePhpVersionInStatusBar()
|
refreshIcon()
|
||||||
|
rebuild()
|
||||||
} else {
|
} else {
|
||||||
Log.perf("Skipping version refresh due to busy status!")
|
Log.perf("Skipping version refresh due to busy status!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||||
|
@available(*, deprecated, message: "Use the busy status instead")
|
||||||
@objc func updatePhpVersionInStatusBar() {
|
@objc func updatePhpVersionInStatusBar() {
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
@ -139,7 +140,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
@objc func reloadPhpMonitorMenuInBackground() {
|
@objc func reloadPhpMonitorMenuInBackground() {
|
||||||
asyncExecution({
|
asyncExecution({
|
||||||
// This automatically reloads the menu
|
// This automatically reloads the menu
|
||||||
Log.info("Reloading information about the PHP installation (in the background)...")
|
Log.perf("Reloading information about the PHP installation (in the background)...")
|
||||||
}, behaviours: [
|
}, behaviours: [
|
||||||
.setsBusyUI,
|
.setsBusyUI,
|
||||||
.reloadsPhpInstallation,
|
.reloadsPhpInstallation,
|
||||||
@ -150,13 +151,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
|
|
||||||
/** Refreshes the icon with the PHP version. */
|
/** Refreshes the icon with the PHP version. */
|
||||||
@objc func refreshIcon() {
|
@objc func refreshIcon() {
|
||||||
|
|
||||||
Task { @MainActor [self] in
|
Task { @MainActor [self] in
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
Log.perf("Refreshing icon: currently busy")
|
||||||
|
setStatusBar(image: NSImage.statusBarIcon)
|
||||||
} else {
|
} else {
|
||||||
|
Log.perf("Refreshing icon: no longer busy")
|
||||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||||
// Static icon has been requested
|
// Static icon has been requested
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
setStatusBar(image: NSImage.statusBarIconStatic)
|
||||||
} else {
|
} else {
|
||||||
// The dynamic icon has been requested
|
// The dynamic icon has been requested
|
||||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||||
@ -172,13 +176,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon to be displayed as busy. */
|
|
||||||
@objc func setBusyImage() {
|
|
||||||
Task { @MainActor [self] in
|
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Menu Item Functionality
|
// MARK: - Menu Item Functionality
|
||||||
|
|
||||||
@objc func openAbout() {
|
@objc func openAbout() {
|
||||||
@ -206,6 +203,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 +215,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
PhpVersionManagerWindowController.show()
|
PhpVersionManagerWindowController.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openPhpExtensionManager() {
|
||||||
|
PhpExtensionManagerWindowController.show()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func openDonate() {
|
@objc func openDonate() {
|
||||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import Cocoa
|
|||||||
|
|
||||||
extension StatusMenu {
|
extension StatusMenu {
|
||||||
|
|
||||||
func addPhpVersionMenuItems() {
|
@MainActor func addPhpVersionMenuItems() {
|
||||||
if PhpEnvironments.phpInstall == nil {
|
if PhpEnvironments.phpInstall == nil {
|
||||||
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
||||||
addItems([
|
addItems([
|
||||||
@ -34,7 +34,7 @@ extension StatusMenu {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPhpActionMenuItems() {
|
@MainActor func addPhpActionMenuItems() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
addItem(NSMenuItem(title: "mi_busy".localized))
|
addItem(NSMenuItem(title: "mi_busy".localized))
|
||||||
return
|
return
|
||||||
@ -54,7 +54,7 @@ extension StatusMenu {
|
|||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
func addServicesManagerMenuItem() {
|
@MainActor func addServicesManagerMenuItem() {
|
||||||
if PhpEnvironments.shared.isBusy {
|
if PhpEnvironments.shared.isBusy {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSwitchToPhpMenuItems() {
|
@MainActor func addSwitchToPhpMenuItems() {
|
||||||
var shortcutKey = 1
|
var shortcutKey = 1
|
||||||
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
||||||
// Get the short and long version
|
// Get the short and long version
|
||||||
@ -102,14 +102,14 @@ extension StatusMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addLiteModeMenuItem() {
|
@MainActor func addLiteModeMenuItem() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPreferencesMenuItems() {
|
@MainActor func addPreferencesMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_preferences".localized,
|
NSMenuItem(title: "mi_preferences".localized,
|
||||||
@ -119,7 +119,7 @@ extension StatusMenu {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func addCoreMenuItems() {
|
@MainActor func addCoreMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
NSMenuItem.separator(),
|
NSMenuItem.separator(),
|
||||||
NSMenuItem(title: "mi_about".localized,
|
NSMenuItem(title: "mi_about".localized,
|
||||||
@ -131,7 +131,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Valet
|
// MARK: - Valet
|
||||||
|
|
||||||
func addValetMenuItems() {
|
@MainActor func addValetMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_valet".localized),
|
HeaderView.asMenuItem(text: "mi_valet".localized),
|
||||||
NSMenuItem(title: "mi_valet_config".localized,
|
NSMenuItem(title: "mi_valet_config".localized,
|
||||||
@ -146,12 +146,15 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - PHP Configuration
|
// MARK: - PHP Configuration
|
||||||
|
|
||||||
func addConfigurationMenuItems() {
|
@MainActor func addConfigurationMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
||||||
NSMenuItem(title: "mi_php_version_manager".localized,
|
NSMenuItem(title: "mi_php_version_manager".localized,
|
||||||
action: #selector(MainMenu.openPhpVersionManager),
|
action: #selector(MainMenu.openPhpVersionManager),
|
||||||
keyEquivalent: "m"),
|
keyEquivalent: "m"),
|
||||||
|
NSMenuItem(title: "mi_php_ext_manager".localized,
|
||||||
|
action: #selector(MainMenu.openPhpExtensionManager),
|
||||||
|
keyEquivalent: "e"),
|
||||||
NSMenuItem(title: "mi_php_config".localized,
|
NSMenuItem(title: "mi_php_config".localized,
|
||||||
action: #selector(MainMenu.openActiveConfigFolder),
|
action: #selector(MainMenu.openActiveConfigFolder),
|
||||||
keyEquivalent: "c"),
|
keyEquivalent: "c"),
|
||||||
@ -166,7 +169,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Composer
|
// MARK: - Composer
|
||||||
|
|
||||||
func addComposerMenuItems() {
|
@MainActor func addComposerMenuItems() {
|
||||||
addItems([
|
addItems([
|
||||||
HeaderView.asMenuItem(text: "mi_composer".localized),
|
HeaderView.asMenuItem(text: "mi_composer".localized),
|
||||||
NSMenuItem(
|
NSMenuItem(
|
||||||
@ -187,7 +190,7 @@ extension StatusMenu {
|
|||||||
|
|
||||||
// MARK: - Stats
|
// MARK: - Stats
|
||||||
|
|
||||||
func addStatsMenuItem() {
|
@MainActor func addStatsMenuItem() {
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("Not showing stats menu item if no PHP version is linked.")
|
Log.info("Not showing stats menu item if no PHP version is linked.")
|
||||||
return
|
return
|
||||||
@ -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
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ class BetterAlert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
|
||||||
windowController.window?.makeKeyAndOrderFront(nil)
|
windowController.window?.makeKeyAndOrderFront(nil)
|
||||||
windowController.window?.setCenterPosition(offsetY: 70)
|
windowController.window?.setCenterPosition(offsetY: 70)
|
||||||
return NSApplication.shared.runModal(for: windowController.window!)
|
return NSApplication.shared.runModal(for: windowController.window!)
|
||||||
|
@ -20,6 +20,7 @@ enum PreferenceName: String, Codable {
|
|||||||
case globalHotkey = "global_hotkey"
|
case globalHotkey = "global_hotkey"
|
||||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||||
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
||||||
|
case languageOverride = "language_override"
|
||||||
|
|
||||||
// APPEARANCE
|
// APPEARANCE
|
||||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||||
@ -84,7 +85,8 @@ enum PreferenceName: String, Codable {
|
|||||||
],
|
],
|
||||||
.string: [
|
.string: [
|
||||||
.globalHotkey,
|
.globalHotkey,
|
||||||
.iconTypeToDisplay
|
.iconTypeToDisplay,
|
||||||
|
.languageOverride
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ class Preferences {
|
|||||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||||
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
||||||
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
||||||
|
PreferenceName.languageOverride.rawValue: "",
|
||||||
|
|
||||||
/// Preferences: Appearance
|
/// Preferences: Appearance
|
||||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||||
|
@ -17,7 +17,9 @@ class GeneralPreferencesVC: GenericPreferenceVC {
|
|||||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||||
|
|
||||||
_ = vc.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
_ = vc
|
||||||
|
.addView(when: true, vc.getLanguageOptionsPV())
|
||||||
|
.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
||||||
.addView(when: true, vc.getAutoRestartServicesPV())
|
.addView(when: true, vc.getAutoRestartServicesPV())
|
||||||
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
||||||
.addView(when: true, vc.getShortcutPV())
|
.addView(when: true, vc.getShortcutPV())
|
||||||
|
@ -48,11 +48,44 @@ class GenericPreferenceVC: NSViewController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLanguageOptionsPV() -> NSView {
|
||||||
|
var options = Bundle.main.localizations
|
||||||
|
.filter({ $0 != "Base"})
|
||||||
|
.map({ lang in
|
||||||
|
return PreferenceDropdownOption(
|
||||||
|
label: Locale.current.localizedString(forLanguageCode: lang)!,
|
||||||
|
value: lang
|
||||||
|
)
|
||||||
|
})
|
||||||
|
options.insert(PreferenceDropdownOption(label: "System Default", value: ""), at: 0)
|
||||||
|
|
||||||
|
return SelectPreferenceView.make(
|
||||||
|
sectionText: "prefs.language".localized,
|
||||||
|
descriptionText: "prefs.language_options_desc".localized,
|
||||||
|
options: options,
|
||||||
|
preference: .languageOverride,
|
||||||
|
action: {
|
||||||
|
MainMenu.shared.refreshIcon()
|
||||||
|
MainMenu.shared.rebuild()
|
||||||
|
|
||||||
|
if let window = App.shared.preferencesWindowController?.window {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "alert.language_changed.title".localized
|
||||||
|
alert.informativeText = "alert.language_changed.subtitle".localized
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.addButton(withTitle: "generic.ok".localized)
|
||||||
|
alert.beginSheetModal(for: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func getIconOptionsPV() -> NSView {
|
func getIconOptionsPV() -> NSView {
|
||||||
return SelectPreferenceView.make(
|
return SelectPreferenceView.make(
|
||||||
sectionText: "",
|
sectionText: "",
|
||||||
descriptionText: "prefs.icon_options_desc".localized,
|
descriptionText: "prefs.icon_options_desc".localized,
|
||||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
options: MenuBarIcon.allCases
|
||||||
|
.map({ return PreferenceDropdownOption(label: $0.rawValue, value: $0.rawValue) }),
|
||||||
localizationPrefix: "prefs.icon_options",
|
localizationPrefix: "prefs.icon_options",
|
||||||
preference: .iconTypeToDisplay,
|
preference: .iconTypeToDisplay,
|
||||||
action: {
|
action: {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +84,10 @@ class Stats {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func clearCurrentGlobalPhpVersion() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: InternalStats.lastGlobalPhpVersion.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine if the sponsor message should be displayed.
|
Determine if the sponsor message should be displayed.
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
|
@ -9,30 +9,34 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class SelectPreferenceView: NSView, XibLoadable {
|
struct PreferenceDropdownOption {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectPreferenceView: NSView, XibLoadable {
|
||||||
@IBOutlet weak var labelSection: NSTextField!
|
@IBOutlet weak var labelSection: NSTextField!
|
||||||
@IBOutlet weak var labelDescription: NSTextField!
|
@IBOutlet weak var labelDescription: NSTextField!
|
||||||
@IBOutlet weak var popupButton: NSPopUpButton!
|
@IBOutlet weak var popupButton: NSPopUpButton!
|
||||||
|
|
||||||
var localizationPrefix: String = ""
|
var localizationPrefix: String?
|
||||||
var imagePrefix: String?
|
var imagePrefix: String?
|
||||||
|
|
||||||
var options: [String] = [] {
|
var options: [PreferenceDropdownOption] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
self.popupButton.removeAllItems()
|
self.popupButton.removeAllItems()
|
||||||
self.options.forEach { value in
|
self.options.forEach { option in
|
||||||
self.popupButton.addItem(
|
if let prefix = localizationPrefix {
|
||||||
withTitle: "\(localizationPrefix).\(value)".localized
|
self.popupButton.addItem(withTitle: "\(prefix).\(option.label)".localized)
|
||||||
)
|
} else {
|
||||||
|
self.popupButton.addItem(withTitle: option.label)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if imagePrefix == nil {
|
if let prefix = imagePrefix {
|
||||||
return
|
self.popupButton.itemArray.enumerated().forEach { item in
|
||||||
}
|
item.element.image = NSImage(named: "\(prefix)_\(self.options[item.offset].value)")
|
||||||
|
}
|
||||||
self.popupButton.itemArray.enumerated().forEach { item in
|
|
||||||
item.element.image = NSImage(named: "\(imagePrefix!)_\(self.options[item.offset])")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,19 +47,18 @@ class SelectPreferenceView: NSView, XibLoadable {
|
|||||||
didSet {
|
didSet {
|
||||||
let value = Preferences.preferences[preference] as! String
|
let value = Preferences.preferences[preference] as! String
|
||||||
self.options.enumerated().forEach { option in
|
self.options.enumerated().forEach { option in
|
||||||
if option.element == value {
|
if option.element.value == value {
|
||||||
self.popupButton.selectItem(at: option.offset)
|
self.popupButton.selectItem(at: option.offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable function_parameter_count
|
|
||||||
static func make(
|
static func make(
|
||||||
sectionText: String,
|
sectionText: String,
|
||||||
descriptionText: String,
|
descriptionText: String,
|
||||||
options: [String],
|
options: [PreferenceDropdownOption],
|
||||||
localizationPrefix: String,
|
localizationPrefix: String? = nil,
|
||||||
imagePrefix: String? = nil,
|
imagePrefix: String? = nil,
|
||||||
preference: PreferenceName,
|
preference: PreferenceName,
|
||||||
action: @escaping () -> Void) -> NSView {
|
action: @escaping () -> Void) -> NSView {
|
||||||
@ -72,11 +75,10 @@ class SelectPreferenceView: NSView, XibLoadable {
|
|||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
// swiftlint:enable function_parameter_count
|
|
||||||
|
|
||||||
@IBAction func valueChanged(_ sender: Any) {
|
@IBAction func valueChanged(_ sender: Any) {
|
||||||
let index = self.popupButton.indexOfSelectedItem
|
let index = self.popupButton.indexOfSelectedItem
|
||||||
Preferences.update(.iconTypeToDisplay, value: self.options[index])
|
Preferences.update(self.preference, value: self.options[index].value)
|
||||||
self.action()
|
self.action()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@ -13,16 +13,16 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||||
<rect key="frame" x="183" y="5" width="395" height="14"/>
|
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||||
<rect key="frame" x="13" y="29" width="154" height="16"/>
|
<rect key="frame" x="-2" y="29" width="154" height="16"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
@ -33,7 +33,7 @@
|
|||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YaB-Tg-Ir3">
|
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YaB-Tg-Ir3">
|
||||||
<rect key="frame" x="182" y="23" width="110" height="25"/>
|
<rect key="frame" x="167" y="23" width="110" height="25"/>
|
||||||
<popUpButtonCell key="cell" type="push" title="Icon Option" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SaA-mm-HBo" id="Su6-C4-aGo">
|
<popUpButtonCell key="cell" type="push" title="Icon Option" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SaA-mm-HBo" id="Su6-C4-aGo">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="menu"/>
|
<font key="font" metaFont="menu"/>
|
||||||
@ -58,7 +58,7 @@
|
|||||||
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
||||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
||||||
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="15" id="Ztd-uk-4aw"/>
|
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" id="aBU-J8-gRK"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -27,15 +27,15 @@ struct HelpButton: View {
|
|||||||
.buttonStyle(BorderlessButtonStyle())
|
.buttonStyle(BorderlessButtonStyle())
|
||||||
.focusable(false)
|
.focusable(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
struct HelpButton_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
#Preview("Light Mode") {
|
||||||
Group {
|
HelpButton(action: {})
|
||||||
HelpButton(action: {}).padding()
|
.padding(100)
|
||||||
.previewDisplayName("Light Mode")
|
}
|
||||||
HelpButton(action: {}).padding().preferredColorScheme(.dark)
|
|
||||||
.previewDisplayName("Dark Mode")
|
#Preview("Dark Mode") {
|
||||||
}
|
HelpButton(action: {})
|
||||||
}
|
.padding(100)
|
||||||
}
|
.preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,9 @@ var isRunningSwiftUIPreview: Bool {
|
|||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
public static var appPrimary: Color = Color("AppColor")
|
public static var appPrimary: Color = Color("AppColor")
|
||||||
public static var appSecondary: Color = Color("AppSecondary")
|
|
||||||
|
// This next one is generated automatically via asset catalogs now
|
||||||
|
// public static var appSecondary: Color = Color("AppSecondary")
|
||||||
|
|
||||||
public static var debug: Color = {
|
public static var debug: Color = {
|
||||||
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
||||||
|
@ -30,8 +30,6 @@ struct NoDomainResults: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NoDomainResults_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
NoDomainResults()
|
||||||
NoDomainResults()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -126,78 +126,82 @@ struct DisclaimerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VersionPopoverView_Previews: PreviewProvider {
|
#Preview("Unknown Requirement") {
|
||||||
static var previews: some View {
|
VersionPopoverView(
|
||||||
VersionPopoverView(
|
site: FakeValetSite(
|
||||||
site: FakeValetSite(
|
fakeWithName: "amazingwebsite",
|
||||||
fakeWithName: "amazingwebsite",
|
tld: "test",
|
||||||
tld: "test",
|
secure: true,
|
||||||
secure: true,
|
path: "/path/to/site",
|
||||||
path: "/path/to/site",
|
linked: true,
|
||||||
linked: true,
|
constraint: ""
|
||||||
constraint: ""
|
),
|
||||||
),
|
validPhpVersions: [],
|
||||||
validPhpVersions: [],
|
parent: nil
|
||||||
parent: nil
|
)
|
||||||
)
|
}
|
||||||
.previewDisplayName("Unknown Requirement")
|
|
||||||
|
#Preview("Requirement Matches") {
|
||||||
VersionPopoverView(
|
VersionPopoverView(
|
||||||
site: FakeValetSite(
|
site: FakeValetSite(
|
||||||
fakeWithName: "amazingwebsite",
|
fakeWithName: "amazingwebsite",
|
||||||
tld: "test",
|
tld: "test",
|
||||||
secure: true,
|
secure: true,
|
||||||
path: "/path/to/site",
|
path: "/path/to/site",
|
||||||
linked: true,
|
linked: true,
|
||||||
constraint: "^8.1"
|
constraint: "^8.1"
|
||||||
),
|
),
|
||||||
validPhpVersions: [],
|
validPhpVersions: [],
|
||||||
parent: nil
|
parent: nil
|
||||||
)
|
)
|
||||||
.previewDisplayName("Requirement Matches")
|
}
|
||||||
VersionPopoverView(
|
|
||||||
site: FakeValetSite(
|
#Preview("Isolated") {
|
||||||
fakeWithName: "anothersite",
|
VersionPopoverView(
|
||||||
tld: "test",
|
site: FakeValetSite(
|
||||||
secure: true,
|
fakeWithName: "anothersite",
|
||||||
path: "/path/to/site",
|
tld: "test",
|
||||||
linked: true,
|
secure: true,
|
||||||
constraint: "^8.0",
|
path: "/path/to/site",
|
||||||
isolated: "8.0"
|
linked: true,
|
||||||
),
|
constraint: "^8.0",
|
||||||
validPhpVersions: [],
|
isolated: "8.0"
|
||||||
parent: nil
|
),
|
||||||
)
|
validPhpVersions: [],
|
||||||
.previewDisplayName("Isolated")
|
parent: nil
|
||||||
VersionPopoverView(
|
)
|
||||||
site: FakeValetSite(
|
}
|
||||||
fakeWithName: "anothersite",
|
|
||||||
tld: "test",
|
#Preview("Isolated Mismatch") {
|
||||||
secure: true,
|
VersionPopoverView(
|
||||||
path: "/path/to/site",
|
site: FakeValetSite(
|
||||||
linked: true,
|
fakeWithName: "anothersite",
|
||||||
constraint: "^8.0",
|
tld: "test",
|
||||||
isolated: "7.4"
|
secure: true,
|
||||||
),
|
path: "/path/to/site",
|
||||||
validPhpVersions: [],
|
linked: true,
|
||||||
parent: nil
|
constraint: "^8.0",
|
||||||
)
|
isolated: "7.4"
|
||||||
.previewDisplayName("Isolated Mismatch")
|
),
|
||||||
VersionPopoverView(
|
validPhpVersions: [],
|
||||||
site: FakeValetSite(
|
parent: nil
|
||||||
fakeWithName: "anothersite",
|
)
|
||||||
tld: "test",
|
}
|
||||||
secure: true,
|
|
||||||
path: "/path/to/site",
|
#Preview("Recommend Alternatives") {
|
||||||
linked: true,
|
VersionPopoverView(
|
||||||
constraint: "^8.0"
|
site: FakeValetSite(
|
||||||
),
|
fakeWithName: "anothersite",
|
||||||
validPhpVersions: [
|
tld: "test",
|
||||||
VersionNumber(major: 8, minor: 0, patch: 0),
|
secure: true,
|
||||||
VersionNumber(major: 8, minor: 1, patch: 0)
|
path: "/path/to/site",
|
||||||
],
|
linked: true,
|
||||||
parent: nil
|
constraint: "^8.0"
|
||||||
)
|
),
|
||||||
.previewDisplayName("Recommend Alternatives")
|
validPhpVersions: [
|
||||||
}
|
VersionNumber(major: 8, minor: 0, patch: 0),
|
||||||
|
VersionNumber(major: 8, minor: 1, patch: 0)
|
||||||
|
],
|
||||||
|
parent: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,7 @@ struct HeaderView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HeaderView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
HeaderView(text: "Hello world")
|
||||||
HeaderView(text: "Hello world")
|
.frame(width: 330.0)
|
||||||
.frame(width: 330.0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -18,5 +18,6 @@ struct SectionHeaderView: View {
|
|||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.appSecondary)
|
.foregroundColor(.appSecondary)
|
||||||
.background(Color.debug)
|
.background(Color.debug)
|
||||||
|
.minimumScaleFactor(0.8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,23 +172,21 @@ struct ServiceView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ServicesView_Previews: PreviewProvider {
|
#Preview("Active 1") {
|
||||||
static var previews: some View {
|
ServicesView(manager: FakeServicesManager(
|
||||||
ServicesView(manager: FakeServicesManager(
|
formulae: ["php", "nginx", "dnsmasq"],
|
||||||
formulae: ["php", "nginx", "dnsmasq"],
|
status: .active
|
||||||
status: .active
|
), perRow: 4)
|
||||||
), perRow: 4)
|
.frame(width: 330.0)
|
||||||
.frame(width: 330.0)
|
}
|
||||||
.previewDisplayName("Active 1")
|
|
||||||
|
#Preview("Active 2") {
|
||||||
ServicesView(manager: FakeServicesManager(
|
ServicesView(manager: FakeServicesManager(
|
||||||
formulae: [
|
formulae: [
|
||||||
"php", "nginx", "dnsmasq", "thing1",
|
"php", "nginx", "dnsmasq", "thing1",
|
||||||
"thing2", "thing3", "thing4", "thing5"
|
"thing2", "thing3", "thing4", "thing5"
|
||||||
],
|
],
|
||||||
status: .inactive
|
status: .inactive
|
||||||
), perRow: 4)
|
), perRow: 4)
|
||||||
.frame(width: 330.0)
|
.frame(width: 330.0)
|
||||||
.previewDisplayName("Active 2")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -60,38 +60,49 @@ struct StatsView: View {
|
|||||||
.padding(.leading, 30)
|
.padding(.leading, 30)
|
||||||
.padding(.trailing, 30)
|
.padding(.trailing, 30)
|
||||||
} else {
|
} else {
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 30) {
|
HStack(alignment: .center, spacing: 10) {
|
||||||
VStack(alignment: .center, spacing: 3) {
|
VStack(alignment: .center, spacing: 3) {
|
||||||
SectionHeaderView(text: "mi_memory_limit".localized.uppercased())
|
SectionHeaderView(text: "mi_memory_limit".localized.uppercased())
|
||||||
Text(memoryLimit)
|
Text(memoryLimit)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.font(.system(size: 16))
|
.font(.system(size: 16))
|
||||||
}
|
}
|
||||||
|
Divider()
|
||||||
VStack(alignment: .center, spacing: 3) {
|
VStack(alignment: .center, spacing: 3) {
|
||||||
SectionHeaderView(text: "mi_post_max_size".localized.uppercased())
|
SectionHeaderView(text: "mi_post_max_size".localized.uppercased())
|
||||||
Text(maxPostSize)
|
Text(maxPostSize)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.font(.system(size: 16))
|
.font(.system(size: 16))
|
||||||
}
|
}
|
||||||
|
Divider()
|
||||||
VStack(alignment: .center, spacing: 3) {
|
VStack(alignment: .center, spacing: 3) {
|
||||||
SectionHeaderView(text: "mi_upload_max_filesize".localized.uppercased())
|
SectionHeaderView(text: "mi_upload_max_filesize".localized.uppercased())
|
||||||
Text(maxUploadSize)
|
Text(maxUploadSize)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.font(.system(size: 16))
|
.font(.system(size: 16))
|
||||||
}
|
}
|
||||||
|
Divider().hidden()
|
||||||
|
Button {
|
||||||
|
Task { @MainActor in
|
||||||
|
MainMenu.shared.openConfigGUI()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "gearshape.fill")
|
||||||
|
}
|
||||||
|
.accessibility(identifier: "phpConfigButton")
|
||||||
|
.focusable(false)
|
||||||
|
.frame(minWidth: 30, alignment: .center)
|
||||||
}
|
}
|
||||||
.padding(10)
|
.padding(5)
|
||||||
.background(Color.debug)
|
.background(Color.debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StatsView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
StatsView(
|
||||||
StatsView(
|
memoryLimit: "1024 MB",
|
||||||
memoryLimit: "1024 MB",
|
maxPostSize: "1024 MB",
|
||||||
maxPostSize: "1024 MB",
|
maxUploadSize: "1024 MB"
|
||||||
maxUploadSize: "1024 MB"
|
).frame(height: 100)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
//
|
|
||||||
// ProgressViewSubject.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 11/03/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class ProgressViewSubject: ObservableObject {
|
|
||||||
@Published var title: String
|
|
||||||
@Published var description: String?
|
|
||||||
@Published var progress: Double
|
|
||||||
|
|
||||||
init(title: String, description: String) {
|
|
||||||
self.title = title
|
|
||||||
self.description = description
|
|
||||||
self.progress = 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
//
|
|
||||||
// ProgressWindowView.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 11/03/2023.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ProgressWindowView: View {
|
|
||||||
@ObservedObject var subject: ProgressViewSubject
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(subject.title)
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.bold()
|
|
||||||
if subject.description != nil {
|
|
||||||
Text(subject.description!)
|
|
||||||
.font(.system(size: 13))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.leading, 20)
|
|
||||||
.padding(.top, 12)
|
|
||||||
ProgressView(value: subject.progress)
|
|
||||||
.padding(.top, 0)
|
|
||||||
.padding(.bottom, 12)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor static func display(_ subject: ProgressViewSubject) async -> NSWindowController {
|
|
||||||
let view = ProgressWindowView(subject: subject)
|
|
||||||
|
|
||||||
let window = NSWindow(
|
|
||||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 240),
|
|
||||||
styleMask: [.titled, .closable, .utilityWindow],
|
|
||||||
backing: .buffered,
|
|
||||||
defer: false
|
|
||||||
)
|
|
||||||
|
|
||||||
window.title = ""
|
|
||||||
window.titlebarAppearsTransparent = true
|
|
||||||
window.contentView = NSHostingView(rootView: view)
|
|
||||||
let controller = NSWindowController(window: window)
|
|
||||||
controller.showWindow(nil)
|
|
||||||
controller.positionWindowInTopLeftCorner()
|
|
||||||
controller.window?.makeKeyAndOrderFront(self)
|
|
||||||
// NSApp.activate(ignoringOtherApps: true)
|
|
||||||
return controller
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ProgressWindowView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
ProgressWindowView(
|
|
||||||
subject: ProgressViewSubject(
|
|
||||||
title: "Long running task",
|
|
||||||
description: "Please be patient"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,7 +20,7 @@ class TerminalProgressWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
|
|
||||||
windowController.showWindow(windowController)
|
windowController.showWindow(windowController)
|
||||||
windowController.window?.makeKeyAndOrderFront(nil)
|
windowController.window?.makeKeyAndOrderFront(nil)
|
||||||
windowController.positionWindowInTopLeftCorner()
|
windowController.positionWindowInTopRightCorner()
|
||||||
|
|
||||||
windowController.progressView?.labelTitle.stringValue = title
|
windowController.progressView?.labelTitle.stringValue = title
|
||||||
windowController.progressView?.labelDescription.stringValue = description
|
windowController.progressView?.labelDescription.stringValue = description
|
||||||
|
@ -17,13 +17,13 @@ extension App {
|
|||||||
onChange: { Task { await self.onHomebrewPhpModification() } }
|
onChange: { Task { await self.onHomebrewPhpModification() } }
|
||||||
)
|
)
|
||||||
|
|
||||||
App.shared.watchers[.homebrewBinaries] = notifier
|
App.shared.watchers["homebrewBinaries"] = notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
public func destroyHomebrewWatchers() {
|
public func destroyHomebrewWatchers() {
|
||||||
// Removing requires termination and then removing reference
|
// Removing requires termination and then removing reference
|
||||||
self.watchers[.homebrewBinaries]?.terminate()
|
self.watchers["homebrewBinaries"]?.terminate()
|
||||||
self.watchers[.homebrewBinaries] = nil
|
self.watchers["homebrewBinaries"] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func onHomebrewPhpModification() async {
|
public func onHomebrewPhpModification() async {
|
||||||
@ -31,10 +31,13 @@ extension App {
|
|||||||
Log.info("Something changed in the Homebrew binary directory...")
|
Log.info("Something changed in the Homebrew binary directory...")
|
||||||
await PhpEnvironments.detectPhpVersions()
|
await PhpEnvironments.detectPhpVersions()
|
||||||
await MainMenu.shared.refreshActiveInstallation()
|
await MainMenu.shared.refreshActiveInstallation()
|
||||||
// let new = PhpEnvironments.shared.currentInstall?.version.text
|
|
||||||
|
|
||||||
// TODO:
|
//
|
||||||
// Check if the new and previous version are different
|
// TODO: PHP Guard 2.0
|
||||||
// if so, we can show a notification if needed
|
// Check if the new and previous version of PHP are different
|
||||||
|
// if so, we can show a notification if needed or alert the user
|
||||||
|
//
|
||||||
|
// let new = PhpEnvironments.shared.currentInstall?.version.text
|
||||||
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,52 +10,52 @@ import Foundation
|
|||||||
|
|
||||||
extension App {
|
extension App {
|
||||||
|
|
||||||
func startWatcher(_ url: URL) {
|
func startWatchManager(_ url: URL) {
|
||||||
Log.perf("No watcher currently active...")
|
Log.perf("Starting config watch manager...")
|
||||||
self.watcher = PhpConfigWatcher(for: url)
|
self.watchManager = ConfigWatchManager(for: url)
|
||||||
|
|
||||||
self.watcher.didChange = { url in
|
self.watchManager.didChange = { url in
|
||||||
Log.perf("Something has changed in: \(url)")
|
Log.perf("Something has changed in: \(url)")
|
||||||
|
|
||||||
// Check if the watcher has last updated the menu less than 0.75s ago
|
// Check if the watcher has last updated the menu less than 0.75s ago
|
||||||
let distance = self.watcher.lastUpdate?.distance(to: Date().timeIntervalSince1970)
|
let distance = self.watchManager.lastUpdate?.distance(to: Date().timeIntervalSince1970)
|
||||||
if distance == nil || distance != nil && distance! > 0.75 {
|
if distance == nil || distance != nil && distance! > 0.75 {
|
||||||
Log.perf("Refreshing menu...")
|
Log.perf("Refreshing menu...")
|
||||||
Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() }
|
Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() }
|
||||||
self.watcher.lastUpdate = Date().timeIntervalSince1970
|
self.watchManager.lastUpdate = Date().timeIntervalSince1970
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePhpConfigWatcher(forceReload: Bool = false) {
|
func handlePhpConfigWatcher(forceReload: Bool = false) {
|
||||||
if ActiveFileSystem.shared is TestableFileSystem {
|
if ActiveFileSystem.shared is TestableFileSystem {
|
||||||
Log.warn("FS watcher is disabled when using testable filesystem.")
|
Log.warn("Config watch manager is disabled when using testable filesystem.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let install = PhpEnvironments.phpInstall else {
|
guard let install = PhpEnvironments.phpInstall else {
|
||||||
Log.info("It appears as if no PHP installation is currently active.")
|
Log.info("It appears as if no PHP installation is currently active.")
|
||||||
Log.info("The FS watcher will be disabled until a PHP install is active.")
|
Log.info("The config watch manager be disabled until a PHP install is active.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(install.version.short)")
|
let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(install.version.short)")
|
||||||
|
|
||||||
// Check whether the watcher exists and schedule on the main thread
|
// Check whether the manager exists and schedule on the main thread
|
||||||
// if we don't consistently do this, the app will create duplicate watchers
|
// if we don't consistently do this, the app will create duplicate watchers
|
||||||
// due to timing issues, which creates retain cycles.
|
// due to timing issues, which creates retain cycles
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
// Watcher needs to be created
|
// Watcher needs to be created
|
||||||
if self.watcher == nil {
|
if self.watchManager == nil {
|
||||||
self.startWatcher(url)
|
self.startWatchManager(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watcher needs to be updated
|
// Watcher needs to be updated
|
||||||
if self.watcher.url != url || forceReload {
|
if self.watchManager.url != url || forceReload {
|
||||||
self.watcher.disable()
|
self.watchManager.disable()
|
||||||
self.watcher = nil
|
self.watchManager = nil
|
||||||
Log.perf("Watcher has stopped watching files. Starting new one...")
|
Log.perf("Watcher has stopped watching files. Starting new one...")
|
||||||
self.startWatcher(url)
|
self.startWatchManager(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
76
phpmon/Domain/Watcher/ConfigFSNotifier.swift
Normal file
76
phpmon/Domain/Watcher/ConfigFSNotifier.swift
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//
|
||||||
|
// ConfigFSNotifier.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 24/10/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ConfigFSNotifier {
|
||||||
|
|
||||||
|
enum Behaviour {
|
||||||
|
case reloadsMenu
|
||||||
|
case reloadsWatchers
|
||||||
|
}
|
||||||
|
|
||||||
|
private var parent: ConfigWatchManager!
|
||||||
|
|
||||||
|
private var monitoredFolderFileDescriptor: CInt = -1
|
||||||
|
|
||||||
|
private var folderMonitorSource: DispatchSourceFileSystemObject?
|
||||||
|
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
init(
|
||||||
|
for url: URL,
|
||||||
|
eventMask: DispatchSource.FileSystemEvent,
|
||||||
|
parent: ConfigWatchManager,
|
||||||
|
behaviour: ConfigFSNotifier.Behaviour = .reloadsMenu
|
||||||
|
) {
|
||||||
|
self.url = url
|
||||||
|
self.parent = parent
|
||||||
|
self.startMonitoring(eventMask, behaviour: behaviour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMonitoring(
|
||||||
|
_ eventMask: DispatchSource.FileSystemEvent,
|
||||||
|
behaviour: ConfigFSNotifier.Behaviour
|
||||||
|
) {
|
||||||
|
guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoredFolderFileDescriptor = open(url.path, O_EVTONLY)
|
||||||
|
folderMonitorSource = DispatchSource.makeFileSystemObjectSource(
|
||||||
|
fileDescriptor: monitoredFolderFileDescriptor,
|
||||||
|
eventMask: eventMask,
|
||||||
|
queue: parent.folderMonitorQueue
|
||||||
|
)
|
||||||
|
|
||||||
|
folderMonitorSource?.setEventHandler { [weak self] in
|
||||||
|
if behaviour == .reloadsWatchers
|
||||||
|
&& !ConfigWatchManager.ignoresModificationsToConfigValues {
|
||||||
|
// Reload all configuration watchers
|
||||||
|
return App.shared.handlePhpConfigWatcher(forceReload: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.parent.didChange?(self!.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
folderMonitorSource?.setCancelHandler { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
close(self.monitoredFolderFileDescriptor)
|
||||||
|
self.monitoredFolderFileDescriptor = -1
|
||||||
|
self.folderMonitorSource = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
folderMonitorSource?.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopMonitoring() {
|
||||||
|
folderMonitorSource?.cancel()
|
||||||
|
self.parent = nil
|
||||||
|
}
|
||||||
|
}
|
82
phpmon/Domain/Watcher/ConfigWatchManager.swift
Normal file
82
phpmon/Domain/Watcher/ConfigWatchManager.swift
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
//
|
||||||
|
// ConfigWatchManager.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 30/03/2021.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ConfigWatchManager {
|
||||||
|
|
||||||
|
static var ignoresModificationsToConfigValues: Bool = false
|
||||||
|
|
||||||
|
let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent)
|
||||||
|
|
||||||
|
let url: URL
|
||||||
|
var didChange: ((URL) -> Void)?
|
||||||
|
var lastUpdate: TimeInterval?
|
||||||
|
|
||||||
|
var watchers: [ConfigFSNotifier] = []
|
||||||
|
|
||||||
|
init(for url: URL) {
|
||||||
|
if FileSystem is TestableFileSystem {
|
||||||
|
fatalError("""
|
||||||
|
ConfigWatchManager is currently incompatible with a testable filesystem!"
|
||||||
|
You are not allowed to instantiate these while using a testable filesystem.
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
// Add a watcher for php.ini
|
||||||
|
self.addWatcher(for: self.url.appendingPathComponent("php.ini"), eventMask: .write)
|
||||||
|
|
||||||
|
// Add a watcher for conf.d (in case a new file is added or a file is deleted)
|
||||||
|
// This watcher, when triggered, will restart all watchers
|
||||||
|
self.addWatcher(for: self.url.appendingPathComponent("conf.d"), eventMask: .all, behaviour: .reloadsWatchers)
|
||||||
|
|
||||||
|
// Scan the conf.d folder for .ini files, and add a watcher for each file
|
||||||
|
let filePaths = FileManager.default.enumerator(
|
||||||
|
atPath: self.url.appendingPathComponent("conf.d").path
|
||||||
|
)?.allObjects as! [String]
|
||||||
|
|
||||||
|
// Loop over the .ini files that we discovered
|
||||||
|
filePaths.filter { $0.contains(".ini") }.forEach { (file) in
|
||||||
|
// Add a watcher for each file we have discovered
|
||||||
|
self.addWatcher(for: self.url.appendingPathComponent("conf.d/\(file)"), eventMask: .write)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.perf("A watcher exists for the following config paths:")
|
||||||
|
Log.perf(self.watchers.map({ watcher in
|
||||||
|
return watcher.url.relativePath
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addWatcher(
|
||||||
|
for url: URL,
|
||||||
|
eventMask: DispatchSource.FileSystemEvent,
|
||||||
|
behaviour: ConfigFSNotifier.Behaviour = .reloadsMenu
|
||||||
|
) {
|
||||||
|
if !FileSystem.anyExists(url.path) {
|
||||||
|
Log.warn("No watcher was created for \(url.path) because the requested file does not exist.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let watcher = ConfigFSNotifier(for: url, eventMask: eventMask, parent: self, behaviour: behaviour)
|
||||||
|
self.watchers.append(watcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
func disable() {
|
||||||
|
Log.perf("Turning off all individual existing watchers...")
|
||||||
|
self.watchers.forEach { (watcher) in
|
||||||
|
watcher.stopMonitoring()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,12 +6,9 @@
|
|||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Foundation
|
||||||
|
|
||||||
class FSNotifier {
|
class FSNotifier {
|
||||||
enum Kind {
|
|
||||||
case homebrewLocks, homebrewBinaries
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var shared: FSNotifier! = nil
|
public static var shared: FSNotifier! = nil
|
||||||
|
|
||||||
@ -66,4 +63,5 @@ class FSNotifier {
|
|||||||
deinit {
|
deinit {
|
||||||
Log.perf("FSNotifier for \(self.url) will be deinitialized.")
|
Log.perf("FSNotifier for \(self.url) will be deinitialized.")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,154 +0,0 @@
|
|||||||
//
|
|
||||||
// FolderWatcher.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 30/03/2021.
|
|
||||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
class PhpConfigWatcher {
|
|
||||||
|
|
||||||
let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent)
|
|
||||||
|
|
||||||
let url: URL
|
|
||||||
|
|
||||||
var didChange: ((URL) -> Void)?
|
|
||||||
|
|
||||||
var lastUpdate: TimeInterval?
|
|
||||||
|
|
||||||
var watchers: [FSWatcher] = []
|
|
||||||
|
|
||||||
init(for url: URL) {
|
|
||||||
if FileSystem is TestableFileSystem {
|
|
||||||
fatalError("""
|
|
||||||
PhpConfigWatcher is not compatible with testable FS! "
|
|
||||||
You are not allowed to instantiate these while using a testable FS.
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
self.url = url
|
|
||||||
|
|
||||||
// Add a watcher for php.ini
|
|
||||||
self.addWatcher(for: self.url.appendingPathComponent("php.ini"), eventMask: .write)
|
|
||||||
|
|
||||||
// Add a watcher for conf.d (in case a new file is added or a file is deleted)
|
|
||||||
// This watcher, when triggered, will restart all watchers
|
|
||||||
self.addWatcher(for: self.url.appendingPathComponent("conf.d"), eventMask: .all, behaviour: .reloadsWatchers)
|
|
||||||
|
|
||||||
// Scan the conf.d folder for .ini files, and add a watcher for each file
|
|
||||||
let enumerator = FileManager.default.enumerator(atPath: self.url.appendingPathComponent("conf.d").path)
|
|
||||||
let filePaths = enumerator?.allObjects as! [String]
|
|
||||||
|
|
||||||
// Loop over the .ini files that we discovered
|
|
||||||
filePaths.filter { $0.contains(".ini") }.forEach { (file) in
|
|
||||||
// Add a watcher for each file we have discovered
|
|
||||||
self.addWatcher(for: self.url.appendingPathComponent("conf.d/\(file)"), eventMask: .write)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.perf("A watcher exists for the following config paths:")
|
|
||||||
Log.perf(self.watchers.map({ watcher in
|
|
||||||
return watcher.url.relativePath
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func addWatcher(
|
|
||||||
for url: URL,
|
|
||||||
eventMask: DispatchSource.FileSystemEvent,
|
|
||||||
behaviour: FSWatcherBehaviour = .reloadsMenu
|
|
||||||
) {
|
|
||||||
if !FileSystem.anyExists(url.path) {
|
|
||||||
Log.warn("No watcher was created for \(url.path) because the requested file does not exist.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let watcher = FSWatcher(for: url, eventMask: eventMask, parent: self, behaviour: behaviour)
|
|
||||||
self.watchers.append(watcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
func disable() {
|
|
||||||
Log.perf("Turning off all individual existing watchers...")
|
|
||||||
self.watchers.forEach { (watcher) in
|
|
||||||
watcher.stopMonitoring()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
enum FSWatcherBehaviour {
|
|
||||||
case reloadsMenu
|
|
||||||
case reloadsWatchers
|
|
||||||
}
|
|
||||||
|
|
||||||
class FSWatcher {
|
|
||||||
|
|
||||||
private var parent: PhpConfigWatcher!
|
|
||||||
|
|
||||||
private var monitoredFolderFileDescriptor: CInt = -1
|
|
||||||
|
|
||||||
private var folderMonitorSource: DispatchSourceFileSystemObject?
|
|
||||||
|
|
||||||
let url: URL
|
|
||||||
|
|
||||||
init(
|
|
||||||
for url: URL,
|
|
||||||
eventMask: DispatchSource.FileSystemEvent,
|
|
||||||
parent: PhpConfigWatcher,
|
|
||||||
behaviour: FSWatcherBehaviour = .reloadsMenu
|
|
||||||
) {
|
|
||||||
self.url = url
|
|
||||||
self.parent = parent
|
|
||||||
self.startMonitoring(eventMask, behaviour: behaviour)
|
|
||||||
}
|
|
||||||
|
|
||||||
func startMonitoring(
|
|
||||||
_ eventMask: DispatchSource.FileSystemEvent,
|
|
||||||
behaviour: FSWatcherBehaviour
|
|
||||||
) {
|
|
||||||
guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the file or folder referenced by URL for monitoring only.
|
|
||||||
monitoredFolderFileDescriptor = open(url.path, O_EVTONLY)
|
|
||||||
folderMonitorSource = DispatchSource.makeFileSystemObjectSource(
|
|
||||||
fileDescriptor: monitoredFolderFileDescriptor,
|
|
||||||
eventMask: eventMask,
|
|
||||||
queue: parent.folderMonitorQueue
|
|
||||||
)
|
|
||||||
|
|
||||||
// Define the block to call when a file change is detected.
|
|
||||||
folderMonitorSource?.setEventHandler { [weak self] in
|
|
||||||
// The default behaviour is to reload the menu
|
|
||||||
switch behaviour {
|
|
||||||
case .reloadsMenu:
|
|
||||||
// Default behaviour: reload the menu items
|
|
||||||
self?.parent.didChange?(self!.url)
|
|
||||||
case .reloadsWatchers:
|
|
||||||
// Alternative behaviour: reload all watchers
|
|
||||||
App.shared.handlePhpConfigWatcher(forceReload: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
|
|
||||||
folderMonitorSource?.setCancelHandler { [weak self] in
|
|
||||||
guard let self = self else { return }
|
|
||||||
close(self.monitoredFolderFileDescriptor)
|
|
||||||
self.monitoredFolderFileDescriptor = -1
|
|
||||||
self.folderMonitorSource = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start monitoring the directory via the source.
|
|
||||||
folderMonitorSource?.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopMonitoring() {
|
|
||||||
folderMonitorSource?.cancel()
|
|
||||||
self.parent = nil
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,21 +16,19 @@ class DomainListKindCell: NSTableCellView, DomainListCellProtocol {
|
|||||||
|
|
||||||
func populateCell(with site: ValetSite) {
|
func populateCell(with site: ValetSite) {
|
||||||
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
||||||
imageViewType.image = NSImage(
|
imageViewType.image = site.aliasPath == nil
|
||||||
named: site.aliasPath == nil
|
? NSImage.iconParked
|
||||||
? "IconParked"
|
: NSImage.iconLinked
|
||||||
: "IconLinked"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Unless, of course, this is a default site
|
// Unless, of course, this is a default site
|
||||||
if site.absolutePath == Valet.shared.config.defaultSite {
|
if site.absolutePath == Valet.shared.config.defaultSite {
|
||||||
imageViewType.image = NSImage(named: "IconDefault")
|
imageViewType.image = NSImage.iconDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateCell(with proxy: ValetProxy) {
|
func populateCell(with proxy: ValetProxy) {
|
||||||
imageViewType.image = NSImage(named: "IconProxy")
|
imageViewType.image = NSImage.iconProxy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,14 +34,13 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
|||||||
|
|
||||||
if site.isolatedPhpVersion != nil {
|
if site.isolatedPhpVersion != nil {
|
||||||
imageViewPhpVersionOK.isHidden = false
|
imageViewPhpVersionOK.isHidden = false
|
||||||
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
|
imageViewPhpVersionOK.image = NSImage.isolated
|
||||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.isolated".localized(site.servingPhpVersion)
|
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.isolated".localized(site.servingPhpVersion)
|
||||||
} else {
|
} else {
|
||||||
imageViewPhpVersionOK.isHidden = (site.preferredPhpVersion == "???"
|
imageViewPhpVersionOK.isHidden = (site.preferredPhpVersion == "???"
|
||||||
|| !site.isCompatibleWithPreferredPhpVersion)
|
|| !site.isCompatibleWithPreferredPhpVersion)
|
||||||
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
|
imageViewPhpVersionOK.image = NSImage.checkmark
|
||||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.preferredPhpVersion)
|
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.preferredPhpVersion)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +110,22 @@ extension DomainListVC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
||||||
|
Task {
|
||||||
|
self.setUIBusy()
|
||||||
|
|
||||||
|
await sender.phpExtension?.toggle()
|
||||||
|
|
||||||
|
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
|
||||||
|
await Actions.restartPhpFpm()
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadContextMenu()
|
||||||
|
|
||||||
|
self.setUINotBusy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func isolateSite(sender: PhpMenuItem) {
|
@objc func isolateSite(sender: PhpMenuItem) {
|
||||||
guard let site = selectedSite else {
|
guard let site = selectedSite else {
|
||||||
return
|
return
|
||||||
|
@ -42,7 +42,20 @@ extension DomainListVC {
|
|||||||
addDisabledIsolation(to: menu)
|
addDisabledIsolation(to: menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSeparator(to: menu)
|
||||||
|
|
||||||
|
if let extensions = site.isolatedPhpVersion?.extensions ?? PhpEnvironments.phpInstall?.extensions,
|
||||||
|
let version = site.isolatedPhpVersion?.versionNumber.short ?? PhpEnvironments.phpInstall?.version.short {
|
||||||
|
menu.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
|
||||||
|
addMenuItemsForExtensions(
|
||||||
|
to: menu,
|
||||||
|
for: extensions,
|
||||||
|
version: version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
menu.addItem(HeaderView.asMenuItem(text: "domain_list.actions".localized))
|
menu.addItem(HeaderView.asMenuItem(text: "domain_list.actions".localized))
|
||||||
|
|
||||||
addToggleSecure(to: menu, secured: site.secured)
|
addToggleSecure(to: menu, secured: site.secured)
|
||||||
addUnlink(to: menu, with: site)
|
addUnlink(to: menu, with: site)
|
||||||
|
|
||||||
@ -150,6 +163,28 @@ extension DomainListVC {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addMenuItemsForExtensions(to menu: NSMenu, for extensions: [PhpExtension], version: String) {
|
||||||
|
var items: [NSMenuItem] = [
|
||||||
|
NSMenuItem(title: "domain_list.applies_to".localized(version))
|
||||||
|
]
|
||||||
|
|
||||||
|
for phpExtension in extensions {
|
||||||
|
let item = ExtensionMenuItem(
|
||||||
|
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
|
||||||
|
action: #selector(self.toggleExtension),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
item.state = phpExtension.enabled ? .on : .off
|
||||||
|
item.phpExtension = phpExtension
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.addItem(NSMenuItem(title: "domain_list.extensions".localized, submenu: items))
|
||||||
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Menu Items for Proxy
|
// MARK: - Menu Items for Proxy
|
||||||
|
|
||||||
private func addMenuItemsForProxy(_ proxy: ValetProxy) {
|
private func addMenuItemsForProxy(_ proxy: ValetProxy) {
|
||||||
|
@ -85,6 +85,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
|
|
||||||
App.shared.domainListWindowController!.showWindow(self)
|
App.shared.domainListWindowController!.showWindow(self)
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
App.shared.domainListWindowController?.window?.orderFrontRegardless()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
@ -31,7 +31,7 @@ struct OnboardingTextItem: View {
|
|||||||
.opacity(self.unavailable ? 0.5 : 1)
|
.opacity(self.unavailable ? 0.5 : 1)
|
||||||
Text(description.localizedForSwiftUI)
|
Text(description.localizedForSwiftUI)
|
||||||
.foregroundColor(Color.secondary)
|
.foregroundColor(Color.secondary)
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 12))
|
||||||
.lineLimit(4)
|
.lineLimit(4)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.frame(minWidth: 0, maxWidth: 800, alignment: .leading)
|
.frame(minWidth: 0, maxWidth: 800, alignment: .leading)
|
||||||
@ -51,7 +51,7 @@ struct OnboardingView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(nsImage: NSApp.applicationIconImage)
|
Image(nsImage: NSApp.applicationIconImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 80, height: 80)
|
.frame(width: 100, height: 100)
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, 5)
|
||||||
.padding(.trailing, 25)
|
.padding(.trailing, 25)
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
@ -126,20 +126,19 @@ struct OnboardingView: View {
|
|||||||
Button("onboarding.tour.close".localized) {
|
Button("onboarding.tour.close".localized) {
|
||||||
App.shared.onboardingWindowController?.close()
|
App.shared.onboardingWindowController?.close()
|
||||||
}
|
}
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, 15)
|
||||||
.padding(.top, 10)
|
.padding(.top, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading)
|
.padding(.leading)
|
||||||
.padding(.trailing)
|
.padding(.trailing)
|
||||||
|
.padding(.bottom, 0)
|
||||||
}
|
}
|
||||||
|
.frame(width: 600)
|
||||||
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OnboardingView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
OnboardingView()
|
||||||
Group {
|
|
||||||
OnboardingView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class OnboardingWindowController: PMWindowController {
|
|||||||
window.titlebarAppearsTransparent = true
|
window.titlebarAppearsTransparent = true
|
||||||
window.delegate = delegate ?? windowController
|
window.delegate = delegate ?? windowController
|
||||||
window.contentView = NSHostingView(rootView: OnboardingView())
|
window.contentView = NSHostingView(rootView: OnboardingView())
|
||||||
window.setContentSize(NSSize(width: 600, height: 600))
|
window.setContentSize(window.contentView!.fittingSize)
|
||||||
|
|
||||||
App.shared.onboardingWindowController = windowController
|
App.shared.onboardingWindowController = windowController
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,20 @@ class BytePhpPreference: PhpPreference {
|
|||||||
// MARK: Save Value
|
// MARK: Save Value
|
||||||
|
|
||||||
private func updatedFieldValue() {
|
private func updatedFieldValue() {
|
||||||
internalValue = "\(value)\(unit.rawValue)"
|
if value == -1 {
|
||||||
|
// In case we're dealing with unlimited value, we don't need a unit
|
||||||
|
internalValue = "-1"
|
||||||
|
} else {
|
||||||
|
// We need to append the unit otherwise
|
||||||
|
internalValue = "\(value)\(unit.rawValue)"
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try PhpPreference.persistToIniFile(key: self.key, value: self.internalValue)
|
||||||
|
Log.info("The preference \(key) was updated to: \(value)")
|
||||||
|
} catch {
|
||||||
|
Log.info("The preference \(key) could not be updated")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func readFrom(internalValue: String) -> (UnitOption, Int)? {
|
public static func readFrom(internalValue: String) -> (UnitOption, Int)? {
|
||||||
|
@ -15,6 +15,14 @@ class PhpPreference {
|
|||||||
init(key: String) {
|
init(key: String) {
|
||||||
self.key = key
|
self.key = key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static func persistToIniFile(key: String, value: String) throws {
|
||||||
|
if let file = PhpEnvironments.shared.getConfigFile(forKey: key) {
|
||||||
|
return try file.replace(key: key, value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw PhpConfigurationFile.ReplacementErrors.missingFile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BoolPhpPreference: PhpPreference {
|
class BoolPhpPreference: PhpPreference {
|
||||||
|
@ -35,12 +35,12 @@ struct PreferenceContainer<ControlView: View>: View {
|
|||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
controlView
|
controlView
|
||||||
|
|
||||||
Text(self.description.localizedForSwiftUI)
|
Text(self.description.localizedForSwiftUI)
|
||||||
|
.lineLimit(nil)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
.foregroundColor(Color.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(5)
|
.padding(5)
|
||||||
@ -51,6 +51,7 @@ struct ByteLimitView: View {
|
|||||||
@State private var unit: BytePhpPreference.UnitOption
|
@State private var unit: BytePhpPreference.UnitOption
|
||||||
@State private var numberText: String
|
@State private var numberText: String
|
||||||
@State private var unlimited: Bool
|
@State private var unlimited: Bool
|
||||||
|
@State private var timer: Timer?
|
||||||
|
|
||||||
private var preference: BytePhpPreference
|
private var preference: BytePhpPreference
|
||||||
|
|
||||||
@ -65,9 +66,11 @@ struct ByteLimitView: View {
|
|||||||
if !unlimited {
|
if !unlimited {
|
||||||
HStack {
|
HStack {
|
||||||
TextField("", text: $numberText)
|
TextField("", text: $numberText)
|
||||||
.onChange(of: numberText) { newText in
|
.onChange(of: numberText) { [weak preference] newText in
|
||||||
self.preference.value = Int(newText) ?? 256
|
timer?.invalidate()
|
||||||
print(self.preference.internalValue)
|
timer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in
|
||||||
|
preference?.value = Int(newText) ?? 256
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Picker("Limit Name", selection: $unit) {
|
Picker("Limit Name", selection: $unit) {
|
||||||
ForEach(BytePhpPreference.UnitOption.allCases, id: \.self) {
|
ForEach(BytePhpPreference.UnitOption.allCases, id: \.self) {
|
||||||
@ -79,25 +82,35 @@ struct ByteLimitView: View {
|
|||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
.onChange(of: unit) { newValue in
|
.onChange(of: unit) { newValue in
|
||||||
self.preference.unit = newValue
|
self.preference.unit = newValue
|
||||||
print(self.preference.internalValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle(isOn: $unlimited) {
|
Toggle(isOn: $unlimited) {
|
||||||
Label("Allow unlimited usage", systemImage: "heart").labelStyle(.titleOnly)
|
Text("confman.byte_limit.unlimited".localizedForSwiftUI)
|
||||||
}
|
}.onChange(of: unlimited, perform: { [weak preference] unlimited in
|
||||||
|
timer?.invalidate()
|
||||||
|
timer = Timer.scheduledTimer(withTimeInterval: 0.8, repeats: false) { _ in
|
||||||
|
preference?.value = unlimited ? -1 : 512
|
||||||
|
preference?.unit = .megabyte
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ByteLimitView_Previews: PreviewProvider {
|
#Preview("Byte Limit View") {
|
||||||
static var previews: some View {
|
PreferenceContainer(
|
||||||
PreferenceContainer(name: "Max Size", description: "Some maximum size") {
|
name: "Max Size",
|
||||||
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
|
description:
|
||||||
}
|
"Here's an extensive description that is obviously way too long but it should wrap." +
|
||||||
|
"The point of the wrapping text is that is allows us to see what's going on with the layout here."
|
||||||
ConfigManagerView()
|
) {
|
||||||
.frame(width: 600, height: .infinity)
|
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
|
||||||
.previewDisplayName("Config Manager")
|
}.frame(width: 600, height: 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview("Config Manager") {
|
||||||
|
ConfigManagerView()
|
||||||
|
.frame(width: 600, height: .infinity)
|
||||||
|
.previewDisplayName("Config Manager")
|
||||||
}
|
}
|
||||||
|
@ -13,14 +13,14 @@ struct ConfigManagerView: View {
|
|||||||
var preferences: [PhpPreference] = [
|
var preferences: [PhpPreference] = [
|
||||||
BytePhpPreference(key: "memory_limit"),
|
BytePhpPreference(key: "memory_limit"),
|
||||||
BytePhpPreference(key: "post_max_size"),
|
BytePhpPreference(key: "post_max_size"),
|
||||||
BoolPhpPreference(key: "file_uploads"),
|
// BoolPhpPreference(key: "file_uploads"),
|
||||||
BytePhpPreference(key: "upload_max_filesize")
|
BytePhpPreference(key: "upload_max_filesize")
|
||||||
]
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack(alignment: .center, spacing: 15) {
|
HStack(alignment: .center, spacing: 15) {
|
||||||
Image(systemName: "square.and.pencil.circle.fill")
|
Image(systemName: "gearshape.fill")
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 40, height: 40)
|
||||||
.foregroundColor(Color.blue)
|
.foregroundColor(Color.blue)
|
||||||
@ -51,6 +51,7 @@ struct ConfigManagerView: View {
|
|||||||
if let preference = preference as? BytePhpPreference {
|
if let preference = preference as? BytePhpPreference {
|
||||||
ByteLimitView(preference: preference)
|
ByteLimitView(preference: preference)
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
if let preference = preference as? BoolPhpPreference {
|
if let preference = preference as? BoolPhpPreference {
|
||||||
Toggle("", isOn: preference.$value)
|
Toggle("", isOn: preference.$value)
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
@ -59,6 +60,7 @@ struct ConfigManagerView: View {
|
|||||||
if let preference = preference as? StringPhpPreference {
|
if let preference = preference as? StringPhpPreference {
|
||||||
TextField("Placeholder", text: preference.$value)
|
TextField("Placeholder", text: preference.$value)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}.frame(maxWidth: .infinity)
|
}.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}.padding(10)
|
}.padding(10)
|
||||||
@ -67,7 +69,7 @@ struct ConfigManagerView: View {
|
|||||||
|
|
||||||
VStack(alignment: .trailing) {
|
VStack(alignment: .trailing) {
|
||||||
Button("Close", action: {
|
Button("Close", action: {
|
||||||
|
App.shared.phpConfigManagerWindowController?.close()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
@ -78,14 +80,10 @@ struct ConfigManagerView: View {
|
|||||||
alignment: .topTrailing
|
alignment: .topTrailing
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}.frame(maxHeight: 485)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ConfigManagerView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
ConfigManagerView().frame(width: 600)
|
||||||
ConfigManagerView()
|
|
||||||
.frame(width: 600)
|
|
||||||
.previewDisplayName("Live Preview")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// ConfigManagerWindowController.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 12/09/2023.
|
||||||
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class PhpConfigManagerWindowController: PMWindowController {
|
||||||
|
|
||||||
|
// MARK: - Window Identifier
|
||||||
|
|
||||||
|
override var windowName: String {
|
||||||
|
return "ConfigManager"
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func create(delegate: NSWindowDelegate?) {
|
||||||
|
let windowController = Self()
|
||||||
|
windowController.window = NSWindow()
|
||||||
|
|
||||||
|
guard let window = windowController.window else { return }
|
||||||
|
window.title = ""
|
||||||
|
window.styleMask = [.titled, .closable, .miniaturizable]
|
||||||
|
window.titlebarAppearsTransparent = true
|
||||||
|
window.delegate = delegate ?? windowController
|
||||||
|
window.contentView = NSHostingView(rootView: ConfigManagerView())
|
||||||
|
window.setContentSize(NSSize(width: 600, height: 480))
|
||||||
|
|
||||||
|
App.shared.phpConfigManagerWindowController = windowController
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||||
|
if App.shared.phpConfigManagerWindowController == nil {
|
||||||
|
Self.create(delegate: delegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
App.shared.phpConfigManagerWindowController?.showWindow(self)
|
||||||
|
App.shared.phpConfigManagerWindowController?.positionWindowInTopRightCorner()
|
||||||
|
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
App.shared.phpConfigManagerWindowController?.window?.orderFrontRegardless()
|
||||||
|
}
|
||||||
|
}
|
@ -34,8 +34,8 @@ class WarningManager: ObservableObject {
|
|||||||
.trimmingCharacters(in: .whitespacesAndNewlines) == "1"
|
.trimmingCharacters(in: .whitespacesAndNewlines) == "1"
|
||||||
},
|
},
|
||||||
name: "Running PHP Monitor with Rosetta on M1",
|
name: "Running PHP Monitor with Rosetta on M1",
|
||||||
title: "warnings.arm_compatibility.title".localized,
|
title: "warnings.arm_compatibility.title",
|
||||||
paragraphs: { return ["warnings.arm_compatibility.description".localized] },
|
paragraphs: { return ["warnings.arm_compatibility.description"] },
|
||||||
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-and-Apple-Silicon"
|
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-and-Apple-Silicon"
|
||||||
),
|
),
|
||||||
Warning(
|
Warning(
|
||||||
@ -44,11 +44,11 @@ class WarningManager: ObservableObject {
|
|||||||
!FileSystem.isWriteableFile("/usr/local/bin/")
|
!FileSystem.isWriteableFile("/usr/local/bin/")
|
||||||
},
|
},
|
||||||
name: "Helpers cannot be symlinked and not in PATH",
|
name: "Helpers cannot be symlinked and not in PATH",
|
||||||
title: "warnings.helper_permissions.title".localized,
|
title: "warnings.helper_permissions.title",
|
||||||
paragraphs: { return [
|
paragraphs: { return [
|
||||||
"warnings.helper_permissions.description".localized,
|
"warnings.helper_permissions.description",
|
||||||
"warnings.helper_permissions.unavailable".localized,
|
"warnings.helper_permissions.unavailable",
|
||||||
"warnings.helper_permissions.symlink".localized
|
"warnings.helper_permissions.symlink"
|
||||||
] },
|
] },
|
||||||
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries"
|
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries"
|
||||||
),
|
),
|
||||||
@ -58,7 +58,7 @@ class WarningManager: ObservableObject {
|
|||||||
return !PhpConfigChecker.shared.missing.isEmpty
|
return !PhpConfigChecker.shared.missing.isEmpty
|
||||||
},
|
},
|
||||||
name: "Your PHP installation is missing configuration files",
|
name: "Your PHP installation is missing configuration files",
|
||||||
title: "warnings.files_missing.title".localized,
|
title: "warnings.files_missing.title",
|
||||||
paragraphs: { return [
|
paragraphs: { return [
|
||||||
"warnings.files_missing.description".localized(
|
"warnings.files_missing.description".localized(
|
||||||
PhpConfigChecker.shared.missing.joined(separator: "\n• ")
|
PhpConfigChecker.shared.missing.joined(separator: "\n• ")
|
||||||
|
@ -25,8 +25,6 @@ struct NoWarningsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NoWarningsView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
NoWarningsView().padding()
|
||||||
NoWarningsView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -89,19 +89,19 @@ struct PhpDoctorView: View {
|
|||||||
}
|
}
|
||||||
.listRowInsets(EdgeInsets())
|
.listRowInsets(EdgeInsets())
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
.frame(minHeight: 350, maxHeight: .infinity, alignment: .top)
|
||||||
}
|
}
|
||||||
|
.frame(width: 600)
|
||||||
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WarningListView_Previews: PreviewProvider {
|
#Preview("Empty List") {
|
||||||
static var previews: some View {
|
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
|
||||||
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
|
.frame(width: 600, height: 480)
|
||||||
.frame(width: 600, height: 480)
|
}
|
||||||
.previewDisplayName("Empty List")
|
|
||||||
|
#Preview("List With All Warnings") {
|
||||||
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
|
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
|
||||||
.frame(width: 600, height: 480)
|
.frame(width: 600, height: 480)
|
||||||
.previewDisplayName("List With All Warnings")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class PhpDoctorWindowController: PMWindowController {
|
|||||||
window.titlebarAppearsTransparent = true
|
window.titlebarAppearsTransparent = true
|
||||||
window.delegate = delegate ?? windowController
|
window.delegate = delegate ?? windowController
|
||||||
window.contentView = NSHostingView(rootView: PhpDoctorView())
|
window.contentView = NSHostingView(rootView: PhpDoctorView())
|
||||||
window.setContentSize(NSSize(width: 600, height: 480))
|
window.setContentSize(window.contentView!.fittingSize)
|
||||||
|
|
||||||
App.shared.phpDoctorWindowController = windowController
|
App.shared.phpDoctorWindowController = windowController
|
||||||
}
|
}
|
||||||
@ -41,5 +41,6 @@ class PhpDoctorWindowController: PMWindowController {
|
|||||||
App.shared.phpDoctorWindowController?.window?.setCenterPosition(offsetY: 70)
|
App.shared.phpDoctorWindowController?.window?.setCenterPosition(offsetY: 70)
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
App.shared.phpDoctorWindowController?.window?.orderFrontRegardless()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user