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"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -112,13 +112,15 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
|
||||
|
||||
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
||||
|
||||
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
|
||||
|
||||
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
||||
|
||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
|
||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.3.
|
||||
|
||||
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
||||
|
||||
|
@ -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 |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 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
|
||||
|
||||
@ -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 |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
|
@ -14,8 +14,6 @@ class Actions {
|
||||
|
||||
public static func linkPhp() async {
|
||||
await brew("link php --overwrite --force")
|
||||
|
||||
// TODO: Verify that this worked, if not, notify the user
|
||||
}
|
||||
|
||||
public static func restartPhpFpm() async {
|
||||
|
@ -19,11 +19,40 @@ struct Constants {
|
||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||
|
||||
/**
|
||||
* The PHP versions that are considered pre-release versions.
|
||||
PHP Monitor supplies a hardcoded list of PHP packages in its own
|
||||
PHP Version Manager.
|
||||
|
||||
This hardcoded list will expire and will need to be modified when
|
||||
the cutoff date occurs, which is when the `php` formula will
|
||||
become PHP 8.4, and a new build will need to be made.
|
||||
|
||||
If users launch an older version of the app, then a warning
|
||||
will be displayed to let them know that certain operations
|
||||
will not work correctly and that they need to update their app.
|
||||
*/
|
||||
static let ExperimentalPhpVersions: Set = [
|
||||
"8.3", "8.4"
|
||||
]
|
||||
static let PhpFormulaeCutoffDate = "2024-11-01"
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -32,8 +61,7 @@ struct Constants {
|
||||
static let DetectedPhpVersions: Set = [
|
||||
"5.6",
|
||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2",
|
||||
"8.3",
|
||||
"8.0", "8.1", "8.2", "8.3",
|
||||
"8.4"
|
||||
]
|
||||
|
||||
@ -91,6 +119,8 @@ struct Constants {
|
||||
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
||||
)!
|
||||
|
||||
// EAP URLs
|
||||
|
||||
static let EarlyAccessCaskFile = URL(
|
||||
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
||||
)!
|
||||
|
@ -17,6 +17,7 @@ public class Paths {
|
||||
|
||||
internal var baseDir: Paths.HomebrewDir
|
||||
private var userName: String
|
||||
private var preferredShell: String
|
||||
|
||||
init() {
|
||||
// Assume the default directory is correct
|
||||
@ -31,9 +32,11 @@ public class Paths {
|
||||
}
|
||||
|
||||
userName = identity()
|
||||
preferredShell = preferred_shell()
|
||||
|
||||
if !isRunningSwiftUIPreview {
|
||||
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"
|
||||
}
|
||||
|
||||
public static var tapPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Library/Taps"
|
||||
}
|
||||
|
||||
public static var caskroomPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||
}
|
||||
|
||||
public static var shell: String {
|
||||
return shared.preferredShell
|
||||
}
|
||||
|
||||
// MARK: - Flexible Binaries
|
||||
// (these can be in multiple locations, so we scan common places because)
|
||||
// (PHP Monitor will not use the user's own PATH)
|
||||
|
@ -15,4 +15,10 @@ extension Date {
|
||||
return dateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
static func fromString(_ string: String) -> Date? {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||
return dateFormatter.date(from: string)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,6 +8,18 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct Localization {
|
||||
static var preferredLanguage: String? {
|
||||
guard let language = Preferences.preferences[.languageOverride] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if language.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
return language
|
||||
}
|
||||
|
||||
static var bundle: Bundle = {
|
||||
if !isRunningTests {
|
||||
return Bundle.main
|
||||
@ -32,7 +44,15 @@ struct Localization {
|
||||
|
||||
extension String {
|
||||
var localized: String {
|
||||
let string = NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
|
||||
var preferredBundle: Bundle = Localization.bundle
|
||||
|
||||
if let preferred = Localization.preferredLanguage,
|
||||
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
|
||||
let bundle = Bundle(path: path) {
|
||||
preferredBundle = bundle
|
||||
}
|
||||
|
||||
let string = NSLocalizedString(self, tableName: nil, bundle: preferredBundle, value: "", comment: "")
|
||||
|
||||
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
||||
if string == self {
|
||||
|
@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
|
||||
// MARK: — FS Attributes
|
||||
|
||||
func makeExecutable(_ path: String) throws {
|
||||
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
}
|
||||
|
||||
// MARK: - Checks
|
||||
|
@ -75,7 +75,7 @@ class MenuBarImageGenerator {
|
||||
|
||||
// Then we'll fetch the image we want on the left
|
||||
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
||||
if iconType == nil {
|
||||
if iconType == nil || !MenuBarIcon.allCases.map({ $0.rawValue }).contains(iconType) {
|
||||
Log.warn("Invalid icon type found, using the default")
|
||||
iconType = MenuBarIcon.iconPhp.rawValue
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
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 window = self.window else { return }
|
||||
|
||||
|
@ -10,7 +10,6 @@ import Foundation
|
||||
|
||||
/**
|
||||
Run a simple blocking Shell command on the user's own system.
|
||||
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
|
||||
*/
|
||||
public func system(_ command: String) -> String {
|
||||
let task = Process()
|
||||
@ -65,3 +64,11 @@ public func identity() -> String {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Load extension information
|
||||
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||
|
||||
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
||||
iniFiles.append(file)
|
||||
}
|
||||
|
||||
// Get configuration values
|
||||
limits = Limits(
|
||||
memory_limit: getByteCount(key: "memory_limit"),
|
||||
@ -76,15 +69,10 @@ class ActivePhpInstallation {
|
||||
post_max_size: getByteCount(key: "post_max_size")
|
||||
)
|
||||
|
||||
// Return a list of .ini files parsed after php.ini
|
||||
let paths = Command.execute(
|
||||
path: Paths.php,
|
||||
arguments: ["-r", "echo php_ini_scanned_files();"],
|
||||
trimNewlines: false
|
||||
)
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.split(separator: ",")
|
||||
.map { String($0) }
|
||||
let paths = ActiveShell.shared
|
||||
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||
.split(separator: "\n")
|
||||
.map { String($0) }
|
||||
|
||||
// See if any extensions are present in said .ini files
|
||||
paths.forEach { (iniFilePath) in
|
||||
|
@ -13,7 +13,7 @@ class PhpEnvironments {
|
||||
// MARK: - Initializer
|
||||
|
||||
/**
|
||||
|
||||
Loads the currently active PHP installation upon startup. May be empty.
|
||||
*/
|
||||
init() {
|
||||
self.currentInstall = ActivePhpInstallation.load()
|
||||
@ -29,7 +29,7 @@ class PhpEnvironments {
|
||||
/**
|
||||
Determine which PHP version the `php` formula is aliased to.
|
||||
*/
|
||||
func determinePhpAlias() async {
|
||||
@MainActor func determinePhpAlias() async {
|
||||
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
|
||||
|
||||
self.homebrewPackage = try! JSONDecoder().decode(
|
||||
@ -37,7 +37,27 @@ class PhpEnvironments {
|
||||
from: brewPhpAlias.data(using: .utf8)!
|
||||
).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
|
||||
@ -49,12 +69,10 @@ class PhpEnvironments {
|
||||
static let shared = PhpEnvironments()
|
||||
|
||||
/** Whether the switcher is busy performing any actions. */
|
||||
var isBusy: Bool = false {
|
||||
@MainActor var isBusy: Bool = false {
|
||||
didSet {
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.setBusyImage()
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +86,14 @@ class PhpEnvironments {
|
||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||
|
||||
/** Information about the currently linked PHP installation. */
|
||||
var currentInstall: ActivePhpInstallation?
|
||||
var currentInstall: ActivePhpInstallation? {
|
||||
didSet {
|
||||
// Let the PHP extension manager, if it exists, know the version changed
|
||||
if let version = currentInstall?.version.short {
|
||||
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The version that the `php` formula via Brew is aliased to on the current system.
|
||||
@ -79,7 +104,12 @@ class PhpEnvironments {
|
||||
|
||||
As such, we take that information from Homebrew.
|
||||
*/
|
||||
static var brewPhpAlias: String {
|
||||
static var brewPhpAlias: String = ""
|
||||
|
||||
/**
|
||||
It's possible for the alias to be newer than the actual installed version of PHP.
|
||||
*/
|
||||
static var homebrewBrewPhpAlias: String {
|
||||
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
|
||||
|
||||
return PhpEnvironments.shared.homebrewPackage.version
|
||||
@ -146,7 +176,12 @@ class PhpEnvironments {
|
||||
|
||||
// Avoid inserting a duplicate
|
||||
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||
supportedVersions.insert(phpAlias)
|
||||
let phpAliasInstall = PhpInstallation(phpAlias)
|
||||
// Before inserting, ensure that the actual output matches the alias
|
||||
// if that isn't the case, our formula remains out-of-date
|
||||
if !phpAliasInstall.isMissingBinary {
|
||||
supportedVersions.insert(phpAlias)
|
||||
}
|
||||
}
|
||||
|
||||
availablePhpVersions = Array(supportedVersions)
|
||||
|
@ -49,8 +49,10 @@ class PhpHelper {
|
||||
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
|
||||
.resolvingSymlinksInPath().path
|
||||
|
||||
// The contents of the script!
|
||||
let script = script(path, keyPhrase, version, dotless)
|
||||
// Check if the user uses Fish
|
||||
let script = Paths.shell.contains("/fish")
|
||||
? fishScript(path, keyPhrase, version, dotless)
|
||||
: zshScript(path, keyPhrase, version, dotless)
|
||||
|
||||
Task { @MainActor in
|
||||
try FileSystem.writeAtomicallyToFile(destination, content: script)
|
||||
@ -78,7 +80,7 @@ class PhpHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static func script(
|
||||
private static func zshScript(
|
||||
_ path: String,
|
||||
_ keyPhrase: 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 {
|
||||
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
||||
let destination = "/usr/local/bin/pm\(dotless)"
|
||||
|
@ -69,8 +69,9 @@ class PhpConfigurationFile: CreatedFromFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
enum ReplacementErrors: Error {
|
||||
public enum ReplacementErrors: Error {
|
||||
case missingKey
|
||||
case missingFile
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,10 +96,16 @@ class PhpConfigurationFile: CreatedFromFile {
|
||||
// Replace the specific line
|
||||
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
|
||||
try self.lines.joined(separator: "\n")
|
||||
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
|
||||
|
||||
// Ensure watcher behaviour is reverted
|
||||
ConfigWatchManager.ignoresModificationsToConfigValues = false
|
||||
|
||||
// Reload the original file
|
||||
self.reload()
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class PhpExtension {
|
||||
|
||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||
|
||||
self.enabled = !line.contains(";")
|
||||
self.enabled = !line.starts(with: ";")
|
||||
self.file = file
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ class PhpExtension {
|
||||
You may need to restart the other services in order for this change to apply.
|
||||
*/
|
||||
func toggle() async {
|
||||
let newLine = enabled
|
||||
let newLine = !line.starts(with: ";")
|
||||
// DISABLED: Commented out line
|
||||
? "; \(line)"
|
||||
// ENABLED: Line where the comment delimiter (;) is removed
|
||||
@ -84,14 +84,14 @@ class PhpExtension {
|
||||
|
||||
await sed(file: file, original: line, replacement: newLine)
|
||||
|
||||
enabled.toggle()
|
||||
self.enabled = !newLine.starts(with: ";")
|
||||
self.line = newLine
|
||||
|
||||
if !isRunningTests {
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Static Methods
|
||||
|
@ -12,19 +12,36 @@ class PhpInstallation {
|
||||
|
||||
var versionNumber: VersionNumber
|
||||
|
||||
var iniFiles: [PhpConfigurationFile] = []
|
||||
|
||||
var isMissingBinary: Bool = false
|
||||
|
||||
var isHealthy: Bool = true
|
||||
|
||||
var extensions: [PhpExtension] {
|
||||
return self.iniFiles.flatMap({ $0.extensions })
|
||||
}
|
||||
|
||||
/**
|
||||
In order to determine details about a PHP installation,
|
||||
we’ll simply run `php-config --version` in the relevant directory.
|
||||
*/
|
||||
init(_ version: String) {
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
|
||||
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||
|
||||
let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||
versionNumber = VersionNumber.make(from: version)!
|
||||
|
||||
self.versionNumber = VersionNumber.make(from: version)!
|
||||
determineVersion(phpConfigExecutablePath, phpExecutablePath)
|
||||
determineHealth(phpExecutablePath)
|
||||
determineIniFiles(phpExecutablePath)
|
||||
|
||||
// Find all enabled extensions
|
||||
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
|
||||
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
|
||||
}
|
||||
|
||||
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
|
||||
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||
let longVersionString = Command.execute(
|
||||
path: phpConfigExecutablePath,
|
||||
@ -34,9 +51,15 @@ class PhpInstallation {
|
||||
|
||||
// The parser should always work, or the string has to be very unusual.
|
||||
// If so, the app SHOULD crash, so that the users report what's up.
|
||||
self.versionNumber = try! VersionNumber.parse(longVersionString)
|
||||
versionNumber = try! VersionNumber.parse(longVersionString)
|
||||
} else {
|
||||
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
|
||||
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
|
||||
isMissingBinary = true
|
||||
}
|
||||
}
|
||||
|
||||
private func determineHealth(_ phpExecutablePath: String) {
|
||||
if FileSystem.fileExists(phpExecutablePath) {
|
||||
let testCommand = Command.execute(
|
||||
path: phpExecutablePath,
|
||||
@ -53,4 +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)
|
||||
}
|
||||
|
||||
// MARK: - PHP FPM pool
|
||||
// MARK: - Corrections
|
||||
|
||||
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||
@ -54,37 +54,7 @@ extension InternalSwitcher {
|
||||
return false
|
||||
}
|
||||
|
||||
func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||
return [
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/php-fpm.d/valet-fpm.conf",
|
||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||
],
|
||||
applies: { Valet.shared.version!.major > 2 }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/error_log.ini",
|
||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||
],
|
||||
applies: { return true }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/php-memory-limits.ini",
|
||||
source: "/cli/stubs/php-memory-limits.ini",
|
||||
replacements: [:],
|
||||
applies: { return true }
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||
public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||
let files = self.getExpectedConfigurationFiles(for: version)
|
||||
|
||||
// For each of the files, attempt to fix anything that is wrong
|
||||
@ -124,6 +94,38 @@ extension InternalSwitcher {
|
||||
return outcomes.contains(true)
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
private func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||
return [
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/php-fpm.d/valet-fpm.conf",
|
||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||
],
|
||||
applies: { Valet.shared.version!.major > 2 }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/error_log.ini",
|
||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||
],
|
||||
applies: { return true }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/php-memory-limits.ini",
|
||||
source: "/cli/stubs/php-memory-limits.ini",
|
||||
replacements: [:],
|
||||
applies: { return true }
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct ExpectedConfigurationFile {
|
||||
|
@ -86,14 +86,37 @@ class RealShell: ShellProtocol {
|
||||
|
||||
// MARK: - Shellable Protocol
|
||||
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
let task = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
|
||||
if Log.shared.verbosity == .cli {
|
||||
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
}
|
||||
|
||||
func pipe(_ command: String) async -> ShellOutput {
|
||||
let task = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
// Seriously slow down how long it takes for the shell to return output
|
||||
// (in order to debug or identify async issues)
|
||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||
Log.info("[SLOW SHELL] \(command)")
|
||||
await delay(seconds: 3.0)
|
||||
@ -104,20 +127,20 @@ class RealShell: ShellProtocol {
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
let stdOut = String(
|
||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!
|
||||
|
||||
let stdErr = String(
|
||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!
|
||||
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
|
||||
if Log.shared.verbosity == .cli {
|
||||
var args = task.arguments ?? []
|
||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||
var log = """
|
||||
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
}
|
||||
|
||||
private func log(task: Process, stdOut: String, stdErr: String) {
|
||||
var args = task.arguments ?? []
|
||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||
var log = """
|
||||
|
||||
<~~~~~~~~~~~~~~~~~~~~~~~
|
||||
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
||||
@ -126,22 +149,19 @@ class RealShell: ShellProtocol {
|
||||
\(stdOut)
|
||||
"""
|
||||
|
||||
if !stdErr.isEmpty {
|
||||
log.append("""
|
||||
if !stdErr.isEmpty {
|
||||
log.append("""
|
||||
[ERR]:
|
||||
\(stdErr)
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
log.append("""
|
||||
log.append("""
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~>
|
||||
|
||||
""")
|
||||
|
||||
Log.info(log)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
Log.info(log)
|
||||
}
|
||||
|
||||
func quiet(_ command: String) async {
|
||||
|
@ -14,6 +14,16 @@ protocol ShellProtocol {
|
||||
*/
|
||||
var PATH: String { get }
|
||||
|
||||
/**
|
||||
Run a command synchronously. Use with caution.
|
||||
|
||||
Common usage:
|
||||
```
|
||||
let output = Shell.sync("php -v")
|
||||
```
|
||||
*/
|
||||
func sync(_ command: String) -> ShellOutput
|
||||
|
||||
/**
|
||||
Run a command asynchronously.
|
||||
Returns the most relevant output (prefers error output if it exists).
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// PhpFormulaeStatus.swift
|
||||
// BusyStatus.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 02/05/2023.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpFormulaeStatus: ObservableObject {
|
||||
class BusyStatus: ObservableObject {
|
||||
@Published var busy: Bool
|
||||
@Published var title: String
|
||||
@Published var description: String
|
||||
@ -18,4 +18,12 @@ class PhpFormulaeStatus: ObservableObject {
|
||||
self.title = title
|
||||
self.description = description
|
||||
}
|
||||
|
||||
public static func notBusy() -> BusyStatus {
|
||||
return BusyStatus(busy: false, title: "", description: "")
|
||||
}
|
||||
|
||||
public static func busy() -> BusyStatus {
|
||||
return BusyStatus(busy: false, title: "", description: "")
|
||||
}
|
||||
}
|
@ -43,6 +43,7 @@ public struct TestableConfiguration: Codable {
|
||||
private var primaryPhpVersion: VersionNumber?
|
||||
private var secondaryPhpVersions: [VersionNumber] = []
|
||||
|
||||
// swiftlint:disable function_body_length
|
||||
mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) {
|
||||
if primary {
|
||||
if primaryPhpVersion != nil {
|
||||
@ -72,12 +73,26 @@ public struct TestableConfiguration: Codable {
|
||||
: .fake(.text)
|
||||
]) { (_, new) in new }
|
||||
|
||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"]
|
||||
= version.long
|
||||
// PHP configuration files
|
||||
self.shellOutput["/opt/homebrew/opt/php@\(version.short)/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||
|
||||
// PHP Homebrew operations
|
||||
self.shellOutput["/opt/homebrew/bin/brew unlink php@\(version.short)"] = .delayed(0.2, "OK")
|
||||
self.shellOutput["sudo /opt/homebrew/bin/brew services stop php@\(version.short)"] = .delayed(0.2, "OK")
|
||||
self.shellOutput["sudo /opt/homebrew/bin/brew services start php@\(version.short)"] = .delayed(0.2, "OK")
|
||||
self.shellOutput["/opt/homebrew/bin/brew link php@\(version.short) --overwrite --force"] = .delayed(0.2, "OK")
|
||||
|
||||
// PHP version output
|
||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"] = version.long
|
||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php -v"] = "OK"
|
||||
|
||||
if primary {
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php"]
|
||||
= .instant("php")
|
||||
// Files expected to be present for currently linked PHP version
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php"] =
|
||||
.instant("php")
|
||||
self.shellOutput["/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||
self.filesystem["/opt/homebrew/opt/php"]
|
||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
||||
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
||||
@ -88,12 +103,8 @@ public struct TestableConfiguration: Codable {
|
||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
|
||||
self.commandOutput["/opt/homebrew/bin/php-config --version"]
|
||||
= version.long
|
||||
self.commandOutput["/opt/homebrew/bin/php -r echo php_ini_scanned_files();"] =
|
||||
"""
|
||||
/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini,
|
||||
"""
|
||||
} else {
|
||||
|
||||
// Output expected to be present for non-linked PHP versions
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
||||
BatchFakeShellOutput.instant(
|
||||
self.secondaryPhpVersions
|
||||
@ -102,6 +113,7 @@ public struct TestableConfiguration: Codable {
|
||||
)
|
||||
}
|
||||
}
|
||||
// swiftlint:enable function_body_length
|
||||
|
||||
// MARK: Interactions
|
||||
|
||||
|
@ -19,6 +19,17 @@ public class TestableShell: ShellProtocol {
|
||||
|
||||
var expectations: [String: BatchFakeShellOutput] = [:]
|
||||
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
// This assertion will only fire during test builds
|
||||
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
|
||||
|
||||
guard let expectation = expectations[command] else {
|
||||
return .err("No Expected Output")
|
||||
}
|
||||
|
||||
return expectation.syncOutput()
|
||||
}
|
||||
|
||||
func quiet(_ command: String) async {
|
||||
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
||||
}
|
||||
@ -112,6 +123,29 @@ struct BatchFakeShellOutput: Codable {
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
Outputs the fake shell output as expected, but does this synchronously.
|
||||
*/
|
||||
public func syncOutput(
|
||||
ignoreDelay: Bool = false
|
||||
) -> ShellOutput {
|
||||
let output = ShellOutput.empty()
|
||||
|
||||
for item in items {
|
||||
if !ignoreDelay {
|
||||
Thread.sleep(forTimeInterval: item.delay)
|
||||
}
|
||||
|
||||
if item.stream == .stdErr {
|
||||
output.err += item.output
|
||||
} else if item.stream == .stdOut {
|
||||
output.out += item.output
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
||||
*/
|
||||
|
@ -46,8 +46,10 @@ extension App {
|
||||
}
|
||||
|
||||
hotkey.keyDownHandler = {
|
||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,21 +74,24 @@ class App {
|
||||
/** The window controller of the onboarding window. */
|
||||
var onboardingWindowController: OnboardingWindowController?
|
||||
|
||||
/** The window controller of the config manager window. */
|
||||
var phpConfigManagerWindowController: PhpConfigManagerWindowController?
|
||||
|
||||
/** The window controller of the warnings window. */
|
||||
var phpDoctorWindowController: PhpDoctorWindowController?
|
||||
|
||||
/** The window controller of the warnings window. */
|
||||
/** The window controller of the PHP version manager window. */
|
||||
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
||||
|
||||
/** The window controller of the PHP extension manager window. */
|
||||
var phpExtensionManagerWindowController: PhpExtensionManagerWindowController?
|
||||
|
||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||
var detectedApplications: [Application] = []
|
||||
|
||||
/** The warning manager, responsible for keeping track of warnings. */
|
||||
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. */
|
||||
var timer: Timer?
|
||||
|
||||
@ -117,8 +120,12 @@ class App {
|
||||
|
||||
// MARK: - App Watchers
|
||||
|
||||
/** Individual filesystem watchers, which are, i.e. responsible for watching the Homebrew folders. */
|
||||
var watchers: [String: FSNotifier] = [:]
|
||||
|
||||
/**
|
||||
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
||||
The `ConfigWatchManager` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
||||
This manager object can immediately start or stop all watchers (or pause them) all at once.
|
||||
*/
|
||||
var watcher: PhpConfigWatcher!
|
||||
var watchManager: ConfigWatchManager!
|
||||
}
|
||||
|
@ -109,6 +109,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
|
||||
static func initializeTestingProfile(_ path: String) {
|
||||
Log.info("The configuration with path `\(path)` is being requested...")
|
||||
// Clear for PHP Guard
|
||||
Stats.clearCurrentGlobalPhpVersion()
|
||||
// Load the configuration file
|
||||
TestableConfiguration.loadFrom(path: path).apply()
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,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>
|
||||
<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="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
||||
@ -429,7 +429,7 @@
|
||||
</toolbarItem>
|
||||
<searchToolbarItem implicitItemIdentifier="7C834FBE-7118-4082-A09F-7CBECEC1356A" label="Search" paletteLabel="Search" visibilityPriority="1001" id="G2g-jS-RVc">
|
||||
<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"/>
|
||||
<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">
|
||||
@ -521,9 +521,6 @@
|
||||
<subviews>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8zu-cF-KCX">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -531,15 +528,15 @@
|
||||
DQ
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="4Uf-fh-jWJ"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="primaryButtonAction:" target="hkw-9V-NxP" id="W7d-3b-pZT"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TCp-nS-HN2">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -547,6 +544,9 @@ DQ
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="QWZ-BA-0g9"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="secondaryButtonAction:" target="hkw-9V-NxP" id="YJs-Hu-lFP"/>
|
||||
</connections>
|
||||
@ -575,7 +575,7 @@ Gw
|
||||
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="wIl-uw-y3p"/>
|
||||
</constraints>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" selectable="YES" title="This is a slightly more expanded explanation." id="rY3-Nd-Iit">
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -610,7 +610,7 @@ Gw
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="7eT-Hw-EL9"/>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" selectable="YES" id="7iW-Lc-DqO">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -685,9 +685,6 @@ DQ
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -695,11 +692,14 @@ DQ
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<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"/>
|
||||
@ -710,7 +710,7 @@ Gw
|
||||
<outlet property="delegate" destination="glS-wF-sEU" id="Dyf-0M-Gwj"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -728,7 +728,7 @@ Gw
|
||||
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<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"/>
|
||||
@ -743,7 +743,7 @@ Gw
|
||||
<url key="url" string="file:///Users/"/>
|
||||
</pathCell>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
|
||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||
@ -751,7 +751,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -886,7 +886,7 @@ Gw
|
||||
<rect key="frame" x="69" y="0.0" width="200" height="54"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<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"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
|
||||
<font key="font" metaFont="systemSemibold" size="13"/>
|
||||
@ -894,7 +894,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -937,13 +937,13 @@ Gw
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="smallSystemBold"/>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="70" id="MBa-bB-DTB"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="pressedPhpVersion:" target="T49-0U-d58" id="jVO-TS-F6d"/>
|
||||
</connections>
|
||||
@ -987,11 +987,11 @@ Gw
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<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"/>
|
||||
<subviews>
|
||||
<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>
|
||||
<constraint firstAttribute="width" constant="18" id="XcB-uw-szU"/>
|
||||
<constraint firstAttribute="height" constant="18" id="bGN-Vh-Sh0"/>
|
||||
@ -1024,10 +1024,10 @@ Gw
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<textFieldCell key="cell" alignment="left" title="Laravel" id="0lu-L6-oKr">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -1035,7 +1035,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" alignment="left" title="PHP 8.0" id="puf-Jh-ham">
|
||||
<font key="font" metaFont="miniSystem"/>
|
||||
@ -1125,7 +1125,7 @@ Gw
|
||||
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
@ -1136,7 +1136,7 @@ Gw
|
||||
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<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"/>
|
||||
@ -1144,7 +1144,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
@ -1152,7 +1152,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<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"/>
|
||||
@ -1194,9 +1194,6 @@ DQ
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -1204,11 +1201,14 @@ DQ
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="uCc-fF-wS2"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<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"/>
|
||||
@ -1237,7 +1237,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
|
||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||
@ -1245,7 +1245,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -1340,9 +1340,6 @@ Gw
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
|
||||
<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">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -1350,6 +1347,9 @@ Gw
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="Zhu-D8-cLK"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="pressedCancel:" target="gOD-Gu-zDG" id="wMp-sM-0A4"/>
|
||||
</connections>
|
||||
@ -1389,7 +1389,7 @@ Gw
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</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"/>
|
||||
<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"/>
|
||||
@ -1397,7 +1397,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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
|
||||
.contains("Library not loaded")
|
||||
},
|
||||
name: "`no dyld issue detected",
|
||||
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||
titleText: "startup.errors.dyld_library.title".localized,
|
||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||
Paths.optPath
|
||||
@ -241,6 +241,20 @@ class Startup {
|
||||
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)
|
||||
// =================================================================================
|
||||
EnvironmentCheck(
|
||||
|
@ -62,12 +62,13 @@ struct ComposerJson: Decodable {
|
||||
public func getNotableDependencies() -> [String: String] {
|
||||
var notable: [String: String] = [:]
|
||||
|
||||
var scan = Array(PhpFrameworks.DependencyList.keys)
|
||||
scan.append("php")
|
||||
let scan = Array(ProjectTypeDetection.CommonDependencyList.keys) +
|
||||
Array(ProjectTypeDetection.SpecificDependencyList.keys) +
|
||||
["php"]
|
||||
|
||||
scan.forEach { dependency in
|
||||
if dependencies?[dependency] != nil {
|
||||
notable[dependency] = dependencies![dependency]
|
||||
if let resolvedDependency = dependencies?[dependency] {
|
||||
notable[dependency] = resolvedDependency
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,8 +28,6 @@ import Foundation
|
||||
}
|
||||
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
MainMenu.shared.setBusyImage()
|
||||
MainMenu.shared.rebuild()
|
||||
|
||||
window = TerminalProgressWindowController.display(
|
||||
title: "alert.composer_progress.title".localized,
|
||||
@ -106,14 +104,11 @@ import Foundation
|
||||
|
||||
private func removeBusyStatus() {
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
Task { @MainActor in
|
||||
MainMenu.shared.updatePhpVersionInStatusBar()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Alert
|
||||
|
||||
@MainActor private func presentMissingAlert() {
|
||||
private func presentMissingAlert() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.composer_missing.title".localized,
|
||||
|
@ -8,20 +8,20 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PhpFrameworks {
|
||||
|
||||
struct ProjectTypeDetection {
|
||||
/**
|
||||
This list should probably be reversed when checked, because some of these
|
||||
will also require either `laravel/framework` or `symfony/symfony`.
|
||||
This list is only checked if the specific dependency list doesn't report a match.
|
||||
*/
|
||||
public static let DependencyList = [
|
||||
|
||||
// COMMON FRAMEWORKS
|
||||
public static let CommonDependencyList = [
|
||||
"laravel/framework": "Laravel",
|
||||
"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",
|
||||
"cakephp/app": "CakePHP",
|
||||
"craftcms/craft": "Craft",
|
||||
@ -37,30 +37,8 @@ struct PhpFrameworks {
|
||||
"johnpbloch/wordpress-core": "WordPress",
|
||||
"zendframework/zendframework": "Zend",
|
||||
"zendframework/zend-mvc": "Zend",
|
||||
"typo3/cms-core": "Typo3"
|
||||
// "magento/*": "Magento",
|
||||
// "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"
|
||||
]
|
||||
"typo3/cms-core": "Typo3",
|
||||
"slim/slim": "Slim"
|
||||
]
|
||||
|
||||
/**
|
||||
@ -82,4 +60,25 @@ struct PhpFrameworks {
|
||||
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
|
||||
|
||||
class BrewFormulaeObservable: ObservableObject {
|
||||
@Published var phpVersions: [BrewFormula] = []
|
||||
|
||||
var upgradeable: [BrewFormula] {
|
||||
return phpVersions.filter { formula in
|
||||
formula.hasUpgrade
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Brew {
|
||||
static let shared = Brew()
|
||||
|
||||
@ -45,10 +35,11 @@ class Brew {
|
||||
|
||||
/// Each formula for each PHP version that can be installed.
|
||||
public static let phpVersionFormulae = [
|
||||
"8.3": "shivammathur/php/php@8.3",
|
||||
"8.4": "shivammathur/php/php@8.4",
|
||||
"8.3": "php@8.3",
|
||||
"8.2": "php@8.2",
|
||||
"8.1": "php@8.1",
|
||||
"8.0": "php@8.0",
|
||||
"8.0": "shivammathur/php/php@8.0",
|
||||
"7.4": "shivammathur/php/php@7.4",
|
||||
"7.3": "shivammathur/php/php@7.3",
|
||||
"7.2": "shivammathur/php/php@7.2",
|
||||
|
@ -27,6 +27,21 @@ class BrewDiagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Logs a bunch of useful information during startup.
|
||||
*/
|
||||
public static func logBootInformation() {
|
||||
Log.info(BrewDiagnostics.customCaskInstalled
|
||||
? "[BREW] The app has been installed via Homebrew Cask."
|
||||
: "[BREW] The app has been installed directly (optimal)."
|
||||
)
|
||||
|
||||
Log.info(BrewDiagnostics.usesNginxFullFormula
|
||||
? "[BREW] The app will be using the `nginx-full` formula."
|
||||
: "[BREW] The app will be using the `nginx` formula."
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Determines whether the PHP Monitor Cask is installed.
|
||||
*/
|
||||
@ -46,6 +61,43 @@ class BrewDiagnostics {
|
||||
return destination.contains("/nginx-full/")
|
||||
}()
|
||||
|
||||
/**
|
||||
It is possible to have outdated symlinks for PHP installations. This can mean that certain PHP installations
|
||||
are going to be reported incorrectly (e.g. `php@8.2` links to an installation in a `8.3` folder after an upgrade).
|
||||
|
||||
To ensure this does not cause issues, PHP Monitor will automatically remove all incorrect PHP symlinks.
|
||||
*/
|
||||
public static func checkForOutdatedPhpInstallationSymlinks() async {
|
||||
// Set up a regular expression
|
||||
let regex = try! NSRegularExpression(pattern: "^php@[0-9]+\\.[0-9]+$", options: .caseInsensitive)
|
||||
|
||||
// Check for incorrect versions
|
||||
if let contents = try? FileSystem.getShallowContentsOfDirectory("\(Paths.optPath)")
|
||||
.filter({
|
||||
let range = NSRange($0.startIndex..., in: $0)
|
||||
return regex.firstMatch(in: $0, options: [], range: range) != nil
|
||||
}) {
|
||||
|
||||
for symlink in contents {
|
||||
let version = symlink.replacingOccurrences(of: "php@", with: "")
|
||||
if let destination = try? FileSystem.getDestinationOfSymlink("\(Paths.optPath)/\(symlink)") {
|
||||
if !destination.contains("Cellar/php/\(version)")
|
||||
&& !destination.contains("Cellar/php@\(version)") {
|
||||
Log.err("Symlink for \(symlink) is incorrect. Removing...")
|
||||
do {
|
||||
try FileSystem.remove("\(Paths.optPath)/\(symlink)")
|
||||
Log.info("Incorrect symlink for \(symlink) has been successfully removed.")
|
||||
} catch {
|
||||
Log.err("Symlink for \(symlink) was incorrect but could not be removed!")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.warn("Could not read symlink at: \(Paths.optPath)/\(symlink)! Symlink check skipped.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||
|
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
98
phpmon/Domain/Integrations/Homebrew/BrewPhpExtension.swift
Normal file
@ -0,0 +1,98 @@
|
||||
//
|
||||
// BrewPhpExtension.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 27/11/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct BrewPhpExtension: Hashable, Comparable {
|
||||
let name: String
|
||||
let phpVersion: String
|
||||
let isInstalled: Bool
|
||||
let path: String
|
||||
let dependencies: [String]
|
||||
|
||||
var extensionDependencies: [String] {
|
||||
return dependencies
|
||||
.filter {
|
||||
$0.contains("shivammathur/extensions/") && $0.contains("@\(phpVersion)")
|
||||
}
|
||||
.map {
|
||||
$0.replacingOccurrences(of: "shivammathur/extensions/", with: "")
|
||||
.replacingOccurrences(of: "@\(phpVersion)", with: "")
|
||||
}
|
||||
}
|
||||
|
||||
var formulaName: String {
|
||||
return "\(name)@\(phpVersion)"
|
||||
}
|
||||
|
||||
init(path: String, name: String, phpVersion: String) {
|
||||
self.path = path
|
||||
self.name = name
|
||||
self.phpVersion = phpVersion
|
||||
|
||||
self.isInstalled = BrewPhpExtension.hasInstallationReceipt(
|
||||
for: "\(name)@\(phpVersion)"
|
||||
)
|
||||
|
||||
self.dependencies = BrewPhpExtension.extractDependencies(from: path)
|
||||
}
|
||||
|
||||
var hasAlternativeInstall: Bool {
|
||||
guard let php = PhpEnvironments.shared.cachedPhpInstallations[self.phpVersion] else {
|
||||
return false
|
||||
}
|
||||
|
||||
let alreadyDiscovered = php.extensions.contains(where: { $0.name == self.name })
|
||||
|
||||
return alreadyDiscovered && !isInstalled
|
||||
}
|
||||
|
||||
internal func firstDependent(in exts: [BrewPhpExtension]) -> BrewPhpExtension? {
|
||||
return exts
|
||||
.filter({ $0.isInstalled })
|
||||
.first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") }
|
||||
}
|
||||
|
||||
static func hasInstallationReceipt(for formulaName: String) -> Bool {
|
||||
return FileSystem.fileExists("\(Paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
|
||||
}
|
||||
|
||||
static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||
return lhs.name < rhs.name
|
||||
}
|
||||
|
||||
static func == (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
|
||||
return lhs.name == rhs.name
|
||||
}
|
||||
|
||||
private static func extractDependencies(from path: String) -> [String] {
|
||||
let regexPattern = #"depends_on "(.*)""#
|
||||
var dependencies: [String] = []
|
||||
|
||||
guard let content = try? FileSystem.getStringFromFile(path) else {
|
||||
return []
|
||||
}
|
||||
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: regexPattern, options: [])
|
||||
let range = NSRange(content.startIndex..<content.endIndex, in: content)
|
||||
let matches = regex.matches(in: content, options: [], range: range)
|
||||
|
||||
for match in matches {
|
||||
if let range = Range(match.range(at: 1), in: content) {
|
||||
let dependencyName = String(content[range])
|
||||
dependencies.append(dependencyName)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// BrewFormula.swift
|
||||
// BrewPhpFormula.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 17/03/2023.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct BrewFormula {
|
||||
struct BrewPhpFormula: Equatable {
|
||||
/// Name of the formula.
|
||||
let name: String
|
||||
|
||||
@ -21,6 +21,8 @@ struct BrewFormula {
|
||||
/// The upgrade that is currently available, if it exists.
|
||||
let upgradeVersion: String?
|
||||
|
||||
// TODO: A rebuild attribute could be checked, to check if a Tap update exists for a pre-release version
|
||||
|
||||
/// Whether this formula is a stable version of PHP.
|
||||
let prerelease: Bool
|
||||
|
||||
@ -48,6 +50,25 @@ struct BrewFormula {
|
||||
return upgradeVersion != nil
|
||||
}
|
||||
|
||||
/// Whether this formula alias is different.
|
||||
var hasUpgradedFormulaAlias: Bool {
|
||||
return self.shortVersion == PhpEnvironments.homebrewBrewPhpAlias
|
||||
&& PhpEnvironments.homebrewBrewPhpAlias != PhpEnvironments.brewPhpAlias
|
||||
}
|
||||
|
||||
var unavailableAfterUpgrade: Bool {
|
||||
if installedVersion == nil || upgradeVersion == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if let installed = try? VersionNumber.parse(self.installedVersion!),
|
||||
let upgrade = try? VersionNumber.parse(self.upgradeVersion!) {
|
||||
return upgrade.short != installed.short
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/// The associated Homebrew folder with this PHP formula.
|
||||
var homebrewFolder: String {
|
||||
let resolved = name
|
||||
@ -60,7 +81,7 @@ struct BrewFormula {
|
||||
/// The short version associated with this formula, if installed.
|
||||
var shortVersion: String? {
|
||||
guard let version = self.installedVersion else {
|
||||
return nil
|
||||
return self.displayName.replacingOccurrences(of: "PHP ", with: "")
|
||||
}
|
||||
|
||||
return VersionNumber.make(from: version)?.short ?? nil
|
||||
@ -81,6 +102,7 @@ struct BrewFormula {
|
||||
return nil
|
||||
}
|
||||
|
||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?.isHealthy ?? nil
|
||||
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?
|
||||
.isHealthy ?? nil
|
||||
}
|
||||
}
|
@ -8,22 +8,23 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol HandlesBrewFormulae {
|
||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula]
|
||||
protocol HandlesBrewPhpFormulae {
|
||||
func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula]
|
||||
func refreshPhpVersions(loadOutdated: Bool) async
|
||||
}
|
||||
|
||||
extension HandlesBrewFormulae {
|
||||
extension HandlesBrewPhpFormulae {
|
||||
public func refreshPhpVersions(loadOutdated: Bool) async {
|
||||
let items = await loadPhpVersions(loadOutdated: loadOutdated)
|
||||
Task { @MainActor in
|
||||
await PhpEnvironments.shared.determinePhpAlias()
|
||||
Brew.shared.formulae.phpVersions = items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BrewFormulaeHandler: HandlesBrewFormulae {
|
||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] {
|
||||
class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
|
||||
var outdated: [OutdatedFormula]?
|
||||
|
||||
if loadOutdated {
|
||||
@ -43,7 +44,8 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
@ -53,7 +55,7 @@ class BrewFormulaeHandler: HandlesBrewFormulae {
|
||||
})?.current_version
|
||||
}
|
||||
|
||||
return BrewFormula(
|
||||
return BrewPhpFormula(
|
||||
name: formula,
|
||||
displayName: "PHP \(version)",
|
||||
installedVersion: fullVersion,
|
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
53
phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// BrewTapFormulae.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 01/11/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class BrewTapFormulae {
|
||||
public static func from(tap: String) -> [String: [BrewPhpExtension]] {
|
||||
let directory = "\(Paths.tapPath)/\(tap)/Formula"
|
||||
|
||||
let files = try? FileSystem.getShallowContentsOfDirectory(directory)
|
||||
|
||||
var availableExtensions = [String: [BrewPhpExtension]]()
|
||||
|
||||
guard let files = files else {
|
||||
return availableExtensions
|
||||
}
|
||||
|
||||
let regex = try! NSRegularExpression(pattern: "(\\w+)@(\\d+\\.\\d+)\\.rb")
|
||||
|
||||
for file in files {
|
||||
let matches = regex.matches(in: file, range: NSRange(file.startIndex..., in: file))
|
||||
if let match = matches.first {
|
||||
if let phpExtensionRange = Range(match.range(at: 1), in: file),
|
||||
let versionRange = Range(match.range(at: 2), in: file) {
|
||||
// Determine what the extension's name is
|
||||
let phpExtensionName = String(file[phpExtensionRange])
|
||||
|
||||
// Determine what PHP version this is for
|
||||
let phpVersion = String(file[versionRange])
|
||||
|
||||
// Create a new BrewPhpExtension object (determines if installed)
|
||||
let phpExtension = BrewPhpExtension(
|
||||
path: "\(Paths.tapPath)/\(tap)/Formula/\(file)",
|
||||
name: phpExtensionName,
|
||||
phpVersion: phpVersion
|
||||
)
|
||||
|
||||
// Append the extension to the list
|
||||
var extensions = availableExtensions[phpVersion, default: []]
|
||||
extensions.append(phpExtension)
|
||||
availableExtensions[phpVersion] = extensions.sorted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return availableExtensions
|
||||
}
|
||||
}
|
@ -10,6 +10,8 @@ import Foundation
|
||||
|
||||
protocol BrewCommand {
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
|
||||
|
||||
func getCommandTitle() -> String
|
||||
}
|
||||
|
||||
extension BrewCommand {
|
||||
@ -31,6 +33,44 @@ extension BrewCommand {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
internal func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
var loggedMessages: [String] = []
|
||||
|
||||
let (process, _) = try! await Shell.attach(
|
||||
command,
|
||||
didReceiveOutput: { text, _ in
|
||||
if !text.isEmpty {
|
||||
Log.perf(text)
|
||||
loggedMessages.append(text)
|
||||
}
|
||||
|
||||
if let (number, text) = self.reportInstallationProgress(text) {
|
||||
onProgress(.create(value: number, title: getCommandTitle(), description: text))
|
||||
}
|
||||
},
|
||||
withTimeout: .minutes(15)
|
||||
)
|
||||
|
||||
if process.terminationStatus <= 0 {
|
||||
loggedMessages = []
|
||||
return
|
||||
} else {
|
||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||
}
|
||||
}
|
||||
|
||||
internal func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
if !BrewDiagnostics.installedTaps.contains("shivammathur/php") {
|
||||
let command = "brew tap shivammathur/php"
|
||||
try await run(command, onProgress)
|
||||
}
|
||||
|
||||
if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") {
|
||||
let command = "brew tap shivammathur/extensions"
|
||||
try await run(command, onProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BrewCommandProgress {
|
||||
|
@ -0,0 +1,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
|
||||
|
||||
class InstallAndUpgradeCommand: BrewCommand {
|
||||
|
||||
class ModifyPhpVersionCommand: BrewCommand {
|
||||
let title: String
|
||||
let installing: [BrewFormula]
|
||||
let upgrading: [BrewFormula]
|
||||
let installing: [BrewPhpFormula]
|
||||
let upgrading: [BrewPhpFormula]
|
||||
let phpGuard: PhpGuard
|
||||
|
||||
func getCommandTitle() -> String {
|
||||
return title
|
||||
}
|
||||
|
||||
/**
|
||||
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
|
||||
The process will be executed in two steps: first upgrades, then installations.
|
||||
|
||||
Upgrades come first because... well, otherwise installations may very well break.
|
||||
Each version that is installed will need to be checked afterwards (if it is OK).
|
||||
Each version that is installed will need to be checked afterwards. Installing a
|
||||
newer formula may break other PHP installations, which in turn need to be fixed.
|
||||
|
||||
- Important: If any PHP formula is a major upgrade that causes a PHP "version" to be
|
||||
uninstalled, this is remedied by running `upgradeMainPhpFormula()`. This process
|
||||
will ensure that the upgrade is applied, but the also that old version is
|
||||
re-installed and linked again.
|
||||
*/
|
||||
public init(
|
||||
title: String,
|
||||
upgrading: [BrewFormula],
|
||||
installing: [BrewFormula]
|
||||
upgrading: [BrewPhpFormula],
|
||||
installing: [BrewPhpFormula]
|
||||
) {
|
||||
self.title = title
|
||||
self.installing = installing
|
||||
@ -33,17 +43,32 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
}
|
||||
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
let progressTitle = "Please wait..."
|
||||
let progressTitle = "phpman.steps.wait".localized
|
||||
|
||||
onProgress(.create(
|
||||
value: 0.2,
|
||||
title: progressTitle,
|
||||
description: "PHP Monitor is preparing Homebrew..."
|
||||
description: "phpman.steps.preparing".localized
|
||||
))
|
||||
|
||||
// Try to run all upgrade and installation operations
|
||||
try await self.upgradePackages(onProgress)
|
||||
try await self.installPackages(onProgress)
|
||||
// Determine if a formula will become unavailable
|
||||
// This is the case when `php` will be bumped to a new version
|
||||
let unavailable = upgrading.first(where: { formula in
|
||||
formula.unavailableAfterUpgrade
|
||||
})
|
||||
|
||||
// Make sure the tap is installed
|
||||
try await self.checkPhpTap(onProgress)
|
||||
|
||||
if unavailable == nil {
|
||||
// Try to run all upgrade and installation operations
|
||||
try await self.upgradePackages(onProgress)
|
||||
try await self.installPackages(onProgress)
|
||||
} else {
|
||||
// Simply upgrade `php` to the latest version
|
||||
try await self.upgradeMainPhpFormula(unavailable!, onProgress)
|
||||
await PhpEnvironments.shared.determinePhpAlias()
|
||||
}
|
||||
|
||||
// Re-check the installed versions
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
@ -55,6 +80,27 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
await self.completedOperations(onProgress)
|
||||
}
|
||||
|
||||
private func upgradeMainPhpFormula(
|
||||
_ unavailable: BrewPhpFormula,
|
||||
_ onProgress: @escaping (BrewCommandProgress) -> Void
|
||||
) async throws {
|
||||
// Determine which version was previously available (that will become unavailable)
|
||||
guard let short = try? VersionNumber
|
||||
.parse(unavailable.installedVersion!).short else {
|
||||
return
|
||||
}
|
||||
|
||||
// Upgrade the main formula
|
||||
let command = """
|
||||
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
|
||||
\(Paths.brew) upgrade php;
|
||||
\(Paths.brew) install php@\(short);
|
||||
"""
|
||||
|
||||
// Run the upgrade command
|
||||
try await run(command, onProgress)
|
||||
}
|
||||
|
||||
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
// If no upgrades are needed, early exit
|
||||
if self.upgrading.isEmpty {
|
||||
@ -117,35 +163,12 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
try await run(command, onProgress)
|
||||
}
|
||||
|
||||
private func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
var loggedMessages: [String] = []
|
||||
|
||||
let (process, _) = try! await Shell.attach(
|
||||
command,
|
||||
didReceiveOutput: { text, _ in
|
||||
if !text.isEmpty {
|
||||
Log.perf(text)
|
||||
loggedMessages.append(text)
|
||||
}
|
||||
|
||||
if let (number, text) = self.reportInstallationProgress(text) {
|
||||
onProgress(.create(value: number, title: self.title, description: text))
|
||||
}
|
||||
},
|
||||
withTimeout: .minutes(15)
|
||||
)
|
||||
|
||||
if process.terminationStatus <= 0 {
|
||||
loggedMessages = []
|
||||
return
|
||||
} else {
|
||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||
}
|
||||
}
|
||||
|
||||
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
|
||||
// Reload and restart PHP versions
|
||||
onProgress(.create(value: 0.95, title: self.title, description: "Reloading PHP versions..."))
|
||||
onProgress(.create(value: 0.95, title: self.title, description: "phpman.steps.reloading".localized))
|
||||
|
||||
// Ensure all symlinks are correctly linked
|
||||
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||
|
||||
// Check which version of PHP are now installed
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
@ -164,9 +187,8 @@ class InstallAndUpgradeCommand: BrewCommand {
|
||||
// Let the UI know that the installation has been completed
|
||||
onProgress(.create(
|
||||
value: 1,
|
||||
title: "Operation completed!",
|
||||
description: "The installation has succeeded."
|
||||
title: "phpman.steps.completed".localized,
|
||||
description: "phpman.steps.success".localized
|
||||
))
|
||||
}
|
||||
|
||||
}
|
@ -21,13 +21,15 @@ class RemovePhpVersionCommand: BrewCommand {
|
||||
self.phpGuard = PhpGuard()
|
||||
}
|
||||
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
let progressTitle = "Removing PHP \(version)..."
|
||||
func getCommandTitle() -> String {
|
||||
return "phpman.steps.removing".localized("PHP \(version)...")
|
||||
}
|
||||
|
||||
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
|
||||
onProgress(.create(
|
||||
value: 0.2,
|
||||
title: progressTitle,
|
||||
description: "Please wait while Homebrew removes PHP \(version)..."
|
||||
title: getCommandTitle(),
|
||||
description: "phpman.steps.wait".localized
|
||||
))
|
||||
|
||||
let command = """
|
||||
@ -56,7 +58,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
||||
)
|
||||
|
||||
if process.terminationStatus <= 0 {
|
||||
onProgress(.create(value: 0.95, title: progressTitle, description: "Reloading PHP versions..."))
|
||||
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
|
||||
@ -66,7 +68,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
||||
await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true)
|
||||
}
|
||||
|
||||
onProgress(.create(value: 1, title: progressTitle, description: "The operation has succeeded."))
|
||||
onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized))
|
||||
} else {
|
||||
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
|
||||
}
|
@ -9,6 +9,10 @@
|
||||
import Foundation
|
||||
|
||||
class FakeCommand: BrewCommand {
|
||||
func getCommandTitle() -> String {
|
||||
return "Hello"
|
||||
}
|
||||
|
||||
let version: String
|
||||
|
||||
init(version: String) {
|
||||
|
@ -141,7 +141,7 @@ class ValetSite: ValetListable {
|
||||
self.determineDriverViaComposer()
|
||||
|
||||
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() {
|
||||
self.driverDeterminedByComposer = true
|
||||
|
||||
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
|
||||
if self.notableComposerDependencies.keys.contains(key) {
|
||||
self.driver = value
|
||||
}
|
||||
for (key, value) in ProjectTypeDetection.SpecificDependencyList
|
||||
where notableComposerDependencies.keys.contains(key) {
|
||||
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
|
||||
}
|
||||
|
||||
setBusyImage()
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
PhpEnvironments.shared.delegate = self
|
||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||
|
||||
@ -298,13 +297,12 @@ extension MainMenu {
|
||||
}
|
||||
|
||||
@objc func switchToPhpVersion(_ version: String) {
|
||||
setBusyImage()
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
PhpEnvironments.shared.delegate = self
|
||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
|
||||
Task(priority: .userInitiated) { [unowned self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||
|
||||
@ -325,13 +323,12 @@ extension MainMenu {
|
||||
*/
|
||||
func switchToPhp(_ version: String) async {
|
||||
Task { @MainActor [self] in
|
||||
setBusyImage()
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
PhpEnvironments.shared.delegate = self
|
||||
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
}
|
||||
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
await PhpEnvironments.switcher.performSwitch(to: version)
|
||||
|
||||
|
@ -45,21 +45,16 @@ extension MainMenu {
|
||||
.broadcastServicesUpdate
|
||||
]
|
||||
) {
|
||||
if behaviours.contains(.reloadsPhpInstallation) {
|
||||
if behaviours.contains(.reloadsPhpInstallation) || behaviours.contains(.setsBusyUI) {
|
||||
PhpEnvironments.shared.isBusy = true
|
||||
}
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
setBusyImage()
|
||||
}
|
||||
|
||||
Task(priority: .userInitiated) { [unowned self] in
|
||||
var error: Error?
|
||||
|
||||
do { try execute() } catch let e { error = e }
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
do { try execute() } catch let e {
|
||||
error = e
|
||||
Log.err(e)
|
||||
}
|
||||
|
||||
Task { @MainActor [self, error] in
|
||||
@ -68,15 +63,18 @@ extension MainMenu {
|
||||
}
|
||||
|
||||
if behaviours.contains(.updatesMenuBarContents) {
|
||||
updatePhpVersionInStatusBar()
|
||||
} else if behaviours.contains(.setsBusyUI) {
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
}
|
||||
|
||||
if behaviours.contains(.broadcastServicesUpdate) {
|
||||
Task { await ServicesManager.shared.reloadServicesStatus() }
|
||||
}
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
}
|
||||
|
||||
if error != nil {
|
||||
return failure(error!)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ extension MainMenu {
|
||||
func startup() async {
|
||||
// Start with the icon
|
||||
Task { @MainActor in
|
||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
self.setStatusBar(image: NSImage.statusBarIcon)
|
||||
}
|
||||
|
||||
if await Startup().checkEnvironment() {
|
||||
@ -32,19 +32,14 @@ extension MainMenu {
|
||||
// Determine what the `php` formula is aliased to
|
||||
await PhpEnvironments.shared.determinePhpAlias()
|
||||
|
||||
// Make sure that broken symlinks are removed ASAP
|
||||
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
|
||||
|
||||
// Initialize preferences
|
||||
_ = Preferences.shared
|
||||
|
||||
// Determine install method
|
||||
Log.info(BrewDiagnostics.customCaskInstalled
|
||||
? "[BREW] The app has been installed via Homebrew Cask."
|
||||
: "[BREW] The app has been installed directly (optimal)."
|
||||
)
|
||||
|
||||
Log.info(BrewDiagnostics.usesNginxFullFormula
|
||||
? "[BREW] The app will be using the `nginx-full` formula."
|
||||
: "[BREW] The app will be using the `nginx` formula."
|
||||
)
|
||||
// Put some useful diagnostics information in log
|
||||
BrewDiagnostics.logBootInformation()
|
||||
|
||||
// Attempt to find out more info about Valet
|
||||
if Valet.shared.version != nil {
|
||||
@ -63,9 +58,6 @@ extension MainMenu {
|
||||
// Check for an alias conflict
|
||||
await BrewDiagnostics.checkForCaskConflict()
|
||||
|
||||
// Update the icon
|
||||
updatePhpVersionInStatusBar()
|
||||
|
||||
// Attempt to find out if PHP-FPM is broken
|
||||
PhpEnvironments.prepare()
|
||||
|
||||
@ -76,7 +68,6 @@ extension MainMenu {
|
||||
WarningManager.shared.evaluateWarnings()
|
||||
|
||||
// Set up the config watchers on launch (updated automatically when switching)
|
||||
Log.info("Setting up watchers...")
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
|
||||
// Detect built-in and custom applications
|
||||
@ -105,9 +96,33 @@ extension MainMenu {
|
||||
Valet.shared.notifyAboutUnsupportedTLD()
|
||||
}
|
||||
|
||||
// Keep track of which PHP versions are currently about to release
|
||||
Log.info("Experimental PHP versions are: \(Constants.ExperimentalPhpVersions)")
|
||||
|
||||
// Find out which services are active
|
||||
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
|
||||
|
||||
// Post-launch stats and update check, but only if not running tests
|
||||
await performPostLaunchActions()
|
||||
|
||||
// Check if the linked version has changed between launches of phpmon
|
||||
PhpGuard().compareToLastGlobalVersion()
|
||||
|
||||
// We are ready!
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
|
||||
// Finally!
|
||||
Log.info("PHP Monitor is ready to serve!")
|
||||
|
||||
// Check if we upgraded from a previous version
|
||||
AppUpdater.checkIfUpdateWasPerformed()
|
||||
}
|
||||
|
||||
/**
|
||||
Performs a set of post-launch actions, like incrementing stats and checking for updates.
|
||||
(This code is skipped when running SwiftUI previews.)
|
||||
*/
|
||||
private func performPostLaunchActions() async {
|
||||
if !isRunningSwiftUIPreview {
|
||||
Stats.incrementSuccessfulLaunchCount()
|
||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||
@ -121,15 +136,6 @@ extension MainMenu {
|
||||
await AppUpdater().checkForUpdates(userInitiated: false)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the linked version has changed between launches of phpmon
|
||||
PhpGuard().compareToLastGlobalVersion()
|
||||
|
||||
// We are ready!
|
||||
Log.info("PHP Monitor is ready to serve!")
|
||||
|
||||
// Check if we upgraded just now
|
||||
AppUpdater.checkIfUpdateWasPerformed()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,9 @@ extension MainMenu {
|
||||
|
||||
nonisolated func switcherDidCompleteSwitch(to version: String) {
|
||||
// Mark as no longer busy
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
Task { @MainActor in
|
||||
PhpEnvironments.shared.isBusy = false
|
||||
}
|
||||
|
||||
Task { // Things to do after reloading domain list data
|
||||
if Valet.installed {
|
||||
@ -25,7 +27,7 @@ extension MainMenu {
|
||||
|
||||
// Perform UI updates on main thread
|
||||
Task { @MainActor [self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
|
||||
if Valet.installed && !PhpEnvironments.shared.validate(version) {
|
||||
|
@ -37,8 +37,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
// MARK: - UI related
|
||||
|
||||
/**
|
||||
Rebuilds the menu (either asynchronously or synchronously).
|
||||
Defaults to rebuilding the menu asynchronously.
|
||||
Rebuilds the menu on the main thread.
|
||||
*/
|
||||
func rebuild() {
|
||||
Task { @MainActor [self] in
|
||||
@ -80,13 +79,15 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
@objc func refreshActiveInstallation() {
|
||||
if !PhpEnvironments.shared.isBusy {
|
||||
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
|
||||
updatePhpVersionInStatusBar()
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
} else {
|
||||
Log.perf("Skipping version refresh due to busy status!")
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||
@available(*, deprecated, message: "Use the busy status instead")
|
||||
@objc func updatePhpVersionInStatusBar() {
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
@ -139,7 +140,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
@objc func reloadPhpMonitorMenuInBackground() {
|
||||
asyncExecution({
|
||||
// This automatically reloads the menu
|
||||
Log.info("Reloading information about the PHP installation (in the background)...")
|
||||
Log.perf("Reloading information about the PHP installation (in the background)...")
|
||||
}, behaviours: [
|
||||
.setsBusyUI,
|
||||
.reloadsPhpInstallation,
|
||||
@ -150,13 +151,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
|
||||
/** Refreshes the icon with the PHP version. */
|
||||
@objc func refreshIcon() {
|
||||
|
||||
Task { @MainActor [self] in
|
||||
if PhpEnvironments.shared.isBusy {
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
Log.perf("Refreshing icon: currently busy")
|
||||
setStatusBar(image: NSImage.statusBarIcon)
|
||||
} else {
|
||||
Log.perf("Refreshing icon: no longer busy")
|
||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||
// Static icon has been requested
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
||||
setStatusBar(image: NSImage.statusBarIconStatic)
|
||||
} else {
|
||||
// The dynamic icon has been requested
|
||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||
@ -172,13 +176,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the icon to be displayed as busy. */
|
||||
@objc func setBusyImage() {
|
||||
Task { @MainActor [self] in
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Menu Item Functionality
|
||||
|
||||
@objc func openAbout() {
|
||||
@ -206,6 +203,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
PhpDoctorWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openConfigGUI() {
|
||||
PhpConfigManagerWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openDomainList() {
|
||||
DomainListVC.show()
|
||||
}
|
||||
@ -214,6 +215,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
PhpVersionManagerWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openPhpExtensionManager() {
|
||||
PhpExtensionManagerWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openDonate() {
|
||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import Cocoa
|
||||
|
||||
extension StatusMenu {
|
||||
|
||||
func addPhpVersionMenuItems() {
|
||||
@MainActor func addPhpVersionMenuItems() {
|
||||
if PhpEnvironments.phpInstall == nil {
|
||||
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
|
||||
addItems([
|
||||
@ -34,7 +34,7 @@ extension StatusMenu {
|
||||
))
|
||||
}
|
||||
|
||||
func addPhpActionMenuItems() {
|
||||
@MainActor func addPhpActionMenuItems() {
|
||||
if PhpEnvironments.shared.isBusy {
|
||||
addItem(NSMenuItem(title: "mi_busy".localized))
|
||||
return
|
||||
@ -54,7 +54,7 @@ extension StatusMenu {
|
||||
self.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
func addServicesManagerMenuItem() {
|
||||
@MainActor func addServicesManagerMenuItem() {
|
||||
if PhpEnvironments.shared.isBusy {
|
||||
return
|
||||
}
|
||||
@ -65,7 +65,7 @@ extension StatusMenu {
|
||||
])
|
||||
}
|
||||
|
||||
func addSwitchToPhpMenuItems() {
|
||||
@MainActor func addSwitchToPhpMenuItems() {
|
||||
var shortcutKey = 1
|
||||
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
|
||||
// Get the short and long version
|
||||
@ -102,14 +102,14 @@ extension StatusMenu {
|
||||
}
|
||||
}
|
||||
|
||||
func addLiteModeMenuItem() {
|
||||
@MainActor func addLiteModeMenuItem() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_lite_mode".localized, action: #selector(MainMenu.openLiteModeInfo))
|
||||
])
|
||||
}
|
||||
|
||||
func addPreferencesMenuItems() {
|
||||
@MainActor func addPreferencesMenuItems() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_preferences".localized,
|
||||
@ -119,7 +119,7 @@ extension StatusMenu {
|
||||
])
|
||||
}
|
||||
|
||||
func addCoreMenuItems() {
|
||||
@MainActor func addCoreMenuItems() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_about".localized,
|
||||
@ -131,7 +131,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Valet
|
||||
|
||||
func addValetMenuItems() {
|
||||
@MainActor func addValetMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_valet".localized),
|
||||
NSMenuItem(title: "mi_valet_config".localized,
|
||||
@ -146,12 +146,15 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - PHP Configuration
|
||||
|
||||
func addConfigurationMenuItems() {
|
||||
@MainActor func addConfigurationMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
||||
NSMenuItem(title: "mi_php_version_manager".localized,
|
||||
action: #selector(MainMenu.openPhpVersionManager),
|
||||
keyEquivalent: "m"),
|
||||
NSMenuItem(title: "mi_php_ext_manager".localized,
|
||||
action: #selector(MainMenu.openPhpExtensionManager),
|
||||
keyEquivalent: "e"),
|
||||
NSMenuItem(title: "mi_php_config".localized,
|
||||
action: #selector(MainMenu.openActiveConfigFolder),
|
||||
keyEquivalent: "c"),
|
||||
@ -166,7 +169,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Composer
|
||||
|
||||
func addComposerMenuItems() {
|
||||
@MainActor func addComposerMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_composer".localized),
|
||||
NSMenuItem(
|
||||
@ -187,7 +190,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Stats
|
||||
|
||||
func addStatsMenuItem() {
|
||||
@MainActor func addStatsMenuItem() {
|
||||
guard let install = PhpEnvironments.phpInstall else {
|
||||
Log.info("Not showing stats menu item if no PHP version is linked.")
|
||||
return
|
||||
@ -204,7 +207,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
func addExtensionsMenuItems() {
|
||||
@MainActor func addExtensionsMenuItems() {
|
||||
guard let install = PhpEnvironments.phpInstall else {
|
||||
Log.info("Not showing extensions menu items if no PHP version is linked.")
|
||||
return
|
||||
@ -225,7 +228,7 @@ extension StatusMenu {
|
||||
|
||||
// MARK: - Presets
|
||||
|
||||
func addPresetsMenuItem() {
|
||||
@MainActor func addPresetsMenuItem() {
|
||||
guard let presets = Preferences.custom.presets else {
|
||||
addEmptyPresetHelp()
|
||||
return
|
||||
|
@ -9,7 +9,7 @@ import Cocoa
|
||||
|
||||
class StatusMenu: NSMenu {
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
func addMenuItems() {
|
||||
@MainActor func addMenuItems() {
|
||||
addPhpVersionMenuItems()
|
||||
addItem(NSMenuItem.separator())
|
||||
|
||||
|
@ -91,6 +91,7 @@ class BetterAlert {
|
||||
}
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
windowController.window?.makeKeyAndOrderFront(nil)
|
||||
windowController.window?.setCenterPosition(offsetY: 70)
|
||||
return NSApplication.shared.runModal(for: windowController.window!)
|
||||
|
@ -20,6 +20,7 @@ enum PreferenceName: String, Codable {
|
||||
case globalHotkey = "global_hotkey"
|
||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
||||
case languageOverride = "language_override"
|
||||
|
||||
// APPEARANCE
|
||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||
@ -84,7 +85,8 @@ enum PreferenceName: String, Codable {
|
||||
],
|
||||
.string: [
|
||||
.globalHotkey,
|
||||
.iconTypeToDisplay
|
||||
.iconTypeToDisplay,
|
||||
.languageOverride
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ class Preferences {
|
||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
||||
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
||||
PreferenceName.languageOverride.rawValue: "",
|
||||
|
||||
/// Preferences: Appearance
|
||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||
|
@ -17,7 +17,9 @@ class GeneralPreferencesVC: GenericPreferenceVC {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||
|
||||
_ = vc.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
||||
_ = vc
|
||||
.addView(when: true, vc.getLanguageOptionsPV())
|
||||
.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
|
||||
.addView(when: true, vc.getAutoRestartServicesPV())
|
||||
.addView(when: true, vc.getAutomaticComposerUpdatePV())
|
||||
.addView(when: true, vc.getShortcutPV())
|
||||
|
@ -48,11 +48,44 @@ class GenericPreferenceVC: NSViewController {
|
||||
)
|
||||
}
|
||||
|
||||
func getLanguageOptionsPV() -> NSView {
|
||||
var options = Bundle.main.localizations
|
||||
.filter({ $0 != "Base"})
|
||||
.map({ lang in
|
||||
return PreferenceDropdownOption(
|
||||
label: Locale.current.localizedString(forLanguageCode: lang)!,
|
||||
value: lang
|
||||
)
|
||||
})
|
||||
options.insert(PreferenceDropdownOption(label: "System Default", value: ""), at: 0)
|
||||
|
||||
return SelectPreferenceView.make(
|
||||
sectionText: "prefs.language".localized,
|
||||
descriptionText: "prefs.language_options_desc".localized,
|
||||
options: options,
|
||||
preference: .languageOverride,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.rebuild()
|
||||
|
||||
if let window = App.shared.preferencesWindowController?.window {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "alert.language_changed.title".localized
|
||||
alert.informativeText = "alert.language_changed.subtitle".localized
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: "generic.ok".localized)
|
||||
alert.beginSheetModal(for: window)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func getIconOptionsPV() -> NSView {
|
||||
return SelectPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.icon_options_desc".localized,
|
||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
||||
options: MenuBarIcon.allCases
|
||||
.map({ return PreferenceDropdownOption(label: $0.rawValue, value: $0.rawValue) }),
|
||||
localizationPrefix: "prefs.icon_options",
|
||||
preference: .iconTypeToDisplay,
|
||||
action: {
|
||||
|
@ -65,9 +65,11 @@ class PreferencesWindowController: PMWindowController {
|
||||
App.shared.preferencesWindowController?.showWindow(self)
|
||||
|
||||
if justCreated {
|
||||
App.shared.preferencesWindowController?.positionWindowInTopLeftCorner()
|
||||
App.shared.preferencesWindowController?.positionWindowInTopRightCorner()
|
||||
}
|
||||
|
||||
App.shared.preferencesWindowController?.window?.orderFrontRegardless()
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
|
@ -84,6 +84,10 @@ class Stats {
|
||||
)
|
||||
}
|
||||
|
||||
public static func clearCurrentGlobalPhpVersion() {
|
||||
UserDefaults.standard.removeObject(forKey: InternalStats.lastGlobalPhpVersion.rawValue)
|
||||
}
|
||||
|
||||
/**
|
||||
Determine if the sponsor message should be displayed.
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@ -23,7 +23,7 @@
|
||||
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@ -31,7 +31,7 @@
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||
<rect key="frame" x="-2" y="27" width="154" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||
|
@ -9,30 +9,34 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class SelectPreferenceView: NSView, XibLoadable {
|
||||
struct PreferenceDropdownOption {
|
||||
let label: String
|
||||
let value: String
|
||||
}
|
||||
|
||||
class SelectPreferenceView: NSView, XibLoadable {
|
||||
@IBOutlet weak var labelSection: NSTextField!
|
||||
@IBOutlet weak var labelDescription: NSTextField!
|
||||
@IBOutlet weak var popupButton: NSPopUpButton!
|
||||
|
||||
var localizationPrefix: String = ""
|
||||
var localizationPrefix: String?
|
||||
var imagePrefix: String?
|
||||
|
||||
var options: [String] = [] {
|
||||
var options: [PreferenceDropdownOption] = [] {
|
||||
didSet {
|
||||
self.popupButton.removeAllItems()
|
||||
self.options.forEach { value in
|
||||
self.popupButton.addItem(
|
||||
withTitle: "\(localizationPrefix).\(value)".localized
|
||||
)
|
||||
self.options.forEach { option in
|
||||
if let prefix = localizationPrefix {
|
||||
self.popupButton.addItem(withTitle: "\(prefix).\(option.label)".localized)
|
||||
} else {
|
||||
self.popupButton.addItem(withTitle: option.label)
|
||||
}
|
||||
}
|
||||
|
||||
if imagePrefix == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.popupButton.itemArray.enumerated().forEach { item in
|
||||
item.element.image = NSImage(named: "\(imagePrefix!)_\(self.options[item.offset])")
|
||||
if let prefix = imagePrefix {
|
||||
self.popupButton.itemArray.enumerated().forEach { item in
|
||||
item.element.image = NSImage(named: "\(prefix)_\(self.options[item.offset].value)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,19 +47,18 @@ class SelectPreferenceView: NSView, XibLoadable {
|
||||
didSet {
|
||||
let value = Preferences.preferences[preference] as! String
|
||||
self.options.enumerated().forEach { option in
|
||||
if option.element == value {
|
||||
if option.element.value == value {
|
||||
self.popupButton.selectItem(at: option.offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable function_parameter_count
|
||||
static func make(
|
||||
sectionText: String,
|
||||
descriptionText: String,
|
||||
options: [String],
|
||||
localizationPrefix: String,
|
||||
options: [PreferenceDropdownOption],
|
||||
localizationPrefix: String? = nil,
|
||||
imagePrefix: String? = nil,
|
||||
preference: PreferenceName,
|
||||
action: @escaping () -> Void) -> NSView {
|
||||
@ -72,11 +75,10 @@ class SelectPreferenceView: NSView, XibLoadable {
|
||||
|
||||
return view
|
||||
}
|
||||
// swiftlint:enable function_parameter_count
|
||||
|
||||
@IBAction func valueChanged(_ sender: Any) {
|
||||
let index = self.popupButton.indexOfSelectedItem
|
||||
Preferences.update(.iconTypeToDisplay, value: self.options[index])
|
||||
Preferences.update(self.preference, value: self.options[index].value)
|
||||
self.action()
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@ -13,16 +13,16 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="596" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||
<rect key="frame" x="183" y="5" width="395" height="14"/>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
|
||||
<rect key="frame" x="168" y="5" width="410" height="14"/>
|
||||
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||
<rect key="frame" x="13" y="29" width="154" height="16"/>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
|
||||
<rect key="frame" x="-2" y="29" width="154" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
|
||||
</constraints>
|
||||
@ -33,7 +33,7 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YaB-Tg-Ir3">
|
||||
<rect key="frame" x="182" y="23" width="110" height="25"/>
|
||||
<rect key="frame" x="167" y="23" width="110" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Icon Option" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="SaA-mm-HBo" id="Su6-C4-aGo">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@ -58,7 +58,7 @@
|
||||
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="YaB-Tg-Ir3" secondAttribute="bottom" constant="8" symbolic="YES" id="Mji-pe-CNl"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="YaB-Tg-Ir3" secondAttribute="trailing" constant="20" symbolic="YES" id="Zlg-jj-uKY"/>
|
||||
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="15" id="Ztd-uk-4aw"/>
|
||||
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" id="aBU-J8-gRK"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
|
@ -27,15 +27,15 @@ struct HelpButton: View {
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
.focusable(false)
|
||||
}
|
||||
|
||||
struct HelpButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
HelpButton(action: {}).padding()
|
||||
.previewDisplayName("Light Mode")
|
||||
HelpButton(action: {}).padding().preferredColorScheme(.dark)
|
||||
.previewDisplayName("Dark Mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Light Mode") {
|
||||
HelpButton(action: {})
|
||||
.padding(100)
|
||||
}
|
||||
|
||||
#Preview("Dark Mode") {
|
||||
HelpButton(action: {})
|
||||
.padding(100)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
@ -27,7 +27,9 @@ var isRunningSwiftUIPreview: Bool {
|
||||
|
||||
extension Color {
|
||||
public static var appPrimary: Color = Color("AppColor")
|
||||
public static var appSecondary: Color = Color("AppSecondary")
|
||||
|
||||
// This next one is generated automatically via asset catalogs now
|
||||
// public static var appSecondary: Color = Color("AppSecondary")
|
||||
|
||||
public static var debug: Color = {
|
||||
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
|
||||
|
@ -30,8 +30,6 @@ struct NoDomainResults: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct NoDomainResults_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NoDomainResults()
|
||||
}
|
||||
#Preview {
|
||||
NoDomainResults()
|
||||
}
|
||||
|
@ -126,78 +126,82 @@ struct DisclaimerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct VersionPopoverView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: ""
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Unknown Requirement")
|
||||
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.1"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Requirement Matches")
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0",
|
||||
isolated: "8.0"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Isolated")
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0",
|
||||
isolated: "7.4"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Isolated Mismatch")
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0"
|
||||
),
|
||||
validPhpVersions: [
|
||||
VersionNumber(major: 8, minor: 0, patch: 0),
|
||||
VersionNumber(major: 8, minor: 1, patch: 0)
|
||||
],
|
||||
parent: nil
|
||||
)
|
||||
.previewDisplayName("Recommend Alternatives")
|
||||
}
|
||||
#Preview("Unknown Requirement") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: ""
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("Requirement Matches") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "amazingwebsite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.1"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("Isolated") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0",
|
||||
isolated: "8.0"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("Isolated Mismatch") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0",
|
||||
isolated: "7.4"
|
||||
),
|
||||
validPhpVersions: [],
|
||||
parent: nil
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("Recommend Alternatives") {
|
||||
VersionPopoverView(
|
||||
site: FakeValetSite(
|
||||
fakeWithName: "anothersite",
|
||||
tld: "test",
|
||||
secure: true,
|
||||
path: "/path/to/site",
|
||||
linked: true,
|
||||
constraint: "^8.0"
|
||||
),
|
||||
validPhpVersions: [
|
||||
VersionNumber(major: 8, minor: 0, patch: 0),
|
||||
VersionNumber(major: 8, minor: 1, patch: 0)
|
||||
],
|
||||
parent: nil
|
||||
)
|
||||
}
|
||||
|
@ -45,9 +45,7 @@ struct HeaderView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct HeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HeaderView(text: "Hello world")
|
||||
.frame(width: 330.0)
|
||||
}
|
||||
#Preview {
|
||||
HeaderView(text: "Hello world")
|
||||
.frame(width: 330.0)
|
||||
}
|
||||
|
@ -18,5 +18,6 @@ struct SectionHeaderView: View {
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.appSecondary)
|
||||
.background(Color.debug)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
}
|
||||
|
@ -172,23 +172,21 @@ struct ServiceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ServicesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: ["php", "nginx", "dnsmasq"],
|
||||
status: .active
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
.previewDisplayName("Active 1")
|
||||
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: [
|
||||
"php", "nginx", "dnsmasq", "thing1",
|
||||
"thing2", "thing3", "thing4", "thing5"
|
||||
],
|
||||
status: .inactive
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
.previewDisplayName("Active 2")
|
||||
}
|
||||
#Preview("Active 1") {
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: ["php", "nginx", "dnsmasq"],
|
||||
status: .active
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
}
|
||||
|
||||
#Preview("Active 2") {
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: [
|
||||
"php", "nginx", "dnsmasq", "thing1",
|
||||
"thing2", "thing3", "thing4", "thing5"
|
||||
],
|
||||
status: .inactive
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
}
|
||||
|
@ -60,38 +60,49 @@ struct StatsView: View {
|
||||
.padding(.leading, 30)
|
||||
.padding(.trailing, 30)
|
||||
} else {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 30) {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
VStack(alignment: .center, spacing: 3) {
|
||||
SectionHeaderView(text: "mi_memory_limit".localized.uppercased())
|
||||
Text(memoryLimit)
|
||||
.fontWeight(.medium)
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
Divider()
|
||||
VStack(alignment: .center, spacing: 3) {
|
||||
SectionHeaderView(text: "mi_post_max_size".localized.uppercased())
|
||||
Text(maxPostSize)
|
||||
.fontWeight(.medium)
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
Divider()
|
||||
VStack(alignment: .center, spacing: 3) {
|
||||
SectionHeaderView(text: "mi_upload_max_filesize".localized.uppercased())
|
||||
Text(maxUploadSize)
|
||||
.fontWeight(.medium)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StatsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
StatsView(
|
||||
memoryLimit: "1024 MB",
|
||||
maxPostSize: "1024 MB",
|
||||
maxUploadSize: "1024 MB"
|
||||
)
|
||||
}
|
||||
#Preview {
|
||||
StatsView(
|
||||
memoryLimit: "1024 MB",
|
||||
maxPostSize: "1024 MB",
|
||||
maxUploadSize: "1024 MB"
|
||||
).frame(height: 100)
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
//
|
||||
// ProgressViewSubject.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 11/03/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class ProgressViewSubject: ObservableObject {
|
||||
@Published var title: String
|
||||
@Published var description: String?
|
||||
@Published var progress: Double
|
||||
|
||||
init(title: String, description: String) {
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.progress = 0
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
//
|
||||
// ProgressWindowView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 11/03/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProgressWindowView: View {
|
||||
@ObservedObject var subject: ProgressViewSubject
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(subject.title)
|
||||
.font(.system(size: 14))
|
||||
.bold()
|
||||
if subject.description != nil {
|
||||
Text(subject.description!)
|
||||
.font(.system(size: 13))
|
||||
}
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
.padding(.top, 12)
|
||||
ProgressView(value: subject.progress)
|
||||
.padding(.top, 0)
|
||||
.padding(.bottom, 12)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor static func display(_ subject: ProgressViewSubject) async -> NSWindowController {
|
||||
let view = ProgressWindowView(subject: subject)
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 240),
|
||||
styleMask: [.titled, .closable, .utilityWindow],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
window.title = ""
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.contentView = NSHostingView(rootView: view)
|
||||
let controller = NSWindowController(window: window)
|
||||
controller.showWindow(nil)
|
||||
controller.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.window?.makeKeyAndOrderFront(nil)
|
||||
windowController.positionWindowInTopLeftCorner()
|
||||
windowController.positionWindowInTopRightCorner()
|
||||
|
||||
windowController.progressView?.labelTitle.stringValue = title
|
||||
windowController.progressView?.labelDescription.stringValue = description
|
||||
|
@ -17,13 +17,13 @@ extension App {
|
||||
onChange: { Task { await self.onHomebrewPhpModification() } }
|
||||
)
|
||||
|
||||
App.shared.watchers[.homebrewBinaries] = notifier
|
||||
App.shared.watchers["homebrewBinaries"] = notifier
|
||||
}
|
||||
|
||||
public func destroyHomebrewWatchers() {
|
||||
// Removing requires termination and then removing reference
|
||||
self.watchers[.homebrewBinaries]?.terminate()
|
||||
self.watchers[.homebrewBinaries] = nil
|
||||
self.watchers["homebrewBinaries"]?.terminate()
|
||||
self.watchers["homebrewBinaries"] = nil
|
||||
}
|
||||
|
||||
public func onHomebrewPhpModification() async {
|
||||
@ -31,10 +31,13 @@ extension App {
|
||||
Log.info("Something changed in the Homebrew binary directory...")
|
||||
await PhpEnvironments.detectPhpVersions()
|
||||
await MainMenu.shared.refreshActiveInstallation()
|
||||
// let new = PhpEnvironments.shared.currentInstall?.version.text
|
||||
|
||||
// TODO:
|
||||
// Check if the new and previous version are different
|
||||
// if so, we can show a notification if needed
|
||||
//
|
||||
// TODO: PHP Guard 2.0
|
||||
// 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 {
|
||||
|
||||
func startWatcher(_ url: URL) {
|
||||
Log.perf("No watcher currently active...")
|
||||
self.watcher = PhpConfigWatcher(for: url)
|
||||
func startWatchManager(_ url: URL) {
|
||||
Log.perf("Starting config watch manager...")
|
||||
self.watchManager = ConfigWatchManager(for: url)
|
||||
|
||||
self.watcher.didChange = { url in
|
||||
self.watchManager.didChange = { url in
|
||||
Log.perf("Something has changed in: \(url)")
|
||||
|
||||
// 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 {
|
||||
Log.perf("Refreshing menu...")
|
||||
Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() }
|
||||
self.watcher.lastUpdate = Date().timeIntervalSince1970
|
||||
self.watchManager.lastUpdate = Date().timeIntervalSince1970
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlePhpConfigWatcher(forceReload: Bool = false) {
|
||||
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
|
||||
}
|
||||
|
||||
guard let install = PhpEnvironments.phpInstall else {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
// due to timing issues, which creates retain cycles.
|
||||
// due to timing issues, which creates retain cycles
|
||||
Task { @MainActor in
|
||||
// Watcher needs to be created
|
||||
if self.watcher == nil {
|
||||
self.startWatcher(url)
|
||||
if self.watchManager == nil {
|
||||
self.startWatchManager(url)
|
||||
}
|
||||
|
||||
// Watcher needs to be updated
|
||||
if self.watcher.url != url || forceReload {
|
||||
self.watcher.disable()
|
||||
self.watcher = nil
|
||||
if self.watchManager.url != url || forceReload {
|
||||
self.watchManager.disable()
|
||||
self.watchManager = nil
|
||||
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.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class FSNotifier {
|
||||
enum Kind {
|
||||
case homebrewLocks, homebrewBinaries
|
||||
}
|
||||
|
||||
public static var shared: FSNotifier! = nil
|
||||
|
||||
@ -66,4 +63,5 @@ class FSNotifier {
|
||||
deinit {
|
||||
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) {
|
||||
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
||||
imageViewType.image = NSImage(
|
||||
named: site.aliasPath == nil
|
||||
? "IconParked"
|
||||
: "IconLinked"
|
||||
)
|
||||
imageViewType.image = site.aliasPath == nil
|
||||
? NSImage.iconParked
|
||||
: NSImage.iconLinked
|
||||
|
||||
// Unless, of course, this is a default site
|
||||
if site.absolutePath == Valet.shared.config.defaultSite {
|
||||
imageViewType.image = NSImage(named: "IconDefault")
|
||||
imageViewType.image = NSImage.iconDefault
|
||||
}
|
||||
|
||||
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
||||
}
|
||||
|
||||
func populateCell(with proxy: ValetProxy) {
|
||||
imageViewType.image = NSImage(named: "IconProxy")
|
||||
imageViewType.image = NSImage.iconProxy
|
||||
}
|
||||
}
|
||||
|
@ -34,14 +34,13 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||
|
||||
if site.isolatedPhpVersion != nil {
|
||||
imageViewPhpVersionOK.isHidden = false
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
|
||||
imageViewPhpVersionOK.image = NSImage.isolated
|
||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.isolated".localized(site.servingPhpVersion)
|
||||
} else {
|
||||
imageViewPhpVersionOK.isHidden = (site.preferredPhpVersion == "???"
|
||||
|| !site.isCompatibleWithPreferredPhpVersion)
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
|
||||
imageViewPhpVersionOK.image = NSImage.checkmark
|
||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.preferredPhpVersion)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
guard let site = selectedSite else {
|
||||
return
|
||||
|
@ -42,7 +42,20 @@ extension DomainListVC {
|
||||
addDisabledIsolation(to: menu)
|
||||
}
|
||||
|
||||
addSeparator(to: menu)
|
||||
|
||||
if let extensions = site.isolatedPhpVersion?.extensions ?? PhpEnvironments.phpInstall?.extensions,
|
||||
let version = site.isolatedPhpVersion?.versionNumber.short ?? PhpEnvironments.phpInstall?.version.short {
|
||||
menu.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
|
||||
addMenuItemsForExtensions(
|
||||
to: menu,
|
||||
for: extensions,
|
||||
version: version
|
||||
)
|
||||
}
|
||||
|
||||
menu.addItem(HeaderView.asMenuItem(text: "domain_list.actions".localized))
|
||||
|
||||
addToggleSecure(to: menu, secured: site.secured)
|
||||
addUnlink(to: menu, with: site)
|
||||
|
||||
@ -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
|
||||
|
||||
private func addMenuItemsForProxy(_ proxy: ValetProxy) {
|
||||
|
@ -85,6 +85,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
|
||||
App.shared.domainListWindowController!.showWindow(self)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
App.shared.domainListWindowController?.window?.orderFrontRegardless()
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
@ -31,7 +31,7 @@ struct OnboardingTextItem: View {
|
||||
.opacity(self.unavailable ? 0.5 : 1)
|
||||
Text(description.localizedForSwiftUI)
|
||||
.foregroundColor(Color.secondary)
|
||||
.font(.system(size: 13))
|
||||
.font(.system(size: 12))
|
||||
.lineLimit(4)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(minWidth: 0, maxWidth: 800, alignment: .leading)
|
||||
@ -51,7 +51,7 @@ struct OnboardingView: View {
|
||||
HStack {
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.frame(width: 100, height: 100)
|
||||
.padding(.bottom, 5)
|
||||
.padding(.trailing, 25)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
@ -126,20 +126,19 @@ struct OnboardingView: View {
|
||||
Button("onboarding.tour.close".localized) {
|
||||
App.shared.onboardingWindowController?.close()
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.bottom, 15)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
}
|
||||
.padding(.leading)
|
||||
.padding(.trailing)
|
||||
.padding(.bottom, 0)
|
||||
}
|
||||
.frame(width: 600)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
OnboardingView()
|
||||
}
|
||||
}
|
||||
#Preview {
|
||||
OnboardingView()
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class OnboardingWindowController: PMWindowController {
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.delegate = delegate ?? windowController
|
||||
window.contentView = NSHostingView(rootView: OnboardingView())
|
||||
window.setContentSize(NSSize(width: 600, height: 600))
|
||||
window.setContentSize(window.contentView!.fittingSize)
|
||||
|
||||
App.shared.onboardingWindowController = windowController
|
||||
}
|
||||
|
@ -52,7 +52,20 @@ class BytePhpPreference: PhpPreference {
|
||||
// MARK: Save Value
|
||||
|
||||
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)? {
|
||||
|
@ -15,6 +15,14 @@ class PhpPreference {
|
||||
init(key: String) {
|
||||
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 {
|
||||
|
@ -35,12 +35,12 @@ struct PreferenceContainer<ControlView: View>: View {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
controlView
|
||||
|
||||
Text(self.description.localizedForSwiftUI)
|
||||
.lineLimit(nil)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
@ -51,6 +51,7 @@ struct ByteLimitView: View {
|
||||
@State private var unit: BytePhpPreference.UnitOption
|
||||
@State private var numberText: String
|
||||
@State private var unlimited: Bool
|
||||
@State private var timer: Timer?
|
||||
|
||||
private var preference: BytePhpPreference
|
||||
|
||||
@ -65,9 +66,11 @@ struct ByteLimitView: View {
|
||||
if !unlimited {
|
||||
HStack {
|
||||
TextField("", text: $numberText)
|
||||
.onChange(of: numberText) { newText in
|
||||
self.preference.value = Int(newText) ?? 256
|
||||
print(self.preference.internalValue)
|
||||
.onChange(of: numberText) { [weak preference] newText in
|
||||
timer?.invalidate()
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in
|
||||
preference?.value = Int(newText) ?? 256
|
||||
}
|
||||
}
|
||||
Picker("Limit Name", selection: $unit) {
|
||||
ForEach(BytePhpPreference.UnitOption.allCases, id: \.self) {
|
||||
@ -79,25 +82,35 @@ struct ByteLimitView: View {
|
||||
.pickerStyle(.menu)
|
||||
.onChange(of: unit) { newValue in
|
||||
self.preference.unit = newValue
|
||||
print(self.preference.internalValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
static var previews: some View {
|
||||
PreferenceContainer(name: "Max Size", description: "Some maximum size") {
|
||||
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
|
||||
}
|
||||
|
||||
ConfigManagerView()
|
||||
.frame(width: 600, height: .infinity)
|
||||
.previewDisplayName("Config Manager")
|
||||
}
|
||||
#Preview("Byte Limit View") {
|
||||
PreferenceContainer(
|
||||
name: "Max Size",
|
||||
description:
|
||||
"Here's an extensive description that is obviously way too long but it should wrap." +
|
||||
"The point of the wrapping text is that is allows us to see what's going on with the layout here."
|
||||
) {
|
||||
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
|
||||
}.frame(width: 600, height: 200)
|
||||
}
|
||||
|
||||
#Preview("Config Manager") {
|
||||
ConfigManagerView()
|
||||
.frame(width: 600, height: .infinity)
|
||||
.previewDisplayName("Config Manager")
|
||||
}
|
||||
|
@ -13,14 +13,14 @@ struct ConfigManagerView: View {
|
||||
var preferences: [PhpPreference] = [
|
||||
BytePhpPreference(key: "memory_limit"),
|
||||
BytePhpPreference(key: "post_max_size"),
|
||||
BoolPhpPreference(key: "file_uploads"),
|
||||
// BoolPhpPreference(key: "file_uploads"),
|
||||
BytePhpPreference(key: "upload_max_filesize")
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack(alignment: .center, spacing: 15) {
|
||||
Image(systemName: "square.and.pencil.circle.fill")
|
||||
Image(systemName: "gearshape.fill")
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(Color.blue)
|
||||
@ -51,6 +51,7 @@ struct ConfigManagerView: View {
|
||||
if let preference = preference as? BytePhpPreference {
|
||||
ByteLimitView(preference: preference)
|
||||
}
|
||||
/*
|
||||
if let preference = preference as? BoolPhpPreference {
|
||||
Toggle("", isOn: preference.$value)
|
||||
.toggleStyle(.switch)
|
||||
@ -59,6 +60,7 @@ struct ConfigManagerView: View {
|
||||
if let preference = preference as? StringPhpPreference {
|
||||
TextField("Placeholder", text: preference.$value)
|
||||
}
|
||||
*/
|
||||
}.frame(maxWidth: .infinity)
|
||||
}
|
||||
}.padding(10)
|
||||
@ -67,7 +69,7 @@ struct ConfigManagerView: View {
|
||||
|
||||
VStack(alignment: .trailing) {
|
||||
Button("Close", action: {
|
||||
|
||||
App.shared.phpConfigManagerWindowController?.close()
|
||||
})
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
@ -78,14 +80,10 @@ struct ConfigManagerView: View {
|
||||
alignment: .topTrailing
|
||||
)
|
||||
}
|
||||
}
|
||||
}.frame(maxHeight: 485)
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigManagerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ConfigManagerView()
|
||||
.frame(width: 600)
|
||||
.previewDisplayName("Live Preview")
|
||||
}
|
||||
#Preview {
|
||||
ConfigManagerView().frame(width: 600)
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
name: "Running PHP Monitor with Rosetta on M1",
|
||||
title: "warnings.arm_compatibility.title".localized,
|
||||
paragraphs: { return ["warnings.arm_compatibility.description".localized] },
|
||||
title: "warnings.arm_compatibility.title",
|
||||
paragraphs: { return ["warnings.arm_compatibility.description"] },
|
||||
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-and-Apple-Silicon"
|
||||
),
|
||||
Warning(
|
||||
@ -44,11 +44,11 @@ class WarningManager: ObservableObject {
|
||||
!FileSystem.isWriteableFile("/usr/local/bin/")
|
||||
},
|
||||
name: "Helpers cannot be symlinked and not in PATH",
|
||||
title: "warnings.helper_permissions.title".localized,
|
||||
title: "warnings.helper_permissions.title",
|
||||
paragraphs: { return [
|
||||
"warnings.helper_permissions.description".localized,
|
||||
"warnings.helper_permissions.unavailable".localized,
|
||||
"warnings.helper_permissions.symlink".localized
|
||||
"warnings.helper_permissions.description",
|
||||
"warnings.helper_permissions.unavailable",
|
||||
"warnings.helper_permissions.symlink"
|
||||
] },
|
||||
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries"
|
||||
),
|
||||
@ -58,7 +58,7 @@ class WarningManager: ObservableObject {
|
||||
return !PhpConfigChecker.shared.missing.isEmpty
|
||||
},
|
||||
name: "Your PHP installation is missing configuration files",
|
||||
title: "warnings.files_missing.title".localized,
|
||||
title: "warnings.files_missing.title",
|
||||
paragraphs: { return [
|
||||
"warnings.files_missing.description".localized(
|
||||
PhpConfigChecker.shared.missing.joined(separator: "\n• ")
|
||||
|
@ -25,8 +25,6 @@ struct NoWarningsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct NoWarningsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NoWarningsView()
|
||||
}
|
||||
#Preview {
|
||||
NoWarningsView().padding()
|
||||
}
|
||||
|
@ -89,19 +89,19 @@ struct PhpDoctorView: View {
|
||||
}
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listStyle(.plain)
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.frame(minHeight: 350, maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
.frame(width: 600)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
}
|
||||
|
||||
struct WarningListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
|
||||
.frame(width: 600, height: 480)
|
||||
.previewDisplayName("Empty List")
|
||||
|
||||
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
|
||||
.frame(width: 600, height: 480)
|
||||
.previewDisplayName("List With All Warnings")
|
||||
}
|
||||
#Preview("Empty List") {
|
||||
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
|
||||
.frame(width: 600, height: 480)
|
||||
}
|
||||
|
||||
#Preview("List With All Warnings") {
|
||||
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
|
||||
.frame(width: 600, height: 480)
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class PhpDoctorWindowController: PMWindowController {
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.delegate = delegate ?? windowController
|
||||
window.contentView = NSHostingView(rootView: PhpDoctorView())
|
||||
window.setContentSize(NSSize(width: 600, height: 480))
|
||||
window.setContentSize(window.contentView!.fittingSize)
|
||||
|
||||
App.shared.phpDoctorWindowController = windowController
|
||||
}
|
||||
@ -41,5 +41,6 @@ class PhpDoctorWindowController: PMWindowController {
|
||||
App.shared.phpDoctorWindowController?.window?.setCenterPosition(offsetY: 70)
|
||||
|
||||
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