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