mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-06 03:20:09 +02:00
✅ Added linting
This commit is contained in:
15
.swiftlint.yml
Normal file
15
.swiftlint.yml
Normal file
@ -0,0 +1,15 @@
|
||||
disabled_rules:
|
||||
- todo
|
||||
- identifier_name
|
||||
- force_try
|
||||
- force_cast
|
||||
|
||||
opt_in_rules:
|
||||
- empty_count
|
||||
|
||||
included:
|
||||
- phpmon
|
||||
- phpmon-tests
|
||||
|
||||
excluded:
|
||||
- phpmon/Vendor
|
@ -230,6 +230,7 @@
|
||||
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
|
||||
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
|
||||
C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
|
||||
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; };
|
||||
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
|
||||
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
|
||||
C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */; };
|
||||
@ -405,6 +406,7 @@
|
||||
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
|
||||
C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = "<group>"; };
|
||||
C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = "<group>"; };
|
||||
C4F5FBCC28218C93001065C5 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
|
||||
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
|
||||
@ -549,6 +551,7 @@
|
||||
C4E713562570150F00007428 /* SECURITY.md */,
|
||||
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
|
||||
54D9E0C027E4F5E9003B9AD9 /* LICENSE */,
|
||||
C4F5FBCC28218C93001065C5 /* .swiftlint.yml */,
|
||||
C4E713572570151400007428 /* docs */,
|
||||
C41C1B3522B0097F00E7CF16 /* phpmon */,
|
||||
C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
|
||||
@ -974,6 +977,7 @@
|
||||
C41C1B2F22B0097F00E7CF16 /* Sources */,
|
||||
C41C1B3022B0097F00E7CF16 /* Frameworks */,
|
||||
C41C1B3122B0097F00E7CF16 /* Resources */,
|
||||
C4F5FBCB28216985001065C5 /* Run `swiftlint` */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -1089,6 +1093,27 @@
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
C4F5FBCB28216985001065C5 /* Run `swiftlint` */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run `swiftlint`";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
C41C1B2F22B0097F00E7CF16 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
@ -1291,6 +1316,7 @@
|
||||
C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */,
|
||||
C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */,
|
||||
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
|
||||
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */,
|
||||
C40B24F227A310770018C7D2 /* Events.swift in Sources */,
|
||||
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
|
||||
C4C0E8E027F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */,
|
||||
|
@ -15,11 +15,11 @@ class CommandTest: XCTestCase {
|
||||
path: Paths.php,
|
||||
arguments: ["-v"]
|
||||
)
|
||||
|
||||
|
||||
XCTAssert(version.contains("(cli)"))
|
||||
XCTAssert(version.contains("NTS"))
|
||||
XCTAssert(version.contains("built"))
|
||||
XCTAssert(version.contains("Zend"))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import XCTest
|
||||
|
||||
class HomebrewPackageTest: XCTestCase {
|
||||
|
||||
|
||||
// - MARK: SYNTHETIC TESTS
|
||||
|
||||
static var jsonBrewFile: URL {
|
||||
@ -22,7 +22,7 @@ class HomebrewPackageTest: XCTestCase {
|
||||
let package = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self, from: json.data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
|
||||
XCTAssertEqual(package.name, "php")
|
||||
XCTAssertEqual(package.full_name, "php")
|
||||
XCTAssertEqual(package.aliases.first!, "php@8.0")
|
||||
@ -30,23 +30,23 @@ class HomebrewPackageTest: XCTestCase {
|
||||
installed.version.starts(with: "8.0")
|
||||
}), true)
|
||||
}
|
||||
|
||||
|
||||
static var jsonBrewServicesFile: URL {
|
||||
return Bundle(for: Self.self)
|
||||
.url(forResource: "brew-services", withExtension: "json")!
|
||||
}
|
||||
|
||||
|
||||
func testCanParseServicesJson() throws {
|
||||
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
|
||||
let services = try! JSONDecoder().decode(
|
||||
[HomebrewService].self, from: json.data(using: .utf8)!
|
||||
)
|
||||
|
||||
|
||||
XCTAssertGreaterThan(services.count, 0)
|
||||
XCTAssertEqual(services.first?.name, "dnsmasq")
|
||||
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
|
||||
}
|
||||
|
||||
|
||||
// - MARK: LIVE TESTS
|
||||
|
||||
/// This test requires that you have a valid Homebrew installation set up,
|
||||
@ -63,13 +63,13 @@ class HomebrewPackageTest: XCTestCase {
|
||||
).filter({ service in
|
||||
return ["php", "nginx", "dnsmasq"].contains(service.name)
|
||||
})
|
||||
|
||||
XCTAssertTrue(services.contains(where: {$0.name == "php"} ))
|
||||
XCTAssertTrue(services.contains(where: {$0.name == "nginx"} ))
|
||||
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} ))
|
||||
|
||||
XCTAssertTrue(services.contains(where: {$0.name == "php"}))
|
||||
XCTAssertTrue(services.contains(where: {$0.name == "nginx"}))
|
||||
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"}))
|
||||
XCTAssertEqual(services.count, 3)
|
||||
}
|
||||
|
||||
|
||||
/// This test requires that you have a valid Homebrew installation set up,
|
||||
/// and requires the `php` formula to be installed.
|
||||
/// If this test fails, there is an issue with your Homebrew installation
|
||||
@ -79,7 +79,7 @@ class HomebrewPackageTest: XCTestCase {
|
||||
[HomebrewPackage].self,
|
||||
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
|
||||
XCTAssertTrue(package.name == "php")
|
||||
}
|
||||
}
|
||||
|
@ -9,23 +9,23 @@
|
||||
import XCTest
|
||||
|
||||
class NginxConfigurationTest: XCTestCase {
|
||||
|
||||
|
||||
static var regularUrl: URL {
|
||||
return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")!
|
||||
}
|
||||
|
||||
|
||||
static var isolatedUrl: URL {
|
||||
return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")!
|
||||
}
|
||||
|
||||
|
||||
static var proxyUrl: URL {
|
||||
return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")!
|
||||
}
|
||||
|
||||
|
||||
static var secureProxyUrl: URL {
|
||||
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")!
|
||||
}
|
||||
|
||||
|
||||
func testCanDetermineSiteNameAndTld() throws {
|
||||
XCTAssertEqual(
|
||||
"nginx-site",
|
||||
@ -36,32 +36,32 @@ class NginxConfigurationTest: XCTestCase {
|
||||
NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).tld
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func testCanDetermineIsolation() throws {
|
||||
XCTAssertNil(
|
||||
NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).isolatedVersion
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(
|
||||
"8.1",
|
||||
NginxConfiguration(filePath: NginxConfigurationTest.isolatedUrl.path).isolatedVersion
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func testCanDetermineProxy() throws {
|
||||
let proxied = NginxConfiguration(filePath: NginxConfigurationTest.proxyUrl.path)
|
||||
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
|
||||
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
||||
|
||||
|
||||
let normal = NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path)
|
||||
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
|
||||
XCTAssertEqual(nil, normal.proxy)
|
||||
}
|
||||
|
||||
|
||||
func testCanDetermineSecuredProxy() throws {
|
||||
let proxied = NginxConfiguration(filePath: NginxConfigurationTest.secureProxyUrl.path)
|
||||
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
|
||||
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,24 +9,24 @@
|
||||
import XCTest
|
||||
|
||||
class PhpExtensionTest: XCTestCase {
|
||||
|
||||
|
||||
static var phpIniFileUrl: URL {
|
||||
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
|
||||
}
|
||||
|
||||
func testCanLoadExtension() throws {
|
||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||
|
||||
|
||||
XCTAssertGreaterThan(extensions.count, 0)
|
||||
}
|
||||
|
||||
|
||||
func testExtensionNameIsCorrect() throws {
|
||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||
|
||||
|
||||
let extensionNames = extensions.map { (ext) -> String in
|
||||
return ext.name
|
||||
}
|
||||
|
||||
|
||||
// These 6 should be found
|
||||
XCTAssertTrue(extensionNames.contains("xdebug"))
|
||||
XCTAssertTrue(extensionNames.contains("imagick"))
|
||||
@ -34,41 +34,41 @@ class PhpExtensionTest: XCTestCase {
|
||||
XCTAssertTrue(extensionNames.contains("opcache"))
|
||||
XCTAssertTrue(extensionNames.contains("yaml"))
|
||||
XCTAssertTrue(extensionNames.contains("custom"))
|
||||
|
||||
|
||||
XCTAssertFalse(extensionNames.contains("fake"))
|
||||
XCTAssertFalse(extensionNames.contains("nice"))
|
||||
}
|
||||
|
||||
|
||||
func testExtensionStatusIsCorrect() throws {
|
||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||
|
||||
|
||||
// xdebug should be enabled
|
||||
XCTAssertEqual(extensions[0].enabled, true)
|
||||
|
||||
|
||||
// imagick should be disabled
|
||||
XCTAssertEqual(extensions[1].enabled, false)
|
||||
}
|
||||
|
||||
|
||||
func testToggleWorksAsExpected() throws {
|
||||
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
||||
let extensions = PhpExtension.load(from: destination)
|
||||
XCTAssertEqual(extensions.count, 6)
|
||||
|
||||
|
||||
// Try to disable xdebug (should be detected first)!
|
||||
let xdebug = extensions.first!
|
||||
XCTAssertTrue(xdebug.name == "xdebug")
|
||||
XCTAssertEqual(xdebug.enabled, true)
|
||||
xdebug.toggle()
|
||||
XCTAssertEqual(xdebug.enabled, false)
|
||||
|
||||
|
||||
// Check if the file contains the appropriate data
|
||||
let file = try! String(contentsOf: destination, encoding: .utf8)
|
||||
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
|
||||
|
||||
|
||||
// Make sure if we load the data again, it's disabled
|
||||
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
|
||||
}
|
||||
|
||||
|
||||
func testCanRetrieveXdebugMode() throws {
|
||||
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
|
||||
XCTAssertEqual(value, "coverage")
|
||||
|
@ -9,14 +9,14 @@
|
||||
import XCTest
|
||||
|
||||
class ValetConfigurationTest: XCTestCase {
|
||||
|
||||
|
||||
static var jsonConfigFileUrl: URL {
|
||||
return Bundle(for: Self.self).url(
|
||||
forResource: "valet-config",
|
||||
withExtension: "json"
|
||||
)!
|
||||
}
|
||||
|
||||
|
||||
func testCanLoadConfigFile() throws {
|
||||
let json = try? String(
|
||||
contentsOf: Self.jsonConfigFileUrl,
|
||||
@ -26,7 +26,7 @@ class ValetConfigurationTest: XCTestCase {
|
||||
Valet.Configuration.self,
|
||||
from: json!.data(using: .utf8)!
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(config.tld, "test")
|
||||
XCTAssertEqual(config.paths, [
|
||||
"/Users/username/.config/valet/Sites",
|
||||
@ -35,5 +35,5 @@ class ValetConfigurationTest: XCTestCase {
|
||||
XCTAssertEqual(config.defaultSite, "/Users/username/default-site")
|
||||
XCTAssertEqual(config.loopback, "127.0.0.1")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,12 +9,12 @@
|
||||
import Foundation
|
||||
|
||||
class Utility {
|
||||
|
||||
|
||||
public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? {
|
||||
if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) {
|
||||
let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)")
|
||||
|
||||
|
||||
do {
|
||||
try FileManager.default.copyItem(at: bundleURL, to: targetURL)
|
||||
return targetURL
|
||||
@ -22,7 +22,7 @@ class Utility {
|
||||
Log.err("Unable to copy file: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ class PhpVersionDetectionTest: XCTestCase {
|
||||
"php@5.6",
|
||||
"php@5.4" // should be omitted, not supported
|
||||
], checkBinaries: false, generateHelpers: false)
|
||||
|
||||
|
||||
XCTAssertEqual(outcome, ["8.0", "7.0"])
|
||||
}
|
||||
}
|
||||
|
@ -36,13 +36,13 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
nil
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func testPhpVersionNumberParse() throws {
|
||||
XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
|
||||
XCTAssertTrue(error is VersionParseError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testCanCheckFixedConstraints() throws {
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
@ -51,7 +51,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0"]).all
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"])
|
||||
@ -59,7 +59,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0.3"]).all
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
@ -67,7 +67,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0"]).all
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
@ -76,7 +76,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
.make(from: []).all
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func testCanCheckCaretConstraints() throws {
|
||||
// 1. Imprecise checks
|
||||
XCTAssertEqual(
|
||||
@ -86,7 +86,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
|
||||
// 2. Imprecise check with precise constraint (lenient AKA not strict)
|
||||
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
|
||||
XCTAssertEqual(
|
||||
@ -96,7 +96,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
|
||||
// 3. Imprecise check with precise constraint (strict mode)
|
||||
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
|
||||
XCTAssertEqual(
|
||||
@ -106,7 +106,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
|
||||
)
|
||||
|
||||
|
||||
// 4. Precise members and constraint all around
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
@ -115,7 +115,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
|
||||
|
||||
// 5. Precise members but imprecise constraint (strict mode)
|
||||
// In strict mode the constraint's patch version is assumed to be 0
|
||||
XCTAssertEqual(
|
||||
@ -125,7 +125,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
|
||||
|
||||
// 6. Precise members but imprecise constraint (lenient mode)
|
||||
// In lenient mode the constraint's patch version is assumed to be equal
|
||||
XCTAssertEqual(
|
||||
@ -136,7 +136,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func testCanCheckTildeConstraints() throws {
|
||||
// 1. Imprecise checks
|
||||
XCTAssertEqual(
|
||||
@ -146,7 +146,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
|
||||
// 2. Imprecise check with precise constraint (lenient AKA not strict)
|
||||
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
|
||||
XCTAssertEqual(
|
||||
@ -159,7 +159,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0"]).all
|
||||
)
|
||||
|
||||
|
||||
// 3. Imprecise check with precise constraint (strict mode)
|
||||
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
|
||||
XCTAssertEqual(
|
||||
@ -172,7 +172,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: []).all
|
||||
)
|
||||
|
||||
|
||||
// 4. Precise members and constraint all around
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
@ -183,7 +183,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0.10"]).all
|
||||
)
|
||||
|
||||
|
||||
// 5. Precise members but imprecise constraint (strict mode)
|
||||
// In strict mode the constraint's patch version is assumed to be 0.
|
||||
XCTAssertEqual(
|
||||
@ -193,7 +193,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
|
||||
|
||||
// 6. Precise members but imprecise constraint (lenient mode)
|
||||
// In lenient mode the constraint's patch version is assumed to be equal.
|
||||
// (Strictness does not make any difference here, but both should be tested.)
|
||||
@ -205,7 +205,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func testCanCheckGreaterThanOrEqualConstraints() throws {
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
@ -214,7 +214,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
@ -222,7 +222,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
|
||||
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
@ -231,7 +231,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3"]).all
|
||||
)
|
||||
|
||||
|
||||
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
@ -241,7 +241,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
.make(from: ["7.4", "7.3", "7.2"]).all
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func testCanCheckGreaterThanConstraints() throws {
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
@ -250,7 +250,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
@ -259,7 +259,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2"]).all
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
@ -268,7 +268,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3"]).all
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
|
||||
@ -277,7 +277,7 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.3.1", "7.2.9", "7.2"]).all
|
||||
)
|
||||
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
|
||||
|
@ -14,5 +14,5 @@ class ValetVersionExtractorTest: XCTestCase {
|
||||
let version = valet("--version", sudo: false)
|
||||
XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3"))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -14,12 +14,12 @@ class VersionExtractorTest: XCTestCase {
|
||||
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
|
||||
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
|
||||
}
|
||||
|
||||
|
||||
func testVersionComparison() {
|
||||
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
|
||||
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
|
||||
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)
|
||||
XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,42 +9,37 @@ import Foundation
|
||||
import AppKit
|
||||
|
||||
class Actions {
|
||||
|
||||
|
||||
// MARK: - Services
|
||||
|
||||
public static func restartPhpFpm()
|
||||
{
|
||||
|
||||
public static func restartPhpFpm() {
|
||||
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
|
||||
}
|
||||
|
||||
public static func restartNginx()
|
||||
{
|
||||
|
||||
public static func restartNginx() {
|
||||
brew("services restart nginx", sudo: true)
|
||||
}
|
||||
|
||||
public static func restartDnsMasq()
|
||||
{
|
||||
|
||||
public static func restartDnsMasq() {
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
}
|
||||
|
||||
public static func stopAllServices()
|
||||
{
|
||||
|
||||
public static func stopAllServices() {
|
||||
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
|
||||
brew("services stop nginx", sudo: true)
|
||||
brew("services stop dnsmasq", sudo: true)
|
||||
}
|
||||
|
||||
public static func fixHomebrewPermissions() throws
|
||||
{
|
||||
|
||||
public static func fixHomebrewPermissions() throws {
|
||||
var servicesCommands = [
|
||||
"\(Paths.brew) services stop nginx",
|
||||
"\(Paths.brew) services stop dnsmasq",
|
||||
"\(Paths.brew) services stop dnsmasq"
|
||||
]
|
||||
var cellarCommands = [
|
||||
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
|
||||
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq"
|
||||
]
|
||||
|
||||
|
||||
PhpEnv.shared.availablePhpVersions.forEach { version in
|
||||
let formula = version == PhpEnv.brewPhpVersion
|
||||
? "php"
|
||||
@ -52,66 +47,61 @@ class Actions {
|
||||
servicesCommands.append("\(Paths.brew) services stop \(formula)")
|
||||
cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
|
||||
}
|
||||
|
||||
|
||||
let script =
|
||||
servicesCommands.joined(separator: " && ")
|
||||
+ " && "
|
||||
+ cellarCommands.joined(separator: " && ")
|
||||
|
||||
|
||||
let appleScript = NSAppleScript(
|
||||
source: "do shell script \"\(script)\" with administrator privileges"
|
||||
)
|
||||
|
||||
|
||||
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
|
||||
|
||||
if (eventResult == nil) {
|
||||
|
||||
if eventResult == nil {
|
||||
throw HomebrewPermissionError(kind: .applescriptNilError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Finding Config Files
|
||||
|
||||
public static func openGenericPhpConfigFolder()
|
||||
{
|
||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
|
||||
|
||||
public static func openGenericPhpConfigFolder() {
|
||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]
|
||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||
}
|
||||
|
||||
public static func openGlobalComposerFolder()
|
||||
{
|
||||
|
||||
public static func openGlobalComposerFolder() {
|
||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".composer/composer.json")
|
||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||
}
|
||||
|
||||
public static func openPhpConfigFolder(version: String)
|
||||
{
|
||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
|
||||
|
||||
public static func openPhpConfigFolder(version: String) {
|
||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
|
||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||
}
|
||||
|
||||
public static func openValetConfigFolder()
|
||||
{
|
||||
|
||||
public static func openValetConfigFolder() {
|
||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".config/valet")
|
||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||
}
|
||||
|
||||
// MARK: - Other Actions
|
||||
|
||||
public static func createTempPhpInfoFile() -> URL
|
||||
{
|
||||
|
||||
public static func createTempPhpInfoFile() -> URL {
|
||||
// Write a file called `phpmon_phpinfo.php` to /tmp
|
||||
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
||||
|
||||
|
||||
// Tell php-cgi to run the PHP and output as an .html file
|
||||
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
||||
|
||||
|
||||
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Fix My Valet
|
||||
|
||||
|
||||
/**
|
||||
Detects all currently available PHP versions,
|
||||
and unlinks each and every one of them.
|
||||
@ -124,8 +114,7 @@ class Actions {
|
||||
If this does not solve the issue, the user may need to install additional
|
||||
extensions and/or run `composer global update`.
|
||||
*/
|
||||
public static func fixMyValet(completed: @escaping () -> Void)
|
||||
{
|
||||
public static func fixMyValet(completed: @escaping () -> Void) {
|
||||
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
brew("services restart php", sudo: true)
|
||||
|
@ -8,7 +8,7 @@
|
||||
import Cocoa
|
||||
|
||||
public class Command {
|
||||
|
||||
|
||||
/**
|
||||
Immediately executes a command.
|
||||
|
||||
@ -20,21 +20,21 @@ public class Command {
|
||||
let task = Process()
|
||||
task.launchPath = path
|
||||
task.arguments = arguments
|
||||
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.launch()
|
||||
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
|
||||
|
||||
if (trimNewlines) {
|
||||
|
||||
if trimNewlines {
|
||||
return output.components(separatedBy: .newlines)
|
||||
.filter({ !$0.isEmpty })
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,13 +8,13 @@
|
||||
import Cocoa
|
||||
|
||||
struct Constants {
|
||||
|
||||
|
||||
/**
|
||||
* The latest PHP version that is considered to be stable at the time of release.
|
||||
* This version number is currently not used (only as a default fallback).
|
||||
*/
|
||||
static let LatestStablePhpVersion = "8.1"
|
||||
|
||||
|
||||
/**
|
||||
The minimum version of Valet that is recommended.
|
||||
If the installed version is older, a notification will be shown
|
||||
@ -24,7 +24,7 @@ struct Constants {
|
||||
See also: https://github.com/laravel/valet/releases/tag/v2.16.2
|
||||
*/
|
||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||
|
||||
|
||||
/**
|
||||
* The PHP versions supported by this application.
|
||||
* Versions that do not appear in this array are omitted from the list.
|
||||
@ -42,7 +42,7 @@ struct Constants {
|
||||
"7.4",
|
||||
"8.0",
|
||||
"8.1",
|
||||
|
||||
|
||||
// ====================
|
||||
// EXPERIMENTAL SUPPORT
|
||||
// ====================
|
||||
@ -50,9 +50,9 @@ struct Constants {
|
||||
// dev release. In this case, that means that the version below is detected.
|
||||
"8.2"
|
||||
]
|
||||
|
||||
|
||||
struct Urls {
|
||||
|
||||
|
||||
static let DonationPayment = URL(
|
||||
string: "https://nicoverbruggen.be/sponsor#pay-now"
|
||||
)!
|
||||
@ -62,7 +62,7 @@ struct Constants {
|
||||
static let FrequentlyAskedQuestions = URL(
|
||||
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
|
||||
)!
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
class Events {
|
||||
|
||||
|
||||
static let ServicesUpdated = Notification.Name("ServicesUpdated")
|
||||
|
||||
|
||||
}
|
||||
|
@ -11,28 +11,25 @@
|
||||
/**
|
||||
Runs a `valet` command. Defaults to running as superuser.
|
||||
*/
|
||||
func valet(_ command: String, sudo: Bool = true) -> String
|
||||
{
|
||||
func valet(_ command: String, sudo: Bool = true) -> String {
|
||||
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
|
||||
}
|
||||
|
||||
/**
|
||||
Runs a `brew` command. Can run as superuser.
|
||||
*/
|
||||
func brew(_ command: String, sudo: Bool = false)
|
||||
{
|
||||
func brew(_ command: String, sudo: Bool = false) {
|
||||
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
|
||||
}
|
||||
|
||||
/**
|
||||
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
|
||||
*/
|
||||
func sed(file: String, original: String, replacement: String)
|
||||
{
|
||||
func sed(file: String, original: String, replacement: String) {
|
||||
// Escape slashes (or `sed` won't work)
|
||||
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
|
||||
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
|
||||
|
||||
|
||||
// Check if gsed exists; it is able to follow symlinks,
|
||||
// which we want to do to toggle the extension
|
||||
if Filesystem.fileExists("\(Paths.binPath)/gsed") {
|
||||
@ -45,8 +42,7 @@ func sed(file: String, original: String, replacement: String)
|
||||
/**
|
||||
Uses `grep` to determine whether a particular query string can be found in a particular file.
|
||||
*/
|
||||
func grepContains(file: String, query: String) -> Bool
|
||||
{
|
||||
func grepContains(file: String, query: String) -> Bool {
|
||||
return Shell.pipe("""
|
||||
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
|
||||
""")
|
||||
|
@ -9,50 +9,50 @@
|
||||
import Foundation
|
||||
|
||||
class Log {
|
||||
|
||||
|
||||
static var shared = Log()
|
||||
|
||||
|
||||
enum Verbosity: Int {
|
||||
case error = 1,
|
||||
warning = 2,
|
||||
info = 3,
|
||||
performance = 4
|
||||
|
||||
|
||||
public func isApplicable() -> Bool {
|
||||
return Log.shared.verbosity.rawValue >= self.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var verbosity: Verbosity = .warning
|
||||
|
||||
|
||||
static func err(_ item: Any) {
|
||||
if Verbosity.error.isApplicable() {
|
||||
print("[E] \(item)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func warn(_ item: Any) {
|
||||
if Verbosity.warning.isApplicable() {
|
||||
print("[W] \(item)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func info(_ item: Any) {
|
||||
if Verbosity.info.isApplicable() {
|
||||
print("\(item)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func perf(_ item: Any) {
|
||||
if Verbosity.performance.isApplicable() {
|
||||
print("[P] \(item)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func separator(as verbosity: Verbosity = .info) {
|
||||
if verbosity.isApplicable() {
|
||||
print("==================================")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,71 +12,71 @@ import Foundation
|
||||
The path to the Homebrew directory and the user's name are fetched only once, at boot.
|
||||
*/
|
||||
public class Paths {
|
||||
|
||||
|
||||
public static let shared = Paths()
|
||||
|
||||
|
||||
internal var baseDir: Paths.HomebrewDir
|
||||
|
||||
|
||||
private var userName: String
|
||||
|
||||
|
||||
init() {
|
||||
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
||||
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
|
||||
}
|
||||
|
||||
|
||||
public func detectBinaryPaths() {
|
||||
detectComposerBinary()
|
||||
}
|
||||
|
||||
// - MARK: Binaries
|
||||
|
||||
|
||||
public static var valet: String {
|
||||
return "\(binPath)/valet"
|
||||
}
|
||||
|
||||
|
||||
public static var brew: String {
|
||||
return "\(binPath)/brew"
|
||||
}
|
||||
|
||||
|
||||
public static var php: String {
|
||||
return "\(binPath)/php"
|
||||
}
|
||||
|
||||
|
||||
public static var phpConfig: String {
|
||||
return "\(binPath)/php-config"
|
||||
}
|
||||
|
||||
|
||||
// - MARK: Detected Binaries
|
||||
|
||||
|
||||
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
|
||||
public static var composer: String? = nil
|
||||
|
||||
public static var composer: String?
|
||||
|
||||
// - MARK: Paths
|
||||
|
||||
|
||||
public static var whoami: String {
|
||||
return shared.userName
|
||||
}
|
||||
|
||||
|
||||
public static var cellarPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Cellar"
|
||||
}
|
||||
|
||||
|
||||
public static var binPath: String {
|
||||
return "\(shared.baseDir.rawValue)/bin"
|
||||
}
|
||||
|
||||
|
||||
public static var optPath: String {
|
||||
return "\(shared.baseDir.rawValue)/opt"
|
||||
}
|
||||
|
||||
|
||||
public static var etcPath: String {
|
||||
return "\(shared.baseDir.rawValue)/etc"
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
|
||||
|
||||
private func detectComposerBinary() {
|
||||
if Filesystem.fileExists("/usr/local/bin/composer") {
|
||||
Paths.composer = "/usr/local/bin/composer"
|
||||
@ -87,12 +87,12 @@ public class Paths {
|
||||
Log.warn("Composer was not found.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Enum
|
||||
|
||||
|
||||
public enum HomebrewDir: String {
|
||||
case opt = "/opt/homebrew"
|
||||
case usr = "/usr/local"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
extension Process {
|
||||
|
||||
|
||||
/**
|
||||
When a process is running in the background, it can send content to standard
|
||||
output or standard error, just like it would in a terminal. Using `listen`
|
||||
@ -22,10 +22,10 @@ extension Process {
|
||||
) {
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
|
||||
self.standardOutput = outputPipe
|
||||
self.standardError = errorPipe
|
||||
|
||||
|
||||
[
|
||||
(outputPipe, didReceiveStandardOutputData),
|
||||
(errorPipe, didReceiveStandardErrorData)
|
||||
@ -35,15 +35,18 @@ extension Process {
|
||||
forName: NSNotification.Name.NSFileHandleDataAvailable,
|
||||
object: pipe.fileHandleForReading,
|
||||
queue: nil
|
||||
) { notification in
|
||||
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
|
||||
) { _ in
|
||||
if let outputString = String(
|
||||
data: pipe.fileHandleForReading.availableData,
|
||||
encoding: String.Encoding.utf8
|
||||
) {
|
||||
callback(outputString)
|
||||
}
|
||||
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
After the process is done running, you'll want to stop listening.
|
||||
*/
|
||||
@ -55,5 +58,5 @@ extension Process {
|
||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,35 +8,35 @@
|
||||
import Cocoa
|
||||
|
||||
public class Shell {
|
||||
|
||||
|
||||
// MARK: - Invoke static functions
|
||||
|
||||
|
||||
public static func run(
|
||||
_ command: String,
|
||||
requiresPath: Bool = false
|
||||
) {
|
||||
Shell.user.run(command, requiresPath: requiresPath)
|
||||
}
|
||||
|
||||
|
||||
public static func pipe(
|
||||
_ command: String,
|
||||
requiresPath: Bool = false
|
||||
) -> String {
|
||||
return Shell.user.pipe(command, requiresPath: requiresPath)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
|
||||
/**
|
||||
We now require macOS 11, so no need to detect which terminal to use.
|
||||
*/
|
||||
public var shell: String = "/bin/sh"
|
||||
|
||||
|
||||
/**
|
||||
Singleton to access a user shell (with --login)
|
||||
*/
|
||||
public static let user = Shell()
|
||||
|
||||
|
||||
/**
|
||||
Runs a shell command without using the output.
|
||||
Uses the default shell.
|
||||
@ -51,7 +51,7 @@ public class Shell {
|
||||
// Equivalent of piping to /dev/null; don't do anything with the string
|
||||
_ = Shell.pipe(command, requiresPath: requiresPath)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Runs a shell command and returns the output.
|
||||
|
||||
@ -69,7 +69,7 @@ public class Shell {
|
||||
)
|
||||
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Runs the command and returns a `ShellOutput` object, which contains info about the process.
|
||||
|
||||
@ -81,16 +81,16 @@ public class Shell {
|
||||
_ command: String,
|
||||
requiresPath: Bool = false
|
||||
) -> Shell.Output {
|
||||
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
|
||||
let task = self.createTask(for: command, requiresPath: requiresPath)
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
|
||||
return Shell.Output(
|
||||
standardOutput: String(
|
||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
@ -103,7 +103,7 @@ public class Shell {
|
||||
task: task
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Creates a new process with the correct PATH and shell.
|
||||
*/
|
||||
@ -111,19 +111,19 @@ public class Shell {
|
||||
let tailoredCommand = requiresPath
|
||||
? "export PATH=\(Paths.binPath):$PATH && \(command)"
|
||||
: command
|
||||
|
||||
|
||||
let task = Process()
|
||||
task.launchPath = self.shell
|
||||
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
|
||||
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
|
||||
public class Output {
|
||||
public let standardOutput: String
|
||||
public let errorOutput: String
|
||||
public let task: Process
|
||||
|
||||
|
||||
init(standardOutput: String,
|
||||
errorOutput: String,
|
||||
task: Process) {
|
||||
|
@ -15,9 +15,9 @@ struct HomebrewPermissionError: Error, AlertableError {
|
||||
enum Kind: String {
|
||||
case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
|
||||
}
|
||||
|
||||
|
||||
let kind: Kind
|
||||
|
||||
|
||||
func getErrorMessageKey() -> String {
|
||||
return "alert.errors.\(self.kind.rawValue)"
|
||||
}
|
||||
|
@ -8,11 +8,11 @@
|
||||
import Cocoa
|
||||
|
||||
extension Date {
|
||||
|
||||
|
||||
func toString() -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return dateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,21 +9,21 @@
|
||||
import Cocoa
|
||||
|
||||
extension NSMenu {
|
||||
|
||||
|
||||
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
|
||||
newItem.keyEquivalentModifierMask = modifier
|
||||
self.addItem(newItem)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@IBDesignable class LocalizedMenuItem: NSMenuItem {
|
||||
|
||||
|
||||
@IBInspectable
|
||||
var localizationKey: String? {
|
||||
didSet {
|
||||
self.title = localizationKey?.localized ?? self.title
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,29 +10,29 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
extension NSWindow {
|
||||
|
||||
|
||||
/**
|
||||
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
|
||||
*/
|
||||
func shake(){
|
||||
func shake() {
|
||||
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
|
||||
|
||||
|
||||
let frame: CGRect = self.frame
|
||||
let shakeAnimation :CAKeyframeAnimation = CAKeyframeAnimation()
|
||||
|
||||
let shakeAnimation: CAKeyframeAnimation = CAKeyframeAnimation()
|
||||
|
||||
let shakePath = CGMutablePath()
|
||||
shakePath.move( to: CGPoint(x:NSMinX(frame), y:NSMinY(frame)))
|
||||
|
||||
shakePath.move( to: CGPoint(x: frame.minX, y: frame.minY))
|
||||
|
||||
for _ in 0...numberOfShakes-1 {
|
||||
shakePath.addLine(to: CGPoint(x:NSMinX(frame) - frame.size.width * vigourOfShake, y:NSMinY(frame)))
|
||||
shakePath.addLine(to: CGPoint(x:NSMinX(frame) + frame.size.width * vigourOfShake, y:NSMinY(frame)))
|
||||
shakePath.addLine(to: CGPoint(x: frame.minX - frame.size.width * vigourOfShake, y: frame.minY))
|
||||
shakePath.addLine(to: CGPoint(x: frame.minX + frame.size.width * vigourOfShake, y: frame.minY))
|
||||
}
|
||||
|
||||
|
||||
shakePath.closeSubpath()
|
||||
shakeAnimation.path = shakePath
|
||||
shakeAnimation.duration = durationOfShake
|
||||
|
||||
self.animations = ["frameOrigin":shakeAnimation]
|
||||
|
||||
self.animations = ["frameOrigin": shakeAnimation]
|
||||
self.animator().setFrameOrigin(self.frame.origin)
|
||||
}
|
||||
}
|
||||
|
@ -7,28 +7,28 @@
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
|
||||
var localized: String {
|
||||
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
|
||||
}
|
||||
|
||||
|
||||
func localized(_ args: CVarArg...) -> String {
|
||||
String(format: self.localized, arguments: args)
|
||||
}
|
||||
|
||||
|
||||
func countInstances(of stringToFind: String) -> Int {
|
||||
if (stringToFind.isEmpty) {
|
||||
if stringToFind.isEmpty {
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
var count = 0
|
||||
var searchRange: Range<String.Index>?
|
||||
|
||||
|
||||
while let foundRange = range(of: stringToFind, options: [], range: searchRange) {
|
||||
count += 1
|
||||
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
|
||||
}
|
||||
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ extension String {
|
||||
let end = r.upperBound
|
||||
return String(self[start ..< end])
|
||||
}
|
||||
|
||||
|
||||
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
|
||||
/*
|
||||
<1> We split the version by period (.).
|
||||
@ -50,12 +50,12 @@ extension String {
|
||||
*/
|
||||
func versionCompare(_ otherVersion: String) -> ComparisonResult {
|
||||
let versionDelimiter = "."
|
||||
|
||||
|
||||
var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
|
||||
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
|
||||
|
||||
|
||||
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
|
||||
|
||||
|
||||
if zeroDiff == 0 { // <3>
|
||||
// Same format, compare normally
|
||||
return self.compare(otherVersion, options: .numeric)
|
||||
@ -70,5 +70,5 @@ extension String {
|
||||
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,25 +12,25 @@ import Cocoa
|
||||
// Adapted from: https://stackoverflow.com/a/46268778
|
||||
|
||||
protocol XibLoadable {
|
||||
|
||||
|
||||
static var xibName: String? { get }
|
||||
static func createFromXib(in bundle: Bundle) -> Self?
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension XibLoadable where Self: NSView {
|
||||
|
||||
|
||||
static var xibName: String? {
|
||||
return String(describing: Self.self)
|
||||
}
|
||||
|
||||
|
||||
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
|
||||
guard let xibName = xibName else { return nil }
|
||||
var topLevelArray: NSArray? = nil
|
||||
var topLevelArray: NSArray?
|
||||
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
|
||||
guard let results = topLevelArray else { return nil }
|
||||
let views = Array<Any>(results).filter { $0 is Self }
|
||||
let views = [Any](results).filter { $0 is Self }
|
||||
return views.last as? Self
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
import Cocoa
|
||||
|
||||
class Alert {
|
||||
|
||||
|
||||
public static func confirm(
|
||||
onWindow window: NSWindow,
|
||||
messageText: String,
|
||||
@ -21,13 +21,13 @@ class Alert {
|
||||
if !Thread.isMainThread {
|
||||
fatalError("You should always present alerts on the main thread!")
|
||||
}
|
||||
|
||||
|
||||
let alert = NSAlert.init()
|
||||
alert.alertStyle = style
|
||||
alert.messageText = messageText
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: buttonTitle)
|
||||
if (!secondButtonTitle.isEmpty) {
|
||||
if !secondButtonTitle.isEmpty {
|
||||
alert.addButton(withTitle: secondButtonTitle)
|
||||
}
|
||||
alert.beginSheetModal(for: window) { response in
|
||||
@ -36,5 +36,5 @@ class Alert {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,23 +12,23 @@ import Foundation
|
||||
/// In most cases this is going to be a code editor, but it could also be another application
|
||||
/// that supports opening those directories, like a visual Git client or a terminal app.
|
||||
class Application {
|
||||
|
||||
|
||||
enum AppType {
|
||||
case editor, browser, git_gui, terminal, user_supplied
|
||||
}
|
||||
|
||||
|
||||
/// Name of the app. Used for display purposes and to determine `name.app` exists.
|
||||
let name: String
|
||||
|
||||
|
||||
/// Application type. Depending on the type, a different action might occur.
|
||||
let type: AppType
|
||||
|
||||
|
||||
/// Initializer. Used to detect a specific app of a specific type.
|
||||
init(_ name: String, _ type: AppType) {
|
||||
self.name = name
|
||||
self.type = type
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Attempt to open a specific directory in the app of choice.
|
||||
(This will open the app if it isn't open yet.)
|
||||
@ -36,7 +36,7 @@ class Application {
|
||||
@objc public func openDirectory(file: String) {
|
||||
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
|
||||
}
|
||||
|
||||
|
||||
/** Checks if the app is installed. */
|
||||
func isInstalled() -> Bool {
|
||||
// If this script does not complain, the app exists!
|
||||
@ -45,7 +45,7 @@ class Application {
|
||||
requiresPath: false
|
||||
).task.terminationStatus == 0
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Detect which apps are available to open a specific directory.
|
||||
*/
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Cocoa
|
||||
|
||||
class Filesystem {
|
||||
|
||||
|
||||
/**
|
||||
Checks if a file exists at the provided path.
|
||||
Uses `FileManager`.
|
||||
@ -19,5 +19,5 @@ class Filesystem {
|
||||
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,19 +9,19 @@ import Foundation
|
||||
import UserNotifications
|
||||
|
||||
class LocalNotification {
|
||||
|
||||
|
||||
public static func send(title: String, subtitle: String) {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.body = subtitle
|
||||
|
||||
|
||||
let uuidString = UUID().uuidString
|
||||
let request = UNNotificationRequest(
|
||||
identifier: uuidString,
|
||||
content: content,
|
||||
trigger: nil
|
||||
)
|
||||
|
||||
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.add(request) { (error) in
|
||||
if error != nil {
|
||||
@ -29,5 +29,5 @@ class LocalNotification {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,40 +8,40 @@
|
||||
import Cocoa
|
||||
|
||||
class MenuBarImageGenerator {
|
||||
|
||||
|
||||
/**
|
||||
Takes a string and converts it to an image that can be displayed in the menu bar.
|
||||
The width of the NSImage depends on the length of the text.
|
||||
*/
|
||||
public static func textToImage(text: String) -> NSImage {
|
||||
|
||||
|
||||
let font = NSFont.systemFont(ofSize: 14, weight: .medium)
|
||||
|
||||
|
||||
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
|
||||
let textFontAttributes = [
|
||||
NSAttributedString.Key.font: font,
|
||||
NSAttributedString.Key.foregroundColor: NSColor.black,
|
||||
NSAttributedString.Key.paragraphStyle: textStyle
|
||||
]
|
||||
|
||||
let padding : CGFloat = 2.0;
|
||||
|
||||
|
||||
let padding: CGFloat = 2.0
|
||||
|
||||
// Create an attributed string so we'll know how wide the item will need to be
|
||||
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
|
||||
let textSize = attributedString.size()
|
||||
|
||||
|
||||
// Add padding to the width of the menu bar item
|
||||
let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height)
|
||||
let image = NSImage(size: size)
|
||||
|
||||
|
||||
// Set the image rect with the appropriate dimensions
|
||||
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
|
||||
|
||||
|
||||
// Position the text inside the image rect
|
||||
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
|
||||
|
||||
|
||||
let targetImage: NSImage = NSImage(size: image.size)
|
||||
|
||||
|
||||
let representation: NSBitmapImageRep = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil,
|
||||
pixelsWide: Int(image.size.width),
|
||||
@ -54,40 +54,40 @@ class MenuBarImageGenerator {
|
||||
bytesPerRow: 0,
|
||||
bitsPerPixel: 0
|
||||
)!
|
||||
|
||||
|
||||
targetImage.addRepresentation(representation)
|
||||
targetImage.lockFocus()
|
||||
|
||||
image.draw(in: imageRect)
|
||||
text.draw(in: textRect, withAttributes: textFontAttributes)
|
||||
|
||||
|
||||
targetImage.unlockFocus()
|
||||
return targetImage
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The same as before, but also attempts to add an icon to the left.
|
||||
*/
|
||||
public static func textToImageWithIcon(text: String) -> NSImage {
|
||||
|
||||
|
||||
// We'll start out with the image containing the text
|
||||
let textImage = self.textToImage(text: text)
|
||||
|
||||
|
||||
// Then we'll fetch the image we want on the left
|
||||
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
||||
if iconType == nil {
|
||||
Log.warn("Invalid icon type found, using the default")
|
||||
iconType = MenuBarIcon.iconPhp.rawValue
|
||||
}
|
||||
|
||||
|
||||
let iconImage = NSImage(named: "MenuBar_\(iconType!)")!
|
||||
|
||||
|
||||
// We'll need to reference the width of the icon a bunch of times
|
||||
let iconWidthSize = iconImage.size.width
|
||||
|
||||
|
||||
// There will also be an additional divider between the image and the text (image)
|
||||
let divider: CGFloat = 3
|
||||
|
||||
|
||||
// Use a fixed size for the height of the menu bar (18pt)
|
||||
let imageRect = CGRect(
|
||||
x: 0,
|
||||
@ -95,14 +95,14 @@ class MenuBarImageGenerator {
|
||||
width: textImage.size.width + iconWidthSize + divider,
|
||||
height: 18
|
||||
)
|
||||
|
||||
|
||||
// Create a new image, we'll draw the text and our icon in there
|
||||
let image: NSImage = NSImage(size: imageRect.size)
|
||||
image.lockFocus()
|
||||
|
||||
// Calculate the offset between the image and the text
|
||||
let offset = imageRect.size.width - textImage.size.width
|
||||
|
||||
|
||||
// Draw the text with a negative x offset (so there is room on the left for the icon)
|
||||
textImage.draw(
|
||||
in: imageRect,
|
||||
@ -115,7 +115,7 @@ class MenuBarImageGenerator {
|
||||
operation: .overlay,
|
||||
fraction: 1
|
||||
)
|
||||
|
||||
|
||||
// Draw the icon directly in the left of the imageRect (where we left space)
|
||||
iconImage.draw(
|
||||
in: imageRect,
|
||||
@ -128,11 +128,11 @@ class MenuBarImageGenerator {
|
||||
operation: .overlay,
|
||||
fraction: 1
|
||||
)
|
||||
|
||||
|
||||
// We're done with this image
|
||||
image.unlockFocus()
|
||||
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -15,32 +15,32 @@ import Cocoa
|
||||
- Note: This class does make a simple assumption: each window controller corresponds to a single view.
|
||||
*/
|
||||
class PMWindowController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
|
||||
public var windowName: String {
|
||||
fatalError("Please specify a window name")
|
||||
}
|
||||
|
||||
|
||||
override func showWindow(_ sender: Any?) {
|
||||
super.showWindow(sender)
|
||||
App.shared.register(window: windowName)
|
||||
}
|
||||
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
App.shared.remove(window: windowName)
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
Log.perf("Window controller '\(windowName)' was deinitialized")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension NSWindowController {
|
||||
|
||||
|
||||
public func positionWindowInTopLeftCorner() {
|
||||
guard let frame = NSScreen.main?.frame else { return }
|
||||
guard let window = self.window else { return }
|
||||
|
||||
|
||||
window.setFrame(NSRect(
|
||||
x: frame.size.width - window.frame.size.width - 20,
|
||||
y: frame.size.height - window.frame.size.height - 40,
|
||||
@ -48,5 +48,5 @@ extension NSWindowController {
|
||||
height: window.frame.height
|
||||
), display: true)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
class VersionExtractor {
|
||||
|
||||
|
||||
/**
|
||||
This attempts to extract the version number from any given string.
|
||||
*/
|
||||
@ -19,26 +19,26 @@ class VersionExtractor {
|
||||
pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
|
||||
options: []
|
||||
)
|
||||
|
||||
|
||||
let match = regex.matches(
|
||||
in: string,
|
||||
options: [],
|
||||
range: NSMakeRange(0, string.count)
|
||||
range: NSRange(location: 0, length: string.count)
|
||||
).first
|
||||
|
||||
|
||||
guard let match = match else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let range = Range(
|
||||
match.range(withName: "version"),
|
||||
in: string
|
||||
)!
|
||||
|
||||
|
||||
return String(string[range])
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -21,78 +21,78 @@ class ActivePhpInstallation {
|
||||
var version: Version!
|
||||
var limits: Limits!
|
||||
var extensions: [PhpExtension]!
|
||||
|
||||
|
||||
// MARK: - Computed
|
||||
|
||||
|
||||
var formula: String {
|
||||
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init() {
|
||||
// Show information about the current version
|
||||
getVersion()
|
||||
|
||||
|
||||
// If an error occurred, exit early
|
||||
if (version.error) {
|
||||
if version.error {
|
||||
limits = Limits()
|
||||
extensions = []
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Load extension information
|
||||
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||
extensions = PhpExtension.load(from: path)
|
||||
|
||||
|
||||
// Get configuration values
|
||||
limits = Limits(
|
||||
memory_limit: getByteCount(key: "memory_limit"),
|
||||
upload_max_filesize: getByteCount(key: "upload_max_filesize"),
|
||||
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();"])
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.split(separator: ",")
|
||||
.map { String($0) }
|
||||
|
||||
|
||||
// See if any extensions are present in said .ini files
|
||||
paths.forEach { (iniFilePath) in
|
||||
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
||||
if exts.count > 0 {
|
||||
extensions.append(contentsOf: exts)
|
||||
let loadedExtensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
||||
if loadedExtensions.isEmpty {
|
||||
extensions.append(contentsOf: loadedExtensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
|
||||
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
|
||||
*/
|
||||
private func getVersion() -> Void {
|
||||
private func getVersion() {
|
||||
self.version = Version()
|
||||
|
||||
|
||||
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
|
||||
|
||||
if (version == "" || version.contains("Warning") || version.contains("Error")) {
|
||||
|
||||
if version == "" || version.contains("Warning") || version.contains("Error") {
|
||||
self.version.short = "💩 BROKEN"
|
||||
self.version.long = ""
|
||||
self.version.error = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// That's the long version
|
||||
self.version.long = version
|
||||
|
||||
|
||||
// Next up, let's strip away the minor version number
|
||||
let segments = self.version.long.components(separatedBy: ".")
|
||||
|
||||
|
||||
// Get the first two elements
|
||||
self.version.short = segments[0...1].joined(separator: ".")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Retrieves the display value for a specific key in the `.ini` file.
|
||||
|
||||
@ -110,18 +110,18 @@ class ActivePhpInstallation {
|
||||
*/
|
||||
private func getByteCount(key: String) -> String {
|
||||
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
|
||||
|
||||
|
||||
// Check if the value is unlimited
|
||||
if (value == "-1") {
|
||||
if value == "-1" {
|
||||
return "∞"
|
||||
}
|
||||
|
||||
|
||||
// Check if the syntax is valid otherwise
|
||||
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
|
||||
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
|
||||
let match = regex.matches(in: value, options: [], range: NSRange(location: 0, length: value.count)).first
|
||||
return (match == nil) ? "⚠️" : "\(value)B"
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Determine if PHP-FPM is configured correctly.
|
||||
|
||||
@ -135,11 +135,11 @@ class ActivePhpInstallation {
|
||||
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
|
||||
return Shell.pipe("cat \(fileName)").contains("valet.sock")
|
||||
}
|
||||
|
||||
|
||||
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
|
||||
return Filesystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Structs
|
||||
|
||||
/**
|
||||
@ -153,7 +153,7 @@ class ActivePhpInstallation {
|
||||
var long = "???"
|
||||
var error = false
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Struct containing information about the limits of the current PHP installation.
|
||||
Includes: memory limit, max upload size and max post size.
|
||||
@ -163,5 +163,5 @@ class ActivePhpInstallation {
|
||||
var upload_max_filesize = "???"
|
||||
var post_max_size = "???"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,15 +9,15 @@
|
||||
import Foundation
|
||||
|
||||
class Xdebug {
|
||||
|
||||
|
||||
public static var enabled: Bool {
|
||||
return !self.mode.isEmpty
|
||||
}
|
||||
|
||||
|
||||
public static var mode: String {
|
||||
return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
|
||||
}
|
||||
|
||||
|
||||
public static var modes: [String] {
|
||||
return [
|
||||
"off",
|
||||
@ -26,8 +26,8 @@ class Xdebug {
|
||||
"debug",
|
||||
"gcstats",
|
||||
"profile",
|
||||
"trace",
|
||||
"trace"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,18 +8,18 @@
|
||||
import Foundation
|
||||
|
||||
struct HomebrewPackage: Decodable {
|
||||
|
||||
|
||||
let name: String
|
||||
let full_name: String
|
||||
let aliases: [String]
|
||||
let installed: [HomebrewInstalled]
|
||||
let linked_keg: String?
|
||||
|
||||
|
||||
public var version: String {
|
||||
return aliases.first!
|
||||
.replacingOccurrences(of: "php@", with: "")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
struct HomebrewInstalled: Decodable {
|
||||
|
@ -18,7 +18,7 @@ struct HomebrewService: Decodable, Equatable {
|
||||
let status: String?
|
||||
let log_path: String?
|
||||
let error_log_path: String?
|
||||
|
||||
|
||||
public static func loadAll(
|
||||
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
|
||||
completion: @escaping ([HomebrewService]) -> Void
|
||||
@ -27,11 +27,11 @@ struct HomebrewService: Decodable, Equatable {
|
||||
let data = Shell
|
||||
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
|
||||
.data(using: .utf8)!
|
||||
|
||||
|
||||
let services = try! JSONDecoder()
|
||||
.decode([HomebrewService].self, from: data)
|
||||
.filter({ return filter.contains($0.name) })
|
||||
|
||||
|
||||
completion(services)
|
||||
}
|
||||
}
|
||||
|
@ -9,42 +9,42 @@
|
||||
import Foundation
|
||||
|
||||
class PhpEnv {
|
||||
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
|
||||
init() {
|
||||
self.currentInstall = ActivePhpInstallation()
|
||||
|
||||
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
|
||||
|
||||
|
||||
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json")
|
||||
|
||||
self.homebrewPackage = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self,
|
||||
from: brewPhpAlias.data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
|
||||
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
||||
/** The delegate that is informed of updates. */
|
||||
weak var delegate: PhpSwitcherDelegate?
|
||||
|
||||
/** The static app instance. Accessible at any time. */
|
||||
static let shared = PhpEnv()
|
||||
|
||||
|
||||
/** Whether the switcher is busy performing any actions. */
|
||||
var isBusy: Bool = false
|
||||
|
||||
|
||||
/** All available versions of PHP. */
|
||||
var availablePhpVersions: [String] = []
|
||||
|
||||
|
||||
/** Cached information about the PHP installations. */
|
||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||
|
||||
|
||||
/** Information about the currently linked PHP installation. */
|
||||
var currentInstall: ActivePhpInstallation
|
||||
|
||||
|
||||
/**
|
||||
The version that the `php` formula via Brew is aliased to on the current system.
|
||||
|
||||
@ -57,63 +57,62 @@ class PhpEnv {
|
||||
static var brewPhpVersion: String {
|
||||
return Self.shared.homebrewPackage.version
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
The currently linked and active PHP installation.
|
||||
*/
|
||||
static var phpInstall: ActivePhpInstallation {
|
||||
return Self.shared.currentInstall
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Information we were able to discern from the Homebrew info command.
|
||||
*/
|
||||
var homebrewPackage: HomebrewPackage! = nil
|
||||
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
|
||||
public static var switcher: PhpSwitcher {
|
||||
return InternalSwitcher()
|
||||
}
|
||||
|
||||
public static func detectPhpVersions() -> Void {
|
||||
|
||||
public static func detectPhpVersions() {
|
||||
_ = Self.shared.detectPhpVersions()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Detects which versions of PHP are installed.
|
||||
*/
|
||||
public func detectPhpVersions() -> [String]
|
||||
{
|
||||
public func detectPhpVersions() -> [String] {
|
||||
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
|
||||
|
||||
|
||||
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
|
||||
|
||||
|
||||
// Make sure the aliased version is detected
|
||||
// The user may have `php` installed, but not e.g. `php@8.0`
|
||||
// We should also detect that as a version that is installed
|
||||
let phpAlias = homebrewPackage.version
|
||||
|
||||
|
||||
// Avoid inserting a duplicate
|
||||
if (!versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php")) {
|
||||
if !versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||
versionsOnly.append(phpAlias)
|
||||
}
|
||||
|
||||
|
||||
Log.info("The PHP versions that were detected are: \(versionsOnly)")
|
||||
|
||||
|
||||
availablePhpVersions = versionsOnly
|
||||
|
||||
|
||||
var mappedVersions: [String: PhpInstallation] = [:]
|
||||
|
||||
|
||||
availablePhpVersions.forEach { version in
|
||||
mappedVersions[version] = PhpInstallation(version)
|
||||
}
|
||||
|
||||
|
||||
cachedPhpInstallations = mappedVersions
|
||||
|
||||
|
||||
return versionsOnly
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Extracts valid PHP versions from an array of strings.
|
||||
This array of strings is usually retrieved from `grep`.
|
||||
@ -126,14 +125,14 @@ class PhpEnv {
|
||||
checkBinaries: Bool = true,
|
||||
generateHelpers: Bool = true
|
||||
) -> [String] {
|
||||
var output : [String] = []
|
||||
|
||||
var output: [String] = []
|
||||
|
||||
var supported = Constants.SupportedPhpVersions
|
||||
|
||||
|
||||
if !Valet.enabled(feature: .supportForPhp56) {
|
||||
supported.removeAll { $0 == "5.6" }
|
||||
}
|
||||
|
||||
|
||||
versions.filter { (version) -> Bool in
|
||||
// Omit everything that doesn't start with php@
|
||||
// (e.g. something-php@8.0 won't be detected)
|
||||
@ -144,19 +143,18 @@ class PhpEnv {
|
||||
// is supported and where the binary exists (avoids broken installs)
|
||||
if !output.contains(version)
|
||||
&& supported.contains(version)
|
||||
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
|
||||
{
|
||||
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
|
||||
output.append(version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if generateHelpers {
|
||||
output.forEach { PhpHelper.generate(for: $0) }
|
||||
}
|
||||
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
|
||||
public func validVersions(for constraint: String) -> [PhpVersionNumber] {
|
||||
constraint.split(separator: "|").flatMap {
|
||||
return PhpVersionNumberCollection
|
||||
@ -164,7 +162,7 @@ class PhpEnv {
|
||||
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Validates whether the currently running version matches the provided version.
|
||||
*/
|
||||
@ -173,7 +171,7 @@ class PhpEnv {
|
||||
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -9,27 +9,28 @@
|
||||
import Foundation
|
||||
|
||||
class PhpHelper {
|
||||
|
||||
|
||||
static let keyPhrase = "This file was automatically generated by PHP Monitor."
|
||||
|
||||
|
||||
public static func generate(for version: String) {
|
||||
// Take the PHP version (e.g. "7.2") and generate a dotless version
|
||||
let dotless = version.replacingOccurrences(of: ".", with: "")
|
||||
|
||||
|
||||
do {
|
||||
let destination = "/usr/local/bin/pm\(dotless)"
|
||||
if FileManager.default.fileExists(atPath: destination) {
|
||||
let contents = try String(contentsOfFile: destination)
|
||||
if !contents.contains(keyPhrase) {
|
||||
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor (or is unreadable). Not updating this file.")
|
||||
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
|
||||
+ "(or is unreadable). Not updating this file.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Let's follow the symlink to the PHP binary folder
|
||||
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
|
||||
.resolvingSymlinksInPath().path
|
||||
|
||||
|
||||
// The contents of the script!
|
||||
let script = """
|
||||
#!/bin/zsh
|
||||
@ -41,14 +42,14 @@ class PhpHelper {
|
||||
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
|
||||
export PATH=\(path):$PATH
|
||||
"""
|
||||
|
||||
|
||||
// Write to the destination
|
||||
try script.write(
|
||||
to: URL(fileURLWithPath: destination),
|
||||
atomically: true,
|
||||
encoding: String.Encoding.utf8
|
||||
)
|
||||
|
||||
|
||||
// Make sure the file is executable
|
||||
Shell.run("chmod +x \(destination)")
|
||||
} catch {
|
||||
@ -56,5 +57,5 @@ class PhpHelper {
|
||||
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,21 +10,21 @@ import Foundation
|
||||
|
||||
public struct PhpVersionNumberCollection: Equatable {
|
||||
let versions: [PhpVersionNumber]
|
||||
|
||||
|
||||
public static func make(from versions: [String]) -> Self {
|
||||
return PhpVersionNumberCollection(
|
||||
versions: versions.map { try! PhpVersionNumber.parse($0) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
public var first: PhpVersionNumber? {
|
||||
return self.versions.first
|
||||
}
|
||||
|
||||
|
||||
public var all: [PhpVersionNumber] {
|
||||
return self.versions
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Checks if any versions of PHP are valid for the constraint provided.
|
||||
Due to the complexity of evaluating these, a important test is maintained.
|
||||
@ -61,13 +61,13 @@ public struct PhpVersionNumberCollection: Equatable {
|
||||
// Strict constraint (e.g. "7.0") -> returns specific version
|
||||
return self.versions.filter { $0.isSameAs(version, strict) }
|
||||
}
|
||||
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
|
||||
// Caret range means that the major version is never higher but minor version can be higher
|
||||
// ^7.2 will be compatible with all versions between 7.2 and 8.0
|
||||
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
|
||||
}
|
||||
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
|
||||
// Tilde range means that most specific digit is used as the basis.
|
||||
return self.versions.filter {
|
||||
@ -78,11 +78,11 @@ public struct PhpVersionNumberCollection: Equatable {
|
||||
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
|
||||
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
|
||||
}
|
||||
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
|
||||
return self.versions.filter { $0.isNewerThan(version, strict) }
|
||||
}
|
||||
@ -95,47 +95,52 @@ public struct PhpVersionNumber: Equatable {
|
||||
let major: Int
|
||||
let minor: Int
|
||||
let patch: Int?
|
||||
|
||||
|
||||
public func toString() -> String {
|
||||
return self.patch == nil
|
||||
? "\(major).\(minor)"
|
||||
: "\(major).\(minor).\(patch!)"
|
||||
}
|
||||
|
||||
|
||||
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
|
||||
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
|
||||
}
|
||||
|
||||
|
||||
public var homebrewVersion: String {
|
||||
return "\(major).\(minor)"
|
||||
}
|
||||
|
||||
|
||||
public enum MatchType: String {
|
||||
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
|
||||
|
||||
// TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
|
||||
/*
|
||||
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
public static func parse(_ text: String) throws -> Self {
|
||||
guard let versionText = VersionExtractor.from(text) else {
|
||||
throw VersionParseError()
|
||||
}
|
||||
|
||||
|
||||
return Self.make(from: versionText)!
|
||||
}
|
||||
|
||||
|
||||
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
|
||||
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
|
||||
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
|
||||
|
||||
|
||||
let match = regex.matches(
|
||||
in: versionString,
|
||||
options: [],
|
||||
range: NSRange(location: 0, length: versionString.count)
|
||||
).first
|
||||
|
||||
if match != nil {
|
||||
let major = Int(
|
||||
versionString[Range(match!.range(withName: "major"), in: versionString)!]
|
||||
@ -143,24 +148,24 @@ public struct PhpVersionNumber: Equatable {
|
||||
let minor = Int(
|
||||
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
|
||||
)!
|
||||
var patch: Int? = nil
|
||||
var patch: Int?
|
||||
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
|
||||
patch = Int(versionString[minorRange])
|
||||
}
|
||||
return Self(major: major, minor: minor, patch: patch)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: Comparison Logic
|
||||
|
||||
|
||||
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return self.major == version.major
|
||||
&& self.minor == version.minor
|
||||
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
|
||||
}
|
||||
|
||||
|
||||
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return (
|
||||
self.major > version.major ||
|
||||
@ -169,7 +174,7 @@ public struct PhpVersionNumber: Equatable {
|
||||
&& self.patch(strict) > version.patch(strict)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return self.major == version.major &&
|
||||
(
|
||||
@ -177,12 +182,12 @@ public struct PhpVersionNumber: Equatable {
|
||||
|| self.minor > version.minor
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return self.major == version.major && self.minor == version.minor
|
||||
&& self.patch(strict, version) >= version.patch(strict)
|
||||
}
|
||||
|
||||
|
||||
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return self.major == version.major
|
||||
&& self.minor >= version.minor
|
||||
|
@ -16,24 +16,26 @@ import Foundation
|
||||
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
|
||||
*/
|
||||
class PhpExtension {
|
||||
|
||||
|
||||
/// The file where this extension was located.
|
||||
var file: String
|
||||
|
||||
|
||||
/// The original string that was used to determine this extension is active.
|
||||
var line: String
|
||||
|
||||
/// The name of the extension. This is always identical to the name found in the original string. If you want to display this name, capitalize this.
|
||||
|
||||
/// The name of the extension. This is always identical to the name found in the original string.
|
||||
/// If you want to display this name, capitalize this.
|
||||
var name: String
|
||||
|
||||
|
||||
/// Whether the extension has been enabled.
|
||||
var enabled: Bool
|
||||
|
||||
|
||||
/// The file where this extension was located, but only the filename, not the full path to the .ini file.
|
||||
var fileNameOnly: String {
|
||||
return String(file.split(separator: "/").last ?? "php.ini")
|
||||
}
|
||||
|
||||
|
||||
// swiftlint:disable line_length
|
||||
/**
|
||||
This regular expression will allow us to identify lines which activate an extension.
|
||||
|
||||
@ -47,29 +49,31 @@ class PhpExtension {
|
||||
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
||||
*/
|
||||
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
||||
|
||||
// swiftlint:enable line_length
|
||||
|
||||
/**
|
||||
When registering an extension, we do that based on the line found inside the .ini file.
|
||||
*/
|
||||
init(_ line: String, file: String) {
|
||||
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
|
||||
let match = regex.matches(in: line, options: [], range: NSMakeRange(0, line.count)).first
|
||||
let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
|
||||
let range = Range(match!.range(withName: "name"), in: line)!
|
||||
|
||||
|
||||
self.line = line
|
||||
|
||||
|
||||
let fullPath = String(line[range])
|
||||
.replacingOccurrences(of: "\"", with: "") // replace excess "
|
||||
.replacingOccurrences(of: ".so", with: "") // replace excess .so
|
||||
|
||||
|
||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||
|
||||
|
||||
self.enabled = !line.contains(";")
|
||||
self.file = file
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply.
|
||||
This simply toggles the extension in the .ini file.
|
||||
You may need to restart the other services in order for this change to apply.
|
||||
*/
|
||||
func toggle() {
|
||||
let newLine = enabled
|
||||
@ -77,25 +81,25 @@ class PhpExtension {
|
||||
? "; \(line)"
|
||||
// ENABLED: Line where the comment delimiter (;) is removed
|
||||
: line.replacingOccurrences(of: "; ", with: "")
|
||||
|
||||
|
||||
sed(file: file, original: line, replacement: newLine)
|
||||
|
||||
|
||||
enabled.toggle()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Static Methods
|
||||
|
||||
|
||||
/**
|
||||
This method will attempt to identify all extensions in the .ini file at a certain URL.
|
||||
*/
|
||||
static func load(from path: URL) -> [PhpExtension] {
|
||||
let file = try? String(contentsOf: path, encoding: .utf8)
|
||||
|
||||
if (file == nil) {
|
||||
|
||||
if file == nil {
|
||||
Log.err("There was an issue reading the file. Assuming no extensions were found.")
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
return file!.components(separatedBy: "\n")
|
||||
.filter {
|
||||
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||
@ -104,5 +108,5 @@ class PhpExtension {
|
||||
return PhpExtension($0, file: path.path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,28 +9,28 @@
|
||||
import Foundation
|
||||
|
||||
class PhpInstallation {
|
||||
|
||||
|
||||
var versionNumber: PhpVersionNumber
|
||||
|
||||
|
||||
/**
|
||||
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"
|
||||
self.versionNumber = PhpVersionNumber.make(from: version)!
|
||||
|
||||
|
||||
if Filesystem.fileExists(phpConfigExecutablePath) {
|
||||
let longVersionString = Command.execute(
|
||||
path: phpConfigExecutablePath,
|
||||
arguments: ["--version"]
|
||||
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
|
||||
// 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! PhpVersionNumber.parse(longVersionString)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
class InternalSwitcher: PhpSwitcher {
|
||||
|
||||
|
||||
/**
|
||||
Switching to a new PHP version involves:
|
||||
- unlinking the current version
|
||||
@ -20,50 +20,49 @@ class InternalSwitcher: PhpSwitcher {
|
||||
the version that is switched to may or may not be identical to `php`
|
||||
(without @version).
|
||||
*/
|
||||
func performSwitch(to version: String, completion: @escaping () -> Void)
|
||||
{
|
||||
func performSwitch(to version: String, completion: @escaping () -> Void) {
|
||||
Log.info("Switching to \(version), unlinking all versions...")
|
||||
|
||||
|
||||
let isolated = Valet.shared.sites.filter { site in
|
||||
site.isolatedPhpVersion != nil
|
||||
}.map { site in
|
||||
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
|
||||
}
|
||||
|
||||
|
||||
var versions: Set<String> = [version]
|
||||
|
||||
if (Valet.enabled(feature: .isolatedSites)) {
|
||||
|
||||
if Valet.enabled(feature: .isolatedSites) {
|
||||
versions = versions.union(isolated)
|
||||
}
|
||||
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
|
||||
PhpEnv.shared.availablePhpVersions.forEach { (available) in
|
||||
group.enter()
|
||||
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.disableDefaultPhpFpmPool(available)
|
||||
self.stopPhpVersion(available)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
group.notify(queue: .global(qos: .userInitiated)) {
|
||||
Log.info("All versions have been unlinked!")
|
||||
Log.info("Linking the new version!")
|
||||
|
||||
|
||||
for formula in versions {
|
||||
self.startPhpVersion(formula, primary: (version == formula))
|
||||
}
|
||||
|
||||
|
||||
Log.info("Restarting nginx, just to be sure!")
|
||||
brew("services restart nginx", sudo: true)
|
||||
|
||||
|
||||
Log.info("The new version(s) have been linked!")
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func disableDefaultPhpFpmPool(_ version: String) {
|
||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||
if FileManager.default.fileExists(atPath: pool) {
|
||||
@ -71,8 +70,9 @@ class InternalSwitcher: PhpSwitcher {
|
||||
let existing = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf")!
|
||||
let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
|
||||
do {
|
||||
if (FileManager.default.fileExists(atPath: new.path)) {
|
||||
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), cleaning up so the newer `www.conf` can be moved again.")
|
||||
if FileManager.default.fileExists(atPath: new.path) {
|
||||
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
|
||||
+ "cleaning up so the newer `www.conf` can be moved again.")
|
||||
try FileManager.default.removeItem(at: new)
|
||||
}
|
||||
try FileManager.default.moveItem(at: existing, to: new)
|
||||
@ -82,26 +82,26 @@ class InternalSwitcher: PhpSwitcher {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func stopPhpVersion(_ version: String) {
|
||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
brew("unlink \(formula)")
|
||||
brew("services stop \(formula)", sudo: true)
|
||||
Log.info("Unlinked and stopped services for \(formula)")
|
||||
}
|
||||
|
||||
|
||||
private func startPhpVersion(_ version: String, primary: Bool) {
|
||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
|
||||
if (primary) {
|
||||
|
||||
if primary {
|
||||
Log.info("\(formula) is the primary formula, linking and starting services...")
|
||||
brew("link \(formula) --overwrite --force")
|
||||
} else {
|
||||
Log.info("\(formula) is an isolated PHP version, starting services only...")
|
||||
}
|
||||
|
||||
|
||||
brew("services start \(formula)", sudo: true)
|
||||
|
||||
|
||||
if Valet.enabled(feature: .isolatedSites) && primary {
|
||||
let socketVersion = version.replacingOccurrences(of: ".", with: "")
|
||||
Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
|
||||
@ -109,5 +109,5 @@ class InternalSwitcher: PhpSwitcher {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,15 +9,15 @@
|
||||
import Foundation
|
||||
|
||||
protocol PhpSwitcherDelegate: AnyObject {
|
||||
|
||||
|
||||
func switcherDidStartSwitching(to version: String)
|
||||
|
||||
|
||||
func switcherDidCompleteSwitch(to version: String)
|
||||
|
||||
|
||||
}
|
||||
|
||||
protocol PhpSwitcher {
|
||||
|
||||
|
||||
func performSwitch(to version: String, completion: @escaping () -> Void)
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ import Cocoa
|
||||
import Foundation
|
||||
|
||||
extension App {
|
||||
|
||||
|
||||
// MARK: - Application State
|
||||
|
||||
|
||||
/**
|
||||
Registers a window as currently open.
|
||||
*/
|
||||
@ -22,7 +22,7 @@ extension App {
|
||||
}
|
||||
updateActivationPolicy()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Removes a window, assuming it was closed.
|
||||
*/
|
||||
@ -32,13 +32,13 @@ extension App {
|
||||
}
|
||||
updateActivationPolicy()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
If there are any open windows, the app will be a regular app.
|
||||
If there are no windows open, the app will be an accessory (toolbar) app.
|
||||
*/
|
||||
public func updateActivationPolicy() {
|
||||
NSApp.setActivationPolicy(openWindows.count > 0 ? .regular : .accessory)
|
||||
NSApp.setActivationPolicy(!openWindows.isEmpty ? .regular : .accessory)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,9 +9,9 @@
|
||||
import Cocoa
|
||||
|
||||
extension App {
|
||||
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
|
||||
/**
|
||||
On startup, the preferences should be loaded from the .plist,
|
||||
and we'll enable the shortcut if it is set.
|
||||
@ -22,20 +22,20 @@ extension App {
|
||||
Log.info("No global hotkey was saved in preferences. None set.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Make sure we can parse the JSON into the desired format
|
||||
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
|
||||
Log.err("No global hotkey loaded, could not be parsed!")
|
||||
shortcutHotkey = nil
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
shortcutHotkey = HotKey(keyCombo: KeyCombo(
|
||||
carbonKeyCode: keybindPref.keyCode,
|
||||
carbonModifiers: keybindPref.carbonFlags
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Sets up the action that needs to occur when the shortcut key is pressed
|
||||
(opens the menu).
|
||||
@ -44,11 +44,11 @@ extension App {
|
||||
guard let hotkey = shortcutHotkey else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
hotkey.keyDownHandler = {
|
||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,19 +8,19 @@
|
||||
import Cocoa
|
||||
|
||||
class App {
|
||||
|
||||
|
||||
// MARK: Static Vars
|
||||
|
||||
|
||||
/** The static app instance. Accessible at any time. */
|
||||
static let shared = App()
|
||||
|
||||
|
||||
/** Retrieve the version number from the main info dictionary, Info.plist. */
|
||||
static var version: String {
|
||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
|
||||
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
|
||||
return "\(version) (\(build))"
|
||||
}
|
||||
|
||||
|
||||
static var architecture: String {
|
||||
var systeminfo = utsname()
|
||||
uname(&systeminfo)
|
||||
@ -34,37 +34,37 @@ class App {
|
||||
}
|
||||
return machine
|
||||
}
|
||||
|
||||
|
||||
// MARK: Variables
|
||||
|
||||
|
||||
/** The list of preferences that are currently active. */
|
||||
var preferences: [PreferenceName: Bool]!
|
||||
|
||||
|
||||
/** The window controller of the currently active preferences window. */
|
||||
var preferencesWindowController: PrefsWC? = nil
|
||||
|
||||
var preferencesWindowController: PrefsWC?
|
||||
|
||||
/** The window controller of the currently active site list window. */
|
||||
var domainListWindowController: DomainListWC? = nil
|
||||
|
||||
var domainListWindowController: DomainListWC?
|
||||
|
||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||
var detectedApplications: [Application] = []
|
||||
|
||||
|
||||
/** Timer that will periodically reload info about the user's PHP installation. */
|
||||
var timer: Timer?
|
||||
|
||||
// MARK: - Global Hotkey
|
||||
|
||||
|
||||
/**
|
||||
The shortcut the user has requested.
|
||||
*/
|
||||
var shortcutHotkey: HotKey? = nil {
|
||||
var shortcutHotkey: HotKey? {
|
||||
didSet {
|
||||
setupGlobalHotkeyListener()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Activation Policy
|
||||
|
||||
|
||||
/**
|
||||
Variable that keeps track of which windows are currently open.
|
||||
(Please note that window controllers remain open in memory once opened.)
|
||||
@ -74,9 +74,9 @@ class App {
|
||||
(as a normal app or as a toolbar app).
|
||||
*/
|
||||
var openWindows: [String] = []
|
||||
|
||||
|
||||
// MARK: - App Watchers
|
||||
|
||||
|
||||
/**
|
||||
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
||||
*/
|
||||
|
@ -10,7 +10,7 @@ import Cocoa
|
||||
import Foundation
|
||||
|
||||
extension AppDelegate {
|
||||
|
||||
|
||||
/**
|
||||
This is an entry point for future development for integrating with the PHP Monitor
|
||||
application URL. You can use the `phpmon://` protocol to communicate with the app.
|
||||
@ -21,20 +21,20 @@ extension AppDelegate {
|
||||
Please note that PHP Monitor needs to be running in the background for this to work.
|
||||
*/
|
||||
func application(_ application: NSApplication, open urls: [URL]) {
|
||||
|
||||
|
||||
if !Preferences.isEnabled(.allowProtocolForIntegrations) {
|
||||
Log.info("Acting on commands via phpmon:// has been disabled.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let url = urls.first else { return }
|
||||
|
||||
|
||||
self.interpretCommand(
|
||||
url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""),
|
||||
commands: InterApp.getCommands()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func interpretCommand(_ command: String, commands: [InterApp.Action]) {
|
||||
commands.forEach { action in
|
||||
if command.starts(with: action.command) {
|
||||
@ -44,4 +44,3 @@ extension AppDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,20 +22,20 @@ import AppKit
|
||||
For more information about this, please see the ActivationPolicy-related extension.
|
||||
*/
|
||||
extension AppDelegate {
|
||||
|
||||
|
||||
// MARK: - Menu Interactions
|
||||
|
||||
|
||||
@IBAction func addSiteLinkPressed(_ sender: Any) {
|
||||
DomainListVC.show()
|
||||
|
||||
|
||||
guard let windowController = App.shared.domainListWindowController else { return }
|
||||
windowController.pressedAddLink(nil)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func reloadDomainListPressed(_ sender: Any) {
|
||||
let vc = App.shared.domainListWindowController?
|
||||
.window?.contentViewController as? DomainListVC
|
||||
|
||||
|
||||
if vc != nil {
|
||||
// If the view exists, directly reload the list of sites
|
||||
vc!.reloadDomains()
|
||||
@ -44,12 +44,12 @@ extension AppDelegate {
|
||||
Valet.shared.reloadSites()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func focusSearchField(_ sender: Any) {
|
||||
DomainListVC.show()
|
||||
|
||||
|
||||
guard let windowController = App.shared.domainListWindowController else { return }
|
||||
windowController.searchToolbarItem.searchField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ import Foundation
|
||||
import UserNotifications
|
||||
|
||||
extension AppDelegate {
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
|
||||
/**
|
||||
Sets up notifications. That does mean we need to ask for permission first.
|
||||
If we cannot get permission, we should log this.
|
||||
@ -30,7 +30,7 @@ extension AppDelegate {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Ensure that the application displays notifications even when the app is active.
|
||||
*/
|
||||
@ -42,5 +42,5 @@ extension AppDelegate {
|
||||
) {
|
||||
completionHandler([.banner])
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,55 +10,55 @@ import UserNotifications
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
|
||||
/**
|
||||
The Shell singleton that keeps track of the history of all
|
||||
(invoked by PHP Monitor) shell commands. It is used to
|
||||
invoke all commands in this application.
|
||||
*/
|
||||
let sharedShell: Shell
|
||||
|
||||
|
||||
/**
|
||||
The App singleton contains information about the state of
|
||||
the application and global variables.
|
||||
*/
|
||||
let state: App
|
||||
|
||||
|
||||
/**
|
||||
The MainMenu singleton is responsible for rendering the
|
||||
menu bar item and its menu, as well as its actions.
|
||||
*/
|
||||
let menu: MainMenu
|
||||
|
||||
|
||||
/**
|
||||
The paths singleton that determines where Homebrew is installed,
|
||||
and where to look for binaries.
|
||||
*/
|
||||
let paths: Paths
|
||||
|
||||
|
||||
/**
|
||||
The Valet singleton that determines all information
|
||||
about Valet and its current configuration.
|
||||
*/
|
||||
let valet: Valet
|
||||
|
||||
|
||||
/**
|
||||
The PhpEnv singleton that handles PHP version
|
||||
detection, as well as switching. It is initialized
|
||||
when the app is ready and passed all checks.
|
||||
*/
|
||||
var phpEnvironment: PhpEnv! = nil
|
||||
|
||||
|
||||
/**
|
||||
The logger is responsible for different levels of logging.
|
||||
You can tweak the verbosity in the `init` method here.
|
||||
*/
|
||||
var logger = Log.shared
|
||||
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
|
||||
/**
|
||||
When the application initializes, create all singletons.
|
||||
*/
|
||||
@ -78,13 +78,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
self.valet = Valet.shared
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
||||
func initializeSwitcher() {
|
||||
self.phpEnvironment = PhpEnv.shared
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
|
||||
/**
|
||||
When the application has finished launching, we'll want to set up
|
||||
the user notification center permissions, and kickoff the menu
|
||||
@ -96,5 +96,5 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
// Make sure the menu performs its initial checks
|
||||
Task { await menu.startup() }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,18 +9,18 @@
|
||||
import Foundation
|
||||
|
||||
class InterApp {
|
||||
|
||||
|
||||
public static var bindings: [Action] = []
|
||||
|
||||
|
||||
public static func register(_ action: Action) {
|
||||
self.bindings.append(action)
|
||||
}
|
||||
|
||||
|
||||
public struct Action {
|
||||
let command: String
|
||||
let action: (String) -> Void
|
||||
}
|
||||
|
||||
|
||||
static func getCommands() -> [InterApp.Action] { return [
|
||||
InterApp.Action(command: "list", action: { _ in
|
||||
DomainListVC.show()
|
||||
@ -61,7 +61,7 @@ class InterApp {
|
||||
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
|
||||
).withPrimary(text: "OK").show()
|
||||
}
|
||||
}),
|
||||
})
|
||||
]}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import Foundation
|
||||
import AppKit
|
||||
|
||||
class Startup {
|
||||
|
||||
|
||||
/**
|
||||
Checks the user's environment and checks if PHP Monitor can be used properly.
|
||||
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
|
||||
@ -17,30 +17,29 @@ class Startup {
|
||||
If this method returns false, there was a failed check and an alert was displayed.
|
||||
If this method returns true, then all checks succeeded and the app can continue.
|
||||
*/
|
||||
func checkEnvironment() async -> Bool
|
||||
{
|
||||
func checkEnvironment() async -> Bool {
|
||||
// Do the important system setup checks
|
||||
Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)")
|
||||
|
||||
|
||||
for check in self.checks {
|
||||
if await check.succeeds() {
|
||||
Log.info("[OK] \(check.name)")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// If we get here, something's gone wrong and the check has failed...
|
||||
Log.info("[FAIL] \(check.name)")
|
||||
showAlert(for: check)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// If we get here, nothing has gone wrong. That's what we want!
|
||||
initializeSwitcher()
|
||||
Log.separator(as: .info)
|
||||
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Displays an alert for a particular check. There are two types of alerts:
|
||||
- ones that require an app restart, which prompt the user to exit the app
|
||||
@ -59,7 +58,7 @@ class Startup {
|
||||
exit(1)
|
||||
}).show()
|
||||
}
|
||||
|
||||
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: check.titleText,
|
||||
@ -70,7 +69,7 @@ class Startup {
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Because the Switcher requires various environment guarantees, the switcher is only
|
||||
initialized when it is done working. The switcher must be initialized on the main thread.
|
||||
@ -81,9 +80,9 @@ class Startup {
|
||||
appDelegate.initializeSwitcher()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Check (List)
|
||||
|
||||
|
||||
public var checks: [EnvironmentCheck] = [
|
||||
// =================================================================================
|
||||
// The Homebrew binary must exist.
|
||||
@ -196,9 +195,9 @@ class Startup {
|
||||
descriptionText: "startup.errors.valet_version_unknown.desc".localized
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
// MARK: - EnvironmentCheck struct
|
||||
|
||||
|
||||
/**
|
||||
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary.
|
||||
Checks that require an app restart will always lead to an alert and app termination shortly after.
|
||||
@ -211,7 +210,7 @@ class Startup {
|
||||
let descriptionText: String
|
||||
let buttonText: String
|
||||
let requiresAppRestart: Bool
|
||||
|
||||
|
||||
init(
|
||||
command: @escaping () async -> Bool,
|
||||
name: String,
|
||||
@ -229,7 +228,7 @@ class Startup {
|
||||
self.buttonText = buttonText
|
||||
self.requiresAppRestart = requiresAppRestart
|
||||
}
|
||||
|
||||
|
||||
public func succeeds() async -> Bool {
|
||||
return await !self.command()
|
||||
}
|
||||
|
@ -10,43 +10,43 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class AddProxyVC: NSViewController, NSTextFieldDelegate {
|
||||
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
|
||||
@IBOutlet weak var textFieldTitle: NSTextField!
|
||||
@IBOutlet weak var textFieldProxySubject: NSTextField!
|
||||
@IBOutlet weak var textFieldDomainName: NSTextField!
|
||||
|
||||
|
||||
@IBOutlet weak var inputProxySubject: NSTextField!
|
||||
@IBOutlet weak var inputDomainName: NSTextField!
|
||||
|
||||
|
||||
@IBOutlet weak var previewText: NSTextField!
|
||||
|
||||
|
||||
@IBOutlet weak var buttonSecure: NSButton!
|
||||
@IBOutlet weak var buttonCreateProxy: NSButton!
|
||||
@IBOutlet weak var buttonCancel: NSButton!
|
||||
|
||||
|
||||
@IBOutlet weak var textFieldSecure: NSTextField!
|
||||
@IBOutlet weak var textFieldError: NSTextField!
|
||||
|
||||
|
||||
// MARK: - View Lifecycle
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
loadStaticLocalisedStrings()
|
||||
|
||||
|
||||
buttonCreateProxy.isEnabled = false
|
||||
updatePreview()
|
||||
validate()
|
||||
}
|
||||
|
||||
|
||||
private func dismissView(outcome: NSApplication.ModalResponse) {
|
||||
guard let window = view.window, let parent = window.sheetParent else { return }
|
||||
parent.endSheet(window, returnCode: outcome)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Localisation
|
||||
|
||||
|
||||
func loadStaticLocalisedStrings() {
|
||||
textFieldTitle.stringValue = "domain_list.add.set_up_proxy".localized
|
||||
textFieldProxySubject.stringValue = "domain_list.add.proxy_subject".localized
|
||||
@ -55,101 +55,101 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
|
||||
buttonCancel.title = "domain_list.add.cancel".localized
|
||||
buttonCreateProxy.title = "domain_list.add.create_proxy".localized
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Outlet Interactions
|
||||
|
||||
|
||||
@IBAction func pressedSecure(_ sender: Any) {
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func pressedCreateProxy(_ sender: Any) {
|
||||
// TODO: Validate the input before allowing proxy creation
|
||||
|
||||
|
||||
let domain = self.inputDomainName.stringValue
|
||||
let proxyName = self.inputProxySubject.stringValue
|
||||
let secure = self.buttonSecure.state == .on ? " --secure" : ""
|
||||
|
||||
|
||||
dismissView(outcome: .OK)
|
||||
|
||||
|
||||
App.shared.domainListWindowController?.contentVC.setUIBusy()
|
||||
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
Shell.run("\(Paths.valet) proxy \(domain) \(proxyName)\(secure)", requiresPath: true)
|
||||
Actions.restartNginx()
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
App.shared.domainListWindowController?.contentVC.setUINotBusy()
|
||||
App.shared.domainListWindowController?.pressedReload(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func pressedCancel(_ sender: Any) {
|
||||
dismissView(outcome: .cancel)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Text Field Delegate
|
||||
|
||||
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
updateTextField()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
|
||||
private func validate() {
|
||||
_ = validate(
|
||||
domain: inputDomainName.stringValue,
|
||||
proxy: inputProxySubject.stringValue
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func validate(domain: String, proxy: String) -> Bool {
|
||||
if domain.isEmpty {
|
||||
textFieldError.isHidden = false
|
||||
textFieldError.stringValue = "domain_list.add.errors.empty".localized
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if proxy.isEmpty {
|
||||
textFieldError.isHidden = false
|
||||
textFieldError.stringValue = "domain_list.add.errors.empty_proxy".localized
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if Valet.shared.sites.contains(where: { $0.name == domain }) {
|
||||
textFieldError.isHidden = false
|
||||
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
textFieldError.isHidden = true
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func updateTextField() {
|
||||
inputDomainName.stringValue = inputDomainName.stringValue
|
||||
.replacingOccurrences(of: " ", with: "-")
|
||||
|
||||
|
||||
buttonCreateProxy.isEnabled = validate(
|
||||
domain: inputDomainName.stringValue,
|
||||
proxy: inputProxySubject.stringValue
|
||||
)
|
||||
|
||||
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
|
||||
func updatePreview() {
|
||||
buttonSecure.title = "domain_list.add.secure_after_creation"
|
||||
.localized(
|
||||
inputDomainName.stringValue,
|
||||
Valet.shared.config.tld
|
||||
)
|
||||
|
||||
if (inputProxySubject.stringValue.isEmpty || inputDomainName.stringValue.isEmpty) {
|
||||
|
||||
if inputProxySubject.stringValue.isEmpty || inputDomainName.stringValue.isEmpty {
|
||||
previewText.stringValue = "domain_list.add.empty_fields".localized
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
previewText.stringValue = "domain_list.add.proxy_available"
|
||||
.localized(
|
||||
inputProxySubject.stringValue,
|
||||
@ -158,5 +158,5 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
|
||||
Valet.shared.config.tld
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,37 +10,37 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
||||
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
|
||||
@IBOutlet weak var textFieldTitle: NSTextField!
|
||||
|
||||
@IBOutlet weak var pathControl: NSPathControl!
|
||||
@IBOutlet weak var inputDomainName: NSTextField!
|
||||
|
||||
|
||||
@IBOutlet weak var previewText: NSTextField!
|
||||
|
||||
|
||||
@IBOutlet weak var buttonSecure: NSButton!
|
||||
@IBOutlet weak var buttonCreateLink: NSButton!
|
||||
@IBOutlet weak var buttonCancel: NSButton!
|
||||
|
||||
|
||||
@IBOutlet weak var textFieldSecure: NSTextField!
|
||||
@IBOutlet weak var textFieldError: NSTextField!
|
||||
|
||||
|
||||
// MARK: - View Lifecycle
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
loadStaticLocalisedStrings()
|
||||
}
|
||||
|
||||
|
||||
private func dismissView(outcome: NSApplication.ModalResponse) {
|
||||
guard let window = self.view.window, let parent = window.sheetParent else { return }
|
||||
parent.endSheet(window, returnCode: outcome)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Localisation
|
||||
|
||||
|
||||
func loadStaticLocalisedStrings() {
|
||||
textFieldTitle.stringValue = "domain_list.add.link_folder".localized
|
||||
inputDomainName.placeholderString = "domain_list.add.domain_name_placeholder".localized
|
||||
@ -48,13 +48,13 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
||||
buttonCancel.title = "domain_list.add.cancel".localized
|
||||
buttonCreateLink.title = "domain_list.add.create_link".localized
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Outlet Interactions
|
||||
|
||||
|
||||
@IBAction func pressedCreateLink(_ sender: Any) {
|
||||
let path = pathControl.url!.path
|
||||
let name = inputDomainName.stringValue
|
||||
|
||||
|
||||
if !FileManager.default.fileExists(atPath: path) {
|
||||
Alert.confirm(
|
||||
onWindow: view.window!,
|
||||
@ -68,18 +68,18 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Adding `valet links` is a workaround for Valet malforming the config.json file
|
||||
// TODO: I will have to investigate and report this behaviour if possible
|
||||
Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links", requiresPath: true)
|
||||
|
||||
|
||||
dismissView(outcome: .OK)
|
||||
|
||||
|
||||
// Reset search
|
||||
App.shared.domainListWindowController?
|
||||
.searchToolbarItem
|
||||
.searchField.stringValue = ""
|
||||
|
||||
|
||||
// Add the new item and scrolls to it
|
||||
App.shared.domainListWindowController?
|
||||
.contentVC
|
||||
@ -88,60 +88,60 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
||||
secure: buttonSecure.state == .on
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func pressedCancel(_ sender: Any) {
|
||||
dismissView(outcome: .cancel)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func pressedSecure(_ sender: Any) {
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Text Field Delegate
|
||||
|
||||
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
updateTextField()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
|
||||
private func isValidLinkName(_ name: String) -> Bool {
|
||||
if name.isEmpty {
|
||||
textFieldError.isHidden = false
|
||||
textFieldError.stringValue = "domain_list.add.errors.empty".localized
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if Valet.shared.sites.contains(where: { $0.name == name }) {
|
||||
textFieldError.isHidden = false
|
||||
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
textFieldError.isHidden = true
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func updateTextField() {
|
||||
inputDomainName.stringValue = inputDomainName.stringValue
|
||||
.replacingOccurrences(of: " ", with: "-")
|
||||
|
||||
|
||||
buttonCreateLink.isEnabled = isValidLinkName(inputDomainName.stringValue)
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
|
||||
func updatePreview() {
|
||||
buttonSecure.title = "domain_list.add.secure_after_creation"
|
||||
.localized(
|
||||
inputDomainName.stringValue,
|
||||
Valet.shared.config.tld
|
||||
)
|
||||
|
||||
if (inputDomainName.stringValue.isEmpty) {
|
||||
|
||||
if inputDomainName.stringValue.isEmpty {
|
||||
previewText.stringValue = "domain_list.add.empty_fields".localized
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
previewText.stringValue = "domain_list.add.folder_available"
|
||||
.localized(
|
||||
buttonSecure.state == .on ? "https" : "http",
|
||||
|
@ -9,12 +9,11 @@
|
||||
import Cocoa
|
||||
import AppKit
|
||||
|
||||
class DomainListKindCell: NSTableCellView, DomainListCellProtocol
|
||||
{
|
||||
class DomainListKindCell: NSTableCellView, DomainListCellProtocol {
|
||||
static let reusableName = "domainListKindCell"
|
||||
|
||||
|
||||
@IBOutlet weak var imageViewType: NSImageView!
|
||||
|
||||
|
||||
func populateCell(with site: ValetSite) {
|
||||
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
||||
imageViewType.image = NSImage(
|
||||
@ -22,15 +21,15 @@ class DomainListKindCell: NSTableCellView, DomainListCellProtocol
|
||||
? "IconParked"
|
||||
: "IconLinked"
|
||||
)
|
||||
|
||||
|
||||
// Unless, of course, this is a default site
|
||||
if site.absolutePath == Valet.shared.config.defaultSite {
|
||||
imageViewType.image = NSImage(named: "IconDefault")
|
||||
}
|
||||
|
||||
|
||||
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
||||
}
|
||||
|
||||
|
||||
func populateCell(with proxy: ValetProxy) {
|
||||
imageViewType.image = NSImage(named: "IconProxy")
|
||||
}
|
||||
|
@ -9,18 +9,17 @@
|
||||
import Cocoa
|
||||
import AppKit
|
||||
|
||||
class DomainListNameCell: NSTableCellView, DomainListCellProtocol
|
||||
{
|
||||
class DomainListNameCell: NSTableCellView, DomainListCellProtocol {
|
||||
static let reusableName = "domainListNameCell"
|
||||
|
||||
|
||||
@IBOutlet weak var labelSiteName: NSTextField!
|
||||
@IBOutlet weak var labelPathName: NSTextField!
|
||||
|
||||
|
||||
func populateCell(with site: ValetSite) {
|
||||
labelSiteName.stringValue = "\(site.name).\(site.tld)"
|
||||
labelPathName.stringValue = site.absolutePathRelative
|
||||
}
|
||||
|
||||
|
||||
func populateCell(with proxy: ValetProxy) {
|
||||
labelSiteName.stringValue = "\(proxy.domain).\(proxy.tld)"
|
||||
labelPathName.stringValue = proxy.target
|
||||
|
@ -9,22 +9,21 @@
|
||||
import Cocoa
|
||||
import AppKit
|
||||
|
||||
class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
|
||||
{
|
||||
class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||
static let reusableName = "domainListPhpCell"
|
||||
|
||||
var site: ValetSite? = nil
|
||||
|
||||
|
||||
var site: ValetSite?
|
||||
|
||||
@IBOutlet weak var buttonPhpVersion: NSButton!
|
||||
@IBOutlet weak var imageViewPhpVersionOK: NSImageView!
|
||||
|
||||
|
||||
func populateCell(with site: ValetSite) {
|
||||
self.site = site
|
||||
|
||||
|
||||
buttonPhpVersion.title = " PHP \(site.servingPhpVersion)"
|
||||
|
||||
|
||||
imageViewPhpVersionOK.toolTip = nil
|
||||
|
||||
|
||||
if site.isolatedPhpVersion != nil {
|
||||
imageViewPhpVersionOK.isHidden = false
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
|
||||
@ -34,45 +33,45 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
|
||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp)
|
||||
}
|
||||
|
||||
|
||||
buttonPhpVersion.isHidden = false
|
||||
imageViewPhpVersionOK.isHidden = false
|
||||
}
|
||||
|
||||
|
||||
func populateCell(with proxy: ValetProxy) {
|
||||
buttonPhpVersion.isHidden = true
|
||||
imageViewPhpVersionOK.isHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@IBAction func pressedPhpVersion(_ sender: Any) {
|
||||
guard let site = self.site else { return }
|
||||
|
||||
|
||||
let alert = NSAlert.init()
|
||||
alert.alertStyle = .informational
|
||||
|
||||
|
||||
var information = ""
|
||||
|
||||
if (self.site?.isolatedPhpVersion != nil) {
|
||||
|
||||
if self.site?.isolatedPhpVersion != nil {
|
||||
information += "alert.composer_php_isolated.desc".localized(
|
||||
self.site!.isolatedPhpVersion!.versionNumber.homebrewVersion,
|
||||
PhpEnv.phpInstall.version.short
|
||||
)
|
||||
information += "\n\n"
|
||||
}
|
||||
|
||||
|
||||
information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)"
|
||||
.localized
|
||||
|
||||
|
||||
alert.messageText = "alert.composer_php_requirement.title"
|
||||
.localized("\(site.name).\(Valet.shared.config.tld)", site.composerPhp)
|
||||
alert.informativeText = information
|
||||
|
||||
|
||||
alert.addButton(withTitle: "site_link.close".localized)
|
||||
|
||||
|
||||
var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue
|
||||
var map: [Int: String] = [:]
|
||||
|
||||
|
||||
if site.isolatedPhpVersion == nil {
|
||||
// Determine which installed versions would be ideal to switch to,
|
||||
// but make sure to exclude the currently linked version
|
||||
@ -83,7 +82,7 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
|
||||
map[mapIndex] = version.homebrewVersion
|
||||
mapIndex += 1
|
||||
}
|
||||
|
||||
|
||||
// Site is not isolated, show options to switch global PHP version
|
||||
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) { response in
|
||||
if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue {
|
||||
@ -99,5 +98,5 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
|
||||
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,18 +9,17 @@
|
||||
import Cocoa
|
||||
import AppKit
|
||||
|
||||
class DomainListTLSCell: NSTableCellView, DomainListCellProtocol
|
||||
{
|
||||
class DomainListTLSCell: NSTableCellView, DomainListCellProtocol {
|
||||
static let reusableName = "domainListTLSCell"
|
||||
|
||||
|
||||
@IBOutlet weak var imageViewLock: NSImageView!
|
||||
|
||||
|
||||
func populateCell(with site: ValetSite) {
|
||||
imageViewLock.contentTintColor = site.secured
|
||||
? NSColor(named: "IconColorGreen")
|
||||
: NSColor(named: "IconColorRed")
|
||||
}
|
||||
|
||||
|
||||
func populateCell(with proxy: ValetProxy) {
|
||||
imageViewLock.contentTintColor = proxy.secured
|
||||
? NSColor(named: "IconColorGreen")
|
||||
|
@ -9,26 +9,25 @@
|
||||
import Cocoa
|
||||
import AppKit
|
||||
|
||||
class DomainListTypeCell: NSTableCellView, DomainListCellProtocol
|
||||
{
|
||||
class DomainListTypeCell: NSTableCellView, DomainListCellProtocol {
|
||||
static let reusableName = "domainListTypeCell"
|
||||
|
||||
|
||||
@IBOutlet weak var labelDriver: NSTextField!
|
||||
@IBOutlet weak var labelPhpVersion: NSTextField!
|
||||
|
||||
|
||||
func populateCell(with site: ValetSite) {
|
||||
labelDriver.stringValue = site.driver ?? "driver.not_detected".localized
|
||||
|
||||
|
||||
// Determine the Laravel version
|
||||
if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") {
|
||||
let constraint = site.notableComposerDependencies["laravel/framework"]!
|
||||
labelDriver.stringValue = "Laravel (\(constraint))"
|
||||
}
|
||||
|
||||
|
||||
// PHP version
|
||||
labelPhpVersion.stringValue = site.composerPhp == "???" ? "Any PHP" : "PHP \(site.composerPhp)"
|
||||
}
|
||||
|
||||
|
||||
func populateCell(with proxy: ValetProxy) {
|
||||
labelDriver.stringValue = "Proxy"
|
||||
labelPhpVersion.stringValue = "Active"
|
||||
|
@ -17,7 +17,7 @@ extension DomainListVC {
|
||||
let action = selectedSite!.secured ? "unsecure" : "secure"
|
||||
let selectedSite = selectedSite!
|
||||
let command = "cd '\(selectedSite.absolutePath)' && sudo \(Paths.valet) \(action) && exit;"
|
||||
|
||||
|
||||
waitAndExecute {
|
||||
Shell.run(command, requiresPath: true)
|
||||
} completion: { [self] in
|
||||
@ -41,18 +41,18 @@ extension DomainListVC {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4])
|
||||
tableView.deselectRow(rowToReload)
|
||||
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func openInBrowser() {
|
||||
guard let selected = self.selected else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let url = selected.getListableUrl() else {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
@ -63,29 +63,29 @@ extension DomainListVC {
|
||||
.show()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
|
||||
@objc func openInFinder() {
|
||||
Shell.run("open '\(selectedSite!.absolutePath)'")
|
||||
}
|
||||
|
||||
|
||||
@objc func openInTerminal() {
|
||||
Shell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath)'")
|
||||
}
|
||||
|
||||
|
||||
@objc func openWithEditor(sender: EditorMenuItem) {
|
||||
guard let editor = sender.editor else { return }
|
||||
editor.openDirectory(file: selectedSite!.absolutePath)
|
||||
}
|
||||
|
||||
|
||||
@objc func isolateSite(sender: PhpMenuItem) {
|
||||
let command = "sudo \(Paths.valet) isolate php@\(sender.version) --site '\(self.selectedSite!.name)' && exit;"
|
||||
|
||||
|
||||
self.performAction(command: command) {
|
||||
self.selectedSite!.determineIsolated()
|
||||
|
||||
|
||||
if self.selectedSite!.isolatedPhpVersion == nil {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
@ -98,22 +98,22 @@ extension DomainListVC {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func removeIsolatedSite() {
|
||||
self.performAction(command: "sudo \(Paths.valet) unisolate --site '\(self.selectedSite!.name)' && exit;") {
|
||||
self.selectedSite!.isolatedPhpVersion = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func unlinkSite() {
|
||||
guard let site = selectedSite else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if site.aliasPath == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Alert.confirm(
|
||||
onWindow: view.window!,
|
||||
messageText: "domain_list.confirm_unlink".localized(site.name),
|
||||
@ -127,12 +127,12 @@ extension DomainListVC {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@objc func removeProxy() {
|
||||
guard let proxy = selectedProxy else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Alert.confirm(
|
||||
onWindow: view.window!,
|
||||
messageText: "domain_list.confirm_unproxy".localized("\(proxy.domain).\(proxy.tld)"),
|
||||
@ -146,10 +146,10 @@ extension DomainListVC {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func performAction(command: String, beforeCellReload: @escaping () -> Void) {
|
||||
let rowToReload = tableView.selectedRow
|
||||
|
||||
|
||||
waitAndExecute {
|
||||
Shell.run(command, requiresPath: true)
|
||||
} completion: { [self] in
|
||||
@ -159,5 +159,5 @@ extension DomainListVC {
|
||||
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,13 +9,13 @@
|
||||
import Cocoa
|
||||
|
||||
extension DomainListVC {
|
||||
|
||||
|
||||
internal func reloadContextMenu() {
|
||||
guard let selected = selected else {
|
||||
tableView.menu = nil
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let selected = selected as? ValetSite {
|
||||
addMenuItemsForSite(selected)
|
||||
return
|
||||
@ -25,29 +25,29 @@ extension DomainListVC {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Menu Items for Site
|
||||
|
||||
|
||||
private func addMenuItemsForSite(_ site: ValetSite) {
|
||||
let menu = NSMenu()
|
||||
|
||||
|
||||
addSystemApps(to: menu)
|
||||
addSeparator(to: menu)
|
||||
addDetectedApps(to: menu)
|
||||
addSeparator(to: menu)
|
||||
|
||||
|
||||
if Valet.enabled(feature: .isolatedSites) {
|
||||
addIsolate(to: menu, with: site)
|
||||
} else {
|
||||
addDisabledIsolation(to: menu)
|
||||
}
|
||||
|
||||
|
||||
addUnlink(to: menu, with: site)
|
||||
addToggleSecure(to: menu, with: site)
|
||||
|
||||
|
||||
tableView.menu = menu
|
||||
}
|
||||
|
||||
|
||||
private func addSystemApps(to menu: NSMenu) {
|
||||
menu.addItem(withTitle: "domain_list.system_apps".localized, action: nil, keyEquivalent: "")
|
||||
menu.addItem(
|
||||
@ -66,13 +66,13 @@ extension DomainListVC {
|
||||
keyEquivalent: "B"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func addDetectedApps(to menu: NSMenu) {
|
||||
if (applications.count > 0) {
|
||||
if !applications.isEmpty {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
menu.addItem(withTitle: "domain_list.detected_apps".localized, action: nil, keyEquivalent: "")
|
||||
|
||||
for (_, editor) in applications.enumerated() {
|
||||
|
||||
for editor in applications {
|
||||
let editorMenuItem = EditorMenuItem(
|
||||
title: "Open with \(editor.name)",
|
||||
action: #selector(self.openWithEditor(sender:)),
|
||||
@ -83,9 +83,9 @@ extension DomainListVC {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func addUnlink(to menu: NSMenu, with site: ValetSite) {
|
||||
if (site.aliasPath != nil) {
|
||||
if site.aliasPath != nil {
|
||||
menu.addItem(
|
||||
withTitle: "domain_list.unlink".localized,
|
||||
action: #selector(self.unlinkSite),
|
||||
@ -94,25 +94,29 @@ extension DomainListVC {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func addDisabledIsolation(to menu: NSMenu) {
|
||||
menu.addItem(withTitle: "domain_list.isolation_unavailable".localized, action: nil, keyEquivalent: "")
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
|
||||
private func addIsolate(to menu: NSMenu, with site: ValetSite) {
|
||||
if site.isolatedPhpVersion == nil {
|
||||
// ISOLATION POSSIBLE
|
||||
let isolationMenuItem = NSMenuItem(title:"domain_list.isolate".localized, action: nil, keyEquivalent: "")
|
||||
let isolationMenuItem = NSMenuItem(title: "domain_list.isolate".localized, action: nil, keyEquivalent: "")
|
||||
let submenu = NSMenu()
|
||||
submenu.addItem(withTitle: "Choose a PHP version", action: nil, keyEquivalent: "")
|
||||
for version in PhpEnv.shared.availablePhpVersions.reversed() {
|
||||
let item = PhpMenuItem(title: "Always use PHP \(version)", action: #selector(self.isolateSite), keyEquivalent: "")
|
||||
let item = PhpMenuItem(
|
||||
title: "Always use PHP \(version)",
|
||||
action: #selector(self.isolateSite),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
item.version = version
|
||||
submenu.addItem(item)
|
||||
}
|
||||
menu.setSubmenu(submenu, for: isolationMenuItem)
|
||||
|
||||
|
||||
menu.addItem(isolationMenuItem)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
} else {
|
||||
@ -125,7 +129,7 @@ extension DomainListVC {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func addToggleSecure(to menu: NSMenu, with site: ValetSite) {
|
||||
menu.addItem(
|
||||
withTitle: site.secured
|
||||
@ -135,9 +139,9 @@ extension DomainListVC {
|
||||
keyEquivalent: ""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Menu Items for Proxy
|
||||
|
||||
|
||||
private func addMenuItemsForProxy(_ proxy: ValetProxy) {
|
||||
let menu = NSMenu()
|
||||
addOpenProxyInBrowser(to: menu)
|
||||
@ -145,7 +149,7 @@ extension DomainListVC {
|
||||
addRemoveProxy(to: menu)
|
||||
tableView.menu = menu
|
||||
}
|
||||
|
||||
|
||||
private func addOpenProxyInBrowser(to menu: NSMenu) {
|
||||
menu.addItem(
|
||||
withTitle: "domain_list.open_in_browser".localized,
|
||||
@ -153,7 +157,7 @@ extension DomainListVC {
|
||||
keyEquivalent: "B"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func addRemoveProxy(to menu: NSMenu) {
|
||||
menu.addItem(
|
||||
withTitle: "domain_list.unproxy".localized,
|
||||
@ -161,11 +165,11 @@ extension DomainListVC {
|
||||
keyEquivalent: ""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Shared
|
||||
|
||||
|
||||
private func addSeparator(to menu: NSMenu) {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,62 +10,62 @@ import Cocoa
|
||||
import Carbon
|
||||
|
||||
class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
|
||||
@IBOutlet weak var tableView: NSTableView!
|
||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
|
||||
/// List of sites that will be displayed in this view. Originates from the `Valet` object.
|
||||
var domains: [DomainListable] = []
|
||||
|
||||
|
||||
/// Array that contains various apps that might open a particular site directory.
|
||||
var applications: [Application] {
|
||||
return App.shared.detectedApplications
|
||||
}
|
||||
|
||||
|
||||
/// The last sort descriptor used.
|
||||
var sortDescriptor: NSSortDescriptor? = nil
|
||||
|
||||
var sortDescriptor: NSSortDescriptor?
|
||||
|
||||
/// String that was last searched for. Empty by default.
|
||||
var lastSearchedFor = ""
|
||||
|
||||
|
||||
// MARK: - Helper Variables
|
||||
|
||||
|
||||
var selectedSite: ValetSite? {
|
||||
if tableView.selectedRow == -1 {
|
||||
return nil
|
||||
}
|
||||
return domains[tableView.selectedRow] as? ValetSite
|
||||
}
|
||||
|
||||
|
||||
var selectedProxy: ValetProxy? {
|
||||
if tableView.selectedRow == -1 {
|
||||
return nil
|
||||
}
|
||||
return domains[tableView.selectedRow] as? ValetProxy
|
||||
}
|
||||
|
||||
|
||||
var selected: DomainListable? {
|
||||
if tableView.selectedRow == -1 {
|
||||
return nil
|
||||
}
|
||||
return domains[tableView.selectedRow]
|
||||
}
|
||||
|
||||
var timer: Timer? = nil
|
||||
|
||||
|
||||
var timer: Timer?
|
||||
|
||||
// MARK: - Display
|
||||
|
||||
|
||||
public static func create(delegate: NSWindowDelegate?) {
|
||||
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
|
||||
|
||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
let windowController = storyboard.instantiateController(
|
||||
withIdentifier: "domainListWindow"
|
||||
) as! DomainListWC
|
||||
|
||||
|
||||
windowController.window!.title = "domain_list.title".localized
|
||||
windowController.window!.subtitle = "domain_list.subtitle".localized
|
||||
windowController.window!.delegate = delegate
|
||||
@ -75,24 +75,24 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
windowController.window!.minSize = NSSize(width: 550, height: 200)
|
||||
windowController.window!.delegate = windowController
|
||||
windowController.window!.setFrameAutosaveName("domainListWindow")
|
||||
|
||||
|
||||
App.shared.domainListWindowController = windowController
|
||||
}
|
||||
|
||||
|
||||
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||
if (App.shared.domainListWindowController == nil) {
|
||||
if App.shared.domainListWindowController == nil {
|
||||
Self.create(delegate: delegate)
|
||||
}
|
||||
|
||||
|
||||
App.shared.domainListWindowController!.showWindow(self)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
tableView.doubleAction = #selector(self.doubleClicked(sender:))
|
||||
|
||||
|
||||
if !Valet.shared.sites.isEmpty {
|
||||
// Preloaded list
|
||||
domains = Valet.getDomainListable()
|
||||
@ -101,9 +101,9 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
reloadDomains()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Async Operations
|
||||
|
||||
|
||||
/**
|
||||
Disables the UI so the user cannot interact with it.
|
||||
Also shows a spinner to indicate that we're busy.
|
||||
@ -113,12 +113,12 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in
|
||||
self.progressIndicator.startAnimation(true)
|
||||
})
|
||||
|
||||
|
||||
tableView.alphaValue = 0.3
|
||||
tableView.isEnabled = false
|
||||
tableView.selectRowIndexes([], byExtendingSelection: true)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Re-enables the UI so the user can interact with it.
|
||||
*/
|
||||
@ -128,7 +128,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
tableView.alphaValue = 1.0
|
||||
tableView.isEnabled = true
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Executes a specific callback and fires the completion callback,
|
||||
while updating the UI as required. As long as the completion callback
|
||||
@ -137,12 +137,11 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
- Parameter execute: Callback of the work that needs to happen.
|
||||
- Parameter completion: Callback that is fired when the work is done.
|
||||
*/
|
||||
internal func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
|
||||
{
|
||||
internal func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {}) {
|
||||
setUIBusy()
|
||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||
execute()
|
||||
|
||||
|
||||
// For a smoother animation, expect at least a 0.2 second delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
|
||||
completion()
|
||||
@ -150,9 +149,9 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Site Data Loading
|
||||
|
||||
|
||||
func reloadDomains() {
|
||||
waitAndExecute {
|
||||
Valet.shared.reloadSites()
|
||||
@ -161,29 +160,24 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
searchedFor(text: lastSearchedFor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func applySortDescriptor(_ descriptor: NSSortDescriptor) {
|
||||
sortDescriptor = descriptor
|
||||
|
||||
|
||||
var sorted = self.domains
|
||||
|
||||
|
||||
switch descriptor.key {
|
||||
case "Secure":
|
||||
sorted = self.domains.sorted { $0.getListableSecured() && !$1.getListableSecured() }; break
|
||||
case "Domain":
|
||||
sorted = self.domains.sorted { $0.getListableAbsolutePath() < $1.getListableAbsolutePath() }; break
|
||||
case "PHP":
|
||||
sorted = self.domains.sorted { $0.getListablePhpVersion() < $1.getListablePhpVersion() }; break
|
||||
case "Kind":
|
||||
sorted = self.domains.sorted { $0.getListableKind() < $1.getListableKind() }; break
|
||||
case "Type":
|
||||
sorted = self.domains.sorted { $0.getListableType() < $1.getListableType() }; break
|
||||
default: break;
|
||||
case "Secure": sorted = self.domains.sorted { $0.getListableSecured() && !$1.getListableSecured() }
|
||||
case "Domain": sorted = self.domains.sorted { $0.getListableAbsolutePath() < $1.getListableAbsolutePath() }
|
||||
case "PHP": sorted = self.domains.sorted { $0.getListablePhpVersion() < $1.getListablePhpVersion() }
|
||||
case "Kind": sorted = self.domains.sorted { $0.getListableKind() < $1.getListableKind() }
|
||||
case "Type": sorted = self.domains.sorted { $0.getListableType() < $1.getListableType() }
|
||||
default: break
|
||||
}
|
||||
|
||||
|
||||
self.domains = descriptor.ascending ? sorted.reversed() : sorted
|
||||
}
|
||||
|
||||
|
||||
func addedNewSite(name: String, secure: Bool) {
|
||||
waitAndExecute {
|
||||
Valet.shared.reloadSites()
|
||||
@ -191,7 +185,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
find(name, secure)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func find(_ name: String, _ secure: Bool = false) {
|
||||
domains = Valet.getDomainListable()
|
||||
searchedFor(text: "")
|
||||
@ -199,19 +193,19 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.selectRowIndexes([site.offset], byExtendingSelection: false)
|
||||
self.tableView.scrollRowToVisible(site.offset)
|
||||
if (secure && !site.element.getListableSecured()) {
|
||||
if secure && !site.element.getListableSecured() {
|
||||
self.toggleSecure()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Table View Delegate
|
||||
|
||||
|
||||
func numberOfRows(in tableView: NSTableView) -> Int {
|
||||
return domains.count
|
||||
}
|
||||
|
||||
|
||||
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
|
||||
guard let sortDescriptor = tableView.sortDescriptors.first else { return }
|
||||
// Kinda scuffed way of applying sort descriptors here, but it works.
|
||||
@ -219,7 +213,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
applySortDescriptor(sortDescriptor)
|
||||
searchedFor(text: lastSearchedFor)
|
||||
}
|
||||
|
||||
|
||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||
let mapping: [String: String] = [
|
||||
"TLS": DomainListTLSCell.reusableName,
|
||||
@ -228,76 +222,76 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
"KIND": DomainListKindCell.reusableName,
|
||||
"TYPE": DomainListTypeCell.reusableName
|
||||
]
|
||||
|
||||
|
||||
let columnName = tableColumn!.identifier.rawValue
|
||||
let identifier = NSUserInterfaceItemIdentifier(rawValue: mapping[columnName]!)
|
||||
|
||||
|
||||
guard let userCell = tableView.makeView(withIdentifier: identifier, owner: self)
|
||||
as? DomainListCellProtocol else { return nil }
|
||||
|
||||
|
||||
if let site = domains[row] as? ValetSite {
|
||||
userCell.populateCell(with: site)
|
||||
}
|
||||
|
||||
|
||||
if let proxy = domains[row] as? ValetProxy {
|
||||
userCell.populateCell(with: proxy)
|
||||
}
|
||||
|
||||
|
||||
return userCell as? NSView
|
||||
}
|
||||
|
||||
|
||||
func tableViewSelectionDidChange(_ notification: Notification) {
|
||||
reloadContextMenu()
|
||||
}
|
||||
|
||||
|
||||
@objc func doubleClicked(sender: Any) {
|
||||
guard self.selected != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
self.openInBrowser()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - (Search) Text Field Delegate
|
||||
|
||||
|
||||
func reloadTable() {
|
||||
if let sortDescriptor = sortDescriptor {
|
||||
self.applySortDescriptor(sortDescriptor)
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func searchedFor(text: String) {
|
||||
lastSearchedFor = text
|
||||
|
||||
|
||||
let searchString = text.lowercased()
|
||||
|
||||
|
||||
if searchString.isEmpty {
|
||||
domains = Valet.getDomainListable()
|
||||
|
||||
|
||||
reloadTable()
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let splitSearchString: [String] = searchString
|
||||
.split(separator: " ")
|
||||
.map { return String($0) }
|
||||
|
||||
|
||||
domains = Valet.getDomainListable().filter({ site in
|
||||
return !splitSearchString.map { searchString in
|
||||
return site.getListableName().lowercased().contains(searchString)
|
||||
}.contains(false)
|
||||
})
|
||||
|
||||
|
||||
reloadTable()
|
||||
}
|
||||
|
||||
// MARK: - Deinitialization
|
||||
|
||||
|
||||
deinit {
|
||||
Log.perf("DomainListVC deallocated")
|
||||
}
|
||||
|
@ -9,82 +9,82 @@
|
||||
import Cocoa
|
||||
|
||||
class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
|
||||
|
||||
|
||||
// MARK: - Window Identifier
|
||||
|
||||
|
||||
override var windowName: String {
|
||||
return "DomainList"
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
|
||||
@IBOutlet weak var searchToolbarItem: NSSearchToolbarItem!
|
||||
|
||||
|
||||
// MARK: - Window Lifecycle
|
||||
|
||||
|
||||
override func windowDidLoad() {
|
||||
super.windowDidLoad()
|
||||
self.searchToolbarItem.searchField.delegate = self
|
||||
self.searchToolbarItem.searchField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Search functionality
|
||||
|
||||
|
||||
var contentVC: DomainListVC {
|
||||
return self.contentViewController as! DomainListVC
|
||||
}
|
||||
|
||||
|
||||
var searchTimer: Timer?
|
||||
|
||||
|
||||
func controlTextDidChange(_ notification: Notification) {
|
||||
guard let searchField = notification.object as? NSSearchField else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
self.searchTimer?.invalidate()
|
||||
|
||||
|
||||
searchTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false, block: { _ in
|
||||
self.contentVC.searchedFor(text: searchField.stringValue)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Reload functionality
|
||||
|
||||
|
||||
@IBAction func pressedReload(_ sender: Any?) {
|
||||
contentVC.reloadDomains()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func pressedAddLink(_ sender: Any?) {
|
||||
showSelectionWindow()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Add a new site
|
||||
|
||||
|
||||
func showSelectionWindow() {
|
||||
let storyboard = NSStoryboard(name: "Main", bundle : nil)
|
||||
|
||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
let windowController = storyboard.instantiateController(
|
||||
withIdentifier: "showSelectionWindow"
|
||||
) as! NSWindowController
|
||||
|
||||
|
||||
let viewController = windowController.window!
|
||||
.contentViewController as! SelectionVC
|
||||
|
||||
|
||||
viewController.domainListWC = self
|
||||
|
||||
|
||||
self.window?.beginSheet(windowController.window!)
|
||||
}
|
||||
|
||||
|
||||
func startCreateLinkFlow() {
|
||||
self.showFolderSelectionForLink()
|
||||
}
|
||||
|
||||
|
||||
func startCreateProxyFlow() {
|
||||
self.showProxyPopup()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Popups
|
||||
|
||||
|
||||
private func showFolderSelectionForLink() {
|
||||
let dialog = NSOpenPanel()
|
||||
dialog.message = "domain_list.add.modal_description".localized
|
||||
@ -95,37 +95,37 @@ class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate
|
||||
dialog.canChooseFiles = false
|
||||
dialog.beginSheetModal(for: self.window!) { response in
|
||||
let result = dialog.url
|
||||
if (result != nil && response == .OK) {
|
||||
if result != nil && response == .OK {
|
||||
let path: String = result!.path
|
||||
self.showLinkPopup(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func showLinkPopup(_ folder: String) {
|
||||
let storyboard = NSStoryboard(name: "Main", bundle : nil)
|
||||
|
||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
let windowController = storyboard.instantiateController(
|
||||
withIdentifier: "addSiteWindow"
|
||||
) as! NSWindowController
|
||||
|
||||
|
||||
let viewController = windowController.window!.contentViewController as! AddSiteVC
|
||||
viewController.pathControl.url = URL(fileURLWithPath: folder)
|
||||
viewController.inputDomainName.stringValue = String(folder.split(separator: "/").last!)
|
||||
viewController.updateTextField()
|
||||
|
||||
|
||||
self.window?.beginSheet(windowController.window!)
|
||||
}
|
||||
|
||||
|
||||
private func showProxyPopup() {
|
||||
let storyboard = NSStoryboard(name: "Main", bundle : nil)
|
||||
|
||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
let windowController = storyboard.instantiateController(
|
||||
withIdentifier: "addProxyWindow"
|
||||
) as! NSWindowController
|
||||
|
||||
|
||||
// let viewController = windowController.window!.contentViewController as! AddSiteVC
|
||||
|
||||
|
||||
self.window?.beginSheet(windowController.window!)
|
||||
}
|
||||
}
|
||||
|
@ -10,31 +10,31 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class SelectionVC: NSViewController {
|
||||
|
||||
|
||||
weak var domainListWC: DomainListWC?
|
||||
|
||||
|
||||
@IBOutlet weak var textFieldTitle: NSTextField!
|
||||
@IBOutlet weak var textFieldDescription: NSTextField!
|
||||
@IBOutlet weak var buttonCreateLink: NSButton!
|
||||
@IBOutlet weak var buttonCreateProxy: NSButton!
|
||||
@IBOutlet weak var buttonCancel: NSButton!
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
loadStaticLocalisedStrings()
|
||||
}
|
||||
|
||||
|
||||
override func viewDidAppear() {
|
||||
view.window?.makeFirstResponder(buttonCreateLink)
|
||||
}
|
||||
|
||||
|
||||
private func dismissView(outcome: NSApplication.ModalResponse) {
|
||||
guard let window = self.view.window, let parent = window.sheetParent else { return }
|
||||
parent.endSheet(window, returnCode: outcome)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Localisation
|
||||
|
||||
|
||||
func loadStaticLocalisedStrings() {
|
||||
textFieldTitle.stringValue = "selection.title".localized
|
||||
textFieldDescription.stringValue = "selection.description".localized
|
||||
@ -42,21 +42,21 @@ class SelectionVC: NSViewController {
|
||||
buttonCreateLink.title = "selection.create_link".localized
|
||||
buttonCreateProxy.title = "selection.create_proxy".localized
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Outlet Interactions
|
||||
|
||||
|
||||
@IBAction func pressedCreateLink(_ sender: Any) {
|
||||
self.dismissView(outcome: .continue)
|
||||
domainListWC?.startCreateLinkFlow()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func pressedCreateProxy(_ sender: Any) {
|
||||
self.dismissView(outcome: .continue)
|
||||
domainListWC?.startCreateProxyFlow()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func pressedCancel(_ sender: Any) {
|
||||
self.dismissView(outcome: .cancel)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -13,59 +13,58 @@ import Foundation
|
||||
to this object.
|
||||
*/
|
||||
struct ComposerJson: Decodable {
|
||||
|
||||
|
||||
// MARK: - JSON structure
|
||||
|
||||
let dependencies: Dictionary<String, String>?
|
||||
let devDependencies: Dictionary<String, String>?
|
||||
|
||||
let dependencies: [String: String]?
|
||||
let devDependencies: [String: String]?
|
||||
let configuration: Config?
|
||||
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case dependencies = "require"
|
||||
case devDependencies = "require-dev"
|
||||
case configuration = "config"
|
||||
}
|
||||
|
||||
|
||||
struct Config: Decodable {
|
||||
let platform: Platform?
|
||||
}
|
||||
|
||||
|
||||
struct Platform: Decodable {
|
||||
let php: String?
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
|
||||
/**
|
||||
Checks what the PHP version constraint is.
|
||||
Returns a tuple (constraint, location of constraint).
|
||||
*/
|
||||
public func getPhpVersion() -> (String, ValetSite.VersionSource)
|
||||
{
|
||||
public func getPhpVersion() -> (String, ValetSite.VersionSource) {
|
||||
// Check if in platform
|
||||
if configuration?.platform?.php != nil {
|
||||
return (configuration!.platform!.php!, .platform)
|
||||
}
|
||||
|
||||
|
||||
// Check if in dependencies
|
||||
if dependencies?["php"] != nil {
|
||||
return (dependencies!["php"]!, .require)
|
||||
}
|
||||
|
||||
|
||||
// Unknown!
|
||||
return ("???", .unknown)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Checks if any notable dependencies can be resolved.
|
||||
Only notable dependencies are saved.
|
||||
*/
|
||||
public func getNotableDependencies() -> [String: String] {
|
||||
var notable: [String: String] = [:]
|
||||
|
||||
|
||||
var scan = Array(PhpFrameworks.DependencyList.keys)
|
||||
scan.append("php")
|
||||
|
||||
|
||||
scan.forEach { dependency in
|
||||
if dependencies?[dependency] != nil {
|
||||
notable[dependency] = dependencies![dependency]
|
||||
@ -74,7 +73,5 @@ struct ComposerJson: Decodable {
|
||||
|
||||
return notable
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -10,11 +10,11 @@ import Foundation
|
||||
|
||||
class ComposerWindow {
|
||||
|
||||
private var menu: MainMenu? = nil
|
||||
private var menu: MainMenu?
|
||||
private var shouldNotify: Bool! = nil
|
||||
private var completion: ((Bool) -> Void)! = nil
|
||||
private var window: ProgressWindowController? = nil
|
||||
|
||||
private var window: ProgressWindowController?
|
||||
|
||||
/**
|
||||
Updates the global dependencies and runs the completion callback when done.
|
||||
*/
|
||||
@ -22,33 +22,33 @@ class ComposerWindow {
|
||||
self.menu = MainMenu.shared
|
||||
self.shouldNotify = notify
|
||||
self.completion = completion
|
||||
|
||||
|
||||
Paths.shared.detectBinaryPaths()
|
||||
if Paths.composer == nil {
|
||||
presentMissingAlert()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
PhpEnv.shared.isBusy = true
|
||||
menu?.setBusyImage()
|
||||
menu?.rebuild()
|
||||
|
||||
|
||||
window = ProgressWindowController.display(
|
||||
title: "alert.composer_progress.title".localized,
|
||||
description: "alert.composer_progress.info".localized
|
||||
)
|
||||
|
||||
|
||||
window?.setType(info: true)
|
||||
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
let task = Shell.user.createTask(
|
||||
for: "\(Paths.composer!) global update", requiresPath: true
|
||||
)
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.window?.addToConsole("\(Paths.composer!) global update\n")
|
||||
}
|
||||
|
||||
|
||||
task.listen(
|
||||
didReceiveStandardOutputData: { string in
|
||||
DispatchQueue.main.async {
|
||||
@ -63,11 +63,11 @@ class ComposerWindow {
|
||||
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
task.haltListening()
|
||||
|
||||
|
||||
if task.terminationStatus <= 0 {
|
||||
composerUpdateSucceeded()
|
||||
} else {
|
||||
@ -75,12 +75,12 @@ class ComposerWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func composerUpdateSucceeded() {
|
||||
// Closing the window should happen after a slight delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in
|
||||
window?.close()
|
||||
if (shouldNotify) {
|
||||
if shouldNotify {
|
||||
LocalNotification.send(
|
||||
title: "alert.composer_success.title".localized,
|
||||
subtitle: "alert.composer_success.info".localized
|
||||
@ -91,7 +91,7 @@ class ComposerWindow {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func composerUpdateFailed() {
|
||||
// Showing that something failed should be shown immediately
|
||||
DispatchQueue.main.async { [self] in
|
||||
@ -103,18 +103,18 @@ class ComposerWindow {
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Main Menu Update
|
||||
|
||||
|
||||
private func removeBusyStatus() {
|
||||
PhpEnv.shared.isBusy = false
|
||||
DispatchQueue.main.async { [self] in
|
||||
menu?.updatePhpVersionInStatusBar()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Alert
|
||||
|
||||
|
||||
private func presentMissingAlert() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
struct PhpFrameworks {
|
||||
|
||||
|
||||
/**
|
||||
This list should probably be reversed when checked, because some of these
|
||||
will also require either `laravel/framework` or `symfony/symfony`.
|
||||
@ -17,10 +17,10 @@ struct PhpFrameworks {
|
||||
public static let DependencyList = [
|
||||
|
||||
// COMMON FRAMEWORKS
|
||||
"laravel/framework" : "Laravel",
|
||||
"laravel/framework": "Laravel",
|
||||
"symfony/symfony": "Symfony",
|
||||
"laravel/lumen": "Lumen",
|
||||
|
||||
|
||||
// VARIOUS CMS
|
||||
"roots/bedrock": "Bedrock",
|
||||
"cakephp/app": "CakePHP",
|
||||
@ -37,15 +37,15 @@ struct PhpFrameworks {
|
||||
"johnpbloch/wordpress-core": "WordPress",
|
||||
"zendframework/zendframework": "Zend",
|
||||
"zendframework/zend-mvc": "Zend",
|
||||
"typo3/cms-core": "Typo3",
|
||||
|
||||
"typo3/cms-core": "Typo3"
|
||||
|
||||
// TODO (6.0): Handle these in v6.0
|
||||
// "magento/*": "Magento",
|
||||
// "concrete5/*": "Concrete5",
|
||||
// "contao/*": "Contao",
|
||||
// "slim/*": "Slim",
|
||||
]
|
||||
|
||||
|
||||
public static let FileMapping: [String: [String]] = [
|
||||
"Drupal": [
|
||||
// Legacy installations
|
||||
@ -61,10 +61,10 @@ struct PhpFrameworks {
|
||||
],
|
||||
"Typo3": [
|
||||
"/typo3",
|
||||
"/public/typo3",
|
||||
"/public/typo3"
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
/**
|
||||
There are two cases where users are unlikely to use `composer`,
|
||||
when setting up a Drupal or a WordPress project. For performance
|
||||
@ -75,13 +75,13 @@ struct PhpFrameworks {
|
||||
let found = entry.value
|
||||
.map { path in return Filesystem.fileExists(basePath + path) }
|
||||
.contains(true)
|
||||
|
||||
|
||||
if found {
|
||||
return entry.key
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
class HomebrewDiagnostics {
|
||||
|
||||
|
||||
/**
|
||||
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`).
|
||||
@ -17,50 +17,61 @@ class HomebrewDiagnostics {
|
||||
|
||||
This check only needs to be performed if the `shivammathur/php` tap is active.
|
||||
*/
|
||||
public static func hasAliasConflict() -> Bool
|
||||
{
|
||||
public static func hasAliasConflict() -> Bool {
|
||||
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
|
||||
|
||||
|
||||
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
|
||||
Log.info("The user does not appear to have tapped: shivammathur/php")
|
||||
return false
|
||||
} else {
|
||||
Log.info("The user DOES have the following tapped: shivammathur/php")
|
||||
Log.info("Checking for `php` formula conflicts...")
|
||||
|
||||
|
||||
let tapPhp = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self,
|
||||
from: tapAlias.data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
|
||||
if tapPhp.version != PhpEnv.brewPhpVersion {
|
||||
Log.warn("The `php` formula alias seems to be the different between the tap and core. This could be a problem!")
|
||||
Log.warn("The `php` formula alias seems to be the different between the tap and core. "
|
||||
+ "This could be a problem!")
|
||||
Log.info("Determining whether both of these versions are installed...")
|
||||
|
||||
|
||||
let bothInstalled = PhpEnv.shared.availablePhpVersions.contains(tapPhp.version)
|
||||
&& PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion)
|
||||
|
||||
|
||||
if bothInstalled {
|
||||
Log.warn("Both conflicting aliases seem to be installed, warning the user!")
|
||||
} else {
|
||||
Log.info("Conflicting aliases are not both installed, seems fine!")
|
||||
}
|
||||
|
||||
|
||||
return bothInstalled
|
||||
}
|
||||
|
||||
|
||||
Log.info("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static func presentAlertAboutConflict() {
|
||||
DispatchQueue.main.async {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.php_alias_conflict.title".localized,
|
||||
subtitle: "alert.php_alias_conflict.info".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
In order to see if we support the --json syntax, we'll query nginx.
|
||||
If the JSON response cannot be parsed, Homebrew is probably out of date.
|
||||
*/
|
||||
public static func cannotLoadService(_ name: String = "nginx") -> Bool
|
||||
{
|
||||
public static func cannotLoadService(_ name: String = "nginx") -> Bool {
|
||||
let serviceInfo = try? JSONDecoder().decode(
|
||||
[HomebrewService].self,
|
||||
from: Shell.pipe(
|
||||
@ -68,7 +79,7 @@ class HomebrewDiagnostics {
|
||||
requiresPath: true
|
||||
).data(using: .utf8)!
|
||||
)
|
||||
|
||||
|
||||
return serviceInfo == nil
|
||||
}
|
||||
}
|
||||
|
@ -9,32 +9,32 @@
|
||||
import Foundation
|
||||
|
||||
class NginxConfiguration {
|
||||
|
||||
|
||||
/** Contents of the Nginx file in question, as a string. */
|
||||
var contents: String!
|
||||
|
||||
|
||||
/** The name of the domain, usually derived from the name of the file. */
|
||||
var domain: String
|
||||
|
||||
|
||||
/** The TLD of the domain, usually derived from the name of the file. */
|
||||
var tld: String
|
||||
|
||||
|
||||
init(filePath: String) {
|
||||
let path = filePath.replacingOccurrences(
|
||||
of: "~",
|
||||
with: "/Users/\(Paths.whoami)"
|
||||
)
|
||||
|
||||
|
||||
self.contents = try! String(contentsOfFile: path)
|
||||
|
||||
|
||||
let domain = String(path.split(separator: "/").last!)
|
||||
let tld = String(domain.split(separator: ".").last!)
|
||||
|
||||
|
||||
self.domain = domain
|
||||
.replacingOccurrences(of: ".\(tld)", with: "")
|
||||
self.tld = tld
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Retrieves what address this domain is proxying.
|
||||
*/
|
||||
@ -43,13 +43,13 @@ class NginxConfiguration {
|
||||
pattern: #"proxy_pass (?<proxy>.*:\d*);"#,
|
||||
options: []
|
||||
)
|
||||
|
||||
guard let match = regex.firstMatch(in: contents, range: NSMakeRange(0, contents.count))
|
||||
|
||||
guard let match = regex.firstMatch(in: contents, range: NSRange(location: 0, length: contents.count))
|
||||
else { return nil }
|
||||
|
||||
|
||||
return contents[Range(match.range(withName: "proxy"), in: contents)!]
|
||||
}()
|
||||
|
||||
|
||||
/**
|
||||
Retrieves which isolated version is active for this domain (if applicable).
|
||||
*/
|
||||
@ -59,13 +59,13 @@ class NginxConfiguration {
|
||||
pattern: #"(ISOLATED_PHP_VERSION=(php)?(@)?)((?<major>\d)(.)?(?<minor>\d))"#,
|
||||
options: []
|
||||
)
|
||||
|
||||
guard let match = regex.firstMatch(in: contents, range: NSMakeRange(0, contents.count))
|
||||
|
||||
guard let match = regex.firstMatch(in: contents, range: NSRange(location: 0, length: contents.count))
|
||||
else { return nil }
|
||||
|
||||
let major: String = contents[Range(match.range(withName: "major"), in: contents)!],
|
||||
minor: String = contents[Range(match.range(withName: "minor"), in: contents)!]
|
||||
|
||||
|
||||
return "\(major).\(minor)"
|
||||
}()
|
||||
}
|
||||
|
@ -9,19 +9,19 @@
|
||||
import Foundation
|
||||
|
||||
protocol DomainListable {
|
||||
|
||||
|
||||
func getListableName() -> String
|
||||
|
||||
|
||||
func getListableSecured() -> Bool
|
||||
|
||||
|
||||
func getListableAbsolutePath() -> String
|
||||
|
||||
|
||||
func getListablePhpVersion() -> String
|
||||
|
||||
|
||||
func getListableKind() -> String
|
||||
|
||||
|
||||
func getListableType() -> String
|
||||
|
||||
|
||||
func getListableUrl() -> URL?
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
protocol ProxyScanner {
|
||||
|
||||
|
||||
func resolveProxies(directoryPath: String) -> [ValetProxy]
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,10 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class ValetProxyScanner: ProxyScanner
|
||||
{
|
||||
func resolveProxies(directoryPath: String) -> [ValetProxy]
|
||||
{
|
||||
class ValetProxyScanner: ProxyScanner {
|
||||
func resolveProxies(directoryPath: String) -> [ValetProxy] {
|
||||
return try! FileManager
|
||||
.default
|
||||
.contentsOfDirectory(atPath: directoryPath)
|
||||
|
@ -9,5 +9,5 @@
|
||||
import Foundation
|
||||
|
||||
extension ValetProxy {
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,46 +8,45 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class ValetProxy: DomainListable
|
||||
{
|
||||
class ValetProxy: DomainListable {
|
||||
var domain: String
|
||||
var tld: String
|
||||
var target: String
|
||||
var secured: Bool = false
|
||||
|
||||
|
||||
init(_ configuration: NginxConfiguration) {
|
||||
self.domain = configuration.domain
|
||||
self.tld = configuration.tld
|
||||
self.target = configuration.proxy!
|
||||
self.secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - DomainListable Protocol
|
||||
|
||||
|
||||
func getListableName() -> String {
|
||||
return self.domain
|
||||
}
|
||||
|
||||
|
||||
func getListableSecured() -> Bool {
|
||||
return self.secured
|
||||
}
|
||||
|
||||
|
||||
func getListableAbsolutePath() -> String {
|
||||
return self.domain
|
||||
}
|
||||
|
||||
|
||||
func getListablePhpVersion() -> String {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
func getListableKind() -> String {
|
||||
return "proxy"
|
||||
}
|
||||
|
||||
|
||||
func getListableType() -> String {
|
||||
return "proxy"
|
||||
}
|
||||
|
||||
|
||||
func getListableUrl() -> URL? {
|
||||
return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)")
|
||||
}
|
||||
|
@ -6,33 +6,35 @@
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
class FakeSiteScanner: SiteScanner
|
||||
{
|
||||
class FakeSiteScanner: SiteScanner {
|
||||
let fakes = [
|
||||
ValetSite(fakeWithName: "laravel", tld: "test", secure: true, path: "~/Code/laravel/framework", linked: true),
|
||||
|
||||
ValetSite(fakeWithName: "tailwind", tld: "test", secure: true, path: "~/Code/tailwind/site", linked: true, constraint: "8.0"),
|
||||
|
||||
ValetSite(fakeWithName: "forge", tld: "test", secure: true, path: "~/Code/laravel/forge", linked: true),
|
||||
|
||||
ValetSite(fakeWithName: "laravel", tld: "test", secure: true,
|
||||
path: "~/Code/laravel/framework", linked: true),
|
||||
|
||||
ValetSite(fakeWithName: "tailwind", tld: "test", secure: true,
|
||||
path: "~/Code/tailwind/site", linked: true, constraint: "8.0"),
|
||||
|
||||
ValetSite(fakeWithName: "forge", tld: "test", secure: true,
|
||||
path: "~/Code/laravel/forge", linked: true),
|
||||
|
||||
ValetSite(fakeWithName: "concord", tld: "test", secure: false,
|
||||
path: "~/Code/concord", linked: true, driver: "Laravel (^8.0)", constraint: "^7.4", isolated: "7.4"),
|
||||
|
||||
|
||||
ValetSite(fakeWithName: "drupal", tld: "test", secure: false,
|
||||
path: "~/Sites/drupal", linked: false, driver: "Drupal", constraint: "^7.4", isolated: "7.4"),
|
||||
|
||||
|
||||
ValetSite(fakeWithName: "wordpress", tld: "test", secure: false,
|
||||
path: "~/Sites/wordpress", linked: false, driver: "WordPress", constraint: "^7.4", isolated: "7.4")
|
||||
]
|
||||
|
||||
|
||||
func resolveSiteCount(paths: [String]) -> Int {
|
||||
return fakes.count
|
||||
}
|
||||
|
||||
|
||||
func resolveSitesFrom(paths: [String]) -> [ValetSite] {
|
||||
return fakes
|
||||
}
|
||||
|
||||
|
||||
func resolveSite(path: String) -> ValetSite? {
|
||||
return nil
|
||||
}
|
||||
|
@ -8,11 +8,10 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SiteScanner
|
||||
{
|
||||
protocol SiteScanner {
|
||||
func resolveSiteCount(paths: [String]) -> Int
|
||||
|
||||
|
||||
func resolveSitesFrom(paths: [String]) -> [ValetSite]
|
||||
|
||||
|
||||
func resolveSite(path: String) -> ValetSite?
|
||||
}
|
||||
|
@ -8,39 +8,38 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class ValetSiteScanner: SiteScanner
|
||||
{
|
||||
class ValetSiteScanner: SiteScanner {
|
||||
func resolveSiteCount(paths: [String]) -> Int {
|
||||
return paths.map { path in
|
||||
|
||||
|
||||
let entries = try! FileManager.default
|
||||
.contentsOfDirectory(atPath: path)
|
||||
|
||||
|
||||
return entries
|
||||
.map { self.isSite($0, forPath: path) }
|
||||
.filter{ $0 == true}
|
||||
.filter { $0 == true}
|
||||
.count
|
||||
|
||||
|
||||
}.reduce(0, +)
|
||||
}
|
||||
|
||||
|
||||
func resolveSitesFrom(paths: [String]) -> [ValetSite] {
|
||||
var sites: [ValetSite] = []
|
||||
|
||||
|
||||
paths.forEach { path in
|
||||
let entries = try! FileManager.default
|
||||
.contentsOfDirectory(atPath: path)
|
||||
|
||||
|
||||
return entries.forEach {
|
||||
if let site = self.resolveSite(path: "\(path)/\($0)") {
|
||||
sites.append(site)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return sites
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Determines whether the site can be resolved as a symbolic link or as a directory.
|
||||
Regular files are ignored, and the site is added to Valet's list of sites.
|
||||
@ -48,46 +47,46 @@ class ValetSiteScanner: SiteScanner
|
||||
func resolveSite(path: String) -> ValetSite? {
|
||||
// Get the TLD from the global Valet object
|
||||
let tld = Valet.shared.config.tld
|
||||
|
||||
|
||||
// See if the file is a symlink, if so, resolve it
|
||||
guard let attrs = try? FileManager.default.attributesOfItem(atPath: path) else {
|
||||
Log.warn("Could not parse the site: \(path), skipping!")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// We can also determine whether the thing at the path is a directory, too
|
||||
let type = attrs[FileAttributeKey.type] as! FileAttributeType
|
||||
|
||||
|
||||
// We should also check that we can interpret the path correctly
|
||||
if URL(fileURLWithPath: path).lastPathComponent == "" {
|
||||
Log.warn("Could not parse the site: \(path), skipping!")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if type == FileAttributeType.typeSymbolicLink {
|
||||
return ValetSite(aliasPath: path, tld: tld)
|
||||
} else if type == FileAttributeType.typeDirectory {
|
||||
return ValetSite(absolutePath: path, tld: tld)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Determines whether the site can be resolved as a symbolic link or as a directory.
|
||||
Regular files are ignored. Returns true if the path can be parsed.
|
||||
*/
|
||||
private func isSite(_ entry: String, forPath path: String) -> Bool {
|
||||
let siteDir = path + "/" + entry
|
||||
|
||||
|
||||
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
|
||||
|
||||
|
||||
let type = attrs[FileAttributeKey.type] as! FileAttributeType
|
||||
|
||||
|
||||
if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
extension ValetSite {
|
||||
|
||||
|
||||
convenience init(
|
||||
fakeWithName name: String,
|
||||
tld: String,
|
||||
@ -23,14 +23,14 @@ extension ValetSite {
|
||||
self.init(name: name, tld: tld, absolutePath: path, aliasPath: nil, makeDeterminations: false)
|
||||
self.secured = secure
|
||||
self.composerPhp = constraint
|
||||
|
||||
|
||||
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
|
||||
.map { string in
|
||||
return PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
||||
return !PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
||||
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
.count > 0
|
||||
.isEmpty
|
||||
}.contains(true)
|
||||
|
||||
|
||||
self.driver = driver
|
||||
self.driverDeterminedByComposer = true
|
||||
if linked {
|
||||
@ -40,5 +40,5 @@ extension ValetSite {
|
||||
self.isolatedPhpVersion = PhpInstallation(isolated)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,63 +9,63 @@
|
||||
import Foundation
|
||||
|
||||
class ValetSite: DomainListable {
|
||||
|
||||
|
||||
/// Name of the site. Does not include the TLD.
|
||||
var name: String
|
||||
|
||||
|
||||
/// The absolute path to the directory that is served.
|
||||
var absolutePath: String
|
||||
|
||||
|
||||
/// The absolute path to the directory that is served,
|
||||
/// replacing the user's home folder with ~.
|
||||
lazy var absolutePathRelative: String = {
|
||||
return self.absolutePath
|
||||
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
|
||||
}()
|
||||
|
||||
|
||||
/// The TLD used to locate this site.
|
||||
var tld: String = "test"
|
||||
|
||||
|
||||
/// The PHP version that is being used to serve this site specifically (if not global).
|
||||
var isolatedPhpVersion: PhpInstallation?
|
||||
|
||||
|
||||
/// Location of the alias. If set, this is a linked domain.
|
||||
var aliasPath: String?
|
||||
|
||||
|
||||
/// Whether the site has been secured.
|
||||
var secured: Bool!
|
||||
|
||||
|
||||
/// What driver is currently in use. If not detected, defaults to nil.
|
||||
var driver: String? = nil
|
||||
|
||||
var driver: String?
|
||||
|
||||
/// Whether the driver was determined by checking the Composer file.
|
||||
var driverDeterminedByComposer: Bool = false
|
||||
|
||||
|
||||
/// A list of notable Composer dependencies.
|
||||
var notableComposerDependencies: [String: String] = [:]
|
||||
|
||||
|
||||
/// The PHP version as discovered in `composer.json` or in .valetphprc.
|
||||
var composerPhp: String = "???"
|
||||
|
||||
|
||||
/// Check whether the PHP version is valid for the currently linked version.
|
||||
var composerPhpCompatibleWithLinked: Bool = false
|
||||
|
||||
|
||||
/// How the PHP version was determined.
|
||||
var composerPhpSource: VersionSource = .unknown
|
||||
|
||||
|
||||
/// Which version of PHP is actually used to serve this site.
|
||||
var servingPhpVersion: String {
|
||||
return self.isolatedPhpVersion?.versionNumber.homebrewVersion
|
||||
?? PhpEnv.phpInstall.version.short
|
||||
}
|
||||
|
||||
|
||||
enum VersionSource: String {
|
||||
case unknown = "unknown"
|
||||
case require = "require"
|
||||
case platform = "platform"
|
||||
case valetphprc = "valetphprc"
|
||||
case unknown
|
||||
case require
|
||||
case platform
|
||||
case valetphprc
|
||||
}
|
||||
|
||||
|
||||
init(
|
||||
name: String,
|
||||
tld: String,
|
||||
@ -78,7 +78,7 @@ class ValetSite: DomainListable {
|
||||
self.absolutePath = absolutePath
|
||||
self.aliasPath = aliasPath
|
||||
self.secured = false
|
||||
|
||||
|
||||
if makeDeterminations {
|
||||
determineSecured()
|
||||
determineComposerPhpVersion()
|
||||
@ -86,25 +86,26 @@ class ValetSite: DomainListable {
|
||||
determineIsolated()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
convenience init(absolutePath: String, tld: String) {
|
||||
let name = URL(fileURLWithPath: absolutePath).lastPathComponent
|
||||
self.init(name: name, tld: tld, absolutePath: absolutePath)
|
||||
}
|
||||
|
||||
|
||||
convenience init(aliasPath: String, tld: String) {
|
||||
let name = URL(fileURLWithPath: aliasPath).lastPathComponent
|
||||
let absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
|
||||
self.init(name: name, tld: tld, absolutePath: absolutePath, aliasPath: aliasPath)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Determine whether a site is isolated.
|
||||
*/
|
||||
public func determineIsolated() {
|
||||
if let version = ValetSite.isolatedVersion("~/.config/valet/Nginx/\(self.name).\(self.tld)") {
|
||||
if (!PhpEnv.shared.cachedPhpInstallations.keys.contains(version)) {
|
||||
Log.err("The PHP version \(version) is isolated for the site \(self.name) but that PHP version is unavailable.")
|
||||
if !PhpEnv.shared.cachedPhpInstallations.keys.contains(version) {
|
||||
Log.err("The PHP version \(version) is isolated for the site \(self.name) "
|
||||
+ "but that PHP version is unavailable.")
|
||||
return
|
||||
}
|
||||
self.isolatedPhpVersion = PhpEnv.shared.cachedPhpInstallations[version]
|
||||
@ -112,7 +113,7 @@ class ValetSite: DomainListable {
|
||||
self.isolatedPhpVersion = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Checks if a certificate file can be found in the `valet/Certificates` directory.
|
||||
- Note: The file is not validated, only its presence is checked.
|
||||
@ -120,7 +121,7 @@ class ValetSite: DomainListable {
|
||||
public func determineSecured() {
|
||||
secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.name).\(self.tld).key")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Checks if `composer.json` exists in the folder, and extracts notable information:
|
||||
|
||||
@ -132,36 +133,36 @@ class ValetSite: DomainListable {
|
||||
with the currently linked version of PHP (see `composerPhpMatchesSystem`).
|
||||
*/
|
||||
public func determineComposerPhpVersion() {
|
||||
|
||||
|
||||
self.determineComposerInformation()
|
||||
self.determineValetPhpFileInfo()
|
||||
|
||||
if self.composerPhp == "???" {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Split the composer list (on "|") to evaluate multiple constraints
|
||||
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
|
||||
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
|
||||
.map { string in
|
||||
return PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
||||
return !PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
||||
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
.count > 0
|
||||
.isEmpty
|
||||
}.contains(true)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Determine the driver to be displayed in the list of sites. In v5.0, this has been changed
|
||||
to load the "framework" or "project type" instead.
|
||||
*/
|
||||
public func determineDriver() {
|
||||
self.determineDriverViaComposer()
|
||||
|
||||
|
||||
if self.driver == nil {
|
||||
self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Check the dependency list and see if a particular dependency can't be found.
|
||||
We'll revert the dependency list so that Laravel and Symfony are detected last.
|
||||
@ -171,28 +172,28 @@ class ValetSite: DomainListable {
|
||||
*/
|
||||
private func determineDriverViaComposer() {
|
||||
self.driverDeterminedByComposer = true
|
||||
|
||||
|
||||
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
|
||||
if self.notableComposerDependencies.keys.contains(key) {
|
||||
self.driver = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Checks the contents of the composer.json file and determine the notable dependencies,
|
||||
as well as the requested PHP version. If no composer.json file is found, nothing happens.
|
||||
*/
|
||||
private func determineComposerInformation() {
|
||||
let path = "\(absolutePath)/composer.json"
|
||||
|
||||
|
||||
do {
|
||||
if Filesystem.fileExists(path) {
|
||||
let decoded = try JSONDecoder().decode(
|
||||
ComposerJson.self,
|
||||
from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)!
|
||||
)
|
||||
|
||||
|
||||
(self.composerPhp, self.composerPhpSource) = decoded.getPhpVersion()
|
||||
self.notableComposerDependencies = decoded.getNotableDependencies()
|
||||
}
|
||||
@ -200,13 +201,13 @@ class ValetSite: DomainListable {
|
||||
Log.err("Something went wrong reading the Composer JSON file.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Checks the contents of the .valetphprc file and determine the version, if possible.
|
||||
*/
|
||||
private func determineValetPhpFileInfo() {
|
||||
let path = "\(absolutePath)/.valetphprc"
|
||||
|
||||
|
||||
do {
|
||||
if Filesystem.fileExists(path) {
|
||||
let contents = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
|
||||
@ -219,45 +220,45 @@ class ValetSite: DomainListable {
|
||||
Log.err("Something went wrong parsing the .valetphprc file")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - File Parsing
|
||||
|
||||
|
||||
public static func isolatedVersion(_ filePath: String) -> String? {
|
||||
if Filesystem.fileExists(filePath) {
|
||||
return NginxConfiguration
|
||||
.init(filePath: filePath)
|
||||
.isolatedVersion
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - DomainListable Protocol
|
||||
|
||||
|
||||
func getListableName() -> String {
|
||||
return self.name
|
||||
}
|
||||
|
||||
|
||||
func getListableSecured() -> Bool {
|
||||
return self.secured
|
||||
}
|
||||
|
||||
|
||||
func getListableAbsolutePath() -> String {
|
||||
return self.absolutePath
|
||||
}
|
||||
|
||||
|
||||
func getListablePhpVersion() -> String {
|
||||
return self.servingPhpVersion
|
||||
}
|
||||
|
||||
|
||||
func getListableKind() -> String {
|
||||
return (self.aliasPath == nil) ? "linked" : "parked"
|
||||
}
|
||||
|
||||
|
||||
func getListableType() -> String {
|
||||
return self.driver ?? "ZZZ"
|
||||
}
|
||||
|
||||
|
||||
func getListableUrl() -> URL? {
|
||||
return URL(string: "\(self.secured ? "https://" : "http://")\(self.name).\(Valet.shared.config.tld)")
|
||||
}
|
||||
|
@ -9,32 +9,32 @@
|
||||
import Foundation
|
||||
|
||||
class Valet {
|
||||
|
||||
|
||||
enum FeatureFlag {
|
||||
case isolatedSites,
|
||||
supportForPhp56
|
||||
}
|
||||
|
||||
|
||||
static let shared = Valet()
|
||||
|
||||
|
||||
/// The version of Valet that was detected.
|
||||
var version: String! = nil
|
||||
|
||||
|
||||
/// The Valet configuration file.
|
||||
var config: Valet.Configuration!
|
||||
|
||||
|
||||
/// A cached list of sites that were detected after analyzing the paths set up for Valet.
|
||||
var sites: [ValetSite] = []
|
||||
|
||||
|
||||
/// A cached list of proxies that were detecting after analyzing the Nginx paths.
|
||||
var proxies: [ValetProxy] = []
|
||||
|
||||
|
||||
/// Whether we're busy with some blocking operation.
|
||||
var isBusy: Bool = false
|
||||
|
||||
|
||||
/// Various feature flags. Enabled based on the installed Valet version.
|
||||
var features: [FeatureFlag] = []
|
||||
|
||||
|
||||
/// When initialising the Valet singleton, assume no sites or proxies loaded.
|
||||
/// We will load the version later.
|
||||
init() {
|
||||
@ -42,7 +42,7 @@ class Valet {
|
||||
self.sites = []
|
||||
self.proxies = []
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
If marketing mode is enabled, show a list of sites that are used for promotional screenshots.
|
||||
This can be done by swapping out the real Valet scanner with one that always returns a fixed
|
||||
@ -52,25 +52,43 @@ class Valet {
|
||||
if ProcessInfo.processInfo.environment["PHPMON_MARKETING_MODE"] != nil {
|
||||
return FakeSiteScanner()
|
||||
}
|
||||
|
||||
|
||||
return ValetSiteScanner()
|
||||
}
|
||||
|
||||
|
||||
static var proxyScanner: ProxyScanner {
|
||||
return ValetProxyScanner()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Check if a particular feature is enabled.
|
||||
*/
|
||||
public static func enabled(feature: FeatureFlag) -> Bool {
|
||||
return self.shared.features.contains(feature)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Retrieve a list of all domains, including sites & proxies.
|
||||
*/
|
||||
public static func getDomainListable() -> [DomainListable] {
|
||||
return self.shared.sites + self.shared.proxies
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Notify the user about a non-default TLD being set.
|
||||
*/
|
||||
public static func notifyAboutUnsupportedTLD() {
|
||||
if Valet.shared.config.tld != "test" {
|
||||
DispatchQueue.main.async {
|
||||
BetterAlert().withInformation(
|
||||
title: "alert.warnings.tld_issue.title".localized,
|
||||
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
||||
description: "alert.warnings.tld_issue.description".localized
|
||||
).withPrimary(text: "OK").show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
We don't want to load the initial config.json file as soon as the class is initialised.
|
||||
|
||||
@ -83,7 +101,7 @@ class Valet {
|
||||
public func loadConfiguration() {
|
||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".config/valet/config.json")
|
||||
|
||||
|
||||
do {
|
||||
config = try JSONDecoder().decode(
|
||||
Valet.Configuration.self,
|
||||
@ -93,7 +111,7 @@ class Valet {
|
||||
Log.err(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Starts the preload of sites, but only if the maximum amount of sites is 30.
|
||||
For users with more sites, the site list is loaded when they bring up the site list window.
|
||||
@ -110,21 +128,21 @@ class Valet {
|
||||
Log.info("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Reloads the list of sites, assuming that the list isn't being reloaded at the time.
|
||||
(We don't want to do duplicate or parallel work!)
|
||||
*/
|
||||
public func reloadSites() {
|
||||
loadConfiguration()
|
||||
|
||||
if (isBusy) {
|
||||
|
||||
if isBusy {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
resolvePaths()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Depending on the version of Valet that is active, the feature set of PHP Monitor will change.
|
||||
|
||||
@ -132,9 +150,9 @@ class Valet {
|
||||
`enabled(feature)`, which contains information about the feature set of the version of Valet that is currently
|
||||
in use. This allows PHP Monitor to do different things when Valet 3.0 is enabled.
|
||||
*/
|
||||
public func evaluateFeatureSupport() -> Void {
|
||||
public func evaluateFeatureSupport() {
|
||||
let isOlderThanVersionThree = version.versionCompare("3.0") == .orderedAscending
|
||||
|
||||
|
||||
if isOlderThanVersionThree {
|
||||
self.features.append(.supportForPhp56)
|
||||
} else {
|
||||
@ -142,16 +160,16 @@ class Valet {
|
||||
self.features.append(.isolatedSites)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Checks if the version of Valet is more recent than the minimum version required for PHP Monitor to function.
|
||||
Should this procedure fail, the user will get an alert notifying them that the version of Valet they have
|
||||
installed is not recent enough.
|
||||
*/
|
||||
public func validateVersion() -> Void {
|
||||
public func validateVersion() {
|
||||
// 1. Evaluate feature support
|
||||
Valet.shared.evaluateFeatureSupport()
|
||||
|
||||
|
||||
// 2. Notify user if the version is too old
|
||||
if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending {
|
||||
let version = version
|
||||
@ -160,16 +178,20 @@ class Valet {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.min_valet_version.title".localized,
|
||||
subtitle:"alert.min_valet_version.info".localized(version!, Constants.MinimumRecommendedValetVersion)
|
||||
subtitle: "alert.min_valet_version.info".localized(
|
||||
version!,
|
||||
Constants.MinimumRecommendedValetVersion
|
||||
)
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
Log.info("Valet version \(version!) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
|
||||
Log.info("Valet version \(version!) is recent enough, OK " +
|
||||
"(recommended: \(Constants.MinimumRecommendedValetVersion))")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Returns a count of how many sites are linked and parked.
|
||||
*/
|
||||
@ -177,19 +199,19 @@ class Valet {
|
||||
return Self.siteScanner
|
||||
.resolveSiteCount(paths: config.paths)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Resolves all paths and creates linked or parked site instances that can be referenced later.
|
||||
*/
|
||||
private func resolvePaths() {
|
||||
isBusy = true
|
||||
|
||||
|
||||
sites = Self.siteScanner
|
||||
.resolveSitesFrom(paths: config.paths)
|
||||
.sorted {
|
||||
$0.absolutePath < $1.absolutePath
|
||||
}
|
||||
|
||||
|
||||
proxies = Self.proxyScanner
|
||||
.resolveProxies(
|
||||
directoryPath: FileManager.default
|
||||
@ -197,32 +219,34 @@ class Valet {
|
||||
.appendingPathComponent(".config/valet/Nginx")
|
||||
.path
|
||||
)
|
||||
|
||||
|
||||
if let defaultPath = Valet.shared.config.defaultSite,
|
||||
let site = ValetSiteScanner().resolveSite(path: defaultPath) {
|
||||
sites.insert(site, at: 0)
|
||||
}
|
||||
|
||||
|
||||
isBusy = false
|
||||
}
|
||||
|
||||
|
||||
struct Configuration: Decodable {
|
||||
/// Top level domain suffix. Usually "test" but can be set to something else.
|
||||
/// - Important: Does not include the actual dot. ("test", not ".test"!)
|
||||
let tld: String
|
||||
|
||||
|
||||
/// The paths that need to be checked.
|
||||
let paths: [String]
|
||||
|
||||
|
||||
/// The loopback address. Optional.
|
||||
let loopback: String?
|
||||
|
||||
|
||||
/// The default site that is served if the domain is not found. Optional.
|
||||
let defaultSite: String?
|
||||
|
||||
|
||||
// swiftlint:disable nesting
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case tld, paths, loopback, defaultSite = "default"
|
||||
}
|
||||
// swiftlint:enable nesting
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class HeaderView: NSView, XibLoadable {
|
||||
|
||||
|
||||
@IBOutlet weak var textField: NSTextField!
|
||||
|
||||
|
||||
static func asMenuItem(text: String) -> NSMenuItem {
|
||||
let view = Self.createFromXib()
|
||||
view!.textField.stringValue = text.uppercased()
|
||||
@ -21,5 +21,5 @@ class HeaderView: NSView, XibLoadable {
|
||||
item.target = self
|
||||
return item
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,16 +9,16 @@
|
||||
import Foundation
|
||||
|
||||
extension MainMenu {
|
||||
|
||||
|
||||
// MARK: - Nicer callbacks
|
||||
|
||||
|
||||
enum AsyncBehaviour {
|
||||
case setsBusyUI
|
||||
case reloadsPhpInstallation
|
||||
case updatesMenuBarContents
|
||||
case broadcastServicesUpdate
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Attempts asynchronous execution of a callback that may throw an `Error`.
|
||||
While the callback is being executed, the UI will be marked as busy.
|
||||
@ -48,40 +48,40 @@ extension MainMenu {
|
||||
if behaviours.contains(.reloadsPhpInstallation) {
|
||||
PhpEnv.shared.isBusy = true
|
||||
}
|
||||
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
setBusyImage()
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||
var error: Error? = nil
|
||||
|
||||
var error: Error?
|
||||
|
||||
do { try execute() } catch let e { error = e }
|
||||
|
||||
|
||||
if behaviours.contains(.setsBusyUI) {
|
||||
PhpEnv.shared.isBusy = false
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async { [self] in
|
||||
if behaviours.contains(.reloadsPhpInstallation) {
|
||||
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
||||
}
|
||||
|
||||
|
||||
if behaviours.contains(.updatesMenuBarContents) {
|
||||
updatePhpVersionInStatusBar()
|
||||
} else if behaviours.contains(.setsBusyUI) {
|
||||
} else if behaviours.contains(.setsBusyUI) {
|
||||
refreshIcon()
|
||||
}
|
||||
|
||||
|
||||
if behaviours.contains(.broadcastServicesUpdate) {
|
||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||
}
|
||||
|
||||
|
||||
error == nil ? success() : failure(error!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func asyncWithBusyUI(
|
||||
_ execute: @escaping () throws -> Void,
|
||||
completion: @escaping () -> Void = {}
|
||||
@ -92,5 +92,5 @@ extension MainMenu {
|
||||
completion()
|
||||
}, behaviours: [.setsBusyUI])
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,15 +10,15 @@ import Foundation
|
||||
import AppKit
|
||||
|
||||
extension MainMenu {
|
||||
|
||||
|
||||
@objc func fixMyValet() {
|
||||
let previousVersion = PhpEnv.phpInstall.version.short
|
||||
|
||||
|
||||
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
|
||||
presentAlertForMissingFormula()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if !BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_my_valet.title".localized,
|
||||
@ -26,12 +26,11 @@ extension MainMenu {
|
||||
)
|
||||
.withPrimary(text: "alert.fix_my_valet.ok".localized)
|
||||
.withSecondary(text: "alert.fix_my_valet.cancel".localized)
|
||||
.didSelectPrimary()
|
||||
{
|
||||
.didSelectPrimary() {
|
||||
Log.info("The user has chosen to abort Fix My Valet")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Actions.fixMyValet {
|
||||
DispatchQueue.main.async {
|
||||
if previousVersion == PhpEnv.brewPhpVersion {
|
||||
@ -42,7 +41,7 @@ extension MainMenu {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func presentAlertForMissingFormula() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
@ -52,7 +51,7 @@ extension MainMenu {
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
private func presentAlertForSameVersion() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
@ -63,7 +62,7 @@ extension MainMenu {
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
private func presentAlertForDifferentVersion(version: String) {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
@ -76,10 +75,10 @@ extension MainMenu {
|
||||
MainMenu.shared.switchToPhpVersion(version)
|
||||
})
|
||||
.withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnv.brewPhpVersion))
|
||||
.withTertiary(text: "", action: { alert in
|
||||
.withTertiary(text: "", action: { _ in
|
||||
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
|
||||
})
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -17,14 +17,14 @@ extension MainMenu {
|
||||
DispatchQueue.main.async {
|
||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
}
|
||||
|
||||
|
||||
if await Startup().checkEnvironment() {
|
||||
self.onEnvironmentPass()
|
||||
} else {
|
||||
self.onEnvironmentFail()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
When the environment is all clear and the app can run, let's go.
|
||||
*/
|
||||
@ -33,72 +33,61 @@ extension MainMenu {
|
||||
if Valet.shared.version != nil {
|
||||
Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)")
|
||||
}
|
||||
|
||||
|
||||
// Validate the version (this will enforce which versions of PHP are supported)
|
||||
Valet.shared.validateVersion()
|
||||
|
||||
|
||||
// Actually detect the PHP versions
|
||||
PhpEnv.detectPhpVersions()
|
||||
|
||||
|
||||
// Check for an alias conflict
|
||||
if HomebrewDiagnostics.hasAliasConflict() {
|
||||
DispatchQueue.main.async {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.php_alias_conflict.title".localized,
|
||||
subtitle: "alert.php_alias_conflict.info".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
HomebrewDiagnostics.presentAlertAboutConflict()
|
||||
}
|
||||
|
||||
|
||||
updatePhpVersionInStatusBar()
|
||||
|
||||
|
||||
Log.info("Determining broken PHP-FPM...")
|
||||
// Attempt to find out if PHP-FPM is broken
|
||||
let installation = PhpEnv.phpInstall
|
||||
installation.notifyAboutBrokenPhpFpm()
|
||||
|
||||
// Set up the config watchers on launch (these are automatically updated via delegate methods if the user switches)
|
||||
|
||||
// Set up the config watchers on launch
|
||||
// (these are automatically updated via delegate methods if the user switches)
|
||||
Log.info("Setting up watchers...")
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
|
||||
|
||||
// Detect applications (preset + custom)
|
||||
Log.info("Detecting applications...")
|
||||
App.shared.detectedApplications = Application.detectPresetApplications()
|
||||
|
||||
let customApps = Preferences.custom.scanApps.map { appName in
|
||||
return Application(appName, .user_supplied)
|
||||
}.filter { app in
|
||||
return app.isInstalled()
|
||||
}
|
||||
|
||||
App.shared.detectedApplications.append(contentsOf: customApps)
|
||||
|
||||
let appNames = App.shared.detectedApplications.map { app in
|
||||
return app.name
|
||||
}
|
||||
|
||||
Log.info("Detected applications: \(appNames)")
|
||||
|
||||
|
||||
// Load the global hotkey
|
||||
App.shared.loadGlobalHotkey()
|
||||
|
||||
|
||||
// Preload sites
|
||||
Valet.shared.startPreloadingSites()
|
||||
|
||||
|
||||
// A non-default TLD is not officially supported since Valet 3.2.x
|
||||
if (Valet.shared.config.tld != "test") {
|
||||
DispatchQueue.main.async {
|
||||
BetterAlert().withInformation(
|
||||
title: "alert.warnings.tld_issue.title".localized,
|
||||
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
||||
description: "alert.warnings.tld_issue.description".localized
|
||||
).withPrimary(text: "OK").show()
|
||||
}
|
||||
}
|
||||
|
||||
Valet.notifyAboutUnsupportedTLD()
|
||||
|
||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||
|
||||
|
||||
Log.info("PHP Monitor is ready to serve!")
|
||||
|
||||
|
||||
// Schedule a request to fetch the PHP version every 60 seconds
|
||||
DispatchQueue.main.async { [self] in
|
||||
App.shared.timer = Timer.scheduledTimer(
|
||||
@ -109,17 +98,17 @@ extension MainMenu {
|
||||
repeats: true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Stats.incrementSuccessfulLaunchCount()
|
||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
When the environment is not OK, present an alert to inform the user.
|
||||
*/
|
||||
private func onEnvironmentFail() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
||||
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.cannot_start.title".localized,
|
||||
@ -132,7 +121,7 @@ extension MainMenu {
|
||||
exit(1)
|
||||
})
|
||||
.show()
|
||||
|
||||
|
||||
Task { await startup() }
|
||||
}
|
||||
}
|
||||
|
@ -9,34 +9,34 @@
|
||||
import Foundation
|
||||
|
||||
extension MainMenu {
|
||||
|
||||
|
||||
// MARK: - PhpSwitcherDelegate
|
||||
|
||||
|
||||
func switcherDidStartSwitching(to version: String) {}
|
||||
|
||||
|
||||
func switcherDidCompleteSwitch(to version: String) {
|
||||
// Update the PHP version
|
||||
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
||||
|
||||
|
||||
// Ensure the config watcher gets reloaded
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
|
||||
|
||||
// Mark as no longer busy
|
||||
PhpEnv.shared.isBusy = false
|
||||
|
||||
|
||||
// Reload the site list
|
||||
self.reloadDomainListData()
|
||||
|
||||
|
||||
// Perform UI updates on main thread
|
||||
DispatchQueue.main.async { [self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
rebuild()
|
||||
|
||||
|
||||
if !PhpEnv.shared.validate(version) {
|
||||
self.suggestFixMyValet(failed: version)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Run composer updates
|
||||
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
|
||||
ComposerWindow().updateGlobalDependencies(
|
||||
@ -45,17 +45,17 @@ extension MainMenu {
|
||||
self.notifyAboutVersionChange(to: version)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
} else {
|
||||
self.notifyAboutVersionChange(to: version)
|
||||
}
|
||||
|
||||
|
||||
// Update stats
|
||||
Stats.incrementSuccessfulSwitchCount()
|
||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainActor private func suggestFixMyValet(failed version: String) {
|
||||
let outcome = BetterAlert()
|
||||
.withInformation(
|
||||
@ -69,7 +69,7 @@ extension MainMenu {
|
||||
MainMenu.shared.fixMyValet()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func reloadDomainListData() {
|
||||
if let window = App.shared.domainListWindowController {
|
||||
DispatchQueue.main.async {
|
||||
@ -79,13 +79,13 @@ extension MainMenu {
|
||||
Valet.shared.reloadSites()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func notifyAboutVersionChange(to version: String) {
|
||||
LocalNotification.send(
|
||||
title: String(format: "notification.version_changed_title".localized, version),
|
||||
subtitle: String(format: "notification.version_changed_desc".localized, version)
|
||||
)
|
||||
|
||||
|
||||
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
|
||||
}
|
||||
}
|
||||
|
@ -10,18 +10,18 @@ import Cocoa
|
||||
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
|
||||
|
||||
static let shared = MainMenu()
|
||||
|
||||
weak var menuDelegate: NSMenuDelegate? = nil
|
||||
|
||||
|
||||
weak var menuDelegate: NSMenuDelegate?
|
||||
|
||||
/**
|
||||
The status bar item with variable length.
|
||||
*/
|
||||
let statusItem = NSStatusBar.system.statusItem(
|
||||
withLength: NSStatusItem.variableLength
|
||||
)
|
||||
|
||||
|
||||
// MARK: - UI related
|
||||
|
||||
|
||||
/**
|
||||
Rebuilds the menu (either asynchronously or synchronously).
|
||||
Defaults to rebuilding the menu asynchronously.
|
||||
@ -31,13 +31,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
self.rebuildMenu()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Update the menu item on the main thread
|
||||
DispatchQueue.main.async { [self] in
|
||||
self.rebuildMenu()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Update the menu's contents, based on what's going on.
|
||||
This will rebuild the entire menu, so this can take a few moments.
|
||||
@ -47,35 +47,35 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
private func rebuildMenu() {
|
||||
// Create a new menu
|
||||
let menu = StatusMenu()
|
||||
|
||||
|
||||
// Add the PHP versions (or error messages)
|
||||
menu.addPhpVersionMenuItems()
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
// Add the possible actions
|
||||
menu.addPhpActionMenuItems()
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
// Add Valet interactions
|
||||
menu.addValetMenuItems()
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
// Add services
|
||||
menu.addRemainingMenuItems()
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
// Add about & quit menu items
|
||||
menu.addCoreMenuItems()
|
||||
|
||||
|
||||
// Make sure every item can be interacted with
|
||||
menu.items.forEach({ (item) in
|
||||
item.target = self
|
||||
})
|
||||
|
||||
|
||||
statusItem.menu = menu
|
||||
statusItem.menu?.delegate = self
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Sets the status bar image based on a version string.
|
||||
*/
|
||||
@ -86,7 +86,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
: MenuBarImageGenerator.textToImage(text: version)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Sets the status bar image, based on the provided NSImage.
|
||||
The image will be used as a template image.
|
||||
@ -97,9 +97,9 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
button.image = image
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - User Interface
|
||||
|
||||
|
||||
/** Reloads which PHP versions is currently active. */
|
||||
@objc func refreshActiveInstallation() {
|
||||
if !PhpEnv.shared.isBusy {
|
||||
@ -109,13 +109,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
Log.perf("Skipping version refresh due to busy status")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||
@objc func updatePhpVersionInStatusBar() {
|
||||
refreshIcon()
|
||||
rebuild()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Reloads the menu in the foreground.
|
||||
This mimics the exact behaviours of `asyncExecution` as set in the method below.
|
||||
@ -126,7 +126,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
rebuild(async: false)
|
||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||
}
|
||||
|
||||
|
||||
/** Reloads the menu in the background, using `asyncExecution`. */
|
||||
@objc func reloadPhpMonitorMenuInBackground() {
|
||||
asyncExecution({
|
||||
@ -139,11 +139,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
.updatesMenuBarContents
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
/** Refreshes the icon with the PHP version. */
|
||||
@objc func refreshIcon() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
if (PhpEnv.shared.isBusy) {
|
||||
if PhpEnv.shared.isBusy {
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
} else {
|
||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||
@ -157,16 +157,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Updates the icon to be displayed as busy. */
|
||||
@objc func setBusyImage() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
|
||||
@objc func fixHomebrewPermissions() {
|
||||
if !BetterAlert()
|
||||
.withInformation(
|
||||
@ -179,7 +179,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
.didSelectPrimary() {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
asyncExecution {
|
||||
try Actions.fixHomebrewPermissions()
|
||||
} success: {
|
||||
@ -195,13 +195,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
BetterAlert.show(for: error as! HomebrewPermissionError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func restartPhpFpm() {
|
||||
asyncExecution {
|
||||
Actions.restartPhpFpm()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func restartAllServices() {
|
||||
asyncExecution {
|
||||
Actions.restartDnsMasq()
|
||||
@ -216,7 +216,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func stopAllServices() {
|
||||
asyncExecution {
|
||||
Actions.stopAllServices()
|
||||
@ -229,79 +229,79 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func restartNginx() {
|
||||
asyncExecution {
|
||||
Actions.restartNginx()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func restartDnsMasq() {
|
||||
asyncExecution {
|
||||
Actions.restartDnsMasq()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func toggleXdebugMode(sender: XdebugMenuItem) {
|
||||
Log.info("Switching Xdebug to mode: \(sender.mode)")
|
||||
}
|
||||
|
||||
|
||||
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
||||
asyncExecution {
|
||||
sender.phpExtension?.toggle()
|
||||
|
||||
|
||||
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
|
||||
Actions.restartPhpFpm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func openPhpInfo() {
|
||||
var url: URL? = nil
|
||||
|
||||
var url: URL?
|
||||
|
||||
asyncWithBusyUI {
|
||||
url = Actions.createTempPhpInfoFile()
|
||||
} completion: {
|
||||
if url != nil { NSWorkspace.shared.open(url!) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func updateGlobalComposerDependencies() {
|
||||
ComposerWindow().updateGlobalDependencies(
|
||||
notify: true,
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@objc func openActiveConfigFolder() {
|
||||
if (PhpEnv.phpInstall.version.error) {
|
||||
if PhpEnv.phpInstall.version.error {
|
||||
// php version was not identified
|
||||
Actions.openGenericPhpConfigFolder()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// php version was identified
|
||||
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
|
||||
}
|
||||
|
||||
|
||||
@objc func openGlobalComposerFolder() {
|
||||
Actions.openGlobalComposerFolder()
|
||||
}
|
||||
|
||||
|
||||
@objc func openValetConfigFolder() {
|
||||
Actions.openValetConfigFolder()
|
||||
}
|
||||
|
||||
|
||||
@objc func switchToPhpVersion(sender: PhpMenuItem) {
|
||||
self.switchToPhpVersion(sender.version)
|
||||
}
|
||||
|
||||
|
||||
@objc func switchToPhpVersion(_ version: String) {
|
||||
setBusyImage()
|
||||
PhpEnv.shared.isBusy = true
|
||||
PhpEnv.shared.delegate = self
|
||||
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
rebuild()
|
||||
@ -313,38 +313,38 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Menu Item Functionality
|
||||
|
||||
|
||||
@objc func openAbout() {
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
NSApplication.shared.orderFrontStandardAboutPanel()
|
||||
}
|
||||
|
||||
|
||||
@objc func openPrefs() {
|
||||
PrefsVC.show()
|
||||
}
|
||||
|
||||
|
||||
@objc func openDomainList() {
|
||||
DomainListVC.show()
|
||||
}
|
||||
|
||||
|
||||
@objc func openDonate() {
|
||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||
}
|
||||
|
||||
|
||||
@objc func terminateApp() {
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Menu Delegate
|
||||
|
||||
|
||||
func menuWillOpen(_ menu: NSMenu) {
|
||||
// Make sure the shortcut key does not trigger this when the menu is open
|
||||
App.shared.shortcutHotkey?.isPaused = true
|
||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||
}
|
||||
|
||||
|
||||
func menuDidClose(_ menu: NSMenu) {
|
||||
// When the menu is closed, allow the shortcut to work again
|
||||
App.shared.shortcutHotkey?.isPaused = false
|
||||
|
@ -20,15 +20,15 @@ import Cocoa
|
||||
service information should also not happen in a view. Yet here we are.
|
||||
*/
|
||||
class ServicesView: NSView, XibLoadable {
|
||||
|
||||
|
||||
@IBOutlet weak var imageViewPhp: NSImageView!
|
||||
@IBOutlet weak var imageViewNginx: NSImageView!
|
||||
@IBOutlet weak var imageViewDnsmasq: NSImageView!
|
||||
|
||||
|
||||
@IBOutlet weak var textFieldPhp: NSTextField!
|
||||
|
||||
|
||||
static var services: [String: HomebrewService] = [:]
|
||||
|
||||
|
||||
static func asMenuItem() -> NSMenuItem {
|
||||
let view = Self.createFromXib()!
|
||||
[view.imageViewPhp, view.imageViewNginx, view.imageViewDnsmasq].forEach { imageView in
|
||||
@ -48,20 +48,20 @@ class ServicesView: NSView, XibLoadable {
|
||||
@objc func updateInformation() {
|
||||
self.loadData()
|
||||
}
|
||||
|
||||
|
||||
func loadData() {
|
||||
self.applyAllInfoFieldsFromCachedValue()
|
||||
HomebrewService.loadAll { services in
|
||||
ServicesView.services = Dictionary(uniqueKeysWithValues: services.map{ ($0.name, $0) })
|
||||
ServicesView.services = Dictionary(uniqueKeysWithValues: services.map { ($0.name, $0) })
|
||||
self.applyAllInfoFieldsFromCachedValue()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func applyAllInfoFieldsFromCachedValue() {
|
||||
if ServicesView.services.keys.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.textFieldPhp.stringValue = PhpEnv.phpInstall.formula.uppercased()
|
||||
self.applyServiceStyling(PhpEnv.phpInstall.formula, self.imageViewPhp)
|
||||
@ -69,24 +69,24 @@ class ServicesView: NSView, XibLoadable {
|
||||
self.applyServiceStyling("dnsmasq", self.imageViewDnsmasq)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func applyServiceStyling(_ serviceName: String, _ imageView: NSImageView) {
|
||||
if ServicesView.services[serviceName] == nil {
|
||||
imageView.image = NSImage(named: "ServiceLoading")
|
||||
imageView.contentTintColor = NSColor(named: "IconColorNormal")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if ServicesView.services[serviceName]!.running {
|
||||
imageView.image = NSImage(named: "ServiceOn")
|
||||
imageView.contentTintColor = NSColor(named: "IconColorNormal")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
imageView.image = NSImage(named: "ServiceOff")
|
||||
imageView.contentTintColor = NSColor(named: "IconColorRed")
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self, name: Events.ServicesUpdated, object: nil)
|
||||
}
|
||||
|
@ -10,15 +10,15 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class StatsView: NSView, XibLoadable {
|
||||
|
||||
|
||||
@IBOutlet weak var titleMemLimit: NSTextField!
|
||||
@IBOutlet weak var titleMaxPost: NSTextField!
|
||||
@IBOutlet weak var titleMaxUpload: NSTextField!
|
||||
|
||||
|
||||
@IBOutlet weak var labelMemLimit: NSTextField!
|
||||
@IBOutlet weak var labelMaxPost: NSTextField!
|
||||
@IBOutlet weak var labelMaxUpload: NSTextField!
|
||||
|
||||
|
||||
static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem {
|
||||
let view = Self.createFromXib()
|
||||
view!.titleMemLimit.stringValue = "mi_memory_limit".localized.uppercased()
|
||||
@ -32,5 +32,5 @@ class StatsView: NSView, XibLoadable {
|
||||
item.target = self
|
||||
return item
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -7,8 +7,8 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
class StatusMenu : NSMenu {
|
||||
|
||||
class StatusMenu: NSMenu {
|
||||
|
||||
func addPhpVersionMenuItems() {
|
||||
if PhpEnv.phpInstall.version.error {
|
||||
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
|
||||
@ -16,114 +16,134 @@ class StatusMenu : NSMenu {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let phpVersionText = "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)"
|
||||
addItem(HeaderView.asMenuItem(text: phpVersionText))
|
||||
}
|
||||
|
||||
|
||||
func addPhpActionMenuItems() {
|
||||
if PhpEnv.shared.isBusy {
|
||||
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
|
||||
return
|
||||
}
|
||||
|
||||
if PhpEnv.shared.availablePhpVersions.count == 0 {
|
||||
|
||||
if PhpEnv.shared.availablePhpVersions.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
self.addSwitchToPhpMenuItems()
|
||||
self.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
self.addItem(ServicesView.asMenuItem())
|
||||
self.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
|
||||
func addValetMenuItems() {
|
||||
self.addItem(HeaderView.asMenuItem(text: "mi_valet".localized))
|
||||
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
|
||||
self.addItem(NSMenuItem(title: "mi_domain_list".localized, action: #selector(MainMenu.openDomainList), keyEquivalent: "l"))
|
||||
self.addItem(NSMenuItem(
|
||||
title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
|
||||
self.addItem(NSMenuItem(
|
||||
title: "mi_domain_list".localized, action: #selector(MainMenu.openDomainList), keyEquivalent: "l"))
|
||||
self.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
|
||||
func addRemainingMenuItems() {
|
||||
self.addConfigurationMenuItems()
|
||||
|
||||
|
||||
self.addItem(NSMenuItem.separator())
|
||||
|
||||
self.addComposerMenuItems()
|
||||
|
||||
if (PhpEnv.shared.isBusy) {
|
||||
|
||||
if PhpEnv.shared.isBusy {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
self.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
self.addStatsMenuItem()
|
||||
|
||||
self.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
self.addExtensionsMenuItems()
|
||||
|
||||
|
||||
self.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
// self.addXdebugMenuItem()
|
||||
|
||||
self.addFirstAidAndServicesMenuItems()
|
||||
}
|
||||
|
||||
|
||||
func addCoreMenuItems() {
|
||||
self.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(MainMenu.openPrefs), keyEquivalent: ","))
|
||||
self.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(MainMenu.openAbout), keyEquivalent: ""))
|
||||
self.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(MainMenu.terminateApp), keyEquivalent: "q"))
|
||||
self.addItem(
|
||||
NSMenuItem(title: "mi_preferences".localized, action: #selector(MainMenu.openPrefs), keyEquivalent: ",")
|
||||
)
|
||||
self.addItem(
|
||||
NSMenuItem(title: "mi_about".localized, action: #selector(MainMenu.openAbout), keyEquivalent: "")
|
||||
)
|
||||
self.addItem(
|
||||
NSMenuItem(title: "mi_quit".localized, action: #selector(MainMenu.terminateApp), keyEquivalent: "q")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Remaining Menu Items
|
||||
|
||||
|
||||
func addConfigurationMenuItems() {
|
||||
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
|
||||
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
|
||||
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
|
||||
self.addItem(
|
||||
NSMenuItem(title: "mi_php_config".localized,
|
||||
action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c")
|
||||
)
|
||||
self.addItem(
|
||||
NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func addComposerMenuItems() {
|
||||
self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized))
|
||||
self.addItem(NSMenuItem(title: "mi_global_composer".localized, action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g"))
|
||||
|
||||
let composerMenuItem = NSMenuItem(title: "mi_update_global_composer".localized, action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies), keyEquivalent: "g")
|
||||
self.addItem(
|
||||
NSMenuItem(title: "mi_global_composer".localized,
|
||||
action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g")
|
||||
)
|
||||
|
||||
let composerMenuItem = NSMenuItem(
|
||||
title: "mi_update_global_composer".localized,
|
||||
action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies),
|
||||
keyEquivalent: "g"
|
||||
)
|
||||
composerMenuItem.keyEquivalentModifierMask = .shift
|
||||
|
||||
|
||||
self.addItem(composerMenuItem)
|
||||
}
|
||||
|
||||
|
||||
func addStatsMenuItem() {
|
||||
guard let stats = PhpEnv.phpInstall.limits else { return }
|
||||
|
||||
|
||||
self.addItem(StatsView.asMenuItem(
|
||||
memory: stats.memory_limit,
|
||||
post: stats.post_max_size,
|
||||
upload: stats.upload_max_filesize)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func addExtensionsMenuItems() {
|
||||
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
|
||||
|
||||
if (PhpEnv.phpInstall.extensions.count == 0) {
|
||||
|
||||
if PhpEnv.phpInstall.extensions.isEmpty {
|
||||
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
|
||||
}
|
||||
|
||||
|
||||
var shortcutKey = 1
|
||||
for phpExtension in PhpEnv.phpInstall.extensions {
|
||||
self.addExtensionItem(phpExtension, shortcutKey)
|
||||
shortcutKey += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func addXdebugMenuItem() {
|
||||
if !Xdebug.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let xdebugSwitch = NSMenuItem(
|
||||
title: "mi_xdebug_mode".localized,
|
||||
action: nil,
|
||||
@ -131,7 +151,7 @@ class StatusMenu : NSMenu {
|
||||
)
|
||||
let xdebugModesMenu = NSMenu()
|
||||
let xdebugMode = Xdebug.mode
|
||||
|
||||
|
||||
for mode in Xdebug.modes {
|
||||
let item = XdebugMenuItem(
|
||||
title: mode,
|
||||
@ -142,100 +162,119 @@ class StatusMenu : NSMenu {
|
||||
item.mode = mode
|
||||
xdebugModesMenu.addItem(item)
|
||||
}
|
||||
|
||||
|
||||
for item in xdebugModesMenu.items {
|
||||
item.target = MainMenu.shared
|
||||
}
|
||||
|
||||
|
||||
self.setSubmenu(xdebugModesMenu, for: xdebugSwitch)
|
||||
self.addItem(xdebugSwitch)
|
||||
}
|
||||
|
||||
|
||||
func addFirstAidAndServicesMenuItems() {
|
||||
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
|
||||
let servicesMenu = NSMenu()
|
||||
|
||||
|
||||
let fixMyValetMenuItem = NSMenuItem(
|
||||
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
|
||||
action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
|
||||
)
|
||||
fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized
|
||||
servicesMenu.addItem(fixMyValetMenuItem)
|
||||
|
||||
|
||||
let fixHomebrewMenuItem = NSMenuItem(
|
||||
title: "mi_fix_brew_permissions".localized(),
|
||||
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: ""
|
||||
)
|
||||
fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized
|
||||
servicesMenu.addItem(fixHomebrewMenuItem)
|
||||
|
||||
|
||||
servicesMenu.addItem(NSMenuItem.separator())
|
||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
|
||||
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
|
||||
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
|
||||
withKeyModifier: [.command, .shift])
|
||||
|
||||
NSMenuItem(title: "mi_restart_dnsmasq".localized,
|
||||
action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_php_fpm".localized,
|
||||
action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_nginx".localized,
|
||||
action: #selector(MainMenu.restartNginx), keyEquivalent: "n")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_all_services".localized,
|
||||
action: #selector(MainMenu.restartAllServices), keyEquivalent: "s")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_stop_all_services".localized,
|
||||
action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
|
||||
withKeyModifier: [.command, .shift]
|
||||
)
|
||||
|
||||
servicesMenu.addItem(NSMenuItem.separator())
|
||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized))
|
||||
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r"))
|
||||
|
||||
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_php_refresh".localized,
|
||||
action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r")
|
||||
)
|
||||
|
||||
for item in servicesMenu.items {
|
||||
item.target = MainMenu.shared
|
||||
}
|
||||
|
||||
|
||||
self.setSubmenu(servicesMenu, for: services)
|
||||
self.addItem(services)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private Helpers
|
||||
|
||||
|
||||
private func addSwitchToPhpMenuItems() {
|
||||
var shortcutKey = 1
|
||||
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
|
||||
|
||||
|
||||
// Get the short and long version
|
||||
let shortVersion = PhpEnv.shared.availablePhpVersions[index]
|
||||
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber
|
||||
|
||||
|
||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||
let versionString = long ? longVersion.toString() : shortVersion
|
||||
|
||||
|
||||
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
||||
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
|
||||
let menuItem = PhpMenuItem(
|
||||
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
|
||||
action: (shortVersion == PhpEnv.phpInstall.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
|
||||
action: (shortVersion == PhpEnv.phpInstall.version.short)
|
||||
? nil
|
||||
: action, keyEquivalent: "\(shortcutKey)"
|
||||
)
|
||||
|
||||
|
||||
menuItem.version = shortVersion
|
||||
shortcutKey = shortcutKey + 1
|
||||
|
||||
shortcutKey += 1
|
||||
|
||||
self.addItem(menuItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
||||
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
||||
|
||||
|
||||
let menuItem = ExtensionMenuItem(
|
||||
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
|
||||
action: #selector(MainMenu.toggleExtension),
|
||||
keyEquivalent: keyEquivalent
|
||||
)
|
||||
|
||||
|
||||
if menuItem.keyEquivalent != "" {
|
||||
menuItem.keyEquivalentModifierMask = [.option]
|
||||
}
|
||||
|
||||
|
||||
menuItem.state = phpExtension.enabled ? .on : .off
|
||||
menuItem.phpExtension = phpExtension
|
||||
|
||||
|
||||
self.addItem(menuItem)
|
||||
}
|
||||
}
|
||||
@ -251,9 +290,9 @@ class XdebugMenuItem: NSMenuItem {
|
||||
}
|
||||
|
||||
class ExtensionMenuItem: NSMenuItem {
|
||||
var phpExtension: PhpExtension? = nil
|
||||
var phpExtension: PhpExtension?
|
||||
}
|
||||
|
||||
class EditorMenuItem: NSMenuItem {
|
||||
var editor: Application? = nil
|
||||
var editor: Application?
|
||||
}
|
||||
|
@ -10,47 +10,47 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class BetterAlert {
|
||||
|
||||
|
||||
var windowController: NSWindowController!
|
||||
|
||||
|
||||
var noticeVC: BetterAlertVC {
|
||||
return self.windowController.contentViewController as! BetterAlertVC
|
||||
}
|
||||
|
||||
|
||||
init() {
|
||||
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
|
||||
|
||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
self.windowController = storyboard.instantiateController(
|
||||
withIdentifier: "noticeWindow"
|
||||
) as? NSWindowController
|
||||
}
|
||||
|
||||
|
||||
public static func make() -> BetterAlert {
|
||||
return BetterAlert()
|
||||
}
|
||||
|
||||
|
||||
public func withPrimary(
|
||||
text: String,
|
||||
action: @escaping (BetterAlertVC) -> Void = {
|
||||
vc in vc.close(with: .alertFirstButtonReturn)
|
||||
action: @escaping (BetterAlertVC) -> Void = { vc in
|
||||
vc.close(with: .alertFirstButtonReturn)
|
||||
}
|
||||
) -> Self {
|
||||
self.noticeVC.buttonPrimary.title = text
|
||||
self.noticeVC.actionPrimary = action
|
||||
return self
|
||||
}
|
||||
|
||||
|
||||
public func withSecondary(
|
||||
text: String,
|
||||
action: ((BetterAlertVC) -> Void)? = {
|
||||
vc in vc.close(with: .alertSecondButtonReturn)
|
||||
action: ((BetterAlertVC) -> Void)? = { vc in
|
||||
vc.close(with: .alertSecondButtonReturn)
|
||||
}
|
||||
) -> Self {
|
||||
self.noticeVC.buttonSecondary.title = text
|
||||
self.noticeVC.actionSecondary = action
|
||||
return self
|
||||
}
|
||||
|
||||
|
||||
public func withTertiary(
|
||||
text: String = "",
|
||||
action: ((BetterAlertVC) -> Void)? = nil
|
||||
@ -62,7 +62,7 @@ class BetterAlert {
|
||||
self.noticeVC.actionTertiary = action
|
||||
return self
|
||||
}
|
||||
|
||||
|
||||
public func withInformation(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
@ -71,15 +71,15 @@ class BetterAlert {
|
||||
self.noticeVC.labelTitle.stringValue = title
|
||||
self.noticeVC.labelSubtitle.stringValue = subtitle
|
||||
self.noticeVC.labelDescription.stringValue = description
|
||||
|
||||
|
||||
// If the description is missing, handle the excess space and change the top margin
|
||||
if (description == "") {
|
||||
if description == "" {
|
||||
self.noticeVC.labelDescription.isHidden = true
|
||||
self.noticeVC.primaryButtonTopMargin.constant = 0
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Shows the modal and returns a ModalResponse.
|
||||
If you wish to simply show the alert and disregard the outcome, use `show`.
|
||||
@ -88,12 +88,12 @@ class BetterAlert {
|
||||
if !Thread.isMainThread {
|
||||
fatalError("You should always present alerts on the main thread!")
|
||||
}
|
||||
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
windowController.window?.makeKeyAndOrderFront(nil)
|
||||
return NSApplication.shared.runModal(for: windowController.window!)
|
||||
}
|
||||
|
||||
|
||||
/** Shows the modal and returns true if the user pressed the primary button. */
|
||||
public func didSelectPrimary() -> Bool {
|
||||
return self.runModal() == .alertFirstButtonReturn
|
||||
@ -105,7 +105,7 @@ class BetterAlert {
|
||||
public func show() {
|
||||
_ = self.runModal()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Shows the modal for a particular error.
|
||||
*/
|
||||
|
@ -10,30 +10,30 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class BetterAlertVC: NSViewController {
|
||||
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
|
||||
@IBOutlet weak var labelTitle: NSTextField!
|
||||
@IBOutlet weak var labelSubtitle: NSTextField!
|
||||
@IBOutlet weak var labelDescription: NSTextField!
|
||||
|
||||
|
||||
@IBOutlet weak var buttonPrimary: NSButton!
|
||||
@IBOutlet weak var buttonSecondary: NSButton!
|
||||
@IBOutlet weak var buttonTertiary: NSButton!
|
||||
|
||||
|
||||
var actionPrimary: (BetterAlertVC) -> Void = { _ in }
|
||||
var actionSecondary: ((BetterAlertVC) -> Void)?
|
||||
var actionTertiary: ((BetterAlertVC) -> Void)?
|
||||
|
||||
|
||||
@IBOutlet weak var imageView: NSImageView!
|
||||
|
||||
|
||||
@IBOutlet weak var primaryButtonTopMargin: NSLayoutConstraint!
|
||||
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
|
||||
override func viewWillAppear() {
|
||||
imageView.image = NSApp.applicationIconImage
|
||||
|
||||
|
||||
if actionSecondary == nil {
|
||||
buttonSecondary.isHidden = true
|
||||
}
|
||||
@ -41,22 +41,21 @@ class BetterAlertVC: NSViewController {
|
||||
buttonTertiary.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func viewDidAppear() {
|
||||
view.window?.makeFirstResponder(buttonPrimary)
|
||||
}
|
||||
|
||||
|
||||
|
||||
deinit {
|
||||
Log.perf("A BetterAlert has been deinitialized.")
|
||||
}
|
||||
|
||||
|
||||
// MARK: Outlet Actions
|
||||
|
||||
|
||||
@IBAction func primaryButtonAction(_ sender: Any) {
|
||||
self.actionPrimary(self)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func secondaryButtonAction(_ sender: Any) {
|
||||
if self.actionSecondary != nil {
|
||||
self.actionSecondary!(self)
|
||||
@ -64,16 +63,16 @@ class BetterAlertVC: NSViewController {
|
||||
self.close(with: .alertSecondButtonReturn)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func tertiaryButtonAction(_ sender: Any) {
|
||||
if self.actionSecondary != nil {
|
||||
self.actionTertiary!(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func close(with code: NSApplication.ModalResponse) {
|
||||
self.view.window?.close()
|
||||
NSApplication.shared.stopModal(withCode: code)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
extension ActivePhpInstallation {
|
||||
|
||||
|
||||
/**
|
||||
It is always possible that the system configuration for PHP-FPM has not been set up for Valet.
|
||||
This can occur when a user manually installs a new PHP version, but does not run `valet install`.
|
||||
@ -32,5 +32,5 @@ extension ActivePhpInstallation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,21 +9,21 @@
|
||||
import Foundation
|
||||
|
||||
struct GlobalKeybindPreference: Codable, CustomStringConvertible {
|
||||
|
||||
|
||||
// MARK: - Internal variables
|
||||
|
||||
let function : Bool
|
||||
let control : Bool
|
||||
let command : Bool
|
||||
let shift : Bool
|
||||
let option : Bool
|
||||
let capsLock : Bool
|
||||
let carbonFlags : UInt32
|
||||
let characters : String?
|
||||
let keyCode : UInt32
|
||||
|
||||
|
||||
let function: Bool
|
||||
let control: Bool
|
||||
let command: Bool
|
||||
let shift: Bool
|
||||
let option: Bool
|
||||
let capsLock: Bool
|
||||
let carbonFlags: UInt32
|
||||
let characters: String?
|
||||
let keyCode: UInt32
|
||||
|
||||
// MARK: - How the keybind is display in Preferences
|
||||
|
||||
|
||||
var description: String {
|
||||
var stringBuilder = ""
|
||||
if self.function {
|
||||
@ -49,19 +49,19 @@ struct GlobalKeybindPreference: Codable, CustomStringConvertible {
|
||||
}
|
||||
return "\(stringBuilder)"
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Persisting data to UserDefaults (as JSON)
|
||||
|
||||
|
||||
public func toJson() -> String {
|
||||
let jsonData = try! JSONEncoder().encode(self)
|
||||
return String(data: jsonData, encoding: .utf8)!
|
||||
}
|
||||
|
||||
|
||||
public static func fromJson(_ string: String?) -> GlobalKeybindPreference? {
|
||||
if string == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if let jsonData = string!.data(using: .utf8) {
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
|
@ -39,24 +39,24 @@ enum InternalStats: String {
|
||||
}
|
||||
|
||||
class Preferences {
|
||||
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
|
||||
static var shared = Preferences()
|
||||
|
||||
|
||||
var customPreferences: CustomPrefs
|
||||
|
||||
|
||||
var cachedPreferences: [PreferenceName: Any?]
|
||||
|
||||
|
||||
public init() {
|
||||
Preferences.handleFirstTimeLaunch()
|
||||
cachedPreferences = Self.cache()
|
||||
customPreferences = CustomPrefs(scanApps: [])
|
||||
loadCustomPreferences()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - First Time Run
|
||||
|
||||
|
||||
/**
|
||||
Note: macOS seems to cache plist values in memory as well as in files.
|
||||
You can find the persisted configuration file in: ~/Library/Preferences/com.nicoverbruggen.phpmon.plist
|
||||
@ -81,17 +81,17 @@ class Preferences {
|
||||
InternalStats.launchCount.rawValue: 0,
|
||||
InternalStats.didSeeSponsorEncouragement.rawValue: false
|
||||
])
|
||||
|
||||
|
||||
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
|
||||
handleMigration()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Log.info("Saving first-time preferences!")
|
||||
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Sometimes preferences will change, and a migration is required to take the user's previous preference
|
||||
and migrate it over to the new type. For example, the choice to disable the icon next to the version
|
||||
@ -99,26 +99,25 @@ class Preferences {
|
||||
*/
|
||||
static func handleMigration() {
|
||||
// If the user chose the "no icon" option, migrate it over
|
||||
if (
|
||||
if
|
||||
UserDefaults.standard.value(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue) != nil &&
|
||||
UserDefaults.standard.bool(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue) == false
|
||||
) {
|
||||
UserDefaults.standard.bool(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue) == false {
|
||||
Log.info("The preference where the user chose no icon has been migrated over.")
|
||||
UserDefaults.standard.set(MenuBarIcon.noIcon.rawValue, forKey: PreferenceName.iconTypeToDisplay.rawValue)
|
||||
UserDefaults.standard.removeObject(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - API
|
||||
|
||||
|
||||
static var preferences: [PreferenceName: Any?] {
|
||||
return Self.shared.cachedPreferences
|
||||
}
|
||||
|
||||
|
||||
static var custom: CustomPrefs {
|
||||
return Self.shared.customPreferences
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Determine whether a particular preference is enabled.
|
||||
- Important: Requires the preference to have a corresponding boolean value, or a fatal error will be thrown.
|
||||
@ -130,38 +129,45 @@ class Preferences {
|
||||
fatalError("\(preference) is not a valid boolean preference!")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Internal Functionality
|
||||
|
||||
|
||||
private static func cache() -> [PreferenceName: Any] {
|
||||
return [
|
||||
// Part 1: Always Booleans
|
||||
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
|
||||
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
|
||||
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
|
||||
.autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any,
|
||||
.allowProtocolForIntegrations: UserDefaults.standard.bool(forKey: PreferenceName.allowProtocolForIntegrations.rawValue) as Any,
|
||||
|
||||
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
|
||||
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
|
||||
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
|
||||
.autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any,
|
||||
.allowProtocolForIntegrations: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.allowProtocolForIntegrations.rawValue) as Any,
|
||||
|
||||
// Part 2: Always Strings
|
||||
.globalHotkey: UserDefaults.standard.string(forKey: PreferenceName.globalHotkey.rawValue) as Any,
|
||||
.iconTypeToDisplay: UserDefaults.standard.string(forKey: PreferenceName.iconTypeToDisplay.rawValue) as Any,
|
||||
.globalHotkey: UserDefaults.standard.string(
|
||||
forKey: PreferenceName.globalHotkey.rawValue) as Any,
|
||||
.iconTypeToDisplay: UserDefaults.standard.string(
|
||||
forKey: PreferenceName.iconTypeToDisplay.rawValue) as Any
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
static func update(_ preference: PreferenceName, value: Any?) {
|
||||
if (value == nil) {
|
||||
if value == nil {
|
||||
UserDefaults.standard.removeObject(forKey: preference.rawValue)
|
||||
} else {
|
||||
UserDefaults.standard.setValue(value, forKey: preference.rawValue)
|
||||
}
|
||||
UserDefaults.standard.synchronize()
|
||||
|
||||
|
||||
// Update the preferences cache in memory!
|
||||
Preferences.shared.cachedPreferences = Preferences.cache()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Custom Preferences
|
||||
|
||||
|
||||
private func loadCustomPreferences() {
|
||||
let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json")
|
||||
if Filesystem.fileExists(url.path) {
|
||||
@ -171,7 +177,7 @@ class Preferences {
|
||||
Log.info("There was no .phpmon.conf.json file to be loaded.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func loadCustomPreferencesFile(_ url: URL) {
|
||||
do {
|
||||
customPreferences = try JSONDecoder().decode(
|
||||
@ -183,5 +189,5 @@ class Preferences {
|
||||
Log.warn("The .phpmon.conf.json file seems to be missing or malformed.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,105 +10,133 @@ import Cocoa
|
||||
import Carbon
|
||||
|
||||
class PrefsVC: NSViewController {
|
||||
|
||||
|
||||
// MARK: - Window Identifier
|
||||
|
||||
|
||||
@IBOutlet weak var stackView: NSStackView!
|
||||
|
||||
|
||||
// MARK: - Display
|
||||
|
||||
|
||||
public static func create(delegate: NSWindowDelegate?) {
|
||||
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
|
||||
|
||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
let windowController = storyboard.instantiateController(
|
||||
withIdentifier: "preferencesWindow"
|
||||
) as! PrefsWC
|
||||
|
||||
|
||||
windowController.window!.title = "prefs.title".localized
|
||||
windowController.window!.subtitle = "prefs.subtitle".localized
|
||||
windowController.window!.delegate = delegate
|
||||
windowController.window!.styleMask = [.titled, .closable, .miniaturizable]
|
||||
windowController.window!.delegate = windowController
|
||||
windowController.positionWindowInTopLeftCorner()
|
||||
|
||||
|
||||
App.shared.preferencesWindowController = windowController
|
||||
}
|
||||
|
||||
|
||||
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||
if (App.shared.preferencesWindowController == nil) {
|
||||
if App.shared.preferencesWindowController == nil {
|
||||
Self.create(delegate: delegate)
|
||||
}
|
||||
|
||||
|
||||
App.shared.preferencesWindowController!.showWindow(self)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
[
|
||||
CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.dynamic_icon".localized,
|
||||
descriptionText: "prefs.dynamic_icon_desc".localized,
|
||||
checkboxText: "prefs.dynamic_icon_title".localized,
|
||||
preference: .shouldDisplayDynamicIcon,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
}
|
||||
),
|
||||
SelectPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.icon_options_desc".localized,
|
||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
||||
localizationPrefix: "prefs.icon_options",
|
||||
preference: .iconTypeToDisplay,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
}
|
||||
),
|
||||
CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.info_density".localized,
|
||||
descriptionText: "prefs.display_full_php_version_desc".localized,
|
||||
checkboxText: "prefs.display_full_php_version".localized,
|
||||
preference: .fullPhpVersionDynamicIcon,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
),
|
||||
CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.services".localized,
|
||||
descriptionText: "prefs.auto_restart_services_desc".localized,
|
||||
checkboxText: "prefs.auto_restart_services_title".localized,
|
||||
preference: .autoServiceRestartAfterExtensionToggle,
|
||||
action: {}
|
||||
),
|
||||
CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.switcher".localized,
|
||||
descriptionText: "prefs.auto_composer_update_desc".localized,
|
||||
checkboxText: "prefs.auto_composer_update_title".localized,
|
||||
preference: .autoComposerGlobalUpdateAfterSwitch,
|
||||
action: {}
|
||||
),
|
||||
HotkeyPreferenceView.make(
|
||||
sectionText: "prefs.global_shortcut".localized,
|
||||
descriptionText: "prefs.shortcut_desc".localized,
|
||||
self
|
||||
),
|
||||
CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.integrations".localized,
|
||||
descriptionText: "prefs.open_protocol_desc".localized,
|
||||
checkboxText: "prefs.open_protocol_title".localized,
|
||||
preference: .allowProtocolForIntegrations,
|
||||
action: {}
|
||||
),
|
||||
getDynamicIconPreferenceView(),
|
||||
getIconOptionsPreferenceView(),
|
||||
getIconDensityPreferenceView(),
|
||||
getAutoRestartPreferenceView(),
|
||||
getAutomaticComposerUpdatePreferenceView(),
|
||||
getShortcutPreferenceView(),
|
||||
getIntegrationsPreferenceView()
|
||||
].forEach({ self.stackView.addArrangedSubview($0) })
|
||||
}
|
||||
|
||||
|
||||
private func getDynamicIconPreferenceView() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.dynamic_icon".localized,
|
||||
descriptionText: "prefs.dynamic_icon_desc".localized,
|
||||
checkboxText: "prefs.dynamic_icon_title".localized,
|
||||
preference: .shouldDisplayDynamicIcon,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func getIconOptionsPreferenceView() -> NSView {
|
||||
return SelectPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.icon_options_desc".localized,
|
||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
||||
localizationPrefix: "prefs.icon_options",
|
||||
preference: .iconTypeToDisplay,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func getIconDensityPreferenceView() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.info_density".localized,
|
||||
descriptionText: "prefs.display_full_php_version_desc".localized,
|
||||
checkboxText: "prefs.display_full_php_version".localized,
|
||||
preference: .fullPhpVersionDynamicIcon,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func getAutoRestartPreferenceView() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.services".localized,
|
||||
descriptionText: "prefs.auto_restart_services_desc".localized,
|
||||
checkboxText: "prefs.auto_restart_services_title".localized,
|
||||
preference: .autoServiceRestartAfterExtensionToggle,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
private func getAutomaticComposerUpdatePreferenceView() -> NSView {
|
||||
CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.switcher".localized,
|
||||
descriptionText: "prefs.auto_composer_update_desc".localized,
|
||||
checkboxText: "prefs.auto_composer_update_title".localized,
|
||||
preference: .autoComposerGlobalUpdateAfterSwitch,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
private func getShortcutPreferenceView() -> NSView {
|
||||
return HotkeyPreferenceView.make(
|
||||
sectionText: "prefs.global_shortcut".localized,
|
||||
descriptionText: "prefs.shortcut_desc".localized,
|
||||
self
|
||||
)
|
||||
}
|
||||
|
||||
private func getIntegrationsPreferenceView() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.integrations".localized,
|
||||
descriptionText: "prefs.open_protocol_desc".localized,
|
||||
checkboxText: "prefs.open_protocol_title".localized,
|
||||
preference: .allowProtocolForIntegrations,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Listening for hotkey delegate
|
||||
|
||||
var listeningForHotkeyView: HotkeyPreferenceView? = nil
|
||||
|
||||
|
||||
var listeningForHotkeyView: HotkeyPreferenceView?
|
||||
|
||||
override func viewWillDisappear() {
|
||||
if listeningForHotkeyView !== nil {
|
||||
listeningForHotkeyView = nil
|
||||
@ -116,7 +144,7 @@ class PrefsVC: NSViewController {
|
||||
}
|
||||
|
||||
// MARK: - Deinitialization
|
||||
|
||||
|
||||
deinit {
|
||||
Log.perf("PrefsVC deallocated")
|
||||
}
|
||||
|
@ -14,18 +14,18 @@ struct Keys {
|
||||
}
|
||||
|
||||
class PrefsWC: PMWindowController {
|
||||
|
||||
|
||||
// MARK: - Window Identifier
|
||||
|
||||
|
||||
override var windowName: String {
|
||||
return "Preferences"
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Key Interaction
|
||||
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
super.keyDown(with: event)
|
||||
|
||||
|
||||
if let vc = contentViewController as? PrefsVC {
|
||||
if vc.listeningForHotkeyView != nil {
|
||||
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
|
||||
@ -37,5 +37,5 @@ class PrefsWC: PMWindowController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,9 +8,9 @@
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
|
||||
class Stats {
|
||||
|
||||
|
||||
/**
|
||||
Keep track of how many times the app has been successfully launched.
|
||||
|
||||
@ -23,7 +23,7 @@ class Stats {
|
||||
forKey: InternalStats.launchCount.rawValue
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Keep track of how many times the app has successfully switched
|
||||
between different PHP versions.
|
||||
@ -37,7 +37,7 @@ class Stats {
|
||||
forKey: InternalStats.switchCount.rawValue
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Did the user see the sponsor encouragement / thank you message?
|
||||
Annoying the user is the worst, so let's not show the message twice.
|
||||
@ -47,7 +47,7 @@ class Stats {
|
||||
forKey: InternalStats.didSeeSponsorEncouragement.rawValue
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Increment the successful launch count. This should only be
|
||||
called when the user has not encountered ANY issues starting
|
||||
@ -59,7 +59,7 @@ class Stats {
|
||||
forKey: InternalStats.launchCount.rawValue
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Increment the successful switch count.
|
||||
*/
|
||||
@ -69,7 +69,7 @@ class Stats {
|
||||
forKey: InternalStats.switchCount.rawValue
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Determine if the sponsor message should be displayed.
|
||||
|
||||
@ -86,19 +86,20 @@ class Stats {
|
||||
(see `didSeeSponsorEncouragement`)
|
||||
*/
|
||||
public static func evaluateSponsorMessageShouldBeDisplayed() {
|
||||
|
||||
|
||||
if Bundle.main.bundleIdentifier?.contains("beta") ?? false {
|
||||
return Log.info("Sponsor messages never apply to beta builds.")
|
||||
}
|
||||
|
||||
|
||||
if Stats.didSeeSponsorEncouragement {
|
||||
return Log.info("Awesome, the user has already seen the sponsor message.")
|
||||
}
|
||||
|
||||
|
||||
if Stats.successfulLaunchCount < 7 && Stats.successfulSwitchCount < 40 {
|
||||
return Log.info("It is too soon to see the sponsor message (launched \(Stats.successfulLaunchCount) times, switched \(Stats.successfulSwitchCount) times).")
|
||||
return Log.info("It is too soon to see the sponsor message (launched \(Stats.successfulLaunchCount) " +
|
||||
"times, switched \(Stats.successfulSwitchCount) times).")
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let donate = BetterAlert()
|
||||
.withInformation(
|
||||
@ -117,9 +118,9 @@ class Stats {
|
||||
Log.info("The user is an absolute badass for choosing this option. Thank you.")
|
||||
NSWorkspace.shared.open(Constants.Urls.DonationPayment)
|
||||
}
|
||||
|
||||
|
||||
UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -6,26 +6,30 @@
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class CheckboxPreferenceView: NSView, XibLoadable {
|
||||
|
||||
|
||||
@IBOutlet weak var labelSection: NSTextField!
|
||||
@IBOutlet weak var labelDescription: NSTextField!
|
||||
@IBOutlet weak var buttonCheckbox: NSButton!
|
||||
|
||||
|
||||
var action: (() -> Void)!
|
||||
|
||||
|
||||
var preference: PreferenceName! {
|
||||
didSet {
|
||||
self.buttonCheckbox.state = Preferences.isEnabled(self.preference) ? .on : .off
|
||||
}
|
||||
}
|
||||
|
||||
static func make(sectionText: String, descriptionText: String, checkboxText: String, preference: PreferenceName, action: @escaping () -> Void) -> NSView {
|
||||
|
||||
static func make(
|
||||
sectionText: String,
|
||||
descriptionText: String,
|
||||
checkboxText: String,
|
||||
preference: PreferenceName,
|
||||
action: @escaping () -> Void
|
||||
) -> NSView {
|
||||
let view = Self.createFromXib()!
|
||||
view.labelSection.stringValue = sectionText
|
||||
view.labelDescription.stringValue = descriptionText
|
||||
@ -34,10 +38,10 @@ class CheckboxPreferenceView: NSView, XibLoadable {
|
||||
view.action = action
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
@IBAction func toggled(_ sender: Any) {
|
||||
Preferences.update(self.preference, value: buttonCheckbox.state == .on)
|
||||
self.action()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user