1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-12-21 11:10:08 +01:00

Added linting

This commit is contained in:
2022-05-03 18:16:26 +02:00
parent 790f63e8c9
commit 4d04275c57
111 changed files with 1774 additions and 1642 deletions

15
.swiftlint.yml Normal file
View 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

View File

@@ -230,6 +230,7 @@
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; }; C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.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 */; }; 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 */; }; C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; }; C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
@@ -549,6 +551,7 @@
C4E713562570150F00007428 /* SECURITY.md */, C4E713562570150F00007428 /* SECURITY.md */,
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */, C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
54D9E0C027E4F5E9003B9AD9 /* LICENSE */, 54D9E0C027E4F5E9003B9AD9 /* LICENSE */,
C4F5FBCC28218C93001065C5 /* .swiftlint.yml */,
C4E713572570151400007428 /* docs */, C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */, C41C1B3522B0097F00E7CF16 /* phpmon */,
C4F7807A25D7F84B000DBC97 /* phpmon-tests */, C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
@@ -974,6 +977,7 @@
C41C1B2F22B0097F00E7CF16 /* Sources */, C41C1B2F22B0097F00E7CF16 /* Sources */,
C41C1B3022B0097F00E7CF16 /* Frameworks */, C41C1B3022B0097F00E7CF16 /* Frameworks */,
C41C1B3122B0097F00E7CF16 /* Resources */, C41C1B3122B0097F00E7CF16 /* Resources */,
C4F5FBCB28216985001065C5 /* Run `swiftlint` */,
); );
buildRules = ( buildRules = (
); );
@@ -1089,6 +1093,27 @@
}; };
/* End PBXResourcesBuildPhase section */ /* 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 */ /* Begin PBXSourcesBuildPhase section */
C41C1B2F22B0097F00E7CF16 /* Sources */ = { C41C1B2F22B0097F00E7CF16 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
@@ -1291,6 +1316,7 @@
C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */, C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */,
C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */, C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */,
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */, C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */,
C40B24F227A310770018C7D2 /* Events.swift in Sources */, C40B24F227A310770018C7D2 /* Events.swift in Sources */,
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */, C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
C4C0E8E027F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */, C4C0E8E027F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */,

View File

@@ -15,11 +15,11 @@ class CommandTest: XCTestCase {
path: Paths.php, path: Paths.php,
arguments: ["-v"] arguments: ["-v"]
) )
XCTAssert(version.contains("(cli)")) XCTAssert(version.contains("(cli)"))
XCTAssert(version.contains("NTS")) XCTAssert(version.contains("NTS"))
XCTAssert(version.contains("built")) XCTAssert(version.contains("built"))
XCTAssert(version.contains("Zend")) XCTAssert(version.contains("Zend"))
} }
} }

View File

@@ -9,7 +9,7 @@
import XCTest import XCTest
class HomebrewPackageTest: XCTestCase { class HomebrewPackageTest: XCTestCase {
// - MARK: SYNTHETIC TESTS // - MARK: SYNTHETIC TESTS
static var jsonBrewFile: URL { static var jsonBrewFile: URL {
@@ -22,7 +22,7 @@ class HomebrewPackageTest: XCTestCase {
let package = try! JSONDecoder().decode( let package = try! JSONDecoder().decode(
[HomebrewPackage].self, from: json.data(using: .utf8)! [HomebrewPackage].self, from: json.data(using: .utf8)!
).first! ).first!
XCTAssertEqual(package.name, "php") XCTAssertEqual(package.name, "php")
XCTAssertEqual(package.full_name, "php") XCTAssertEqual(package.full_name, "php")
XCTAssertEqual(package.aliases.first!, "php@8.0") XCTAssertEqual(package.aliases.first!, "php@8.0")
@@ -30,23 +30,23 @@ class HomebrewPackageTest: XCTestCase {
installed.version.starts(with: "8.0") installed.version.starts(with: "8.0")
}), true) }), true)
} }
static var jsonBrewServicesFile: URL { static var jsonBrewServicesFile: URL {
return Bundle(for: Self.self) return Bundle(for: Self.self)
.url(forResource: "brew-services", withExtension: "json")! .url(forResource: "brew-services", withExtension: "json")!
} }
func testCanParseServicesJson() throws { func testCanParseServicesJson() throws {
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8) let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
let services = try! JSONDecoder().decode( let services = try! JSONDecoder().decode(
[HomebrewService].self, from: json.data(using: .utf8)! [HomebrewService].self, from: json.data(using: .utf8)!
) )
XCTAssertGreaterThan(services.count, 0) XCTAssertGreaterThan(services.count, 0)
XCTAssertEqual(services.first?.name, "dnsmasq") XCTAssertEqual(services.first?.name, "dnsmasq")
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq") XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
} }
// - MARK: LIVE TESTS // - MARK: LIVE TESTS
/// This test requires that you have a valid Homebrew installation set up, /// This test requires that you have a valid Homebrew installation set up,
@@ -63,13 +63,13 @@ class HomebrewPackageTest: XCTestCase {
).filter({ service in ).filter({ service in
return ["php", "nginx", "dnsmasq"].contains(service.name) return ["php", "nginx", "dnsmasq"].contains(service.name)
}) })
XCTAssertTrue(services.contains(where: {$0.name == "php"} )) XCTAssertTrue(services.contains(where: {$0.name == "php"}))
XCTAssertTrue(services.contains(where: {$0.name == "nginx"} )) XCTAssertTrue(services.contains(where: {$0.name == "nginx"}))
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} )) XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"}))
XCTAssertEqual(services.count, 3) XCTAssertEqual(services.count, 3)
} }
/// This test requires that you have a valid Homebrew installation set up, /// This test requires that you have a valid Homebrew installation set up,
/// and requires the `php` formula to be installed. /// and requires the `php` formula to be installed.
/// If this test fails, there is an issue with your Homebrew installation /// If this test fails, there is an issue with your Homebrew installation
@@ -79,7 +79,7 @@ class HomebrewPackageTest: XCTestCase {
[HomebrewPackage].self, [HomebrewPackage].self,
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)! from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
).first! ).first!
XCTAssertTrue(package.name == "php") XCTAssertTrue(package.name == "php")
} }
} }

View File

@@ -9,23 +9,23 @@
import XCTest import XCTest
class NginxConfigurationTest: XCTestCase { class NginxConfigurationTest: XCTestCase {
static var regularUrl: URL { static var regularUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")! return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")!
} }
static var isolatedUrl: URL { static var isolatedUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")! return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")!
} }
static var proxyUrl: URL { static var proxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")! return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")!
} }
static var secureProxyUrl: URL { static var secureProxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")! return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")!
} }
func testCanDetermineSiteNameAndTld() throws { func testCanDetermineSiteNameAndTld() throws {
XCTAssertEqual( XCTAssertEqual(
"nginx-site", "nginx-site",
@@ -36,32 +36,32 @@ class NginxConfigurationTest: XCTestCase {
NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).tld NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).tld
) )
} }
func testCanDetermineIsolation() throws { func testCanDetermineIsolation() throws {
XCTAssertNil( XCTAssertNil(
NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).isolatedVersion NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).isolatedVersion
) )
XCTAssertEqual( XCTAssertEqual(
"8.1", "8.1",
NginxConfiguration(filePath: NginxConfigurationTest.isolatedUrl.path).isolatedVersion NginxConfiguration(filePath: NginxConfigurationTest.isolatedUrl.path).isolatedVersion
) )
} }
func testCanDetermineProxy() throws { func testCanDetermineProxy() throws {
let proxied = NginxConfiguration(filePath: NginxConfigurationTest.proxyUrl.path) let proxied = NginxConfiguration(filePath: NginxConfigurationTest.proxyUrl.path)
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf")) XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy) XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
let normal = NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path) let normal = NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path)
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf")) XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual(nil, normal.proxy) XCTAssertEqual(nil, normal.proxy)
} }
func testCanDetermineSecuredProxy() throws { func testCanDetermineSecuredProxy() throws {
let proxied = NginxConfiguration(filePath: NginxConfigurationTest.secureProxyUrl.path) let proxied = NginxConfiguration(filePath: NginxConfigurationTest.secureProxyUrl.path)
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf")) XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy) XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
} }
} }

View File

@@ -9,24 +9,24 @@
import XCTest import XCTest
class PhpExtensionTest: XCTestCase { class PhpExtensionTest: XCTestCase {
static var phpIniFileUrl: URL { static var phpIniFileUrl: URL {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")! return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
} }
func testCanLoadExtension() throws { func testCanLoadExtension() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl) let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
XCTAssertGreaterThan(extensions.count, 0) XCTAssertGreaterThan(extensions.count, 0)
} }
func testExtensionNameIsCorrect() throws { func testExtensionNameIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl) let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensionNames = extensions.map { (ext) -> String in let extensionNames = extensions.map { (ext) -> String in
return ext.name return ext.name
} }
// These 6 should be found // These 6 should be found
XCTAssertTrue(extensionNames.contains("xdebug")) XCTAssertTrue(extensionNames.contains("xdebug"))
XCTAssertTrue(extensionNames.contains("imagick")) XCTAssertTrue(extensionNames.contains("imagick"))
@@ -34,41 +34,41 @@ class PhpExtensionTest: XCTestCase {
XCTAssertTrue(extensionNames.contains("opcache")) XCTAssertTrue(extensionNames.contains("opcache"))
XCTAssertTrue(extensionNames.contains("yaml")) XCTAssertTrue(extensionNames.contains("yaml"))
XCTAssertTrue(extensionNames.contains("custom")) XCTAssertTrue(extensionNames.contains("custom"))
XCTAssertFalse(extensionNames.contains("fake")) XCTAssertFalse(extensionNames.contains("fake"))
XCTAssertFalse(extensionNames.contains("nice")) XCTAssertFalse(extensionNames.contains("nice"))
} }
func testExtensionStatusIsCorrect() throws { func testExtensionStatusIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl) let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
// xdebug should be enabled // xdebug should be enabled
XCTAssertEqual(extensions[0].enabled, true) XCTAssertEqual(extensions[0].enabled, true)
// imagick should be disabled // imagick should be disabled
XCTAssertEqual(extensions[1].enabled, false) XCTAssertEqual(extensions[1].enabled, false)
} }
func testToggleWorksAsExpected() throws { func testToggleWorksAsExpected() throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.load(from: destination) let extensions = PhpExtension.load(from: destination)
XCTAssertEqual(extensions.count, 6) XCTAssertEqual(extensions.count, 6)
// Try to disable xdebug (should be detected first)! // Try to disable xdebug (should be detected first)!
let xdebug = extensions.first! let xdebug = extensions.first!
XCTAssertTrue(xdebug.name == "xdebug") XCTAssertTrue(xdebug.name == "xdebug")
XCTAssertEqual(xdebug.enabled, true) XCTAssertEqual(xdebug.enabled, true)
xdebug.toggle() xdebug.toggle()
XCTAssertEqual(xdebug.enabled, false) XCTAssertEqual(xdebug.enabled, false)
// Check if the file contains the appropriate data // Check if the file contains the appropriate data
let file = try! String(contentsOf: destination, encoding: .utf8) let file = try! String(contentsOf: destination, encoding: .utf8)
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\"")) XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
// Make sure if we load the data again, it's disabled // Make sure if we load the data again, it's disabled
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false) XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
} }
func testCanRetrieveXdebugMode() throws { func testCanRetrieveXdebugMode() throws {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"]) let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
XCTAssertEqual(value, "coverage") XCTAssertEqual(value, "coverage")

View File

@@ -9,14 +9,14 @@
import XCTest import XCTest
class ValetConfigurationTest: XCTestCase { class ValetConfigurationTest: XCTestCase {
static var jsonConfigFileUrl: URL { static var jsonConfigFileUrl: URL {
return Bundle(for: Self.self).url( return Bundle(for: Self.self).url(
forResource: "valet-config", forResource: "valet-config",
withExtension: "json" withExtension: "json"
)! )!
} }
func testCanLoadConfigFile() throws { func testCanLoadConfigFile() throws {
let json = try? String( let json = try? String(
contentsOf: Self.jsonConfigFileUrl, contentsOf: Self.jsonConfigFileUrl,
@@ -26,7 +26,7 @@ class ValetConfigurationTest: XCTestCase {
Valet.Configuration.self, Valet.Configuration.self,
from: json!.data(using: .utf8)! from: json!.data(using: .utf8)!
) )
XCTAssertEqual(config.tld, "test") XCTAssertEqual(config.tld, "test")
XCTAssertEqual(config.paths, [ XCTAssertEqual(config.paths, [
"/Users/username/.config/valet/Sites", "/Users/username/.config/valet/Sites",
@@ -35,5 +35,5 @@ class ValetConfigurationTest: XCTestCase {
XCTAssertEqual(config.defaultSite, "/Users/username/default-site") XCTAssertEqual(config.defaultSite, "/Users/username/default-site")
XCTAssertEqual(config.loopback, "127.0.0.1") XCTAssertEqual(config.loopback, "127.0.0.1")
} }
} }

View File

@@ -9,12 +9,12 @@
import Foundation import Foundation
class Utility { class Utility {
public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? { public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? {
if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) { if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) {
let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true) let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)") let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)")
do { do {
try FileManager.default.copyItem(at: bundleURL, to: targetURL) try FileManager.default.copyItem(at: bundleURL, to: targetURL)
return targetURL return targetURL
@@ -22,7 +22,7 @@ class Utility {
Log.err("Unable to copy file: \(error)") Log.err("Unable to copy file: \(error)")
} }
} }
return nil return nil
} }
} }

View File

@@ -23,7 +23,7 @@ class PhpVersionDetectionTest: XCTestCase {
"php@5.6", "php@5.6",
"php@5.4" // should be omitted, not supported "php@5.4" // should be omitted, not supported
], checkBinaries: false, generateHelpers: false) ], checkBinaries: false, generateHelpers: false)
XCTAssertEqual(outcome, ["8.0", "7.0"]) XCTAssertEqual(outcome, ["8.0", "7.0"])
} }
} }

View File

@@ -36,13 +36,13 @@ class PhpVersionNumberTest: XCTestCase {
nil nil
) )
} }
func testPhpVersionNumberParse() throws { func testPhpVersionNumberParse() throws {
XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
XCTAssertTrue(error is VersionParseError) XCTAssertTrue(error is VersionParseError)
} }
} }
func testCanCheckFixedConstraints() throws { func testCanCheckFixedConstraints() throws {
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -51,7 +51,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0"]).all .make(from: ["7.0"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"]) .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 PhpVersionNumberCollection
.make(from: ["7.0.3"]).all .make(from: ["7.0.3"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -67,7 +67,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0"]).all .make(from: ["7.0"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -76,7 +76,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: []).all .make(from: []).all
) )
} }
func testCanCheckCaretConstraints() throws { func testCanCheckCaretConstraints() throws {
// 1. Imprecise checks // 1. Imprecise checks
XCTAssertEqual( XCTAssertEqual(
@@ -86,7 +86,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// 2. Imprecise check with precise constraint (lenient AKA not strict) // 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. // These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual( XCTAssertEqual(
@@ -96,7 +96,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// 3. Imprecise check with precise constraint (strict mode) // 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc. // These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual( XCTAssertEqual(
@@ -106,7 +106,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all .make(from: ["7.4", "7.3", "7.2", "7.1"]).all
) )
// 4. Precise members and constraint all around // 4. Precise members and constraint all around
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -115,7 +115,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .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) // 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0 // In strict mode the constraint's patch version is assumed to be 0
XCTAssertEqual( XCTAssertEqual(
@@ -125,7 +125,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .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) // 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal // In lenient mode the constraint's patch version is assumed to be equal
XCTAssertEqual( 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 .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
} }
func testCanCheckTildeConstraints() throws { func testCanCheckTildeConstraints() throws {
// 1. Imprecise checks // 1. Imprecise checks
XCTAssertEqual( XCTAssertEqual(
@@ -146,7 +146,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// 2. Imprecise check with precise constraint (lenient AKA not strict) // 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. // These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual( XCTAssertEqual(
@@ -159,7 +159,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0"]).all .make(from: ["7.0"]).all
) )
// 3. Imprecise check with precise constraint (strict mode) // 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc. // These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual( XCTAssertEqual(
@@ -172,7 +172,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: []).all .make(from: []).all
) )
// 4. Precise members and constraint all around // 4. Precise members and constraint all around
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -183,7 +183,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0.10"]).all .make(from: ["7.0.10"]).all
) )
// 5. Precise members but imprecise constraint (strict mode) // 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0. // In strict mode the constraint's patch version is assumed to be 0.
XCTAssertEqual( XCTAssertEqual(
@@ -193,7 +193,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .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) // 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal. // 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.) // (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 .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
} }
func testCanCheckGreaterThanOrEqualConstraints() throws { func testCanCheckGreaterThanOrEqualConstraints() throws {
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -214,7 +214,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -222,7 +222,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .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) // Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -231,7 +231,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all .make(from: ["7.4", "7.3"]).all
) )
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999) // Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -241,7 +241,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4", "7.3", "7.2"]).all .make(from: ["7.4", "7.3", "7.2"]).all
) )
} }
func testCanCheckGreaterThanConstraints() throws { func testCanCheckGreaterThanConstraints() throws {
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -250,7 +250,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all .make(from: ["7.4", "7.3", "7.2", "7.1"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -259,7 +259,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2"]).all .make(from: ["7.4", "7.3", "7.2"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -268,7 +268,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all .make(from: ["7.4", "7.3"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"]) .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 PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2"]).all .make(from: ["7.3.1", "7.2.9", "7.2"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"]) .make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])

View File

@@ -14,5 +14,5 @@ class ValetVersionExtractorTest: XCTestCase {
let version = valet("--version", sudo: false) let version = valet("--version", sudo: false)
XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3")) XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3"))
} }
} }

View File

@@ -14,12 +14,12 @@ class VersionExtractorTest: XCTestCase {
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1") XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0") XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
} }
func testVersionComparison() { func testVersionComparison() {
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending) XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending) XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame) XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)
XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending) XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending)
} }
} }

View File

@@ -9,42 +9,37 @@ import Foundation
import AppKit import AppKit
class Actions { class Actions {
// MARK: - Services // MARK: - Services
public static func restartPhpFpm() public static func restartPhpFpm() {
{
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true) brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
} }
public static func restartNginx() public static func restartNginx() {
{
brew("services restart nginx", sudo: true) brew("services restart nginx", sudo: true)
} }
public static func restartDnsMasq() public static func restartDnsMasq() {
{
brew("services restart dnsmasq", sudo: true) 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 \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true) brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true) brew("services stop dnsmasq", sudo: true)
} }
public static func fixHomebrewPermissions() throws public static func fixHomebrewPermissions() throws {
{
var servicesCommands = [ var servicesCommands = [
"\(Paths.brew) services stop nginx", "\(Paths.brew) services stop nginx",
"\(Paths.brew) services stop dnsmasq", "\(Paths.brew) services stop dnsmasq"
] ]
var cellarCommands = [ var cellarCommands = [
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx", "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq" "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq"
] ]
PhpEnv.shared.availablePhpVersions.forEach { version in PhpEnv.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnv.brewPhpVersion let formula = version == PhpEnv.brewPhpVersion
? "php" ? "php"
@@ -52,66 +47,61 @@ class Actions {
servicesCommands.append("\(Paths.brew) services stop \(formula)") servicesCommands.append("\(Paths.brew) services stop \(formula)")
cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)") cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
} }
let script = let script =
servicesCommands.joined(separator: " && ") servicesCommands.joined(separator: " && ")
+ " && " + " && "
+ cellarCommands.joined(separator: " && ") + cellarCommands.joined(separator: " && ")
let appleScript = NSAppleScript( let appleScript = NSAppleScript(
source: "do shell script \"\(script)\" with administrator privileges" source: "do shell script \"\(script)\" with administrator privileges"
) )
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil) let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
if (eventResult == nil) { if eventResult == nil {
throw HomebrewPermissionError(kind: .applescriptNilError) throw HomebrewPermissionError(kind: .applescriptNilError)
} }
} }
// MARK: - Finding Config Files // MARK: - Finding Config Files
public static func openGenericPhpConfigFolder() public static func openGenericPhpConfigFolder() {
{ let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
} }
public static func openGlobalComposerFolder() public static func openGlobalComposerFolder() {
{
let file = FileManager.default.homeDirectoryForCurrentUser let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json") .appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
} }
public static func openPhpConfigFolder(version: String) public static func openPhpConfigFolder(version: String) {
{ let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
} }
public static func openValetConfigFolder() public static func openValetConfigFolder() {
{
let file = FileManager.default.homeDirectoryForCurrentUser let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet") .appendingPathComponent(".config/valet")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
} }
// MARK: - Other Actions // MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL public static func createTempPhpInfoFile() -> URL {
{
// Write a file called `phpmon_phpinfo.php` to /tmp // Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8) 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 // 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") Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")! return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
} }
// MARK: - Fix My Valet // MARK: - Fix My Valet
/** /**
Detects all currently available PHP versions, Detects all currently available PHP versions,
and unlinks each and every one of them. 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 If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`. 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: { InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
brew("services restart dnsmasq", sudo: true) brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true) brew("services restart php", sudo: true)

View File

@@ -8,7 +8,7 @@
import Cocoa import Cocoa
public class Command { public class Command {
/** /**
Immediately executes a command. Immediately executes a command.
@@ -20,21 +20,21 @@ public class Command {
let task = Process() let task = Process()
task.launchPath = path task.launchPath = path
task.arguments = arguments task.arguments = arguments
let pipe = Pipe() let pipe = Pipe()
task.standardOutput = pipe task.standardOutput = pipe
task.launch() task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile() let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String.init(data: data, encoding: String.Encoding.utf8)! let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
if (trimNewlines) { if trimNewlines {
return output.components(separatedBy: .newlines) return output.components(separatedBy: .newlines)
.filter({ !$0.isEmpty }) .filter({ !$0.isEmpty })
.joined(separator: "\n") .joined(separator: "\n")
} }
return output return output
} }
} }

View File

@@ -8,13 +8,13 @@
import Cocoa import Cocoa
struct Constants { struct Constants {
/** /**
* The latest PHP version that is considered to be stable at the time of release. * 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). * This version number is currently not used (only as a default fallback).
*/ */
static let LatestStablePhpVersion = "8.1" static let LatestStablePhpVersion = "8.1"
/** /**
The minimum version of Valet that is recommended. The minimum version of Valet that is recommended.
If the installed version is older, a notification will be shown 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 See also: https://github.com/laravel/valet/releases/tag/v2.16.2
*/ */
static let MinimumRecommendedValetVersion = "2.16.2" static let MinimumRecommendedValetVersion = "2.16.2"
/** /**
* The PHP versions supported by this application. * The PHP versions supported by this application.
* Versions that do not appear in this array are omitted from the list. * Versions that do not appear in this array are omitted from the list.
@@ -42,7 +42,7 @@ struct Constants {
"7.4", "7.4",
"8.0", "8.0",
"8.1", "8.1",
// ==================== // ====================
// EXPERIMENTAL SUPPORT // EXPERIMENTAL SUPPORT
// ==================== // ====================
@@ -50,9 +50,9 @@ struct Constants {
// dev release. In this case, that means that the version below is detected. // dev release. In this case, that means that the version below is detected.
"8.2" "8.2"
] ]
struct Urls { struct Urls {
static let DonationPayment = URL( static let DonationPayment = URL(
string: "https://nicoverbruggen.be/sponsor#pay-now" string: "https://nicoverbruggen.be/sponsor#pay-now"
)! )!
@@ -62,7 +62,7 @@ struct Constants {
static let FrequentlyAskedQuestions = URL( static let FrequentlyAskedQuestions = URL(
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting" string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
)! )!
} }
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
class Events { class Events {
static let ServicesUpdated = Notification.Name("ServicesUpdated") static let ServicesUpdated = Notification.Name("ServicesUpdated")
} }

View File

@@ -11,28 +11,25 @@
/** /**
Runs a `valet` command. Defaults to running as superuser. 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) return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
} }
/** /**
Runs a `brew` command. Can run as superuser. 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)") Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
} }
/** /**
Runs `sed` in order to replace all occurrences of a string in a specific file with another. 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) // Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/") let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/") let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
// Check if gsed exists; it is able to follow symlinks, // Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension // which we want to do to toggle the extension
if Filesystem.fileExists("\(Paths.binPath)/gsed") { 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. 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(""" return Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO" grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""") """)

View File

@@ -9,50 +9,50 @@
import Foundation import Foundation
class Log { class Log {
static var shared = Log() static var shared = Log()
enum Verbosity: Int { enum Verbosity: Int {
case error = 1, case error = 1,
warning = 2, warning = 2,
info = 3, info = 3,
performance = 4 performance = 4
public func isApplicable() -> Bool { public func isApplicable() -> Bool {
return Log.shared.verbosity.rawValue >= self.rawValue return Log.shared.verbosity.rawValue >= self.rawValue
} }
} }
var verbosity: Verbosity = .warning var verbosity: Verbosity = .warning
static func err(_ item: Any) { static func err(_ item: Any) {
if Verbosity.error.isApplicable() { if Verbosity.error.isApplicable() {
print("[E] \(item)") print("[E] \(item)")
} }
} }
static func warn(_ item: Any) { static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() { if Verbosity.warning.isApplicable() {
print("[W] \(item)") print("[W] \(item)")
} }
} }
static func info(_ item: Any) { static func info(_ item: Any) {
if Verbosity.info.isApplicable() { if Verbosity.info.isApplicable() {
print("\(item)") print("\(item)")
} }
} }
static func perf(_ item: Any) { static func perf(_ item: Any) {
if Verbosity.performance.isApplicable() { if Verbosity.performance.isApplicable() {
print("[P] \(item)") print("[P] \(item)")
} }
} }
static func separator(as verbosity: Verbosity = .info) { static func separator(as verbosity: Verbosity = .info) {
if verbosity.isApplicable() { if verbosity.isApplicable() {
print("==================================") print("==================================")
} }
} }
} }

View File

@@ -12,71 +12,71 @@ import Foundation
The path to the Homebrew directory and the user's name are fetched only once, at boot. The path to the Homebrew directory and the user's name are fetched only once, at boot.
*/ */
public class Paths { public class Paths {
public static let shared = Paths() public static let shared = Paths()
internal var baseDir: Paths.HomebrewDir internal var baseDir: Paths.HomebrewDir
private var userName: String private var userName: String
init() { init() {
baseDir = App.architecture != "x86_64" ? .opt : .usr baseDir = App.architecture != "x86_64" ? .opt : .usr
userName = String(Shell.pipe("whoami").split(separator: "\n")[0]) userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
} }
public func detectBinaryPaths() { public func detectBinaryPaths() {
detectComposerBinary() detectComposerBinary()
} }
// - MARK: Binaries // - MARK: Binaries
public static var valet: String { public static var valet: String {
return "\(binPath)/valet" return "\(binPath)/valet"
} }
public static var brew: String { public static var brew: String {
return "\(binPath)/brew" return "\(binPath)/brew"
} }
public static var php: String { public static var php: String {
return "\(binPath)/php" return "\(binPath)/php"
} }
public static var phpConfig: String { public static var phpConfig: String {
return "\(binPath)/php-config" return "\(binPath)/php-config"
} }
// - MARK: Detected Binaries // - MARK: Detected Binaries
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */ /** 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 // - MARK: Paths
public static var whoami: String { public static var whoami: String {
return shared.userName return shared.userName
} }
public static var cellarPath: String { public static var cellarPath: String {
return "\(shared.baseDir.rawValue)/Cellar" return "\(shared.baseDir.rawValue)/Cellar"
} }
public static var binPath: String { public static var binPath: String {
return "\(shared.baseDir.rawValue)/bin" return "\(shared.baseDir.rawValue)/bin"
} }
public static var optPath: String { public static var optPath: String {
return "\(shared.baseDir.rawValue)/opt" return "\(shared.baseDir.rawValue)/opt"
} }
public static var etcPath: String { public static var etcPath: String {
return "\(shared.baseDir.rawValue)/etc" return "\(shared.baseDir.rawValue)/etc"
} }
// MARK: - Flexible Binaries // MARK: - Flexible Binaries
// (these can be in multiple locations, so we scan common places because) // (these can be in multiple locations, so we scan common places because)
// (PHP Monitor will not use the user's own PATH) // (PHP Monitor will not use the user's own PATH)
private func detectComposerBinary() { private func detectComposerBinary() {
if Filesystem.fileExists("/usr/local/bin/composer") { if Filesystem.fileExists("/usr/local/bin/composer") {
Paths.composer = "/usr/local/bin/composer" Paths.composer = "/usr/local/bin/composer"
@@ -87,12 +87,12 @@ public class Paths {
Log.warn("Composer was not found.") Log.warn("Composer was not found.")
} }
} }
// MARK: - Enum // MARK: - Enum
public enum HomebrewDir: String { public enum HomebrewDir: String {
case opt = "/opt/homebrew" case opt = "/opt/homebrew"
case usr = "/usr/local" case usr = "/usr/local"
} }
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
extension Process { extension Process {
/** /**
When a process is running in the background, it can send content to standard 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` output or standard error, just like it would in a terminal. Using `listen`
@@ -22,10 +22,10 @@ extension Process {
) { ) {
let outputPipe = Pipe() let outputPipe = Pipe()
let errorPipe = Pipe() let errorPipe = Pipe()
self.standardOutput = outputPipe self.standardOutput = outputPipe
self.standardError = errorPipe self.standardError = errorPipe
[ [
(outputPipe, didReceiveStandardOutputData), (outputPipe, didReceiveStandardOutputData),
(errorPipe, didReceiveStandardErrorData) (errorPipe, didReceiveStandardErrorData)
@@ -35,15 +35,18 @@ extension Process {
forName: NSNotification.Name.NSFileHandleDataAvailable, forName: NSNotification.Name.NSFileHandleDataAvailable,
object: pipe.fileHandleForReading, object: pipe.fileHandleForReading,
queue: nil queue: nil
) { notification in ) { _ in
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) { if let outputString = String(
data: pipe.fileHandleForReading.availableData,
encoding: String.Encoding.utf8
) {
callback(outputString) callback(outputString)
} }
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify() pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
} }
} }
} }
/** /**
After the process is done running, you'll want to stop listening. After the process is done running, you'll want to stop listening.
*/ */
@@ -55,5 +58,5 @@ extension Process {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading) NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
} }
} }
} }

View File

@@ -8,35 +8,35 @@
import Cocoa import Cocoa
public class Shell { public class Shell {
// MARK: - Invoke static functions // MARK: - Invoke static functions
public static func run( public static func run(
_ command: String, _ command: String,
requiresPath: Bool = false requiresPath: Bool = false
) { ) {
Shell.user.run(command, requiresPath: requiresPath) Shell.user.run(command, requiresPath: requiresPath)
} }
public static func pipe( public static func pipe(
_ command: String, _ command: String,
requiresPath: Bool = false requiresPath: Bool = false
) -> String { ) -> String {
return Shell.user.pipe(command, requiresPath: requiresPath) return Shell.user.pipe(command, requiresPath: requiresPath)
} }
// MARK: - Singleton // MARK: - Singleton
/** /**
We now require macOS 11, so no need to detect which terminal to use. We now require macOS 11, so no need to detect which terminal to use.
*/ */
public var shell: String = "/bin/sh" public var shell: String = "/bin/sh"
/** /**
Singleton to access a user shell (with --login) Singleton to access a user shell (with --login)
*/ */
public static let user = Shell() public static let user = Shell()
/** /**
Runs a shell command without using the output. Runs a shell command without using the output.
Uses the default shell. Uses the default shell.
@@ -51,7 +51,7 @@ public class Shell {
// Equivalent of piping to /dev/null; don't do anything with the string // Equivalent of piping to /dev/null; don't do anything with the string
_ = Shell.pipe(command, requiresPath: requiresPath) _ = Shell.pipe(command, requiresPath: requiresPath)
} }
/** /**
Runs a shell command and returns the output. Runs a shell command and returns the output.
@@ -69,7 +69,7 @@ public class Shell {
) )
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
} }
/** /**
Runs the command and returns a `ShellOutput` object, which contains info about the process. Runs the command and returns a `ShellOutput` object, which contains info about the process.
@@ -81,16 +81,16 @@ public class Shell {
_ command: String, _ command: String,
requiresPath: Bool = false requiresPath: Bool = false
) -> Shell.Output { ) -> Shell.Output {
let outputPipe = Pipe() let outputPipe = Pipe()
let errorPipe = Pipe() let errorPipe = Pipe()
let task = self.createTask(for: command, requiresPath: requiresPath) let task = self.createTask(for: command, requiresPath: requiresPath)
task.standardOutput = outputPipe task.standardOutput = outputPipe
task.standardError = errorPipe task.standardError = errorPipe
task.launch() task.launch()
task.waitUntilExit() task.waitUntilExit()
return Shell.Output( return Shell.Output(
standardOutput: String( standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(), data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
@@ -103,7 +103,7 @@ public class Shell {
task: task task: task
) )
} }
/** /**
Creates a new process with the correct PATH and shell. Creates a new process with the correct PATH and shell.
*/ */
@@ -111,19 +111,19 @@ public class Shell {
let tailoredCommand = requiresPath let tailoredCommand = requiresPath
? "export PATH=\(Paths.binPath):$PATH && \(command)" ? "export PATH=\(Paths.binPath):$PATH && \(command)"
: command : command
let task = Process() let task = Process()
task.launchPath = self.shell task.launchPath = self.shell
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand] task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
return task return task
} }
public class Output { public class Output {
public let standardOutput: String public let standardOutput: String
public let errorOutput: String public let errorOutput: String
public let task: Process public let task: Process
init(standardOutput: String, init(standardOutput: String,
errorOutput: String, errorOutput: String,
task: Process) { task: Process) {

View File

@@ -15,9 +15,9 @@ struct HomebrewPermissionError: Error, AlertableError {
enum Kind: String { enum Kind: String {
case applescriptNilError = "homebrew_permissions.applescript_returned_nil" case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
} }
let kind: Kind let kind: Kind
func getErrorMessageKey() -> String { func getErrorMessageKey() -> String {
return "alert.errors.\(self.kind.rawValue)" return "alert.errors.\(self.kind.rawValue)"
} }

View File

@@ -8,11 +8,11 @@
import Cocoa import Cocoa
extension Date { extension Date {
func toString() -> String { func toString() -> String {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.string(from: self) return dateFormatter.string(from: self)
} }
} }

View File

@@ -9,21 +9,21 @@
import Cocoa import Cocoa
extension NSMenu { extension NSMenu {
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) { open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
newItem.keyEquivalentModifierMask = modifier newItem.keyEquivalentModifierMask = modifier
self.addItem(newItem) self.addItem(newItem)
} }
} }
@IBDesignable class LocalizedMenuItem: NSMenuItem { @IBDesignable class LocalizedMenuItem: NSMenuItem {
@IBInspectable @IBInspectable
var localizationKey: String? { var localizationKey: String? {
didSet { didSet {
self.title = localizationKey?.localized ?? self.title self.title = localizationKey?.localized ?? self.title
} }
} }
} }

View File

@@ -10,29 +10,29 @@ import Foundation
import Cocoa import Cocoa
extension NSWindow { extension NSWindow {
/** /**
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/ 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 numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
let frame: CGRect = self.frame let frame: CGRect = self.frame
let shakeAnimation :CAKeyframeAnimation = CAKeyframeAnimation() let shakeAnimation: CAKeyframeAnimation = CAKeyframeAnimation()
let shakePath = CGMutablePath() 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 { for _ in 0...numberOfShakes-1 {
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:NSMinX(frame) + frame.size.width * vigourOfShake, y:NSMinY(frame))) shakePath.addLine(to: CGPoint(x: frame.minX + frame.size.width * vigourOfShake, y: frame.minY))
} }
shakePath.closeSubpath() shakePath.closeSubpath()
shakeAnimation.path = shakePath shakeAnimation.path = shakePath
shakeAnimation.duration = durationOfShake shakeAnimation.duration = durationOfShake
self.animations = ["frameOrigin":shakeAnimation] self.animations = ["frameOrigin": shakeAnimation]
self.animator().setFrameOrigin(self.frame.origin) self.animator().setFrameOrigin(self.frame.origin)
} }
} }

View File

@@ -7,28 +7,28 @@
import Foundation import Foundation
extension String { extension String {
var localized: String { var localized: String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "") return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
} }
func localized(_ args: CVarArg...) -> String { func localized(_ args: CVarArg...) -> String {
String(format: self.localized, arguments: args) String(format: self.localized, arguments: args)
} }
func countInstances(of stringToFind: String) -> Int { func countInstances(of stringToFind: String) -> Int {
if (stringToFind.isEmpty) { if stringToFind.isEmpty {
return 0 return 0
} }
var count = 0 var count = 0
var searchRange: Range<String.Index>? var searchRange: Range<String.Index>?
while let foundRange = range(of: stringToFind, options: [], range: searchRange) { while let foundRange = range(of: stringToFind, options: [], range: searchRange) {
count += 1 count += 1
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex)) searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
} }
return count return count
} }
@@ -37,7 +37,7 @@ extension String {
let end = r.upperBound let end = r.upperBound
return String(self[start ..< end]) return String(self[start ..< end])
} }
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/ // Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
/* /*
<1> We split the version by period (.). <1> We split the version by period (.).
@@ -50,12 +50,12 @@ extension String {
*/ */
func versionCompare(_ otherVersion: String) -> ComparisonResult { func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "." let versionDelimiter = "."
var versionComponents = self.components(separatedBy: versionDelimiter) // <1> var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter) var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2> let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
if zeroDiff == 0 { // <3> if zeroDiff == 0 { // <3>
// Same format, compare normally // Same format, compare normally
return self.compare(otherVersion, options: .numeric) return self.compare(otherVersion, options: .numeric)
@@ -70,5 +70,5 @@ extension String {
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6> .compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
} }
} }
} }

View File

@@ -12,25 +12,25 @@ import Cocoa
// Adapted from: https://stackoverflow.com/a/46268778 // Adapted from: https://stackoverflow.com/a/46268778
protocol XibLoadable { protocol XibLoadable {
static var xibName: String? { get } static var xibName: String? { get }
static func createFromXib(in bundle: Bundle) -> Self? static func createFromXib(in bundle: Bundle) -> Self?
} }
extension XibLoadable where Self: NSView { extension XibLoadable where Self: NSView {
static var xibName: String? { static var xibName: String? {
return String(describing: Self.self) return String(describing: Self.self)
} }
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? { static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
guard let xibName = xibName else { return nil } guard let xibName = xibName else { return nil }
var topLevelArray: NSArray? = nil var topLevelArray: NSArray?
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray) bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
guard let results = topLevelArray else { return nil } 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 return views.last as? Self
} }
} }

View File

@@ -8,7 +8,7 @@
import Cocoa import Cocoa
class Alert { class Alert {
public static func confirm( public static func confirm(
onWindow window: NSWindow, onWindow window: NSWindow,
messageText: String, messageText: String,
@@ -21,13 +21,13 @@ class Alert {
if !Thread.isMainThread { if !Thread.isMainThread {
fatalError("You should always present alerts on the main thread!") fatalError("You should always present alerts on the main thread!")
} }
let alert = NSAlert.init() let alert = NSAlert.init()
alert.alertStyle = style alert.alertStyle = style
alert.messageText = messageText alert.messageText = messageText
alert.informativeText = informativeText alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle) alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) { if !secondButtonTitle.isEmpty {
alert.addButton(withTitle: secondButtonTitle) alert.addButton(withTitle: secondButtonTitle)
} }
alert.beginSheetModal(for: window) { response in alert.beginSheetModal(for: window) { response in
@@ -36,5 +36,5 @@ class Alert {
} }
} }
} }
} }

View File

@@ -12,23 +12,23 @@ import Foundation
/// In most cases this is going to be a code editor, but it could also be another application /// 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. /// that supports opening those directories, like a visual Git client or a terminal app.
class Application { class Application {
enum AppType { enum AppType {
case editor, browser, git_gui, terminal, user_supplied case editor, browser, git_gui, terminal, user_supplied
} }
/// Name of the app. Used for display purposes and to determine `name.app` exists. /// Name of the app. Used for display purposes and to determine `name.app` exists.
let name: String let name: String
/// Application type. Depending on the type, a different action might occur. /// Application type. Depending on the type, a different action might occur.
let type: AppType let type: AppType
/// Initializer. Used to detect a specific app of a specific type. /// Initializer. Used to detect a specific app of a specific type.
init(_ name: String, _ type: AppType) { init(_ name: String, _ type: AppType) {
self.name = name self.name = name
self.type = type self.type = type
} }
/** /**
Attempt to open a specific directory in the app of choice. Attempt to open a specific directory in the app of choice.
(This will open the app if it isn't open yet.) (This will open the app if it isn't open yet.)
@@ -36,7 +36,7 @@ class Application {
@objc public func openDirectory(file: String) { @objc public func openDirectory(file: String) {
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"") return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
} }
/** Checks if the app is installed. */ /** Checks if the app is installed. */
func isInstalled() -> Bool { func isInstalled() -> Bool {
// If this script does not complain, the app exists! // If this script does not complain, the app exists!
@@ -45,7 +45,7 @@ class Application {
requiresPath: false requiresPath: false
).task.terminationStatus == 0 ).task.terminationStatus == 0
} }
/** /**
Detect which apps are available to open a specific directory. Detect which apps are available to open a specific directory.
*/ */

View File

@@ -9,7 +9,7 @@
import Cocoa import Cocoa
class Filesystem { class Filesystem {
/** /**
Checks if a file exists at the provided path. Checks if a file exists at the provided path.
Uses `FileManager`. Uses `FileManager`.
@@ -19,5 +19,5 @@ class Filesystem {
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)") atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
) )
} }
} }

View File

@@ -9,19 +9,19 @@ import Foundation
import UserNotifications import UserNotifications
class LocalNotification { class LocalNotification {
public static func send(title: String, subtitle: String) { public static func send(title: String, subtitle: String) {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = title content.title = title
content.body = subtitle content.body = subtitle
let uuidString = UUID().uuidString let uuidString = UUID().uuidString
let request = UNNotificationRequest( let request = UNNotificationRequest(
identifier: uuidString, identifier: uuidString,
content: content, content: content,
trigger: nil trigger: nil
) )
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) { (error) in notificationCenter.add(request) { (error) in
if error != nil { if error != nil {
@@ -29,5 +29,5 @@ class LocalNotification {
} }
} }
} }
} }

View File

@@ -8,40 +8,40 @@
import Cocoa import Cocoa
class MenuBarImageGenerator { class MenuBarImageGenerator {
/** /**
Takes a string and converts it to an image that can be displayed in the menu bar. 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. The width of the NSImage depends on the length of the text.
*/ */
public static func textToImage(text: String) -> NSImage { public static func textToImage(text: String) -> NSImage {
let font = NSFont.systemFont(ofSize: 14, weight: .medium) let font = NSFont.systemFont(ofSize: 14, weight: .medium)
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [ let textFontAttributes = [
NSAttributedString.Key.font: font, NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: NSColor.black, NSAttributedString.Key.foregroundColor: NSColor.black,
NSAttributedString.Key.paragraphStyle: textStyle 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 // Create an attributed string so we'll know how wide the item will need to be
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes) let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
let textSize = attributedString.size() let textSize = attributedString.size()
// Add padding to the width of the menu bar item // Add padding to the width of the menu bar item
let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height) let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height)
let image = NSImage(size: size) let image = NSImage(size: size)
// Set the image rect with the appropriate dimensions // Set the image rect with the appropriate dimensions
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
// Position the text inside the image rect // Position the text inside the image rect
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height) let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
let targetImage: NSImage = NSImage(size: image.size) let targetImage: NSImage = NSImage(size: image.size)
let representation: NSBitmapImageRep = NSBitmapImageRep( let representation: NSBitmapImageRep = NSBitmapImageRep(
bitmapDataPlanes: nil, bitmapDataPlanes: nil,
pixelsWide: Int(image.size.width), pixelsWide: Int(image.size.width),
@@ -54,40 +54,40 @@ class MenuBarImageGenerator {
bytesPerRow: 0, bytesPerRow: 0,
bitsPerPixel: 0 bitsPerPixel: 0
)! )!
targetImage.addRepresentation(representation) targetImage.addRepresentation(representation)
targetImage.lockFocus() targetImage.lockFocus()
image.draw(in: imageRect) image.draw(in: imageRect)
text.draw(in: textRect, withAttributes: textFontAttributes) text.draw(in: textRect, withAttributes: textFontAttributes)
targetImage.unlockFocus() targetImage.unlockFocus()
return targetImage return targetImage
} }
/** /**
The same as before, but also attempts to add an icon to the left. The same as before, but also attempts to add an icon to the left.
*/ */
public static func textToImageWithIcon(text: String) -> NSImage { public static func textToImageWithIcon(text: String) -> NSImage {
// We'll start out with the image containing the text // We'll start out with the image containing the text
let textImage = self.textToImage(text: text) let textImage = self.textToImage(text: text)
// Then we'll fetch the image we want on the left // Then we'll fetch the image we want on the left
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
if iconType == nil { if iconType == nil {
Log.warn("Invalid icon type found, using the default") Log.warn("Invalid icon type found, using the default")
iconType = MenuBarIcon.iconPhp.rawValue iconType = MenuBarIcon.iconPhp.rawValue
} }
let iconImage = NSImage(named: "MenuBar_\(iconType!)")! let iconImage = NSImage(named: "MenuBar_\(iconType!)")!
// We'll need to reference the width of the icon a bunch of times // We'll need to reference the width of the icon a bunch of times
let iconWidthSize = iconImage.size.width let iconWidthSize = iconImage.size.width
// There will also be an additional divider between the image and the text (image) // There will also be an additional divider between the image and the text (image)
let divider: CGFloat = 3 let divider: CGFloat = 3
// Use a fixed size for the height of the menu bar (18pt) // Use a fixed size for the height of the menu bar (18pt)
let imageRect = CGRect( let imageRect = CGRect(
x: 0, x: 0,
@@ -95,14 +95,14 @@ class MenuBarImageGenerator {
width: textImage.size.width + iconWidthSize + divider, width: textImage.size.width + iconWidthSize + divider,
height: 18 height: 18
) )
// Create a new image, we'll draw the text and our icon in there // Create a new image, we'll draw the text and our icon in there
let image: NSImage = NSImage(size: imageRect.size) let image: NSImage = NSImage(size: imageRect.size)
image.lockFocus() image.lockFocus()
// Calculate the offset between the image and the text // Calculate the offset between the image and the text
let offset = imageRect.size.width - textImage.size.width 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) // Draw the text with a negative x offset (so there is room on the left for the icon)
textImage.draw( textImage.draw(
in: imageRect, in: imageRect,
@@ -115,7 +115,7 @@ class MenuBarImageGenerator {
operation: .overlay, operation: .overlay,
fraction: 1 fraction: 1
) )
// Draw the icon directly in the left of the imageRect (where we left space) // Draw the icon directly in the left of the imageRect (where we left space)
iconImage.draw( iconImage.draw(
in: imageRect, in: imageRect,
@@ -128,11 +128,11 @@ class MenuBarImageGenerator {
operation: .overlay, operation: .overlay,
fraction: 1 fraction: 1
) )
// We're done with this image // We're done with this image
image.unlockFocus() image.unlockFocus()
return image return image
} }
} }

View File

@@ -15,32 +15,32 @@ import Cocoa
- Note: This class does make a simple assumption: each window controller corresponds to a single view. - Note: This class does make a simple assumption: each window controller corresponds to a single view.
*/ */
class PMWindowController: NSWindowController, NSWindowDelegate { class PMWindowController: NSWindowController, NSWindowDelegate {
public var windowName: String { public var windowName: String {
fatalError("Please specify a window name") fatalError("Please specify a window name")
} }
override func showWindow(_ sender: Any?) { override func showWindow(_ sender: Any?) {
super.showWindow(sender) super.showWindow(sender)
App.shared.register(window: windowName) App.shared.register(window: windowName)
} }
func windowWillClose(_ notification: Notification) { func windowWillClose(_ notification: Notification) {
App.shared.remove(window: windowName) App.shared.remove(window: windowName)
} }
deinit { deinit {
Log.perf("Window controller '\(windowName)' was deinitialized") Log.perf("Window controller '\(windowName)' was deinitialized")
} }
} }
extension NSWindowController { extension NSWindowController {
public func positionWindowInTopLeftCorner() { public func positionWindowInTopLeftCorner() {
guard let frame = NSScreen.main?.frame else { return } guard let frame = NSScreen.main?.frame else { return }
guard let window = self.window else { return } guard let window = self.window else { return }
window.setFrame(NSRect( window.setFrame(NSRect(
x: frame.size.width - window.frame.size.width - 20, x: frame.size.width - window.frame.size.width - 20,
y: frame.size.height - window.frame.size.height - 40, y: frame.size.height - window.frame.size.height - 40,
@@ -48,5 +48,5 @@ extension NSWindowController {
height: window.frame.height height: window.frame.height
), display: true) ), display: true)
} }
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
class VersionExtractor { class VersionExtractor {
/** /**
This attempts to extract the version number from any given string. This attempts to extract the version number from any given string.
*/ */
@@ -19,26 +19,26 @@ class VersionExtractor {
pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#, pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
options: [] options: []
) )
let match = regex.matches( let match = regex.matches(
in: string, in: string,
options: [], options: [],
range: NSMakeRange(0, string.count) range: NSRange(location: 0, length: string.count)
).first ).first
guard let match = match else { guard let match = match else {
return nil return nil
} }
let range = Range( let range = Range(
match.range(withName: "version"), match.range(withName: "version"),
in: string in: string
)! )!
return String(string[range]) return String(string[range])
} catch { } catch {
return nil return nil
} }
} }
} }

View File

@@ -21,78 +21,78 @@ class ActivePhpInstallation {
var version: Version! var version: Version!
var limits: Limits! var limits: Limits!
var extensions: [PhpExtension]! var extensions: [PhpExtension]!
// MARK: - Computed // MARK: - Computed
var formula: String { var formula: String {
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)" return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
} }
// MARK: - Initializer // MARK: - Initializer
init() { init() {
// Show information about the current version // Show information about the current version
getVersion() getVersion()
// If an error occurred, exit early // If an error occurred, exit early
if (version.error) { if version.error {
limits = Limits() limits = Limits()
extensions = [] extensions = []
return return
} }
// Load extension information // Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path) extensions = PhpExtension.load(from: path)
// Get configuration values // Get configuration values
limits = Limits( limits = Limits(
memory_limit: getByteCount(key: "memory_limit"), memory_limit: getByteCount(key: "memory_limit"),
upload_max_filesize: getByteCount(key: "upload_max_filesize"), upload_max_filesize: getByteCount(key: "upload_max_filesize"),
post_max_size: getByteCount(key: "post_max_size") post_max_size: getByteCount(key: "post_max_size")
) )
// Return a list of .ini files parsed after php.ini // Return a list of .ini files parsed after php.ini
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"]) let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
.replacingOccurrences(of: "\n", with: "") .replacingOccurrences(of: "\n", with: "")
.split(separator: ",") .split(separator: ",")
.map { String($0) } .map { String($0) }
// See if any extensions are present in said .ini files // See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in paths.forEach { (iniFilePath) in
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath)) let loadedExtensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if exts.count > 0 { if loadedExtensions.isEmpty {
extensions.append(contentsOf: exts) extensions.append(contentsOf: loadedExtensions)
} }
} }
} }
/** /**
When the app tries to retrieve the version, the installation is considered broken if the output is nothing, 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. _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() self.version = Version()
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true) 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.short = "💩 BROKEN"
self.version.long = "" self.version.long = ""
self.version.error = true self.version.error = true
return return
} }
// That's the long version // That's the long version
self.version.long = version self.version.long = version
// Next up, let's strip away the minor version number // Next up, let's strip away the minor version number
let segments = self.version.long.components(separatedBy: ".") let segments = self.version.long.components(separatedBy: ".")
// Get the first two elements // Get the first two elements
self.version.short = segments[0...1].joined(separator: ".") self.version.short = segments[0...1].joined(separator: ".")
} }
/** /**
Retrieves the display value for a specific key in the `.ini` file. Retrieves the display value for a specific key in the `.ini` file.
@@ -110,18 +110,18 @@ class ActivePhpInstallation {
*/ */
private func getByteCount(key: String) -> String { private func getByteCount(key: String) -> String {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"]) let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
// Check if the value is unlimited // Check if the value is unlimited
if (value == "-1") { if value == "-1" {
return "" return ""
} }
// Check if the syntax is valid otherwise // Check if the syntax is valid otherwise
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: []) 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" return (match == nil) ? "⚠️" : "\(value)B"
} }
/** /**
Determine if PHP-FPM is configured correctly. Determine if PHP-FPM is configured correctly.
@@ -135,11 +135,11 @@ class ActivePhpInstallation {
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf" let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
return Shell.pipe("cat \(fileName)").contains("valet.sock") return Shell.pipe("cat \(fileName)").contains("valet.sock")
} }
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :) // 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") return Filesystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
} }
// MARK: - Structs // MARK: - Structs
/** /**
@@ -153,7 +153,7 @@ class ActivePhpInstallation {
var long = "???" var long = "???"
var error = false var error = false
} }
/** /**
Struct containing information about the limits of the current PHP installation. Struct containing information about the limits of the current PHP installation.
Includes: memory limit, max upload size and max post size. Includes: memory limit, max upload size and max post size.
@@ -163,5 +163,5 @@ class ActivePhpInstallation {
var upload_max_filesize = "???" var upload_max_filesize = "???"
var post_max_size = "???" var post_max_size = "???"
} }
} }

View File

@@ -9,15 +9,15 @@
import Foundation import Foundation
class Xdebug { class Xdebug {
public static var enabled: Bool { public static var enabled: Bool {
return !self.mode.isEmpty return !self.mode.isEmpty
} }
public static var mode: String { public static var mode: String {
return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"]) return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
} }
public static var modes: [String] { public static var modes: [String] {
return [ return [
"off", "off",
@@ -26,8 +26,8 @@ class Xdebug {
"debug", "debug",
"gcstats", "gcstats",
"profile", "profile",
"trace", "trace"
] ]
} }
} }

View File

@@ -8,18 +8,18 @@
import Foundation import Foundation
struct HomebrewPackage: Decodable { struct HomebrewPackage: Decodable {
let name: String let name: String
let full_name: String let full_name: String
let aliases: [String] let aliases: [String]
let installed: [HomebrewInstalled] let installed: [HomebrewInstalled]
let linked_keg: String? let linked_keg: String?
public var version: String { public var version: String {
return aliases.first! return aliases.first!
.replacingOccurrences(of: "php@", with: "") .replacingOccurrences(of: "php@", with: "")
} }
} }
struct HomebrewInstalled: Decodable { struct HomebrewInstalled: Decodable {

View File

@@ -18,7 +18,7 @@ struct HomebrewService: Decodable, Equatable {
let status: String? let status: String?
let log_path: String? let log_path: String?
let error_log_path: String? let error_log_path: String?
public static func loadAll( public static func loadAll(
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"], filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
completion: @escaping ([HomebrewService]) -> Void completion: @escaping ([HomebrewService]) -> Void
@@ -27,11 +27,11 @@ struct HomebrewService: Decodable, Equatable {
let data = Shell let data = Shell
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true) .pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
.data(using: .utf8)! .data(using: .utf8)!
let services = try! JSONDecoder() let services = try! JSONDecoder()
.decode([HomebrewService].self, from: data) .decode([HomebrewService].self, from: data)
.filter({ return filter.contains($0.name) }) .filter({ return filter.contains($0.name) })
completion(services) completion(services)
} }
} }

View File

@@ -9,42 +9,42 @@
import Foundation import Foundation
class PhpEnv { class PhpEnv {
// MARK: - Initializer // MARK: - Initializer
init() { init() {
self.currentInstall = ActivePhpInstallation() 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( self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self, [HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)! from: brewPhpAlias.data(using: .utf8)!
).first! ).first!
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!") Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
} }
// MARK: - Properties // MARK: - Properties
/** The delegate that is informed of updates. */ /** The delegate that is informed of updates. */
weak var delegate: PhpSwitcherDelegate? weak var delegate: PhpSwitcherDelegate?
/** The static app instance. Accessible at any time. */ /** The static app instance. Accessible at any time. */
static let shared = PhpEnv() static let shared = PhpEnv()
/** Whether the switcher is busy performing any actions. */ /** Whether the switcher is busy performing any actions. */
var isBusy: Bool = false var isBusy: Bool = false
/** All available versions of PHP. */ /** All available versions of PHP. */
var availablePhpVersions: [String] = [] var availablePhpVersions: [String] = []
/** Cached information about the PHP installations. */ /** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:] var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */ /** Information about the currently linked PHP installation. */
var currentInstall: ActivePhpInstallation var currentInstall: ActivePhpInstallation
/** /**
The version that the `php` formula via Brew is aliased to on the current system. The version that the `php` formula via Brew is aliased to on the current system.
@@ -57,63 +57,62 @@ class PhpEnv {
static var brewPhpVersion: String { static var brewPhpVersion: String {
return Self.shared.homebrewPackage.version return Self.shared.homebrewPackage.version
} }
/** /**
The currently linked and active PHP installation. The currently linked and active PHP installation.
*/ */
static var phpInstall: ActivePhpInstallation { static var phpInstall: ActivePhpInstallation {
return Self.shared.currentInstall return Self.shared.currentInstall
} }
/** /**
Information we were able to discern from the Homebrew info command. Information we were able to discern from the Homebrew info command.
*/ */
var homebrewPackage: HomebrewPackage! = nil var homebrewPackage: HomebrewPackage! = nil
// MARK: - Methods // MARK: - Methods
public static var switcher: PhpSwitcher { public static var switcher: PhpSwitcher {
return InternalSwitcher() return InternalSwitcher()
} }
public static func detectPhpVersions() -> Void { public static func detectPhpVersions() {
_ = Self.shared.detectPhpVersions() _ = Self.shared.detectPhpVersions()
} }
/** /**
Detects which versions of PHP are installed. Detects which versions of PHP are installed.
*/ */
public func detectPhpVersions() -> [String] public func detectPhpVersions() -> [String] {
{
let files = Shell.pipe("ls \(Paths.optPath) | grep php@") let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n")) var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
// Make sure the aliased version is detected // Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0` // The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed // We should also detect that as a version that is installed
let phpAlias = homebrewPackage.version let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate // 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) versionsOnly.append(phpAlias)
} }
Log.info("The PHP versions that were detected are: \(versionsOnly)") Log.info("The PHP versions that were detected are: \(versionsOnly)")
availablePhpVersions = versionsOnly availablePhpVersions = versionsOnly
var mappedVersions: [String: PhpInstallation] = [:] var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version) mappedVersions[version] = PhpInstallation(version)
} }
cachedPhpInstallations = mappedVersions cachedPhpInstallations = mappedVersions
return versionsOnly return versionsOnly
} }
/** /**
Extracts valid PHP versions from an array of strings. Extracts valid PHP versions from an array of strings.
This array of strings is usually retrieved from `grep`. This array of strings is usually retrieved from `grep`.
@@ -126,14 +125,14 @@ class PhpEnv {
checkBinaries: Bool = true, checkBinaries: Bool = true,
generateHelpers: Bool = true generateHelpers: Bool = true
) -> [String] { ) -> [String] {
var output : [String] = [] var output: [String] = []
var supported = Constants.SupportedPhpVersions var supported = Constants.SupportedPhpVersions
if !Valet.enabled(feature: .supportForPhp56) { if !Valet.enabled(feature: .supportForPhp56) {
supported.removeAll { $0 == "5.6" } supported.removeAll { $0 == "5.6" }
} }
versions.filter { (version) -> Bool in versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@ // Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected) // (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) // is supported and where the binary exists (avoids broken installs)
if !output.contains(version) if !output.contains(version)
&& supported.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) output.append(version)
} }
} }
if generateHelpers { if generateHelpers {
output.forEach { PhpHelper.generate(for: $0) } output.forEach { PhpHelper.generate(for: $0) }
} }
return output return output
} }
public func validVersions(for constraint: String) -> [PhpVersionNumber] { public func validVersions(for constraint: String) -> [PhpVersionNumber] {
constraint.split(separator: "|").flatMap { constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection return PhpVersionNumberCollection
@@ -164,7 +162,7 @@ class PhpEnv {
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines)) .matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
} }
} }
/** /**
Validates whether the currently running version matches the provided version. 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.") Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
return true return true
} }
return false return false
} }
} }

View File

@@ -9,27 +9,28 @@
import Foundation import Foundation
class PhpHelper { class PhpHelper {
static let keyPhrase = "This file was automatically generated by PHP Monitor." static let keyPhrase = "This file was automatically generated by PHP Monitor."
public static func generate(for version: String) { public static func generate(for version: String) {
// Take the PHP version (e.g. "7.2") and generate a dotless version // Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "") let dotless = version.replacingOccurrences(of: ".", with: "")
do { do {
let destination = "/usr/local/bin/pm\(dotless)" let destination = "/usr/local/bin/pm\(dotless)"
if FileManager.default.fileExists(atPath: destination) { if FileManager.default.fileExists(atPath: destination) {
let contents = try String(contentsOfFile: destination) let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) { 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 return
} }
} }
// Let's follow the symlink to the PHP binary folder // Let's follow the symlink to the PHP binary folder
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin") let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path .resolvingSymlinksInPath().path
// The contents of the script! // The contents of the script!
let script = """ let script = """
#!/bin/zsh #!/bin/zsh
@@ -41,14 +42,14 @@ class PhpHelper {
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!"; || echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH export PATH=\(path):$PATH
""" """
// Write to the destination // Write to the destination
try script.write( try script.write(
to: URL(fileURLWithPath: destination), to: URL(fileURLWithPath: destination),
atomically: true, atomically: true,
encoding: String.Encoding.utf8 encoding: String.Encoding.utf8
) )
// Make sure the file is executable // Make sure the file is executable
Shell.run("chmod +x \(destination)") Shell.run("chmod +x \(destination)")
} catch { } catch {
@@ -56,5 +57,5 @@ class PhpHelper {
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)") Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
} }
} }
} }

View File

@@ -10,21 +10,21 @@ import Foundation
public struct PhpVersionNumberCollection: Equatable { public struct PhpVersionNumberCollection: Equatable {
let versions: [PhpVersionNumber] let versions: [PhpVersionNumber]
public static func make(from versions: [String]) -> Self { public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection( return PhpVersionNumberCollection(
versions: versions.map { try! PhpVersionNumber.parse($0) } versions: versions.map { try! PhpVersionNumber.parse($0) }
) )
} }
public var first: PhpVersionNumber? { public var first: PhpVersionNumber? {
return self.versions.first return self.versions.first
} }
public var all: [PhpVersionNumber] { public var all: [PhpVersionNumber] {
return self.versions return self.versions
} }
/** /**
Checks if any versions of PHP are valid for the constraint provided. Checks if any versions of PHP are valid for the constraint provided.
Due to the complexity of evaluating these, a important test is maintained. 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 // Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter { $0.isSameAs(version, strict) } return self.versions.filter { $0.isSameAs(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) { 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 // 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 // ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) } return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) { if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis. // Tilde range means that most specific digit is used as the basis.
return self.versions.filter { return self.versions.filter {
@@ -78,11 +78,11 @@ public struct PhpVersionNumberCollection: Equatable {
: $0.hasSameMajorButNewerOrSameMinor(version, strict) : $0.hasSameMajorButNewerOrSameMinor(version, strict)
} }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) { if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) } return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) { if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) } return self.versions.filter { $0.isNewerThan(version, strict) }
} }
@@ -95,47 +95,52 @@ public struct PhpVersionNumber: Equatable {
let major: Int let major: Int
let minor: Int let minor: Int
let patch: Int? let patch: Int?
public func toString() -> String { public func toString() -> String {
return self.patch == nil return self.patch == nil
? "\(major).\(minor)" ? "\(major).\(minor)"
: "\(major).\(minor).\(patch!)" : "\(major).\(minor).\(patch!)"
} }
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int { public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999) return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
} }
public var homebrewVersion: String { public var homebrewVersion: String {
return "\(major).\(minor)" return "\(major).\(minor)"
} }
public enum MatchType: String { public enum MatchType: String {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"# case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case caretVersionRange = #"^\^(?<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 tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<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"# case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
// TODO: (6.0) Handle these cases (even though I suspect these are uncommon) // TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
/* /*
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"# case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"# case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
*/ */
} }
public static func parse(_ text: String) throws -> Self { public static func parse(_ text: String) throws -> Self {
guard let versionText = VersionExtractor.from(text) else { guard let versionText = VersionExtractor.from(text) else {
throw VersionParseError() throw VersionParseError()
} }
return Self.make(from: versionText)! return Self.make(from: versionText)!
} }
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? { public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: []) 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 { if match != nil {
let major = Int( let major = Int(
versionString[Range(match!.range(withName: "major"), in: versionString)!] versionString[Range(match!.range(withName: "major"), in: versionString)!]
@@ -143,24 +148,24 @@ public struct PhpVersionNumber: Equatable {
let minor = Int( let minor = Int(
versionString[Range(match!.range(withName: "minor"), in: versionString)!] 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) { if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
patch = Int(versionString[minorRange]) patch = Int(versionString[minorRange])
} }
return Self(major: major, minor: minor, patch: patch) return Self(major: major, minor: minor, patch: patch)
} }
return nil return nil
} }
// MARK: Comparison Logic // MARK: Comparison Logic
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major return self.major == version.major
&& self.minor == version.minor && self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true) && (strict ? self.patch(strict, version) == version.patch(strict) : true)
} }
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return ( return (
self.major > version.major || self.major > version.major ||
@@ -169,7 +174,7 @@ public struct PhpVersionNumber: Equatable {
&& self.patch(strict) > version.patch(strict) && self.patch(strict) > version.patch(strict)
) )
} }
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && return self.major == version.major &&
( (
@@ -177,12 +182,12 @@ public struct PhpVersionNumber: Equatable {
|| self.minor > version.minor || self.minor > version.minor
) )
} }
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict) && self.patch(strict, version) >= version.patch(strict)
} }
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major return self.major == version.major
&& self.minor >= version.minor && self.minor >= version.minor

View File

@@ -16,24 +16,26 @@ import Foundation
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/ instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
*/ */
class PhpExtension { class PhpExtension {
/// The file where this extension was located. /// The file where this extension was located.
var file: String var file: String
/// The original string that was used to determine this extension is active. /// The original string that was used to determine this extension is active.
var line: String 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 var name: String
/// Whether the extension has been enabled. /// Whether the extension has been enabled.
var enabled: Bool var enabled: Bool
/// The file where this extension was located, but only the filename, not the full path to the .ini file. /// The file where this extension was located, but only the filename, not the full path to the .ini file.
var fileNameOnly: String { var fileNameOnly: String {
return String(file.split(separator: "/").last ?? "php.ini") 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. 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. - 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)"?)$"# 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. When registering an extension, we do that based on the line found inside the .ini file.
*/ */
init(_ line: String, file: String) { init(_ line: String, file: String) {
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: []) 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)! let range = Range(match!.range(withName: "name"), in: line)!
self.line = line self.line = line
let fullPath = String(line[range]) let fullPath = String(line[range])
.replacingOccurrences(of: "\"", with: "") // replace excess " .replacingOccurrences(of: "\"", with: "") // replace excess "
.replacingOccurrences(of: ".so", with: "") // replace excess .so .replacingOccurrences(of: ".so", with: "") // replace excess .so
self.name = String(fullPath.split(separator: "/").last!) // take last segment self.name = String(fullPath.split(separator: "/").last!) // take last segment
self.enabled = !line.contains(";") self.enabled = !line.contains(";")
self.file = file 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() { func toggle() {
let newLine = enabled let newLine = enabled
@@ -77,25 +81,25 @@ class PhpExtension {
? "; \(line)" ? "; \(line)"
// ENABLED: Line where the comment delimiter (;) is removed // ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "") : line.replacingOccurrences(of: "; ", with: "")
sed(file: file, original: line, replacement: newLine) sed(file: file, original: line, replacement: newLine)
enabled.toggle() enabled.toggle()
} }
// MARK: - Static Methods // MARK: - Static Methods
/** /**
This method will attempt to identify all extensions in the .ini file at a certain URL. This method will attempt to identify all extensions in the .ini file at a certain URL.
*/ */
static func load(from path: URL) -> [PhpExtension] { static func load(from path: URL) -> [PhpExtension] {
let file = try? String(contentsOf: path, encoding: .utf8) 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.") Log.err("There was an issue reading the file. Assuming no extensions were found.")
return [] return []
} }
return file!.components(separatedBy: "\n") return file!.components(separatedBy: "\n")
.filter { .filter {
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
@@ -104,5 +108,5 @@ class PhpExtension {
return PhpExtension($0, file: path.path) return PhpExtension($0, file: path.path)
} }
} }
} }

View File

@@ -9,28 +9,28 @@
import Foundation import Foundation
class PhpInstallation { class PhpInstallation {
var versionNumber: PhpVersionNumber var versionNumber: PhpVersionNumber
/** /**
In order to determine details about a PHP installation, well simply run `php-config --version` In order to determine details about a PHP installation, well simply run `php-config --version`
in the relevant directory. in the relevant directory.
*/ */
init(_ version: String) { init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config" let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
self.versionNumber = PhpVersionNumber.make(from: version)! self.versionNumber = PhpVersionNumber.make(from: version)!
if Filesystem.fileExists(phpConfigExecutablePath) { if Filesystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute( let longVersionString = Command.execute(
path: phpConfigExecutablePath, path: phpConfigExecutablePath,
arguments: ["--version"] arguments: ["--version"]
).trimmingCharacters(in: .whitespacesAndNewlines) ).trimmingCharacters(in: .whitespacesAndNewlines)
// The parser should always work, or the string has to be very unusual. // The parser should always work, or the string has to be very unusual.
// If so, the app SHOULD crash, so that the users report what's up. // If so, the app SHOULD crash, so that the users report what's up.
self.versionNumber = try! PhpVersionNumber.parse(longVersionString) self.versionNumber = try! PhpVersionNumber.parse(longVersionString)
} }
} }
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
class InternalSwitcher: PhpSwitcher { class InternalSwitcher: PhpSwitcher {
/** /**
Switching to a new PHP version involves: Switching to a new PHP version involves:
- unlinking the current version - 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` the version that is switched to may or may not be identical to `php`
(without @version). (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...") Log.info("Switching to \(version), unlinking all versions...")
let isolated = Valet.shared.sites.filter { site in let isolated = Valet.shared.sites.filter { site in
site.isolatedPhpVersion != nil site.isolatedPhpVersion != nil
}.map { site in }.map { site in
return site.isolatedPhpVersion!.versionNumber.homebrewVersion return site.isolatedPhpVersion!.versionNumber.homebrewVersion
} }
var versions: Set<String> = [version] var versions: Set<String> = [version]
if (Valet.enabled(feature: .isolatedSites)) { if Valet.enabled(feature: .isolatedSites) {
versions = versions.union(isolated) versions = versions.union(isolated)
} }
let group = DispatchGroup() let group = DispatchGroup()
PhpEnv.shared.availablePhpVersions.forEach { (available) in PhpEnv.shared.availablePhpVersions.forEach { (available) in
group.enter() group.enter()
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
self.disableDefaultPhpFpmPool(available) self.disableDefaultPhpFpmPool(available)
self.stopPhpVersion(available) self.stopPhpVersion(available)
group.leave() group.leave()
} }
} }
group.notify(queue: .global(qos: .userInitiated)) { group.notify(queue: .global(qos: .userInitiated)) {
Log.info("All versions have been unlinked!") Log.info("All versions have been unlinked!")
Log.info("Linking the new version!") Log.info("Linking the new version!")
for formula in versions { for formula in versions {
self.startPhpVersion(formula, primary: (version == formula)) self.startPhpVersion(formula, primary: (version == formula))
} }
Log.info("Restarting nginx, just to be sure!") Log.info("Restarting nginx, just to be sure!")
brew("services restart nginx", sudo: true) brew("services restart nginx", sudo: true)
Log.info("The new version(s) have been linked!") Log.info("The new version(s) have been linked!")
completion() completion()
} }
} }
private func disableDefaultPhpFpmPool(_ version: String) { private func disableDefaultPhpFpmPool(_ version: String) {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf" let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileManager.default.fileExists(atPath: pool) { 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 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")! let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
do { do {
if (FileManager.default.fileExists(atPath: new.path)) { 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.") 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.removeItem(at: new)
} }
try FileManager.default.moveItem(at: existing, to: new) try FileManager.default.moveItem(at: existing, to: new)
@@ -82,26 +82,26 @@ class InternalSwitcher: PhpSwitcher {
} }
} }
} }
private func stopPhpVersion(_ version: String) { private func stopPhpVersion(_ version: String) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)" let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink \(formula)") brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true) brew("services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)") Log.info("Unlinked and stopped services for \(formula)")
} }
private func startPhpVersion(_ version: String, primary: Bool) { private func startPhpVersion(_ version: String, primary: Bool) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)" let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
if (primary) { if primary {
Log.info("\(formula) is the primary formula, linking and starting services...") Log.info("\(formula) is the primary formula, linking and starting services...")
brew("link \(formula) --overwrite --force") brew("link \(formula) --overwrite --force")
} else { } else {
Log.info("\(formula) is an isolated PHP version, starting services only...") Log.info("\(formula) is an isolated PHP version, starting services only...")
} }
brew("services start \(formula)", sudo: true) brew("services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary { if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "") let socketVersion = version.replacingOccurrences(of: ".", with: "")
Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock") Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
@@ -109,5 +109,5 @@ class InternalSwitcher: PhpSwitcher {
} }
} }
} }

View File

@@ -9,15 +9,15 @@
import Foundation import Foundation
protocol PhpSwitcherDelegate: AnyObject { protocol PhpSwitcherDelegate: AnyObject {
func switcherDidStartSwitching(to version: String) func switcherDidStartSwitching(to version: String)
func switcherDidCompleteSwitch(to version: String) func switcherDidCompleteSwitch(to version: String)
} }
protocol PhpSwitcher { protocol PhpSwitcher {
func performSwitch(to version: String, completion: @escaping () -> Void) func performSwitch(to version: String, completion: @escaping () -> Void)
} }

View File

@@ -10,9 +10,9 @@ import Cocoa
import Foundation import Foundation
extension App { extension App {
// MARK: - Application State // MARK: - Application State
/** /**
Registers a window as currently open. Registers a window as currently open.
*/ */
@@ -22,7 +22,7 @@ extension App {
} }
updateActivationPolicy() updateActivationPolicy()
} }
/** /**
Removes a window, assuming it was closed. Removes a window, assuming it was closed.
*/ */
@@ -32,13 +32,13 @@ extension App {
} }
updateActivationPolicy() updateActivationPolicy()
} }
/** /**
If there are any open windows, the app will be a regular app. 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. If there are no windows open, the app will be an accessory (toolbar) app.
*/ */
public func updateActivationPolicy() { public func updateActivationPolicy() {
NSApp.setActivationPolicy(openWindows.count > 0 ? .regular : .accessory) NSApp.setActivationPolicy(!openWindows.isEmpty ? .regular : .accessory)
} }
} }

View File

@@ -9,9 +9,9 @@
import Cocoa import Cocoa
extension App { extension App {
// MARK: - Methods // MARK: - Methods
/** /**
On startup, the preferences should be loaded from the .plist, On startup, the preferences should be loaded from the .plist,
and we'll enable the shortcut if it is set. 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.") Log.info("No global hotkey was saved in preferences. None set.")
return return
} }
// Make sure we can parse the JSON into the desired format // Make sure we can parse the JSON into the desired format
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else { guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
Log.err("No global hotkey loaded, could not be parsed!") Log.err("No global hotkey loaded, could not be parsed!")
shortcutHotkey = nil shortcutHotkey = nil
return return
} }
shortcutHotkey = HotKey(keyCombo: KeyCombo( shortcutHotkey = HotKey(keyCombo: KeyCombo(
carbonKeyCode: keybindPref.keyCode, carbonKeyCode: keybindPref.keyCode,
carbonModifiers: keybindPref.carbonFlags carbonModifiers: keybindPref.carbonFlags
)) ))
} }
/** /**
Sets up the action that needs to occur when the shortcut key is pressed Sets up the action that needs to occur when the shortcut key is pressed
(opens the menu). (opens the menu).
@@ -44,11 +44,11 @@ extension App {
guard let hotkey = shortcutHotkey else { guard let hotkey = shortcutHotkey else {
return return
} }
hotkey.keyDownHandler = { hotkey.keyDownHandler = {
MainMenu.shared.statusItem.button?.performClick(nil) MainMenu.shared.statusItem.button?.performClick(nil)
NSApplication.shared.activate(ignoringOtherApps: true) NSApplication.shared.activate(ignoringOtherApps: true)
} }
} }
} }

View File

@@ -8,19 +8,19 @@
import Cocoa import Cocoa
class App { class App {
// MARK: Static Vars // MARK: Static Vars
/** The static app instance. Accessible at any time. */ /** The static app instance. Accessible at any time. */
static let shared = App() static let shared = App()
/** Retrieve the version number from the main info dictionary, Info.plist. */ /** Retrieve the version number from the main info dictionary, Info.plist. */
static var version: String { static var version: String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
return "\(version) (\(build))" return "\(version) (\(build))"
} }
static var architecture: String { static var architecture: String {
var systeminfo = utsname() var systeminfo = utsname()
uname(&systeminfo) uname(&systeminfo)
@@ -34,37 +34,37 @@ class App {
} }
return machine return machine
} }
// MARK: Variables // MARK: Variables
/** The list of preferences that are currently active. */ /** The list of preferences that are currently active. */
var preferences: [PreferenceName: Bool]! var preferences: [PreferenceName: Bool]!
/** The window controller of the currently active preferences window. */ /** 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. */ /** 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. */ /** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = [] var detectedApplications: [Application] = []
/** Timer that will periodically reload info about the user's PHP installation. */ /** Timer that will periodically reload info about the user's PHP installation. */
var timer: Timer? var timer: Timer?
// MARK: - Global Hotkey // MARK: - Global Hotkey
/** /**
The shortcut the user has requested. The shortcut the user has requested.
*/ */
var shortcutHotkey: HotKey? = nil { var shortcutHotkey: HotKey? {
didSet { didSet {
setupGlobalHotkeyListener() setupGlobalHotkeyListener()
} }
} }
// MARK: - Activation Policy // MARK: - Activation Policy
/** /**
Variable that keeps track of which windows are currently open. Variable that keeps track of which windows are currently open.
(Please note that window controllers remain open in memory once opened.) (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). (as a normal app or as a toolbar app).
*/ */
var openWindows: [String] = [] var openWindows: [String] = []
// MARK: - App Watchers // MARK: - App Watchers
/** /**
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder. The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
*/ */

View File

@@ -10,7 +10,7 @@ import Cocoa
import Foundation import Foundation
extension AppDelegate { extension AppDelegate {
/** /**
This is an entry point for future development for integrating with the PHP Monitor 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. 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. Please note that PHP Monitor needs to be running in the background for this to work.
*/ */
func application(_ application: NSApplication, open urls: [URL]) { func application(_ application: NSApplication, open urls: [URL]) {
if !Preferences.isEnabled(.allowProtocolForIntegrations) { if !Preferences.isEnabled(.allowProtocolForIntegrations) {
Log.info("Acting on commands via phpmon:// has been disabled.") Log.info("Acting on commands via phpmon:// has been disabled.")
return return
} }
guard let url = urls.first else { return } guard let url = urls.first else { return }
self.interpretCommand( self.interpretCommand(
url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""), url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""),
commands: InterApp.getCommands() commands: InterApp.getCommands()
) )
} }
private func interpretCommand(_ command: String, commands: [InterApp.Action]) { private func interpretCommand(_ command: String, commands: [InterApp.Action]) {
commands.forEach { action in commands.forEach { action in
if command.starts(with: action.command) { if command.starts(with: action.command) {
@@ -44,4 +44,3 @@ extension AppDelegate {
} }
} }
} }

View File

@@ -22,20 +22,20 @@ import AppKit
For more information about this, please see the ActivationPolicy-related extension. For more information about this, please see the ActivationPolicy-related extension.
*/ */
extension AppDelegate { extension AppDelegate {
// MARK: - Menu Interactions // MARK: - Menu Interactions
@IBAction func addSiteLinkPressed(_ sender: Any) { @IBAction func addSiteLinkPressed(_ sender: Any) {
DomainListVC.show() DomainListVC.show()
guard let windowController = App.shared.domainListWindowController else { return } guard let windowController = App.shared.domainListWindowController else { return }
windowController.pressedAddLink(nil) windowController.pressedAddLink(nil)
} }
@IBAction func reloadDomainListPressed(_ sender: Any) { @IBAction func reloadDomainListPressed(_ sender: Any) {
let vc = App.shared.domainListWindowController? let vc = App.shared.domainListWindowController?
.window?.contentViewController as? DomainListVC .window?.contentViewController as? DomainListVC
if vc != nil { if vc != nil {
// If the view exists, directly reload the list of sites // If the view exists, directly reload the list of sites
vc!.reloadDomains() vc!.reloadDomains()
@@ -44,12 +44,12 @@ extension AppDelegate {
Valet.shared.reloadSites() Valet.shared.reloadSites()
} }
} }
@IBAction func focusSearchField(_ sender: Any) { @IBAction func focusSearchField(_ sender: Any) {
DomainListVC.show() DomainListVC.show()
guard let windowController = App.shared.domainListWindowController else { return } guard let windowController = App.shared.domainListWindowController else { return }
windowController.searchToolbarItem.searchField.becomeFirstResponder() windowController.searchToolbarItem.searchField.becomeFirstResponder()
} }
} }

View File

@@ -10,9 +10,9 @@ import Foundation
import UserNotifications import UserNotifications
extension AppDelegate { extension AppDelegate {
// MARK: - Notifications // MARK: - Notifications
/** /**
Sets up notifications. That does mean we need to ask for permission first. Sets up notifications. That does mean we need to ask for permission first.
If we cannot get permission, we should log this. 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. Ensure that the application displays notifications even when the app is active.
*/ */
@@ -42,5 +42,5 @@ extension AppDelegate {
) { ) {
completionHandler([.banner]) completionHandler([.banner])
} }
} }

View File

@@ -10,55 +10,55 @@ import UserNotifications
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
// MARK: - Variables // MARK: - Variables
/** /**
The Shell singleton that keeps track of the history of all The Shell singleton that keeps track of the history of all
(invoked by PHP Monitor) shell commands. It is used to (invoked by PHP Monitor) shell commands. It is used to
invoke all commands in this application. invoke all commands in this application.
*/ */
let sharedShell: Shell let sharedShell: Shell
/** /**
The App singleton contains information about the state of The App singleton contains information about the state of
the application and global variables. the application and global variables.
*/ */
let state: App let state: App
/** /**
The MainMenu singleton is responsible for rendering the The MainMenu singleton is responsible for rendering the
menu bar item and its menu, as well as its actions. menu bar item and its menu, as well as its actions.
*/ */
let menu: MainMenu let menu: MainMenu
/** /**
The paths singleton that determines where Homebrew is installed, The paths singleton that determines where Homebrew is installed,
and where to look for binaries. and where to look for binaries.
*/ */
let paths: Paths let paths: Paths
/** /**
The Valet singleton that determines all information The Valet singleton that determines all information
about Valet and its current configuration. about Valet and its current configuration.
*/ */
let valet: Valet let valet: Valet
/** /**
The PhpEnv singleton that handles PHP version The PhpEnv singleton that handles PHP version
detection, as well as switching. It is initialized detection, as well as switching. It is initialized
when the app is ready and passed all checks. when the app is ready and passed all checks.
*/ */
var phpEnvironment: PhpEnv! = nil var phpEnvironment: PhpEnv! = nil
/** /**
The logger is responsible for different levels of logging. The logger is responsible for different levels of logging.
You can tweak the verbosity in the `init` method here. You can tweak the verbosity in the `init` method here.
*/ */
var logger = Log.shared var logger = Log.shared
// MARK: - Initializer // MARK: - Initializer
/** /**
When the application initializes, create all singletons. When the application initializes, create all singletons.
*/ */
@@ -78,13 +78,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
self.valet = Valet.shared self.valet = Valet.shared
super.init() super.init()
} }
func initializeSwitcher() { func initializeSwitcher() {
self.phpEnvironment = PhpEnv.shared self.phpEnvironment = PhpEnv.shared
} }
// MARK: - Lifecycle // MARK: - Lifecycle
/** /**
When the application has finished launching, we'll want to set up When the application has finished launching, we'll want to set up
the user notification center permissions, and kickoff the menu 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 // Make sure the menu performs its initial checks
Task { await menu.startup() } Task { await menu.startup() }
} }
} }

View File

@@ -9,18 +9,18 @@
import Foundation import Foundation
class InterApp { class InterApp {
public static var bindings: [Action] = [] public static var bindings: [Action] = []
public static func register(_ action: Action) { public static func register(_ action: Action) {
self.bindings.append(action) self.bindings.append(action)
} }
public struct Action { public struct Action {
let command: String let command: String
let action: (String) -> Void let action: (String) -> Void
} }
static func getCommands() -> [InterApp.Action] { return [ static func getCommands() -> [InterApp.Action] { return [
InterApp.Action(command: "list", action: { _ in InterApp.Action(command: "list", action: { _ in
DomainListVC.show() 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." subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
).withPrimary(text: "OK").show() ).withPrimary(text: "OK").show()
} }
}), })
]} ]}
} }

View File

@@ -9,7 +9,7 @@ import Foundation
import AppKit import AppKit
class Startup { class Startup {
/** /**
Checks the user's environment and checks if PHP Monitor can be used properly. Checks the user's environment and checks if PHP Monitor can be used properly.
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more. This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
@@ -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 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. 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 // Do the important system setup checks
Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)") Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)")
for check in self.checks { for check in self.checks {
if await check.succeeds() { if await check.succeeds() {
Log.info("[OK] \(check.name)") Log.info("[OK] \(check.name)")
continue continue
} }
// If we get here, something's gone wrong and the check has failed... // If we get here, something's gone wrong and the check has failed...
Log.info("[FAIL] \(check.name)") Log.info("[FAIL] \(check.name)")
showAlert(for: check) showAlert(for: check)
return false return false
} }
// If we get here, nothing has gone wrong. That's what we want! // If we get here, nothing has gone wrong. That's what we want!
initializeSwitcher() initializeSwitcher()
Log.separator(as: .info) Log.separator(as: .info)
Log.info("PHP Monitor has determined the application has successfully passed all checks.") Log.info("PHP Monitor has determined the application has successfully passed all checks.")
return true return true
} }
/** /**
Displays an alert for a particular check. There are two types of alerts: Displays an alert for a particular check. There are two types of alerts:
- ones that require an app restart, which prompt the user to exit the app - ones that require an app restart, which prompt the user to exit the app
@@ -59,7 +58,7 @@ class Startup {
exit(1) exit(1)
}).show() }).show()
} }
BetterAlert() BetterAlert()
.withInformation( .withInformation(
title: check.titleText, title: check.titleText,
@@ -70,7 +69,7 @@ class Startup {
.show() .show()
} }
} }
/** /**
Because the Switcher requires various environment guarantees, the switcher is only 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. initialized when it is done working. The switcher must be initialized on the main thread.
@@ -81,9 +80,9 @@ class Startup {
appDelegate.initializeSwitcher() appDelegate.initializeSwitcher()
} }
} }
// MARK: - Check (List) // MARK: - Check (List)
public var checks: [EnvironmentCheck] = [ public var checks: [EnvironmentCheck] = [
// ================================================================================= // =================================================================================
// The Homebrew binary must exist. // The Homebrew binary must exist.
@@ -196,9 +195,9 @@ class Startup {
descriptionText: "startup.errors.valet_version_unknown.desc".localized descriptionText: "startup.errors.valet_version_unknown.desc".localized
) )
] ]
// MARK: - EnvironmentCheck struct // MARK: - EnvironmentCheck struct
/** /**
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary. 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. 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 descriptionText: String
let buttonText: String let buttonText: String
let requiresAppRestart: Bool let requiresAppRestart: Bool
init( init(
command: @escaping () async -> Bool, command: @escaping () async -> Bool,
name: String, name: String,
@@ -229,7 +228,7 @@ class Startup {
self.buttonText = buttonText self.buttonText = buttonText
self.requiresAppRestart = requiresAppRestart self.requiresAppRestart = requiresAppRestart
} }
public func succeeds() async -> Bool { public func succeeds() async -> Bool {
return await !self.command() return await !self.command()
} }

View File

@@ -10,43 +10,43 @@ import Foundation
import Cocoa import Cocoa
class AddProxyVC: NSViewController, NSTextFieldDelegate { class AddProxyVC: NSViewController, NSTextFieldDelegate {
// MARK: - Outlets // MARK: - Outlets
@IBOutlet weak var textFieldTitle: NSTextField! @IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var textFieldProxySubject: NSTextField! @IBOutlet weak var textFieldProxySubject: NSTextField!
@IBOutlet weak var textFieldDomainName: NSTextField! @IBOutlet weak var textFieldDomainName: NSTextField!
@IBOutlet weak var inputProxySubject: NSTextField! @IBOutlet weak var inputProxySubject: NSTextField!
@IBOutlet weak var inputDomainName: NSTextField! @IBOutlet weak var inputDomainName: NSTextField!
@IBOutlet weak var previewText: NSTextField! @IBOutlet weak var previewText: NSTextField!
@IBOutlet weak var buttonSecure: NSButton! @IBOutlet weak var buttonSecure: NSButton!
@IBOutlet weak var buttonCreateProxy: NSButton! @IBOutlet weak var buttonCreateProxy: NSButton!
@IBOutlet weak var buttonCancel: NSButton! @IBOutlet weak var buttonCancel: NSButton!
@IBOutlet weak var textFieldSecure: NSTextField! @IBOutlet weak var textFieldSecure: NSTextField!
@IBOutlet weak var textFieldError: NSTextField! @IBOutlet weak var textFieldError: NSTextField!
// MARK: - View Lifecycle // MARK: - View Lifecycle
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
loadStaticLocalisedStrings() loadStaticLocalisedStrings()
buttonCreateProxy.isEnabled = false buttonCreateProxy.isEnabled = false
updatePreview() updatePreview()
validate() validate()
} }
private func dismissView(outcome: NSApplication.ModalResponse) { private func dismissView(outcome: NSApplication.ModalResponse) {
guard let window = view.window, let parent = window.sheetParent else { return } guard let window = view.window, let parent = window.sheetParent else { return }
parent.endSheet(window, returnCode: outcome) parent.endSheet(window, returnCode: outcome)
} }
// MARK: - Localisation // MARK: - Localisation
func loadStaticLocalisedStrings() { func loadStaticLocalisedStrings() {
textFieldTitle.stringValue = "domain_list.add.set_up_proxy".localized textFieldTitle.stringValue = "domain_list.add.set_up_proxy".localized
textFieldProxySubject.stringValue = "domain_list.add.proxy_subject".localized textFieldProxySubject.stringValue = "domain_list.add.proxy_subject".localized
@@ -55,101 +55,101 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
buttonCancel.title = "domain_list.add.cancel".localized buttonCancel.title = "domain_list.add.cancel".localized
buttonCreateProxy.title = "domain_list.add.create_proxy".localized buttonCreateProxy.title = "domain_list.add.create_proxy".localized
} }
// MARK: - Outlet Interactions // MARK: - Outlet Interactions
@IBAction func pressedSecure(_ sender: Any) { @IBAction func pressedSecure(_ sender: Any) {
updatePreview() updatePreview()
} }
@IBAction func pressedCreateProxy(_ sender: Any) { @IBAction func pressedCreateProxy(_ sender: Any) {
// TODO: Validate the input before allowing proxy creation // TODO: Validate the input before allowing proxy creation
let domain = self.inputDomainName.stringValue let domain = self.inputDomainName.stringValue
let proxyName = self.inputProxySubject.stringValue let proxyName = self.inputProxySubject.stringValue
let secure = self.buttonSecure.state == .on ? " --secure" : "" let secure = self.buttonSecure.state == .on ? " --secure" : ""
dismissView(outcome: .OK) dismissView(outcome: .OK)
App.shared.domainListWindowController?.contentVC.setUIBusy() App.shared.domainListWindowController?.contentVC.setUIBusy()
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
Shell.run("\(Paths.valet) proxy \(domain) \(proxyName)\(secure)", requiresPath: true) Shell.run("\(Paths.valet) proxy \(domain) \(proxyName)\(secure)", requiresPath: true)
Actions.restartNginx() Actions.restartNginx()
DispatchQueue.main.async { DispatchQueue.main.async {
App.shared.domainListWindowController?.contentVC.setUINotBusy() App.shared.domainListWindowController?.contentVC.setUINotBusy()
App.shared.domainListWindowController?.pressedReload(nil) App.shared.domainListWindowController?.pressedReload(nil)
} }
} }
} }
@IBAction func pressedCancel(_ sender: Any) { @IBAction func pressedCancel(_ sender: Any) {
dismissView(outcome: .cancel) dismissView(outcome: .cancel)
} }
// MARK: - Text Field Delegate // MARK: - Text Field Delegate
func controlTextDidChange(_ obj: Notification) { func controlTextDidChange(_ obj: Notification) {
updateTextField() updateTextField()
} }
// MARK: - Helper Methods // MARK: - Helper Methods
private func validate() { private func validate() {
_ = validate( _ = validate(
domain: inputDomainName.stringValue, domain: inputDomainName.stringValue,
proxy: inputProxySubject.stringValue proxy: inputProxySubject.stringValue
) )
} }
private func validate(domain: String, proxy: String) -> Bool { private func validate(domain: String, proxy: String) -> Bool {
if domain.isEmpty { if domain.isEmpty {
textFieldError.isHidden = false textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.empty".localized textFieldError.stringValue = "domain_list.add.errors.empty".localized
return false return false
} }
if proxy.isEmpty { if proxy.isEmpty {
textFieldError.isHidden = false textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.empty_proxy".localized textFieldError.stringValue = "domain_list.add.errors.empty_proxy".localized
return false return false
} }
if Valet.shared.sites.contains(where: { $0.name == domain }) { if Valet.shared.sites.contains(where: { $0.name == domain }) {
textFieldError.isHidden = false textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
return false return false
} }
textFieldError.isHidden = true textFieldError.isHidden = true
return true return true
} }
func updateTextField() { func updateTextField() {
inputDomainName.stringValue = inputDomainName.stringValue inputDomainName.stringValue = inputDomainName.stringValue
.replacingOccurrences(of: " ", with: "-") .replacingOccurrences(of: " ", with: "-")
buttonCreateProxy.isEnabled = validate( buttonCreateProxy.isEnabled = validate(
domain: inputDomainName.stringValue, domain: inputDomainName.stringValue,
proxy: inputProxySubject.stringValue proxy: inputProxySubject.stringValue
) )
updatePreview() updatePreview()
} }
func updatePreview() { func updatePreview() {
buttonSecure.title = "domain_list.add.secure_after_creation" buttonSecure.title = "domain_list.add.secure_after_creation"
.localized( .localized(
inputDomainName.stringValue, inputDomainName.stringValue,
Valet.shared.config.tld 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 previewText.stringValue = "domain_list.add.empty_fields".localized
return return
} }
previewText.stringValue = "domain_list.add.proxy_available" previewText.stringValue = "domain_list.add.proxy_available"
.localized( .localized(
inputProxySubject.stringValue, inputProxySubject.stringValue,
@@ -158,5 +158,5 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
Valet.shared.config.tld Valet.shared.config.tld
) )
} }
} }

View File

@@ -10,37 +10,37 @@ import Foundation
import Cocoa import Cocoa
class AddSiteVC: NSViewController, NSTextFieldDelegate { class AddSiteVC: NSViewController, NSTextFieldDelegate {
// MARK: - Outlets // MARK: - Outlets
@IBOutlet weak var textFieldTitle: NSTextField! @IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var pathControl: NSPathControl! @IBOutlet weak var pathControl: NSPathControl!
@IBOutlet weak var inputDomainName: NSTextField! @IBOutlet weak var inputDomainName: NSTextField!
@IBOutlet weak var previewText: NSTextField! @IBOutlet weak var previewText: NSTextField!
@IBOutlet weak var buttonSecure: NSButton! @IBOutlet weak var buttonSecure: NSButton!
@IBOutlet weak var buttonCreateLink: NSButton! @IBOutlet weak var buttonCreateLink: NSButton!
@IBOutlet weak var buttonCancel: NSButton! @IBOutlet weak var buttonCancel: NSButton!
@IBOutlet weak var textFieldSecure: NSTextField! @IBOutlet weak var textFieldSecure: NSTextField!
@IBOutlet weak var textFieldError: NSTextField! @IBOutlet weak var textFieldError: NSTextField!
// MARK: - View Lifecycle // MARK: - View Lifecycle
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
loadStaticLocalisedStrings() loadStaticLocalisedStrings()
} }
private func dismissView(outcome: NSApplication.ModalResponse) { private func dismissView(outcome: NSApplication.ModalResponse) {
guard let window = self.view.window, let parent = window.sheetParent else { return } guard let window = self.view.window, let parent = window.sheetParent else { return }
parent.endSheet(window, returnCode: outcome) parent.endSheet(window, returnCode: outcome)
} }
// MARK: - Localisation // MARK: - Localisation
func loadStaticLocalisedStrings() { func loadStaticLocalisedStrings() {
textFieldTitle.stringValue = "domain_list.add.link_folder".localized textFieldTitle.stringValue = "domain_list.add.link_folder".localized
inputDomainName.placeholderString = "domain_list.add.domain_name_placeholder".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 buttonCancel.title = "domain_list.add.cancel".localized
buttonCreateLink.title = "domain_list.add.create_link".localized buttonCreateLink.title = "domain_list.add.create_link".localized
} }
// MARK: - Outlet Interactions // MARK: - Outlet Interactions
@IBAction func pressedCreateLink(_ sender: Any) { @IBAction func pressedCreateLink(_ sender: Any) {
let path = pathControl.url!.path let path = pathControl.url!.path
let name = inputDomainName.stringValue let name = inputDomainName.stringValue
if !FileManager.default.fileExists(atPath: path) { if !FileManager.default.fileExists(atPath: path) {
Alert.confirm( Alert.confirm(
onWindow: view.window!, onWindow: view.window!,
@@ -68,18 +68,18 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
) )
return return
} }
// Adding `valet links` is a workaround for Valet malforming the config.json file // 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 // TODO: I will have to investigate and report this behaviour if possible
Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links", requiresPath: true) Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links", requiresPath: true)
dismissView(outcome: .OK) dismissView(outcome: .OK)
// Reset search // Reset search
App.shared.domainListWindowController? App.shared.domainListWindowController?
.searchToolbarItem .searchToolbarItem
.searchField.stringValue = "" .searchField.stringValue = ""
// Add the new item and scrolls to it // Add the new item and scrolls to it
App.shared.domainListWindowController? App.shared.domainListWindowController?
.contentVC .contentVC
@@ -88,60 +88,60 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
secure: buttonSecure.state == .on secure: buttonSecure.state == .on
) )
} }
@IBAction func pressedCancel(_ sender: Any) { @IBAction func pressedCancel(_ sender: Any) {
dismissView(outcome: .cancel) dismissView(outcome: .cancel)
} }
@IBAction func pressedSecure(_ sender: Any) { @IBAction func pressedSecure(_ sender: Any) {
updatePreview() updatePreview()
} }
// MARK: - Text Field Delegate // MARK: - Text Field Delegate
func controlTextDidChange(_ obj: Notification) { func controlTextDidChange(_ obj: Notification) {
updateTextField() updateTextField()
} }
// MARK: - Helper Methods // MARK: - Helper Methods
private func isValidLinkName(_ name: String) -> Bool { private func isValidLinkName(_ name: String) -> Bool {
if name.isEmpty { if name.isEmpty {
textFieldError.isHidden = false textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.empty".localized textFieldError.stringValue = "domain_list.add.errors.empty".localized
return false return false
} }
if Valet.shared.sites.contains(where: { $0.name == name }) { if Valet.shared.sites.contains(where: { $0.name == name }) {
textFieldError.isHidden = false textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
return false return false
} }
textFieldError.isHidden = true textFieldError.isHidden = true
return true return true
} }
func updateTextField() { func updateTextField() {
inputDomainName.stringValue = inputDomainName.stringValue inputDomainName.stringValue = inputDomainName.stringValue
.replacingOccurrences(of: " ", with: "-") .replacingOccurrences(of: " ", with: "-")
buttonCreateLink.isEnabled = isValidLinkName(inputDomainName.stringValue) buttonCreateLink.isEnabled = isValidLinkName(inputDomainName.stringValue)
updatePreview() updatePreview()
} }
func updatePreview() { func updatePreview() {
buttonSecure.title = "domain_list.add.secure_after_creation" buttonSecure.title = "domain_list.add.secure_after_creation"
.localized( .localized(
inputDomainName.stringValue, inputDomainName.stringValue,
Valet.shared.config.tld Valet.shared.config.tld
) )
if (inputDomainName.stringValue.isEmpty) { if inputDomainName.stringValue.isEmpty {
previewText.stringValue = "domain_list.add.empty_fields".localized previewText.stringValue = "domain_list.add.empty_fields".localized
return return
} }
previewText.stringValue = "domain_list.add.folder_available" previewText.stringValue = "domain_list.add.folder_available"
.localized( .localized(
buttonSecure.state == .on ? "https" : "http", buttonSecure.state == .on ? "https" : "http",

View File

@@ -9,12 +9,11 @@
import Cocoa import Cocoa
import AppKit import AppKit
class DomainListKindCell: NSTableCellView, DomainListCellProtocol class DomainListKindCell: NSTableCellView, DomainListCellProtocol {
{
static let reusableName = "domainListKindCell" static let reusableName = "domainListKindCell"
@IBOutlet weak var imageViewType: NSImageView! @IBOutlet weak var imageViewType: NSImageView!
func populateCell(with site: ValetSite) { func populateCell(with site: ValetSite) {
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked). // If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
imageViewType.image = NSImage( imageViewType.image = NSImage(
@@ -22,15 +21,15 @@ class DomainListKindCell: NSTableCellView, DomainListCellProtocol
? "IconParked" ? "IconParked"
: "IconLinked" : "IconLinked"
) )
// Unless, of course, this is a default site // Unless, of course, this is a default site
if site.absolutePath == Valet.shared.config.defaultSite { if site.absolutePath == Valet.shared.config.defaultSite {
imageViewType.image = NSImage(named: "IconDefault") imageViewType.image = NSImage(named: "IconDefault")
} }
imageViewType.contentTintColor = NSColor.tertiaryLabelColor imageViewType.contentTintColor = NSColor.tertiaryLabelColor
} }
func populateCell(with proxy: ValetProxy) { func populateCell(with proxy: ValetProxy) {
imageViewType.image = NSImage(named: "IconProxy") imageViewType.image = NSImage(named: "IconProxy")
} }

View File

@@ -9,18 +9,17 @@
import Cocoa import Cocoa
import AppKit import AppKit
class DomainListNameCell: NSTableCellView, DomainListCellProtocol class DomainListNameCell: NSTableCellView, DomainListCellProtocol {
{
static let reusableName = "domainListNameCell" static let reusableName = "domainListNameCell"
@IBOutlet weak var labelSiteName: NSTextField! @IBOutlet weak var labelSiteName: NSTextField!
@IBOutlet weak var labelPathName: NSTextField! @IBOutlet weak var labelPathName: NSTextField!
func populateCell(with site: ValetSite) { func populateCell(with site: ValetSite) {
labelSiteName.stringValue = "\(site.name).\(site.tld)" labelSiteName.stringValue = "\(site.name).\(site.tld)"
labelPathName.stringValue = site.absolutePathRelative labelPathName.stringValue = site.absolutePathRelative
} }
func populateCell(with proxy: ValetProxy) { func populateCell(with proxy: ValetProxy) {
labelSiteName.stringValue = "\(proxy.domain).\(proxy.tld)" labelSiteName.stringValue = "\(proxy.domain).\(proxy.tld)"
labelPathName.stringValue = proxy.target labelPathName.stringValue = proxy.target

View File

@@ -9,22 +9,21 @@
import Cocoa import Cocoa
import AppKit import AppKit
class DomainListPhpCell: NSTableCellView, DomainListCellProtocol class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
{
static let reusableName = "domainListPhpCell" static let reusableName = "domainListPhpCell"
var site: ValetSite? = nil var site: ValetSite?
@IBOutlet weak var buttonPhpVersion: NSButton! @IBOutlet weak var buttonPhpVersion: NSButton!
@IBOutlet weak var imageViewPhpVersionOK: NSImageView! @IBOutlet weak var imageViewPhpVersionOK: NSImageView!
func populateCell(with site: ValetSite) { func populateCell(with site: ValetSite) {
self.site = site self.site = site
buttonPhpVersion.title = " PHP \(site.servingPhpVersion)" buttonPhpVersion.title = " PHP \(site.servingPhpVersion)"
imageViewPhpVersionOK.toolTip = nil imageViewPhpVersionOK.toolTip = nil
if site.isolatedPhpVersion != nil { if site.isolatedPhpVersion != nil {
imageViewPhpVersionOK.isHidden = false imageViewPhpVersionOK.isHidden = false
imageViewPhpVersionOK.image = NSImage(named: "Isolated") imageViewPhpVersionOK.image = NSImage(named: "Isolated")
@@ -34,45 +33,45 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
imageViewPhpVersionOK.image = NSImage(named: "Checkmark") imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp) imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp)
} }
buttonPhpVersion.isHidden = false buttonPhpVersion.isHidden = false
imageViewPhpVersionOK.isHidden = false imageViewPhpVersionOK.isHidden = false
} }
func populateCell(with proxy: ValetProxy) { func populateCell(with proxy: ValetProxy) {
buttonPhpVersion.isHidden = true buttonPhpVersion.isHidden = true
imageViewPhpVersionOK.isHidden = true imageViewPhpVersionOK.isHidden = true
return return
} }
@IBAction func pressedPhpVersion(_ sender: Any) { @IBAction func pressedPhpVersion(_ sender: Any) {
guard let site = self.site else { return } guard let site = self.site else { return }
let alert = NSAlert.init() let alert = NSAlert.init()
alert.alertStyle = .informational alert.alertStyle = .informational
var information = "" var information = ""
if (self.site?.isolatedPhpVersion != nil) { if self.site?.isolatedPhpVersion != nil {
information += "alert.composer_php_isolated.desc".localized( information += "alert.composer_php_isolated.desc".localized(
self.site!.isolatedPhpVersion!.versionNumber.homebrewVersion, self.site!.isolatedPhpVersion!.versionNumber.homebrewVersion,
PhpEnv.phpInstall.version.short PhpEnv.phpInstall.version.short
) )
information += "\n\n" information += "\n\n"
} }
information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)" information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)"
.localized .localized
alert.messageText = "alert.composer_php_requirement.title" alert.messageText = "alert.composer_php_requirement.title"
.localized("\(site.name).\(Valet.shared.config.tld)", site.composerPhp) .localized("\(site.name).\(Valet.shared.config.tld)", site.composerPhp)
alert.informativeText = information alert.informativeText = information
alert.addButton(withTitle: "site_link.close".localized) alert.addButton(withTitle: "site_link.close".localized)
var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue
var map: [Int: String] = [:] var map: [Int: String] = [:]
if site.isolatedPhpVersion == nil { if site.isolatedPhpVersion == nil {
// Determine which installed versions would be ideal to switch to, // Determine which installed versions would be ideal to switch to,
// but make sure to exclude the currently linked version // but make sure to exclude the currently linked version
@@ -83,7 +82,7 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
map[mapIndex] = version.homebrewVersion map[mapIndex] = version.homebrewVersion
mapIndex += 1 mapIndex += 1
} }
// Site is not isolated, show options to switch global PHP version // Site is not isolated, show options to switch global PHP version
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) { response in alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) { response in
if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue { if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue {
@@ -99,5 +98,5 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) alert.beginSheetModal(for: App.shared.domainListWindowController!.window!)
} }
} }
} }

View File

@@ -9,18 +9,17 @@
import Cocoa import Cocoa
import AppKit import AppKit
class DomainListTLSCell: NSTableCellView, DomainListCellProtocol class DomainListTLSCell: NSTableCellView, DomainListCellProtocol {
{
static let reusableName = "domainListTLSCell" static let reusableName = "domainListTLSCell"
@IBOutlet weak var imageViewLock: NSImageView! @IBOutlet weak var imageViewLock: NSImageView!
func populateCell(with site: ValetSite) { func populateCell(with site: ValetSite) {
imageViewLock.contentTintColor = site.secured imageViewLock.contentTintColor = site.secured
? NSColor(named: "IconColorGreen") ? NSColor(named: "IconColorGreen")
: NSColor(named: "IconColorRed") : NSColor(named: "IconColorRed")
} }
func populateCell(with proxy: ValetProxy) { func populateCell(with proxy: ValetProxy) {
imageViewLock.contentTintColor = proxy.secured imageViewLock.contentTintColor = proxy.secured
? NSColor(named: "IconColorGreen") ? NSColor(named: "IconColorGreen")

View File

@@ -9,26 +9,25 @@
import Cocoa import Cocoa
import AppKit import AppKit
class DomainListTypeCell: NSTableCellView, DomainListCellProtocol class DomainListTypeCell: NSTableCellView, DomainListCellProtocol {
{
static let reusableName = "domainListTypeCell" static let reusableName = "domainListTypeCell"
@IBOutlet weak var labelDriver: NSTextField! @IBOutlet weak var labelDriver: NSTextField!
@IBOutlet weak var labelPhpVersion: NSTextField! @IBOutlet weak var labelPhpVersion: NSTextField!
func populateCell(with site: ValetSite) { func populateCell(with site: ValetSite) {
labelDriver.stringValue = site.driver ?? "driver.not_detected".localized labelDriver.stringValue = site.driver ?? "driver.not_detected".localized
// Determine the Laravel version // Determine the Laravel version
if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") { if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") {
let constraint = site.notableComposerDependencies["laravel/framework"]! let constraint = site.notableComposerDependencies["laravel/framework"]!
labelDriver.stringValue = "Laravel (\(constraint))" labelDriver.stringValue = "Laravel (\(constraint))"
} }
// PHP version // PHP version
labelPhpVersion.stringValue = site.composerPhp == "???" ? "Any PHP" : "PHP \(site.composerPhp)" labelPhpVersion.stringValue = site.composerPhp == "???" ? "Any PHP" : "PHP \(site.composerPhp)"
} }
func populateCell(with proxy: ValetProxy) { func populateCell(with proxy: ValetProxy) {
labelDriver.stringValue = "Proxy" labelDriver.stringValue = "Proxy"
labelPhpVersion.stringValue = "Active" labelPhpVersion.stringValue = "Active"

View File

@@ -17,7 +17,7 @@ extension DomainListVC {
let action = selectedSite!.secured ? "unsecure" : "secure" let action = selectedSite!.secured ? "unsecure" : "secure"
let selectedSite = selectedSite! let selectedSite = selectedSite!
let command = "cd '\(selectedSite.absolutePath)' && sudo \(Paths.valet) \(action) && exit;" let command = "cd '\(selectedSite.absolutePath)' && sudo \(Paths.valet) \(action) && exit;"
waitAndExecute { waitAndExecute {
Shell.run(command, requiresPath: true) Shell.run(command, requiresPath: true)
} completion: { [self] in } completion: { [self] in
@@ -41,18 +41,18 @@ extension DomainListVC {
) )
) )
} }
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4]) tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4])
tableView.deselectRow(rowToReload) tableView.deselectRow(rowToReload)
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true) tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
} }
} }
@objc func openInBrowser() { @objc func openInBrowser() {
guard let selected = self.selected else { guard let selected = self.selected else {
return return
} }
guard let url = selected.getListableUrl() else { guard let url = selected.getListableUrl() else {
BetterAlert() BetterAlert()
.withInformation( .withInformation(
@@ -63,29 +63,29 @@ extension DomainListVC {
.show() .show()
return return
} }
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
} }
@objc func openInFinder() { @objc func openInFinder() {
Shell.run("open '\(selectedSite!.absolutePath)'") Shell.run("open '\(selectedSite!.absolutePath)'")
} }
@objc func openInTerminal() { @objc func openInTerminal() {
Shell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath)'") Shell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath)'")
} }
@objc func openWithEditor(sender: EditorMenuItem) { @objc func openWithEditor(sender: EditorMenuItem) {
guard let editor = sender.editor else { return } guard let editor = sender.editor else { return }
editor.openDirectory(file: selectedSite!.absolutePath) editor.openDirectory(file: selectedSite!.absolutePath)
} }
@objc func isolateSite(sender: PhpMenuItem) { @objc func isolateSite(sender: PhpMenuItem) {
let command = "sudo \(Paths.valet) isolate php@\(sender.version) --site '\(self.selectedSite!.name)' && exit;" let command = "sudo \(Paths.valet) isolate php@\(sender.version) --site '\(self.selectedSite!.name)' && exit;"
self.performAction(command: command) { self.performAction(command: command) {
self.selectedSite!.determineIsolated() self.selectedSite!.determineIsolated()
if self.selectedSite!.isolatedPhpVersion == nil { if self.selectedSite!.isolatedPhpVersion == nil {
BetterAlert() BetterAlert()
.withInformation( .withInformation(
@@ -98,22 +98,22 @@ extension DomainListVC {
} }
} }
} }
@objc func removeIsolatedSite() { @objc func removeIsolatedSite() {
self.performAction(command: "sudo \(Paths.valet) unisolate --site '\(self.selectedSite!.name)' && exit;") { self.performAction(command: "sudo \(Paths.valet) unisolate --site '\(self.selectedSite!.name)' && exit;") {
self.selectedSite!.isolatedPhpVersion = nil self.selectedSite!.isolatedPhpVersion = nil
} }
} }
@objc func unlinkSite() { @objc func unlinkSite() {
guard let site = selectedSite else { guard let site = selectedSite else {
return return
} }
if site.aliasPath == nil { if site.aliasPath == nil {
return return
} }
Alert.confirm( Alert.confirm(
onWindow: view.window!, onWindow: view.window!,
messageText: "domain_list.confirm_unlink".localized(site.name), messageText: "domain_list.confirm_unlink".localized(site.name),
@@ -127,12 +127,12 @@ extension DomainListVC {
} }
) )
} }
@objc func removeProxy() { @objc func removeProxy() {
guard let proxy = selectedProxy else { guard let proxy = selectedProxy else {
return return
} }
Alert.confirm( Alert.confirm(
onWindow: view.window!, onWindow: view.window!,
messageText: "domain_list.confirm_unproxy".localized("\(proxy.domain).\(proxy.tld)"), messageText: "domain_list.confirm_unproxy".localized("\(proxy.domain).\(proxy.tld)"),
@@ -146,10 +146,10 @@ extension DomainListVC {
} }
) )
} }
private func performAction(command: String, beforeCellReload: @escaping () -> Void) { private func performAction(command: String, beforeCellReload: @escaping () -> Void) {
let rowToReload = tableView.selectedRow let rowToReload = tableView.selectedRow
waitAndExecute { waitAndExecute {
Shell.run(command, requiresPath: true) Shell.run(command, requiresPath: true)
} completion: { [self] in } completion: { [self] in
@@ -159,5 +159,5 @@ extension DomainListVC {
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true) tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
} }
} }
} }

View File

@@ -9,13 +9,13 @@
import Cocoa import Cocoa
extension DomainListVC { extension DomainListVC {
internal func reloadContextMenu() { internal func reloadContextMenu() {
guard let selected = selected else { guard let selected = selected else {
tableView.menu = nil tableView.menu = nil
return return
} }
if let selected = selected as? ValetSite { if let selected = selected as? ValetSite {
addMenuItemsForSite(selected) addMenuItemsForSite(selected)
return return
@@ -25,29 +25,29 @@ extension DomainListVC {
return return
} }
} }
// MARK: - Menu Items for Site // MARK: - Menu Items for Site
private func addMenuItemsForSite(_ site: ValetSite) { private func addMenuItemsForSite(_ site: ValetSite) {
let menu = NSMenu() let menu = NSMenu()
addSystemApps(to: menu) addSystemApps(to: menu)
addSeparator(to: menu) addSeparator(to: menu)
addDetectedApps(to: menu) addDetectedApps(to: menu)
addSeparator(to: menu) addSeparator(to: menu)
if Valet.enabled(feature: .isolatedSites) { if Valet.enabled(feature: .isolatedSites) {
addIsolate(to: menu, with: site) addIsolate(to: menu, with: site)
} else { } else {
addDisabledIsolation(to: menu) addDisabledIsolation(to: menu)
} }
addUnlink(to: menu, with: site) addUnlink(to: menu, with: site)
addToggleSecure(to: menu, with: site) addToggleSecure(to: menu, with: site)
tableView.menu = menu tableView.menu = menu
} }
private func addSystemApps(to menu: NSMenu) { private func addSystemApps(to menu: NSMenu) {
menu.addItem(withTitle: "domain_list.system_apps".localized, action: nil, keyEquivalent: "") menu.addItem(withTitle: "domain_list.system_apps".localized, action: nil, keyEquivalent: "")
menu.addItem( menu.addItem(
@@ -66,13 +66,13 @@ extension DomainListVC {
keyEquivalent: "B" keyEquivalent: "B"
) )
} }
private func addDetectedApps(to menu: NSMenu) { private func addDetectedApps(to menu: NSMenu) {
if (applications.count > 0) { if !applications.isEmpty {
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: "domain_list.detected_apps".localized, action: nil, keyEquivalent: "") menu.addItem(withTitle: "domain_list.detected_apps".localized, action: nil, keyEquivalent: "")
for (_, editor) in applications.enumerated() { for editor in applications {
let editorMenuItem = EditorMenuItem( let editorMenuItem = EditorMenuItem(
title: "Open with \(editor.name)", title: "Open with \(editor.name)",
action: #selector(self.openWithEditor(sender:)), action: #selector(self.openWithEditor(sender:)),
@@ -83,9 +83,9 @@ extension DomainListVC {
} }
} }
} }
private func addUnlink(to menu: NSMenu, with site: ValetSite) { private func addUnlink(to menu: NSMenu, with site: ValetSite) {
if (site.aliasPath != nil) { if site.aliasPath != nil {
menu.addItem( menu.addItem(
withTitle: "domain_list.unlink".localized, withTitle: "domain_list.unlink".localized,
action: #selector(self.unlinkSite), action: #selector(self.unlinkSite),
@@ -94,25 +94,29 @@ extension DomainListVC {
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
} }
} }
private func addDisabledIsolation(to menu: NSMenu) { private func addDisabledIsolation(to menu: NSMenu) {
menu.addItem(withTitle: "domain_list.isolation_unavailable".localized, action: nil, keyEquivalent: "") menu.addItem(withTitle: "domain_list.isolation_unavailable".localized, action: nil, keyEquivalent: "")
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
} }
private func addIsolate(to menu: NSMenu, with site: ValetSite) { private func addIsolate(to menu: NSMenu, with site: ValetSite) {
if site.isolatedPhpVersion == nil { if site.isolatedPhpVersion == nil {
// ISOLATION POSSIBLE // 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() let submenu = NSMenu()
submenu.addItem(withTitle: "Choose a PHP version", action: nil, keyEquivalent: "") submenu.addItem(withTitle: "Choose a PHP version", action: nil, keyEquivalent: "")
for version in PhpEnv.shared.availablePhpVersions.reversed() { 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 item.version = version
submenu.addItem(item) submenu.addItem(item)
} }
menu.setSubmenu(submenu, for: isolationMenuItem) menu.setSubmenu(submenu, for: isolationMenuItem)
menu.addItem(isolationMenuItem) menu.addItem(isolationMenuItem)
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
} else { } else {
@@ -125,7 +129,7 @@ extension DomainListVC {
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
} }
} }
private func addToggleSecure(to menu: NSMenu, with site: ValetSite) { private func addToggleSecure(to menu: NSMenu, with site: ValetSite) {
menu.addItem( menu.addItem(
withTitle: site.secured withTitle: site.secured
@@ -135,9 +139,9 @@ extension DomainListVC {
keyEquivalent: "" keyEquivalent: ""
) )
} }
// MARK: - Menu Items for Proxy // MARK: - Menu Items for Proxy
private func addMenuItemsForProxy(_ proxy: ValetProxy) { private func addMenuItemsForProxy(_ proxy: ValetProxy) {
let menu = NSMenu() let menu = NSMenu()
addOpenProxyInBrowser(to: menu) addOpenProxyInBrowser(to: menu)
@@ -145,7 +149,7 @@ extension DomainListVC {
addRemoveProxy(to: menu) addRemoveProxy(to: menu)
tableView.menu = menu tableView.menu = menu
} }
private func addOpenProxyInBrowser(to menu: NSMenu) { private func addOpenProxyInBrowser(to menu: NSMenu) {
menu.addItem( menu.addItem(
withTitle: "domain_list.open_in_browser".localized, withTitle: "domain_list.open_in_browser".localized,
@@ -153,7 +157,7 @@ extension DomainListVC {
keyEquivalent: "B" keyEquivalent: "B"
) )
} }
private func addRemoveProxy(to menu: NSMenu) { private func addRemoveProxy(to menu: NSMenu) {
menu.addItem( menu.addItem(
withTitle: "domain_list.unproxy".localized, withTitle: "domain_list.unproxy".localized,
@@ -161,11 +165,11 @@ extension DomainListVC {
keyEquivalent: "" keyEquivalent: ""
) )
} }
// MARK: - Shared // MARK: - Shared
private func addSeparator(to menu: NSMenu) { private func addSeparator(to menu: NSMenu) {
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
} }
} }

View File

@@ -10,62 +10,62 @@ import Cocoa
import Carbon import Carbon
class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource { class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
// MARK: - Outlets // MARK: - Outlets
@IBOutlet weak var tableView: NSTableView! @IBOutlet weak var tableView: NSTableView!
@IBOutlet weak var progressIndicator: NSProgressIndicator! @IBOutlet weak var progressIndicator: NSProgressIndicator!
// MARK: - Variables // MARK: - Variables
/// List of sites that will be displayed in this view. Originates from the `Valet` object. /// List of sites that will be displayed in this view. Originates from the `Valet` object.
var domains: [DomainListable] = [] var domains: [DomainListable] = []
/// Array that contains various apps that might open a particular site directory. /// Array that contains various apps that might open a particular site directory.
var applications: [Application] { var applications: [Application] {
return App.shared.detectedApplications return App.shared.detectedApplications
} }
/// The last sort descriptor used. /// The last sort descriptor used.
var sortDescriptor: NSSortDescriptor? = nil var sortDescriptor: NSSortDescriptor?
/// String that was last searched for. Empty by default. /// String that was last searched for. Empty by default.
var lastSearchedFor = "" var lastSearchedFor = ""
// MARK: - Helper Variables // MARK: - Helper Variables
var selectedSite: ValetSite? { var selectedSite: ValetSite? {
if tableView.selectedRow == -1 { if tableView.selectedRow == -1 {
return nil return nil
} }
return domains[tableView.selectedRow] as? ValetSite return domains[tableView.selectedRow] as? ValetSite
} }
var selectedProxy: ValetProxy? { var selectedProxy: ValetProxy? {
if tableView.selectedRow == -1 { if tableView.selectedRow == -1 {
return nil return nil
} }
return domains[tableView.selectedRow] as? ValetProxy return domains[tableView.selectedRow] as? ValetProxy
} }
var selected: DomainListable? { var selected: DomainListable? {
if tableView.selectedRow == -1 { if tableView.selectedRow == -1 {
return nil return nil
} }
return domains[tableView.selectedRow] return domains[tableView.selectedRow]
} }
var timer: Timer? = nil var timer: Timer?
// MARK: - Display // MARK: - Display
public static func create(delegate: NSWindowDelegate?) { public static func create(delegate: NSWindowDelegate?) {
let storyboard = NSStoryboard(name: "Main" , bundle : nil) let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController( let windowController = storyboard.instantiateController(
withIdentifier: "domainListWindow" withIdentifier: "domainListWindow"
) as! DomainListWC ) as! DomainListWC
windowController.window!.title = "domain_list.title".localized windowController.window!.title = "domain_list.title".localized
windowController.window!.subtitle = "domain_list.subtitle".localized windowController.window!.subtitle = "domain_list.subtitle".localized
windowController.window!.delegate = delegate windowController.window!.delegate = delegate
@@ -75,24 +75,24 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
windowController.window!.minSize = NSSize(width: 550, height: 200) windowController.window!.minSize = NSSize(width: 550, height: 200)
windowController.window!.delegate = windowController windowController.window!.delegate = windowController
windowController.window!.setFrameAutosaveName("domainListWindow") windowController.window!.setFrameAutosaveName("domainListWindow")
App.shared.domainListWindowController = windowController App.shared.domainListWindowController = windowController
} }
public static func show(delegate: NSWindowDelegate? = nil) { public static func show(delegate: NSWindowDelegate? = nil) {
if (App.shared.domainListWindowController == nil) { if App.shared.domainListWindowController == nil {
Self.create(delegate: delegate) Self.create(delegate: delegate)
} }
App.shared.domainListWindowController!.showWindow(self) App.shared.domainListWindowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
// MARK: - Lifecycle // MARK: - Lifecycle
override func viewDidLoad() { override func viewDidLoad() {
tableView.doubleAction = #selector(self.doubleClicked(sender:)) tableView.doubleAction = #selector(self.doubleClicked(sender:))
if !Valet.shared.sites.isEmpty { if !Valet.shared.sites.isEmpty {
// Preloaded list // Preloaded list
domains = Valet.getDomainListable() domains = Valet.getDomainListable()
@@ -101,9 +101,9 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
reloadDomains() reloadDomains()
} }
} }
// MARK: - Async Operations // MARK: - Async Operations
/** /**
Disables the UI so the user cannot interact with it. Disables the UI so the user cannot interact with it.
Also shows a spinner to indicate that we're busy. 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 timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in
self.progressIndicator.startAnimation(true) self.progressIndicator.startAnimation(true)
}) })
tableView.alphaValue = 0.3 tableView.alphaValue = 0.3
tableView.isEnabled = false tableView.isEnabled = false
tableView.selectRowIndexes([], byExtendingSelection: true) tableView.selectRowIndexes([], byExtendingSelection: true)
} }
/** /**
Re-enables the UI so the user can interact with it. 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.alphaValue = 1.0
tableView.isEnabled = true tableView.isEnabled = true
} }
/** /**
Executes a specific callback and fires the completion callback, Executes a specific callback and fires the completion callback,
while updating the UI as required. As long as 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 execute: Callback of the work that needs to happen.
- Parameter completion: Callback that is fired when the work is done. - 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() setUIBusy()
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
execute() execute()
// For a smoother animation, expect at least a 0.2 second delay // For a smoother animation, expect at least a 0.2 second delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
completion() completion()
@@ -150,9 +149,9 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
} }
} }
} }
// MARK: - Site Data Loading // MARK: - Site Data Loading
func reloadDomains() { func reloadDomains() {
waitAndExecute { waitAndExecute {
Valet.shared.reloadSites() Valet.shared.reloadSites()
@@ -161,29 +160,24 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
searchedFor(text: lastSearchedFor) searchedFor(text: lastSearchedFor)
} }
} }
func applySortDescriptor(_ descriptor: NSSortDescriptor) { func applySortDescriptor(_ descriptor: NSSortDescriptor) {
sortDescriptor = descriptor sortDescriptor = descriptor
var sorted = self.domains var sorted = self.domains
switch descriptor.key { switch descriptor.key {
case "Secure": case "Secure": sorted = self.domains.sorted { $0.getListableSecured() && !$1.getListableSecured() }
sorted = self.domains.sorted { $0.getListableSecured() && !$1.getListableSecured() }; break case "Domain": sorted = self.domains.sorted { $0.getListableAbsolutePath() < $1.getListableAbsolutePath() }
case "Domain": case "PHP": sorted = self.domains.sorted { $0.getListablePhpVersion() < $1.getListablePhpVersion() }
sorted = self.domains.sorted { $0.getListableAbsolutePath() < $1.getListableAbsolutePath() }; break case "Kind": sorted = self.domains.sorted { $0.getListableKind() < $1.getListableKind() }
case "PHP": case "Type": sorted = self.domains.sorted { $0.getListableType() < $1.getListableType() }
sorted = self.domains.sorted { $0.getListablePhpVersion() < $1.getListablePhpVersion() }; break default: 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;
} }
self.domains = descriptor.ascending ? sorted.reversed() : sorted self.domains = descriptor.ascending ? sorted.reversed() : sorted
} }
func addedNewSite(name: String, secure: Bool) { func addedNewSite(name: String, secure: Bool) {
waitAndExecute { waitAndExecute {
Valet.shared.reloadSites() Valet.shared.reloadSites()
@@ -191,7 +185,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
find(name, secure) find(name, secure)
} }
} }
private func find(_ name: String, _ secure: Bool = false) { private func find(_ name: String, _ secure: Bool = false) {
domains = Valet.getDomainListable() domains = Valet.getDomainListable()
searchedFor(text: "") searchedFor(text: "")
@@ -199,19 +193,19 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
DispatchQueue.main.async { DispatchQueue.main.async {
self.tableView.selectRowIndexes([site.offset], byExtendingSelection: false) self.tableView.selectRowIndexes([site.offset], byExtendingSelection: false)
self.tableView.scrollRowToVisible(site.offset) self.tableView.scrollRowToVisible(site.offset)
if (secure && !site.element.getListableSecured()) { if secure && !site.element.getListableSecured() {
self.toggleSecure() self.toggleSecure()
} }
} }
} }
} }
// MARK: - Table View Delegate // MARK: - Table View Delegate
func numberOfRows(in tableView: NSTableView) -> Int { func numberOfRows(in tableView: NSTableView) -> Int {
return domains.count return domains.count
} }
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) { func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
guard let sortDescriptor = tableView.sortDescriptors.first else { return } guard let sortDescriptor = tableView.sortDescriptors.first else { return }
// Kinda scuffed way of applying sort descriptors here, but it works. // Kinda scuffed way of applying sort descriptors here, but it works.
@@ -219,7 +213,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
applySortDescriptor(sortDescriptor) applySortDescriptor(sortDescriptor)
searchedFor(text: lastSearchedFor) searchedFor(text: lastSearchedFor)
} }
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let mapping: [String: String] = [ let mapping: [String: String] = [
"TLS": DomainListTLSCell.reusableName, "TLS": DomainListTLSCell.reusableName,
@@ -228,76 +222,76 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
"KIND": DomainListKindCell.reusableName, "KIND": DomainListKindCell.reusableName,
"TYPE": DomainListTypeCell.reusableName "TYPE": DomainListTypeCell.reusableName
] ]
let columnName = tableColumn!.identifier.rawValue let columnName = tableColumn!.identifier.rawValue
let identifier = NSUserInterfaceItemIdentifier(rawValue: mapping[columnName]!) let identifier = NSUserInterfaceItemIdentifier(rawValue: mapping[columnName]!)
guard let userCell = tableView.makeView(withIdentifier: identifier, owner: self) guard let userCell = tableView.makeView(withIdentifier: identifier, owner: self)
as? DomainListCellProtocol else { return nil } as? DomainListCellProtocol else { return nil }
if let site = domains[row] as? ValetSite { if let site = domains[row] as? ValetSite {
userCell.populateCell(with: site) userCell.populateCell(with: site)
} }
if let proxy = domains[row] as? ValetProxy { if let proxy = domains[row] as? ValetProxy {
userCell.populateCell(with: proxy) userCell.populateCell(with: proxy)
} }
return userCell as? NSView return userCell as? NSView
} }
func tableViewSelectionDidChange(_ notification: Notification) { func tableViewSelectionDidChange(_ notification: Notification) {
reloadContextMenu() reloadContextMenu()
} }
@objc func doubleClicked(sender: Any) { @objc func doubleClicked(sender: Any) {
guard self.selected != nil else { guard self.selected != nil else {
return return
} }
self.openInBrowser() self.openInBrowser()
} }
// MARK: - (Search) Text Field Delegate // MARK: - (Search) Text Field Delegate
func reloadTable() { func reloadTable() {
if let sortDescriptor = sortDescriptor { if let sortDescriptor = sortDescriptor {
self.applySortDescriptor(sortDescriptor) self.applySortDescriptor(sortDescriptor)
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.tableView.reloadData() self.tableView.reloadData()
} }
} }
func searchedFor(text: String) { func searchedFor(text: String) {
lastSearchedFor = text lastSearchedFor = text
let searchString = text.lowercased() let searchString = text.lowercased()
if searchString.isEmpty { if searchString.isEmpty {
domains = Valet.getDomainListable() domains = Valet.getDomainListable()
reloadTable() reloadTable()
return return
} }
let splitSearchString: [String] = searchString let splitSearchString: [String] = searchString
.split(separator: " ") .split(separator: " ")
.map { return String($0) } .map { return String($0) }
domains = Valet.getDomainListable().filter({ site in domains = Valet.getDomainListable().filter({ site in
return !splitSearchString.map { searchString in return !splitSearchString.map { searchString in
return site.getListableName().lowercased().contains(searchString) return site.getListableName().lowercased().contains(searchString)
}.contains(false) }.contains(false)
}) })
reloadTable() reloadTable()
} }
// MARK: - Deinitialization // MARK: - Deinitialization
deinit { deinit {
Log.perf("DomainListVC deallocated") Log.perf("DomainListVC deallocated")
} }

View File

@@ -9,82 +9,82 @@
import Cocoa import Cocoa
class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate { class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
// MARK: - Window Identifier // MARK: - Window Identifier
override var windowName: String { override var windowName: String {
return "DomainList" return "DomainList"
} }
// MARK: - Outlets // MARK: - Outlets
@IBOutlet weak var searchToolbarItem: NSSearchToolbarItem! @IBOutlet weak var searchToolbarItem: NSSearchToolbarItem!
// MARK: - Window Lifecycle // MARK: - Window Lifecycle
override func windowDidLoad() { override func windowDidLoad() {
super.windowDidLoad() super.windowDidLoad()
self.searchToolbarItem.searchField.delegate = self self.searchToolbarItem.searchField.delegate = self
self.searchToolbarItem.searchField.becomeFirstResponder() self.searchToolbarItem.searchField.becomeFirstResponder()
} }
// MARK: - Search functionality // MARK: - Search functionality
var contentVC: DomainListVC { var contentVC: DomainListVC {
return self.contentViewController as! DomainListVC return self.contentViewController as! DomainListVC
} }
var searchTimer: Timer? var searchTimer: Timer?
func controlTextDidChange(_ notification: Notification) { func controlTextDidChange(_ notification: Notification) {
guard let searchField = notification.object as? NSSearchField else { guard let searchField = notification.object as? NSSearchField else {
return return
} }
self.searchTimer?.invalidate() self.searchTimer?.invalidate()
searchTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false, block: { _ in searchTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false, block: { _ in
self.contentVC.searchedFor(text: searchField.stringValue) self.contentVC.searchedFor(text: searchField.stringValue)
}) })
} }
// MARK: - Reload functionality // MARK: - Reload functionality
@IBAction func pressedReload(_ sender: Any?) { @IBAction func pressedReload(_ sender: Any?) {
contentVC.reloadDomains() contentVC.reloadDomains()
} }
@IBAction func pressedAddLink(_ sender: Any?) { @IBAction func pressedAddLink(_ sender: Any?) {
showSelectionWindow() showSelectionWindow()
} }
// MARK: - Add a new site // MARK: - Add a new site
func showSelectionWindow() { func showSelectionWindow() {
let storyboard = NSStoryboard(name: "Main", bundle : nil) let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController( let windowController = storyboard.instantiateController(
withIdentifier: "showSelectionWindow" withIdentifier: "showSelectionWindow"
) as! NSWindowController ) as! NSWindowController
let viewController = windowController.window! let viewController = windowController.window!
.contentViewController as! SelectionVC .contentViewController as! SelectionVC
viewController.domainListWC = self viewController.domainListWC = self
self.window?.beginSheet(windowController.window!) self.window?.beginSheet(windowController.window!)
} }
func startCreateLinkFlow() { func startCreateLinkFlow() {
self.showFolderSelectionForLink() self.showFolderSelectionForLink()
} }
func startCreateProxyFlow() { func startCreateProxyFlow() {
self.showProxyPopup() self.showProxyPopup()
} }
// MARK: - Popups // MARK: - Popups
private func showFolderSelectionForLink() { private func showFolderSelectionForLink() {
let dialog = NSOpenPanel() let dialog = NSOpenPanel()
dialog.message = "domain_list.add.modal_description".localized dialog.message = "domain_list.add.modal_description".localized
@@ -95,37 +95,37 @@ class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate
dialog.canChooseFiles = false dialog.canChooseFiles = false
dialog.beginSheetModal(for: self.window!) { response in dialog.beginSheetModal(for: self.window!) { response in
let result = dialog.url let result = dialog.url
if (result != nil && response == .OK) { if result != nil && response == .OK {
let path: String = result!.path let path: String = result!.path
self.showLinkPopup(path) self.showLinkPopup(path)
} }
} }
} }
private func showLinkPopup(_ folder: String) { private func showLinkPopup(_ folder: String) {
let storyboard = NSStoryboard(name: "Main", bundle : nil) let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController( let windowController = storyboard.instantiateController(
withIdentifier: "addSiteWindow" withIdentifier: "addSiteWindow"
) as! NSWindowController ) as! NSWindowController
let viewController = windowController.window!.contentViewController as! AddSiteVC let viewController = windowController.window!.contentViewController as! AddSiteVC
viewController.pathControl.url = URL(fileURLWithPath: folder) viewController.pathControl.url = URL(fileURLWithPath: folder)
viewController.inputDomainName.stringValue = String(folder.split(separator: "/").last!) viewController.inputDomainName.stringValue = String(folder.split(separator: "/").last!)
viewController.updateTextField() viewController.updateTextField()
self.window?.beginSheet(windowController.window!) self.window?.beginSheet(windowController.window!)
} }
private func showProxyPopup() { private func showProxyPopup() {
let storyboard = NSStoryboard(name: "Main", bundle : nil) let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController( let windowController = storyboard.instantiateController(
withIdentifier: "addProxyWindow" withIdentifier: "addProxyWindow"
) as! NSWindowController ) as! NSWindowController
// let viewController = windowController.window!.contentViewController as! AddSiteVC // let viewController = windowController.window!.contentViewController as! AddSiteVC
self.window?.beginSheet(windowController.window!) self.window?.beginSheet(windowController.window!)
} }
} }

View File

@@ -10,31 +10,31 @@ import Foundation
import Cocoa import Cocoa
class SelectionVC: NSViewController { class SelectionVC: NSViewController {
weak var domainListWC: DomainListWC? weak var domainListWC: DomainListWC?
@IBOutlet weak var textFieldTitle: NSTextField! @IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var textFieldDescription: NSTextField! @IBOutlet weak var textFieldDescription: NSTextField!
@IBOutlet weak var buttonCreateLink: NSButton! @IBOutlet weak var buttonCreateLink: NSButton!
@IBOutlet weak var buttonCreateProxy: NSButton! @IBOutlet weak var buttonCreateProxy: NSButton!
@IBOutlet weak var buttonCancel: NSButton! @IBOutlet weak var buttonCancel: NSButton!
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
loadStaticLocalisedStrings() loadStaticLocalisedStrings()
} }
override func viewDidAppear() { override func viewDidAppear() {
view.window?.makeFirstResponder(buttonCreateLink) view.window?.makeFirstResponder(buttonCreateLink)
} }
private func dismissView(outcome: NSApplication.ModalResponse) { private func dismissView(outcome: NSApplication.ModalResponse) {
guard let window = self.view.window, let parent = window.sheetParent else { return } guard let window = self.view.window, let parent = window.sheetParent else { return }
parent.endSheet(window, returnCode: outcome) parent.endSheet(window, returnCode: outcome)
} }
// MARK: - Localisation // MARK: - Localisation
func loadStaticLocalisedStrings() { func loadStaticLocalisedStrings() {
textFieldTitle.stringValue = "selection.title".localized textFieldTitle.stringValue = "selection.title".localized
textFieldDescription.stringValue = "selection.description".localized textFieldDescription.stringValue = "selection.description".localized
@@ -42,21 +42,21 @@ class SelectionVC: NSViewController {
buttonCreateLink.title = "selection.create_link".localized buttonCreateLink.title = "selection.create_link".localized
buttonCreateProxy.title = "selection.create_proxy".localized buttonCreateProxy.title = "selection.create_proxy".localized
} }
// MARK: - Outlet Interactions // MARK: - Outlet Interactions
@IBAction func pressedCreateLink(_ sender: Any) { @IBAction func pressedCreateLink(_ sender: Any) {
self.dismissView(outcome: .continue) self.dismissView(outcome: .continue)
domainListWC?.startCreateLinkFlow() domainListWC?.startCreateLinkFlow()
} }
@IBAction func pressedCreateProxy(_ sender: Any) { @IBAction func pressedCreateProxy(_ sender: Any) {
self.dismissView(outcome: .continue) self.dismissView(outcome: .continue)
domainListWC?.startCreateProxyFlow() domainListWC?.startCreateProxyFlow()
} }
@IBAction func pressedCancel(_ sender: Any) { @IBAction func pressedCancel(_ sender: Any) {
self.dismissView(outcome: .cancel) self.dismissView(outcome: .cancel)
} }
} }

View File

@@ -13,59 +13,58 @@ import Foundation
to this object. to this object.
*/ */
struct ComposerJson: Decodable { struct ComposerJson: Decodable {
// MARK: - JSON structure // MARK: - JSON structure
let dependencies: Dictionary<String, String>? let dependencies: [String: String]?
let devDependencies: Dictionary<String, String>? let devDependencies: [String: String]?
let configuration: Config? let configuration: Config?
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case dependencies = "require" case dependencies = "require"
case devDependencies = "require-dev" case devDependencies = "require-dev"
case configuration = "config" case configuration = "config"
} }
struct Config: Decodable { struct Config: Decodable {
let platform: Platform? let platform: Platform?
} }
struct Platform: Decodable { struct Platform: Decodable {
let php: String? let php: String?
} }
// MARK: - Helpers // MARK: - Helpers
/** /**
Checks what the PHP version constraint is. Checks what the PHP version constraint is.
Returns a tuple (constraint, location of constraint). Returns a tuple (constraint, location of constraint).
*/ */
public func getPhpVersion() -> (String, ValetSite.VersionSource) public func getPhpVersion() -> (String, ValetSite.VersionSource) {
{
// Check if in platform // Check if in platform
if configuration?.platform?.php != nil { if configuration?.platform?.php != nil {
return (configuration!.platform!.php!, .platform) return (configuration!.platform!.php!, .platform)
} }
// Check if in dependencies // Check if in dependencies
if dependencies?["php"] != nil { if dependencies?["php"] != nil {
return (dependencies!["php"]!, .require) return (dependencies!["php"]!, .require)
} }
// Unknown! // Unknown!
return ("???", .unknown) return ("???", .unknown)
} }
/** /**
Checks if any notable dependencies can be resolved. Checks if any notable dependencies can be resolved.
Only notable dependencies are saved. Only notable dependencies are saved.
*/ */
public func getNotableDependencies() -> [String: String] { public func getNotableDependencies() -> [String: String] {
var notable: [String: String] = [:] var notable: [String: String] = [:]
var scan = Array(PhpFrameworks.DependencyList.keys) var scan = Array(PhpFrameworks.DependencyList.keys)
scan.append("php") scan.append("php")
scan.forEach { dependency in scan.forEach { dependency in
if dependencies?[dependency] != nil { if dependencies?[dependency] != nil {
notable[dependency] = dependencies![dependency] notable[dependency] = dependencies![dependency]
@@ -74,7 +73,5 @@ struct ComposerJson: Decodable {
return notable return notable
} }
} }

View File

@@ -10,11 +10,11 @@ import Foundation
class ComposerWindow { class ComposerWindow {
private var menu: MainMenu? = nil private var menu: MainMenu?
private var shouldNotify: Bool! = nil private var shouldNotify: Bool! = nil
private var completion: ((Bool) -> Void)! = 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. Updates the global dependencies and runs the completion callback when done.
*/ */
@@ -22,33 +22,33 @@ class ComposerWindow {
self.menu = MainMenu.shared self.menu = MainMenu.shared
self.shouldNotify = notify self.shouldNotify = notify
self.completion = completion self.completion = completion
Paths.shared.detectBinaryPaths() Paths.shared.detectBinaryPaths()
if Paths.composer == nil { if Paths.composer == nil {
presentMissingAlert() presentMissingAlert()
return return
} }
PhpEnv.shared.isBusy = true PhpEnv.shared.isBusy = true
menu?.setBusyImage() menu?.setBusyImage()
menu?.rebuild() menu?.rebuild()
window = ProgressWindowController.display( window = ProgressWindowController.display(
title: "alert.composer_progress.title".localized, title: "alert.composer_progress.title".localized,
description: "alert.composer_progress.info".localized description: "alert.composer_progress.info".localized
) )
window?.setType(info: true) window?.setType(info: true)
DispatchQueue.global(qos: .userInitiated).async { [self] in DispatchQueue.global(qos: .userInitiated).async { [self] in
let task = Shell.user.createTask( let task = Shell.user.createTask(
for: "\(Paths.composer!) global update", requiresPath: true for: "\(Paths.composer!) global update", requiresPath: true
) )
DispatchQueue.main.async { DispatchQueue.main.async {
self.window?.addToConsole("\(Paths.composer!) global update\n") self.window?.addToConsole("\(Paths.composer!) global update\n")
} }
task.listen( task.listen(
didReceiveStandardOutputData: { string in didReceiveStandardOutputData: { string in
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -63,11 +63,11 @@ class ComposerWindow {
// Log.perf("\(string.trimmingCharacters(in: .newlines))") // Log.perf("\(string.trimmingCharacters(in: .newlines))")
} }
) )
task.launch() task.launch()
task.waitUntilExit() task.waitUntilExit()
task.haltListening() task.haltListening()
if task.terminationStatus <= 0 { if task.terminationStatus <= 0 {
composerUpdateSucceeded() composerUpdateSucceeded()
} else { } else {
@@ -75,12 +75,12 @@ class ComposerWindow {
} }
} }
} }
private func composerUpdateSucceeded() { private func composerUpdateSucceeded() {
// Closing the window should happen after a slight delay // Closing the window should happen after a slight delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in
window?.close() window?.close()
if (shouldNotify) { if shouldNotify {
LocalNotification.send( LocalNotification.send(
title: "alert.composer_success.title".localized, title: "alert.composer_success.title".localized,
subtitle: "alert.composer_success.info".localized subtitle: "alert.composer_success.info".localized
@@ -91,7 +91,7 @@ class ComposerWindow {
completion(true) completion(true)
} }
} }
private func composerUpdateFailed() { private func composerUpdateFailed() {
// Showing that something failed should be shown immediately // Showing that something failed should be shown immediately
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
@@ -103,18 +103,18 @@ class ComposerWindow {
completion(false) completion(false)
} }
} }
// MARK: Main Menu Update // MARK: Main Menu Update
private func removeBusyStatus() { private func removeBusyStatus() {
PhpEnv.shared.isBusy = false PhpEnv.shared.isBusy = false
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
menu?.updatePhpVersionInStatusBar() menu?.updatePhpVersionInStatusBar()
} }
} }
// MARK: Alert // MARK: Alert
private func presentMissingAlert() { private func presentMissingAlert() {
BetterAlert() BetterAlert()
.withInformation( .withInformation(

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
struct PhpFrameworks { struct PhpFrameworks {
/** /**
This list should probably be reversed when checked, because some of these This list should probably be reversed when checked, because some of these
will also require either `laravel/framework` or `symfony/symfony`. will also require either `laravel/framework` or `symfony/symfony`.
@@ -17,10 +17,10 @@ struct PhpFrameworks {
public static let DependencyList = [ public static let DependencyList = [
// COMMON FRAMEWORKS // COMMON FRAMEWORKS
"laravel/framework" : "Laravel", "laravel/framework": "Laravel",
"symfony/symfony": "Symfony", "symfony/symfony": "Symfony",
"laravel/lumen": "Lumen", "laravel/lumen": "Lumen",
// VARIOUS CMS // VARIOUS CMS
"roots/bedrock": "Bedrock", "roots/bedrock": "Bedrock",
"cakephp/app": "CakePHP", "cakephp/app": "CakePHP",
@@ -37,15 +37,15 @@ struct PhpFrameworks {
"johnpbloch/wordpress-core": "WordPress", "johnpbloch/wordpress-core": "WordPress",
"zendframework/zendframework": "Zend", "zendframework/zendframework": "Zend",
"zendframework/zend-mvc": "Zend", "zendframework/zend-mvc": "Zend",
"typo3/cms-core": "Typo3", "typo3/cms-core": "Typo3"
// TODO (6.0): Handle these in v6.0 // TODO (6.0): Handle these in v6.0
// "magento/*": "Magento", // "magento/*": "Magento",
// "concrete5/*": "Concrete5", // "concrete5/*": "Concrete5",
// "contao/*": "Contao", // "contao/*": "Contao",
// "slim/*": "Slim", // "slim/*": "Slim",
] ]
public static let FileMapping: [String: [String]] = [ public static let FileMapping: [String: [String]] = [
"Drupal": [ "Drupal": [
// Legacy installations // Legacy installations
@@ -61,10 +61,10 @@ struct PhpFrameworks {
], ],
"Typo3": [ "Typo3": [
"/typo3", "/typo3",
"/public/typo3", "/public/typo3"
] ]
] ]
/** /**
There are two cases where users are unlikely to use `composer`, There are two cases where users are unlikely to use `composer`,
when setting up a Drupal or a WordPress project. For performance when setting up a Drupal or a WordPress project. For performance
@@ -75,13 +75,13 @@ struct PhpFrameworks {
let found = entry.value let found = entry.value
.map { path in return Filesystem.fileExists(basePath + path) } .map { path in return Filesystem.fileExists(basePath + path) }
.contains(true) .contains(true)
if found { if found {
return entry.key return entry.key
} }
} }
return nil return nil
} }
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
class HomebrewDiagnostics { class HomebrewDiagnostics {
/** /**
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated. It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
This will then result in two different aliases claiming to point to the same formula (`php`). This will then result in two different aliases claiming to point to the same formula (`php`).
@@ -17,50 +17,61 @@ class HomebrewDiagnostics {
This check only needs to be performed if the `shivammathur/php` tap is active. 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") let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") { if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
Log.info("The user does not appear to have tapped: shivammathur/php") Log.info("The user does not appear to have tapped: shivammathur/php")
return false return false
} else { } else {
Log.info("The user DOES have the following tapped: shivammathur/php") Log.info("The user DOES have the following tapped: shivammathur/php")
Log.info("Checking for `php` formula conflicts...") Log.info("Checking for `php` formula conflicts...")
let tapPhp = try! JSONDecoder().decode( let tapPhp = try! JSONDecoder().decode(
[HomebrewPackage].self, [HomebrewPackage].self,
from: tapAlias.data(using: .utf8)! from: tapAlias.data(using: .utf8)!
).first! ).first!
if tapPhp.version != PhpEnv.brewPhpVersion { 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...") Log.info("Determining whether both of these versions are installed...")
let bothInstalled = PhpEnv.shared.availablePhpVersions.contains(tapPhp.version) let bothInstalled = PhpEnv.shared.availablePhpVersions.contains(tapPhp.version)
&& PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) && PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion)
if bothInstalled { if bothInstalled {
Log.warn("Both conflicting aliases seem to be installed, warning the user!") Log.warn("Both conflicting aliases seem to be installed, warning the user!")
} else { } else {
Log.info("Conflicting aliases are not both installed, seems fine!") Log.info("Conflicting aliases are not both installed, seems fine!")
} }
return bothInstalled return bothInstalled
} }
Log.info("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).") Log.info("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
return false 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. 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. 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( let serviceInfo = try? JSONDecoder().decode(
[HomebrewService].self, [HomebrewService].self,
from: Shell.pipe( from: Shell.pipe(
@@ -68,7 +79,7 @@ class HomebrewDiagnostics {
requiresPath: true requiresPath: true
).data(using: .utf8)! ).data(using: .utf8)!
) )
return serviceInfo == nil return serviceInfo == nil
} }
} }

View File

@@ -9,32 +9,32 @@
import Foundation import Foundation
class NginxConfiguration { class NginxConfiguration {
/** Contents of the Nginx file in question, as a string. */ /** Contents of the Nginx file in question, as a string. */
var contents: String! var contents: String!
/** The name of the domain, usually derived from the name of the file. */ /** The name of the domain, usually derived from the name of the file. */
var domain: String var domain: String
/** The TLD of the domain, usually derived from the name of the file. */ /** The TLD of the domain, usually derived from the name of the file. */
var tld: String var tld: String
init(filePath: String) { init(filePath: String) {
let path = filePath.replacingOccurrences( let path = filePath.replacingOccurrences(
of: "~", of: "~",
with: "/Users/\(Paths.whoami)" with: "/Users/\(Paths.whoami)"
) )
self.contents = try! String(contentsOfFile: path) self.contents = try! String(contentsOfFile: path)
let domain = String(path.split(separator: "/").last!) let domain = String(path.split(separator: "/").last!)
let tld = String(domain.split(separator: ".").last!) let tld = String(domain.split(separator: ".").last!)
self.domain = domain self.domain = domain
.replacingOccurrences(of: ".\(tld)", with: "") .replacingOccurrences(of: ".\(tld)", with: "")
self.tld = tld self.tld = tld
} }
/** /**
Retrieves what address this domain is proxying. Retrieves what address this domain is proxying.
*/ */
@@ -43,13 +43,13 @@ class NginxConfiguration {
pattern: #"proxy_pass (?<proxy>.*:\d*);"#, pattern: #"proxy_pass (?<proxy>.*:\d*);"#,
options: [] 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 } else { return nil }
return contents[Range(match.range(withName: "proxy"), in: contents)!] return contents[Range(match.range(withName: "proxy"), in: contents)!]
}() }()
/** /**
Retrieves which isolated version is active for this domain (if applicable). 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))"#, pattern: #"(ISOLATED_PHP_VERSION=(php)?(@)?)((?<major>\d)(.)?(?<minor>\d))"#,
options: [] 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 } else { return nil }
let major: String = contents[Range(match.range(withName: "major"), in: contents)!], let major: String = contents[Range(match.range(withName: "major"), in: contents)!],
minor: String = contents[Range(match.range(withName: "minor"), in: contents)!] minor: String = contents[Range(match.range(withName: "minor"), in: contents)!]
return "\(major).\(minor)" return "\(major).\(minor)"
}() }()
} }

View File

@@ -9,19 +9,19 @@
import Foundation import Foundation
protocol DomainListable { protocol DomainListable {
func getListableName() -> String func getListableName() -> String
func getListableSecured() -> Bool func getListableSecured() -> Bool
func getListableAbsolutePath() -> String func getListableAbsolutePath() -> String
func getListablePhpVersion() -> String func getListablePhpVersion() -> String
func getListableKind() -> String func getListableKind() -> String
func getListableType() -> String func getListableType() -> String
func getListableUrl() -> URL? func getListableUrl() -> URL?
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
protocol ProxyScanner { protocol ProxyScanner {
func resolveProxies(directoryPath: String) -> [ValetProxy] func resolveProxies(directoryPath: String) -> [ValetProxy]
} }

View File

@@ -8,10 +8,8 @@
import Foundation import Foundation
class ValetProxyScanner: ProxyScanner class ValetProxyScanner: ProxyScanner {
{ func resolveProxies(directoryPath: String) -> [ValetProxy] {
func resolveProxies(directoryPath: String) -> [ValetProxy]
{
return try! FileManager return try! FileManager
.default .default
.contentsOfDirectory(atPath: directoryPath) .contentsOfDirectory(atPath: directoryPath)

View File

@@ -9,5 +9,5 @@
import Foundation import Foundation
extension ValetProxy { extension ValetProxy {
} }

View File

@@ -8,46 +8,45 @@
import Foundation import Foundation
class ValetProxy: DomainListable class ValetProxy: DomainListable {
{
var domain: String var domain: String
var tld: String var tld: String
var target: String var target: String
var secured: Bool = false var secured: Bool = false
init(_ configuration: NginxConfiguration) { init(_ configuration: NginxConfiguration) {
self.domain = configuration.domain self.domain = configuration.domain
self.tld = configuration.tld self.tld = configuration.tld
self.target = configuration.proxy! self.target = configuration.proxy!
self.secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key") self.secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
} }
// MARK: - DomainListable Protocol // MARK: - DomainListable Protocol
func getListableName() -> String { func getListableName() -> String {
return self.domain return self.domain
} }
func getListableSecured() -> Bool { func getListableSecured() -> Bool {
return self.secured return self.secured
} }
func getListableAbsolutePath() -> String { func getListableAbsolutePath() -> String {
return self.domain return self.domain
} }
func getListablePhpVersion() -> String { func getListablePhpVersion() -> String {
return "" return ""
} }
func getListableKind() -> String { func getListableKind() -> String {
return "proxy" return "proxy"
} }
func getListableType() -> String { func getListableType() -> String {
return "proxy" return "proxy"
} }
func getListableUrl() -> URL? { func getListableUrl() -> URL? {
return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)") return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)")
} }

View File

@@ -6,33 +6,35 @@
// Copyright © 2022 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
class FakeSiteScanner: SiteScanner class FakeSiteScanner: SiteScanner {
{
let fakes = [ let fakes = [
ValetSite(fakeWithName: "laravel", tld: "test", secure: true, path: "~/Code/laravel/framework", 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: "tailwind", tld: "test", secure: true,
ValetSite(fakeWithName: "forge", tld: "test", secure: true, path: "~/Code/laravel/forge", linked: 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, ValetSite(fakeWithName: "concord", tld: "test", secure: false,
path: "~/Code/concord", linked: true, driver: "Laravel (^8.0)", constraint: "^7.4", isolated: "7.4"), path: "~/Code/concord", linked: true, driver: "Laravel (^8.0)", constraint: "^7.4", isolated: "7.4"),
ValetSite(fakeWithName: "drupal", tld: "test", secure: false, ValetSite(fakeWithName: "drupal", tld: "test", secure: false,
path: "~/Sites/drupal", linked: false, driver: "Drupal", constraint: "^7.4", isolated: "7.4"), path: "~/Sites/drupal", linked: false, driver: "Drupal", constraint: "^7.4", isolated: "7.4"),
ValetSite(fakeWithName: "wordpress", tld: "test", secure: false, ValetSite(fakeWithName: "wordpress", tld: "test", secure: false,
path: "~/Sites/wordpress", linked: false, driver: "WordPress", constraint: "^7.4", isolated: "7.4") path: "~/Sites/wordpress", linked: false, driver: "WordPress", constraint: "^7.4", isolated: "7.4")
] ]
func resolveSiteCount(paths: [String]) -> Int { func resolveSiteCount(paths: [String]) -> Int {
return fakes.count return fakes.count
} }
func resolveSitesFrom(paths: [String]) -> [ValetSite] { func resolveSitesFrom(paths: [String]) -> [ValetSite] {
return fakes return fakes
} }
func resolveSite(path: String) -> ValetSite? { func resolveSite(path: String) -> ValetSite? {
return nil return nil
} }

View File

@@ -8,11 +8,10 @@
import Foundation import Foundation
protocol SiteScanner protocol SiteScanner {
{
func resolveSiteCount(paths: [String]) -> Int func resolveSiteCount(paths: [String]) -> Int
func resolveSitesFrom(paths: [String]) -> [ValetSite] func resolveSitesFrom(paths: [String]) -> [ValetSite]
func resolveSite(path: String) -> ValetSite? func resolveSite(path: String) -> ValetSite?
} }

View File

@@ -8,39 +8,38 @@
import Foundation import Foundation
class ValetSiteScanner: SiteScanner class ValetSiteScanner: SiteScanner {
{
func resolveSiteCount(paths: [String]) -> Int { func resolveSiteCount(paths: [String]) -> Int {
return paths.map { path in return paths.map { path in
let entries = try! FileManager.default let entries = try! FileManager.default
.contentsOfDirectory(atPath: path) .contentsOfDirectory(atPath: path)
return entries return entries
.map { self.isSite($0, forPath: path) } .map { self.isSite($0, forPath: path) }
.filter{ $0 == true} .filter { $0 == true}
.count .count
}.reduce(0, +) }.reduce(0, +)
} }
func resolveSitesFrom(paths: [String]) -> [ValetSite] { func resolveSitesFrom(paths: [String]) -> [ValetSite] {
var sites: [ValetSite] = [] var sites: [ValetSite] = []
paths.forEach { path in paths.forEach { path in
let entries = try! FileManager.default let entries = try! FileManager.default
.contentsOfDirectory(atPath: path) .contentsOfDirectory(atPath: path)
return entries.forEach { return entries.forEach {
if let site = self.resolveSite(path: "\(path)/\($0)") { if let site = self.resolveSite(path: "\(path)/\($0)") {
sites.append(site) sites.append(site)
} }
} }
} }
return sites return sites
} }
/** /**
Determines whether the site can be resolved as a symbolic link or as a directory. 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. 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? { func resolveSite(path: String) -> ValetSite? {
// Get the TLD from the global Valet object // Get the TLD from the global Valet object
let tld = Valet.shared.config.tld let tld = Valet.shared.config.tld
// See if the file is a symlink, if so, resolve it // See if the file is a symlink, if so, resolve it
guard let attrs = try? FileManager.default.attributesOfItem(atPath: path) else { guard let attrs = try? FileManager.default.attributesOfItem(atPath: path) else {
Log.warn("Could not parse the site: \(path), skipping!") Log.warn("Could not parse the site: \(path), skipping!")
return nil return nil
} }
// We can also determine whether the thing at the path is a directory, too // We can also determine whether the thing at the path is a directory, too
let type = attrs[FileAttributeKey.type] as! FileAttributeType let type = attrs[FileAttributeKey.type] as! FileAttributeType
// We should also check that we can interpret the path correctly // We should also check that we can interpret the path correctly
if URL(fileURLWithPath: path).lastPathComponent == "" { if URL(fileURLWithPath: path).lastPathComponent == "" {
Log.warn("Could not parse the site: \(path), skipping!") Log.warn("Could not parse the site: \(path), skipping!")
return nil return nil
} }
if type == FileAttributeType.typeSymbolicLink { if type == FileAttributeType.typeSymbolicLink {
return ValetSite(aliasPath: path, tld: tld) return ValetSite(aliasPath: path, tld: tld)
} else if type == FileAttributeType.typeDirectory { } else if type == FileAttributeType.typeDirectory {
return ValetSite(absolutePath: path, tld: tld) return ValetSite(absolutePath: path, tld: tld)
} }
return nil return nil
} }
/** /**
Determines whether the site can be resolved as a symbolic link or as a directory. 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. Regular files are ignored. Returns true if the path can be parsed.
*/ */
private func isSite(_ entry: String, forPath path: String) -> Bool { private func isSite(_ entry: String, forPath path: String) -> Bool {
let siteDir = path + "/" + entry let siteDir = path + "/" + entry
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir) let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
let type = attrs[FileAttributeKey.type] as! FileAttributeType let type = attrs[FileAttributeKey.type] as! FileAttributeType
if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory { if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory {
return true return true
} }
return false return false
} }
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
extension ValetSite { extension ValetSite {
convenience init( convenience init(
fakeWithName name: String, fakeWithName name: String,
tld: String, tld: String,
@@ -23,14 +23,14 @@ extension ValetSite {
self.init(name: name, tld: tld, absolutePath: path, aliasPath: nil, makeDeterminations: false) self.init(name: name, tld: tld, absolutePath: path, aliasPath: nil, makeDeterminations: false)
self.secured = secure self.secured = secure
self.composerPhp = constraint self.composerPhp = constraint
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|") self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
.map { string in .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)) .matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.count > 0 .isEmpty
}.contains(true) }.contains(true)
self.driver = driver self.driver = driver
self.driverDeterminedByComposer = true self.driverDeterminedByComposer = true
if linked { if linked {
@@ -40,5 +40,5 @@ extension ValetSite {
self.isolatedPhpVersion = PhpInstallation(isolated) self.isolatedPhpVersion = PhpInstallation(isolated)
} }
} }
} }

View File

@@ -9,63 +9,63 @@
import Foundation import Foundation
class ValetSite: DomainListable { class ValetSite: DomainListable {
/// Name of the site. Does not include the TLD. /// Name of the site. Does not include the TLD.
var name: String var name: String
/// The absolute path to the directory that is served. /// The absolute path to the directory that is served.
var absolutePath: String var absolutePath: String
/// The absolute path to the directory that is served, /// The absolute path to the directory that is served,
/// replacing the user's home folder with ~. /// replacing the user's home folder with ~.
lazy var absolutePathRelative: String = { lazy var absolutePathRelative: String = {
return self.absolutePath return self.absolutePath
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~") .replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
}() }()
/// The TLD used to locate this site. /// The TLD used to locate this site.
var tld: String = "test" var tld: String = "test"
/// The PHP version that is being used to serve this site specifically (if not global). /// The PHP version that is being used to serve this site specifically (if not global).
var isolatedPhpVersion: PhpInstallation? var isolatedPhpVersion: PhpInstallation?
/// Location of the alias. If set, this is a linked domain. /// Location of the alias. If set, this is a linked domain.
var aliasPath: String? var aliasPath: String?
/// Whether the site has been secured. /// Whether the site has been secured.
var secured: Bool! var secured: Bool!
/// What driver is currently in use. If not detected, defaults to nil. /// 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. /// Whether the driver was determined by checking the Composer file.
var driverDeterminedByComposer: Bool = false var driverDeterminedByComposer: Bool = false
/// A list of notable Composer dependencies. /// A list of notable Composer dependencies.
var notableComposerDependencies: [String: String] = [:] var notableComposerDependencies: [String: String] = [:]
/// The PHP version as discovered in `composer.json` or in .valetphprc. /// The PHP version as discovered in `composer.json` or in .valetphprc.
var composerPhp: String = "???" var composerPhp: String = "???"
/// Check whether the PHP version is valid for the currently linked version. /// Check whether the PHP version is valid for the currently linked version.
var composerPhpCompatibleWithLinked: Bool = false var composerPhpCompatibleWithLinked: Bool = false
/// How the PHP version was determined. /// How the PHP version was determined.
var composerPhpSource: VersionSource = .unknown var composerPhpSource: VersionSource = .unknown
/// Which version of PHP is actually used to serve this site. /// Which version of PHP is actually used to serve this site.
var servingPhpVersion: String { var servingPhpVersion: String {
return self.isolatedPhpVersion?.versionNumber.homebrewVersion return self.isolatedPhpVersion?.versionNumber.homebrewVersion
?? PhpEnv.phpInstall.version.short ?? PhpEnv.phpInstall.version.short
} }
enum VersionSource: String { enum VersionSource: String {
case unknown = "unknown" case unknown
case require = "require" case require
case platform = "platform" case platform
case valetphprc = "valetphprc" case valetphprc
} }
init( init(
name: String, name: String,
tld: String, tld: String,
@@ -78,7 +78,7 @@ class ValetSite: DomainListable {
self.absolutePath = absolutePath self.absolutePath = absolutePath
self.aliasPath = aliasPath self.aliasPath = aliasPath
self.secured = false self.secured = false
if makeDeterminations { if makeDeterminations {
determineSecured() determineSecured()
determineComposerPhpVersion() determineComposerPhpVersion()
@@ -86,25 +86,26 @@ class ValetSite: DomainListable {
determineIsolated() determineIsolated()
} }
} }
convenience init(absolutePath: String, tld: String) { convenience init(absolutePath: String, tld: String) {
let name = URL(fileURLWithPath: absolutePath).lastPathComponent let name = URL(fileURLWithPath: absolutePath).lastPathComponent
self.init(name: name, tld: tld, absolutePath: absolutePath) self.init(name: name, tld: tld, absolutePath: absolutePath)
} }
convenience init(aliasPath: String, tld: String) { convenience init(aliasPath: String, tld: String) {
let name = URL(fileURLWithPath: aliasPath).lastPathComponent let name = URL(fileURLWithPath: aliasPath).lastPathComponent
let absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath) let absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
self.init(name: name, tld: tld, absolutePath: absolutePath, aliasPath: aliasPath) self.init(name: name, tld: tld, absolutePath: absolutePath, aliasPath: aliasPath)
} }
/** /**
Determine whether a site is isolated. Determine whether a site is isolated.
*/ */
public func determineIsolated() { public func determineIsolated() {
if let version = ValetSite.isolatedVersion("~/.config/valet/Nginx/\(self.name).\(self.tld)") { if let version = ValetSite.isolatedVersion("~/.config/valet/Nginx/\(self.name).\(self.tld)") {
if (!PhpEnv.shared.cachedPhpInstallations.keys.contains(version)) { 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.") Log.err("The PHP version \(version) is isolated for the site \(self.name) "
+ "but that PHP version is unavailable.")
return return
} }
self.isolatedPhpVersion = PhpEnv.shared.cachedPhpInstallations[version] self.isolatedPhpVersion = PhpEnv.shared.cachedPhpInstallations[version]
@@ -112,7 +113,7 @@ class ValetSite: DomainListable {
self.isolatedPhpVersion = nil self.isolatedPhpVersion = nil
} }
} }
/** /**
Checks if a certificate file can be found in the `valet/Certificates` directory. Checks if a certificate file can be found in the `valet/Certificates` directory.
- Note: The file is not validated, only its presence is checked. - Note: The file is not validated, only its presence is checked.
@@ -120,7 +121,7 @@ class ValetSite: DomainListable {
public func determineSecured() { public func determineSecured() {
secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.name).\(self.tld).key") secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.name).\(self.tld).key")
} }
/** /**
Checks if `composer.json` exists in the folder, and extracts notable information: 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`). with the currently linked version of PHP (see `composerPhpMatchesSystem`).
*/ */
public func determineComposerPhpVersion() { public func determineComposerPhpVersion() {
self.determineComposerInformation() self.determineComposerInformation()
self.determineValetPhpFileInfo() self.determineValetPhpFileInfo()
if self.composerPhp == "???" { if self.composerPhp == "???" {
return return
} }
// Split the composer list (on "|") to evaluate multiple constraints // Split the composer list (on "|") to evaluate multiple constraints
// For example, for Laravel 8 projects the value is "^7.3|^8.0" // For example, for Laravel 8 projects the value is "^7.3|^8.0"
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|") self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
.map { string in .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)) .matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.count > 0 .isEmpty
}.contains(true) }.contains(true)
} }
/** /**
Determine the driver to be displayed in the list of sites. In v5.0, this has been changed 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. to load the "framework" or "project type" instead.
*/ */
public func determineDriver() { public func determineDriver() {
self.determineDriverViaComposer() self.determineDriverViaComposer()
if self.driver == nil { if self.driver == nil {
self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath) self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath)
} }
} }
/** /**
Check the dependency list and see if a particular dependency can't be found. 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. We'll revert the dependency list so that Laravel and Symfony are detected last.
@@ -171,28 +172,28 @@ class ValetSite: DomainListable {
*/ */
private func determineDriverViaComposer() { private func determineDriverViaComposer() {
self.driverDeterminedByComposer = true self.driverDeterminedByComposer = true
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
if self.notableComposerDependencies.keys.contains(key) { if self.notableComposerDependencies.keys.contains(key) {
self.driver = value self.driver = value
} }
} }
} }
/** /**
Checks the contents of the composer.json file and determine the notable dependencies, 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. as well as the requested PHP version. If no composer.json file is found, nothing happens.
*/ */
private func determineComposerInformation() { private func determineComposerInformation() {
let path = "\(absolutePath)/composer.json" let path = "\(absolutePath)/composer.json"
do { do {
if Filesystem.fileExists(path) { if Filesystem.fileExists(path) {
let decoded = try JSONDecoder().decode( let decoded = try JSONDecoder().decode(
ComposerJson.self, ComposerJson.self,
from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)! from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)!
) )
(self.composerPhp, self.composerPhpSource) = decoded.getPhpVersion() (self.composerPhp, self.composerPhpSource) = decoded.getPhpVersion()
self.notableComposerDependencies = decoded.getNotableDependencies() self.notableComposerDependencies = decoded.getNotableDependencies()
} }
@@ -200,13 +201,13 @@ class ValetSite: DomainListable {
Log.err("Something went wrong reading the Composer JSON file.") Log.err("Something went wrong reading the Composer JSON file.")
} }
} }
/** /**
Checks the contents of the .valetphprc file and determine the version, if possible. Checks the contents of the .valetphprc file and determine the version, if possible.
*/ */
private func determineValetPhpFileInfo() { private func determineValetPhpFileInfo() {
let path = "\(absolutePath)/.valetphprc" let path = "\(absolutePath)/.valetphprc"
do { do {
if Filesystem.fileExists(path) { if Filesystem.fileExists(path) {
let contents = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8) 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") Log.err("Something went wrong parsing the .valetphprc file")
} }
} }
// MARK: - File Parsing // MARK: - File Parsing
public static func isolatedVersion(_ filePath: String) -> String? { public static func isolatedVersion(_ filePath: String) -> String? {
if Filesystem.fileExists(filePath) { if Filesystem.fileExists(filePath) {
return NginxConfiguration return NginxConfiguration
.init(filePath: filePath) .init(filePath: filePath)
.isolatedVersion .isolatedVersion
} }
return nil return nil
} }
// MARK: - DomainListable Protocol // MARK: - DomainListable Protocol
func getListableName() -> String { func getListableName() -> String {
return self.name return self.name
} }
func getListableSecured() -> Bool { func getListableSecured() -> Bool {
return self.secured return self.secured
} }
func getListableAbsolutePath() -> String { func getListableAbsolutePath() -> String {
return self.absolutePath return self.absolutePath
} }
func getListablePhpVersion() -> String { func getListablePhpVersion() -> String {
return self.servingPhpVersion return self.servingPhpVersion
} }
func getListableKind() -> String { func getListableKind() -> String {
return (self.aliasPath == nil) ? "linked" : "parked" return (self.aliasPath == nil) ? "linked" : "parked"
} }
func getListableType() -> String { func getListableType() -> String {
return self.driver ?? "ZZZ" return self.driver ?? "ZZZ"
} }
func getListableUrl() -> URL? { func getListableUrl() -> URL? {
return URL(string: "\(self.secured ? "https://" : "http://")\(self.name).\(Valet.shared.config.tld)") return URL(string: "\(self.secured ? "https://" : "http://")\(self.name).\(Valet.shared.config.tld)")
} }

View File

@@ -9,32 +9,32 @@
import Foundation import Foundation
class Valet { class Valet {
enum FeatureFlag { enum FeatureFlag {
case isolatedSites, case isolatedSites,
supportForPhp56 supportForPhp56
} }
static let shared = Valet() static let shared = Valet()
/// The version of Valet that was detected. /// The version of Valet that was detected.
var version: String! = nil var version: String! = nil
/// The Valet configuration file. /// The Valet configuration file.
var config: Valet.Configuration! var config: Valet.Configuration!
/// A cached list of sites that were detected after analyzing the paths set up for Valet. /// A cached list of sites that were detected after analyzing the paths set up for Valet.
var sites: [ValetSite] = [] var sites: [ValetSite] = []
/// A cached list of proxies that were detecting after analyzing the Nginx paths. /// A cached list of proxies that were detecting after analyzing the Nginx paths.
var proxies: [ValetProxy] = [] var proxies: [ValetProxy] = []
/// Whether we're busy with some blocking operation. /// Whether we're busy with some blocking operation.
var isBusy: Bool = false var isBusy: Bool = false
/// Various feature flags. Enabled based on the installed Valet version. /// Various feature flags. Enabled based on the installed Valet version.
var features: [FeatureFlag] = [] var features: [FeatureFlag] = []
/// When initialising the Valet singleton, assume no sites or proxies loaded. /// When initialising the Valet singleton, assume no sites or proxies loaded.
/// We will load the version later. /// We will load the version later.
init() { init() {
@@ -42,7 +42,7 @@ class Valet {
self.sites = [] self.sites = []
self.proxies = [] self.proxies = []
} }
/** /**
If marketing mode is enabled, show a list of sites that are used for promotional screenshots. 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 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 { if ProcessInfo.processInfo.environment["PHPMON_MARKETING_MODE"] != nil {
return FakeSiteScanner() return FakeSiteScanner()
} }
return ValetSiteScanner() return ValetSiteScanner()
} }
static var proxyScanner: ProxyScanner { static var proxyScanner: ProxyScanner {
return ValetProxyScanner() return ValetProxyScanner()
} }
/** /**
Check if a particular feature is enabled. Check if a particular feature is enabled.
*/ */
public static func enabled(feature: FeatureFlag) -> Bool { public static func enabled(feature: FeatureFlag) -> Bool {
return self.shared.features.contains(feature) return self.shared.features.contains(feature)
} }
/**
Retrieve a list of all domains, including sites & proxies.
*/
public static func getDomainListable() -> [DomainListable] { public static func getDomainListable() -> [DomainListable] {
return self.shared.sites + self.shared.proxies 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. 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() { public func loadConfiguration() {
let file = FileManager.default.homeDirectoryForCurrentUser let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet/config.json") .appendingPathComponent(".config/valet/config.json")
do { do {
config = try JSONDecoder().decode( config = try JSONDecoder().decode(
Valet.Configuration.self, Valet.Configuration.self,
@@ -93,7 +111,7 @@ class Valet {
Log.err(error) Log.err(error)
} }
} }
/** /**
Starts the preload of sites, but only if the maximum amount of sites is 30. 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. 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!") 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. 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!) (We don't want to do duplicate or parallel work!)
*/ */
public func reloadSites() { public func reloadSites() {
loadConfiguration() loadConfiguration()
if (isBusy) { if isBusy {
return return
} }
resolvePaths() resolvePaths()
} }
/** /**
Depending on the version of Valet that is active, the feature set of PHP Monitor will change. 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 `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. 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 let isOlderThanVersionThree = version.versionCompare("3.0") == .orderedAscending
if isOlderThanVersionThree { if isOlderThanVersionThree {
self.features.append(.supportForPhp56) self.features.append(.supportForPhp56)
} else { } else {
@@ -142,16 +160,16 @@ class Valet {
self.features.append(.isolatedSites) self.features.append(.isolatedSites)
} }
} }
/** /**
Checks if the version of Valet is more recent than the minimum version required for PHP Monitor to function. 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 Should this procedure fail, the user will get an alert notifying them that the version of Valet they have
installed is not recent enough. installed is not recent enough.
*/ */
public func validateVersion() -> Void { public func validateVersion() {
// 1. Evaluate feature support // 1. Evaluate feature support
Valet.shared.evaluateFeatureSupport() Valet.shared.evaluateFeatureSupport()
// 2. Notify user if the version is too old // 2. Notify user if the version is too old
if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending { if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending {
let version = version let version = version
@@ -160,16 +178,20 @@ class Valet {
BetterAlert() BetterAlert()
.withInformation( .withInformation(
title: "alert.min_valet_version.title".localized, 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") .withPrimary(text: "OK")
.show() .show()
} }
} else { } 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. Returns a count of how many sites are linked and parked.
*/ */
@@ -177,19 +199,19 @@ class Valet {
return Self.siteScanner return Self.siteScanner
.resolveSiteCount(paths: config.paths) .resolveSiteCount(paths: config.paths)
} }
/** /**
Resolves all paths and creates linked or parked site instances that can be referenced later. Resolves all paths and creates linked or parked site instances that can be referenced later.
*/ */
private func resolvePaths() { private func resolvePaths() {
isBusy = true isBusy = true
sites = Self.siteScanner sites = Self.siteScanner
.resolveSitesFrom(paths: config.paths) .resolveSitesFrom(paths: config.paths)
.sorted { .sorted {
$0.absolutePath < $1.absolutePath $0.absolutePath < $1.absolutePath
} }
proxies = Self.proxyScanner proxies = Self.proxyScanner
.resolveProxies( .resolveProxies(
directoryPath: FileManager.default directoryPath: FileManager.default
@@ -197,32 +219,34 @@ class Valet {
.appendingPathComponent(".config/valet/Nginx") .appendingPathComponent(".config/valet/Nginx")
.path .path
) )
if let defaultPath = Valet.shared.config.defaultSite, if let defaultPath = Valet.shared.config.defaultSite,
let site = ValetSiteScanner().resolveSite(path: defaultPath) { let site = ValetSiteScanner().resolveSite(path: defaultPath) {
sites.insert(site, at: 0) sites.insert(site, at: 0)
} }
isBusy = false isBusy = false
} }
struct Configuration: Decodable { struct Configuration: Decodable {
/// Top level domain suffix. Usually "test" but can be set to something else. /// Top level domain suffix. Usually "test" but can be set to something else.
/// - Important: Does not include the actual dot. ("test", not ".test"!) /// - Important: Does not include the actual dot. ("test", not ".test"!)
let tld: String let tld: String
/// The paths that need to be checked. /// The paths that need to be checked.
let paths: [String] let paths: [String]
/// The loopback address. Optional. /// The loopback address. Optional.
let loopback: String? let loopback: String?
/// The default site that is served if the domain is not found. Optional. /// The default site that is served if the domain is not found. Optional.
let defaultSite: String? let defaultSite: String?
// swiftlint:disable nesting
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case tld, paths, loopback, defaultSite = "default" case tld, paths, loopback, defaultSite = "default"
} }
// swiftlint:enable nesting
} }
} }

View File

@@ -10,9 +10,9 @@ import Foundation
import Cocoa import Cocoa
class HeaderView: NSView, XibLoadable { class HeaderView: NSView, XibLoadable {
@IBOutlet weak var textField: NSTextField! @IBOutlet weak var textField: NSTextField!
static func asMenuItem(text: String) -> NSMenuItem { static func asMenuItem(text: String) -> NSMenuItem {
let view = Self.createFromXib() let view = Self.createFromXib()
view!.textField.stringValue = text.uppercased() view!.textField.stringValue = text.uppercased()
@@ -21,5 +21,5 @@ class HeaderView: NSView, XibLoadable {
item.target = self item.target = self
return item return item
} }
} }

View File

@@ -9,16 +9,16 @@
import Foundation import Foundation
extension MainMenu { extension MainMenu {
// MARK: - Nicer callbacks // MARK: - Nicer callbacks
enum AsyncBehaviour { enum AsyncBehaviour {
case setsBusyUI case setsBusyUI
case reloadsPhpInstallation case reloadsPhpInstallation
case updatesMenuBarContents case updatesMenuBarContents
case broadcastServicesUpdate case broadcastServicesUpdate
} }
/** /**
Attempts asynchronous execution of a callback that may throw an `Error`. Attempts asynchronous execution of a callback that may throw an `Error`.
While the callback is being executed, the UI will be marked as busy. While the callback is being executed, the UI will be marked as busy.
@@ -48,40 +48,40 @@ extension MainMenu {
if behaviours.contains(.reloadsPhpInstallation) { if behaviours.contains(.reloadsPhpInstallation) {
PhpEnv.shared.isBusy = true PhpEnv.shared.isBusy = true
} }
if behaviours.contains(.setsBusyUI) { if behaviours.contains(.setsBusyUI) {
setBusyImage() setBusyImage()
} }
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
var error: Error? = nil var error: Error?
do { try execute() } catch let e { error = e } do { try execute() } catch let e { error = e }
if behaviours.contains(.setsBusyUI) { if behaviours.contains(.setsBusyUI) {
PhpEnv.shared.isBusy = false PhpEnv.shared.isBusy = false
} }
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
if behaviours.contains(.reloadsPhpInstallation) { if behaviours.contains(.reloadsPhpInstallation) {
PhpEnv.shared.currentInstall = ActivePhpInstallation() PhpEnv.shared.currentInstall = ActivePhpInstallation()
} }
if behaviours.contains(.updatesMenuBarContents) { if behaviours.contains(.updatesMenuBarContents) {
updatePhpVersionInStatusBar() updatePhpVersionInStatusBar()
} else if behaviours.contains(.setsBusyUI) { } else if behaviours.contains(.setsBusyUI) {
refreshIcon() refreshIcon()
} }
if behaviours.contains(.broadcastServicesUpdate) { if behaviours.contains(.broadcastServicesUpdate) {
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil) NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
} }
error == nil ? success() : failure(error!) error == nil ? success() : failure(error!)
} }
} }
} }
func asyncWithBusyUI( func asyncWithBusyUI(
_ execute: @escaping () throws -> Void, _ execute: @escaping () throws -> Void,
completion: @escaping () -> Void = {} completion: @escaping () -> Void = {}
@@ -92,5 +92,5 @@ extension MainMenu {
completion() completion()
}, behaviours: [.setsBusyUI]) }, behaviours: [.setsBusyUI])
} }
} }

View File

@@ -10,15 +10,15 @@ import Foundation
import AppKit import AppKit
extension MainMenu { extension MainMenu {
@objc func fixMyValet() { @objc func fixMyValet() {
let previousVersion = PhpEnv.phpInstall.version.short let previousVersion = PhpEnv.phpInstall.version.short
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) { if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
presentAlertForMissingFormula() presentAlertForMissingFormula()
return return
} }
if !BetterAlert() if !BetterAlert()
.withInformation( .withInformation(
title: "alert.fix_my_valet.title".localized, title: "alert.fix_my_valet.title".localized,
@@ -26,12 +26,11 @@ extension MainMenu {
) )
.withPrimary(text: "alert.fix_my_valet.ok".localized) .withPrimary(text: "alert.fix_my_valet.ok".localized)
.withSecondary(text: "alert.fix_my_valet.cancel".localized) .withSecondary(text: "alert.fix_my_valet.cancel".localized)
.didSelectPrimary() .didSelectPrimary() {
{
Log.info("The user has chosen to abort Fix My Valet") Log.info("The user has chosen to abort Fix My Valet")
return return
} }
Actions.fixMyValet { Actions.fixMyValet {
DispatchQueue.main.async { DispatchQueue.main.async {
if previousVersion == PhpEnv.brewPhpVersion { if previousVersion == PhpEnv.brewPhpVersion {
@@ -42,7 +41,7 @@ extension MainMenu {
} }
} }
} }
private func presentAlertForMissingFormula() { private func presentAlertForMissingFormula() {
BetterAlert() BetterAlert()
.withInformation( .withInformation(
@@ -52,7 +51,7 @@ extension MainMenu {
.withPrimary(text: "OK") .withPrimary(text: "OK")
.show() .show()
} }
private func presentAlertForSameVersion() { private func presentAlertForSameVersion() {
BetterAlert() BetterAlert()
.withInformation( .withInformation(
@@ -63,7 +62,7 @@ extension MainMenu {
.withPrimary(text: "OK") .withPrimary(text: "OK")
.show() .show()
} }
private func presentAlertForDifferentVersion(version: String) { private func presentAlertForDifferentVersion(version: String) {
BetterAlert() BetterAlert()
.withInformation( .withInformation(
@@ -76,10 +75,10 @@ extension MainMenu {
MainMenu.shared.switchToPhpVersion(version) MainMenu.shared.switchToPhpVersion(version)
}) })
.withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnv.brewPhpVersion)) .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) NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
}) })
.show() .show()
} }
} }

View File

@@ -17,14 +17,14 @@ extension MainMenu {
DispatchQueue.main.async { DispatchQueue.main.async {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
} }
if await Startup().checkEnvironment() { if await Startup().checkEnvironment() {
self.onEnvironmentPass() self.onEnvironmentPass()
} else { } else {
self.onEnvironmentFail() self.onEnvironmentFail()
} }
} }
/** /**
When the environment is all clear and the app can run, let's go. 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 { if Valet.shared.version != nil {
Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)") 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) // Validate the version (this will enforce which versions of PHP are supported)
Valet.shared.validateVersion() Valet.shared.validateVersion()
// Actually detect the PHP versions // Actually detect the PHP versions
PhpEnv.detectPhpVersions() PhpEnv.detectPhpVersions()
// Check for an alias conflict // Check for an alias conflict
if HomebrewDiagnostics.hasAliasConflict() { if HomebrewDiagnostics.hasAliasConflict() {
DispatchQueue.main.async { HomebrewDiagnostics.presentAlertAboutConflict()
BetterAlert()
.withInformation(
title: "alert.php_alias_conflict.title".localized,
subtitle: "alert.php_alias_conflict.info".localized
)
.withPrimary(text: "OK")
.show()
}
} }
updatePhpVersionInStatusBar() updatePhpVersionInStatusBar()
Log.info("Determining broken PHP-FPM...") Log.info("Determining broken PHP-FPM...")
// Attempt to find out if PHP-FPM is broken // Attempt to find out if PHP-FPM is broken
let installation = PhpEnv.phpInstall let installation = PhpEnv.phpInstall
installation.notifyAboutBrokenPhpFpm() 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...") Log.info("Setting up watchers...")
App.shared.handlePhpConfigWatcher() App.shared.handlePhpConfigWatcher()
// Detect applications (preset + custom) // Detect applications (preset + custom)
Log.info("Detecting applications...") Log.info("Detecting applications...")
App.shared.detectedApplications = Application.detectPresetApplications() App.shared.detectedApplications = Application.detectPresetApplications()
let customApps = Preferences.custom.scanApps.map { appName in let customApps = Preferences.custom.scanApps.map { appName in
return Application(appName, .user_supplied) return Application(appName, .user_supplied)
}.filter { app in }.filter { app in
return app.isInstalled() return app.isInstalled()
} }
App.shared.detectedApplications.append(contentsOf: customApps) App.shared.detectedApplications.append(contentsOf: customApps)
let appNames = App.shared.detectedApplications.map { app in let appNames = App.shared.detectedApplications.map { app in
return app.name return app.name
} }
Log.info("Detected applications: \(appNames)") Log.info("Detected applications: \(appNames)")
// Load the global hotkey // Load the global hotkey
App.shared.loadGlobalHotkey() App.shared.loadGlobalHotkey()
// Preload sites // Preload sites
Valet.shared.startPreloadingSites() Valet.shared.startPreloadingSites()
// A non-default TLD is not officially supported since Valet 3.2.x // A non-default TLD is not officially supported since Valet 3.2.x
if (Valet.shared.config.tld != "test") { Valet.notifyAboutUnsupportedTLD()
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()
}
}
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil) NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
Log.info("PHP Monitor is ready to serve!") Log.info("PHP Monitor is ready to serve!")
// Schedule a request to fetch the PHP version every 60 seconds // Schedule a request to fetch the PHP version every 60 seconds
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
App.shared.timer = Timer.scheduledTimer( App.shared.timer = Timer.scheduledTimer(
@@ -109,17 +98,17 @@ extension MainMenu {
repeats: true repeats: true
) )
} }
Stats.incrementSuccessfulLaunchCount() Stats.incrementSuccessfulLaunchCount()
Stats.evaluateSponsorMessageShouldBeDisplayed() Stats.evaluateSponsorMessageShouldBeDisplayed()
} }
/** /**
When the environment is not OK, present an alert to inform the user. When the environment is not OK, present an alert to inform the user.
*/ */
private func onEnvironmentFail() { private func onEnvironmentFail() {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
BetterAlert() BetterAlert()
.withInformation( .withInformation(
title: "alert.cannot_start.title".localized, title: "alert.cannot_start.title".localized,
@@ -132,7 +121,7 @@ extension MainMenu {
exit(1) exit(1)
}) })
.show() .show()
Task { await startup() } Task { await startup() }
} }
} }

View File

@@ -9,34 +9,34 @@
import Foundation import Foundation
extension MainMenu { extension MainMenu {
// MARK: - PhpSwitcherDelegate // MARK: - PhpSwitcherDelegate
func switcherDidStartSwitching(to version: String) {} func switcherDidStartSwitching(to version: String) {}
func switcherDidCompleteSwitch(to version: String) { func switcherDidCompleteSwitch(to version: String) {
// Update the PHP version // Update the PHP version
PhpEnv.shared.currentInstall = ActivePhpInstallation() PhpEnv.shared.currentInstall = ActivePhpInstallation()
// Ensure the config watcher gets reloaded // Ensure the config watcher gets reloaded
App.shared.handlePhpConfigWatcher() App.shared.handlePhpConfigWatcher()
// Mark as no longer busy // Mark as no longer busy
PhpEnv.shared.isBusy = false PhpEnv.shared.isBusy = false
// Reload the site list // Reload the site list
self.reloadDomainListData() self.reloadDomainListData()
// Perform UI updates on main thread // Perform UI updates on main thread
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
updatePhpVersionInStatusBar() updatePhpVersionInStatusBar()
rebuild() rebuild()
if !PhpEnv.shared.validate(version) { if !PhpEnv.shared.validate(version) {
self.suggestFixMyValet(failed: version) self.suggestFixMyValet(failed: version)
return return
} }
// Run composer updates // Run composer updates
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) { if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
ComposerWindow().updateGlobalDependencies( ComposerWindow().updateGlobalDependencies(
@@ -45,17 +45,17 @@ extension MainMenu {
self.notifyAboutVersionChange(to: version) self.notifyAboutVersionChange(to: version)
} }
) )
} else { } else {
self.notifyAboutVersionChange(to: version) self.notifyAboutVersionChange(to: version)
} }
// Update stats // Update stats
Stats.incrementSuccessfulSwitchCount() Stats.incrementSuccessfulSwitchCount()
Stats.evaluateSponsorMessageShouldBeDisplayed() Stats.evaluateSponsorMessageShouldBeDisplayed()
} }
} }
@MainActor private func suggestFixMyValet(failed version: String) { @MainActor private func suggestFixMyValet(failed version: String) {
let outcome = BetterAlert() let outcome = BetterAlert()
.withInformation( .withInformation(
@@ -69,7 +69,7 @@ extension MainMenu {
MainMenu.shared.fixMyValet() MainMenu.shared.fixMyValet()
} }
} }
private func reloadDomainListData() { private func reloadDomainListData() {
if let window = App.shared.domainListWindowController { if let window = App.shared.domainListWindowController {
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -79,13 +79,13 @@ extension MainMenu {
Valet.shared.reloadSites() Valet.shared.reloadSites()
} }
} }
private func notifyAboutVersionChange(to version: String) { private func notifyAboutVersionChange(to version: String) {
LocalNotification.send( LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, version), title: String(format: "notification.version_changed_title".localized, version),
subtitle: String(format: "notification.version_changed_desc".localized, version) subtitle: String(format: "notification.version_changed_desc".localized, version)
) )
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm() PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
} }
} }

View File

@@ -10,18 +10,18 @@ import Cocoa
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate { class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
static let shared = MainMenu() static let shared = MainMenu()
weak var menuDelegate: NSMenuDelegate? = nil weak var menuDelegate: NSMenuDelegate?
/** /**
The status bar item with variable length. The status bar item with variable length.
*/ */
let statusItem = NSStatusBar.system.statusItem( let statusItem = NSStatusBar.system.statusItem(
withLength: NSStatusItem.variableLength withLength: NSStatusItem.variableLength
) )
// MARK: - UI related // MARK: - UI related
/** /**
Rebuilds the menu (either asynchronously or synchronously). Rebuilds the menu (either asynchronously or synchronously).
Defaults to rebuilding the menu asynchronously. Defaults to rebuilding the menu asynchronously.
@@ -31,13 +31,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
self.rebuildMenu() self.rebuildMenu()
return return
} }
// Update the menu item on the main thread // Update the menu item on the main thread
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
self.rebuildMenu() self.rebuildMenu()
} }
} }
/** /**
Update the menu's contents, based on what's going on. Update the menu's contents, based on what's going on.
This will rebuild the entire menu, so this can take a few moments. 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() { private func rebuildMenu() {
// Create a new menu // Create a new menu
let menu = StatusMenu() let menu = StatusMenu()
// Add the PHP versions (or error messages) // Add the PHP versions (or error messages)
menu.addPhpVersionMenuItems() menu.addPhpVersionMenuItems()
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
// Add the possible actions // Add the possible actions
menu.addPhpActionMenuItems() menu.addPhpActionMenuItems()
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
// Add Valet interactions // Add Valet interactions
menu.addValetMenuItems() menu.addValetMenuItems()
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
// Add services // Add services
menu.addRemainingMenuItems() menu.addRemainingMenuItems()
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
// Add about & quit menu items // Add about & quit menu items
menu.addCoreMenuItems() menu.addCoreMenuItems()
// Make sure every item can be interacted with // Make sure every item can be interacted with
menu.items.forEach({ (item) in menu.items.forEach({ (item) in
item.target = self item.target = self
}) })
statusItem.menu = menu statusItem.menu = menu
statusItem.menu?.delegate = self statusItem.menu?.delegate = self
} }
/** /**
Sets the status bar image based on a version string. Sets the status bar image based on a version string.
*/ */
@@ -86,7 +86,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
: MenuBarImageGenerator.textToImage(text: version) : MenuBarImageGenerator.textToImage(text: version)
) )
} }
/** /**
Sets the status bar image, based on the provided NSImage. Sets the status bar image, based on the provided NSImage.
The image will be used as a template image. The image will be used as a template image.
@@ -97,9 +97,9 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
button.image = image button.image = image
} }
} }
// MARK: - User Interface // MARK: - User Interface
/** Reloads which PHP versions is currently active. */ /** Reloads which PHP versions is currently active. */
@objc func refreshActiveInstallation() { @objc func refreshActiveInstallation() {
if !PhpEnv.shared.isBusy { if !PhpEnv.shared.isBusy {
@@ -109,13 +109,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
Log.perf("Skipping version refresh due to busy status") Log.perf("Skipping version refresh due to busy status")
} }
} }
/** Updates the icon (refresh icon) and rebuilds the menu. */ /** Updates the icon (refresh icon) and rebuilds the menu. */
@objc func updatePhpVersionInStatusBar() { @objc func updatePhpVersionInStatusBar() {
refreshIcon() refreshIcon()
rebuild() rebuild()
} }
/** /**
Reloads the menu in the foreground. Reloads the menu in the foreground.
This mimics the exact behaviours of `asyncExecution` as set in the method below. 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) rebuild(async: false)
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil) NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
} }
/** Reloads the menu in the background, using `asyncExecution`. */ /** Reloads the menu in the background, using `asyncExecution`. */
@objc func reloadPhpMonitorMenuInBackground() { @objc func reloadPhpMonitorMenuInBackground() {
asyncExecution({ asyncExecution({
@@ -139,11 +139,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
.updatesMenuBarContents .updatesMenuBarContents
]) ])
} }
/** Refreshes the icon with the PHP version. */ /** Refreshes the icon with the PHP version. */
@objc func refreshIcon() { @objc func refreshIcon() {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
if (PhpEnv.shared.isBusy) { if PhpEnv.shared.isBusy {
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
} else { } else {
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false { 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. */ /** Updates the icon to be displayed as busy. */
@objc func setBusyImage() { @objc func setBusyImage() {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
} }
} }
// MARK: - Actions // MARK: - Actions
@objc func fixHomebrewPermissions() { @objc func fixHomebrewPermissions() {
if !BetterAlert() if !BetterAlert()
.withInformation( .withInformation(
@@ -179,7 +179,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
.didSelectPrimary() { .didSelectPrimary() {
return return
} }
asyncExecution { asyncExecution {
try Actions.fixHomebrewPermissions() try Actions.fixHomebrewPermissions()
} success: { } success: {
@@ -195,13 +195,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
BetterAlert.show(for: error as! HomebrewPermissionError) BetterAlert.show(for: error as! HomebrewPermissionError)
} }
} }
@objc func restartPhpFpm() { @objc func restartPhpFpm() {
asyncExecution { asyncExecution {
Actions.restartPhpFpm() Actions.restartPhpFpm()
} }
} }
@objc func restartAllServices() { @objc func restartAllServices() {
asyncExecution { asyncExecution {
Actions.restartDnsMasq() Actions.restartDnsMasq()
@@ -216,7 +216,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
} }
} }
} }
@objc func stopAllServices() { @objc func stopAllServices() {
asyncExecution { asyncExecution {
Actions.stopAllServices() Actions.stopAllServices()
@@ -229,79 +229,79 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
} }
} }
} }
@objc func restartNginx() { @objc func restartNginx() {
asyncExecution { asyncExecution {
Actions.restartNginx() Actions.restartNginx()
} }
} }
@objc func restartDnsMasq() { @objc func restartDnsMasq() {
asyncExecution { asyncExecution {
Actions.restartDnsMasq() Actions.restartDnsMasq()
} }
} }
@objc func toggleXdebugMode(sender: XdebugMenuItem) { @objc func toggleXdebugMode(sender: XdebugMenuItem) {
Log.info("Switching Xdebug to mode: \(sender.mode)") Log.info("Switching Xdebug to mode: \(sender.mode)")
} }
@objc func toggleExtension(sender: ExtensionMenuItem) { @objc func toggleExtension(sender: ExtensionMenuItem) {
asyncExecution { asyncExecution {
sender.phpExtension?.toggle() sender.phpExtension?.toggle()
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) { if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
Actions.restartPhpFpm() Actions.restartPhpFpm()
} }
} }
} }
@objc func openPhpInfo() { @objc func openPhpInfo() {
var url: URL? = nil var url: URL?
asyncWithBusyUI { asyncWithBusyUI {
url = Actions.createTempPhpInfoFile() url = Actions.createTempPhpInfoFile()
} completion: { } completion: {
if url != nil { NSWorkspace.shared.open(url!) } if url != nil { NSWorkspace.shared.open(url!) }
} }
} }
@objc func updateGlobalComposerDependencies() { @objc func updateGlobalComposerDependencies() {
ComposerWindow().updateGlobalDependencies( ComposerWindow().updateGlobalDependencies(
notify: true, notify: true,
completion: { _ in } completion: { _ in }
) )
} }
@objc func openActiveConfigFolder() { @objc func openActiveConfigFolder() {
if (PhpEnv.phpInstall.version.error) { if PhpEnv.phpInstall.version.error {
// php version was not identified // php version was not identified
Actions.openGenericPhpConfigFolder() Actions.openGenericPhpConfigFolder()
return return
} }
// php version was identified // php version was identified
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short) Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
} }
@objc func openGlobalComposerFolder() { @objc func openGlobalComposerFolder() {
Actions.openGlobalComposerFolder() Actions.openGlobalComposerFolder()
} }
@objc func openValetConfigFolder() { @objc func openValetConfigFolder() {
Actions.openValetConfigFolder() Actions.openValetConfigFolder()
} }
@objc func switchToPhpVersion(sender: PhpMenuItem) { @objc func switchToPhpVersion(sender: PhpMenuItem) {
self.switchToPhpVersion(sender.version) self.switchToPhpVersion(sender.version)
} }
@objc func switchToPhpVersion(_ version: String) { @objc func switchToPhpVersion(_ version: String) {
setBusyImage() setBusyImage()
PhpEnv.shared.isBusy = true PhpEnv.shared.isBusy = true
PhpEnv.shared.delegate = self PhpEnv.shared.delegate = self
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version) PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
updatePhpVersionInStatusBar() updatePhpVersionInStatusBar()
rebuild() rebuild()
@@ -313,38 +313,38 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
) )
} }
} }
// MARK: - Menu Item Functionality // MARK: - Menu Item Functionality
@objc func openAbout() { @objc func openAbout() {
NSApplication.shared.activate(ignoringOtherApps: true) NSApplication.shared.activate(ignoringOtherApps: true)
NSApplication.shared.orderFrontStandardAboutPanel() NSApplication.shared.orderFrontStandardAboutPanel()
} }
@objc func openPrefs() { @objc func openPrefs() {
PrefsVC.show() PrefsVC.show()
} }
@objc func openDomainList() { @objc func openDomainList() {
DomainListVC.show() DomainListVC.show()
} }
@objc func openDonate() { @objc func openDonate() {
NSWorkspace.shared.open(Constants.Urls.DonationPage) NSWorkspace.shared.open(Constants.Urls.DonationPage)
} }
@objc func terminateApp() { @objc func terminateApp() {
NSApplication.shared.terminate(nil) NSApplication.shared.terminate(nil)
} }
// MARK: - Menu Delegate // MARK: - Menu Delegate
func menuWillOpen(_ menu: NSMenu) { func menuWillOpen(_ menu: NSMenu) {
// Make sure the shortcut key does not trigger this when the menu is open // Make sure the shortcut key does not trigger this when the menu is open
App.shared.shortcutHotkey?.isPaused = true App.shared.shortcutHotkey?.isPaused = true
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil) NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
} }
func menuDidClose(_ menu: NSMenu) { func menuDidClose(_ menu: NSMenu) {
// When the menu is closed, allow the shortcut to work again // When the menu is closed, allow the shortcut to work again
App.shared.shortcutHotkey?.isPaused = false App.shared.shortcutHotkey?.isPaused = false

View File

@@ -20,15 +20,15 @@ import Cocoa
service information should also not happen in a view. Yet here we are. service information should also not happen in a view. Yet here we are.
*/ */
class ServicesView: NSView, XibLoadable { class ServicesView: NSView, XibLoadable {
@IBOutlet weak var imageViewPhp: NSImageView! @IBOutlet weak var imageViewPhp: NSImageView!
@IBOutlet weak var imageViewNginx: NSImageView! @IBOutlet weak var imageViewNginx: NSImageView!
@IBOutlet weak var imageViewDnsmasq: NSImageView! @IBOutlet weak var imageViewDnsmasq: NSImageView!
@IBOutlet weak var textFieldPhp: NSTextField! @IBOutlet weak var textFieldPhp: NSTextField!
static var services: [String: HomebrewService] = [:] static var services: [String: HomebrewService] = [:]
static func asMenuItem() -> NSMenuItem { static func asMenuItem() -> NSMenuItem {
let view = Self.createFromXib()! let view = Self.createFromXib()!
[view.imageViewPhp, view.imageViewNginx, view.imageViewDnsmasq].forEach { imageView in [view.imageViewPhp, view.imageViewNginx, view.imageViewDnsmasq].forEach { imageView in
@@ -48,20 +48,20 @@ class ServicesView: NSView, XibLoadable {
@objc func updateInformation() { @objc func updateInformation() {
self.loadData() self.loadData()
} }
func loadData() { func loadData() {
self.applyAllInfoFieldsFromCachedValue() self.applyAllInfoFieldsFromCachedValue()
HomebrewService.loadAll { services in HomebrewService.loadAll { services in
ServicesView.services = Dictionary(uniqueKeysWithValues: services.map{ ($0.name, $0) }) ServicesView.services = Dictionary(uniqueKeysWithValues: services.map { ($0.name, $0) })
self.applyAllInfoFieldsFromCachedValue() self.applyAllInfoFieldsFromCachedValue()
} }
} }
func applyAllInfoFieldsFromCachedValue() { func applyAllInfoFieldsFromCachedValue() {
if ServicesView.services.keys.isEmpty { if ServicesView.services.keys.isEmpty {
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.textFieldPhp.stringValue = PhpEnv.phpInstall.formula.uppercased() self.textFieldPhp.stringValue = PhpEnv.phpInstall.formula.uppercased()
self.applyServiceStyling(PhpEnv.phpInstall.formula, self.imageViewPhp) self.applyServiceStyling(PhpEnv.phpInstall.formula, self.imageViewPhp)
@@ -69,24 +69,24 @@ class ServicesView: NSView, XibLoadable {
self.applyServiceStyling("dnsmasq", self.imageViewDnsmasq) self.applyServiceStyling("dnsmasq", self.imageViewDnsmasq)
} }
} }
func applyServiceStyling(_ serviceName: String, _ imageView: NSImageView) { func applyServiceStyling(_ serviceName: String, _ imageView: NSImageView) {
if ServicesView.services[serviceName] == nil { if ServicesView.services[serviceName] == nil {
imageView.image = NSImage(named: "ServiceLoading") imageView.image = NSImage(named: "ServiceLoading")
imageView.contentTintColor = NSColor(named: "IconColorNormal") imageView.contentTintColor = NSColor(named: "IconColorNormal")
return return
} }
if ServicesView.services[serviceName]!.running { if ServicesView.services[serviceName]!.running {
imageView.image = NSImage(named: "ServiceOn") imageView.image = NSImage(named: "ServiceOn")
imageView.contentTintColor = NSColor(named: "IconColorNormal") imageView.contentTintColor = NSColor(named: "IconColorNormal")
return return
} }
imageView.image = NSImage(named: "ServiceOff") imageView.image = NSImage(named: "ServiceOff")
imageView.contentTintColor = NSColor(named: "IconColorRed") imageView.contentTintColor = NSColor(named: "IconColorRed")
} }
deinit { deinit {
NotificationCenter.default.removeObserver(self, name: Events.ServicesUpdated, object: nil) NotificationCenter.default.removeObserver(self, name: Events.ServicesUpdated, object: nil)
} }

View File

@@ -10,15 +10,15 @@ import Foundation
import Cocoa import Cocoa
class StatsView: NSView, XibLoadable { class StatsView: NSView, XibLoadable {
@IBOutlet weak var titleMemLimit: NSTextField! @IBOutlet weak var titleMemLimit: NSTextField!
@IBOutlet weak var titleMaxPost: NSTextField! @IBOutlet weak var titleMaxPost: NSTextField!
@IBOutlet weak var titleMaxUpload: NSTextField! @IBOutlet weak var titleMaxUpload: NSTextField!
@IBOutlet weak var labelMemLimit: NSTextField! @IBOutlet weak var labelMemLimit: NSTextField!
@IBOutlet weak var labelMaxPost: NSTextField! @IBOutlet weak var labelMaxPost: NSTextField!
@IBOutlet weak var labelMaxUpload: NSTextField! @IBOutlet weak var labelMaxUpload: NSTextField!
static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem { static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem {
let view = Self.createFromXib() let view = Self.createFromXib()
view!.titleMemLimit.stringValue = "mi_memory_limit".localized.uppercased() view!.titleMemLimit.stringValue = "mi_memory_limit".localized.uppercased()
@@ -32,5 +32,5 @@ class StatsView: NSView, XibLoadable {
item.target = self item.target = self
return item return item
} }
} }

View File

@@ -7,8 +7,8 @@
import Cocoa import Cocoa
class StatusMenu : NSMenu { class StatusMenu: NSMenu {
func addPhpVersionMenuItems() { func addPhpVersionMenuItems() {
if PhpEnv.phpInstall.version.error { if PhpEnv.phpInstall.version.error {
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] { 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 return
} }
let phpVersionText = "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)" let phpVersionText = "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)"
addItem(HeaderView.asMenuItem(text: phpVersionText)) addItem(HeaderView.asMenuItem(text: phpVersionText))
} }
func addPhpActionMenuItems() { func addPhpActionMenuItems() {
if PhpEnv.shared.isBusy { if PhpEnv.shared.isBusy {
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: "")) addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
return return
} }
if PhpEnv.shared.availablePhpVersions.count == 0 { if PhpEnv.shared.availablePhpVersions.isEmpty {
return return
} }
self.addSwitchToPhpMenuItems() self.addSwitchToPhpMenuItems()
self.addItem(NSMenuItem.separator()) self.addItem(NSMenuItem.separator())
self.addItem(ServicesView.asMenuItem()) self.addItem(ServicesView.asMenuItem())
self.addItem(NSMenuItem.separator()) self.addItem(NSMenuItem.separator())
} }
func addValetMenuItems() { func addValetMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_valet".localized)) 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(
self.addItem(NSMenuItem(title: "mi_domain_list".localized, action: #selector(MainMenu.openDomainList), keyEquivalent: "l")) 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()) self.addItem(NSMenuItem.separator())
} }
func addRemainingMenuItems() { func addRemainingMenuItems() {
self.addConfigurationMenuItems() self.addConfigurationMenuItems()
self.addItem(NSMenuItem.separator()) self.addItem(NSMenuItem.separator())
self.addComposerMenuItems() self.addComposerMenuItems()
if (PhpEnv.shared.isBusy) { if PhpEnv.shared.isBusy {
return return
} }
self.addItem(NSMenuItem.separator()) self.addItem(NSMenuItem.separator())
self.addStatsMenuItem() self.addStatsMenuItem()
self.addItem(NSMenuItem.separator()) self.addItem(NSMenuItem.separator())
self.addExtensionsMenuItems() self.addExtensionsMenuItems()
self.addItem(NSMenuItem.separator()) self.addItem(NSMenuItem.separator())
// self.addXdebugMenuItem() // self.addXdebugMenuItem()
self.addFirstAidAndServicesMenuItems() self.addFirstAidAndServicesMenuItems()
} }
func addCoreMenuItems() { func addCoreMenuItems() {
self.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(MainMenu.openPrefs), keyEquivalent: ",")) self.addItem(
self.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(MainMenu.openAbout), keyEquivalent: "")) NSMenuItem(title: "mi_preferences".localized, action: #selector(MainMenu.openPrefs), keyEquivalent: ",")
self.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(MainMenu.terminateApp), keyEquivalent: "q")) )
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 // MARK: Remaining Menu Items
func addConfigurationMenuItems() { func addConfigurationMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized)) self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c")) self.addItem(
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i")) 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() { func addComposerMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized)) self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized))
self.addItem(NSMenuItem(title: "mi_global_composer".localized, action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g")) self.addItem(
NSMenuItem(title: "mi_global_composer".localized,
let composerMenuItem = NSMenuItem(title: "mi_update_global_composer".localized, action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies), keyEquivalent: "g") 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 composerMenuItem.keyEquivalentModifierMask = .shift
self.addItem(composerMenuItem) self.addItem(composerMenuItem)
} }
func addStatsMenuItem() { func addStatsMenuItem() {
guard let stats = PhpEnv.phpInstall.limits else { return } guard let stats = PhpEnv.phpInstall.limits else { return }
self.addItem(StatsView.asMenuItem( self.addItem(StatsView.asMenuItem(
memory: stats.memory_limit, memory: stats.memory_limit,
post: stats.post_max_size, post: stats.post_max_size,
upload: stats.upload_max_filesize) upload: stats.upload_max_filesize)
) )
} }
func addExtensionsMenuItems() { func addExtensionsMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized)) 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: "")) self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
} }
var shortcutKey = 1 var shortcutKey = 1
for phpExtension in PhpEnv.phpInstall.extensions { for phpExtension in PhpEnv.phpInstall.extensions {
self.addExtensionItem(phpExtension, shortcutKey) self.addExtensionItem(phpExtension, shortcutKey)
shortcutKey += 1 shortcutKey += 1
} }
} }
func addXdebugMenuItem() { func addXdebugMenuItem() {
if !Xdebug.enabled { if !Xdebug.enabled {
return return
} }
let xdebugSwitch = NSMenuItem( let xdebugSwitch = NSMenuItem(
title: "mi_xdebug_mode".localized, title: "mi_xdebug_mode".localized,
action: nil, action: nil,
@@ -131,7 +151,7 @@ class StatusMenu : NSMenu {
) )
let xdebugModesMenu = NSMenu() let xdebugModesMenu = NSMenu()
let xdebugMode = Xdebug.mode let xdebugMode = Xdebug.mode
for mode in Xdebug.modes { for mode in Xdebug.modes {
let item = XdebugMenuItem( let item = XdebugMenuItem(
title: mode, title: mode,
@@ -142,100 +162,119 @@ class StatusMenu : NSMenu {
item.mode = mode item.mode = mode
xdebugModesMenu.addItem(item) xdebugModesMenu.addItem(item)
} }
for item in xdebugModesMenu.items { for item in xdebugModesMenu.items {
item.target = MainMenu.shared item.target = MainMenu.shared
} }
self.setSubmenu(xdebugModesMenu, for: xdebugSwitch) self.setSubmenu(xdebugModesMenu, for: xdebugSwitch)
self.addItem(xdebugSwitch) self.addItem(xdebugSwitch)
} }
func addFirstAidAndServicesMenuItems() { func addFirstAidAndServicesMenuItems() {
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "") let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
let servicesMenu = NSMenu() let servicesMenu = NSMenu()
let fixMyValetMenuItem = NSMenuItem( let fixMyValetMenuItem = NSMenuItem(
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion), title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
action: #selector(MainMenu.fixMyValet), keyEquivalent: "" action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
) )
fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized
servicesMenu.addItem(fixMyValetMenuItem) servicesMenu.addItem(fixMyValetMenuItem)
let fixHomebrewMenuItem = NSMenuItem( let fixHomebrewMenuItem = NSMenuItem(
title: "mi_fix_brew_permissions".localized(), title: "mi_fix_brew_permissions".localized(),
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: "" action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: ""
) )
fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized
servicesMenu.addItem(fixHomebrewMenuItem) servicesMenu.addItem(fixHomebrewMenuItem)
servicesMenu.addItem(NSMenuItem.separator()) servicesMenu.addItem(NSMenuItem.separator())
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized)) 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( servicesMenu.addItem(
NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"), NSMenuItem(title: "mi_restart_dnsmasq".localized,
withKeyModifier: [.command, .shift]) 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(NSMenuItem.separator())
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized)) 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 { for item in servicesMenu.items {
item.target = MainMenu.shared item.target = MainMenu.shared
} }
self.setSubmenu(servicesMenu, for: services) self.setSubmenu(servicesMenu, for: services)
self.addItem(services) self.addItem(services)
} }
// MARK: Private Helpers // MARK: Private Helpers
private func addSwitchToPhpMenuItems() { private func addSwitchToPhpMenuItems() {
var shortcutKey = 1 var shortcutKey = 1
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() { for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
// Get the short and long version // Get the short and long version
let shortVersion = PhpEnv.shared.availablePhpVersions[index] let shortVersion = PhpEnv.shared.availablePhpVersions[index]
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
let versionString = long ? longVersion.toString() : shortVersion let versionString = long ? longVersion.toString() : shortVersion
let action = #selector(MainMenu.switchToPhpVersion(sender:)) let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)" let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
let menuItem = PhpMenuItem( let menuItem = PhpMenuItem(
title: "\("mi_php_switch".localized) \(versionString) (\(brew))", 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 menuItem.version = shortVersion
shortcutKey = shortcutKey + 1 shortcutKey += 1
self.addItem(menuItem) self.addItem(menuItem)
} }
} }
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) { private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : "" let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
let menuItem = ExtensionMenuItem( let menuItem = ExtensionMenuItem(
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))", title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
action: #selector(MainMenu.toggleExtension), action: #selector(MainMenu.toggleExtension),
keyEquivalent: keyEquivalent keyEquivalent: keyEquivalent
) )
if menuItem.keyEquivalent != "" { if menuItem.keyEquivalent != "" {
menuItem.keyEquivalentModifierMask = [.option] menuItem.keyEquivalentModifierMask = [.option]
} }
menuItem.state = phpExtension.enabled ? .on : .off menuItem.state = phpExtension.enabled ? .on : .off
menuItem.phpExtension = phpExtension menuItem.phpExtension = phpExtension
self.addItem(menuItem) self.addItem(menuItem)
} }
} }
@@ -251,9 +290,9 @@ class XdebugMenuItem: NSMenuItem {
} }
class ExtensionMenuItem: NSMenuItem { class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension? = nil var phpExtension: PhpExtension?
} }
class EditorMenuItem: NSMenuItem { class EditorMenuItem: NSMenuItem {
var editor: Application? = nil var editor: Application?
} }

View File

@@ -10,47 +10,47 @@ import Foundation
import Cocoa import Cocoa
class BetterAlert { class BetterAlert {
var windowController: NSWindowController! var windowController: NSWindowController!
var noticeVC: BetterAlertVC { var noticeVC: BetterAlertVC {
return self.windowController.contentViewController as! BetterAlertVC return self.windowController.contentViewController as! BetterAlertVC
} }
init() { init() {
let storyboard = NSStoryboard(name: "Main" , bundle : nil) let storyboard = NSStoryboard(name: "Main", bundle: nil)
self.windowController = storyboard.instantiateController( self.windowController = storyboard.instantiateController(
withIdentifier: "noticeWindow" withIdentifier: "noticeWindow"
) as? NSWindowController ) as? NSWindowController
} }
public static func make() -> BetterAlert { public static func make() -> BetterAlert {
return BetterAlert() return BetterAlert()
} }
public func withPrimary( public func withPrimary(
text: String, text: String,
action: @escaping (BetterAlertVC) -> Void = { action: @escaping (BetterAlertVC) -> Void = { vc in
vc in vc.close(with: .alertFirstButtonReturn) vc.close(with: .alertFirstButtonReturn)
} }
) -> Self { ) -> Self {
self.noticeVC.buttonPrimary.title = text self.noticeVC.buttonPrimary.title = text
self.noticeVC.actionPrimary = action self.noticeVC.actionPrimary = action
return self return self
} }
public func withSecondary( public func withSecondary(
text: String, text: String,
action: ((BetterAlertVC) -> Void)? = { action: ((BetterAlertVC) -> Void)? = { vc in
vc in vc.close(with: .alertSecondButtonReturn) vc.close(with: .alertSecondButtonReturn)
} }
) -> Self { ) -> Self {
self.noticeVC.buttonSecondary.title = text self.noticeVC.buttonSecondary.title = text
self.noticeVC.actionSecondary = action self.noticeVC.actionSecondary = action
return self return self
} }
public func withTertiary( public func withTertiary(
text: String = "", text: String = "",
action: ((BetterAlertVC) -> Void)? = nil action: ((BetterAlertVC) -> Void)? = nil
@@ -62,7 +62,7 @@ class BetterAlert {
self.noticeVC.actionTertiary = action self.noticeVC.actionTertiary = action
return self return self
} }
public func withInformation( public func withInformation(
title: String, title: String,
subtitle: String, subtitle: String,
@@ -71,15 +71,15 @@ class BetterAlert {
self.noticeVC.labelTitle.stringValue = title self.noticeVC.labelTitle.stringValue = title
self.noticeVC.labelSubtitle.stringValue = subtitle self.noticeVC.labelSubtitle.stringValue = subtitle
self.noticeVC.labelDescription.stringValue = description self.noticeVC.labelDescription.stringValue = description
// If the description is missing, handle the excess space and change the top margin // 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.labelDescription.isHidden = true
self.noticeVC.primaryButtonTopMargin.constant = 0 self.noticeVC.primaryButtonTopMargin.constant = 0
} }
return self return self
} }
/** /**
Shows the modal and returns a ModalResponse. Shows the modal and returns a ModalResponse.
If you wish to simply show the alert and disregard the outcome, use `show`. If you wish to simply show the alert and disregard the outcome, use `show`.
@@ -88,12 +88,12 @@ class BetterAlert {
if !Thread.isMainThread { if !Thread.isMainThread {
fatalError("You should always present alerts on the main thread!") fatalError("You should always present alerts on the main thread!")
} }
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
windowController.window?.makeKeyAndOrderFront(nil) windowController.window?.makeKeyAndOrderFront(nil)
return NSApplication.shared.runModal(for: windowController.window!) return NSApplication.shared.runModal(for: windowController.window!)
} }
/** Shows the modal and returns true if the user pressed the primary button. */ /** Shows the modal and returns true if the user pressed the primary button. */
public func didSelectPrimary() -> Bool { public func didSelectPrimary() -> Bool {
return self.runModal() == .alertFirstButtonReturn return self.runModal() == .alertFirstButtonReturn
@@ -105,7 +105,7 @@ class BetterAlert {
public func show() { public func show() {
_ = self.runModal() _ = self.runModal()
} }
/** /**
Shows the modal for a particular error. Shows the modal for a particular error.
*/ */

View File

@@ -10,30 +10,30 @@ import Foundation
import Cocoa import Cocoa
class BetterAlertVC: NSViewController { class BetterAlertVC: NSViewController {
// MARK: - Outlets // MARK: - Outlets
@IBOutlet weak var labelTitle: NSTextField! @IBOutlet weak var labelTitle: NSTextField!
@IBOutlet weak var labelSubtitle: NSTextField! @IBOutlet weak var labelSubtitle: NSTextField!
@IBOutlet weak var labelDescription: NSTextField! @IBOutlet weak var labelDescription: NSTextField!
@IBOutlet weak var buttonPrimary: NSButton! @IBOutlet weak var buttonPrimary: NSButton!
@IBOutlet weak var buttonSecondary: NSButton! @IBOutlet weak var buttonSecondary: NSButton!
@IBOutlet weak var buttonTertiary: NSButton! @IBOutlet weak var buttonTertiary: NSButton!
var actionPrimary: (BetterAlertVC) -> Void = { _ in } var actionPrimary: (BetterAlertVC) -> Void = { _ in }
var actionSecondary: ((BetterAlertVC) -> Void)? var actionSecondary: ((BetterAlertVC) -> Void)?
var actionTertiary: ((BetterAlertVC) -> Void)? var actionTertiary: ((BetterAlertVC) -> Void)?
@IBOutlet weak var imageView: NSImageView! @IBOutlet weak var imageView: NSImageView!
@IBOutlet weak var primaryButtonTopMargin: NSLayoutConstraint! @IBOutlet weak var primaryButtonTopMargin: NSLayoutConstraint!
// MARK: - Lifecycle // MARK: - Lifecycle
override func viewWillAppear() { override func viewWillAppear() {
imageView.image = NSApp.applicationIconImage imageView.image = NSApp.applicationIconImage
if actionSecondary == nil { if actionSecondary == nil {
buttonSecondary.isHidden = true buttonSecondary.isHidden = true
} }
@@ -41,22 +41,21 @@ class BetterAlertVC: NSViewController {
buttonTertiary.isHidden = true buttonTertiary.isHidden = true
} }
} }
override func viewDidAppear() { override func viewDidAppear() {
view.window?.makeFirstResponder(buttonPrimary) view.window?.makeFirstResponder(buttonPrimary)
} }
deinit { deinit {
Log.perf("A BetterAlert has been deinitialized.") Log.perf("A BetterAlert has been deinitialized.")
} }
// MARK: Outlet Actions // MARK: Outlet Actions
@IBAction func primaryButtonAction(_ sender: Any) { @IBAction func primaryButtonAction(_ sender: Any) {
self.actionPrimary(self) self.actionPrimary(self)
} }
@IBAction func secondaryButtonAction(_ sender: Any) { @IBAction func secondaryButtonAction(_ sender: Any) {
if self.actionSecondary != nil { if self.actionSecondary != nil {
self.actionSecondary!(self) self.actionSecondary!(self)
@@ -64,16 +63,16 @@ class BetterAlertVC: NSViewController {
self.close(with: .alertSecondButtonReturn) self.close(with: .alertSecondButtonReturn)
} }
} }
@IBAction func tertiaryButtonAction(_ sender: Any) { @IBAction func tertiaryButtonAction(_ sender: Any) {
if self.actionSecondary != nil { if self.actionSecondary != nil {
self.actionTertiary!(self) self.actionTertiary!(self)
} }
} }
public func close(with code: NSApplication.ModalResponse) { public func close(with code: NSApplication.ModalResponse) {
self.view.window?.close() self.view.window?.close()
NSApplication.shared.stopModal(withCode: code) NSApplication.shared.stopModal(withCode: code)
} }
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
extension ActivePhpInstallation { extension ActivePhpInstallation {
/** /**
It is always possible that the system configuration for PHP-FPM has not been set up for Valet. 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`. This can occur when a user manually installs a new PHP version, but does not run `valet install`.
@@ -32,5 +32,5 @@ extension ActivePhpInstallation {
} }
} }
} }
} }

View File

@@ -9,21 +9,21 @@
import Foundation import Foundation
struct GlobalKeybindPreference: Codable, CustomStringConvertible { struct GlobalKeybindPreference: Codable, CustomStringConvertible {
// MARK: - Internal variables // MARK: - Internal variables
let function : Bool let function: Bool
let control : Bool let control: Bool
let command : Bool let command: Bool
let shift : Bool let shift: Bool
let option : Bool let option: Bool
let capsLock : Bool let capsLock: Bool
let carbonFlags : UInt32 let carbonFlags: UInt32
let characters : String? let characters: String?
let keyCode : UInt32 let keyCode: UInt32
// MARK: - How the keybind is display in Preferences // MARK: - How the keybind is display in Preferences
var description: String { var description: String {
var stringBuilder = "" var stringBuilder = ""
if self.function { if self.function {
@@ -49,19 +49,19 @@ struct GlobalKeybindPreference: Codable, CustomStringConvertible {
} }
return "\(stringBuilder)" return "\(stringBuilder)"
} }
// MARK: - Persisting data to UserDefaults (as JSON) // MARK: - Persisting data to UserDefaults (as JSON)
public func toJson() -> String { public func toJson() -> String {
let jsonData = try! JSONEncoder().encode(self) let jsonData = try! JSONEncoder().encode(self)
return String(data: jsonData, encoding: .utf8)! return String(data: jsonData, encoding: .utf8)!
} }
public static func fromJson(_ string: String?) -> GlobalKeybindPreference? { public static func fromJson(_ string: String?) -> GlobalKeybindPreference? {
if string == nil { if string == nil {
return nil return nil
} }
if let jsonData = string!.data(using: .utf8) { if let jsonData = string!.data(using: .utf8) {
let decoder = JSONDecoder() let decoder = JSONDecoder()
do { do {

View File

@@ -39,24 +39,24 @@ enum InternalStats: String {
} }
class Preferences { class Preferences {
// MARK: - Singleton // MARK: - Singleton
static var shared = Preferences() static var shared = Preferences()
var customPreferences: CustomPrefs var customPreferences: CustomPrefs
var cachedPreferences: [PreferenceName: Any?] var cachedPreferences: [PreferenceName: Any?]
public init() { public init() {
Preferences.handleFirstTimeLaunch() Preferences.handleFirstTimeLaunch()
cachedPreferences = Self.cache() cachedPreferences = Self.cache()
customPreferences = CustomPrefs(scanApps: []) customPreferences = CustomPrefs(scanApps: [])
loadCustomPreferences() loadCustomPreferences()
} }
// MARK: - First Time Run // MARK: - First Time Run
/** /**
Note: macOS seems to cache plist values in memory as well as in files. 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 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.launchCount.rawValue: 0,
InternalStats.didSeeSponsorEncouragement.rawValue: false InternalStats.didSeeSponsorEncouragement.rawValue: false
]) ])
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) { if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
handleMigration() handleMigration()
return return
} }
Log.info("Saving first-time preferences!") Log.info("Saving first-time preferences!")
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue) UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
UserDefaults.standard.synchronize() UserDefaults.standard.synchronize()
} }
/** /**
Sometimes preferences will change, and a migration is required to take the user's previous preference 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 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() { static func handleMigration() {
// If the user chose the "no icon" option, migrate it over // If the user chose the "no icon" option, migrate it over
if ( if
UserDefaults.standard.value(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue) != nil && 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.") 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.set(MenuBarIcon.noIcon.rawValue, forKey: PreferenceName.iconTypeToDisplay.rawValue)
UserDefaults.standard.removeObject(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue) UserDefaults.standard.removeObject(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue)
} }
} }
// MARK: - API // MARK: - API
static var preferences: [PreferenceName: Any?] { static var preferences: [PreferenceName: Any?] {
return Self.shared.cachedPreferences return Self.shared.cachedPreferences
} }
static var custom: CustomPrefs { static var custom: CustomPrefs {
return Self.shared.customPreferences return Self.shared.customPreferences
} }
/** /**
Determine whether a particular preference is enabled. Determine whether a particular preference is enabled.
- Important: Requires the preference to have a corresponding boolean value, or a fatal error will be thrown. - 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!") fatalError("\(preference) is not a valid boolean preference!")
} }
} }
// MARK: - Internal Functionality // MARK: - Internal Functionality
private static func cache() -> [PreferenceName: Any] { private static func cache() -> [PreferenceName: Any] {
return [ return [
// Part 1: Always Booleans // Part 1: Always Booleans
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any, .shouldDisplayDynamicIcon: UserDefaults.standard.bool(
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any, forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any, .fullPhpVersionDynamicIcon: UserDefaults.standard.bool(
.autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any, forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
.allowProtocolForIntegrations: UserDefaults.standard.bool(forKey: PreferenceName.allowProtocolForIntegrations.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 // Part 2: Always Strings
.globalHotkey: UserDefaults.standard.string(forKey: PreferenceName.globalHotkey.rawValue) as Any, .globalHotkey: UserDefaults.standard.string(
.iconTypeToDisplay: UserDefaults.standard.string(forKey: PreferenceName.iconTypeToDisplay.rawValue) as Any, forKey: PreferenceName.globalHotkey.rawValue) as Any,
.iconTypeToDisplay: UserDefaults.standard.string(
forKey: PreferenceName.iconTypeToDisplay.rawValue) as Any
] ]
} }
static func update(_ preference: PreferenceName, value: Any?) { static func update(_ preference: PreferenceName, value: Any?) {
if (value == nil) { if value == nil {
UserDefaults.standard.removeObject(forKey: preference.rawValue) UserDefaults.standard.removeObject(forKey: preference.rawValue)
} else { } else {
UserDefaults.standard.setValue(value, forKey: preference.rawValue) UserDefaults.standard.setValue(value, forKey: preference.rawValue)
} }
UserDefaults.standard.synchronize() UserDefaults.standard.synchronize()
// Update the preferences cache in memory! // Update the preferences cache in memory!
Preferences.shared.cachedPreferences = Preferences.cache() Preferences.shared.cachedPreferences = Preferences.cache()
} }
// MARK: - Custom Preferences // MARK: - Custom Preferences
private func loadCustomPreferences() { private func loadCustomPreferences() {
let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json") let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json")
if Filesystem.fileExists(url.path) { if Filesystem.fileExists(url.path) {
@@ -171,7 +177,7 @@ class Preferences {
Log.info("There was no .phpmon.conf.json file to be loaded.") Log.info("There was no .phpmon.conf.json file to be loaded.")
} }
} }
private func loadCustomPreferencesFile(_ url: URL) { private func loadCustomPreferencesFile(_ url: URL) {
do { do {
customPreferences = try JSONDecoder().decode( customPreferences = try JSONDecoder().decode(
@@ -183,5 +189,5 @@ class Preferences {
Log.warn("The .phpmon.conf.json file seems to be missing or malformed.") Log.warn("The .phpmon.conf.json file seems to be missing or malformed.")
} }
} }
} }

View File

@@ -10,105 +10,133 @@ import Cocoa
import Carbon import Carbon
class PrefsVC: NSViewController { class PrefsVC: NSViewController {
// MARK: - Window Identifier // MARK: - Window Identifier
@IBOutlet weak var stackView: NSStackView! @IBOutlet weak var stackView: NSStackView!
// MARK: - Display // MARK: - Display
public static func create(delegate: NSWindowDelegate?) { public static func create(delegate: NSWindowDelegate?) {
let storyboard = NSStoryboard(name: "Main" , bundle : nil) let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController( let windowController = storyboard.instantiateController(
withIdentifier: "preferencesWindow" withIdentifier: "preferencesWindow"
) as! PrefsWC ) as! PrefsWC
windowController.window!.title = "prefs.title".localized windowController.window!.title = "prefs.title".localized
windowController.window!.subtitle = "prefs.subtitle".localized windowController.window!.subtitle = "prefs.subtitle".localized
windowController.window!.delegate = delegate windowController.window!.delegate = delegate
windowController.window!.styleMask = [.titled, .closable, .miniaturizable] windowController.window!.styleMask = [.titled, .closable, .miniaturizable]
windowController.window!.delegate = windowController windowController.window!.delegate = windowController
windowController.positionWindowInTopLeftCorner() windowController.positionWindowInTopLeftCorner()
App.shared.preferencesWindowController = windowController App.shared.preferencesWindowController = windowController
} }
public static func show(delegate: NSWindowDelegate? = nil) { public static func show(delegate: NSWindowDelegate? = nil) {
if (App.shared.preferencesWindowController == nil) { if App.shared.preferencesWindowController == nil {
Self.create(delegate: delegate) Self.create(delegate: delegate)
} }
App.shared.preferencesWindowController!.showWindow(self) App.shared.preferencesWindowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
// MARK: - Lifecycle // MARK: - Lifecycle
override func viewDidLoad() { override func viewDidLoad() {
[ [
CheckboxPreferenceView.make( getDynamicIconPreferenceView(),
sectionText: "prefs.dynamic_icon".localized, getIconOptionsPreferenceView(),
descriptionText: "prefs.dynamic_icon_desc".localized, getIconDensityPreferenceView(),
checkboxText: "prefs.dynamic_icon_title".localized, getAutoRestartPreferenceView(),
preference: .shouldDisplayDynamicIcon, getAutomaticComposerUpdatePreferenceView(),
action: { getShortcutPreferenceView(),
MainMenu.shared.refreshIcon() getIntegrationsPreferenceView()
}
),
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: {}
),
].forEach({ self.stackView.addArrangedSubview($0) }) ].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 // MARK: - Listening for hotkey delegate
var listeningForHotkeyView: HotkeyPreferenceView? = nil var listeningForHotkeyView: HotkeyPreferenceView?
override func viewWillDisappear() { override func viewWillDisappear() {
if listeningForHotkeyView !== nil { if listeningForHotkeyView !== nil {
listeningForHotkeyView = nil listeningForHotkeyView = nil
@@ -116,7 +144,7 @@ class PrefsVC: NSViewController {
} }
// MARK: - Deinitialization // MARK: - Deinitialization
deinit { deinit {
Log.perf("PrefsVC deallocated") Log.perf("PrefsVC deallocated")
} }

View File

@@ -14,18 +14,18 @@ struct Keys {
} }
class PrefsWC: PMWindowController { class PrefsWC: PMWindowController {
// MARK: - Window Identifier // MARK: - Window Identifier
override var windowName: String { override var windowName: String {
return "Preferences" return "Preferences"
} }
// MARK: - Key Interaction // MARK: - Key Interaction
override func keyDown(with event: NSEvent) { override func keyDown(with event: NSEvent) {
super.keyDown(with: event) super.keyDown(with: event)
if let vc = contentViewController as? PrefsVC { if let vc = contentViewController as? PrefsVC {
if vc.listeningForHotkeyView != nil { if vc.listeningForHotkeyView != nil {
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space { if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
@@ -37,5 +37,5 @@ class PrefsWC: PMWindowController {
} }
} }
} }
} }

View File

@@ -8,9 +8,9 @@
import Foundation import Foundation
import Cocoa import Cocoa
class Stats { class Stats {
/** /**
Keep track of how many times the app has been successfully launched. Keep track of how many times the app has been successfully launched.
@@ -23,7 +23,7 @@ class Stats {
forKey: InternalStats.launchCount.rawValue forKey: InternalStats.launchCount.rawValue
) )
} }
/** /**
Keep track of how many times the app has successfully switched Keep track of how many times the app has successfully switched
between different PHP versions. between different PHP versions.
@@ -37,7 +37,7 @@ class Stats {
forKey: InternalStats.switchCount.rawValue forKey: InternalStats.switchCount.rawValue
) )
} }
/** /**
Did the user see the sponsor encouragement / thank you message? Did the user see the sponsor encouragement / thank you message?
Annoying the user is the worst, so let's not show the message twice. Annoying the user is the worst, so let's not show the message twice.
@@ -47,7 +47,7 @@ class Stats {
forKey: InternalStats.didSeeSponsorEncouragement.rawValue forKey: InternalStats.didSeeSponsorEncouragement.rawValue
) )
} }
/** /**
Increment the successful launch count. This should only be Increment the successful launch count. This should only be
called when the user has not encountered ANY issues starting called when the user has not encountered ANY issues starting
@@ -59,7 +59,7 @@ class Stats {
forKey: InternalStats.launchCount.rawValue forKey: InternalStats.launchCount.rawValue
) )
} }
/** /**
Increment the successful switch count. Increment the successful switch count.
*/ */
@@ -69,7 +69,7 @@ class Stats {
forKey: InternalStats.switchCount.rawValue forKey: InternalStats.switchCount.rawValue
) )
} }
/** /**
Determine if the sponsor message should be displayed. Determine if the sponsor message should be displayed.
@@ -86,19 +86,20 @@ class Stats {
(see `didSeeSponsorEncouragement`) (see `didSeeSponsorEncouragement`)
*/ */
public static func evaluateSponsorMessageShouldBeDisplayed() { public static func evaluateSponsorMessageShouldBeDisplayed() {
if Bundle.main.bundleIdentifier?.contains("beta") ?? false { if Bundle.main.bundleIdentifier?.contains("beta") ?? false {
return Log.info("Sponsor messages never apply to beta builds.") return Log.info("Sponsor messages never apply to beta builds.")
} }
if Stats.didSeeSponsorEncouragement { if Stats.didSeeSponsorEncouragement {
return Log.info("Awesome, the user has already seen the sponsor message.") return Log.info("Awesome, the user has already seen the sponsor message.")
} }
if Stats.successfulLaunchCount < 7 && Stats.successfulSwitchCount < 40 { 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 { DispatchQueue.main.async {
let donate = BetterAlert() let donate = BetterAlert()
.withInformation( .withInformation(
@@ -117,9 +118,9 @@ class Stats {
Log.info("The user is an absolute badass for choosing this option. Thank you.") Log.info("The user is an absolute badass for choosing this option. Thank you.")
NSWorkspace.shared.open(Constants.Urls.DonationPayment) NSWorkspace.shared.open(Constants.Urls.DonationPayment)
} }
UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue) UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue)
} }
} }
} }

View File

@@ -6,26 +6,30 @@
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2021 Nico Verbruggen. All rights reserved.
// //
import Foundation
import Foundation import Foundation
import Cocoa import Cocoa
class CheckboxPreferenceView: NSView, XibLoadable { class CheckboxPreferenceView: NSView, XibLoadable {
@IBOutlet weak var labelSection: NSTextField! @IBOutlet weak var labelSection: NSTextField!
@IBOutlet weak var labelDescription: NSTextField! @IBOutlet weak var labelDescription: NSTextField!
@IBOutlet weak var buttonCheckbox: NSButton! @IBOutlet weak var buttonCheckbox: NSButton!
var action: (() -> Void)! var action: (() -> Void)!
var preference: PreferenceName! { var preference: PreferenceName! {
didSet { didSet {
self.buttonCheckbox.state = Preferences.isEnabled(self.preference) ? .on : .off 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()! let view = Self.createFromXib()!
view.labelSection.stringValue = sectionText view.labelSection.stringValue = sectionText
view.labelDescription.stringValue = descriptionText view.labelDescription.stringValue = descriptionText
@@ -34,10 +38,10 @@ class CheckboxPreferenceView: NSView, XibLoadable {
view.action = action view.action = action
return view return view
} }
@IBAction func toggled(_ sender: Any) { @IBAction func toggled(_ sender: Any) {
Preferences.update(self.preference, value: buttonCheckbox.state == .on) Preferences.update(self.preference, value: buttonCheckbox.state == .on)
self.action() self.action()
} }
} }

Some files were not shown because too many files have changed in this diff Show More