mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-12-21 11:10:08 +01:00
✅ Added linting
This commit is contained in:
15
.swiftlint.yml
Normal file
15
.swiftlint.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
disabled_rules:
|
||||||
|
- todo
|
||||||
|
- identifier_name
|
||||||
|
- force_try
|
||||||
|
- force_cast
|
||||||
|
|
||||||
|
opt_in_rules:
|
||||||
|
- empty_count
|
||||||
|
|
||||||
|
included:
|
||||||
|
- phpmon
|
||||||
|
- phpmon-tests
|
||||||
|
|
||||||
|
excluded:
|
||||||
|
- phpmon/Vendor
|
||||||
@@ -230,6 +230,7 @@
|
|||||||
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
|
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
|
||||||
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
|
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
|
||||||
C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
|
C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
|
||||||
|
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; };
|
||||||
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
|
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
|
||||||
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
|
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
|
||||||
C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */; };
|
C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */; };
|
||||||
@@ -405,6 +406,7 @@
|
|||||||
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
|
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
|
||||||
C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = "<group>"; };
|
C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = "<group>"; };
|
||||||
C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = "<group>"; };
|
C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = "<group>"; };
|
||||||
|
C4F5FBCC28218C93001065C5 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
|
||||||
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
|
C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
|
||||||
@@ -549,6 +551,7 @@
|
|||||||
C4E713562570150F00007428 /* SECURITY.md */,
|
C4E713562570150F00007428 /* SECURITY.md */,
|
||||||
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
|
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
|
||||||
54D9E0C027E4F5E9003B9AD9 /* LICENSE */,
|
54D9E0C027E4F5E9003B9AD9 /* LICENSE */,
|
||||||
|
C4F5FBCC28218C93001065C5 /* .swiftlint.yml */,
|
||||||
C4E713572570151400007428 /* docs */,
|
C4E713572570151400007428 /* docs */,
|
||||||
C41C1B3522B0097F00E7CF16 /* phpmon */,
|
C41C1B3522B0097F00E7CF16 /* phpmon */,
|
||||||
C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
|
C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
|
||||||
@@ -974,6 +977,7 @@
|
|||||||
C41C1B2F22B0097F00E7CF16 /* Sources */,
|
C41C1B2F22B0097F00E7CF16 /* Sources */,
|
||||||
C41C1B3022B0097F00E7CF16 /* Frameworks */,
|
C41C1B3022B0097F00E7CF16 /* Frameworks */,
|
||||||
C41C1B3122B0097F00E7CF16 /* Resources */,
|
C41C1B3122B0097F00E7CF16 /* Resources */,
|
||||||
|
C4F5FBCB28216985001065C5 /* Run `swiftlint` */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -1089,6 +1093,27 @@
|
|||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
C4F5FBCB28216985001065C5 /* Run `swiftlint` */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run `swiftlint`";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
C41C1B2F22B0097F00E7CF16 /* Sources */ = {
|
C41C1B2F22B0097F00E7CF16 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
@@ -1291,6 +1316,7 @@
|
|||||||
C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */,
|
C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */,
|
||||||
C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */,
|
C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */,
|
||||||
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
|
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
|
||||||
|
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */,
|
||||||
C40B24F227A310770018C7D2 /* Events.swift in Sources */,
|
C40B24F227A310770018C7D2 /* Events.swift in Sources */,
|
||||||
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
|
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
|
||||||
C4C0E8E027F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */,
|
C4C0E8E027F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */,
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ class CommandTest: XCTestCase {
|
|||||||
path: Paths.php,
|
path: Paths.php,
|
||||||
arguments: ["-v"]
|
arguments: ["-v"]
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssert(version.contains("(cli)"))
|
XCTAssert(version.contains("(cli)"))
|
||||||
XCTAssert(version.contains("NTS"))
|
XCTAssert(version.contains("NTS"))
|
||||||
XCTAssert(version.contains("built"))
|
XCTAssert(version.contains("built"))
|
||||||
XCTAssert(version.contains("Zend"))
|
XCTAssert(version.contains("Zend"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class HomebrewPackageTest: XCTestCase {
|
class HomebrewPackageTest: XCTestCase {
|
||||||
|
|
||||||
// - MARK: SYNTHETIC TESTS
|
// - MARK: SYNTHETIC TESTS
|
||||||
|
|
||||||
static var jsonBrewFile: URL {
|
static var jsonBrewFile: URL {
|
||||||
@@ -22,7 +22,7 @@ class HomebrewPackageTest: XCTestCase {
|
|||||||
let package = try! JSONDecoder().decode(
|
let package = try! JSONDecoder().decode(
|
||||||
[HomebrewPackage].self, from: json.data(using: .utf8)!
|
[HomebrewPackage].self, from: json.data(using: .utf8)!
|
||||||
).first!
|
).first!
|
||||||
|
|
||||||
XCTAssertEqual(package.name, "php")
|
XCTAssertEqual(package.name, "php")
|
||||||
XCTAssertEqual(package.full_name, "php")
|
XCTAssertEqual(package.full_name, "php")
|
||||||
XCTAssertEqual(package.aliases.first!, "php@8.0")
|
XCTAssertEqual(package.aliases.first!, "php@8.0")
|
||||||
@@ -30,23 +30,23 @@ class HomebrewPackageTest: XCTestCase {
|
|||||||
installed.version.starts(with: "8.0")
|
installed.version.starts(with: "8.0")
|
||||||
}), true)
|
}), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
static var jsonBrewServicesFile: URL {
|
static var jsonBrewServicesFile: URL {
|
||||||
return Bundle(for: Self.self)
|
return Bundle(for: Self.self)
|
||||||
.url(forResource: "brew-services", withExtension: "json")!
|
.url(forResource: "brew-services", withExtension: "json")!
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanParseServicesJson() throws {
|
func testCanParseServicesJson() throws {
|
||||||
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
|
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
|
||||||
let services = try! JSONDecoder().decode(
|
let services = try! JSONDecoder().decode(
|
||||||
[HomebrewService].self, from: json.data(using: .utf8)!
|
[HomebrewService].self, from: json.data(using: .utf8)!
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertGreaterThan(services.count, 0)
|
XCTAssertGreaterThan(services.count, 0)
|
||||||
XCTAssertEqual(services.first?.name, "dnsmasq")
|
XCTAssertEqual(services.first?.name, "dnsmasq")
|
||||||
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
|
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
|
||||||
}
|
}
|
||||||
|
|
||||||
// - MARK: LIVE TESTS
|
// - MARK: LIVE TESTS
|
||||||
|
|
||||||
/// This test requires that you have a valid Homebrew installation set up,
|
/// This test requires that you have a valid Homebrew installation set up,
|
||||||
@@ -63,13 +63,13 @@ class HomebrewPackageTest: XCTestCase {
|
|||||||
).filter({ service in
|
).filter({ service in
|
||||||
return ["php", "nginx", "dnsmasq"].contains(service.name)
|
return ["php", "nginx", "dnsmasq"].contains(service.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
XCTAssertTrue(services.contains(where: {$0.name == "php"} ))
|
XCTAssertTrue(services.contains(where: {$0.name == "php"}))
|
||||||
XCTAssertTrue(services.contains(where: {$0.name == "nginx"} ))
|
XCTAssertTrue(services.contains(where: {$0.name == "nginx"}))
|
||||||
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} ))
|
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"}))
|
||||||
XCTAssertEqual(services.count, 3)
|
XCTAssertEqual(services.count, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This test requires that you have a valid Homebrew installation set up,
|
/// This test requires that you have a valid Homebrew installation set up,
|
||||||
/// and requires the `php` formula to be installed.
|
/// and requires the `php` formula to be installed.
|
||||||
/// If this test fails, there is an issue with your Homebrew installation
|
/// If this test fails, there is an issue with your Homebrew installation
|
||||||
@@ -79,7 +79,7 @@ class HomebrewPackageTest: XCTestCase {
|
|||||||
[HomebrewPackage].self,
|
[HomebrewPackage].self,
|
||||||
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
|
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
|
||||||
).first!
|
).first!
|
||||||
|
|
||||||
XCTAssertTrue(package.name == "php")
|
XCTAssertTrue(package.name == "php")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,23 +9,23 @@
|
|||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class NginxConfigurationTest: XCTestCase {
|
class NginxConfigurationTest: XCTestCase {
|
||||||
|
|
||||||
static var regularUrl: URL {
|
static var regularUrl: URL {
|
||||||
return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")!
|
return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")!
|
||||||
}
|
}
|
||||||
|
|
||||||
static var isolatedUrl: URL {
|
static var isolatedUrl: URL {
|
||||||
return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")!
|
return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")!
|
||||||
}
|
}
|
||||||
|
|
||||||
static var proxyUrl: URL {
|
static var proxyUrl: URL {
|
||||||
return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")!
|
return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")!
|
||||||
}
|
}
|
||||||
|
|
||||||
static var secureProxyUrl: URL {
|
static var secureProxyUrl: URL {
|
||||||
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")!
|
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")!
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanDetermineSiteNameAndTld() throws {
|
func testCanDetermineSiteNameAndTld() throws {
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
"nginx-site",
|
"nginx-site",
|
||||||
@@ -36,32 +36,32 @@ class NginxConfigurationTest: XCTestCase {
|
|||||||
NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).tld
|
NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).tld
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanDetermineIsolation() throws {
|
func testCanDetermineIsolation() throws {
|
||||||
XCTAssertNil(
|
XCTAssertNil(
|
||||||
NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).isolatedVersion
|
NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path).isolatedVersion
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
"8.1",
|
"8.1",
|
||||||
NginxConfiguration(filePath: NginxConfigurationTest.isolatedUrl.path).isolatedVersion
|
NginxConfiguration(filePath: NginxConfigurationTest.isolatedUrl.path).isolatedVersion
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanDetermineProxy() throws {
|
func testCanDetermineProxy() throws {
|
||||||
let proxied = NginxConfiguration(filePath: NginxConfigurationTest.proxyUrl.path)
|
let proxied = NginxConfiguration(filePath: NginxConfigurationTest.proxyUrl.path)
|
||||||
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
|
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
|
||||||
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
||||||
|
|
||||||
let normal = NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path)
|
let normal = NginxConfiguration(filePath: NginxConfigurationTest.regularUrl.path)
|
||||||
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
|
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
|
||||||
XCTAssertEqual(nil, normal.proxy)
|
XCTAssertEqual(nil, normal.proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanDetermineSecuredProxy() throws {
|
func testCanDetermineSecuredProxy() throws {
|
||||||
let proxied = NginxConfiguration(filePath: NginxConfigurationTest.secureProxyUrl.path)
|
let proxied = NginxConfiguration(filePath: NginxConfigurationTest.secureProxyUrl.path)
|
||||||
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
|
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
|
||||||
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,24 +9,24 @@
|
|||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class PhpExtensionTest: XCTestCase {
|
class PhpExtensionTest: XCTestCase {
|
||||||
|
|
||||||
static var phpIniFileUrl: URL {
|
static var phpIniFileUrl: URL {
|
||||||
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
|
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanLoadExtension() throws {
|
func testCanLoadExtension() throws {
|
||||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||||
|
|
||||||
XCTAssertGreaterThan(extensions.count, 0)
|
XCTAssertGreaterThan(extensions.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testExtensionNameIsCorrect() throws {
|
func testExtensionNameIsCorrect() throws {
|
||||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||||
|
|
||||||
let extensionNames = extensions.map { (ext) -> String in
|
let extensionNames = extensions.map { (ext) -> String in
|
||||||
return ext.name
|
return ext.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// These 6 should be found
|
// These 6 should be found
|
||||||
XCTAssertTrue(extensionNames.contains("xdebug"))
|
XCTAssertTrue(extensionNames.contains("xdebug"))
|
||||||
XCTAssertTrue(extensionNames.contains("imagick"))
|
XCTAssertTrue(extensionNames.contains("imagick"))
|
||||||
@@ -34,41 +34,41 @@ class PhpExtensionTest: XCTestCase {
|
|||||||
XCTAssertTrue(extensionNames.contains("opcache"))
|
XCTAssertTrue(extensionNames.contains("opcache"))
|
||||||
XCTAssertTrue(extensionNames.contains("yaml"))
|
XCTAssertTrue(extensionNames.contains("yaml"))
|
||||||
XCTAssertTrue(extensionNames.contains("custom"))
|
XCTAssertTrue(extensionNames.contains("custom"))
|
||||||
|
|
||||||
XCTAssertFalse(extensionNames.contains("fake"))
|
XCTAssertFalse(extensionNames.contains("fake"))
|
||||||
XCTAssertFalse(extensionNames.contains("nice"))
|
XCTAssertFalse(extensionNames.contains("nice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testExtensionStatusIsCorrect() throws {
|
func testExtensionStatusIsCorrect() throws {
|
||||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||||
|
|
||||||
// xdebug should be enabled
|
// xdebug should be enabled
|
||||||
XCTAssertEqual(extensions[0].enabled, true)
|
XCTAssertEqual(extensions[0].enabled, true)
|
||||||
|
|
||||||
// imagick should be disabled
|
// imagick should be disabled
|
||||||
XCTAssertEqual(extensions[1].enabled, false)
|
XCTAssertEqual(extensions[1].enabled, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testToggleWorksAsExpected() throws {
|
func testToggleWorksAsExpected() throws {
|
||||||
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
||||||
let extensions = PhpExtension.load(from: destination)
|
let extensions = PhpExtension.load(from: destination)
|
||||||
XCTAssertEqual(extensions.count, 6)
|
XCTAssertEqual(extensions.count, 6)
|
||||||
|
|
||||||
// Try to disable xdebug (should be detected first)!
|
// Try to disable xdebug (should be detected first)!
|
||||||
let xdebug = extensions.first!
|
let xdebug = extensions.first!
|
||||||
XCTAssertTrue(xdebug.name == "xdebug")
|
XCTAssertTrue(xdebug.name == "xdebug")
|
||||||
XCTAssertEqual(xdebug.enabled, true)
|
XCTAssertEqual(xdebug.enabled, true)
|
||||||
xdebug.toggle()
|
xdebug.toggle()
|
||||||
XCTAssertEqual(xdebug.enabled, false)
|
XCTAssertEqual(xdebug.enabled, false)
|
||||||
|
|
||||||
// Check if the file contains the appropriate data
|
// Check if the file contains the appropriate data
|
||||||
let file = try! String(contentsOf: destination, encoding: .utf8)
|
let file = try! String(contentsOf: destination, encoding: .utf8)
|
||||||
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
|
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
|
||||||
|
|
||||||
// Make sure if we load the data again, it's disabled
|
// Make sure if we load the data again, it's disabled
|
||||||
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
|
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanRetrieveXdebugMode() throws {
|
func testCanRetrieveXdebugMode() throws {
|
||||||
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
|
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
|
||||||
XCTAssertEqual(value, "coverage")
|
XCTAssertEqual(value, "coverage")
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class ValetConfigurationTest: XCTestCase {
|
class ValetConfigurationTest: XCTestCase {
|
||||||
|
|
||||||
static var jsonConfigFileUrl: URL {
|
static var jsonConfigFileUrl: URL {
|
||||||
return Bundle(for: Self.self).url(
|
return Bundle(for: Self.self).url(
|
||||||
forResource: "valet-config",
|
forResource: "valet-config",
|
||||||
withExtension: "json"
|
withExtension: "json"
|
||||||
)!
|
)!
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanLoadConfigFile() throws {
|
func testCanLoadConfigFile() throws {
|
||||||
let json = try? String(
|
let json = try? String(
|
||||||
contentsOf: Self.jsonConfigFileUrl,
|
contentsOf: Self.jsonConfigFileUrl,
|
||||||
@@ -26,7 +26,7 @@ class ValetConfigurationTest: XCTestCase {
|
|||||||
Valet.Configuration.self,
|
Valet.Configuration.self,
|
||||||
from: json!.data(using: .utf8)!
|
from: json!.data(using: .utf8)!
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(config.tld, "test")
|
XCTAssertEqual(config.tld, "test")
|
||||||
XCTAssertEqual(config.paths, [
|
XCTAssertEqual(config.paths, [
|
||||||
"/Users/username/.config/valet/Sites",
|
"/Users/username/.config/valet/Sites",
|
||||||
@@ -35,5 +35,5 @@ class ValetConfigurationTest: XCTestCase {
|
|||||||
XCTAssertEqual(config.defaultSite, "/Users/username/default-site")
|
XCTAssertEqual(config.defaultSite, "/Users/username/default-site")
|
||||||
XCTAssertEqual(config.loopback, "127.0.0.1")
|
XCTAssertEqual(config.loopback, "127.0.0.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Utility {
|
class Utility {
|
||||||
|
|
||||||
public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? {
|
public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? {
|
||||||
if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) {
|
if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) {
|
||||||
let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
|
let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)")
|
let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try FileManager.default.copyItem(at: bundleURL, to: targetURL)
|
try FileManager.default.copyItem(at: bundleURL, to: targetURL)
|
||||||
return targetURL
|
return targetURL
|
||||||
@@ -22,7 +22,7 @@ class Utility {
|
|||||||
Log.err("Unable to copy file: \(error)")
|
Log.err("Unable to copy file: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class PhpVersionDetectionTest: XCTestCase {
|
|||||||
"php@5.6",
|
"php@5.6",
|
||||||
"php@5.4" // should be omitted, not supported
|
"php@5.4" // should be omitted, not supported
|
||||||
], checkBinaries: false, generateHelpers: false)
|
], checkBinaries: false, generateHelpers: false)
|
||||||
|
|
||||||
XCTAssertEqual(outcome, ["8.0", "7.0"])
|
XCTAssertEqual(outcome, ["8.0", "7.0"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
nil
|
nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPhpVersionNumberParse() throws {
|
func testPhpVersionNumberParse() throws {
|
||||||
XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
|
XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
|
||||||
XCTAssertTrue(error is VersionParseError)
|
XCTAssertTrue(error is VersionParseError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanCheckFixedConstraints() throws {
|
func testCanCheckFixedConstraints() throws {
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
@@ -51,7 +51,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.0"]).all
|
.make(from: ["7.0"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"])
|
.make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"])
|
||||||
@@ -59,7 +59,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.0.3"]).all
|
.make(from: ["7.0.3"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
@@ -67,7 +67,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.0"]).all
|
.make(from: ["7.0"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
@@ -76,7 +76,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
.make(from: []).all
|
.make(from: []).all
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanCheckCaretConstraints() throws {
|
func testCanCheckCaretConstraints() throws {
|
||||||
// 1. Imprecise checks
|
// 1. Imprecise checks
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
@@ -86,7 +86,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 2. Imprecise check with precise constraint (lenient AKA not strict)
|
// 2. Imprecise check with precise constraint (lenient AKA not strict)
|
||||||
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
|
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
@@ -96,7 +96,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 3. Imprecise check with precise constraint (strict mode)
|
// 3. Imprecise check with precise constraint (strict mode)
|
||||||
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
|
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
@@ -106,7 +106,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
|
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. Precise members and constraint all around
|
// 4. Precise members and constraint all around
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
@@ -115,7 +115,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 5. Precise members but imprecise constraint (strict mode)
|
// 5. Precise members but imprecise constraint (strict mode)
|
||||||
// In strict mode the constraint's patch version is assumed to be 0
|
// In strict mode the constraint's patch version is assumed to be 0
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
@@ -125,7 +125,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 6. Precise members but imprecise constraint (lenient mode)
|
// 6. Precise members but imprecise constraint (lenient mode)
|
||||||
// In lenient mode the constraint's patch version is assumed to be equal
|
// In lenient mode the constraint's patch version is assumed to be equal
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
@@ -136,7 +136,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanCheckTildeConstraints() throws {
|
func testCanCheckTildeConstraints() throws {
|
||||||
// 1. Imprecise checks
|
// 1. Imprecise checks
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
@@ -146,7 +146,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 2. Imprecise check with precise constraint (lenient AKA not strict)
|
// 2. Imprecise check with precise constraint (lenient AKA not strict)
|
||||||
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
|
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
@@ -159,7 +159,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.0"]).all
|
.make(from: ["7.0"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 3. Imprecise check with precise constraint (strict mode)
|
// 3. Imprecise check with precise constraint (strict mode)
|
||||||
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
|
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
@@ -172,7 +172,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: []).all
|
.make(from: []).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. Precise members and constraint all around
|
// 4. Precise members and constraint all around
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
@@ -183,7 +183,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.0.10"]).all
|
.make(from: ["7.0.10"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 5. Precise members but imprecise constraint (strict mode)
|
// 5. Precise members but imprecise constraint (strict mode)
|
||||||
// In strict mode the constraint's patch version is assumed to be 0.
|
// In strict mode the constraint's patch version is assumed to be 0.
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
@@ -193,7 +193,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// 6. Precise members but imprecise constraint (lenient mode)
|
// 6. Precise members but imprecise constraint (lenient mode)
|
||||||
// In lenient mode the constraint's patch version is assumed to be equal.
|
// In lenient mode the constraint's patch version is assumed to be equal.
|
||||||
// (Strictness does not make any difference here, but both should be tested.)
|
// (Strictness does not make any difference here, but both should be tested.)
|
||||||
@@ -205,7 +205,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanCheckGreaterThanOrEqualConstraints() throws {
|
func testCanCheckGreaterThanOrEqualConstraints() throws {
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
@@ -214,7 +214,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
@@ -222,7 +222,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
|
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
@@ -231,7 +231,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3"]).all
|
.make(from: ["7.4", "7.3"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
|
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
@@ -241,7 +241,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
.make(from: ["7.4", "7.3", "7.2"]).all
|
.make(from: ["7.4", "7.3", "7.2"]).all
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanCheckGreaterThanConstraints() throws {
|
func testCanCheckGreaterThanConstraints() throws {
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
@@ -250,7 +250,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
|
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
@@ -259,7 +259,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2"]).all
|
.make(from: ["7.4", "7.3", "7.2"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
@@ -268,7 +268,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.4", "7.3"]).all
|
.make(from: ["7.4", "7.3"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
|
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
|
||||||
@@ -277,7 +277,7 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.3.1", "7.2.9", "7.2"]).all
|
.make(from: ["7.3.1", "7.2.9", "7.2"]).all
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PhpVersionNumberCollection
|
PhpVersionNumberCollection
|
||||||
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
|
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ class ValetVersionExtractorTest: XCTestCase {
|
|||||||
let version = valet("--version", sudo: false)
|
let version = valet("--version", sudo: false)
|
||||||
XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3"))
|
XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ class VersionExtractorTest: XCTestCase {
|
|||||||
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
|
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
|
||||||
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
|
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testVersionComparison() {
|
func testVersionComparison() {
|
||||||
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
|
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
|
||||||
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
|
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
|
||||||
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)
|
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)
|
||||||
XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending)
|
XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,42 +9,37 @@ import Foundation
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
class Actions {
|
class Actions {
|
||||||
|
|
||||||
// MARK: - Services
|
// MARK: - Services
|
||||||
|
|
||||||
public static func restartPhpFpm()
|
public static func restartPhpFpm() {
|
||||||
{
|
|
||||||
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
|
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func restartNginx()
|
public static func restartNginx() {
|
||||||
{
|
|
||||||
brew("services restart nginx", sudo: true)
|
brew("services restart nginx", sudo: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func restartDnsMasq()
|
public static func restartDnsMasq() {
|
||||||
{
|
|
||||||
brew("services restart dnsmasq", sudo: true)
|
brew("services restart dnsmasq", sudo: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func stopAllServices()
|
public static func stopAllServices() {
|
||||||
{
|
|
||||||
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
|
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
|
||||||
brew("services stop nginx", sudo: true)
|
brew("services stop nginx", sudo: true)
|
||||||
brew("services stop dnsmasq", sudo: true)
|
brew("services stop dnsmasq", sudo: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func fixHomebrewPermissions() throws
|
public static func fixHomebrewPermissions() throws {
|
||||||
{
|
|
||||||
var servicesCommands = [
|
var servicesCommands = [
|
||||||
"\(Paths.brew) services stop nginx",
|
"\(Paths.brew) services stop nginx",
|
||||||
"\(Paths.brew) services stop dnsmasq",
|
"\(Paths.brew) services stop dnsmasq"
|
||||||
]
|
]
|
||||||
var cellarCommands = [
|
var cellarCommands = [
|
||||||
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
|
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
|
||||||
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq"
|
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq"
|
||||||
]
|
]
|
||||||
|
|
||||||
PhpEnv.shared.availablePhpVersions.forEach { version in
|
PhpEnv.shared.availablePhpVersions.forEach { version in
|
||||||
let formula = version == PhpEnv.brewPhpVersion
|
let formula = version == PhpEnv.brewPhpVersion
|
||||||
? "php"
|
? "php"
|
||||||
@@ -52,66 +47,61 @@ class Actions {
|
|||||||
servicesCommands.append("\(Paths.brew) services stop \(formula)")
|
servicesCommands.append("\(Paths.brew) services stop \(formula)")
|
||||||
cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
|
cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let script =
|
let script =
|
||||||
servicesCommands.joined(separator: " && ")
|
servicesCommands.joined(separator: " && ")
|
||||||
+ " && "
|
+ " && "
|
||||||
+ cellarCommands.joined(separator: " && ")
|
+ cellarCommands.joined(separator: " && ")
|
||||||
|
|
||||||
let appleScript = NSAppleScript(
|
let appleScript = NSAppleScript(
|
||||||
source: "do shell script \"\(script)\" with administrator privileges"
|
source: "do shell script \"\(script)\" with administrator privileges"
|
||||||
)
|
)
|
||||||
|
|
||||||
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
|
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
|
||||||
|
|
||||||
if (eventResult == nil) {
|
if eventResult == nil {
|
||||||
throw HomebrewPermissionError(kind: .applescriptNilError)
|
throw HomebrewPermissionError(kind: .applescriptNilError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Finding Config Files
|
// MARK: - Finding Config Files
|
||||||
|
|
||||||
public static func openGenericPhpConfigFolder()
|
public static func openGenericPhpConfigFolder() {
|
||||||
{
|
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]
|
||||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
|
|
||||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func openGlobalComposerFolder()
|
public static func openGlobalComposerFolder() {
|
||||||
{
|
|
||||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".composer/composer.json")
|
.appendingPathComponent(".composer/composer.json")
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func openPhpConfigFolder(version: String)
|
public static func openPhpConfigFolder(version: String) {
|
||||||
{
|
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
|
||||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
|
|
||||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func openValetConfigFolder()
|
public static func openValetConfigFolder() {
|
||||||
{
|
|
||||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".config/valet")
|
.appendingPathComponent(".config/valet")
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Other Actions
|
// MARK: - Other Actions
|
||||||
|
|
||||||
public static func createTempPhpInfoFile() -> URL
|
public static func createTempPhpInfoFile() -> URL {
|
||||||
{
|
|
||||||
// Write a file called `phpmon_phpinfo.php` to /tmp
|
// Write a file called `phpmon_phpinfo.php` to /tmp
|
||||||
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
// Tell php-cgi to run the PHP and output as an .html file
|
// Tell php-cgi to run the PHP and output as an .html file
|
||||||
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
||||||
|
|
||||||
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
|
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fix My Valet
|
// MARK: - Fix My Valet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Detects all currently available PHP versions,
|
Detects all currently available PHP versions,
|
||||||
and unlinks each and every one of them.
|
and unlinks each and every one of them.
|
||||||
@@ -124,8 +114,7 @@ class Actions {
|
|||||||
If this does not solve the issue, the user may need to install additional
|
If this does not solve the issue, the user may need to install additional
|
||||||
extensions and/or run `composer global update`.
|
extensions and/or run `composer global update`.
|
||||||
*/
|
*/
|
||||||
public static func fixMyValet(completed: @escaping () -> Void)
|
public static func fixMyValet(completed: @escaping () -> Void) {
|
||||||
{
|
|
||||||
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
|
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
|
||||||
brew("services restart dnsmasq", sudo: true)
|
brew("services restart dnsmasq", sudo: true)
|
||||||
brew("services restart php", sudo: true)
|
brew("services restart php", sudo: true)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
public class Command {
|
public class Command {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Immediately executes a command.
|
Immediately executes a command.
|
||||||
|
|
||||||
@@ -20,21 +20,21 @@ public class Command {
|
|||||||
let task = Process()
|
let task = Process()
|
||||||
task.launchPath = path
|
task.launchPath = path
|
||||||
task.arguments = arguments
|
task.arguments = arguments
|
||||||
|
|
||||||
let pipe = Pipe()
|
let pipe = Pipe()
|
||||||
task.standardOutput = pipe
|
task.standardOutput = pipe
|
||||||
task.launch()
|
task.launch()
|
||||||
|
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
|
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
|
||||||
|
|
||||||
if (trimNewlines) {
|
if trimNewlines {
|
||||||
return output.components(separatedBy: .newlines)
|
return output.components(separatedBy: .newlines)
|
||||||
.filter({ !$0.isEmpty })
|
.filter({ !$0.isEmpty })
|
||||||
.joined(separator: "\n")
|
.joined(separator: "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
struct Constants {
|
struct Constants {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The latest PHP version that is considered to be stable at the time of release.
|
* The latest PHP version that is considered to be stable at the time of release.
|
||||||
* This version number is currently not used (only as a default fallback).
|
* This version number is currently not used (only as a default fallback).
|
||||||
*/
|
*/
|
||||||
static let LatestStablePhpVersion = "8.1"
|
static let LatestStablePhpVersion = "8.1"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The minimum version of Valet that is recommended.
|
The minimum version of Valet that is recommended.
|
||||||
If the installed version is older, a notification will be shown
|
If the installed version is older, a notification will be shown
|
||||||
@@ -24,7 +24,7 @@ struct Constants {
|
|||||||
See also: https://github.com/laravel/valet/releases/tag/v2.16.2
|
See also: https://github.com/laravel/valet/releases/tag/v2.16.2
|
||||||
*/
|
*/
|
||||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The PHP versions supported by this application.
|
* The PHP versions supported by this application.
|
||||||
* Versions that do not appear in this array are omitted from the list.
|
* Versions that do not appear in this array are omitted from the list.
|
||||||
@@ -42,7 +42,7 @@ struct Constants {
|
|||||||
"7.4",
|
"7.4",
|
||||||
"8.0",
|
"8.0",
|
||||||
"8.1",
|
"8.1",
|
||||||
|
|
||||||
// ====================
|
// ====================
|
||||||
// EXPERIMENTAL SUPPORT
|
// EXPERIMENTAL SUPPORT
|
||||||
// ====================
|
// ====================
|
||||||
@@ -50,9 +50,9 @@ struct Constants {
|
|||||||
// dev release. In this case, that means that the version below is detected.
|
// dev release. In this case, that means that the version below is detected.
|
||||||
"8.2"
|
"8.2"
|
||||||
]
|
]
|
||||||
|
|
||||||
struct Urls {
|
struct Urls {
|
||||||
|
|
||||||
static let DonationPayment = URL(
|
static let DonationPayment = URL(
|
||||||
string: "https://nicoverbruggen.be/sponsor#pay-now"
|
string: "https://nicoverbruggen.be/sponsor#pay-now"
|
||||||
)!
|
)!
|
||||||
@@ -62,7 +62,7 @@ struct Constants {
|
|||||||
static let FrequentlyAskedQuestions = URL(
|
static let FrequentlyAskedQuestions = URL(
|
||||||
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
|
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Events {
|
class Events {
|
||||||
|
|
||||||
static let ServicesUpdated = Notification.Name("ServicesUpdated")
|
static let ServicesUpdated = Notification.Name("ServicesUpdated")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,28 +11,25 @@
|
|||||||
/**
|
/**
|
||||||
Runs a `valet` command. Defaults to running as superuser.
|
Runs a `valet` command. Defaults to running as superuser.
|
||||||
*/
|
*/
|
||||||
func valet(_ command: String, sudo: Bool = true) -> String
|
func valet(_ command: String, sudo: Bool = true) -> String {
|
||||||
{
|
|
||||||
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
|
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Runs a `brew` command. Can run as superuser.
|
Runs a `brew` command. Can run as superuser.
|
||||||
*/
|
*/
|
||||||
func brew(_ command: String, sudo: Bool = false)
|
func brew(_ command: String, sudo: Bool = false) {
|
||||||
{
|
|
||||||
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
|
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
|
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
|
||||||
*/
|
*/
|
||||||
func sed(file: String, original: String, replacement: String)
|
func sed(file: String, original: String, replacement: String) {
|
||||||
{
|
|
||||||
// Escape slashes (or `sed` won't work)
|
// Escape slashes (or `sed` won't work)
|
||||||
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
|
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
|
||||||
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
|
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
|
||||||
|
|
||||||
// Check if gsed exists; it is able to follow symlinks,
|
// Check if gsed exists; it is able to follow symlinks,
|
||||||
// which we want to do to toggle the extension
|
// which we want to do to toggle the extension
|
||||||
if Filesystem.fileExists("\(Paths.binPath)/gsed") {
|
if Filesystem.fileExists("\(Paths.binPath)/gsed") {
|
||||||
@@ -45,8 +42,7 @@ func sed(file: String, original: String, replacement: String)
|
|||||||
/**
|
/**
|
||||||
Uses `grep` to determine whether a particular query string can be found in a particular file.
|
Uses `grep` to determine whether a particular query string can be found in a particular file.
|
||||||
*/
|
*/
|
||||||
func grepContains(file: String, query: String) -> Bool
|
func grepContains(file: String, query: String) -> Bool {
|
||||||
{
|
|
||||||
return Shell.pipe("""
|
return Shell.pipe("""
|
||||||
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
|
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
|
||||||
""")
|
""")
|
||||||
|
|||||||
@@ -9,50 +9,50 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Log {
|
class Log {
|
||||||
|
|
||||||
static var shared = Log()
|
static var shared = Log()
|
||||||
|
|
||||||
enum Verbosity: Int {
|
enum Verbosity: Int {
|
||||||
case error = 1,
|
case error = 1,
|
||||||
warning = 2,
|
warning = 2,
|
||||||
info = 3,
|
info = 3,
|
||||||
performance = 4
|
performance = 4
|
||||||
|
|
||||||
public func isApplicable() -> Bool {
|
public func isApplicable() -> Bool {
|
||||||
return Log.shared.verbosity.rawValue >= self.rawValue
|
return Log.shared.verbosity.rawValue >= self.rawValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var verbosity: Verbosity = .warning
|
var verbosity: Verbosity = .warning
|
||||||
|
|
||||||
static func err(_ item: Any) {
|
static func err(_ item: Any) {
|
||||||
if Verbosity.error.isApplicable() {
|
if Verbosity.error.isApplicable() {
|
||||||
print("[E] \(item)")
|
print("[E] \(item)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func warn(_ item: Any) {
|
static func warn(_ item: Any) {
|
||||||
if Verbosity.warning.isApplicable() {
|
if Verbosity.warning.isApplicable() {
|
||||||
print("[W] \(item)")
|
print("[W] \(item)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func info(_ item: Any) {
|
static func info(_ item: Any) {
|
||||||
if Verbosity.info.isApplicable() {
|
if Verbosity.info.isApplicable() {
|
||||||
print("\(item)")
|
print("\(item)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func perf(_ item: Any) {
|
static func perf(_ item: Any) {
|
||||||
if Verbosity.performance.isApplicable() {
|
if Verbosity.performance.isApplicable() {
|
||||||
print("[P] \(item)")
|
print("[P] \(item)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func separator(as verbosity: Verbosity = .info) {
|
static func separator(as verbosity: Verbosity = .info) {
|
||||||
if verbosity.isApplicable() {
|
if verbosity.isApplicable() {
|
||||||
print("==================================")
|
print("==================================")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,71 +12,71 @@ import Foundation
|
|||||||
The path to the Homebrew directory and the user's name are fetched only once, at boot.
|
The path to the Homebrew directory and the user's name are fetched only once, at boot.
|
||||||
*/
|
*/
|
||||||
public class Paths {
|
public class Paths {
|
||||||
|
|
||||||
public static let shared = Paths()
|
public static let shared = Paths()
|
||||||
|
|
||||||
internal var baseDir: Paths.HomebrewDir
|
internal var baseDir: Paths.HomebrewDir
|
||||||
|
|
||||||
private var userName: String
|
private var userName: String
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
||||||
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
|
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
public func detectBinaryPaths() {
|
public func detectBinaryPaths() {
|
||||||
detectComposerBinary()
|
detectComposerBinary()
|
||||||
}
|
}
|
||||||
|
|
||||||
// - MARK: Binaries
|
// - MARK: Binaries
|
||||||
|
|
||||||
public static var valet: String {
|
public static var valet: String {
|
||||||
return "\(binPath)/valet"
|
return "\(binPath)/valet"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var brew: String {
|
public static var brew: String {
|
||||||
return "\(binPath)/brew"
|
return "\(binPath)/brew"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var php: String {
|
public static var php: String {
|
||||||
return "\(binPath)/php"
|
return "\(binPath)/php"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var phpConfig: String {
|
public static var phpConfig: String {
|
||||||
return "\(binPath)/php-config"
|
return "\(binPath)/php-config"
|
||||||
}
|
}
|
||||||
|
|
||||||
// - MARK: Detected Binaries
|
// - MARK: Detected Binaries
|
||||||
|
|
||||||
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
|
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
|
||||||
public static var composer: String? = nil
|
public static var composer: String?
|
||||||
|
|
||||||
// - MARK: Paths
|
// - MARK: Paths
|
||||||
|
|
||||||
public static var whoami: String {
|
public static var whoami: String {
|
||||||
return shared.userName
|
return shared.userName
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var cellarPath: String {
|
public static var cellarPath: String {
|
||||||
return "\(shared.baseDir.rawValue)/Cellar"
|
return "\(shared.baseDir.rawValue)/Cellar"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var binPath: String {
|
public static var binPath: String {
|
||||||
return "\(shared.baseDir.rawValue)/bin"
|
return "\(shared.baseDir.rawValue)/bin"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var optPath: String {
|
public static var optPath: String {
|
||||||
return "\(shared.baseDir.rawValue)/opt"
|
return "\(shared.baseDir.rawValue)/opt"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var etcPath: String {
|
public static var etcPath: String {
|
||||||
return "\(shared.baseDir.rawValue)/etc"
|
return "\(shared.baseDir.rawValue)/etc"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Flexible Binaries
|
// MARK: - Flexible Binaries
|
||||||
// (these can be in multiple locations, so we scan common places because)
|
// (these can be in multiple locations, so we scan common places because)
|
||||||
// (PHP Monitor will not use the user's own PATH)
|
// (PHP Monitor will not use the user's own PATH)
|
||||||
|
|
||||||
private func detectComposerBinary() {
|
private func detectComposerBinary() {
|
||||||
if Filesystem.fileExists("/usr/local/bin/composer") {
|
if Filesystem.fileExists("/usr/local/bin/composer") {
|
||||||
Paths.composer = "/usr/local/bin/composer"
|
Paths.composer = "/usr/local/bin/composer"
|
||||||
@@ -87,12 +87,12 @@ public class Paths {
|
|||||||
Log.warn("Composer was not found.")
|
Log.warn("Composer was not found.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Enum
|
// MARK: - Enum
|
||||||
|
|
||||||
public enum HomebrewDir: String {
|
public enum HomebrewDir: String {
|
||||||
case opt = "/opt/homebrew"
|
case opt = "/opt/homebrew"
|
||||||
case usr = "/usr/local"
|
case usr = "/usr/local"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Process {
|
extension Process {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When a process is running in the background, it can send content to standard
|
When a process is running in the background, it can send content to standard
|
||||||
output or standard error, just like it would in a terminal. Using `listen`
|
output or standard error, just like it would in a terminal. Using `listen`
|
||||||
@@ -22,10 +22,10 @@ extension Process {
|
|||||||
) {
|
) {
|
||||||
let outputPipe = Pipe()
|
let outputPipe = Pipe()
|
||||||
let errorPipe = Pipe()
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
self.standardOutput = outputPipe
|
self.standardOutput = outputPipe
|
||||||
self.standardError = errorPipe
|
self.standardError = errorPipe
|
||||||
|
|
||||||
[
|
[
|
||||||
(outputPipe, didReceiveStandardOutputData),
|
(outputPipe, didReceiveStandardOutputData),
|
||||||
(errorPipe, didReceiveStandardErrorData)
|
(errorPipe, didReceiveStandardErrorData)
|
||||||
@@ -35,15 +35,18 @@ extension Process {
|
|||||||
forName: NSNotification.Name.NSFileHandleDataAvailable,
|
forName: NSNotification.Name.NSFileHandleDataAvailable,
|
||||||
object: pipe.fileHandleForReading,
|
object: pipe.fileHandleForReading,
|
||||||
queue: nil
|
queue: nil
|
||||||
) { notification in
|
) { _ in
|
||||||
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
|
if let outputString = String(
|
||||||
|
data: pipe.fileHandleForReading.availableData,
|
||||||
|
encoding: String.Encoding.utf8
|
||||||
|
) {
|
||||||
callback(outputString)
|
callback(outputString)
|
||||||
}
|
}
|
||||||
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
After the process is done running, you'll want to stop listening.
|
After the process is done running, you'll want to stop listening.
|
||||||
*/
|
*/
|
||||||
@@ -55,5 +58,5 @@ extension Process {
|
|||||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,35 +8,35 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
public class Shell {
|
public class Shell {
|
||||||
|
|
||||||
// MARK: - Invoke static functions
|
// MARK: - Invoke static functions
|
||||||
|
|
||||||
public static func run(
|
public static func run(
|
||||||
_ command: String,
|
_ command: String,
|
||||||
requiresPath: Bool = false
|
requiresPath: Bool = false
|
||||||
) {
|
) {
|
||||||
Shell.user.run(command, requiresPath: requiresPath)
|
Shell.user.run(command, requiresPath: requiresPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func pipe(
|
public static func pipe(
|
||||||
_ command: String,
|
_ command: String,
|
||||||
requiresPath: Bool = false
|
requiresPath: Bool = false
|
||||||
) -> String {
|
) -> String {
|
||||||
return Shell.user.pipe(command, requiresPath: requiresPath)
|
return Shell.user.pipe(command, requiresPath: requiresPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Singleton
|
// MARK: - Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
We now require macOS 11, so no need to detect which terminal to use.
|
We now require macOS 11, so no need to detect which terminal to use.
|
||||||
*/
|
*/
|
||||||
public var shell: String = "/bin/sh"
|
public var shell: String = "/bin/sh"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Singleton to access a user shell (with --login)
|
Singleton to access a user shell (with --login)
|
||||||
*/
|
*/
|
||||||
public static let user = Shell()
|
public static let user = Shell()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Runs a shell command without using the output.
|
Runs a shell command without using the output.
|
||||||
Uses the default shell.
|
Uses the default shell.
|
||||||
@@ -51,7 +51,7 @@ public class Shell {
|
|||||||
// Equivalent of piping to /dev/null; don't do anything with the string
|
// Equivalent of piping to /dev/null; don't do anything with the string
|
||||||
_ = Shell.pipe(command, requiresPath: requiresPath)
|
_ = Shell.pipe(command, requiresPath: requiresPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Runs a shell command and returns the output.
|
Runs a shell command and returns the output.
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ public class Shell {
|
|||||||
)
|
)
|
||||||
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
|
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Runs the command and returns a `ShellOutput` object, which contains info about the process.
|
Runs the command and returns a `ShellOutput` object, which contains info about the process.
|
||||||
|
|
||||||
@@ -81,16 +81,16 @@ public class Shell {
|
|||||||
_ command: String,
|
_ command: String,
|
||||||
requiresPath: Bool = false
|
requiresPath: Bool = false
|
||||||
) -> Shell.Output {
|
) -> Shell.Output {
|
||||||
|
|
||||||
let outputPipe = Pipe()
|
let outputPipe = Pipe()
|
||||||
let errorPipe = Pipe()
|
let errorPipe = Pipe()
|
||||||
|
|
||||||
let task = self.createTask(for: command, requiresPath: requiresPath)
|
let task = self.createTask(for: command, requiresPath: requiresPath)
|
||||||
task.standardOutput = outputPipe
|
task.standardOutput = outputPipe
|
||||||
task.standardError = errorPipe
|
task.standardError = errorPipe
|
||||||
task.launch()
|
task.launch()
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
|
|
||||||
return Shell.Output(
|
return Shell.Output(
|
||||||
standardOutput: String(
|
standardOutput: String(
|
||||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||||
@@ -103,7 +103,7 @@ public class Shell {
|
|||||||
task: task
|
task: task
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates a new process with the correct PATH and shell.
|
Creates a new process with the correct PATH and shell.
|
||||||
*/
|
*/
|
||||||
@@ -111,19 +111,19 @@ public class Shell {
|
|||||||
let tailoredCommand = requiresPath
|
let tailoredCommand = requiresPath
|
||||||
? "export PATH=\(Paths.binPath):$PATH && \(command)"
|
? "export PATH=\(Paths.binPath):$PATH && \(command)"
|
||||||
: command
|
: command
|
||||||
|
|
||||||
let task = Process()
|
let task = Process()
|
||||||
task.launchPath = self.shell
|
task.launchPath = self.shell
|
||||||
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
|
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
|
||||||
|
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Output {
|
public class Output {
|
||||||
public let standardOutput: String
|
public let standardOutput: String
|
||||||
public let errorOutput: String
|
public let errorOutput: String
|
||||||
public let task: Process
|
public let task: Process
|
||||||
|
|
||||||
init(standardOutput: String,
|
init(standardOutput: String,
|
||||||
errorOutput: String,
|
errorOutput: String,
|
||||||
task: Process) {
|
task: Process) {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ struct HomebrewPermissionError: Error, AlertableError {
|
|||||||
enum Kind: String {
|
enum Kind: String {
|
||||||
case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
|
case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
|
||||||
}
|
}
|
||||||
|
|
||||||
let kind: Kind
|
let kind: Kind
|
||||||
|
|
||||||
func getErrorMessageKey() -> String {
|
func getErrorMessageKey() -> String {
|
||||||
return "alert.errors.\(self.kind.rawValue)"
|
return "alert.errors.\(self.kind.rawValue)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
extension Date {
|
extension Date {
|
||||||
|
|
||||||
func toString() -> String {
|
func toString() -> String {
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||||
return dateFormatter.string(from: self)
|
return dateFormatter.string(from: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,21 +9,21 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
extension NSMenu {
|
extension NSMenu {
|
||||||
|
|
||||||
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
|
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
|
||||||
newItem.keyEquivalentModifierMask = modifier
|
newItem.keyEquivalentModifierMask = modifier
|
||||||
self.addItem(newItem)
|
self.addItem(newItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBDesignable class LocalizedMenuItem: NSMenuItem {
|
@IBDesignable class LocalizedMenuItem: NSMenuItem {
|
||||||
|
|
||||||
@IBInspectable
|
@IBInspectable
|
||||||
var localizationKey: String? {
|
var localizationKey: String? {
|
||||||
didSet {
|
didSet {
|
||||||
self.title = localizationKey?.localized ?? self.title
|
self.title = localizationKey?.localized ?? self.title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,29 +10,29 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
extension NSWindow {
|
extension NSWindow {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
|
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
|
||||||
*/
|
*/
|
||||||
func shake(){
|
func shake() {
|
||||||
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
|
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
|
||||||
|
|
||||||
let frame: CGRect = self.frame
|
let frame: CGRect = self.frame
|
||||||
let shakeAnimation :CAKeyframeAnimation = CAKeyframeAnimation()
|
let shakeAnimation: CAKeyframeAnimation = CAKeyframeAnimation()
|
||||||
|
|
||||||
let shakePath = CGMutablePath()
|
let shakePath = CGMutablePath()
|
||||||
shakePath.move( to: CGPoint(x:NSMinX(frame), y:NSMinY(frame)))
|
shakePath.move( to: CGPoint(x: frame.minX, y: frame.minY))
|
||||||
|
|
||||||
for _ in 0...numberOfShakes-1 {
|
for _ in 0...numberOfShakes-1 {
|
||||||
shakePath.addLine(to: CGPoint(x:NSMinX(frame) - frame.size.width * vigourOfShake, y:NSMinY(frame)))
|
shakePath.addLine(to: CGPoint(x: frame.minX - frame.size.width * vigourOfShake, y: frame.minY))
|
||||||
shakePath.addLine(to: CGPoint(x:NSMinX(frame) + frame.size.width * vigourOfShake, y:NSMinY(frame)))
|
shakePath.addLine(to: CGPoint(x: frame.minX + frame.size.width * vigourOfShake, y: frame.minY))
|
||||||
}
|
}
|
||||||
|
|
||||||
shakePath.closeSubpath()
|
shakePath.closeSubpath()
|
||||||
shakeAnimation.path = shakePath
|
shakeAnimation.path = shakePath
|
||||||
shakeAnimation.duration = durationOfShake
|
shakeAnimation.duration = durationOfShake
|
||||||
|
|
||||||
self.animations = ["frameOrigin":shakeAnimation]
|
self.animations = ["frameOrigin": shakeAnimation]
|
||||||
self.animator().setFrameOrigin(self.frame.origin)
|
self.animator().setFrameOrigin(self.frame.origin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,28 +7,28 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|
||||||
var localized: String {
|
var localized: String {
|
||||||
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
|
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func localized(_ args: CVarArg...) -> String {
|
func localized(_ args: CVarArg...) -> String {
|
||||||
String(format: self.localized, arguments: args)
|
String(format: self.localized, arguments: args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func countInstances(of stringToFind: String) -> Int {
|
func countInstances(of stringToFind: String) -> Int {
|
||||||
if (stringToFind.isEmpty) {
|
if stringToFind.isEmpty {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = 0
|
var count = 0
|
||||||
var searchRange: Range<String.Index>?
|
var searchRange: Range<String.Index>?
|
||||||
|
|
||||||
while let foundRange = range(of: stringToFind, options: [], range: searchRange) {
|
while let foundRange = range(of: stringToFind, options: [], range: searchRange) {
|
||||||
count += 1
|
count += 1
|
||||||
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
|
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ extension String {
|
|||||||
let end = r.upperBound
|
let end = r.upperBound
|
||||||
return String(self[start ..< end])
|
return String(self[start ..< end])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
|
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
|
||||||
/*
|
/*
|
||||||
<1> We split the version by period (.).
|
<1> We split the version by period (.).
|
||||||
@@ -50,12 +50,12 @@ extension String {
|
|||||||
*/
|
*/
|
||||||
func versionCompare(_ otherVersion: String) -> ComparisonResult {
|
func versionCompare(_ otherVersion: String) -> ComparisonResult {
|
||||||
let versionDelimiter = "."
|
let versionDelimiter = "."
|
||||||
|
|
||||||
var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
|
var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
|
||||||
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
|
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
|
||||||
|
|
||||||
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
|
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
|
||||||
|
|
||||||
if zeroDiff == 0 { // <3>
|
if zeroDiff == 0 { // <3>
|
||||||
// Same format, compare normally
|
// Same format, compare normally
|
||||||
return self.compare(otherVersion, options: .numeric)
|
return self.compare(otherVersion, options: .numeric)
|
||||||
@@ -70,5 +70,5 @@ extension String {
|
|||||||
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
|
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,25 +12,25 @@ import Cocoa
|
|||||||
// Adapted from: https://stackoverflow.com/a/46268778
|
// Adapted from: https://stackoverflow.com/a/46268778
|
||||||
|
|
||||||
protocol XibLoadable {
|
protocol XibLoadable {
|
||||||
|
|
||||||
static var xibName: String? { get }
|
static var xibName: String? { get }
|
||||||
static func createFromXib(in bundle: Bundle) -> Self?
|
static func createFromXib(in bundle: Bundle) -> Self?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension XibLoadable where Self: NSView {
|
extension XibLoadable where Self: NSView {
|
||||||
|
|
||||||
static var xibName: String? {
|
static var xibName: String? {
|
||||||
return String(describing: Self.self)
|
return String(describing: Self.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
|
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
|
||||||
guard let xibName = xibName else { return nil }
|
guard let xibName = xibName else { return nil }
|
||||||
var topLevelArray: NSArray? = nil
|
var topLevelArray: NSArray?
|
||||||
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
|
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
|
||||||
guard let results = topLevelArray else { return nil }
|
guard let results = topLevelArray else { return nil }
|
||||||
let views = Array<Any>(results).filter { $0 is Self }
|
let views = [Any](results).filter { $0 is Self }
|
||||||
return views.last as? Self
|
return views.last as? Self
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class Alert {
|
class Alert {
|
||||||
|
|
||||||
public static func confirm(
|
public static func confirm(
|
||||||
onWindow window: NSWindow,
|
onWindow window: NSWindow,
|
||||||
messageText: String,
|
messageText: String,
|
||||||
@@ -21,13 +21,13 @@ class Alert {
|
|||||||
if !Thread.isMainThread {
|
if !Thread.isMainThread {
|
||||||
fatalError("You should always present alerts on the main thread!")
|
fatalError("You should always present alerts on the main thread!")
|
||||||
}
|
}
|
||||||
|
|
||||||
let alert = NSAlert.init()
|
let alert = NSAlert.init()
|
||||||
alert.alertStyle = style
|
alert.alertStyle = style
|
||||||
alert.messageText = messageText
|
alert.messageText = messageText
|
||||||
alert.informativeText = informativeText
|
alert.informativeText = informativeText
|
||||||
alert.addButton(withTitle: buttonTitle)
|
alert.addButton(withTitle: buttonTitle)
|
||||||
if (!secondButtonTitle.isEmpty) {
|
if !secondButtonTitle.isEmpty {
|
||||||
alert.addButton(withTitle: secondButtonTitle)
|
alert.addButton(withTitle: secondButtonTitle)
|
||||||
}
|
}
|
||||||
alert.beginSheetModal(for: window) { response in
|
alert.beginSheetModal(for: window) { response in
|
||||||
@@ -36,5 +36,5 @@ class Alert {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,23 +12,23 @@ import Foundation
|
|||||||
/// In most cases this is going to be a code editor, but it could also be another application
|
/// In most cases this is going to be a code editor, but it could also be another application
|
||||||
/// that supports opening those directories, like a visual Git client or a terminal app.
|
/// that supports opening those directories, like a visual Git client or a terminal app.
|
||||||
class Application {
|
class Application {
|
||||||
|
|
||||||
enum AppType {
|
enum AppType {
|
||||||
case editor, browser, git_gui, terminal, user_supplied
|
case editor, browser, git_gui, terminal, user_supplied
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name of the app. Used for display purposes and to determine `name.app` exists.
|
/// Name of the app. Used for display purposes and to determine `name.app` exists.
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
/// Application type. Depending on the type, a different action might occur.
|
/// Application type. Depending on the type, a different action might occur.
|
||||||
let type: AppType
|
let type: AppType
|
||||||
|
|
||||||
/// Initializer. Used to detect a specific app of a specific type.
|
/// Initializer. Used to detect a specific app of a specific type.
|
||||||
init(_ name: String, _ type: AppType) {
|
init(_ name: String, _ type: AppType) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = type
|
self.type = type
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Attempt to open a specific directory in the app of choice.
|
Attempt to open a specific directory in the app of choice.
|
||||||
(This will open the app if it isn't open yet.)
|
(This will open the app if it isn't open yet.)
|
||||||
@@ -36,7 +36,7 @@ class Application {
|
|||||||
@objc public func openDirectory(file: String) {
|
@objc public func openDirectory(file: String) {
|
||||||
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
|
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks if the app is installed. */
|
/** Checks if the app is installed. */
|
||||||
func isInstalled() -> Bool {
|
func isInstalled() -> Bool {
|
||||||
// If this script does not complain, the app exists!
|
// If this script does not complain, the app exists!
|
||||||
@@ -45,7 +45,7 @@ class Application {
|
|||||||
requiresPath: false
|
requiresPath: false
|
||||||
).task.terminationStatus == 0
|
).task.terminationStatus == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Detect which apps are available to open a specific directory.
|
Detect which apps are available to open a specific directory.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class Filesystem {
|
class Filesystem {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks if a file exists at the provided path.
|
Checks if a file exists at the provided path.
|
||||||
Uses `FileManager`.
|
Uses `FileManager`.
|
||||||
@@ -19,5 +19,5 @@ class Filesystem {
|
|||||||
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
|
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ import Foundation
|
|||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
class LocalNotification {
|
class LocalNotification {
|
||||||
|
|
||||||
public static func send(title: String, subtitle: String) {
|
public static func send(title: String, subtitle: String) {
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = title
|
content.title = title
|
||||||
content.body = subtitle
|
content.body = subtitle
|
||||||
|
|
||||||
let uuidString = UUID().uuidString
|
let uuidString = UUID().uuidString
|
||||||
let request = UNNotificationRequest(
|
let request = UNNotificationRequest(
|
||||||
identifier: uuidString,
|
identifier: uuidString,
|
||||||
content: content,
|
content: content,
|
||||||
trigger: nil
|
trigger: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
let notificationCenter = UNUserNotificationCenter.current()
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
notificationCenter.add(request) { (error) in
|
notificationCenter.add(request) { (error) in
|
||||||
if error != nil {
|
if error != nil {
|
||||||
@@ -29,5 +29,5 @@ class LocalNotification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,40 +8,40 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class MenuBarImageGenerator {
|
class MenuBarImageGenerator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Takes a string and converts it to an image that can be displayed in the menu bar.
|
Takes a string and converts it to an image that can be displayed in the menu bar.
|
||||||
The width of the NSImage depends on the length of the text.
|
The width of the NSImage depends on the length of the text.
|
||||||
*/
|
*/
|
||||||
public static func textToImage(text: String) -> NSImage {
|
public static func textToImage(text: String) -> NSImage {
|
||||||
|
|
||||||
let font = NSFont.systemFont(ofSize: 14, weight: .medium)
|
let font = NSFont.systemFont(ofSize: 14, weight: .medium)
|
||||||
|
|
||||||
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
|
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
|
||||||
let textFontAttributes = [
|
let textFontAttributes = [
|
||||||
NSAttributedString.Key.font: font,
|
NSAttributedString.Key.font: font,
|
||||||
NSAttributedString.Key.foregroundColor: NSColor.black,
|
NSAttributedString.Key.foregroundColor: NSColor.black,
|
||||||
NSAttributedString.Key.paragraphStyle: textStyle
|
NSAttributedString.Key.paragraphStyle: textStyle
|
||||||
]
|
]
|
||||||
|
|
||||||
let padding : CGFloat = 2.0;
|
let padding: CGFloat = 2.0
|
||||||
|
|
||||||
// Create an attributed string so we'll know how wide the item will need to be
|
// Create an attributed string so we'll know how wide the item will need to be
|
||||||
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
|
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
|
||||||
let textSize = attributedString.size()
|
let textSize = attributedString.size()
|
||||||
|
|
||||||
// Add padding to the width of the menu bar item
|
// Add padding to the width of the menu bar item
|
||||||
let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height)
|
let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height)
|
||||||
let image = NSImage(size: size)
|
let image = NSImage(size: size)
|
||||||
|
|
||||||
// Set the image rect with the appropriate dimensions
|
// Set the image rect with the appropriate dimensions
|
||||||
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
|
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
|
||||||
|
|
||||||
// Position the text inside the image rect
|
// Position the text inside the image rect
|
||||||
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
|
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
|
||||||
|
|
||||||
let targetImage: NSImage = NSImage(size: image.size)
|
let targetImage: NSImage = NSImage(size: image.size)
|
||||||
|
|
||||||
let representation: NSBitmapImageRep = NSBitmapImageRep(
|
let representation: NSBitmapImageRep = NSBitmapImageRep(
|
||||||
bitmapDataPlanes: nil,
|
bitmapDataPlanes: nil,
|
||||||
pixelsWide: Int(image.size.width),
|
pixelsWide: Int(image.size.width),
|
||||||
@@ -54,40 +54,40 @@ class MenuBarImageGenerator {
|
|||||||
bytesPerRow: 0,
|
bytesPerRow: 0,
|
||||||
bitsPerPixel: 0
|
bitsPerPixel: 0
|
||||||
)!
|
)!
|
||||||
|
|
||||||
targetImage.addRepresentation(representation)
|
targetImage.addRepresentation(representation)
|
||||||
targetImage.lockFocus()
|
targetImage.lockFocus()
|
||||||
|
|
||||||
image.draw(in: imageRect)
|
image.draw(in: imageRect)
|
||||||
text.draw(in: textRect, withAttributes: textFontAttributes)
|
text.draw(in: textRect, withAttributes: textFontAttributes)
|
||||||
|
|
||||||
targetImage.unlockFocus()
|
targetImage.unlockFocus()
|
||||||
return targetImage
|
return targetImage
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The same as before, but also attempts to add an icon to the left.
|
The same as before, but also attempts to add an icon to the left.
|
||||||
*/
|
*/
|
||||||
public static func textToImageWithIcon(text: String) -> NSImage {
|
public static func textToImageWithIcon(text: String) -> NSImage {
|
||||||
|
|
||||||
// We'll start out with the image containing the text
|
// We'll start out with the image containing the text
|
||||||
let textImage = self.textToImage(text: text)
|
let textImage = self.textToImage(text: text)
|
||||||
|
|
||||||
// Then we'll fetch the image we want on the left
|
// Then we'll fetch the image we want on the left
|
||||||
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
||||||
if iconType == nil {
|
if iconType == nil {
|
||||||
Log.warn("Invalid icon type found, using the default")
|
Log.warn("Invalid icon type found, using the default")
|
||||||
iconType = MenuBarIcon.iconPhp.rawValue
|
iconType = MenuBarIcon.iconPhp.rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
let iconImage = NSImage(named: "MenuBar_\(iconType!)")!
|
let iconImage = NSImage(named: "MenuBar_\(iconType!)")!
|
||||||
|
|
||||||
// We'll need to reference the width of the icon a bunch of times
|
// We'll need to reference the width of the icon a bunch of times
|
||||||
let iconWidthSize = iconImage.size.width
|
let iconWidthSize = iconImage.size.width
|
||||||
|
|
||||||
// There will also be an additional divider between the image and the text (image)
|
// There will also be an additional divider between the image and the text (image)
|
||||||
let divider: CGFloat = 3
|
let divider: CGFloat = 3
|
||||||
|
|
||||||
// Use a fixed size for the height of the menu bar (18pt)
|
// Use a fixed size for the height of the menu bar (18pt)
|
||||||
let imageRect = CGRect(
|
let imageRect = CGRect(
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -95,14 +95,14 @@ class MenuBarImageGenerator {
|
|||||||
width: textImage.size.width + iconWidthSize + divider,
|
width: textImage.size.width + iconWidthSize + divider,
|
||||||
height: 18
|
height: 18
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a new image, we'll draw the text and our icon in there
|
// Create a new image, we'll draw the text and our icon in there
|
||||||
let image: NSImage = NSImage(size: imageRect.size)
|
let image: NSImage = NSImage(size: imageRect.size)
|
||||||
image.lockFocus()
|
image.lockFocus()
|
||||||
|
|
||||||
// Calculate the offset between the image and the text
|
// Calculate the offset between the image and the text
|
||||||
let offset = imageRect.size.width - textImage.size.width
|
let offset = imageRect.size.width - textImage.size.width
|
||||||
|
|
||||||
// Draw the text with a negative x offset (so there is room on the left for the icon)
|
// Draw the text with a negative x offset (so there is room on the left for the icon)
|
||||||
textImage.draw(
|
textImage.draw(
|
||||||
in: imageRect,
|
in: imageRect,
|
||||||
@@ -115,7 +115,7 @@ class MenuBarImageGenerator {
|
|||||||
operation: .overlay,
|
operation: .overlay,
|
||||||
fraction: 1
|
fraction: 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// Draw the icon directly in the left of the imageRect (where we left space)
|
// Draw the icon directly in the left of the imageRect (where we left space)
|
||||||
iconImage.draw(
|
iconImage.draw(
|
||||||
in: imageRect,
|
in: imageRect,
|
||||||
@@ -128,11 +128,11 @@ class MenuBarImageGenerator {
|
|||||||
operation: .overlay,
|
operation: .overlay,
|
||||||
fraction: 1
|
fraction: 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// We're done with this image
|
// We're done with this image
|
||||||
image.unlockFocus()
|
image.unlockFocus()
|
||||||
|
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,32 +15,32 @@ import Cocoa
|
|||||||
- Note: This class does make a simple assumption: each window controller corresponds to a single view.
|
- Note: This class does make a simple assumption: each window controller corresponds to a single view.
|
||||||
*/
|
*/
|
||||||
class PMWindowController: NSWindowController, NSWindowDelegate {
|
class PMWindowController: NSWindowController, NSWindowDelegate {
|
||||||
|
|
||||||
public var windowName: String {
|
public var windowName: String {
|
||||||
fatalError("Please specify a window name")
|
fatalError("Please specify a window name")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func showWindow(_ sender: Any?) {
|
override func showWindow(_ sender: Any?) {
|
||||||
super.showWindow(sender)
|
super.showWindow(sender)
|
||||||
App.shared.register(window: windowName)
|
App.shared.register(window: windowName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowWillClose(_ notification: Notification) {
|
func windowWillClose(_ notification: Notification) {
|
||||||
App.shared.remove(window: windowName)
|
App.shared.remove(window: windowName)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Log.perf("Window controller '\(windowName)' was deinitialized")
|
Log.perf("Window controller '\(windowName)' was deinitialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NSWindowController {
|
extension NSWindowController {
|
||||||
|
|
||||||
public func positionWindowInTopLeftCorner() {
|
public func positionWindowInTopLeftCorner() {
|
||||||
guard let frame = NSScreen.main?.frame else { return }
|
guard let frame = NSScreen.main?.frame else { return }
|
||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
|
|
||||||
window.setFrame(NSRect(
|
window.setFrame(NSRect(
|
||||||
x: frame.size.width - window.frame.size.width - 20,
|
x: frame.size.width - window.frame.size.width - 20,
|
||||||
y: frame.size.height - window.frame.size.height - 40,
|
y: frame.size.height - window.frame.size.height - 40,
|
||||||
@@ -48,5 +48,5 @@ extension NSWindowController {
|
|||||||
height: window.frame.height
|
height: window.frame.height
|
||||||
), display: true)
|
), display: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class VersionExtractor {
|
class VersionExtractor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This attempts to extract the version number from any given string.
|
This attempts to extract the version number from any given string.
|
||||||
*/
|
*/
|
||||||
@@ -19,26 +19,26 @@ class VersionExtractor {
|
|||||||
pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
|
pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
|
||||||
options: []
|
options: []
|
||||||
)
|
)
|
||||||
|
|
||||||
let match = regex.matches(
|
let match = regex.matches(
|
||||||
in: string,
|
in: string,
|
||||||
options: [],
|
options: [],
|
||||||
range: NSMakeRange(0, string.count)
|
range: NSRange(location: 0, length: string.count)
|
||||||
).first
|
).first
|
||||||
|
|
||||||
guard let match = match else {
|
guard let match = match else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let range = Range(
|
let range = Range(
|
||||||
match.range(withName: "version"),
|
match.range(withName: "version"),
|
||||||
in: string
|
in: string
|
||||||
)!
|
)!
|
||||||
|
|
||||||
return String(string[range])
|
return String(string[range])
|
||||||
} catch {
|
} catch {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,78 +21,78 @@ class ActivePhpInstallation {
|
|||||||
var version: Version!
|
var version: Version!
|
||||||
var limits: Limits!
|
var limits: Limits!
|
||||||
var extensions: [PhpExtension]!
|
var extensions: [PhpExtension]!
|
||||||
|
|
||||||
// MARK: - Computed
|
// MARK: - Computed
|
||||||
|
|
||||||
var formula: String {
|
var formula: String {
|
||||||
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
|
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Show information about the current version
|
// Show information about the current version
|
||||||
getVersion()
|
getVersion()
|
||||||
|
|
||||||
// If an error occurred, exit early
|
// If an error occurred, exit early
|
||||||
if (version.error) {
|
if version.error {
|
||||||
limits = Limits()
|
limits = Limits()
|
||||||
extensions = []
|
extensions = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load extension information
|
// Load extension information
|
||||||
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||||
extensions = PhpExtension.load(from: path)
|
extensions = PhpExtension.load(from: path)
|
||||||
|
|
||||||
// Get configuration values
|
// Get configuration values
|
||||||
limits = Limits(
|
limits = Limits(
|
||||||
memory_limit: getByteCount(key: "memory_limit"),
|
memory_limit: getByteCount(key: "memory_limit"),
|
||||||
upload_max_filesize: getByteCount(key: "upload_max_filesize"),
|
upload_max_filesize: getByteCount(key: "upload_max_filesize"),
|
||||||
post_max_size: getByteCount(key: "post_max_size")
|
post_max_size: getByteCount(key: "post_max_size")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Return a list of .ini files parsed after php.ini
|
// Return a list of .ini files parsed after php.ini
|
||||||
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
|
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
|
||||||
.replacingOccurrences(of: "\n", with: "")
|
.replacingOccurrences(of: "\n", with: "")
|
||||||
.split(separator: ",")
|
.split(separator: ",")
|
||||||
.map { String($0) }
|
.map { String($0) }
|
||||||
|
|
||||||
// See if any extensions are present in said .ini files
|
// See if any extensions are present in said .ini files
|
||||||
paths.forEach { (iniFilePath) in
|
paths.forEach { (iniFilePath) in
|
||||||
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
let loadedExtensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
||||||
if exts.count > 0 {
|
if loadedExtensions.isEmpty {
|
||||||
extensions.append(contentsOf: exts)
|
extensions.append(contentsOf: loadedExtensions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
|
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
|
||||||
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
|
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
|
||||||
*/
|
*/
|
||||||
private func getVersion() -> Void {
|
private func getVersion() {
|
||||||
self.version = Version()
|
self.version = Version()
|
||||||
|
|
||||||
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
|
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
|
||||||
|
|
||||||
if (version == "" || version.contains("Warning") || version.contains("Error")) {
|
if version == "" || version.contains("Warning") || version.contains("Error") {
|
||||||
self.version.short = "💩 BROKEN"
|
self.version.short = "💩 BROKEN"
|
||||||
self.version.long = ""
|
self.version.long = ""
|
||||||
self.version.error = true
|
self.version.error = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// That's the long version
|
// That's the long version
|
||||||
self.version.long = version
|
self.version.long = version
|
||||||
|
|
||||||
// Next up, let's strip away the minor version number
|
// Next up, let's strip away the minor version number
|
||||||
let segments = self.version.long.components(separatedBy: ".")
|
let segments = self.version.long.components(separatedBy: ".")
|
||||||
|
|
||||||
// Get the first two elements
|
// Get the first two elements
|
||||||
self.version.short = segments[0...1].joined(separator: ".")
|
self.version.short = segments[0...1].joined(separator: ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Retrieves the display value for a specific key in the `.ini` file.
|
Retrieves the display value for a specific key in the `.ini` file.
|
||||||
|
|
||||||
@@ -110,18 +110,18 @@ class ActivePhpInstallation {
|
|||||||
*/
|
*/
|
||||||
private func getByteCount(key: String) -> String {
|
private func getByteCount(key: String) -> String {
|
||||||
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
|
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
|
||||||
|
|
||||||
// Check if the value is unlimited
|
// Check if the value is unlimited
|
||||||
if (value == "-1") {
|
if value == "-1" {
|
||||||
return "∞"
|
return "∞"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the syntax is valid otherwise
|
// Check if the syntax is valid otherwise
|
||||||
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
|
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
|
||||||
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
|
let match = regex.matches(in: value, options: [], range: NSRange(location: 0, length: value.count)).first
|
||||||
return (match == nil) ? "⚠️" : "\(value)B"
|
return (match == nil) ? "⚠️" : "\(value)B"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine if PHP-FPM is configured correctly.
|
Determine if PHP-FPM is configured correctly.
|
||||||
|
|
||||||
@@ -135,11 +135,11 @@ class ActivePhpInstallation {
|
|||||||
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
|
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
|
||||||
return Shell.pipe("cat \(fileName)").contains("valet.sock")
|
return Shell.pipe("cat \(fileName)").contains("valet.sock")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
|
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
|
||||||
return Filesystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
|
return Filesystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Structs
|
// MARK: - Structs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,7 +153,7 @@ class ActivePhpInstallation {
|
|||||||
var long = "???"
|
var long = "???"
|
||||||
var error = false
|
var error = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Struct containing information about the limits of the current PHP installation.
|
Struct containing information about the limits of the current PHP installation.
|
||||||
Includes: memory limit, max upload size and max post size.
|
Includes: memory limit, max upload size and max post size.
|
||||||
@@ -163,5 +163,5 @@ class ActivePhpInstallation {
|
|||||||
var upload_max_filesize = "???"
|
var upload_max_filesize = "???"
|
||||||
var post_max_size = "???"
|
var post_max_size = "???"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,15 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Xdebug {
|
class Xdebug {
|
||||||
|
|
||||||
public static var enabled: Bool {
|
public static var enabled: Bool {
|
||||||
return !self.mode.isEmpty
|
return !self.mode.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var mode: String {
|
public static var mode: String {
|
||||||
return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
|
return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var modes: [String] {
|
public static var modes: [String] {
|
||||||
return [
|
return [
|
||||||
"off",
|
"off",
|
||||||
@@ -26,8 +26,8 @@ class Xdebug {
|
|||||||
"debug",
|
"debug",
|
||||||
"gcstats",
|
"gcstats",
|
||||||
"profile",
|
"profile",
|
||||||
"trace",
|
"trace"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,18 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct HomebrewPackage: Decodable {
|
struct HomebrewPackage: Decodable {
|
||||||
|
|
||||||
let name: String
|
let name: String
|
||||||
let full_name: String
|
let full_name: String
|
||||||
let aliases: [String]
|
let aliases: [String]
|
||||||
let installed: [HomebrewInstalled]
|
let installed: [HomebrewInstalled]
|
||||||
let linked_keg: String?
|
let linked_keg: String?
|
||||||
|
|
||||||
public var version: String {
|
public var version: String {
|
||||||
return aliases.first!
|
return aliases.first!
|
||||||
.replacingOccurrences(of: "php@", with: "")
|
.replacingOccurrences(of: "php@", with: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HomebrewInstalled: Decodable {
|
struct HomebrewInstalled: Decodable {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ struct HomebrewService: Decodable, Equatable {
|
|||||||
let status: String?
|
let status: String?
|
||||||
let log_path: String?
|
let log_path: String?
|
||||||
let error_log_path: String?
|
let error_log_path: String?
|
||||||
|
|
||||||
public static func loadAll(
|
public static func loadAll(
|
||||||
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
|
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
|
||||||
completion: @escaping ([HomebrewService]) -> Void
|
completion: @escaping ([HomebrewService]) -> Void
|
||||||
@@ -27,11 +27,11 @@ struct HomebrewService: Decodable, Equatable {
|
|||||||
let data = Shell
|
let data = Shell
|
||||||
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
|
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
|
||||||
.data(using: .utf8)!
|
.data(using: .utf8)!
|
||||||
|
|
||||||
let services = try! JSONDecoder()
|
let services = try! JSONDecoder()
|
||||||
.decode([HomebrewService].self, from: data)
|
.decode([HomebrewService].self, from: data)
|
||||||
.filter({ return filter.contains($0.name) })
|
.filter({ return filter.contains($0.name) })
|
||||||
|
|
||||||
completion(services)
|
completion(services)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,42 +9,42 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PhpEnv {
|
class PhpEnv {
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.currentInstall = ActivePhpInstallation()
|
self.currentInstall = ActivePhpInstallation()
|
||||||
|
|
||||||
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
|
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json")
|
||||||
|
|
||||||
self.homebrewPackage = try! JSONDecoder().decode(
|
self.homebrewPackage = try! JSONDecoder().decode(
|
||||||
[HomebrewPackage].self,
|
[HomebrewPackage].self,
|
||||||
from: brewPhpAlias.data(using: .utf8)!
|
from: brewPhpAlias.data(using: .utf8)!
|
||||||
).first!
|
).first!
|
||||||
|
|
||||||
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
|
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
/** The delegate that is informed of updates. */
|
/** The delegate that is informed of updates. */
|
||||||
weak var delegate: PhpSwitcherDelegate?
|
weak var delegate: PhpSwitcherDelegate?
|
||||||
|
|
||||||
/** The static app instance. Accessible at any time. */
|
/** The static app instance. Accessible at any time. */
|
||||||
static let shared = PhpEnv()
|
static let shared = PhpEnv()
|
||||||
|
|
||||||
/** Whether the switcher is busy performing any actions. */
|
/** Whether the switcher is busy performing any actions. */
|
||||||
var isBusy: Bool = false
|
var isBusy: Bool = false
|
||||||
|
|
||||||
/** All available versions of PHP. */
|
/** All available versions of PHP. */
|
||||||
var availablePhpVersions: [String] = []
|
var availablePhpVersions: [String] = []
|
||||||
|
|
||||||
/** Cached information about the PHP installations. */
|
/** Cached information about the PHP installations. */
|
||||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||||
|
|
||||||
/** Information about the currently linked PHP installation. */
|
/** Information about the currently linked PHP installation. */
|
||||||
var currentInstall: ActivePhpInstallation
|
var currentInstall: ActivePhpInstallation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The version that the `php` formula via Brew is aliased to on the current system.
|
The version that the `php` formula via Brew is aliased to on the current system.
|
||||||
|
|
||||||
@@ -57,63 +57,62 @@ class PhpEnv {
|
|||||||
static var brewPhpVersion: String {
|
static var brewPhpVersion: String {
|
||||||
return Self.shared.homebrewPackage.version
|
return Self.shared.homebrewPackage.version
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The currently linked and active PHP installation.
|
The currently linked and active PHP installation.
|
||||||
*/
|
*/
|
||||||
static var phpInstall: ActivePhpInstallation {
|
static var phpInstall: ActivePhpInstallation {
|
||||||
return Self.shared.currentInstall
|
return Self.shared.currentInstall
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Information we were able to discern from the Homebrew info command.
|
Information we were able to discern from the Homebrew info command.
|
||||||
*/
|
*/
|
||||||
var homebrewPackage: HomebrewPackage! = nil
|
var homebrewPackage: HomebrewPackage! = nil
|
||||||
|
|
||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
|
|
||||||
public static var switcher: PhpSwitcher {
|
public static var switcher: PhpSwitcher {
|
||||||
return InternalSwitcher()
|
return InternalSwitcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func detectPhpVersions() -> Void {
|
public static func detectPhpVersions() {
|
||||||
_ = Self.shared.detectPhpVersions()
|
_ = Self.shared.detectPhpVersions()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Detects which versions of PHP are installed.
|
Detects which versions of PHP are installed.
|
||||||
*/
|
*/
|
||||||
public func detectPhpVersions() -> [String]
|
public func detectPhpVersions() -> [String] {
|
||||||
{
|
|
||||||
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
|
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
|
||||||
|
|
||||||
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
|
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
|
||||||
|
|
||||||
// Make sure the aliased version is detected
|
// Make sure the aliased version is detected
|
||||||
// The user may have `php` installed, but not e.g. `php@8.0`
|
// The user may have `php` installed, but not e.g. `php@8.0`
|
||||||
// We should also detect that as a version that is installed
|
// We should also detect that as a version that is installed
|
||||||
let phpAlias = homebrewPackage.version
|
let phpAlias = homebrewPackage.version
|
||||||
|
|
||||||
// Avoid inserting a duplicate
|
// Avoid inserting a duplicate
|
||||||
if (!versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php")) {
|
if !versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||||
versionsOnly.append(phpAlias)
|
versionsOnly.append(phpAlias)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info("The PHP versions that were detected are: \(versionsOnly)")
|
Log.info("The PHP versions that were detected are: \(versionsOnly)")
|
||||||
|
|
||||||
availablePhpVersions = versionsOnly
|
availablePhpVersions = versionsOnly
|
||||||
|
|
||||||
var mappedVersions: [String: PhpInstallation] = [:]
|
var mappedVersions: [String: PhpInstallation] = [:]
|
||||||
|
|
||||||
availablePhpVersions.forEach { version in
|
availablePhpVersions.forEach { version in
|
||||||
mappedVersions[version] = PhpInstallation(version)
|
mappedVersions[version] = PhpInstallation(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedPhpInstallations = mappedVersions
|
cachedPhpInstallations = mappedVersions
|
||||||
|
|
||||||
return versionsOnly
|
return versionsOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Extracts valid PHP versions from an array of strings.
|
Extracts valid PHP versions from an array of strings.
|
||||||
This array of strings is usually retrieved from `grep`.
|
This array of strings is usually retrieved from `grep`.
|
||||||
@@ -126,14 +125,14 @@ class PhpEnv {
|
|||||||
checkBinaries: Bool = true,
|
checkBinaries: Bool = true,
|
||||||
generateHelpers: Bool = true
|
generateHelpers: Bool = true
|
||||||
) -> [String] {
|
) -> [String] {
|
||||||
var output : [String] = []
|
var output: [String] = []
|
||||||
|
|
||||||
var supported = Constants.SupportedPhpVersions
|
var supported = Constants.SupportedPhpVersions
|
||||||
|
|
||||||
if !Valet.enabled(feature: .supportForPhp56) {
|
if !Valet.enabled(feature: .supportForPhp56) {
|
||||||
supported.removeAll { $0 == "5.6" }
|
supported.removeAll { $0 == "5.6" }
|
||||||
}
|
}
|
||||||
|
|
||||||
versions.filter { (version) -> Bool in
|
versions.filter { (version) -> Bool in
|
||||||
// Omit everything that doesn't start with php@
|
// Omit everything that doesn't start with php@
|
||||||
// (e.g. something-php@8.0 won't be detected)
|
// (e.g. something-php@8.0 won't be detected)
|
||||||
@@ -144,19 +143,18 @@ class PhpEnv {
|
|||||||
// is supported and where the binary exists (avoids broken installs)
|
// is supported and where the binary exists (avoids broken installs)
|
||||||
if !output.contains(version)
|
if !output.contains(version)
|
||||||
&& supported.contains(version)
|
&& supported.contains(version)
|
||||||
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
|
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
|
||||||
{
|
|
||||||
output.append(version)
|
output.append(version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if generateHelpers {
|
if generateHelpers {
|
||||||
output.forEach { PhpHelper.generate(for: $0) }
|
output.forEach { PhpHelper.generate(for: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
public func validVersions(for constraint: String) -> [PhpVersionNumber] {
|
public func validVersions(for constraint: String) -> [PhpVersionNumber] {
|
||||||
constraint.split(separator: "|").flatMap {
|
constraint.split(separator: "|").flatMap {
|
||||||
return PhpVersionNumberCollection
|
return PhpVersionNumberCollection
|
||||||
@@ -164,7 +162,7 @@ class PhpEnv {
|
|||||||
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
|
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Validates whether the currently running version matches the provided version.
|
Validates whether the currently running version matches the provided version.
|
||||||
*/
|
*/
|
||||||
@@ -173,7 +171,7 @@ class PhpEnv {
|
|||||||
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
|
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,27 +9,28 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PhpHelper {
|
class PhpHelper {
|
||||||
|
|
||||||
static let keyPhrase = "This file was automatically generated by PHP Monitor."
|
static let keyPhrase = "This file was automatically generated by PHP Monitor."
|
||||||
|
|
||||||
public static func generate(for version: String) {
|
public static func generate(for version: String) {
|
||||||
// Take the PHP version (e.g. "7.2") and generate a dotless version
|
// Take the PHP version (e.g. "7.2") and generate a dotless version
|
||||||
let dotless = version.replacingOccurrences(of: ".", with: "")
|
let dotless = version.replacingOccurrences(of: ".", with: "")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let destination = "/usr/local/bin/pm\(dotless)"
|
let destination = "/usr/local/bin/pm\(dotless)"
|
||||||
if FileManager.default.fileExists(atPath: destination) {
|
if FileManager.default.fileExists(atPath: destination) {
|
||||||
let contents = try String(contentsOfFile: destination)
|
let contents = try String(contentsOfFile: destination)
|
||||||
if !contents.contains(keyPhrase) {
|
if !contents.contains(keyPhrase) {
|
||||||
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor (or is unreadable). Not updating this file.")
|
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
|
||||||
|
+ "(or is unreadable). Not updating this file.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's follow the symlink to the PHP binary folder
|
// Let's follow the symlink to the PHP binary folder
|
||||||
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
|
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
|
||||||
.resolvingSymlinksInPath().path
|
.resolvingSymlinksInPath().path
|
||||||
|
|
||||||
// The contents of the script!
|
// The contents of the script!
|
||||||
let script = """
|
let script = """
|
||||||
#!/bin/zsh
|
#!/bin/zsh
|
||||||
@@ -41,14 +42,14 @@ class PhpHelper {
|
|||||||
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
|
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
|
||||||
export PATH=\(path):$PATH
|
export PATH=\(path):$PATH
|
||||||
"""
|
"""
|
||||||
|
|
||||||
// Write to the destination
|
// Write to the destination
|
||||||
try script.write(
|
try script.write(
|
||||||
to: URL(fileURLWithPath: destination),
|
to: URL(fileURLWithPath: destination),
|
||||||
atomically: true,
|
atomically: true,
|
||||||
encoding: String.Encoding.utf8
|
encoding: String.Encoding.utf8
|
||||||
)
|
)
|
||||||
|
|
||||||
// Make sure the file is executable
|
// Make sure the file is executable
|
||||||
Shell.run("chmod +x \(destination)")
|
Shell.run("chmod +x \(destination)")
|
||||||
} catch {
|
} catch {
|
||||||
@@ -56,5 +57,5 @@ class PhpHelper {
|
|||||||
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
|
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,21 +10,21 @@ import Foundation
|
|||||||
|
|
||||||
public struct PhpVersionNumberCollection: Equatable {
|
public struct PhpVersionNumberCollection: Equatable {
|
||||||
let versions: [PhpVersionNumber]
|
let versions: [PhpVersionNumber]
|
||||||
|
|
||||||
public static func make(from versions: [String]) -> Self {
|
public static func make(from versions: [String]) -> Self {
|
||||||
return PhpVersionNumberCollection(
|
return PhpVersionNumberCollection(
|
||||||
versions: versions.map { try! PhpVersionNumber.parse($0) }
|
versions: versions.map { try! PhpVersionNumber.parse($0) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var first: PhpVersionNumber? {
|
public var first: PhpVersionNumber? {
|
||||||
return self.versions.first
|
return self.versions.first
|
||||||
}
|
}
|
||||||
|
|
||||||
public var all: [PhpVersionNumber] {
|
public var all: [PhpVersionNumber] {
|
||||||
return self.versions
|
return self.versions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks if any versions of PHP are valid for the constraint provided.
|
Checks if any versions of PHP are valid for the constraint provided.
|
||||||
Due to the complexity of evaluating these, a important test is maintained.
|
Due to the complexity of evaluating these, a important test is maintained.
|
||||||
@@ -61,13 +61,13 @@ public struct PhpVersionNumberCollection: Equatable {
|
|||||||
// Strict constraint (e.g. "7.0") -> returns specific version
|
// Strict constraint (e.g. "7.0") -> returns specific version
|
||||||
return self.versions.filter { $0.isSameAs(version, strict) }
|
return self.versions.filter { $0.isSameAs(version, strict) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
|
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
|
||||||
// Caret range means that the major version is never higher but minor version can be higher
|
// Caret range means that the major version is never higher but minor version can be higher
|
||||||
// ^7.2 will be compatible with all versions between 7.2 and 8.0
|
// ^7.2 will be compatible with all versions between 7.2 and 8.0
|
||||||
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
|
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
|
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
|
||||||
// Tilde range means that most specific digit is used as the basis.
|
// Tilde range means that most specific digit is used as the basis.
|
||||||
return self.versions.filter {
|
return self.versions.filter {
|
||||||
@@ -78,11 +78,11 @@ public struct PhpVersionNumberCollection: Equatable {
|
|||||||
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
|
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
|
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
|
||||||
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
|
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
|
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
|
||||||
return self.versions.filter { $0.isNewerThan(version, strict) }
|
return self.versions.filter { $0.isNewerThan(version, strict) }
|
||||||
}
|
}
|
||||||
@@ -95,47 +95,52 @@ public struct PhpVersionNumber: Equatable {
|
|||||||
let major: Int
|
let major: Int
|
||||||
let minor: Int
|
let minor: Int
|
||||||
let patch: Int?
|
let patch: Int?
|
||||||
|
|
||||||
public func toString() -> String {
|
public func toString() -> String {
|
||||||
return self.patch == nil
|
return self.patch == nil
|
||||||
? "\(major).\(minor)"
|
? "\(major).\(minor)"
|
||||||
: "\(major).\(minor).\(patch!)"
|
: "\(major).\(minor).\(patch!)"
|
||||||
}
|
}
|
||||||
|
|
||||||
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
|
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
|
||||||
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
|
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var homebrewVersion: String {
|
public var homebrewVersion: String {
|
||||||
return "\(major).\(minor)"
|
return "\(major).\(minor)"
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum MatchType: String {
|
public enum MatchType: String {
|
||||||
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
|
|
||||||
// TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
|
// TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
|
||||||
/*
|
/*
|
||||||
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func parse(_ text: String) throws -> Self {
|
public static func parse(_ text: String) throws -> Self {
|
||||||
guard let versionText = VersionExtractor.from(text) else {
|
guard let versionText = VersionExtractor.from(text) else {
|
||||||
throw VersionParseError()
|
throw VersionParseError()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Self.make(from: versionText)!
|
return Self.make(from: versionText)!
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
|
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
|
||||||
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
|
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
|
||||||
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
|
|
||||||
|
let match = regex.matches(
|
||||||
|
in: versionString,
|
||||||
|
options: [],
|
||||||
|
range: NSRange(location: 0, length: versionString.count)
|
||||||
|
).first
|
||||||
|
|
||||||
if match != nil {
|
if match != nil {
|
||||||
let major = Int(
|
let major = Int(
|
||||||
versionString[Range(match!.range(withName: "major"), in: versionString)!]
|
versionString[Range(match!.range(withName: "major"), in: versionString)!]
|
||||||
@@ -143,24 +148,24 @@ public struct PhpVersionNumber: Equatable {
|
|||||||
let minor = Int(
|
let minor = Int(
|
||||||
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
|
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
|
||||||
)!
|
)!
|
||||||
var patch: Int? = nil
|
var patch: Int?
|
||||||
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
|
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
|
||||||
patch = Int(versionString[minorRange])
|
patch = Int(versionString[minorRange])
|
||||||
}
|
}
|
||||||
return Self(major: major, minor: minor, patch: patch)
|
return Self(major: major, minor: minor, patch: patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Comparison Logic
|
// MARK: Comparison Logic
|
||||||
|
|
||||||
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||||
return self.major == version.major
|
return self.major == version.major
|
||||||
&& self.minor == version.minor
|
&& self.minor == version.minor
|
||||||
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
|
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||||
return (
|
return (
|
||||||
self.major > version.major ||
|
self.major > version.major ||
|
||||||
@@ -169,7 +174,7 @@ public struct PhpVersionNumber: Equatable {
|
|||||||
&& self.patch(strict) > version.patch(strict)
|
&& self.patch(strict) > version.patch(strict)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||||
return self.major == version.major &&
|
return self.major == version.major &&
|
||||||
(
|
(
|
||||||
@@ -177,12 +182,12 @@ public struct PhpVersionNumber: Equatable {
|
|||||||
|| self.minor > version.minor
|
|| self.minor > version.minor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||||
return self.major == version.major && self.minor == version.minor
|
return self.major == version.major && self.minor == version.minor
|
||||||
&& self.patch(strict, version) >= version.patch(strict)
|
&& self.patch(strict, version) >= version.patch(strict)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||||
return self.major == version.major
|
return self.major == version.major
|
||||||
&& self.minor >= version.minor
|
&& self.minor >= version.minor
|
||||||
|
|||||||
@@ -16,24 +16,26 @@ import Foundation
|
|||||||
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
|
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
|
||||||
*/
|
*/
|
||||||
class PhpExtension {
|
class PhpExtension {
|
||||||
|
|
||||||
/// The file where this extension was located.
|
/// The file where this extension was located.
|
||||||
var file: String
|
var file: String
|
||||||
|
|
||||||
/// The original string that was used to determine this extension is active.
|
/// The original string that was used to determine this extension is active.
|
||||||
var line: String
|
var line: String
|
||||||
|
|
||||||
/// The name of the extension. This is always identical to the name found in the original string. If you want to display this name, capitalize this.
|
/// The name of the extension. This is always identical to the name found in the original string.
|
||||||
|
/// If you want to display this name, capitalize this.
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
/// Whether the extension has been enabled.
|
/// Whether the extension has been enabled.
|
||||||
var enabled: Bool
|
var enabled: Bool
|
||||||
|
|
||||||
/// The file where this extension was located, but only the filename, not the full path to the .ini file.
|
/// The file where this extension was located, but only the filename, not the full path to the .ini file.
|
||||||
var fileNameOnly: String {
|
var fileNameOnly: String {
|
||||||
return String(file.split(separator: "/").last ?? "php.ini")
|
return String(file.split(separator: "/").last ?? "php.ini")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable line_length
|
||||||
/**
|
/**
|
||||||
This regular expression will allow us to identify lines which activate an extension.
|
This regular expression will allow us to identify lines which activate an extension.
|
||||||
|
|
||||||
@@ -47,29 +49,31 @@ class PhpExtension {
|
|||||||
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
||||||
*/
|
*/
|
||||||
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
||||||
|
// swiftlint:enable line_length
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When registering an extension, we do that based on the line found inside the .ini file.
|
When registering an extension, we do that based on the line found inside the .ini file.
|
||||||
*/
|
*/
|
||||||
init(_ line: String, file: String) {
|
init(_ line: String, file: String) {
|
||||||
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
|
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
|
||||||
let match = regex.matches(in: line, options: [], range: NSMakeRange(0, line.count)).first
|
let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
|
||||||
let range = Range(match!.range(withName: "name"), in: line)!
|
let range = Range(match!.range(withName: "name"), in: line)!
|
||||||
|
|
||||||
self.line = line
|
self.line = line
|
||||||
|
|
||||||
let fullPath = String(line[range])
|
let fullPath = String(line[range])
|
||||||
.replacingOccurrences(of: "\"", with: "") // replace excess "
|
.replacingOccurrences(of: "\"", with: "") // replace excess "
|
||||||
.replacingOccurrences(of: ".so", with: "") // replace excess .so
|
.replacingOccurrences(of: ".so", with: "") // replace excess .so
|
||||||
|
|
||||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||||
|
|
||||||
self.enabled = !line.contains(";")
|
self.enabled = !line.contains(";")
|
||||||
self.file = file
|
self.file = file
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply.
|
This simply toggles the extension in the .ini file.
|
||||||
|
You may need to restart the other services in order for this change to apply.
|
||||||
*/
|
*/
|
||||||
func toggle() {
|
func toggle() {
|
||||||
let newLine = enabled
|
let newLine = enabled
|
||||||
@@ -77,25 +81,25 @@ class PhpExtension {
|
|||||||
? "; \(line)"
|
? "; \(line)"
|
||||||
// ENABLED: Line where the comment delimiter (;) is removed
|
// ENABLED: Line where the comment delimiter (;) is removed
|
||||||
: line.replacingOccurrences(of: "; ", with: "")
|
: line.replacingOccurrences(of: "; ", with: "")
|
||||||
|
|
||||||
sed(file: file, original: line, replacement: newLine)
|
sed(file: file, original: line, replacement: newLine)
|
||||||
|
|
||||||
enabled.toggle()
|
enabled.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Static Methods
|
// MARK: - Static Methods
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This method will attempt to identify all extensions in the .ini file at a certain URL.
|
This method will attempt to identify all extensions in the .ini file at a certain URL.
|
||||||
*/
|
*/
|
||||||
static func load(from path: URL) -> [PhpExtension] {
|
static func load(from path: URL) -> [PhpExtension] {
|
||||||
let file = try? String(contentsOf: path, encoding: .utf8)
|
let file = try? String(contentsOf: path, encoding: .utf8)
|
||||||
|
|
||||||
if (file == nil) {
|
if file == nil {
|
||||||
Log.err("There was an issue reading the file. Assuming no extensions were found.")
|
Log.err("There was an issue reading the file. Assuming no extensions were found.")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return file!.components(separatedBy: "\n")
|
return file!.components(separatedBy: "\n")
|
||||||
.filter {
|
.filter {
|
||||||
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||||
@@ -104,5 +108,5 @@ class PhpExtension {
|
|||||||
return PhpExtension($0, file: path.path)
|
return PhpExtension($0, file: path.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,28 +9,28 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PhpInstallation {
|
class PhpInstallation {
|
||||||
|
|
||||||
var versionNumber: PhpVersionNumber
|
var versionNumber: PhpVersionNumber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
In order to determine details about a PHP installation, we’ll simply run `php-config --version`
|
In order to determine details about a PHP installation, we’ll simply run `php-config --version`
|
||||||
in the relevant directory.
|
in the relevant directory.
|
||||||
*/
|
*/
|
||||||
init(_ version: String) {
|
init(_ version: String) {
|
||||||
|
|
||||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
||||||
self.versionNumber = PhpVersionNumber.make(from: version)!
|
self.versionNumber = PhpVersionNumber.make(from: version)!
|
||||||
|
|
||||||
if Filesystem.fileExists(phpConfigExecutablePath) {
|
if Filesystem.fileExists(phpConfigExecutablePath) {
|
||||||
let longVersionString = Command.execute(
|
let longVersionString = Command.execute(
|
||||||
path: phpConfigExecutablePath,
|
path: phpConfigExecutablePath,
|
||||||
arguments: ["--version"]
|
arguments: ["--version"]
|
||||||
).trimmingCharacters(in: .whitespacesAndNewlines)
|
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
// The parser should always work, or the string has to be very unusual.
|
// The parser should always work, or the string has to be very unusual.
|
||||||
// If so, the app SHOULD crash, so that the users report what's up.
|
// If so, the app SHOULD crash, so that the users report what's up.
|
||||||
self.versionNumber = try! PhpVersionNumber.parse(longVersionString)
|
self.versionNumber = try! PhpVersionNumber.parse(longVersionString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class InternalSwitcher: PhpSwitcher {
|
class InternalSwitcher: PhpSwitcher {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Switching to a new PHP version involves:
|
Switching to a new PHP version involves:
|
||||||
- unlinking the current version
|
- unlinking the current version
|
||||||
@@ -20,50 +20,49 @@ class InternalSwitcher: PhpSwitcher {
|
|||||||
the version that is switched to may or may not be identical to `php`
|
the version that is switched to may or may not be identical to `php`
|
||||||
(without @version).
|
(without @version).
|
||||||
*/
|
*/
|
||||||
func performSwitch(to version: String, completion: @escaping () -> Void)
|
func performSwitch(to version: String, completion: @escaping () -> Void) {
|
||||||
{
|
|
||||||
Log.info("Switching to \(version), unlinking all versions...")
|
Log.info("Switching to \(version), unlinking all versions...")
|
||||||
|
|
||||||
let isolated = Valet.shared.sites.filter { site in
|
let isolated = Valet.shared.sites.filter { site in
|
||||||
site.isolatedPhpVersion != nil
|
site.isolatedPhpVersion != nil
|
||||||
}.map { site in
|
}.map { site in
|
||||||
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
|
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
var versions: Set<String> = [version]
|
var versions: Set<String> = [version]
|
||||||
|
|
||||||
if (Valet.enabled(feature: .isolatedSites)) {
|
if Valet.enabled(feature: .isolatedSites) {
|
||||||
versions = versions.union(isolated)
|
versions = versions.union(isolated)
|
||||||
}
|
}
|
||||||
|
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
PhpEnv.shared.availablePhpVersions.forEach { (available) in
|
PhpEnv.shared.availablePhpVersions.forEach { (available) in
|
||||||
group.enter()
|
group.enter()
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
self.disableDefaultPhpFpmPool(available)
|
self.disableDefaultPhpFpmPool(available)
|
||||||
self.stopPhpVersion(available)
|
self.stopPhpVersion(available)
|
||||||
group.leave()
|
group.leave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
group.notify(queue: .global(qos: .userInitiated)) {
|
group.notify(queue: .global(qos: .userInitiated)) {
|
||||||
Log.info("All versions have been unlinked!")
|
Log.info("All versions have been unlinked!")
|
||||||
Log.info("Linking the new version!")
|
Log.info("Linking the new version!")
|
||||||
|
|
||||||
for formula in versions {
|
for formula in versions {
|
||||||
self.startPhpVersion(formula, primary: (version == formula))
|
self.startPhpVersion(formula, primary: (version == formula))
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info("Restarting nginx, just to be sure!")
|
Log.info("Restarting nginx, just to be sure!")
|
||||||
brew("services restart nginx", sudo: true)
|
brew("services restart nginx", sudo: true)
|
||||||
|
|
||||||
Log.info("The new version(s) have been linked!")
|
Log.info("The new version(s) have been linked!")
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func disableDefaultPhpFpmPool(_ version: String) {
|
private func disableDefaultPhpFpmPool(_ version: String) {
|
||||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||||
if FileManager.default.fileExists(atPath: pool) {
|
if FileManager.default.fileExists(atPath: pool) {
|
||||||
@@ -71,8 +70,9 @@ class InternalSwitcher: PhpSwitcher {
|
|||||||
let existing = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf")!
|
let existing = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf")!
|
||||||
let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
|
let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
|
||||||
do {
|
do {
|
||||||
if (FileManager.default.fileExists(atPath: new.path)) {
|
if FileManager.default.fileExists(atPath: new.path) {
|
||||||
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), cleaning up so the newer `www.conf` can be moved again.")
|
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
|
||||||
|
+ "cleaning up so the newer `www.conf` can be moved again.")
|
||||||
try FileManager.default.removeItem(at: new)
|
try FileManager.default.removeItem(at: new)
|
||||||
}
|
}
|
||||||
try FileManager.default.moveItem(at: existing, to: new)
|
try FileManager.default.moveItem(at: existing, to: new)
|
||||||
@@ -82,26 +82,26 @@ class InternalSwitcher: PhpSwitcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopPhpVersion(_ version: String) {
|
private func stopPhpVersion(_ version: String) {
|
||||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||||
brew("unlink \(formula)")
|
brew("unlink \(formula)")
|
||||||
brew("services stop \(formula)", sudo: true)
|
brew("services stop \(formula)", sudo: true)
|
||||||
Log.info("Unlinked and stopped services for \(formula)")
|
Log.info("Unlinked and stopped services for \(formula)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startPhpVersion(_ version: String, primary: Bool) {
|
private func startPhpVersion(_ version: String, primary: Bool) {
|
||||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||||
|
|
||||||
if (primary) {
|
if primary {
|
||||||
Log.info("\(formula) is the primary formula, linking and starting services...")
|
Log.info("\(formula) is the primary formula, linking and starting services...")
|
||||||
brew("link \(formula) --overwrite --force")
|
brew("link \(formula) --overwrite --force")
|
||||||
} else {
|
} else {
|
||||||
Log.info("\(formula) is an isolated PHP version, starting services only...")
|
Log.info("\(formula) is an isolated PHP version, starting services only...")
|
||||||
}
|
}
|
||||||
|
|
||||||
brew("services start \(formula)", sudo: true)
|
brew("services start \(formula)", sudo: true)
|
||||||
|
|
||||||
if Valet.enabled(feature: .isolatedSites) && primary {
|
if Valet.enabled(feature: .isolatedSites) && primary {
|
||||||
let socketVersion = version.replacingOccurrences(of: ".", with: "")
|
let socketVersion = version.replacingOccurrences(of: ".", with: "")
|
||||||
Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
|
Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
|
||||||
@@ -109,5 +109,5 @@ class InternalSwitcher: PhpSwitcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,15 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol PhpSwitcherDelegate: AnyObject {
|
protocol PhpSwitcherDelegate: AnyObject {
|
||||||
|
|
||||||
func switcherDidStartSwitching(to version: String)
|
func switcherDidStartSwitching(to version: String)
|
||||||
|
|
||||||
func switcherDidCompleteSwitch(to version: String)
|
func switcherDidCompleteSwitch(to version: String)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol PhpSwitcher {
|
protocol PhpSwitcher {
|
||||||
|
|
||||||
func performSwitch(to version: String, completion: @escaping () -> Void)
|
func performSwitch(to version: String, completion: @escaping () -> Void)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import Cocoa
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension App {
|
extension App {
|
||||||
|
|
||||||
// MARK: - Application State
|
// MARK: - Application State
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Registers a window as currently open.
|
Registers a window as currently open.
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +22,7 @@ extension App {
|
|||||||
}
|
}
|
||||||
updateActivationPolicy()
|
updateActivationPolicy()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Removes a window, assuming it was closed.
|
Removes a window, assuming it was closed.
|
||||||
*/
|
*/
|
||||||
@@ -32,13 +32,13 @@ extension App {
|
|||||||
}
|
}
|
||||||
updateActivationPolicy()
|
updateActivationPolicy()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
If there are any open windows, the app will be a regular app.
|
If there are any open windows, the app will be a regular app.
|
||||||
If there are no windows open, the app will be an accessory (toolbar) app.
|
If there are no windows open, the app will be an accessory (toolbar) app.
|
||||||
*/
|
*/
|
||||||
public func updateActivationPolicy() {
|
public func updateActivationPolicy() {
|
||||||
NSApp.setActivationPolicy(openWindows.count > 0 ? .regular : .accessory)
|
NSApp.setActivationPolicy(!openWindows.isEmpty ? .regular : .accessory)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
extension App {
|
extension App {
|
||||||
|
|
||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
|
|
||||||
/**
|
/**
|
||||||
On startup, the preferences should be loaded from the .plist,
|
On startup, the preferences should be loaded from the .plist,
|
||||||
and we'll enable the shortcut if it is set.
|
and we'll enable the shortcut if it is set.
|
||||||
@@ -22,20 +22,20 @@ extension App {
|
|||||||
Log.info("No global hotkey was saved in preferences. None set.")
|
Log.info("No global hotkey was saved in preferences. None set.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we can parse the JSON into the desired format
|
// Make sure we can parse the JSON into the desired format
|
||||||
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
|
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
|
||||||
Log.err("No global hotkey loaded, could not be parsed!")
|
Log.err("No global hotkey loaded, could not be parsed!")
|
||||||
shortcutHotkey = nil
|
shortcutHotkey = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcutHotkey = HotKey(keyCombo: KeyCombo(
|
shortcutHotkey = HotKey(keyCombo: KeyCombo(
|
||||||
carbonKeyCode: keybindPref.keyCode,
|
carbonKeyCode: keybindPref.keyCode,
|
||||||
carbonModifiers: keybindPref.carbonFlags
|
carbonModifiers: keybindPref.carbonFlags
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Sets up the action that needs to occur when the shortcut key is pressed
|
Sets up the action that needs to occur when the shortcut key is pressed
|
||||||
(opens the menu).
|
(opens the menu).
|
||||||
@@ -44,11 +44,11 @@ extension App {
|
|||||||
guard let hotkey = shortcutHotkey else {
|
guard let hotkey = shortcutHotkey else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hotkey.keyDownHandler = {
|
hotkey.keyDownHandler = {
|
||||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,19 +8,19 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
|
|
||||||
// MARK: Static Vars
|
// MARK: Static Vars
|
||||||
|
|
||||||
/** The static app instance. Accessible at any time. */
|
/** The static app instance. Accessible at any time. */
|
||||||
static let shared = App()
|
static let shared = App()
|
||||||
|
|
||||||
/** Retrieve the version number from the main info dictionary, Info.plist. */
|
/** Retrieve the version number from the main info dictionary, Info.plist. */
|
||||||
static var version: String {
|
static var version: String {
|
||||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
|
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
|
||||||
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
|
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
|
||||||
return "\(version) (\(build))"
|
return "\(version) (\(build))"
|
||||||
}
|
}
|
||||||
|
|
||||||
static var architecture: String {
|
static var architecture: String {
|
||||||
var systeminfo = utsname()
|
var systeminfo = utsname()
|
||||||
uname(&systeminfo)
|
uname(&systeminfo)
|
||||||
@@ -34,37 +34,37 @@ class App {
|
|||||||
}
|
}
|
||||||
return machine
|
return machine
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Variables
|
// MARK: Variables
|
||||||
|
|
||||||
/** The list of preferences that are currently active. */
|
/** The list of preferences that are currently active. */
|
||||||
var preferences: [PreferenceName: Bool]!
|
var preferences: [PreferenceName: Bool]!
|
||||||
|
|
||||||
/** The window controller of the currently active preferences window. */
|
/** The window controller of the currently active preferences window. */
|
||||||
var preferencesWindowController: PrefsWC? = nil
|
var preferencesWindowController: PrefsWC?
|
||||||
|
|
||||||
/** The window controller of the currently active site list window. */
|
/** The window controller of the currently active site list window. */
|
||||||
var domainListWindowController: DomainListWC? = nil
|
var domainListWindowController: DomainListWC?
|
||||||
|
|
||||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||||
var detectedApplications: [Application] = []
|
var detectedApplications: [Application] = []
|
||||||
|
|
||||||
/** Timer that will periodically reload info about the user's PHP installation. */
|
/** Timer that will periodically reload info about the user's PHP installation. */
|
||||||
var timer: Timer?
|
var timer: Timer?
|
||||||
|
|
||||||
// MARK: - Global Hotkey
|
// MARK: - Global Hotkey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The shortcut the user has requested.
|
The shortcut the user has requested.
|
||||||
*/
|
*/
|
||||||
var shortcutHotkey: HotKey? = nil {
|
var shortcutHotkey: HotKey? {
|
||||||
didSet {
|
didSet {
|
||||||
setupGlobalHotkeyListener()
|
setupGlobalHotkeyListener()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Activation Policy
|
// MARK: - Activation Policy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Variable that keeps track of which windows are currently open.
|
Variable that keeps track of which windows are currently open.
|
||||||
(Please note that window controllers remain open in memory once opened.)
|
(Please note that window controllers remain open in memory once opened.)
|
||||||
@@ -74,9 +74,9 @@ class App {
|
|||||||
(as a normal app or as a toolbar app).
|
(as a normal app or as a toolbar app).
|
||||||
*/
|
*/
|
||||||
var openWindows: [String] = []
|
var openWindows: [String] = []
|
||||||
|
|
||||||
// MARK: - App Watchers
|
// MARK: - App Watchers
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Cocoa
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension AppDelegate {
|
extension AppDelegate {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This is an entry point for future development for integrating with the PHP Monitor
|
This is an entry point for future development for integrating with the PHP Monitor
|
||||||
application URL. You can use the `phpmon://` protocol to communicate with the app.
|
application URL. You can use the `phpmon://` protocol to communicate with the app.
|
||||||
@@ -21,20 +21,20 @@ extension AppDelegate {
|
|||||||
Please note that PHP Monitor needs to be running in the background for this to work.
|
Please note that PHP Monitor needs to be running in the background for this to work.
|
||||||
*/
|
*/
|
||||||
func application(_ application: NSApplication, open urls: [URL]) {
|
func application(_ application: NSApplication, open urls: [URL]) {
|
||||||
|
|
||||||
if !Preferences.isEnabled(.allowProtocolForIntegrations) {
|
if !Preferences.isEnabled(.allowProtocolForIntegrations) {
|
||||||
Log.info("Acting on commands via phpmon:// has been disabled.")
|
Log.info("Acting on commands via phpmon:// has been disabled.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = urls.first else { return }
|
guard let url = urls.first else { return }
|
||||||
|
|
||||||
self.interpretCommand(
|
self.interpretCommand(
|
||||||
url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""),
|
url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""),
|
||||||
commands: InterApp.getCommands()
|
commands: InterApp.getCommands()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func interpretCommand(_ command: String, commands: [InterApp.Action]) {
|
private func interpretCommand(_ command: String, commands: [InterApp.Action]) {
|
||||||
commands.forEach { action in
|
commands.forEach { action in
|
||||||
if command.starts(with: action.command) {
|
if command.starts(with: action.command) {
|
||||||
@@ -44,4 +44,3 @@ extension AppDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,20 +22,20 @@ import AppKit
|
|||||||
For more information about this, please see the ActivationPolicy-related extension.
|
For more information about this, please see the ActivationPolicy-related extension.
|
||||||
*/
|
*/
|
||||||
extension AppDelegate {
|
extension AppDelegate {
|
||||||
|
|
||||||
// MARK: - Menu Interactions
|
// MARK: - Menu Interactions
|
||||||
|
|
||||||
@IBAction func addSiteLinkPressed(_ sender: Any) {
|
@IBAction func addSiteLinkPressed(_ sender: Any) {
|
||||||
DomainListVC.show()
|
DomainListVC.show()
|
||||||
|
|
||||||
guard let windowController = App.shared.domainListWindowController else { return }
|
guard let windowController = App.shared.domainListWindowController else { return }
|
||||||
windowController.pressedAddLink(nil)
|
windowController.pressedAddLink(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func reloadDomainListPressed(_ sender: Any) {
|
@IBAction func reloadDomainListPressed(_ sender: Any) {
|
||||||
let vc = App.shared.domainListWindowController?
|
let vc = App.shared.domainListWindowController?
|
||||||
.window?.contentViewController as? DomainListVC
|
.window?.contentViewController as? DomainListVC
|
||||||
|
|
||||||
if vc != nil {
|
if vc != nil {
|
||||||
// If the view exists, directly reload the list of sites
|
// If the view exists, directly reload the list of sites
|
||||||
vc!.reloadDomains()
|
vc!.reloadDomains()
|
||||||
@@ -44,12 +44,12 @@ extension AppDelegate {
|
|||||||
Valet.shared.reloadSites()
|
Valet.shared.reloadSites()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func focusSearchField(_ sender: Any) {
|
@IBAction func focusSearchField(_ sender: Any) {
|
||||||
DomainListVC.show()
|
DomainListVC.show()
|
||||||
|
|
||||||
guard let windowController = App.shared.domainListWindowController else { return }
|
guard let windowController = App.shared.domainListWindowController else { return }
|
||||||
windowController.searchToolbarItem.searchField.becomeFirstResponder()
|
windowController.searchToolbarItem.searchField.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import Foundation
|
|||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
extension AppDelegate {
|
extension AppDelegate {
|
||||||
|
|
||||||
// MARK: - Notifications
|
// MARK: - Notifications
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Sets up notifications. That does mean we need to ask for permission first.
|
Sets up notifications. That does mean we need to ask for permission first.
|
||||||
If we cannot get permission, we should log this.
|
If we cannot get permission, we should log this.
|
||||||
@@ -30,7 +30,7 @@ extension AppDelegate {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Ensure that the application displays notifications even when the app is active.
|
Ensure that the application displays notifications even when the app is active.
|
||||||
*/
|
*/
|
||||||
@@ -42,5 +42,5 @@ extension AppDelegate {
|
|||||||
) {
|
) {
|
||||||
completionHandler([.banner])
|
completionHandler([.banner])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,55 +10,55 @@ import UserNotifications
|
|||||||
|
|
||||||
@NSApplicationMain
|
@NSApplicationMain
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
// MARK: - Variables
|
// MARK: - Variables
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The Shell singleton that keeps track of the history of all
|
The Shell singleton that keeps track of the history of all
|
||||||
(invoked by PHP Monitor) shell commands. It is used to
|
(invoked by PHP Monitor) shell commands. It is used to
|
||||||
invoke all commands in this application.
|
invoke all commands in this application.
|
||||||
*/
|
*/
|
||||||
let sharedShell: Shell
|
let sharedShell: Shell
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The App singleton contains information about the state of
|
The App singleton contains information about the state of
|
||||||
the application and global variables.
|
the application and global variables.
|
||||||
*/
|
*/
|
||||||
let state: App
|
let state: App
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The MainMenu singleton is responsible for rendering the
|
The MainMenu singleton is responsible for rendering the
|
||||||
menu bar item and its menu, as well as its actions.
|
menu bar item and its menu, as well as its actions.
|
||||||
*/
|
*/
|
||||||
let menu: MainMenu
|
let menu: MainMenu
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The paths singleton that determines where Homebrew is installed,
|
The paths singleton that determines where Homebrew is installed,
|
||||||
and where to look for binaries.
|
and where to look for binaries.
|
||||||
*/
|
*/
|
||||||
let paths: Paths
|
let paths: Paths
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The Valet singleton that determines all information
|
The Valet singleton that determines all information
|
||||||
about Valet and its current configuration.
|
about Valet and its current configuration.
|
||||||
*/
|
*/
|
||||||
let valet: Valet
|
let valet: Valet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The PhpEnv singleton that handles PHP version
|
The PhpEnv singleton that handles PHP version
|
||||||
detection, as well as switching. It is initialized
|
detection, as well as switching. It is initialized
|
||||||
when the app is ready and passed all checks.
|
when the app is ready and passed all checks.
|
||||||
*/
|
*/
|
||||||
var phpEnvironment: PhpEnv! = nil
|
var phpEnvironment: PhpEnv! = nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The logger is responsible for different levels of logging.
|
The logger is responsible for different levels of logging.
|
||||||
You can tweak the verbosity in the `init` method here.
|
You can tweak the verbosity in the `init` method here.
|
||||||
*/
|
*/
|
||||||
var logger = Log.shared
|
var logger = Log.shared
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When the application initializes, create all singletons.
|
When the application initializes, create all singletons.
|
||||||
*/
|
*/
|
||||||
@@ -78,13 +78,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
self.valet = Valet.shared
|
self.valet = Valet.shared
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initializeSwitcher() {
|
func initializeSwitcher() {
|
||||||
self.phpEnvironment = PhpEnv.shared
|
self.phpEnvironment = PhpEnv.shared
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When the application has finished launching, we'll want to set up
|
When the application has finished launching, we'll want to set up
|
||||||
the user notification center permissions, and kickoff the menu
|
the user notification center permissions, and kickoff the menu
|
||||||
@@ -96,5 +96,5 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
// Make sure the menu performs its initial checks
|
// Make sure the menu performs its initial checks
|
||||||
Task { await menu.startup() }
|
Task { await menu.startup() }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,18 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class InterApp {
|
class InterApp {
|
||||||
|
|
||||||
public static var bindings: [Action] = []
|
public static var bindings: [Action] = []
|
||||||
|
|
||||||
public static func register(_ action: Action) {
|
public static func register(_ action: Action) {
|
||||||
self.bindings.append(action)
|
self.bindings.append(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Action {
|
public struct Action {
|
||||||
let command: String
|
let command: String
|
||||||
let action: (String) -> Void
|
let action: (String) -> Void
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getCommands() -> [InterApp.Action] { return [
|
static func getCommands() -> [InterApp.Action] { return [
|
||||||
InterApp.Action(command: "list", action: { _ in
|
InterApp.Action(command: "list", action: { _ in
|
||||||
DomainListVC.show()
|
DomainListVC.show()
|
||||||
@@ -61,7 +61,7 @@ class InterApp {
|
|||||||
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
|
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
|
||||||
).withPrimary(text: "OK").show()
|
).withPrimary(text: "OK").show()
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
]}
|
]}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
class Startup {
|
class Startup {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks the user's environment and checks if PHP Monitor can be used properly.
|
Checks the user's environment and checks if PHP Monitor can be used properly.
|
||||||
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
|
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
|
||||||
@@ -17,30 +17,29 @@ class Startup {
|
|||||||
If this method returns false, there was a failed check and an alert was displayed.
|
If this method returns false, there was a failed check and an alert was displayed.
|
||||||
If this method returns true, then all checks succeeded and the app can continue.
|
If this method returns true, then all checks succeeded and the app can continue.
|
||||||
*/
|
*/
|
||||||
func checkEnvironment() async -> Bool
|
func checkEnvironment() async -> Bool {
|
||||||
{
|
|
||||||
// Do the important system setup checks
|
// Do the important system setup checks
|
||||||
Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)")
|
Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)")
|
||||||
|
|
||||||
for check in self.checks {
|
for check in self.checks {
|
||||||
if await check.succeeds() {
|
if await check.succeeds() {
|
||||||
Log.info("[OK] \(check.name)")
|
Log.info("[OK] \(check.name)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, something's gone wrong and the check has failed...
|
// If we get here, something's gone wrong and the check has failed...
|
||||||
Log.info("[FAIL] \(check.name)")
|
Log.info("[FAIL] \(check.name)")
|
||||||
showAlert(for: check)
|
showAlert(for: check)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, nothing has gone wrong. That's what we want!
|
// If we get here, nothing has gone wrong. That's what we want!
|
||||||
initializeSwitcher()
|
initializeSwitcher()
|
||||||
Log.separator(as: .info)
|
Log.separator(as: .info)
|
||||||
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
|
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Displays an alert for a particular check. There are two types of alerts:
|
Displays an alert for a particular check. There are two types of alerts:
|
||||||
- ones that require an app restart, which prompt the user to exit the app
|
- ones that require an app restart, which prompt the user to exit the app
|
||||||
@@ -59,7 +58,7 @@ class Startup {
|
|||||||
exit(1)
|
exit(1)
|
||||||
}).show()
|
}).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: check.titleText,
|
title: check.titleText,
|
||||||
@@ -70,7 +69,7 @@ class Startup {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Because the Switcher requires various environment guarantees, the switcher is only
|
Because the Switcher requires various environment guarantees, the switcher is only
|
||||||
initialized when it is done working. The switcher must be initialized on the main thread.
|
initialized when it is done working. The switcher must be initialized on the main thread.
|
||||||
@@ -81,9 +80,9 @@ class Startup {
|
|||||||
appDelegate.initializeSwitcher()
|
appDelegate.initializeSwitcher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Check (List)
|
// MARK: - Check (List)
|
||||||
|
|
||||||
public var checks: [EnvironmentCheck] = [
|
public var checks: [EnvironmentCheck] = [
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
// The Homebrew binary must exist.
|
// The Homebrew binary must exist.
|
||||||
@@ -196,9 +195,9 @@ class Startup {
|
|||||||
descriptionText: "startup.errors.valet_version_unknown.desc".localized
|
descriptionText: "startup.errors.valet_version_unknown.desc".localized
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
// MARK: - EnvironmentCheck struct
|
// MARK: - EnvironmentCheck struct
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary.
|
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary.
|
||||||
Checks that require an app restart will always lead to an alert and app termination shortly after.
|
Checks that require an app restart will always lead to an alert and app termination shortly after.
|
||||||
@@ -211,7 +210,7 @@ class Startup {
|
|||||||
let descriptionText: String
|
let descriptionText: String
|
||||||
let buttonText: String
|
let buttonText: String
|
||||||
let requiresAppRestart: Bool
|
let requiresAppRestart: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
command: @escaping () async -> Bool,
|
command: @escaping () async -> Bool,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -229,7 +228,7 @@ class Startup {
|
|||||||
self.buttonText = buttonText
|
self.buttonText = buttonText
|
||||||
self.requiresAppRestart = requiresAppRestart
|
self.requiresAppRestart = requiresAppRestart
|
||||||
}
|
}
|
||||||
|
|
||||||
public func succeeds() async -> Bool {
|
public func succeeds() async -> Bool {
|
||||||
return await !self.command()
|
return await !self.command()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,43 +10,43 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class AddProxyVC: NSViewController, NSTextFieldDelegate {
|
class AddProxyVC: NSViewController, NSTextFieldDelegate {
|
||||||
|
|
||||||
// MARK: - Outlets
|
// MARK: - Outlets
|
||||||
|
|
||||||
@IBOutlet weak var textFieldTitle: NSTextField!
|
@IBOutlet weak var textFieldTitle: NSTextField!
|
||||||
@IBOutlet weak var textFieldProxySubject: NSTextField!
|
@IBOutlet weak var textFieldProxySubject: NSTextField!
|
||||||
@IBOutlet weak var textFieldDomainName: NSTextField!
|
@IBOutlet weak var textFieldDomainName: NSTextField!
|
||||||
|
|
||||||
@IBOutlet weak var inputProxySubject: NSTextField!
|
@IBOutlet weak var inputProxySubject: NSTextField!
|
||||||
@IBOutlet weak var inputDomainName: NSTextField!
|
@IBOutlet weak var inputDomainName: NSTextField!
|
||||||
|
|
||||||
@IBOutlet weak var previewText: NSTextField!
|
@IBOutlet weak var previewText: NSTextField!
|
||||||
|
|
||||||
@IBOutlet weak var buttonSecure: NSButton!
|
@IBOutlet weak var buttonSecure: NSButton!
|
||||||
@IBOutlet weak var buttonCreateProxy: NSButton!
|
@IBOutlet weak var buttonCreateProxy: NSButton!
|
||||||
@IBOutlet weak var buttonCancel: NSButton!
|
@IBOutlet weak var buttonCancel: NSButton!
|
||||||
|
|
||||||
@IBOutlet weak var textFieldSecure: NSTextField!
|
@IBOutlet weak var textFieldSecure: NSTextField!
|
||||||
@IBOutlet weak var textFieldError: NSTextField!
|
@IBOutlet weak var textFieldError: NSTextField!
|
||||||
|
|
||||||
// MARK: - View Lifecycle
|
// MARK: - View Lifecycle
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
loadStaticLocalisedStrings()
|
loadStaticLocalisedStrings()
|
||||||
|
|
||||||
buttonCreateProxy.isEnabled = false
|
buttonCreateProxy.isEnabled = false
|
||||||
updatePreview()
|
updatePreview()
|
||||||
validate()
|
validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dismissView(outcome: NSApplication.ModalResponse) {
|
private func dismissView(outcome: NSApplication.ModalResponse) {
|
||||||
guard let window = view.window, let parent = window.sheetParent else { return }
|
guard let window = view.window, let parent = window.sheetParent else { return }
|
||||||
parent.endSheet(window, returnCode: outcome)
|
parent.endSheet(window, returnCode: outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Localisation
|
// MARK: - Localisation
|
||||||
|
|
||||||
func loadStaticLocalisedStrings() {
|
func loadStaticLocalisedStrings() {
|
||||||
textFieldTitle.stringValue = "domain_list.add.set_up_proxy".localized
|
textFieldTitle.stringValue = "domain_list.add.set_up_proxy".localized
|
||||||
textFieldProxySubject.stringValue = "domain_list.add.proxy_subject".localized
|
textFieldProxySubject.stringValue = "domain_list.add.proxy_subject".localized
|
||||||
@@ -55,101 +55,101 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
|
|||||||
buttonCancel.title = "domain_list.add.cancel".localized
|
buttonCancel.title = "domain_list.add.cancel".localized
|
||||||
buttonCreateProxy.title = "domain_list.add.create_proxy".localized
|
buttonCreateProxy.title = "domain_list.add.create_proxy".localized
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Outlet Interactions
|
// MARK: - Outlet Interactions
|
||||||
|
|
||||||
@IBAction func pressedSecure(_ sender: Any) {
|
@IBAction func pressedSecure(_ sender: Any) {
|
||||||
updatePreview()
|
updatePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func pressedCreateProxy(_ sender: Any) {
|
@IBAction func pressedCreateProxy(_ sender: Any) {
|
||||||
// TODO: Validate the input before allowing proxy creation
|
// TODO: Validate the input before allowing proxy creation
|
||||||
|
|
||||||
let domain = self.inputDomainName.stringValue
|
let domain = self.inputDomainName.stringValue
|
||||||
let proxyName = self.inputProxySubject.stringValue
|
let proxyName = self.inputProxySubject.stringValue
|
||||||
let secure = self.buttonSecure.state == .on ? " --secure" : ""
|
let secure = self.buttonSecure.state == .on ? " --secure" : ""
|
||||||
|
|
||||||
dismissView(outcome: .OK)
|
dismissView(outcome: .OK)
|
||||||
|
|
||||||
App.shared.domainListWindowController?.contentVC.setUIBusy()
|
App.shared.domainListWindowController?.contentVC.setUIBusy()
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
Shell.run("\(Paths.valet) proxy \(domain) \(proxyName)\(secure)", requiresPath: true)
|
Shell.run("\(Paths.valet) proxy \(domain) \(proxyName)\(secure)", requiresPath: true)
|
||||||
Actions.restartNginx()
|
Actions.restartNginx()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
App.shared.domainListWindowController?.contentVC.setUINotBusy()
|
App.shared.domainListWindowController?.contentVC.setUINotBusy()
|
||||||
App.shared.domainListWindowController?.pressedReload(nil)
|
App.shared.domainListWindowController?.pressedReload(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func pressedCancel(_ sender: Any) {
|
@IBAction func pressedCancel(_ sender: Any) {
|
||||||
dismissView(outcome: .cancel)
|
dismissView(outcome: .cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Text Field Delegate
|
// MARK: - Text Field Delegate
|
||||||
|
|
||||||
func controlTextDidChange(_ obj: Notification) {
|
func controlTextDidChange(_ obj: Notification) {
|
||||||
updateTextField()
|
updateTextField()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
private func validate() {
|
private func validate() {
|
||||||
_ = validate(
|
_ = validate(
|
||||||
domain: inputDomainName.stringValue,
|
domain: inputDomainName.stringValue,
|
||||||
proxy: inputProxySubject.stringValue
|
proxy: inputProxySubject.stringValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func validate(domain: String, proxy: String) -> Bool {
|
private func validate(domain: String, proxy: String) -> Bool {
|
||||||
if domain.isEmpty {
|
if domain.isEmpty {
|
||||||
textFieldError.isHidden = false
|
textFieldError.isHidden = false
|
||||||
textFieldError.stringValue = "domain_list.add.errors.empty".localized
|
textFieldError.stringValue = "domain_list.add.errors.empty".localized
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if proxy.isEmpty {
|
if proxy.isEmpty {
|
||||||
textFieldError.isHidden = false
|
textFieldError.isHidden = false
|
||||||
textFieldError.stringValue = "domain_list.add.errors.empty_proxy".localized
|
textFieldError.stringValue = "domain_list.add.errors.empty_proxy".localized
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if Valet.shared.sites.contains(where: { $0.name == domain }) {
|
if Valet.shared.sites.contains(where: { $0.name == domain }) {
|
||||||
textFieldError.isHidden = false
|
textFieldError.isHidden = false
|
||||||
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
|
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
textFieldError.isHidden = true
|
textFieldError.isHidden = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTextField() {
|
func updateTextField() {
|
||||||
inputDomainName.stringValue = inputDomainName.stringValue
|
inputDomainName.stringValue = inputDomainName.stringValue
|
||||||
.replacingOccurrences(of: " ", with: "-")
|
.replacingOccurrences(of: " ", with: "-")
|
||||||
|
|
||||||
buttonCreateProxy.isEnabled = validate(
|
buttonCreateProxy.isEnabled = validate(
|
||||||
domain: inputDomainName.stringValue,
|
domain: inputDomainName.stringValue,
|
||||||
proxy: inputProxySubject.stringValue
|
proxy: inputProxySubject.stringValue
|
||||||
)
|
)
|
||||||
|
|
||||||
updatePreview()
|
updatePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePreview() {
|
func updatePreview() {
|
||||||
buttonSecure.title = "domain_list.add.secure_after_creation"
|
buttonSecure.title = "domain_list.add.secure_after_creation"
|
||||||
.localized(
|
.localized(
|
||||||
inputDomainName.stringValue,
|
inputDomainName.stringValue,
|
||||||
Valet.shared.config.tld
|
Valet.shared.config.tld
|
||||||
)
|
)
|
||||||
|
|
||||||
if (inputProxySubject.stringValue.isEmpty || inputDomainName.stringValue.isEmpty) {
|
if inputProxySubject.stringValue.isEmpty || inputDomainName.stringValue.isEmpty {
|
||||||
previewText.stringValue = "domain_list.add.empty_fields".localized
|
previewText.stringValue = "domain_list.add.empty_fields".localized
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
previewText.stringValue = "domain_list.add.proxy_available"
|
previewText.stringValue = "domain_list.add.proxy_available"
|
||||||
.localized(
|
.localized(
|
||||||
inputProxySubject.stringValue,
|
inputProxySubject.stringValue,
|
||||||
@@ -158,5 +158,5 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
|
|||||||
Valet.shared.config.tld
|
Valet.shared.config.tld
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,37 +10,37 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
||||||
|
|
||||||
// MARK: - Outlets
|
// MARK: - Outlets
|
||||||
|
|
||||||
@IBOutlet weak var textFieldTitle: NSTextField!
|
@IBOutlet weak var textFieldTitle: NSTextField!
|
||||||
|
|
||||||
@IBOutlet weak var pathControl: NSPathControl!
|
@IBOutlet weak var pathControl: NSPathControl!
|
||||||
@IBOutlet weak var inputDomainName: NSTextField!
|
@IBOutlet weak var inputDomainName: NSTextField!
|
||||||
|
|
||||||
@IBOutlet weak var previewText: NSTextField!
|
@IBOutlet weak var previewText: NSTextField!
|
||||||
|
|
||||||
@IBOutlet weak var buttonSecure: NSButton!
|
@IBOutlet weak var buttonSecure: NSButton!
|
||||||
@IBOutlet weak var buttonCreateLink: NSButton!
|
@IBOutlet weak var buttonCreateLink: NSButton!
|
||||||
@IBOutlet weak var buttonCancel: NSButton!
|
@IBOutlet weak var buttonCancel: NSButton!
|
||||||
|
|
||||||
@IBOutlet weak var textFieldSecure: NSTextField!
|
@IBOutlet weak var textFieldSecure: NSTextField!
|
||||||
@IBOutlet weak var textFieldError: NSTextField!
|
@IBOutlet weak var textFieldError: NSTextField!
|
||||||
|
|
||||||
// MARK: - View Lifecycle
|
// MARK: - View Lifecycle
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
loadStaticLocalisedStrings()
|
loadStaticLocalisedStrings()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dismissView(outcome: NSApplication.ModalResponse) {
|
private func dismissView(outcome: NSApplication.ModalResponse) {
|
||||||
guard let window = self.view.window, let parent = window.sheetParent else { return }
|
guard let window = self.view.window, let parent = window.sheetParent else { return }
|
||||||
parent.endSheet(window, returnCode: outcome)
|
parent.endSheet(window, returnCode: outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Localisation
|
// MARK: - Localisation
|
||||||
|
|
||||||
func loadStaticLocalisedStrings() {
|
func loadStaticLocalisedStrings() {
|
||||||
textFieldTitle.stringValue = "domain_list.add.link_folder".localized
|
textFieldTitle.stringValue = "domain_list.add.link_folder".localized
|
||||||
inputDomainName.placeholderString = "domain_list.add.domain_name_placeholder".localized
|
inputDomainName.placeholderString = "domain_list.add.domain_name_placeholder".localized
|
||||||
@@ -48,13 +48,13 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
|||||||
buttonCancel.title = "domain_list.add.cancel".localized
|
buttonCancel.title = "domain_list.add.cancel".localized
|
||||||
buttonCreateLink.title = "domain_list.add.create_link".localized
|
buttonCreateLink.title = "domain_list.add.create_link".localized
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Outlet Interactions
|
// MARK: - Outlet Interactions
|
||||||
|
|
||||||
@IBAction func pressedCreateLink(_ sender: Any) {
|
@IBAction func pressedCreateLink(_ sender: Any) {
|
||||||
let path = pathControl.url!.path
|
let path = pathControl.url!.path
|
||||||
let name = inputDomainName.stringValue
|
let name = inputDomainName.stringValue
|
||||||
|
|
||||||
if !FileManager.default.fileExists(atPath: path) {
|
if !FileManager.default.fileExists(atPath: path) {
|
||||||
Alert.confirm(
|
Alert.confirm(
|
||||||
onWindow: view.window!,
|
onWindow: view.window!,
|
||||||
@@ -68,18 +68,18 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adding `valet links` is a workaround for Valet malforming the config.json file
|
// Adding `valet links` is a workaround for Valet malforming the config.json file
|
||||||
// TODO: I will have to investigate and report this behaviour if possible
|
// TODO: I will have to investigate and report this behaviour if possible
|
||||||
Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links", requiresPath: true)
|
Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links", requiresPath: true)
|
||||||
|
|
||||||
dismissView(outcome: .OK)
|
dismissView(outcome: .OK)
|
||||||
|
|
||||||
// Reset search
|
// Reset search
|
||||||
App.shared.domainListWindowController?
|
App.shared.domainListWindowController?
|
||||||
.searchToolbarItem
|
.searchToolbarItem
|
||||||
.searchField.stringValue = ""
|
.searchField.stringValue = ""
|
||||||
|
|
||||||
// Add the new item and scrolls to it
|
// Add the new item and scrolls to it
|
||||||
App.shared.domainListWindowController?
|
App.shared.domainListWindowController?
|
||||||
.contentVC
|
.contentVC
|
||||||
@@ -88,60 +88,60 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
|||||||
secure: buttonSecure.state == .on
|
secure: buttonSecure.state == .on
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func pressedCancel(_ sender: Any) {
|
@IBAction func pressedCancel(_ sender: Any) {
|
||||||
dismissView(outcome: .cancel)
|
dismissView(outcome: .cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func pressedSecure(_ sender: Any) {
|
@IBAction func pressedSecure(_ sender: Any) {
|
||||||
updatePreview()
|
updatePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Text Field Delegate
|
// MARK: - Text Field Delegate
|
||||||
|
|
||||||
func controlTextDidChange(_ obj: Notification) {
|
func controlTextDidChange(_ obj: Notification) {
|
||||||
updateTextField()
|
updateTextField()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
private func isValidLinkName(_ name: String) -> Bool {
|
private func isValidLinkName(_ name: String) -> Bool {
|
||||||
if name.isEmpty {
|
if name.isEmpty {
|
||||||
textFieldError.isHidden = false
|
textFieldError.isHidden = false
|
||||||
textFieldError.stringValue = "domain_list.add.errors.empty".localized
|
textFieldError.stringValue = "domain_list.add.errors.empty".localized
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if Valet.shared.sites.contains(where: { $0.name == name }) {
|
if Valet.shared.sites.contains(where: { $0.name == name }) {
|
||||||
textFieldError.isHidden = false
|
textFieldError.isHidden = false
|
||||||
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
|
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
textFieldError.isHidden = true
|
textFieldError.isHidden = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTextField() {
|
func updateTextField() {
|
||||||
inputDomainName.stringValue = inputDomainName.stringValue
|
inputDomainName.stringValue = inputDomainName.stringValue
|
||||||
.replacingOccurrences(of: " ", with: "-")
|
.replacingOccurrences(of: " ", with: "-")
|
||||||
|
|
||||||
buttonCreateLink.isEnabled = isValidLinkName(inputDomainName.stringValue)
|
buttonCreateLink.isEnabled = isValidLinkName(inputDomainName.stringValue)
|
||||||
updatePreview()
|
updatePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePreview() {
|
func updatePreview() {
|
||||||
buttonSecure.title = "domain_list.add.secure_after_creation"
|
buttonSecure.title = "domain_list.add.secure_after_creation"
|
||||||
.localized(
|
.localized(
|
||||||
inputDomainName.stringValue,
|
inputDomainName.stringValue,
|
||||||
Valet.shared.config.tld
|
Valet.shared.config.tld
|
||||||
)
|
)
|
||||||
|
|
||||||
if (inputDomainName.stringValue.isEmpty) {
|
if inputDomainName.stringValue.isEmpty {
|
||||||
previewText.stringValue = "domain_list.add.empty_fields".localized
|
previewText.stringValue = "domain_list.add.empty_fields".localized
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
previewText.stringValue = "domain_list.add.folder_available"
|
previewText.stringValue = "domain_list.add.folder_available"
|
||||||
.localized(
|
.localized(
|
||||||
buttonSecure.state == .on ? "https" : "http",
|
buttonSecure.state == .on ? "https" : "http",
|
||||||
|
|||||||
@@ -9,12 +9,11 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
class DomainListKindCell: NSTableCellView, DomainListCellProtocol
|
class DomainListKindCell: NSTableCellView, DomainListCellProtocol {
|
||||||
{
|
|
||||||
static let reusableName = "domainListKindCell"
|
static let reusableName = "domainListKindCell"
|
||||||
|
|
||||||
@IBOutlet weak var imageViewType: NSImageView!
|
@IBOutlet weak var imageViewType: NSImageView!
|
||||||
|
|
||||||
func populateCell(with site: ValetSite) {
|
func populateCell(with site: ValetSite) {
|
||||||
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
|
||||||
imageViewType.image = NSImage(
|
imageViewType.image = NSImage(
|
||||||
@@ -22,15 +21,15 @@ class DomainListKindCell: NSTableCellView, DomainListCellProtocol
|
|||||||
? "IconParked"
|
? "IconParked"
|
||||||
: "IconLinked"
|
: "IconLinked"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Unless, of course, this is a default site
|
// Unless, of course, this is a default site
|
||||||
if site.absolutePath == Valet.shared.config.defaultSite {
|
if site.absolutePath == Valet.shared.config.defaultSite {
|
||||||
imageViewType.image = NSImage(named: "IconDefault")
|
imageViewType.image = NSImage(named: "IconDefault")
|
||||||
}
|
}
|
||||||
|
|
||||||
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateCell(with proxy: ValetProxy) {
|
func populateCell(with proxy: ValetProxy) {
|
||||||
imageViewType.image = NSImage(named: "IconProxy")
|
imageViewType.image = NSImage(named: "IconProxy")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,17 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
class DomainListNameCell: NSTableCellView, DomainListCellProtocol
|
class DomainListNameCell: NSTableCellView, DomainListCellProtocol {
|
||||||
{
|
|
||||||
static let reusableName = "domainListNameCell"
|
static let reusableName = "domainListNameCell"
|
||||||
|
|
||||||
@IBOutlet weak var labelSiteName: NSTextField!
|
@IBOutlet weak var labelSiteName: NSTextField!
|
||||||
@IBOutlet weak var labelPathName: NSTextField!
|
@IBOutlet weak var labelPathName: NSTextField!
|
||||||
|
|
||||||
func populateCell(with site: ValetSite) {
|
func populateCell(with site: ValetSite) {
|
||||||
labelSiteName.stringValue = "\(site.name).\(site.tld)"
|
labelSiteName.stringValue = "\(site.name).\(site.tld)"
|
||||||
labelPathName.stringValue = site.absolutePathRelative
|
labelPathName.stringValue = site.absolutePathRelative
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateCell(with proxy: ValetProxy) {
|
func populateCell(with proxy: ValetProxy) {
|
||||||
labelSiteName.stringValue = "\(proxy.domain).\(proxy.tld)"
|
labelSiteName.stringValue = "\(proxy.domain).\(proxy.tld)"
|
||||||
labelPathName.stringValue = proxy.target
|
labelPathName.stringValue = proxy.target
|
||||||
|
|||||||
@@ -9,22 +9,21 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
|
class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||||
{
|
|
||||||
static let reusableName = "domainListPhpCell"
|
static let reusableName = "domainListPhpCell"
|
||||||
|
|
||||||
var site: ValetSite? = nil
|
var site: ValetSite?
|
||||||
|
|
||||||
@IBOutlet weak var buttonPhpVersion: NSButton!
|
@IBOutlet weak var buttonPhpVersion: NSButton!
|
||||||
@IBOutlet weak var imageViewPhpVersionOK: NSImageView!
|
@IBOutlet weak var imageViewPhpVersionOK: NSImageView!
|
||||||
|
|
||||||
func populateCell(with site: ValetSite) {
|
func populateCell(with site: ValetSite) {
|
||||||
self.site = site
|
self.site = site
|
||||||
|
|
||||||
buttonPhpVersion.title = " PHP \(site.servingPhpVersion)"
|
buttonPhpVersion.title = " PHP \(site.servingPhpVersion)"
|
||||||
|
|
||||||
imageViewPhpVersionOK.toolTip = nil
|
imageViewPhpVersionOK.toolTip = nil
|
||||||
|
|
||||||
if site.isolatedPhpVersion != nil {
|
if site.isolatedPhpVersion != nil {
|
||||||
imageViewPhpVersionOK.isHidden = false
|
imageViewPhpVersionOK.isHidden = false
|
||||||
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
|
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
|
||||||
@@ -34,45 +33,45 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
|
|||||||
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
|
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
|
||||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp)
|
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp)
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonPhpVersion.isHidden = false
|
buttonPhpVersion.isHidden = false
|
||||||
imageViewPhpVersionOK.isHidden = false
|
imageViewPhpVersionOK.isHidden = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateCell(with proxy: ValetProxy) {
|
func populateCell(with proxy: ValetProxy) {
|
||||||
buttonPhpVersion.isHidden = true
|
buttonPhpVersion.isHidden = true
|
||||||
imageViewPhpVersionOK.isHidden = true
|
imageViewPhpVersionOK.isHidden = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func pressedPhpVersion(_ sender: Any) {
|
@IBAction func pressedPhpVersion(_ sender: Any) {
|
||||||
guard let site = self.site else { return }
|
guard let site = self.site else { return }
|
||||||
|
|
||||||
let alert = NSAlert.init()
|
let alert = NSAlert.init()
|
||||||
alert.alertStyle = .informational
|
alert.alertStyle = .informational
|
||||||
|
|
||||||
var information = ""
|
var information = ""
|
||||||
|
|
||||||
if (self.site?.isolatedPhpVersion != nil) {
|
if self.site?.isolatedPhpVersion != nil {
|
||||||
information += "alert.composer_php_isolated.desc".localized(
|
information += "alert.composer_php_isolated.desc".localized(
|
||||||
self.site!.isolatedPhpVersion!.versionNumber.homebrewVersion,
|
self.site!.isolatedPhpVersion!.versionNumber.homebrewVersion,
|
||||||
PhpEnv.phpInstall.version.short
|
PhpEnv.phpInstall.version.short
|
||||||
)
|
)
|
||||||
information += "\n\n"
|
information += "\n\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)"
|
information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)"
|
||||||
.localized
|
.localized
|
||||||
|
|
||||||
alert.messageText = "alert.composer_php_requirement.title"
|
alert.messageText = "alert.composer_php_requirement.title"
|
||||||
.localized("\(site.name).\(Valet.shared.config.tld)", site.composerPhp)
|
.localized("\(site.name).\(Valet.shared.config.tld)", site.composerPhp)
|
||||||
alert.informativeText = information
|
alert.informativeText = information
|
||||||
|
|
||||||
alert.addButton(withTitle: "site_link.close".localized)
|
alert.addButton(withTitle: "site_link.close".localized)
|
||||||
|
|
||||||
var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue
|
var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue
|
||||||
var map: [Int: String] = [:]
|
var map: [Int: String] = [:]
|
||||||
|
|
||||||
if site.isolatedPhpVersion == nil {
|
if site.isolatedPhpVersion == nil {
|
||||||
// Determine which installed versions would be ideal to switch to,
|
// Determine which installed versions would be ideal to switch to,
|
||||||
// but make sure to exclude the currently linked version
|
// but make sure to exclude the currently linked version
|
||||||
@@ -83,7 +82,7 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
|
|||||||
map[mapIndex] = version.homebrewVersion
|
map[mapIndex] = version.homebrewVersion
|
||||||
mapIndex += 1
|
mapIndex += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Site is not isolated, show options to switch global PHP version
|
// Site is not isolated, show options to switch global PHP version
|
||||||
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) { response in
|
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) { response in
|
||||||
if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue {
|
if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue {
|
||||||
@@ -99,5 +98,5 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
|
|||||||
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!)
|
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,17 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
class DomainListTLSCell: NSTableCellView, DomainListCellProtocol
|
class DomainListTLSCell: NSTableCellView, DomainListCellProtocol {
|
||||||
{
|
|
||||||
static let reusableName = "domainListTLSCell"
|
static let reusableName = "domainListTLSCell"
|
||||||
|
|
||||||
@IBOutlet weak var imageViewLock: NSImageView!
|
@IBOutlet weak var imageViewLock: NSImageView!
|
||||||
|
|
||||||
func populateCell(with site: ValetSite) {
|
func populateCell(with site: ValetSite) {
|
||||||
imageViewLock.contentTintColor = site.secured
|
imageViewLock.contentTintColor = site.secured
|
||||||
? NSColor(named: "IconColorGreen")
|
? NSColor(named: "IconColorGreen")
|
||||||
: NSColor(named: "IconColorRed")
|
: NSColor(named: "IconColorRed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateCell(with proxy: ValetProxy) {
|
func populateCell(with proxy: ValetProxy) {
|
||||||
imageViewLock.contentTintColor = proxy.secured
|
imageViewLock.contentTintColor = proxy.secured
|
||||||
? NSColor(named: "IconColorGreen")
|
? NSColor(named: "IconColorGreen")
|
||||||
|
|||||||
@@ -9,26 +9,25 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
class DomainListTypeCell: NSTableCellView, DomainListCellProtocol
|
class DomainListTypeCell: NSTableCellView, DomainListCellProtocol {
|
||||||
{
|
|
||||||
static let reusableName = "domainListTypeCell"
|
static let reusableName = "domainListTypeCell"
|
||||||
|
|
||||||
@IBOutlet weak var labelDriver: NSTextField!
|
@IBOutlet weak var labelDriver: NSTextField!
|
||||||
@IBOutlet weak var labelPhpVersion: NSTextField!
|
@IBOutlet weak var labelPhpVersion: NSTextField!
|
||||||
|
|
||||||
func populateCell(with site: ValetSite) {
|
func populateCell(with site: ValetSite) {
|
||||||
labelDriver.stringValue = site.driver ?? "driver.not_detected".localized
|
labelDriver.stringValue = site.driver ?? "driver.not_detected".localized
|
||||||
|
|
||||||
// Determine the Laravel version
|
// Determine the Laravel version
|
||||||
if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") {
|
if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") {
|
||||||
let constraint = site.notableComposerDependencies["laravel/framework"]!
|
let constraint = site.notableComposerDependencies["laravel/framework"]!
|
||||||
labelDriver.stringValue = "Laravel (\(constraint))"
|
labelDriver.stringValue = "Laravel (\(constraint))"
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHP version
|
// PHP version
|
||||||
labelPhpVersion.stringValue = site.composerPhp == "???" ? "Any PHP" : "PHP \(site.composerPhp)"
|
labelPhpVersion.stringValue = site.composerPhp == "???" ? "Any PHP" : "PHP \(site.composerPhp)"
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateCell(with proxy: ValetProxy) {
|
func populateCell(with proxy: ValetProxy) {
|
||||||
labelDriver.stringValue = "Proxy"
|
labelDriver.stringValue = "Proxy"
|
||||||
labelPhpVersion.stringValue = "Active"
|
labelPhpVersion.stringValue = "Active"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ extension DomainListVC {
|
|||||||
let action = selectedSite!.secured ? "unsecure" : "secure"
|
let action = selectedSite!.secured ? "unsecure" : "secure"
|
||||||
let selectedSite = selectedSite!
|
let selectedSite = selectedSite!
|
||||||
let command = "cd '\(selectedSite.absolutePath)' && sudo \(Paths.valet) \(action) && exit;"
|
let command = "cd '\(selectedSite.absolutePath)' && sudo \(Paths.valet) \(action) && exit;"
|
||||||
|
|
||||||
waitAndExecute {
|
waitAndExecute {
|
||||||
Shell.run(command, requiresPath: true)
|
Shell.run(command, requiresPath: true)
|
||||||
} completion: { [self] in
|
} completion: { [self] in
|
||||||
@@ -41,18 +41,18 @@ extension DomainListVC {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4])
|
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4])
|
||||||
tableView.deselectRow(rowToReload)
|
tableView.deselectRow(rowToReload)
|
||||||
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
|
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openInBrowser() {
|
@objc func openInBrowser() {
|
||||||
guard let selected = self.selected else {
|
guard let selected = self.selected else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = selected.getListableUrl() else {
|
guard let url = selected.getListableUrl() else {
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
@@ -63,29 +63,29 @@ extension DomainListVC {
|
|||||||
.show()
|
.show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openInFinder() {
|
@objc func openInFinder() {
|
||||||
Shell.run("open '\(selectedSite!.absolutePath)'")
|
Shell.run("open '\(selectedSite!.absolutePath)'")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openInTerminal() {
|
@objc func openInTerminal() {
|
||||||
Shell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath)'")
|
Shell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath)'")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openWithEditor(sender: EditorMenuItem) {
|
@objc func openWithEditor(sender: EditorMenuItem) {
|
||||||
guard let editor = sender.editor else { return }
|
guard let editor = sender.editor else { return }
|
||||||
editor.openDirectory(file: selectedSite!.absolutePath)
|
editor.openDirectory(file: selectedSite!.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func isolateSite(sender: PhpMenuItem) {
|
@objc func isolateSite(sender: PhpMenuItem) {
|
||||||
let command = "sudo \(Paths.valet) isolate php@\(sender.version) --site '\(self.selectedSite!.name)' && exit;"
|
let command = "sudo \(Paths.valet) isolate php@\(sender.version) --site '\(self.selectedSite!.name)' && exit;"
|
||||||
|
|
||||||
self.performAction(command: command) {
|
self.performAction(command: command) {
|
||||||
self.selectedSite!.determineIsolated()
|
self.selectedSite!.determineIsolated()
|
||||||
|
|
||||||
if self.selectedSite!.isolatedPhpVersion == nil {
|
if self.selectedSite!.isolatedPhpVersion == nil {
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
@@ -98,22 +98,22 @@ extension DomainListVC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func removeIsolatedSite() {
|
@objc func removeIsolatedSite() {
|
||||||
self.performAction(command: "sudo \(Paths.valet) unisolate --site '\(self.selectedSite!.name)' && exit;") {
|
self.performAction(command: "sudo \(Paths.valet) unisolate --site '\(self.selectedSite!.name)' && exit;") {
|
||||||
self.selectedSite!.isolatedPhpVersion = nil
|
self.selectedSite!.isolatedPhpVersion = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func unlinkSite() {
|
@objc func unlinkSite() {
|
||||||
guard let site = selectedSite else {
|
guard let site = selectedSite else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if site.aliasPath == nil {
|
if site.aliasPath == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Alert.confirm(
|
Alert.confirm(
|
||||||
onWindow: view.window!,
|
onWindow: view.window!,
|
||||||
messageText: "domain_list.confirm_unlink".localized(site.name),
|
messageText: "domain_list.confirm_unlink".localized(site.name),
|
||||||
@@ -127,12 +127,12 @@ extension DomainListVC {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func removeProxy() {
|
@objc func removeProxy() {
|
||||||
guard let proxy = selectedProxy else {
|
guard let proxy = selectedProxy else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Alert.confirm(
|
Alert.confirm(
|
||||||
onWindow: view.window!,
|
onWindow: view.window!,
|
||||||
messageText: "domain_list.confirm_unproxy".localized("\(proxy.domain).\(proxy.tld)"),
|
messageText: "domain_list.confirm_unproxy".localized("\(proxy.domain).\(proxy.tld)"),
|
||||||
@@ -146,10 +146,10 @@ extension DomainListVC {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performAction(command: String, beforeCellReload: @escaping () -> Void) {
|
private func performAction(command: String, beforeCellReload: @escaping () -> Void) {
|
||||||
let rowToReload = tableView.selectedRow
|
let rowToReload = tableView.selectedRow
|
||||||
|
|
||||||
waitAndExecute {
|
waitAndExecute {
|
||||||
Shell.run(command, requiresPath: true)
|
Shell.run(command, requiresPath: true)
|
||||||
} completion: { [self] in
|
} completion: { [self] in
|
||||||
@@ -159,5 +159,5 @@ extension DomainListVC {
|
|||||||
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
|
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
extension DomainListVC {
|
extension DomainListVC {
|
||||||
|
|
||||||
internal func reloadContextMenu() {
|
internal func reloadContextMenu() {
|
||||||
guard let selected = selected else {
|
guard let selected = selected else {
|
||||||
tableView.menu = nil
|
tableView.menu = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let selected = selected as? ValetSite {
|
if let selected = selected as? ValetSite {
|
||||||
addMenuItemsForSite(selected)
|
addMenuItemsForSite(selected)
|
||||||
return
|
return
|
||||||
@@ -25,29 +25,29 @@ extension DomainListVC {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Menu Items for Site
|
// MARK: - Menu Items for Site
|
||||||
|
|
||||||
private func addMenuItemsForSite(_ site: ValetSite) {
|
private func addMenuItemsForSite(_ site: ValetSite) {
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
|
|
||||||
addSystemApps(to: menu)
|
addSystemApps(to: menu)
|
||||||
addSeparator(to: menu)
|
addSeparator(to: menu)
|
||||||
addDetectedApps(to: menu)
|
addDetectedApps(to: menu)
|
||||||
addSeparator(to: menu)
|
addSeparator(to: menu)
|
||||||
|
|
||||||
if Valet.enabled(feature: .isolatedSites) {
|
if Valet.enabled(feature: .isolatedSites) {
|
||||||
addIsolate(to: menu, with: site)
|
addIsolate(to: menu, with: site)
|
||||||
} else {
|
} else {
|
||||||
addDisabledIsolation(to: menu)
|
addDisabledIsolation(to: menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
addUnlink(to: menu, with: site)
|
addUnlink(to: menu, with: site)
|
||||||
addToggleSecure(to: menu, with: site)
|
addToggleSecure(to: menu, with: site)
|
||||||
|
|
||||||
tableView.menu = menu
|
tableView.menu = menu
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addSystemApps(to menu: NSMenu) {
|
private func addSystemApps(to menu: NSMenu) {
|
||||||
menu.addItem(withTitle: "domain_list.system_apps".localized, action: nil, keyEquivalent: "")
|
menu.addItem(withTitle: "domain_list.system_apps".localized, action: nil, keyEquivalent: "")
|
||||||
menu.addItem(
|
menu.addItem(
|
||||||
@@ -66,13 +66,13 @@ extension DomainListVC {
|
|||||||
keyEquivalent: "B"
|
keyEquivalent: "B"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addDetectedApps(to menu: NSMenu) {
|
private func addDetectedApps(to menu: NSMenu) {
|
||||||
if (applications.count > 0) {
|
if !applications.isEmpty {
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
menu.addItem(withTitle: "domain_list.detected_apps".localized, action: nil, keyEquivalent: "")
|
menu.addItem(withTitle: "domain_list.detected_apps".localized, action: nil, keyEquivalent: "")
|
||||||
|
|
||||||
for (_, editor) in applications.enumerated() {
|
for editor in applications {
|
||||||
let editorMenuItem = EditorMenuItem(
|
let editorMenuItem = EditorMenuItem(
|
||||||
title: "Open with \(editor.name)",
|
title: "Open with \(editor.name)",
|
||||||
action: #selector(self.openWithEditor(sender:)),
|
action: #selector(self.openWithEditor(sender:)),
|
||||||
@@ -83,9 +83,9 @@ extension DomainListVC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addUnlink(to menu: NSMenu, with site: ValetSite) {
|
private func addUnlink(to menu: NSMenu, with site: ValetSite) {
|
||||||
if (site.aliasPath != nil) {
|
if site.aliasPath != nil {
|
||||||
menu.addItem(
|
menu.addItem(
|
||||||
withTitle: "domain_list.unlink".localized,
|
withTitle: "domain_list.unlink".localized,
|
||||||
action: #selector(self.unlinkSite),
|
action: #selector(self.unlinkSite),
|
||||||
@@ -94,25 +94,29 @@ extension DomainListVC {
|
|||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addDisabledIsolation(to menu: NSMenu) {
|
private func addDisabledIsolation(to menu: NSMenu) {
|
||||||
menu.addItem(withTitle: "domain_list.isolation_unavailable".localized, action: nil, keyEquivalent: "")
|
menu.addItem(withTitle: "domain_list.isolation_unavailable".localized, action: nil, keyEquivalent: "")
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addIsolate(to menu: NSMenu, with site: ValetSite) {
|
private func addIsolate(to menu: NSMenu, with site: ValetSite) {
|
||||||
if site.isolatedPhpVersion == nil {
|
if site.isolatedPhpVersion == nil {
|
||||||
// ISOLATION POSSIBLE
|
// ISOLATION POSSIBLE
|
||||||
let isolationMenuItem = NSMenuItem(title:"domain_list.isolate".localized, action: nil, keyEquivalent: "")
|
let isolationMenuItem = NSMenuItem(title: "domain_list.isolate".localized, action: nil, keyEquivalent: "")
|
||||||
let submenu = NSMenu()
|
let submenu = NSMenu()
|
||||||
submenu.addItem(withTitle: "Choose a PHP version", action: nil, keyEquivalent: "")
|
submenu.addItem(withTitle: "Choose a PHP version", action: nil, keyEquivalent: "")
|
||||||
for version in PhpEnv.shared.availablePhpVersions.reversed() {
|
for version in PhpEnv.shared.availablePhpVersions.reversed() {
|
||||||
let item = PhpMenuItem(title: "Always use PHP \(version)", action: #selector(self.isolateSite), keyEquivalent: "")
|
let item = PhpMenuItem(
|
||||||
|
title: "Always use PHP \(version)",
|
||||||
|
action: #selector(self.isolateSite),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
item.version = version
|
item.version = version
|
||||||
submenu.addItem(item)
|
submenu.addItem(item)
|
||||||
}
|
}
|
||||||
menu.setSubmenu(submenu, for: isolationMenuItem)
|
menu.setSubmenu(submenu, for: isolationMenuItem)
|
||||||
|
|
||||||
menu.addItem(isolationMenuItem)
|
menu.addItem(isolationMenuItem)
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
} else {
|
} else {
|
||||||
@@ -125,7 +129,7 @@ extension DomainListVC {
|
|||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addToggleSecure(to menu: NSMenu, with site: ValetSite) {
|
private func addToggleSecure(to menu: NSMenu, with site: ValetSite) {
|
||||||
menu.addItem(
|
menu.addItem(
|
||||||
withTitle: site.secured
|
withTitle: site.secured
|
||||||
@@ -135,9 +139,9 @@ extension DomainListVC {
|
|||||||
keyEquivalent: ""
|
keyEquivalent: ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Menu Items for Proxy
|
// MARK: - Menu Items for Proxy
|
||||||
|
|
||||||
private func addMenuItemsForProxy(_ proxy: ValetProxy) {
|
private func addMenuItemsForProxy(_ proxy: ValetProxy) {
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
addOpenProxyInBrowser(to: menu)
|
addOpenProxyInBrowser(to: menu)
|
||||||
@@ -145,7 +149,7 @@ extension DomainListVC {
|
|||||||
addRemoveProxy(to: menu)
|
addRemoveProxy(to: menu)
|
||||||
tableView.menu = menu
|
tableView.menu = menu
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addOpenProxyInBrowser(to menu: NSMenu) {
|
private func addOpenProxyInBrowser(to menu: NSMenu) {
|
||||||
menu.addItem(
|
menu.addItem(
|
||||||
withTitle: "domain_list.open_in_browser".localized,
|
withTitle: "domain_list.open_in_browser".localized,
|
||||||
@@ -153,7 +157,7 @@ extension DomainListVC {
|
|||||||
keyEquivalent: "B"
|
keyEquivalent: "B"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addRemoveProxy(to menu: NSMenu) {
|
private func addRemoveProxy(to menu: NSMenu) {
|
||||||
menu.addItem(
|
menu.addItem(
|
||||||
withTitle: "domain_list.unproxy".localized,
|
withTitle: "domain_list.unproxy".localized,
|
||||||
@@ -161,11 +165,11 @@ extension DomainListVC {
|
|||||||
keyEquivalent: ""
|
keyEquivalent: ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Shared
|
// MARK: - Shared
|
||||||
|
|
||||||
private func addSeparator(to menu: NSMenu) {
|
private func addSeparator(to menu: NSMenu) {
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,62 +10,62 @@ import Cocoa
|
|||||||
import Carbon
|
import Carbon
|
||||||
|
|
||||||
class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||||
|
|
||||||
// MARK: - Outlets
|
// MARK: - Outlets
|
||||||
|
|
||||||
@IBOutlet weak var tableView: NSTableView!
|
@IBOutlet weak var tableView: NSTableView!
|
||||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||||
|
|
||||||
// MARK: - Variables
|
// MARK: - Variables
|
||||||
|
|
||||||
/// List of sites that will be displayed in this view. Originates from the `Valet` object.
|
/// List of sites that will be displayed in this view. Originates from the `Valet` object.
|
||||||
var domains: [DomainListable] = []
|
var domains: [DomainListable] = []
|
||||||
|
|
||||||
/// Array that contains various apps that might open a particular site directory.
|
/// Array that contains various apps that might open a particular site directory.
|
||||||
var applications: [Application] {
|
var applications: [Application] {
|
||||||
return App.shared.detectedApplications
|
return App.shared.detectedApplications
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The last sort descriptor used.
|
/// The last sort descriptor used.
|
||||||
var sortDescriptor: NSSortDescriptor? = nil
|
var sortDescriptor: NSSortDescriptor?
|
||||||
|
|
||||||
/// String that was last searched for. Empty by default.
|
/// String that was last searched for. Empty by default.
|
||||||
var lastSearchedFor = ""
|
var lastSearchedFor = ""
|
||||||
|
|
||||||
// MARK: - Helper Variables
|
// MARK: - Helper Variables
|
||||||
|
|
||||||
var selectedSite: ValetSite? {
|
var selectedSite: ValetSite? {
|
||||||
if tableView.selectedRow == -1 {
|
if tableView.selectedRow == -1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return domains[tableView.selectedRow] as? ValetSite
|
return domains[tableView.selectedRow] as? ValetSite
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedProxy: ValetProxy? {
|
var selectedProxy: ValetProxy? {
|
||||||
if tableView.selectedRow == -1 {
|
if tableView.selectedRow == -1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return domains[tableView.selectedRow] as? ValetProxy
|
return domains[tableView.selectedRow] as? ValetProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
var selected: DomainListable? {
|
var selected: DomainListable? {
|
||||||
if tableView.selectedRow == -1 {
|
if tableView.selectedRow == -1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return domains[tableView.selectedRow]
|
return domains[tableView.selectedRow]
|
||||||
}
|
}
|
||||||
|
|
||||||
var timer: Timer? = nil
|
var timer: Timer?
|
||||||
|
|
||||||
// MARK: - Display
|
// MARK: - Display
|
||||||
|
|
||||||
public static func create(delegate: NSWindowDelegate?) {
|
public static func create(delegate: NSWindowDelegate?) {
|
||||||
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
|
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
let windowController = storyboard.instantiateController(
|
let windowController = storyboard.instantiateController(
|
||||||
withIdentifier: "domainListWindow"
|
withIdentifier: "domainListWindow"
|
||||||
) as! DomainListWC
|
) as! DomainListWC
|
||||||
|
|
||||||
windowController.window!.title = "domain_list.title".localized
|
windowController.window!.title = "domain_list.title".localized
|
||||||
windowController.window!.subtitle = "domain_list.subtitle".localized
|
windowController.window!.subtitle = "domain_list.subtitle".localized
|
||||||
windowController.window!.delegate = delegate
|
windowController.window!.delegate = delegate
|
||||||
@@ -75,24 +75,24 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
windowController.window!.minSize = NSSize(width: 550, height: 200)
|
windowController.window!.minSize = NSSize(width: 550, height: 200)
|
||||||
windowController.window!.delegate = windowController
|
windowController.window!.delegate = windowController
|
||||||
windowController.window!.setFrameAutosaveName("domainListWindow")
|
windowController.window!.setFrameAutosaveName("domainListWindow")
|
||||||
|
|
||||||
App.shared.domainListWindowController = windowController
|
App.shared.domainListWindowController = windowController
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func show(delegate: NSWindowDelegate? = nil) {
|
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||||
if (App.shared.domainListWindowController == nil) {
|
if App.shared.domainListWindowController == nil {
|
||||||
Self.create(delegate: delegate)
|
Self.create(delegate: delegate)
|
||||||
}
|
}
|
||||||
|
|
||||||
App.shared.domainListWindowController!.showWindow(self)
|
App.shared.domainListWindowController!.showWindow(self)
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
tableView.doubleAction = #selector(self.doubleClicked(sender:))
|
tableView.doubleAction = #selector(self.doubleClicked(sender:))
|
||||||
|
|
||||||
if !Valet.shared.sites.isEmpty {
|
if !Valet.shared.sites.isEmpty {
|
||||||
// Preloaded list
|
// Preloaded list
|
||||||
domains = Valet.getDomainListable()
|
domains = Valet.getDomainListable()
|
||||||
@@ -101,9 +101,9 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
reloadDomains()
|
reloadDomains()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Async Operations
|
// MARK: - Async Operations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Disables the UI so the user cannot interact with it.
|
Disables the UI so the user cannot interact with it.
|
||||||
Also shows a spinner to indicate that we're busy.
|
Also shows a spinner to indicate that we're busy.
|
||||||
@@ -113,12 +113,12 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in
|
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in
|
||||||
self.progressIndicator.startAnimation(true)
|
self.progressIndicator.startAnimation(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
tableView.alphaValue = 0.3
|
tableView.alphaValue = 0.3
|
||||||
tableView.isEnabled = false
|
tableView.isEnabled = false
|
||||||
tableView.selectRowIndexes([], byExtendingSelection: true)
|
tableView.selectRowIndexes([], byExtendingSelection: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Re-enables the UI so the user can interact with it.
|
Re-enables the UI so the user can interact with it.
|
||||||
*/
|
*/
|
||||||
@@ -128,7 +128,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
tableView.alphaValue = 1.0
|
tableView.alphaValue = 1.0
|
||||||
tableView.isEnabled = true
|
tableView.isEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Executes a specific callback and fires the completion callback,
|
Executes a specific callback and fires the completion callback,
|
||||||
while updating the UI as required. As long as the completion callback
|
while updating the UI as required. As long as the completion callback
|
||||||
@@ -137,12 +137,11 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
- Parameter execute: Callback of the work that needs to happen.
|
- Parameter execute: Callback of the work that needs to happen.
|
||||||
- Parameter completion: Callback that is fired when the work is done.
|
- Parameter completion: Callback that is fired when the work is done.
|
||||||
*/
|
*/
|
||||||
internal func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
|
internal func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {}) {
|
||||||
{
|
|
||||||
setUIBusy()
|
setUIBusy()
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||||
execute()
|
execute()
|
||||||
|
|
||||||
// For a smoother animation, expect at least a 0.2 second delay
|
// For a smoother animation, expect at least a 0.2 second delay
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
|
||||||
completion()
|
completion()
|
||||||
@@ -150,9 +149,9 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Site Data Loading
|
// MARK: - Site Data Loading
|
||||||
|
|
||||||
func reloadDomains() {
|
func reloadDomains() {
|
||||||
waitAndExecute {
|
waitAndExecute {
|
||||||
Valet.shared.reloadSites()
|
Valet.shared.reloadSites()
|
||||||
@@ -161,29 +160,24 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
searchedFor(text: lastSearchedFor)
|
searchedFor(text: lastSearchedFor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applySortDescriptor(_ descriptor: NSSortDescriptor) {
|
func applySortDescriptor(_ descriptor: NSSortDescriptor) {
|
||||||
sortDescriptor = descriptor
|
sortDescriptor = descriptor
|
||||||
|
|
||||||
var sorted = self.domains
|
var sorted = self.domains
|
||||||
|
|
||||||
switch descriptor.key {
|
switch descriptor.key {
|
||||||
case "Secure":
|
case "Secure": sorted = self.domains.sorted { $0.getListableSecured() && !$1.getListableSecured() }
|
||||||
sorted = self.domains.sorted { $0.getListableSecured() && !$1.getListableSecured() }; break
|
case "Domain": sorted = self.domains.sorted { $0.getListableAbsolutePath() < $1.getListableAbsolutePath() }
|
||||||
case "Domain":
|
case "PHP": sorted = self.domains.sorted { $0.getListablePhpVersion() < $1.getListablePhpVersion() }
|
||||||
sorted = self.domains.sorted { $0.getListableAbsolutePath() < $1.getListableAbsolutePath() }; break
|
case "Kind": sorted = self.domains.sorted { $0.getListableKind() < $1.getListableKind() }
|
||||||
case "PHP":
|
case "Type": sorted = self.domains.sorted { $0.getListableType() < $1.getListableType() }
|
||||||
sorted = self.domains.sorted { $0.getListablePhpVersion() < $1.getListablePhpVersion() }; break
|
default: break
|
||||||
case "Kind":
|
|
||||||
sorted = self.domains.sorted { $0.getListableKind() < $1.getListableKind() }; break
|
|
||||||
case "Type":
|
|
||||||
sorted = self.domains.sorted { $0.getListableType() < $1.getListableType() }; break
|
|
||||||
default: break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.domains = descriptor.ascending ? sorted.reversed() : sorted
|
self.domains = descriptor.ascending ? sorted.reversed() : sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
func addedNewSite(name: String, secure: Bool) {
|
func addedNewSite(name: String, secure: Bool) {
|
||||||
waitAndExecute {
|
waitAndExecute {
|
||||||
Valet.shared.reloadSites()
|
Valet.shared.reloadSites()
|
||||||
@@ -191,7 +185,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
find(name, secure)
|
find(name, secure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func find(_ name: String, _ secure: Bool = false) {
|
private func find(_ name: String, _ secure: Bool = false) {
|
||||||
domains = Valet.getDomainListable()
|
domains = Valet.getDomainListable()
|
||||||
searchedFor(text: "")
|
searchedFor(text: "")
|
||||||
@@ -199,19 +193,19 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.tableView.selectRowIndexes([site.offset], byExtendingSelection: false)
|
self.tableView.selectRowIndexes([site.offset], byExtendingSelection: false)
|
||||||
self.tableView.scrollRowToVisible(site.offset)
|
self.tableView.scrollRowToVisible(site.offset)
|
||||||
if (secure && !site.element.getListableSecured()) {
|
if secure && !site.element.getListableSecured() {
|
||||||
self.toggleSecure()
|
self.toggleSecure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Table View Delegate
|
// MARK: - Table View Delegate
|
||||||
|
|
||||||
func numberOfRows(in tableView: NSTableView) -> Int {
|
func numberOfRows(in tableView: NSTableView) -> Int {
|
||||||
return domains.count
|
return domains.count
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
|
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
|
||||||
guard let sortDescriptor = tableView.sortDescriptors.first else { return }
|
guard let sortDescriptor = tableView.sortDescriptors.first else { return }
|
||||||
// Kinda scuffed way of applying sort descriptors here, but it works.
|
// Kinda scuffed way of applying sort descriptors here, but it works.
|
||||||
@@ -219,7 +213,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
applySortDescriptor(sortDescriptor)
|
applySortDescriptor(sortDescriptor)
|
||||||
searchedFor(text: lastSearchedFor)
|
searchedFor(text: lastSearchedFor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||||
let mapping: [String: String] = [
|
let mapping: [String: String] = [
|
||||||
"TLS": DomainListTLSCell.reusableName,
|
"TLS": DomainListTLSCell.reusableName,
|
||||||
@@ -228,76 +222,76 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
|||||||
"KIND": DomainListKindCell.reusableName,
|
"KIND": DomainListKindCell.reusableName,
|
||||||
"TYPE": DomainListTypeCell.reusableName
|
"TYPE": DomainListTypeCell.reusableName
|
||||||
]
|
]
|
||||||
|
|
||||||
let columnName = tableColumn!.identifier.rawValue
|
let columnName = tableColumn!.identifier.rawValue
|
||||||
let identifier = NSUserInterfaceItemIdentifier(rawValue: mapping[columnName]!)
|
let identifier = NSUserInterfaceItemIdentifier(rawValue: mapping[columnName]!)
|
||||||
|
|
||||||
guard let userCell = tableView.makeView(withIdentifier: identifier, owner: self)
|
guard let userCell = tableView.makeView(withIdentifier: identifier, owner: self)
|
||||||
as? DomainListCellProtocol else { return nil }
|
as? DomainListCellProtocol else { return nil }
|
||||||
|
|
||||||
if let site = domains[row] as? ValetSite {
|
if let site = domains[row] as? ValetSite {
|
||||||
userCell.populateCell(with: site)
|
userCell.populateCell(with: site)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let proxy = domains[row] as? ValetProxy {
|
if let proxy = domains[row] as? ValetProxy {
|
||||||
userCell.populateCell(with: proxy)
|
userCell.populateCell(with: proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
return userCell as? NSView
|
return userCell as? NSView
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableViewSelectionDidChange(_ notification: Notification) {
|
func tableViewSelectionDidChange(_ notification: Notification) {
|
||||||
reloadContextMenu()
|
reloadContextMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func doubleClicked(sender: Any) {
|
@objc func doubleClicked(sender: Any) {
|
||||||
guard self.selected != nil else {
|
guard self.selected != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.openInBrowser()
|
self.openInBrowser()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - (Search) Text Field Delegate
|
// MARK: - (Search) Text Field Delegate
|
||||||
|
|
||||||
func reloadTable() {
|
func reloadTable() {
|
||||||
if let sortDescriptor = sortDescriptor {
|
if let sortDescriptor = sortDescriptor {
|
||||||
self.applySortDescriptor(sortDescriptor)
|
self.applySortDescriptor(sortDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.tableView.reloadData()
|
self.tableView.reloadData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchedFor(text: String) {
|
func searchedFor(text: String) {
|
||||||
lastSearchedFor = text
|
lastSearchedFor = text
|
||||||
|
|
||||||
let searchString = text.lowercased()
|
let searchString = text.lowercased()
|
||||||
|
|
||||||
if searchString.isEmpty {
|
if searchString.isEmpty {
|
||||||
domains = Valet.getDomainListable()
|
domains = Valet.getDomainListable()
|
||||||
|
|
||||||
reloadTable()
|
reloadTable()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let splitSearchString: [String] = searchString
|
let splitSearchString: [String] = searchString
|
||||||
.split(separator: " ")
|
.split(separator: " ")
|
||||||
.map { return String($0) }
|
.map { return String($0) }
|
||||||
|
|
||||||
domains = Valet.getDomainListable().filter({ site in
|
domains = Valet.getDomainListable().filter({ site in
|
||||||
return !splitSearchString.map { searchString in
|
return !splitSearchString.map { searchString in
|
||||||
return site.getListableName().lowercased().contains(searchString)
|
return site.getListableName().lowercased().contains(searchString)
|
||||||
}.contains(false)
|
}.contains(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
reloadTable()
|
reloadTable()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Deinitialization
|
// MARK: - Deinitialization
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Log.perf("DomainListVC deallocated")
|
Log.perf("DomainListVC deallocated")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,82 +9,82 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
|
class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
|
||||||
|
|
||||||
// MARK: - Window Identifier
|
// MARK: - Window Identifier
|
||||||
|
|
||||||
override var windowName: String {
|
override var windowName: String {
|
||||||
return "DomainList"
|
return "DomainList"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Outlets
|
// MARK: - Outlets
|
||||||
|
|
||||||
@IBOutlet weak var searchToolbarItem: NSSearchToolbarItem!
|
@IBOutlet weak var searchToolbarItem: NSSearchToolbarItem!
|
||||||
|
|
||||||
// MARK: - Window Lifecycle
|
// MARK: - Window Lifecycle
|
||||||
|
|
||||||
override func windowDidLoad() {
|
override func windowDidLoad() {
|
||||||
super.windowDidLoad()
|
super.windowDidLoad()
|
||||||
self.searchToolbarItem.searchField.delegate = self
|
self.searchToolbarItem.searchField.delegate = self
|
||||||
self.searchToolbarItem.searchField.becomeFirstResponder()
|
self.searchToolbarItem.searchField.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Search functionality
|
// MARK: - Search functionality
|
||||||
|
|
||||||
var contentVC: DomainListVC {
|
var contentVC: DomainListVC {
|
||||||
return self.contentViewController as! DomainListVC
|
return self.contentViewController as! DomainListVC
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchTimer: Timer?
|
var searchTimer: Timer?
|
||||||
|
|
||||||
func controlTextDidChange(_ notification: Notification) {
|
func controlTextDidChange(_ notification: Notification) {
|
||||||
guard let searchField = notification.object as? NSSearchField else {
|
guard let searchField = notification.object as? NSSearchField else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.searchTimer?.invalidate()
|
self.searchTimer?.invalidate()
|
||||||
|
|
||||||
searchTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false, block: { _ in
|
searchTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false, block: { _ in
|
||||||
self.contentVC.searchedFor(text: searchField.stringValue)
|
self.contentVC.searchedFor(text: searchField.stringValue)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Reload functionality
|
// MARK: - Reload functionality
|
||||||
|
|
||||||
@IBAction func pressedReload(_ sender: Any?) {
|
@IBAction func pressedReload(_ sender: Any?) {
|
||||||
contentVC.reloadDomains()
|
contentVC.reloadDomains()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func pressedAddLink(_ sender: Any?) {
|
@IBAction func pressedAddLink(_ sender: Any?) {
|
||||||
showSelectionWindow()
|
showSelectionWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Add a new site
|
// MARK: - Add a new site
|
||||||
|
|
||||||
func showSelectionWindow() {
|
func showSelectionWindow() {
|
||||||
let storyboard = NSStoryboard(name: "Main", bundle : nil)
|
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
let windowController = storyboard.instantiateController(
|
let windowController = storyboard.instantiateController(
|
||||||
withIdentifier: "showSelectionWindow"
|
withIdentifier: "showSelectionWindow"
|
||||||
) as! NSWindowController
|
) as! NSWindowController
|
||||||
|
|
||||||
let viewController = windowController.window!
|
let viewController = windowController.window!
|
||||||
.contentViewController as! SelectionVC
|
.contentViewController as! SelectionVC
|
||||||
|
|
||||||
viewController.domainListWC = self
|
viewController.domainListWC = self
|
||||||
|
|
||||||
self.window?.beginSheet(windowController.window!)
|
self.window?.beginSheet(windowController.window!)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startCreateLinkFlow() {
|
func startCreateLinkFlow() {
|
||||||
self.showFolderSelectionForLink()
|
self.showFolderSelectionForLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
func startCreateProxyFlow() {
|
func startCreateProxyFlow() {
|
||||||
self.showProxyPopup()
|
self.showProxyPopup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Popups
|
// MARK: - Popups
|
||||||
|
|
||||||
private func showFolderSelectionForLink() {
|
private func showFolderSelectionForLink() {
|
||||||
let dialog = NSOpenPanel()
|
let dialog = NSOpenPanel()
|
||||||
dialog.message = "domain_list.add.modal_description".localized
|
dialog.message = "domain_list.add.modal_description".localized
|
||||||
@@ -95,37 +95,37 @@ class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate
|
|||||||
dialog.canChooseFiles = false
|
dialog.canChooseFiles = false
|
||||||
dialog.beginSheetModal(for: self.window!) { response in
|
dialog.beginSheetModal(for: self.window!) { response in
|
||||||
let result = dialog.url
|
let result = dialog.url
|
||||||
if (result != nil && response == .OK) {
|
if result != nil && response == .OK {
|
||||||
let path: String = result!.path
|
let path: String = result!.path
|
||||||
self.showLinkPopup(path)
|
self.showLinkPopup(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showLinkPopup(_ folder: String) {
|
private func showLinkPopup(_ folder: String) {
|
||||||
let storyboard = NSStoryboard(name: "Main", bundle : nil)
|
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
let windowController = storyboard.instantiateController(
|
let windowController = storyboard.instantiateController(
|
||||||
withIdentifier: "addSiteWindow"
|
withIdentifier: "addSiteWindow"
|
||||||
) as! NSWindowController
|
) as! NSWindowController
|
||||||
|
|
||||||
let viewController = windowController.window!.contentViewController as! AddSiteVC
|
let viewController = windowController.window!.contentViewController as! AddSiteVC
|
||||||
viewController.pathControl.url = URL(fileURLWithPath: folder)
|
viewController.pathControl.url = URL(fileURLWithPath: folder)
|
||||||
viewController.inputDomainName.stringValue = String(folder.split(separator: "/").last!)
|
viewController.inputDomainName.stringValue = String(folder.split(separator: "/").last!)
|
||||||
viewController.updateTextField()
|
viewController.updateTextField()
|
||||||
|
|
||||||
self.window?.beginSheet(windowController.window!)
|
self.window?.beginSheet(windowController.window!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showProxyPopup() {
|
private func showProxyPopup() {
|
||||||
let storyboard = NSStoryboard(name: "Main", bundle : nil)
|
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
let windowController = storyboard.instantiateController(
|
let windowController = storyboard.instantiateController(
|
||||||
withIdentifier: "addProxyWindow"
|
withIdentifier: "addProxyWindow"
|
||||||
) as! NSWindowController
|
) as! NSWindowController
|
||||||
|
|
||||||
// let viewController = windowController.window!.contentViewController as! AddSiteVC
|
// let viewController = windowController.window!.contentViewController as! AddSiteVC
|
||||||
|
|
||||||
self.window?.beginSheet(windowController.window!)
|
self.window?.beginSheet(windowController.window!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,31 +10,31 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class SelectionVC: NSViewController {
|
class SelectionVC: NSViewController {
|
||||||
|
|
||||||
weak var domainListWC: DomainListWC?
|
weak var domainListWC: DomainListWC?
|
||||||
|
|
||||||
@IBOutlet weak var textFieldTitle: NSTextField!
|
@IBOutlet weak var textFieldTitle: NSTextField!
|
||||||
@IBOutlet weak var textFieldDescription: NSTextField!
|
@IBOutlet weak var textFieldDescription: NSTextField!
|
||||||
@IBOutlet weak var buttonCreateLink: NSButton!
|
@IBOutlet weak var buttonCreateLink: NSButton!
|
||||||
@IBOutlet weak var buttonCreateProxy: NSButton!
|
@IBOutlet weak var buttonCreateProxy: NSButton!
|
||||||
@IBOutlet weak var buttonCancel: NSButton!
|
@IBOutlet weak var buttonCancel: NSButton!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
loadStaticLocalisedStrings()
|
loadStaticLocalisedStrings()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear() {
|
override func viewDidAppear() {
|
||||||
view.window?.makeFirstResponder(buttonCreateLink)
|
view.window?.makeFirstResponder(buttonCreateLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dismissView(outcome: NSApplication.ModalResponse) {
|
private func dismissView(outcome: NSApplication.ModalResponse) {
|
||||||
guard let window = self.view.window, let parent = window.sheetParent else { return }
|
guard let window = self.view.window, let parent = window.sheetParent else { return }
|
||||||
parent.endSheet(window, returnCode: outcome)
|
parent.endSheet(window, returnCode: outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Localisation
|
// MARK: - Localisation
|
||||||
|
|
||||||
func loadStaticLocalisedStrings() {
|
func loadStaticLocalisedStrings() {
|
||||||
textFieldTitle.stringValue = "selection.title".localized
|
textFieldTitle.stringValue = "selection.title".localized
|
||||||
textFieldDescription.stringValue = "selection.description".localized
|
textFieldDescription.stringValue = "selection.description".localized
|
||||||
@@ -42,21 +42,21 @@ class SelectionVC: NSViewController {
|
|||||||
buttonCreateLink.title = "selection.create_link".localized
|
buttonCreateLink.title = "selection.create_link".localized
|
||||||
buttonCreateProxy.title = "selection.create_proxy".localized
|
buttonCreateProxy.title = "selection.create_proxy".localized
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Outlet Interactions
|
// MARK: - Outlet Interactions
|
||||||
|
|
||||||
@IBAction func pressedCreateLink(_ sender: Any) {
|
@IBAction func pressedCreateLink(_ sender: Any) {
|
||||||
self.dismissView(outcome: .continue)
|
self.dismissView(outcome: .continue)
|
||||||
domainListWC?.startCreateLinkFlow()
|
domainListWC?.startCreateLinkFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func pressedCreateProxy(_ sender: Any) {
|
@IBAction func pressedCreateProxy(_ sender: Any) {
|
||||||
self.dismissView(outcome: .continue)
|
self.dismissView(outcome: .continue)
|
||||||
domainListWC?.startCreateProxyFlow()
|
domainListWC?.startCreateProxyFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func pressedCancel(_ sender: Any) {
|
@IBAction func pressedCancel(_ sender: Any) {
|
||||||
self.dismissView(outcome: .cancel)
|
self.dismissView(outcome: .cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,59 +13,58 @@ import Foundation
|
|||||||
to this object.
|
to this object.
|
||||||
*/
|
*/
|
||||||
struct ComposerJson: Decodable {
|
struct ComposerJson: Decodable {
|
||||||
|
|
||||||
// MARK: - JSON structure
|
// MARK: - JSON structure
|
||||||
|
|
||||||
let dependencies: Dictionary<String, String>?
|
let dependencies: [String: String]?
|
||||||
let devDependencies: Dictionary<String, String>?
|
let devDependencies: [String: String]?
|
||||||
let configuration: Config?
|
let configuration: Config?
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case dependencies = "require"
|
case dependencies = "require"
|
||||||
case devDependencies = "require-dev"
|
case devDependencies = "require-dev"
|
||||||
case configuration = "config"
|
case configuration = "config"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Config: Decodable {
|
struct Config: Decodable {
|
||||||
let platform: Platform?
|
let platform: Platform?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Platform: Decodable {
|
struct Platform: Decodable {
|
||||||
let php: String?
|
let php: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks what the PHP version constraint is.
|
Checks what the PHP version constraint is.
|
||||||
Returns a tuple (constraint, location of constraint).
|
Returns a tuple (constraint, location of constraint).
|
||||||
*/
|
*/
|
||||||
public func getPhpVersion() -> (String, ValetSite.VersionSource)
|
public func getPhpVersion() -> (String, ValetSite.VersionSource) {
|
||||||
{
|
|
||||||
// Check if in platform
|
// Check if in platform
|
||||||
if configuration?.platform?.php != nil {
|
if configuration?.platform?.php != nil {
|
||||||
return (configuration!.platform!.php!, .platform)
|
return (configuration!.platform!.php!, .platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if in dependencies
|
// Check if in dependencies
|
||||||
if dependencies?["php"] != nil {
|
if dependencies?["php"] != nil {
|
||||||
return (dependencies!["php"]!, .require)
|
return (dependencies!["php"]!, .require)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown!
|
// Unknown!
|
||||||
return ("???", .unknown)
|
return ("???", .unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks if any notable dependencies can be resolved.
|
Checks if any notable dependencies can be resolved.
|
||||||
Only notable dependencies are saved.
|
Only notable dependencies are saved.
|
||||||
*/
|
*/
|
||||||
public func getNotableDependencies() -> [String: String] {
|
public func getNotableDependencies() -> [String: String] {
|
||||||
var notable: [String: String] = [:]
|
var notable: [String: String] = [:]
|
||||||
|
|
||||||
var scan = Array(PhpFrameworks.DependencyList.keys)
|
var scan = Array(PhpFrameworks.DependencyList.keys)
|
||||||
scan.append("php")
|
scan.append("php")
|
||||||
|
|
||||||
scan.forEach { dependency in
|
scan.forEach { dependency in
|
||||||
if dependencies?[dependency] != nil {
|
if dependencies?[dependency] != nil {
|
||||||
notable[dependency] = dependencies![dependency]
|
notable[dependency] = dependencies![dependency]
|
||||||
@@ -74,7 +73,5 @@ struct ComposerJson: Decodable {
|
|||||||
|
|
||||||
return notable
|
return notable
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import Foundation
|
|||||||
|
|
||||||
class ComposerWindow {
|
class ComposerWindow {
|
||||||
|
|
||||||
private var menu: MainMenu? = nil
|
private var menu: MainMenu?
|
||||||
private var shouldNotify: Bool! = nil
|
private var shouldNotify: Bool! = nil
|
||||||
private var completion: ((Bool) -> Void)! = nil
|
private var completion: ((Bool) -> Void)! = nil
|
||||||
private var window: ProgressWindowController? = nil
|
private var window: ProgressWindowController?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Updates the global dependencies and runs the completion callback when done.
|
Updates the global dependencies and runs the completion callback when done.
|
||||||
*/
|
*/
|
||||||
@@ -22,33 +22,33 @@ class ComposerWindow {
|
|||||||
self.menu = MainMenu.shared
|
self.menu = MainMenu.shared
|
||||||
self.shouldNotify = notify
|
self.shouldNotify = notify
|
||||||
self.completion = completion
|
self.completion = completion
|
||||||
|
|
||||||
Paths.shared.detectBinaryPaths()
|
Paths.shared.detectBinaryPaths()
|
||||||
if Paths.composer == nil {
|
if Paths.composer == nil {
|
||||||
presentMissingAlert()
|
presentMissingAlert()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
PhpEnv.shared.isBusy = true
|
PhpEnv.shared.isBusy = true
|
||||||
menu?.setBusyImage()
|
menu?.setBusyImage()
|
||||||
menu?.rebuild()
|
menu?.rebuild()
|
||||||
|
|
||||||
window = ProgressWindowController.display(
|
window = ProgressWindowController.display(
|
||||||
title: "alert.composer_progress.title".localized,
|
title: "alert.composer_progress.title".localized,
|
||||||
description: "alert.composer_progress.info".localized
|
description: "alert.composer_progress.info".localized
|
||||||
)
|
)
|
||||||
|
|
||||||
window?.setType(info: true)
|
window?.setType(info: true)
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||||
let task = Shell.user.createTask(
|
let task = Shell.user.createTask(
|
||||||
for: "\(Paths.composer!) global update", requiresPath: true
|
for: "\(Paths.composer!) global update", requiresPath: true
|
||||||
)
|
)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.window?.addToConsole("\(Paths.composer!) global update\n")
|
self.window?.addToConsole("\(Paths.composer!) global update\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
task.listen(
|
task.listen(
|
||||||
didReceiveStandardOutputData: { string in
|
didReceiveStandardOutputData: { string in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@@ -63,11 +63,11 @@ class ComposerWindow {
|
|||||||
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
|
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
task.launch()
|
task.launch()
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
task.haltListening()
|
task.haltListening()
|
||||||
|
|
||||||
if task.terminationStatus <= 0 {
|
if task.terminationStatus <= 0 {
|
||||||
composerUpdateSucceeded()
|
composerUpdateSucceeded()
|
||||||
} else {
|
} else {
|
||||||
@@ -75,12 +75,12 @@ class ComposerWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func composerUpdateSucceeded() {
|
private func composerUpdateSucceeded() {
|
||||||
// Closing the window should happen after a slight delay
|
// Closing the window should happen after a slight delay
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in
|
||||||
window?.close()
|
window?.close()
|
||||||
if (shouldNotify) {
|
if shouldNotify {
|
||||||
LocalNotification.send(
|
LocalNotification.send(
|
||||||
title: "alert.composer_success.title".localized,
|
title: "alert.composer_success.title".localized,
|
||||||
subtitle: "alert.composer_success.info".localized
|
subtitle: "alert.composer_success.info".localized
|
||||||
@@ -91,7 +91,7 @@ class ComposerWindow {
|
|||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func composerUpdateFailed() {
|
private func composerUpdateFailed() {
|
||||||
// Showing that something failed should be shown immediately
|
// Showing that something failed should be shown immediately
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
@@ -103,18 +103,18 @@ class ComposerWindow {
|
|||||||
completion(false)
|
completion(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Main Menu Update
|
// MARK: Main Menu Update
|
||||||
|
|
||||||
private func removeBusyStatus() {
|
private func removeBusyStatus() {
|
||||||
PhpEnv.shared.isBusy = false
|
PhpEnv.shared.isBusy = false
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
menu?.updatePhpVersionInStatusBar()
|
menu?.updatePhpVersionInStatusBar()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Alert
|
// MARK: Alert
|
||||||
|
|
||||||
private func presentMissingAlert() {
|
private func presentMissingAlert() {
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct PhpFrameworks {
|
struct PhpFrameworks {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This list should probably be reversed when checked, because some of these
|
This list should probably be reversed when checked, because some of these
|
||||||
will also require either `laravel/framework` or `symfony/symfony`.
|
will also require either `laravel/framework` or `symfony/symfony`.
|
||||||
@@ -17,10 +17,10 @@ struct PhpFrameworks {
|
|||||||
public static let DependencyList = [
|
public static let DependencyList = [
|
||||||
|
|
||||||
// COMMON FRAMEWORKS
|
// COMMON FRAMEWORKS
|
||||||
"laravel/framework" : "Laravel",
|
"laravel/framework": "Laravel",
|
||||||
"symfony/symfony": "Symfony",
|
"symfony/symfony": "Symfony",
|
||||||
"laravel/lumen": "Lumen",
|
"laravel/lumen": "Lumen",
|
||||||
|
|
||||||
// VARIOUS CMS
|
// VARIOUS CMS
|
||||||
"roots/bedrock": "Bedrock",
|
"roots/bedrock": "Bedrock",
|
||||||
"cakephp/app": "CakePHP",
|
"cakephp/app": "CakePHP",
|
||||||
@@ -37,15 +37,15 @@ struct PhpFrameworks {
|
|||||||
"johnpbloch/wordpress-core": "WordPress",
|
"johnpbloch/wordpress-core": "WordPress",
|
||||||
"zendframework/zendframework": "Zend",
|
"zendframework/zendframework": "Zend",
|
||||||
"zendframework/zend-mvc": "Zend",
|
"zendframework/zend-mvc": "Zend",
|
||||||
"typo3/cms-core": "Typo3",
|
"typo3/cms-core": "Typo3"
|
||||||
|
|
||||||
// TODO (6.0): Handle these in v6.0
|
// TODO (6.0): Handle these in v6.0
|
||||||
// "magento/*": "Magento",
|
// "magento/*": "Magento",
|
||||||
// "concrete5/*": "Concrete5",
|
// "concrete5/*": "Concrete5",
|
||||||
// "contao/*": "Contao",
|
// "contao/*": "Contao",
|
||||||
// "slim/*": "Slim",
|
// "slim/*": "Slim",
|
||||||
]
|
]
|
||||||
|
|
||||||
public static let FileMapping: [String: [String]] = [
|
public static let FileMapping: [String: [String]] = [
|
||||||
"Drupal": [
|
"Drupal": [
|
||||||
// Legacy installations
|
// Legacy installations
|
||||||
@@ -61,10 +61,10 @@ struct PhpFrameworks {
|
|||||||
],
|
],
|
||||||
"Typo3": [
|
"Typo3": [
|
||||||
"/typo3",
|
"/typo3",
|
||||||
"/public/typo3",
|
"/public/typo3"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
There are two cases where users are unlikely to use `composer`,
|
There are two cases where users are unlikely to use `composer`,
|
||||||
when setting up a Drupal or a WordPress project. For performance
|
when setting up a Drupal or a WordPress project. For performance
|
||||||
@@ -75,13 +75,13 @@ struct PhpFrameworks {
|
|||||||
let found = entry.value
|
let found = entry.value
|
||||||
.map { path in return Filesystem.fileExists(basePath + path) }
|
.map { path in return Filesystem.fileExists(basePath + path) }
|
||||||
.contains(true)
|
.contains(true)
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
return entry.key
|
return entry.key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class HomebrewDiagnostics {
|
class HomebrewDiagnostics {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||||
This will then result in two different aliases claiming to point to the same formula (`php`).
|
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||||
@@ -17,50 +17,61 @@ class HomebrewDiagnostics {
|
|||||||
|
|
||||||
This check only needs to be performed if the `shivammathur/php` tap is active.
|
This check only needs to be performed if the `shivammathur/php` tap is active.
|
||||||
*/
|
*/
|
||||||
public static func hasAliasConflict() -> Bool
|
public static func hasAliasConflict() -> Bool {
|
||||||
{
|
|
||||||
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
|
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
|
||||||
|
|
||||||
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
|
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
|
||||||
Log.info("The user does not appear to have tapped: shivammathur/php")
|
Log.info("The user does not appear to have tapped: shivammathur/php")
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
Log.info("The user DOES have the following tapped: shivammathur/php")
|
Log.info("The user DOES have the following tapped: shivammathur/php")
|
||||||
Log.info("Checking for `php` formula conflicts...")
|
Log.info("Checking for `php` formula conflicts...")
|
||||||
|
|
||||||
let tapPhp = try! JSONDecoder().decode(
|
let tapPhp = try! JSONDecoder().decode(
|
||||||
[HomebrewPackage].self,
|
[HomebrewPackage].self,
|
||||||
from: tapAlias.data(using: .utf8)!
|
from: tapAlias.data(using: .utf8)!
|
||||||
).first!
|
).first!
|
||||||
|
|
||||||
if tapPhp.version != PhpEnv.brewPhpVersion {
|
if tapPhp.version != PhpEnv.brewPhpVersion {
|
||||||
Log.warn("The `php` formula alias seems to be the different between the tap and core. This could be a problem!")
|
Log.warn("The `php` formula alias seems to be the different between the tap and core. "
|
||||||
|
+ "This could be a problem!")
|
||||||
Log.info("Determining whether both of these versions are installed...")
|
Log.info("Determining whether both of these versions are installed...")
|
||||||
|
|
||||||
let bothInstalled = PhpEnv.shared.availablePhpVersions.contains(tapPhp.version)
|
let bothInstalled = PhpEnv.shared.availablePhpVersions.contains(tapPhp.version)
|
||||||
&& PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion)
|
&& PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion)
|
||||||
|
|
||||||
if bothInstalled {
|
if bothInstalled {
|
||||||
Log.warn("Both conflicting aliases seem to be installed, warning the user!")
|
Log.warn("Both conflicting aliases seem to be installed, warning the user!")
|
||||||
} else {
|
} else {
|
||||||
Log.info("Conflicting aliases are not both installed, seems fine!")
|
Log.info("Conflicting aliases are not both installed, seems fine!")
|
||||||
}
|
}
|
||||||
|
|
||||||
return bothInstalled
|
return bothInstalled
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
|
Log.info("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func presentAlertAboutConflict() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
BetterAlert()
|
||||||
|
.withInformation(
|
||||||
|
title: "alert.php_alias_conflict.title".localized,
|
||||||
|
subtitle: "alert.php_alias_conflict.info".localized
|
||||||
|
)
|
||||||
|
.withPrimary(text: "OK")
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
In order to see if we support the --json syntax, we'll query nginx.
|
In order to see if we support the --json syntax, we'll query nginx.
|
||||||
If the JSON response cannot be parsed, Homebrew is probably out of date.
|
If the JSON response cannot be parsed, Homebrew is probably out of date.
|
||||||
*/
|
*/
|
||||||
public static func cannotLoadService(_ name: String = "nginx") -> Bool
|
public static func cannotLoadService(_ name: String = "nginx") -> Bool {
|
||||||
{
|
|
||||||
let serviceInfo = try? JSONDecoder().decode(
|
let serviceInfo = try? JSONDecoder().decode(
|
||||||
[HomebrewService].self,
|
[HomebrewService].self,
|
||||||
from: Shell.pipe(
|
from: Shell.pipe(
|
||||||
@@ -68,7 +79,7 @@ class HomebrewDiagnostics {
|
|||||||
requiresPath: true
|
requiresPath: true
|
||||||
).data(using: .utf8)!
|
).data(using: .utf8)!
|
||||||
)
|
)
|
||||||
|
|
||||||
return serviceInfo == nil
|
return serviceInfo == nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,32 +9,32 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class NginxConfiguration {
|
class NginxConfiguration {
|
||||||
|
|
||||||
/** Contents of the Nginx file in question, as a string. */
|
/** Contents of the Nginx file in question, as a string. */
|
||||||
var contents: String!
|
var contents: String!
|
||||||
|
|
||||||
/** The name of the domain, usually derived from the name of the file. */
|
/** The name of the domain, usually derived from the name of the file. */
|
||||||
var domain: String
|
var domain: String
|
||||||
|
|
||||||
/** The TLD of the domain, usually derived from the name of the file. */
|
/** The TLD of the domain, usually derived from the name of the file. */
|
||||||
var tld: String
|
var tld: String
|
||||||
|
|
||||||
init(filePath: String) {
|
init(filePath: String) {
|
||||||
let path = filePath.replacingOccurrences(
|
let path = filePath.replacingOccurrences(
|
||||||
of: "~",
|
of: "~",
|
||||||
with: "/Users/\(Paths.whoami)"
|
with: "/Users/\(Paths.whoami)"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.contents = try! String(contentsOfFile: path)
|
self.contents = try! String(contentsOfFile: path)
|
||||||
|
|
||||||
let domain = String(path.split(separator: "/").last!)
|
let domain = String(path.split(separator: "/").last!)
|
||||||
let tld = String(domain.split(separator: ".").last!)
|
let tld = String(domain.split(separator: ".").last!)
|
||||||
|
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
.replacingOccurrences(of: ".\(tld)", with: "")
|
.replacingOccurrences(of: ".\(tld)", with: "")
|
||||||
self.tld = tld
|
self.tld = tld
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Retrieves what address this domain is proxying.
|
Retrieves what address this domain is proxying.
|
||||||
*/
|
*/
|
||||||
@@ -43,13 +43,13 @@ class NginxConfiguration {
|
|||||||
pattern: #"proxy_pass (?<proxy>.*:\d*);"#,
|
pattern: #"proxy_pass (?<proxy>.*:\d*);"#,
|
||||||
options: []
|
options: []
|
||||||
)
|
)
|
||||||
|
|
||||||
guard let match = regex.firstMatch(in: contents, range: NSMakeRange(0, contents.count))
|
guard let match = regex.firstMatch(in: contents, range: NSRange(location: 0, length: contents.count))
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
return contents[Range(match.range(withName: "proxy"), in: contents)!]
|
return contents[Range(match.range(withName: "proxy"), in: contents)!]
|
||||||
}()
|
}()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Retrieves which isolated version is active for this domain (if applicable).
|
Retrieves which isolated version is active for this domain (if applicable).
|
||||||
*/
|
*/
|
||||||
@@ -59,13 +59,13 @@ class NginxConfiguration {
|
|||||||
pattern: #"(ISOLATED_PHP_VERSION=(php)?(@)?)((?<major>\d)(.)?(?<minor>\d))"#,
|
pattern: #"(ISOLATED_PHP_VERSION=(php)?(@)?)((?<major>\d)(.)?(?<minor>\d))"#,
|
||||||
options: []
|
options: []
|
||||||
)
|
)
|
||||||
|
|
||||||
guard let match = regex.firstMatch(in: contents, range: NSMakeRange(0, contents.count))
|
guard let match = regex.firstMatch(in: contents, range: NSRange(location: 0, length: contents.count))
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
let major: String = contents[Range(match.range(withName: "major"), in: contents)!],
|
let major: String = contents[Range(match.range(withName: "major"), in: contents)!],
|
||||||
minor: String = contents[Range(match.range(withName: "minor"), in: contents)!]
|
minor: String = contents[Range(match.range(withName: "minor"), in: contents)!]
|
||||||
|
|
||||||
return "\(major).\(minor)"
|
return "\(major).\(minor)"
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,19 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol DomainListable {
|
protocol DomainListable {
|
||||||
|
|
||||||
func getListableName() -> String
|
func getListableName() -> String
|
||||||
|
|
||||||
func getListableSecured() -> Bool
|
func getListableSecured() -> Bool
|
||||||
|
|
||||||
func getListableAbsolutePath() -> String
|
func getListableAbsolutePath() -> String
|
||||||
|
|
||||||
func getListablePhpVersion() -> String
|
func getListablePhpVersion() -> String
|
||||||
|
|
||||||
func getListableKind() -> String
|
func getListableKind() -> String
|
||||||
|
|
||||||
func getListableType() -> String
|
func getListableType() -> String
|
||||||
|
|
||||||
func getListableUrl() -> URL?
|
func getListableUrl() -> URL?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol ProxyScanner {
|
protocol ProxyScanner {
|
||||||
|
|
||||||
func resolveProxies(directoryPath: String) -> [ValetProxy]
|
func resolveProxies(directoryPath: String) -> [ValetProxy]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,8 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class ValetProxyScanner: ProxyScanner
|
class ValetProxyScanner: ProxyScanner {
|
||||||
{
|
func resolveProxies(directoryPath: String) -> [ValetProxy] {
|
||||||
func resolveProxies(directoryPath: String) -> [ValetProxy]
|
|
||||||
{
|
|
||||||
return try! FileManager
|
return try! FileManager
|
||||||
.default
|
.default
|
||||||
.contentsOfDirectory(atPath: directoryPath)
|
.contentsOfDirectory(atPath: directoryPath)
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension ValetProxy {
|
extension ValetProxy {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,46 +8,45 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class ValetProxy: DomainListable
|
class ValetProxy: DomainListable {
|
||||||
{
|
|
||||||
var domain: String
|
var domain: String
|
||||||
var tld: String
|
var tld: String
|
||||||
var target: String
|
var target: String
|
||||||
var secured: Bool = false
|
var secured: Bool = false
|
||||||
|
|
||||||
init(_ configuration: NginxConfiguration) {
|
init(_ configuration: NginxConfiguration) {
|
||||||
self.domain = configuration.domain
|
self.domain = configuration.domain
|
||||||
self.tld = configuration.tld
|
self.tld = configuration.tld
|
||||||
self.target = configuration.proxy!
|
self.target = configuration.proxy!
|
||||||
self.secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
|
self.secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - DomainListable Protocol
|
// MARK: - DomainListable Protocol
|
||||||
|
|
||||||
func getListableName() -> String {
|
func getListableName() -> String {
|
||||||
return self.domain
|
return self.domain
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableSecured() -> Bool {
|
func getListableSecured() -> Bool {
|
||||||
return self.secured
|
return self.secured
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableAbsolutePath() -> String {
|
func getListableAbsolutePath() -> String {
|
||||||
return self.domain
|
return self.domain
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListablePhpVersion() -> String {
|
func getListablePhpVersion() -> String {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableKind() -> String {
|
func getListableKind() -> String {
|
||||||
return "proxy"
|
return "proxy"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableType() -> String {
|
func getListableType() -> String {
|
||||||
return "proxy"
|
return "proxy"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableUrl() -> URL? {
|
func getListableUrl() -> URL? {
|
||||||
return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)")
|
return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,33 +6,35 @@
|
|||||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
class FakeSiteScanner: SiteScanner
|
class FakeSiteScanner: SiteScanner {
|
||||||
{
|
|
||||||
let fakes = [
|
let fakes = [
|
||||||
ValetSite(fakeWithName: "laravel", tld: "test", secure: true, path: "~/Code/laravel/framework", linked: true),
|
ValetSite(fakeWithName: "laravel", tld: "test", secure: true,
|
||||||
|
path: "~/Code/laravel/framework", linked: true),
|
||||||
ValetSite(fakeWithName: "tailwind", tld: "test", secure: true, path: "~/Code/tailwind/site", linked: true, constraint: "8.0"),
|
|
||||||
|
ValetSite(fakeWithName: "tailwind", tld: "test", secure: true,
|
||||||
ValetSite(fakeWithName: "forge", tld: "test", secure: true, path: "~/Code/laravel/forge", linked: true),
|
path: "~/Code/tailwind/site", linked: true, constraint: "8.0"),
|
||||||
|
|
||||||
|
ValetSite(fakeWithName: "forge", tld: "test", secure: true,
|
||||||
|
path: "~/Code/laravel/forge", linked: true),
|
||||||
|
|
||||||
ValetSite(fakeWithName: "concord", tld: "test", secure: false,
|
ValetSite(fakeWithName: "concord", tld: "test", secure: false,
|
||||||
path: "~/Code/concord", linked: true, driver: "Laravel (^8.0)", constraint: "^7.4", isolated: "7.4"),
|
path: "~/Code/concord", linked: true, driver: "Laravel (^8.0)", constraint: "^7.4", isolated: "7.4"),
|
||||||
|
|
||||||
ValetSite(fakeWithName: "drupal", tld: "test", secure: false,
|
ValetSite(fakeWithName: "drupal", tld: "test", secure: false,
|
||||||
path: "~/Sites/drupal", linked: false, driver: "Drupal", constraint: "^7.4", isolated: "7.4"),
|
path: "~/Sites/drupal", linked: false, driver: "Drupal", constraint: "^7.4", isolated: "7.4"),
|
||||||
|
|
||||||
ValetSite(fakeWithName: "wordpress", tld: "test", secure: false,
|
ValetSite(fakeWithName: "wordpress", tld: "test", secure: false,
|
||||||
path: "~/Sites/wordpress", linked: false, driver: "WordPress", constraint: "^7.4", isolated: "7.4")
|
path: "~/Sites/wordpress", linked: false, driver: "WordPress", constraint: "^7.4", isolated: "7.4")
|
||||||
]
|
]
|
||||||
|
|
||||||
func resolveSiteCount(paths: [String]) -> Int {
|
func resolveSiteCount(paths: [String]) -> Int {
|
||||||
return fakes.count
|
return fakes.count
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveSitesFrom(paths: [String]) -> [ValetSite] {
|
func resolveSitesFrom(paths: [String]) -> [ValetSite] {
|
||||||
return fakes
|
return fakes
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveSite(path: String) -> ValetSite? {
|
func resolveSite(path: String) -> ValetSite? {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,10 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol SiteScanner
|
protocol SiteScanner {
|
||||||
{
|
|
||||||
func resolveSiteCount(paths: [String]) -> Int
|
func resolveSiteCount(paths: [String]) -> Int
|
||||||
|
|
||||||
func resolveSitesFrom(paths: [String]) -> [ValetSite]
|
func resolveSitesFrom(paths: [String]) -> [ValetSite]
|
||||||
|
|
||||||
func resolveSite(path: String) -> ValetSite?
|
func resolveSite(path: String) -> ValetSite?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,39 +8,38 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class ValetSiteScanner: SiteScanner
|
class ValetSiteScanner: SiteScanner {
|
||||||
{
|
|
||||||
func resolveSiteCount(paths: [String]) -> Int {
|
func resolveSiteCount(paths: [String]) -> Int {
|
||||||
return paths.map { path in
|
return paths.map { path in
|
||||||
|
|
||||||
let entries = try! FileManager.default
|
let entries = try! FileManager.default
|
||||||
.contentsOfDirectory(atPath: path)
|
.contentsOfDirectory(atPath: path)
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
.map { self.isSite($0, forPath: path) }
|
.map { self.isSite($0, forPath: path) }
|
||||||
.filter{ $0 == true}
|
.filter { $0 == true}
|
||||||
.count
|
.count
|
||||||
|
|
||||||
}.reduce(0, +)
|
}.reduce(0, +)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveSitesFrom(paths: [String]) -> [ValetSite] {
|
func resolveSitesFrom(paths: [String]) -> [ValetSite] {
|
||||||
var sites: [ValetSite] = []
|
var sites: [ValetSite] = []
|
||||||
|
|
||||||
paths.forEach { path in
|
paths.forEach { path in
|
||||||
let entries = try! FileManager.default
|
let entries = try! FileManager.default
|
||||||
.contentsOfDirectory(atPath: path)
|
.contentsOfDirectory(atPath: path)
|
||||||
|
|
||||||
return entries.forEach {
|
return entries.forEach {
|
||||||
if let site = self.resolveSite(path: "\(path)/\($0)") {
|
if let site = self.resolveSite(path: "\(path)/\($0)") {
|
||||||
sites.append(site)
|
sites.append(site)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sites
|
return sites
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determines whether the site can be resolved as a symbolic link or as a directory.
|
Determines whether the site can be resolved as a symbolic link or as a directory.
|
||||||
Regular files are ignored, and the site is added to Valet's list of sites.
|
Regular files are ignored, and the site is added to Valet's list of sites.
|
||||||
@@ -48,46 +47,46 @@ class ValetSiteScanner: SiteScanner
|
|||||||
func resolveSite(path: String) -> ValetSite? {
|
func resolveSite(path: String) -> ValetSite? {
|
||||||
// Get the TLD from the global Valet object
|
// Get the TLD from the global Valet object
|
||||||
let tld = Valet.shared.config.tld
|
let tld = Valet.shared.config.tld
|
||||||
|
|
||||||
// See if the file is a symlink, if so, resolve it
|
// See if the file is a symlink, if so, resolve it
|
||||||
guard let attrs = try? FileManager.default.attributesOfItem(atPath: path) else {
|
guard let attrs = try? FileManager.default.attributesOfItem(atPath: path) else {
|
||||||
Log.warn("Could not parse the site: \(path), skipping!")
|
Log.warn("Could not parse the site: \(path), skipping!")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can also determine whether the thing at the path is a directory, too
|
// We can also determine whether the thing at the path is a directory, too
|
||||||
let type = attrs[FileAttributeKey.type] as! FileAttributeType
|
let type = attrs[FileAttributeKey.type] as! FileAttributeType
|
||||||
|
|
||||||
// We should also check that we can interpret the path correctly
|
// We should also check that we can interpret the path correctly
|
||||||
if URL(fileURLWithPath: path).lastPathComponent == "" {
|
if URL(fileURLWithPath: path).lastPathComponent == "" {
|
||||||
Log.warn("Could not parse the site: \(path), skipping!")
|
Log.warn("Could not parse the site: \(path), skipping!")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if type == FileAttributeType.typeSymbolicLink {
|
if type == FileAttributeType.typeSymbolicLink {
|
||||||
return ValetSite(aliasPath: path, tld: tld)
|
return ValetSite(aliasPath: path, tld: tld)
|
||||||
} else if type == FileAttributeType.typeDirectory {
|
} else if type == FileAttributeType.typeDirectory {
|
||||||
return ValetSite(absolutePath: path, tld: tld)
|
return ValetSite(absolutePath: path, tld: tld)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determines whether the site can be resolved as a symbolic link or as a directory.
|
Determines whether the site can be resolved as a symbolic link or as a directory.
|
||||||
Regular files are ignored. Returns true if the path can be parsed.
|
Regular files are ignored. Returns true if the path can be parsed.
|
||||||
*/
|
*/
|
||||||
private func isSite(_ entry: String, forPath path: String) -> Bool {
|
private func isSite(_ entry: String, forPath path: String) -> Bool {
|
||||||
let siteDir = path + "/" + entry
|
let siteDir = path + "/" + entry
|
||||||
|
|
||||||
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
|
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
|
||||||
|
|
||||||
let type = attrs[FileAttributeKey.type] as! FileAttributeType
|
let type = attrs[FileAttributeKey.type] as! FileAttributeType
|
||||||
|
|
||||||
if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory {
|
if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension ValetSite {
|
extension ValetSite {
|
||||||
|
|
||||||
convenience init(
|
convenience init(
|
||||||
fakeWithName name: String,
|
fakeWithName name: String,
|
||||||
tld: String,
|
tld: String,
|
||||||
@@ -23,14 +23,14 @@ extension ValetSite {
|
|||||||
self.init(name: name, tld: tld, absolutePath: path, aliasPath: nil, makeDeterminations: false)
|
self.init(name: name, tld: tld, absolutePath: path, aliasPath: nil, makeDeterminations: false)
|
||||||
self.secured = secure
|
self.secured = secure
|
||||||
self.composerPhp = constraint
|
self.composerPhp = constraint
|
||||||
|
|
||||||
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
|
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
|
||||||
.map { string in
|
.map { string in
|
||||||
return PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
return !PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
||||||
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
|
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
.count > 0
|
.isEmpty
|
||||||
}.contains(true)
|
}.contains(true)
|
||||||
|
|
||||||
self.driver = driver
|
self.driver = driver
|
||||||
self.driverDeterminedByComposer = true
|
self.driverDeterminedByComposer = true
|
||||||
if linked {
|
if linked {
|
||||||
@@ -40,5 +40,5 @@ extension ValetSite {
|
|||||||
self.isolatedPhpVersion = PhpInstallation(isolated)
|
self.isolatedPhpVersion = PhpInstallation(isolated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,63 +9,63 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class ValetSite: DomainListable {
|
class ValetSite: DomainListable {
|
||||||
|
|
||||||
/// Name of the site. Does not include the TLD.
|
/// Name of the site. Does not include the TLD.
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
/// The absolute path to the directory that is served.
|
/// The absolute path to the directory that is served.
|
||||||
var absolutePath: String
|
var absolutePath: String
|
||||||
|
|
||||||
/// The absolute path to the directory that is served,
|
/// The absolute path to the directory that is served,
|
||||||
/// replacing the user's home folder with ~.
|
/// replacing the user's home folder with ~.
|
||||||
lazy var absolutePathRelative: String = {
|
lazy var absolutePathRelative: String = {
|
||||||
return self.absolutePath
|
return self.absolutePath
|
||||||
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
|
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
/// The TLD used to locate this site.
|
/// The TLD used to locate this site.
|
||||||
var tld: String = "test"
|
var tld: String = "test"
|
||||||
|
|
||||||
/// The PHP version that is being used to serve this site specifically (if not global).
|
/// The PHP version that is being used to serve this site specifically (if not global).
|
||||||
var isolatedPhpVersion: PhpInstallation?
|
var isolatedPhpVersion: PhpInstallation?
|
||||||
|
|
||||||
/// Location of the alias. If set, this is a linked domain.
|
/// Location of the alias. If set, this is a linked domain.
|
||||||
var aliasPath: String?
|
var aliasPath: String?
|
||||||
|
|
||||||
/// Whether the site has been secured.
|
/// Whether the site has been secured.
|
||||||
var secured: Bool!
|
var secured: Bool!
|
||||||
|
|
||||||
/// What driver is currently in use. If not detected, defaults to nil.
|
/// What driver is currently in use. If not detected, defaults to nil.
|
||||||
var driver: String? = nil
|
var driver: String?
|
||||||
|
|
||||||
/// Whether the driver was determined by checking the Composer file.
|
/// Whether the driver was determined by checking the Composer file.
|
||||||
var driverDeterminedByComposer: Bool = false
|
var driverDeterminedByComposer: Bool = false
|
||||||
|
|
||||||
/// A list of notable Composer dependencies.
|
/// A list of notable Composer dependencies.
|
||||||
var notableComposerDependencies: [String: String] = [:]
|
var notableComposerDependencies: [String: String] = [:]
|
||||||
|
|
||||||
/// The PHP version as discovered in `composer.json` or in .valetphprc.
|
/// The PHP version as discovered in `composer.json` or in .valetphprc.
|
||||||
var composerPhp: String = "???"
|
var composerPhp: String = "???"
|
||||||
|
|
||||||
/// Check whether the PHP version is valid for the currently linked version.
|
/// Check whether the PHP version is valid for the currently linked version.
|
||||||
var composerPhpCompatibleWithLinked: Bool = false
|
var composerPhpCompatibleWithLinked: Bool = false
|
||||||
|
|
||||||
/// How the PHP version was determined.
|
/// How the PHP version was determined.
|
||||||
var composerPhpSource: VersionSource = .unknown
|
var composerPhpSource: VersionSource = .unknown
|
||||||
|
|
||||||
/// Which version of PHP is actually used to serve this site.
|
/// Which version of PHP is actually used to serve this site.
|
||||||
var servingPhpVersion: String {
|
var servingPhpVersion: String {
|
||||||
return self.isolatedPhpVersion?.versionNumber.homebrewVersion
|
return self.isolatedPhpVersion?.versionNumber.homebrewVersion
|
||||||
?? PhpEnv.phpInstall.version.short
|
?? PhpEnv.phpInstall.version.short
|
||||||
}
|
}
|
||||||
|
|
||||||
enum VersionSource: String {
|
enum VersionSource: String {
|
||||||
case unknown = "unknown"
|
case unknown
|
||||||
case require = "require"
|
case require
|
||||||
case platform = "platform"
|
case platform
|
||||||
case valetphprc = "valetphprc"
|
case valetphprc
|
||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
name: String,
|
name: String,
|
||||||
tld: String,
|
tld: String,
|
||||||
@@ -78,7 +78,7 @@ class ValetSite: DomainListable {
|
|||||||
self.absolutePath = absolutePath
|
self.absolutePath = absolutePath
|
||||||
self.aliasPath = aliasPath
|
self.aliasPath = aliasPath
|
||||||
self.secured = false
|
self.secured = false
|
||||||
|
|
||||||
if makeDeterminations {
|
if makeDeterminations {
|
||||||
determineSecured()
|
determineSecured()
|
||||||
determineComposerPhpVersion()
|
determineComposerPhpVersion()
|
||||||
@@ -86,25 +86,26 @@ class ValetSite: DomainListable {
|
|||||||
determineIsolated()
|
determineIsolated()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(absolutePath: String, tld: String) {
|
convenience init(absolutePath: String, tld: String) {
|
||||||
let name = URL(fileURLWithPath: absolutePath).lastPathComponent
|
let name = URL(fileURLWithPath: absolutePath).lastPathComponent
|
||||||
self.init(name: name, tld: tld, absolutePath: absolutePath)
|
self.init(name: name, tld: tld, absolutePath: absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(aliasPath: String, tld: String) {
|
convenience init(aliasPath: String, tld: String) {
|
||||||
let name = URL(fileURLWithPath: aliasPath).lastPathComponent
|
let name = URL(fileURLWithPath: aliasPath).lastPathComponent
|
||||||
let absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
|
let absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
|
||||||
self.init(name: name, tld: tld, absolutePath: absolutePath, aliasPath: aliasPath)
|
self.init(name: name, tld: tld, absolutePath: absolutePath, aliasPath: aliasPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine whether a site is isolated.
|
Determine whether a site is isolated.
|
||||||
*/
|
*/
|
||||||
public func determineIsolated() {
|
public func determineIsolated() {
|
||||||
if let version = ValetSite.isolatedVersion("~/.config/valet/Nginx/\(self.name).\(self.tld)") {
|
if let version = ValetSite.isolatedVersion("~/.config/valet/Nginx/\(self.name).\(self.tld)") {
|
||||||
if (!PhpEnv.shared.cachedPhpInstallations.keys.contains(version)) {
|
if !PhpEnv.shared.cachedPhpInstallations.keys.contains(version) {
|
||||||
Log.err("The PHP version \(version) is isolated for the site \(self.name) but that PHP version is unavailable.")
|
Log.err("The PHP version \(version) is isolated for the site \(self.name) "
|
||||||
|
+ "but that PHP version is unavailable.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.isolatedPhpVersion = PhpEnv.shared.cachedPhpInstallations[version]
|
self.isolatedPhpVersion = PhpEnv.shared.cachedPhpInstallations[version]
|
||||||
@@ -112,7 +113,7 @@ class ValetSite: DomainListable {
|
|||||||
self.isolatedPhpVersion = nil
|
self.isolatedPhpVersion = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks if a certificate file can be found in the `valet/Certificates` directory.
|
Checks if a certificate file can be found in the `valet/Certificates` directory.
|
||||||
- Note: The file is not validated, only its presence is checked.
|
- Note: The file is not validated, only its presence is checked.
|
||||||
@@ -120,7 +121,7 @@ class ValetSite: DomainListable {
|
|||||||
public func determineSecured() {
|
public func determineSecured() {
|
||||||
secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.name).\(self.tld).key")
|
secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.name).\(self.tld).key")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks if `composer.json` exists in the folder, and extracts notable information:
|
Checks if `composer.json` exists in the folder, and extracts notable information:
|
||||||
|
|
||||||
@@ -132,36 +133,36 @@ class ValetSite: DomainListable {
|
|||||||
with the currently linked version of PHP (see `composerPhpMatchesSystem`).
|
with the currently linked version of PHP (see `composerPhpMatchesSystem`).
|
||||||
*/
|
*/
|
||||||
public func determineComposerPhpVersion() {
|
public func determineComposerPhpVersion() {
|
||||||
|
|
||||||
self.determineComposerInformation()
|
self.determineComposerInformation()
|
||||||
self.determineValetPhpFileInfo()
|
self.determineValetPhpFileInfo()
|
||||||
|
|
||||||
if self.composerPhp == "???" {
|
if self.composerPhp == "???" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the composer list (on "|") to evaluate multiple constraints
|
// Split the composer list (on "|") to evaluate multiple constraints
|
||||||
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
|
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
|
||||||
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
|
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
|
||||||
.map { string in
|
.map { string in
|
||||||
return PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
return !PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
||||||
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
|
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
.count > 0
|
.isEmpty
|
||||||
}.contains(true)
|
}.contains(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine the driver to be displayed in the list of sites. In v5.0, this has been changed
|
Determine the driver to be displayed in the list of sites. In v5.0, this has been changed
|
||||||
to load the "framework" or "project type" instead.
|
to load the "framework" or "project type" instead.
|
||||||
*/
|
*/
|
||||||
public func determineDriver() {
|
public func determineDriver() {
|
||||||
self.determineDriverViaComposer()
|
self.determineDriverViaComposer()
|
||||||
|
|
||||||
if self.driver == nil {
|
if self.driver == nil {
|
||||||
self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath)
|
self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Check the dependency list and see if a particular dependency can't be found.
|
Check the dependency list and see if a particular dependency can't be found.
|
||||||
We'll revert the dependency list so that Laravel and Symfony are detected last.
|
We'll revert the dependency list so that Laravel and Symfony are detected last.
|
||||||
@@ -171,28 +172,28 @@ class ValetSite: DomainListable {
|
|||||||
*/
|
*/
|
||||||
private func determineDriverViaComposer() {
|
private func determineDriverViaComposer() {
|
||||||
self.driverDeterminedByComposer = true
|
self.driverDeterminedByComposer = true
|
||||||
|
|
||||||
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
|
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
|
||||||
if self.notableComposerDependencies.keys.contains(key) {
|
if self.notableComposerDependencies.keys.contains(key) {
|
||||||
self.driver = value
|
self.driver = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks the contents of the composer.json file and determine the notable dependencies,
|
Checks the contents of the composer.json file and determine the notable dependencies,
|
||||||
as well as the requested PHP version. If no composer.json file is found, nothing happens.
|
as well as the requested PHP version. If no composer.json file is found, nothing happens.
|
||||||
*/
|
*/
|
||||||
private func determineComposerInformation() {
|
private func determineComposerInformation() {
|
||||||
let path = "\(absolutePath)/composer.json"
|
let path = "\(absolutePath)/composer.json"
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if Filesystem.fileExists(path) {
|
if Filesystem.fileExists(path) {
|
||||||
let decoded = try JSONDecoder().decode(
|
let decoded = try JSONDecoder().decode(
|
||||||
ComposerJson.self,
|
ComposerJson.self,
|
||||||
from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)!
|
from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)!
|
||||||
)
|
)
|
||||||
|
|
||||||
(self.composerPhp, self.composerPhpSource) = decoded.getPhpVersion()
|
(self.composerPhp, self.composerPhpSource) = decoded.getPhpVersion()
|
||||||
self.notableComposerDependencies = decoded.getNotableDependencies()
|
self.notableComposerDependencies = decoded.getNotableDependencies()
|
||||||
}
|
}
|
||||||
@@ -200,13 +201,13 @@ class ValetSite: DomainListable {
|
|||||||
Log.err("Something went wrong reading the Composer JSON file.")
|
Log.err("Something went wrong reading the Composer JSON file.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks the contents of the .valetphprc file and determine the version, if possible.
|
Checks the contents of the .valetphprc file and determine the version, if possible.
|
||||||
*/
|
*/
|
||||||
private func determineValetPhpFileInfo() {
|
private func determineValetPhpFileInfo() {
|
||||||
let path = "\(absolutePath)/.valetphprc"
|
let path = "\(absolutePath)/.valetphprc"
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if Filesystem.fileExists(path) {
|
if Filesystem.fileExists(path) {
|
||||||
let contents = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
|
let contents = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
|
||||||
@@ -219,45 +220,45 @@ class ValetSite: DomainListable {
|
|||||||
Log.err("Something went wrong parsing the .valetphprc file")
|
Log.err("Something went wrong parsing the .valetphprc file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - File Parsing
|
// MARK: - File Parsing
|
||||||
|
|
||||||
public static func isolatedVersion(_ filePath: String) -> String? {
|
public static func isolatedVersion(_ filePath: String) -> String? {
|
||||||
if Filesystem.fileExists(filePath) {
|
if Filesystem.fileExists(filePath) {
|
||||||
return NginxConfiguration
|
return NginxConfiguration
|
||||||
.init(filePath: filePath)
|
.init(filePath: filePath)
|
||||||
.isolatedVersion
|
.isolatedVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - DomainListable Protocol
|
// MARK: - DomainListable Protocol
|
||||||
|
|
||||||
func getListableName() -> String {
|
func getListableName() -> String {
|
||||||
return self.name
|
return self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableSecured() -> Bool {
|
func getListableSecured() -> Bool {
|
||||||
return self.secured
|
return self.secured
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableAbsolutePath() -> String {
|
func getListableAbsolutePath() -> String {
|
||||||
return self.absolutePath
|
return self.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListablePhpVersion() -> String {
|
func getListablePhpVersion() -> String {
|
||||||
return self.servingPhpVersion
|
return self.servingPhpVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableKind() -> String {
|
func getListableKind() -> String {
|
||||||
return (self.aliasPath == nil) ? "linked" : "parked"
|
return (self.aliasPath == nil) ? "linked" : "parked"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableType() -> String {
|
func getListableType() -> String {
|
||||||
return self.driver ?? "ZZZ"
|
return self.driver ?? "ZZZ"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListableUrl() -> URL? {
|
func getListableUrl() -> URL? {
|
||||||
return URL(string: "\(self.secured ? "https://" : "http://")\(self.name).\(Valet.shared.config.tld)")
|
return URL(string: "\(self.secured ? "https://" : "http://")\(self.name).\(Valet.shared.config.tld)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,32 +9,32 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Valet {
|
class Valet {
|
||||||
|
|
||||||
enum FeatureFlag {
|
enum FeatureFlag {
|
||||||
case isolatedSites,
|
case isolatedSites,
|
||||||
supportForPhp56
|
supportForPhp56
|
||||||
}
|
}
|
||||||
|
|
||||||
static let shared = Valet()
|
static let shared = Valet()
|
||||||
|
|
||||||
/// The version of Valet that was detected.
|
/// The version of Valet that was detected.
|
||||||
var version: String! = nil
|
var version: String! = nil
|
||||||
|
|
||||||
/// The Valet configuration file.
|
/// The Valet configuration file.
|
||||||
var config: Valet.Configuration!
|
var config: Valet.Configuration!
|
||||||
|
|
||||||
/// A cached list of sites that were detected after analyzing the paths set up for Valet.
|
/// A cached list of sites that were detected after analyzing the paths set up for Valet.
|
||||||
var sites: [ValetSite] = []
|
var sites: [ValetSite] = []
|
||||||
|
|
||||||
/// A cached list of proxies that were detecting after analyzing the Nginx paths.
|
/// A cached list of proxies that were detecting after analyzing the Nginx paths.
|
||||||
var proxies: [ValetProxy] = []
|
var proxies: [ValetProxy] = []
|
||||||
|
|
||||||
/// Whether we're busy with some blocking operation.
|
/// Whether we're busy with some blocking operation.
|
||||||
var isBusy: Bool = false
|
var isBusy: Bool = false
|
||||||
|
|
||||||
/// Various feature flags. Enabled based on the installed Valet version.
|
/// Various feature flags. Enabled based on the installed Valet version.
|
||||||
var features: [FeatureFlag] = []
|
var features: [FeatureFlag] = []
|
||||||
|
|
||||||
/// When initialising the Valet singleton, assume no sites or proxies loaded.
|
/// When initialising the Valet singleton, assume no sites or proxies loaded.
|
||||||
/// We will load the version later.
|
/// We will load the version later.
|
||||||
init() {
|
init() {
|
||||||
@@ -42,7 +42,7 @@ class Valet {
|
|||||||
self.sites = []
|
self.sites = []
|
||||||
self.proxies = []
|
self.proxies = []
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
If marketing mode is enabled, show a list of sites that are used for promotional screenshots.
|
If marketing mode is enabled, show a list of sites that are used for promotional screenshots.
|
||||||
This can be done by swapping out the real Valet scanner with one that always returns a fixed
|
This can be done by swapping out the real Valet scanner with one that always returns a fixed
|
||||||
@@ -52,25 +52,43 @@ class Valet {
|
|||||||
if ProcessInfo.processInfo.environment["PHPMON_MARKETING_MODE"] != nil {
|
if ProcessInfo.processInfo.environment["PHPMON_MARKETING_MODE"] != nil {
|
||||||
return FakeSiteScanner()
|
return FakeSiteScanner()
|
||||||
}
|
}
|
||||||
|
|
||||||
return ValetSiteScanner()
|
return ValetSiteScanner()
|
||||||
}
|
}
|
||||||
|
|
||||||
static var proxyScanner: ProxyScanner {
|
static var proxyScanner: ProxyScanner {
|
||||||
return ValetProxyScanner()
|
return ValetProxyScanner()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Check if a particular feature is enabled.
|
Check if a particular feature is enabled.
|
||||||
*/
|
*/
|
||||||
public static func enabled(feature: FeatureFlag) -> Bool {
|
public static func enabled(feature: FeatureFlag) -> Bool {
|
||||||
return self.shared.features.contains(feature)
|
return self.shared.features.contains(feature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Retrieve a list of all domains, including sites & proxies.
|
||||||
|
*/
|
||||||
public static func getDomainListable() -> [DomainListable] {
|
public static func getDomainListable() -> [DomainListable] {
|
||||||
return self.shared.sites + self.shared.proxies
|
return self.shared.sites + self.shared.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Notify the user about a non-default TLD being set.
|
||||||
|
*/
|
||||||
|
public static func notifyAboutUnsupportedTLD() {
|
||||||
|
if Valet.shared.config.tld != "test" {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
BetterAlert().withInformation(
|
||||||
|
title: "alert.warnings.tld_issue.title".localized,
|
||||||
|
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
||||||
|
description: "alert.warnings.tld_issue.description".localized
|
||||||
|
).withPrimary(text: "OK").show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
We don't want to load the initial config.json file as soon as the class is initialised.
|
We don't want to load the initial config.json file as soon as the class is initialised.
|
||||||
|
|
||||||
@@ -83,7 +101,7 @@ class Valet {
|
|||||||
public func loadConfiguration() {
|
public func loadConfiguration() {
|
||||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".config/valet/config.json")
|
.appendingPathComponent(".config/valet/config.json")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
config = try JSONDecoder().decode(
|
config = try JSONDecoder().decode(
|
||||||
Valet.Configuration.self,
|
Valet.Configuration.self,
|
||||||
@@ -93,7 +111,7 @@ class Valet {
|
|||||||
Log.err(error)
|
Log.err(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Starts the preload of sites, but only if the maximum amount of sites is 30.
|
Starts the preload of sites, but only if the maximum amount of sites is 30.
|
||||||
For users with more sites, the site list is loaded when they bring up the site list window.
|
For users with more sites, the site list is loaded when they bring up the site list window.
|
||||||
@@ -110,21 +128,21 @@ class Valet {
|
|||||||
Log.info("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!")
|
Log.info("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Reloads the list of sites, assuming that the list isn't being reloaded at the time.
|
Reloads the list of sites, assuming that the list isn't being reloaded at the time.
|
||||||
(We don't want to do duplicate or parallel work!)
|
(We don't want to do duplicate or parallel work!)
|
||||||
*/
|
*/
|
||||||
public func reloadSites() {
|
public func reloadSites() {
|
||||||
loadConfiguration()
|
loadConfiguration()
|
||||||
|
|
||||||
if (isBusy) {
|
if isBusy {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvePaths()
|
resolvePaths()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Depending on the version of Valet that is active, the feature set of PHP Monitor will change.
|
Depending on the version of Valet that is active, the feature set of PHP Monitor will change.
|
||||||
|
|
||||||
@@ -132,9 +150,9 @@ class Valet {
|
|||||||
`enabled(feature)`, which contains information about the feature set of the version of Valet that is currently
|
`enabled(feature)`, which contains information about the feature set of the version of Valet that is currently
|
||||||
in use. This allows PHP Monitor to do different things when Valet 3.0 is enabled.
|
in use. This allows PHP Monitor to do different things when Valet 3.0 is enabled.
|
||||||
*/
|
*/
|
||||||
public func evaluateFeatureSupport() -> Void {
|
public func evaluateFeatureSupport() {
|
||||||
let isOlderThanVersionThree = version.versionCompare("3.0") == .orderedAscending
|
let isOlderThanVersionThree = version.versionCompare("3.0") == .orderedAscending
|
||||||
|
|
||||||
if isOlderThanVersionThree {
|
if isOlderThanVersionThree {
|
||||||
self.features.append(.supportForPhp56)
|
self.features.append(.supportForPhp56)
|
||||||
} else {
|
} else {
|
||||||
@@ -142,16 +160,16 @@ class Valet {
|
|||||||
self.features.append(.isolatedSites)
|
self.features.append(.isolatedSites)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks if the version of Valet is more recent than the minimum version required for PHP Monitor to function.
|
Checks if the version of Valet is more recent than the minimum version required for PHP Monitor to function.
|
||||||
Should this procedure fail, the user will get an alert notifying them that the version of Valet they have
|
Should this procedure fail, the user will get an alert notifying them that the version of Valet they have
|
||||||
installed is not recent enough.
|
installed is not recent enough.
|
||||||
*/
|
*/
|
||||||
public func validateVersion() -> Void {
|
public func validateVersion() {
|
||||||
// 1. Evaluate feature support
|
// 1. Evaluate feature support
|
||||||
Valet.shared.evaluateFeatureSupport()
|
Valet.shared.evaluateFeatureSupport()
|
||||||
|
|
||||||
// 2. Notify user if the version is too old
|
// 2. Notify user if the version is too old
|
||||||
if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending {
|
if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending {
|
||||||
let version = version
|
let version = version
|
||||||
@@ -160,16 +178,20 @@ class Valet {
|
|||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.min_valet_version.title".localized,
|
title: "alert.min_valet_version.title".localized,
|
||||||
subtitle:"alert.min_valet_version.info".localized(version!, Constants.MinimumRecommendedValetVersion)
|
subtitle: "alert.min_valet_version.info".localized(
|
||||||
|
version!,
|
||||||
|
Constants.MinimumRecommendedValetVersion
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.withPrimary(text: "OK")
|
.withPrimary(text: "OK")
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.info("Valet version \(version!) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
|
Log.info("Valet version \(version!) is recent enough, OK " +
|
||||||
|
"(recommended: \(Constants.MinimumRecommendedValetVersion))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Returns a count of how many sites are linked and parked.
|
Returns a count of how many sites are linked and parked.
|
||||||
*/
|
*/
|
||||||
@@ -177,19 +199,19 @@ class Valet {
|
|||||||
return Self.siteScanner
|
return Self.siteScanner
|
||||||
.resolveSiteCount(paths: config.paths)
|
.resolveSiteCount(paths: config.paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Resolves all paths and creates linked or parked site instances that can be referenced later.
|
Resolves all paths and creates linked or parked site instances that can be referenced later.
|
||||||
*/
|
*/
|
||||||
private func resolvePaths() {
|
private func resolvePaths() {
|
||||||
isBusy = true
|
isBusy = true
|
||||||
|
|
||||||
sites = Self.siteScanner
|
sites = Self.siteScanner
|
||||||
.resolveSitesFrom(paths: config.paths)
|
.resolveSitesFrom(paths: config.paths)
|
||||||
.sorted {
|
.sorted {
|
||||||
$0.absolutePath < $1.absolutePath
|
$0.absolutePath < $1.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
proxies = Self.proxyScanner
|
proxies = Self.proxyScanner
|
||||||
.resolveProxies(
|
.resolveProxies(
|
||||||
directoryPath: FileManager.default
|
directoryPath: FileManager.default
|
||||||
@@ -197,32 +219,34 @@ class Valet {
|
|||||||
.appendingPathComponent(".config/valet/Nginx")
|
.appendingPathComponent(".config/valet/Nginx")
|
||||||
.path
|
.path
|
||||||
)
|
)
|
||||||
|
|
||||||
if let defaultPath = Valet.shared.config.defaultSite,
|
if let defaultPath = Valet.shared.config.defaultSite,
|
||||||
let site = ValetSiteScanner().resolveSite(path: defaultPath) {
|
let site = ValetSiteScanner().resolveSite(path: defaultPath) {
|
||||||
sites.insert(site, at: 0)
|
sites.insert(site, at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
isBusy = false
|
isBusy = false
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Configuration: Decodable {
|
struct Configuration: Decodable {
|
||||||
/// Top level domain suffix. Usually "test" but can be set to something else.
|
/// Top level domain suffix. Usually "test" but can be set to something else.
|
||||||
/// - Important: Does not include the actual dot. ("test", not ".test"!)
|
/// - Important: Does not include the actual dot. ("test", not ".test"!)
|
||||||
let tld: String
|
let tld: String
|
||||||
|
|
||||||
/// The paths that need to be checked.
|
/// The paths that need to be checked.
|
||||||
let paths: [String]
|
let paths: [String]
|
||||||
|
|
||||||
/// The loopback address. Optional.
|
/// The loopback address. Optional.
|
||||||
let loopback: String?
|
let loopback: String?
|
||||||
|
|
||||||
/// The default site that is served if the domain is not found. Optional.
|
/// The default site that is served if the domain is not found. Optional.
|
||||||
let defaultSite: String?
|
let defaultSite: String?
|
||||||
|
|
||||||
|
// swiftlint:disable nesting
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case tld, paths, loopback, defaultSite = "default"
|
case tld, paths, loopback, defaultSite = "default"
|
||||||
}
|
}
|
||||||
|
// swiftlint:enable nesting
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class HeaderView: NSView, XibLoadable {
|
class HeaderView: NSView, XibLoadable {
|
||||||
|
|
||||||
@IBOutlet weak var textField: NSTextField!
|
@IBOutlet weak var textField: NSTextField!
|
||||||
|
|
||||||
static func asMenuItem(text: String) -> NSMenuItem {
|
static func asMenuItem(text: String) -> NSMenuItem {
|
||||||
let view = Self.createFromXib()
|
let view = Self.createFromXib()
|
||||||
view!.textField.stringValue = text.uppercased()
|
view!.textField.stringValue = text.uppercased()
|
||||||
@@ -21,5 +21,5 @@ class HeaderView: NSView, XibLoadable {
|
|||||||
item.target = self
|
item.target = self
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,16 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
|
|
||||||
// MARK: - Nicer callbacks
|
// MARK: - Nicer callbacks
|
||||||
|
|
||||||
enum AsyncBehaviour {
|
enum AsyncBehaviour {
|
||||||
case setsBusyUI
|
case setsBusyUI
|
||||||
case reloadsPhpInstallation
|
case reloadsPhpInstallation
|
||||||
case updatesMenuBarContents
|
case updatesMenuBarContents
|
||||||
case broadcastServicesUpdate
|
case broadcastServicesUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Attempts asynchronous execution of a callback that may throw an `Error`.
|
Attempts asynchronous execution of a callback that may throw an `Error`.
|
||||||
While the callback is being executed, the UI will be marked as busy.
|
While the callback is being executed, the UI will be marked as busy.
|
||||||
@@ -48,40 +48,40 @@ extension MainMenu {
|
|||||||
if behaviours.contains(.reloadsPhpInstallation) {
|
if behaviours.contains(.reloadsPhpInstallation) {
|
||||||
PhpEnv.shared.isBusy = true
|
PhpEnv.shared.isBusy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.setsBusyUI) {
|
if behaviours.contains(.setsBusyUI) {
|
||||||
setBusyImage()
|
setBusyImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||||
var error: Error? = nil
|
var error: Error?
|
||||||
|
|
||||||
do { try execute() } catch let e { error = e }
|
do { try execute() } catch let e { error = e }
|
||||||
|
|
||||||
if behaviours.contains(.setsBusyUI) {
|
if behaviours.contains(.setsBusyUI) {
|
||||||
PhpEnv.shared.isBusy = false
|
PhpEnv.shared.isBusy = false
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
if behaviours.contains(.reloadsPhpInstallation) {
|
if behaviours.contains(.reloadsPhpInstallation) {
|
||||||
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.updatesMenuBarContents) {
|
if behaviours.contains(.updatesMenuBarContents) {
|
||||||
updatePhpVersionInStatusBar()
|
updatePhpVersionInStatusBar()
|
||||||
} else if behaviours.contains(.setsBusyUI) {
|
} else if behaviours.contains(.setsBusyUI) {
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
}
|
}
|
||||||
|
|
||||||
if behaviours.contains(.broadcastServicesUpdate) {
|
if behaviours.contains(.broadcastServicesUpdate) {
|
||||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
error == nil ? success() : failure(error!)
|
error == nil ? success() : failure(error!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func asyncWithBusyUI(
|
func asyncWithBusyUI(
|
||||||
_ execute: @escaping () throws -> Void,
|
_ execute: @escaping () throws -> Void,
|
||||||
completion: @escaping () -> Void = {}
|
completion: @escaping () -> Void = {}
|
||||||
@@ -92,5 +92,5 @@ extension MainMenu {
|
|||||||
completion()
|
completion()
|
||||||
}, behaviours: [.setsBusyUI])
|
}, behaviours: [.setsBusyUI])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import Foundation
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
|
|
||||||
@objc func fixMyValet() {
|
@objc func fixMyValet() {
|
||||||
let previousVersion = PhpEnv.phpInstall.version.short
|
let previousVersion = PhpEnv.phpInstall.version.short
|
||||||
|
|
||||||
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
|
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
|
||||||
presentAlertForMissingFormula()
|
presentAlertForMissingFormula()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !BetterAlert()
|
if !BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.fix_my_valet.title".localized,
|
title: "alert.fix_my_valet.title".localized,
|
||||||
@@ -26,12 +26,11 @@ extension MainMenu {
|
|||||||
)
|
)
|
||||||
.withPrimary(text: "alert.fix_my_valet.ok".localized)
|
.withPrimary(text: "alert.fix_my_valet.ok".localized)
|
||||||
.withSecondary(text: "alert.fix_my_valet.cancel".localized)
|
.withSecondary(text: "alert.fix_my_valet.cancel".localized)
|
||||||
.didSelectPrimary()
|
.didSelectPrimary() {
|
||||||
{
|
|
||||||
Log.info("The user has chosen to abort Fix My Valet")
|
Log.info("The user has chosen to abort Fix My Valet")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Actions.fixMyValet {
|
Actions.fixMyValet {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if previousVersion == PhpEnv.brewPhpVersion {
|
if previousVersion == PhpEnv.brewPhpVersion {
|
||||||
@@ -42,7 +41,7 @@ extension MainMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentAlertForMissingFormula() {
|
private func presentAlertForMissingFormula() {
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
@@ -52,7 +51,7 @@ extension MainMenu {
|
|||||||
.withPrimary(text: "OK")
|
.withPrimary(text: "OK")
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentAlertForSameVersion() {
|
private func presentAlertForSameVersion() {
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
@@ -63,7 +62,7 @@ extension MainMenu {
|
|||||||
.withPrimary(text: "OK")
|
.withPrimary(text: "OK")
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentAlertForDifferentVersion(version: String) {
|
private func presentAlertForDifferentVersion(version: String) {
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
@@ -76,10 +75,10 @@ extension MainMenu {
|
|||||||
MainMenu.shared.switchToPhpVersion(version)
|
MainMenu.shared.switchToPhpVersion(version)
|
||||||
})
|
})
|
||||||
.withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnv.brewPhpVersion))
|
.withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnv.brewPhpVersion))
|
||||||
.withTertiary(text: "", action: { alert in
|
.withTertiary(text: "", action: { _ in
|
||||||
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
|
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
|
||||||
})
|
})
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ extension MainMenu {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||||
}
|
}
|
||||||
|
|
||||||
if await Startup().checkEnvironment() {
|
if await Startup().checkEnvironment() {
|
||||||
self.onEnvironmentPass()
|
self.onEnvironmentPass()
|
||||||
} else {
|
} else {
|
||||||
self.onEnvironmentFail()
|
self.onEnvironmentFail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When the environment is all clear and the app can run, let's go.
|
When the environment is all clear and the app can run, let's go.
|
||||||
*/
|
*/
|
||||||
@@ -33,72 +33,61 @@ extension MainMenu {
|
|||||||
if Valet.shared.version != nil {
|
if Valet.shared.version != nil {
|
||||||
Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)")
|
Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the version (this will enforce which versions of PHP are supported)
|
// Validate the version (this will enforce which versions of PHP are supported)
|
||||||
Valet.shared.validateVersion()
|
Valet.shared.validateVersion()
|
||||||
|
|
||||||
// Actually detect the PHP versions
|
// Actually detect the PHP versions
|
||||||
PhpEnv.detectPhpVersions()
|
PhpEnv.detectPhpVersions()
|
||||||
|
|
||||||
// Check for an alias conflict
|
// Check for an alias conflict
|
||||||
if HomebrewDiagnostics.hasAliasConflict() {
|
if HomebrewDiagnostics.hasAliasConflict() {
|
||||||
DispatchQueue.main.async {
|
HomebrewDiagnostics.presentAlertAboutConflict()
|
||||||
BetterAlert()
|
|
||||||
.withInformation(
|
|
||||||
title: "alert.php_alias_conflict.title".localized,
|
|
||||||
subtitle: "alert.php_alias_conflict.info".localized
|
|
||||||
)
|
|
||||||
.withPrimary(text: "OK")
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePhpVersionInStatusBar()
|
updatePhpVersionInStatusBar()
|
||||||
|
|
||||||
Log.info("Determining broken PHP-FPM...")
|
Log.info("Determining broken PHP-FPM...")
|
||||||
// Attempt to find out if PHP-FPM is broken
|
// Attempt to find out if PHP-FPM is broken
|
||||||
let installation = PhpEnv.phpInstall
|
let installation = PhpEnv.phpInstall
|
||||||
installation.notifyAboutBrokenPhpFpm()
|
installation.notifyAboutBrokenPhpFpm()
|
||||||
|
|
||||||
// Set up the config watchers on launch (these are automatically updated via delegate methods if the user switches)
|
// Set up the config watchers on launch
|
||||||
|
// (these are automatically updated via delegate methods if the user switches)
|
||||||
Log.info("Setting up watchers...")
|
Log.info("Setting up watchers...")
|
||||||
App.shared.handlePhpConfigWatcher()
|
App.shared.handlePhpConfigWatcher()
|
||||||
|
|
||||||
// Detect applications (preset + custom)
|
// Detect applications (preset + custom)
|
||||||
Log.info("Detecting applications...")
|
Log.info("Detecting applications...")
|
||||||
App.shared.detectedApplications = Application.detectPresetApplications()
|
App.shared.detectedApplications = Application.detectPresetApplications()
|
||||||
|
|
||||||
let customApps = Preferences.custom.scanApps.map { appName in
|
let customApps = Preferences.custom.scanApps.map { appName in
|
||||||
return Application(appName, .user_supplied)
|
return Application(appName, .user_supplied)
|
||||||
}.filter { app in
|
}.filter { app in
|
||||||
return app.isInstalled()
|
return app.isInstalled()
|
||||||
}
|
}
|
||||||
|
|
||||||
App.shared.detectedApplications.append(contentsOf: customApps)
|
App.shared.detectedApplications.append(contentsOf: customApps)
|
||||||
|
|
||||||
let appNames = App.shared.detectedApplications.map { app in
|
let appNames = App.shared.detectedApplications.map { app in
|
||||||
return app.name
|
return app.name
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info("Detected applications: \(appNames)")
|
Log.info("Detected applications: \(appNames)")
|
||||||
|
|
||||||
// Load the global hotkey
|
// Load the global hotkey
|
||||||
App.shared.loadGlobalHotkey()
|
App.shared.loadGlobalHotkey()
|
||||||
|
|
||||||
// Preload sites
|
// Preload sites
|
||||||
Valet.shared.startPreloadingSites()
|
Valet.shared.startPreloadingSites()
|
||||||
|
|
||||||
// A non-default TLD is not officially supported since Valet 3.2.x
|
// A non-default TLD is not officially supported since Valet 3.2.x
|
||||||
if (Valet.shared.config.tld != "test") {
|
Valet.notifyAboutUnsupportedTLD()
|
||||||
DispatchQueue.main.async {
|
|
||||||
BetterAlert().withInformation(
|
|
||||||
title: "alert.warnings.tld_issue.title".localized,
|
|
||||||
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
|
||||||
description: "alert.warnings.tld_issue.description".localized
|
|
||||||
).withPrimary(text: "OK").show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||||
|
|
||||||
Log.info("PHP Monitor is ready to serve!")
|
Log.info("PHP Monitor is ready to serve!")
|
||||||
|
|
||||||
// Schedule a request to fetch the PHP version every 60 seconds
|
// Schedule a request to fetch the PHP version every 60 seconds
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
App.shared.timer = Timer.scheduledTimer(
|
App.shared.timer = Timer.scheduledTimer(
|
||||||
@@ -109,17 +98,17 @@ extension MainMenu {
|
|||||||
repeats: true
|
repeats: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Stats.incrementSuccessfulLaunchCount()
|
Stats.incrementSuccessfulLaunchCount()
|
||||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When the environment is not OK, present an alert to inform the user.
|
When the environment is not OK, present an alert to inform the user.
|
||||||
*/
|
*/
|
||||||
private func onEnvironmentFail() {
|
private func onEnvironmentFail() {
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
|
|
||||||
BetterAlert()
|
BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
title: "alert.cannot_start.title".localized,
|
title: "alert.cannot_start.title".localized,
|
||||||
@@ -132,7 +121,7 @@ extension MainMenu {
|
|||||||
exit(1)
|
exit(1)
|
||||||
})
|
})
|
||||||
.show()
|
.show()
|
||||||
|
|
||||||
Task { await startup() }
|
Task { await startup() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,34 +9,34 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension MainMenu {
|
extension MainMenu {
|
||||||
|
|
||||||
// MARK: - PhpSwitcherDelegate
|
// MARK: - PhpSwitcherDelegate
|
||||||
|
|
||||||
func switcherDidStartSwitching(to version: String) {}
|
func switcherDidStartSwitching(to version: String) {}
|
||||||
|
|
||||||
func switcherDidCompleteSwitch(to version: String) {
|
func switcherDidCompleteSwitch(to version: String) {
|
||||||
// Update the PHP version
|
// Update the PHP version
|
||||||
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
||||||
|
|
||||||
// Ensure the config watcher gets reloaded
|
// Ensure the config watcher gets reloaded
|
||||||
App.shared.handlePhpConfigWatcher()
|
App.shared.handlePhpConfigWatcher()
|
||||||
|
|
||||||
// Mark as no longer busy
|
// Mark as no longer busy
|
||||||
PhpEnv.shared.isBusy = false
|
PhpEnv.shared.isBusy = false
|
||||||
|
|
||||||
// Reload the site list
|
// Reload the site list
|
||||||
self.reloadDomainListData()
|
self.reloadDomainListData()
|
||||||
|
|
||||||
// Perform UI updates on main thread
|
// Perform UI updates on main thread
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
updatePhpVersionInStatusBar()
|
updatePhpVersionInStatusBar()
|
||||||
rebuild()
|
rebuild()
|
||||||
|
|
||||||
if !PhpEnv.shared.validate(version) {
|
if !PhpEnv.shared.validate(version) {
|
||||||
self.suggestFixMyValet(failed: version)
|
self.suggestFixMyValet(failed: version)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run composer updates
|
// Run composer updates
|
||||||
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
|
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
|
||||||
ComposerWindow().updateGlobalDependencies(
|
ComposerWindow().updateGlobalDependencies(
|
||||||
@@ -45,17 +45,17 @@ extension MainMenu {
|
|||||||
self.notifyAboutVersionChange(to: version)
|
self.notifyAboutVersionChange(to: version)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
self.notifyAboutVersionChange(to: version)
|
self.notifyAboutVersionChange(to: version)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
Stats.incrementSuccessfulSwitchCount()
|
Stats.incrementSuccessfulSwitchCount()
|
||||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor private func suggestFixMyValet(failed version: String) {
|
@MainActor private func suggestFixMyValet(failed version: String) {
|
||||||
let outcome = BetterAlert()
|
let outcome = BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
@@ -69,7 +69,7 @@ extension MainMenu {
|
|||||||
MainMenu.shared.fixMyValet()
|
MainMenu.shared.fixMyValet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reloadDomainListData() {
|
private func reloadDomainListData() {
|
||||||
if let window = App.shared.domainListWindowController {
|
if let window = App.shared.domainListWindowController {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@@ -79,13 +79,13 @@ extension MainMenu {
|
|||||||
Valet.shared.reloadSites()
|
Valet.shared.reloadSites()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func notifyAboutVersionChange(to version: String) {
|
private func notifyAboutVersionChange(to version: String) {
|
||||||
LocalNotification.send(
|
LocalNotification.send(
|
||||||
title: String(format: "notification.version_changed_title".localized, version),
|
title: String(format: "notification.version_changed_title".localized, version),
|
||||||
subtitle: String(format: "notification.version_changed_desc".localized, version)
|
subtitle: String(format: "notification.version_changed_desc".localized, version)
|
||||||
)
|
)
|
||||||
|
|
||||||
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
|
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ import Cocoa
|
|||||||
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
|
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
|
||||||
|
|
||||||
static let shared = MainMenu()
|
static let shared = MainMenu()
|
||||||
|
|
||||||
weak var menuDelegate: NSMenuDelegate? = nil
|
weak var menuDelegate: NSMenuDelegate?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The status bar item with variable length.
|
The status bar item with variable length.
|
||||||
*/
|
*/
|
||||||
let statusItem = NSStatusBar.system.statusItem(
|
let statusItem = NSStatusBar.system.statusItem(
|
||||||
withLength: NSStatusItem.variableLength
|
withLength: NSStatusItem.variableLength
|
||||||
)
|
)
|
||||||
|
|
||||||
// MARK: - UI related
|
// MARK: - UI related
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Rebuilds the menu (either asynchronously or synchronously).
|
Rebuilds the menu (either asynchronously or synchronously).
|
||||||
Defaults to rebuilding the menu asynchronously.
|
Defaults to rebuilding the menu asynchronously.
|
||||||
@@ -31,13 +31,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
self.rebuildMenu()
|
self.rebuildMenu()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the menu item on the main thread
|
// Update the menu item on the main thread
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
self.rebuildMenu()
|
self.rebuildMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Update the menu's contents, based on what's going on.
|
Update the menu's contents, based on what's going on.
|
||||||
This will rebuild the entire menu, so this can take a few moments.
|
This will rebuild the entire menu, so this can take a few moments.
|
||||||
@@ -47,35 +47,35 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
private func rebuildMenu() {
|
private func rebuildMenu() {
|
||||||
// Create a new menu
|
// Create a new menu
|
||||||
let menu = StatusMenu()
|
let menu = StatusMenu()
|
||||||
|
|
||||||
// Add the PHP versions (or error messages)
|
// Add the PHP versions (or error messages)
|
||||||
menu.addPhpVersionMenuItems()
|
menu.addPhpVersionMenuItems()
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
// Add the possible actions
|
// Add the possible actions
|
||||||
menu.addPhpActionMenuItems()
|
menu.addPhpActionMenuItems()
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
// Add Valet interactions
|
// Add Valet interactions
|
||||||
menu.addValetMenuItems()
|
menu.addValetMenuItems()
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
// Add services
|
// Add services
|
||||||
menu.addRemainingMenuItems()
|
menu.addRemainingMenuItems()
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
// Add about & quit menu items
|
// Add about & quit menu items
|
||||||
menu.addCoreMenuItems()
|
menu.addCoreMenuItems()
|
||||||
|
|
||||||
// Make sure every item can be interacted with
|
// Make sure every item can be interacted with
|
||||||
menu.items.forEach({ (item) in
|
menu.items.forEach({ (item) in
|
||||||
item.target = self
|
item.target = self
|
||||||
})
|
})
|
||||||
|
|
||||||
statusItem.menu = menu
|
statusItem.menu = menu
|
||||||
statusItem.menu?.delegate = self
|
statusItem.menu?.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Sets the status bar image based on a version string.
|
Sets the status bar image based on a version string.
|
||||||
*/
|
*/
|
||||||
@@ -86,7 +86,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
: MenuBarImageGenerator.textToImage(text: version)
|
: MenuBarImageGenerator.textToImage(text: version)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Sets the status bar image, based on the provided NSImage.
|
Sets the status bar image, based on the provided NSImage.
|
||||||
The image will be used as a template image.
|
The image will be used as a template image.
|
||||||
@@ -97,9 +97,9 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
button.image = image
|
button.image = image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - User Interface
|
// MARK: - User Interface
|
||||||
|
|
||||||
/** Reloads which PHP versions is currently active. */
|
/** Reloads which PHP versions is currently active. */
|
||||||
@objc func refreshActiveInstallation() {
|
@objc func refreshActiveInstallation() {
|
||||||
if !PhpEnv.shared.isBusy {
|
if !PhpEnv.shared.isBusy {
|
||||||
@@ -109,13 +109,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
Log.perf("Skipping version refresh due to busy status")
|
Log.perf("Skipping version refresh due to busy status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
/** Updates the icon (refresh icon) and rebuilds the menu. */
|
||||||
@objc func updatePhpVersionInStatusBar() {
|
@objc func updatePhpVersionInStatusBar() {
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
rebuild()
|
rebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Reloads the menu in the foreground.
|
Reloads the menu in the foreground.
|
||||||
This mimics the exact behaviours of `asyncExecution` as set in the method below.
|
This mimics the exact behaviours of `asyncExecution` as set in the method below.
|
||||||
@@ -126,7 +126,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
rebuild(async: false)
|
rebuild(async: false)
|
||||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reloads the menu in the background, using `asyncExecution`. */
|
/** Reloads the menu in the background, using `asyncExecution`. */
|
||||||
@objc func reloadPhpMonitorMenuInBackground() {
|
@objc func reloadPhpMonitorMenuInBackground() {
|
||||||
asyncExecution({
|
asyncExecution({
|
||||||
@@ -139,11 +139,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
.updatesMenuBarContents
|
.updatesMenuBarContents
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Refreshes the icon with the PHP version. */
|
/** Refreshes the icon with the PHP version. */
|
||||||
@objc func refreshIcon() {
|
@objc func refreshIcon() {
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
if (PhpEnv.shared.isBusy) {
|
if PhpEnv.shared.isBusy {
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||||
} else {
|
} else {
|
||||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||||
@@ -157,16 +157,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the icon to be displayed as busy. */
|
/** Updates the icon to be displayed as busy. */
|
||||||
@objc func setBusyImage() {
|
@objc func setBusyImage() {
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
|
|
||||||
@objc func fixHomebrewPermissions() {
|
@objc func fixHomebrewPermissions() {
|
||||||
if !BetterAlert()
|
if !BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
@@ -179,7 +179,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
.didSelectPrimary() {
|
.didSelectPrimary() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
asyncExecution {
|
asyncExecution {
|
||||||
try Actions.fixHomebrewPermissions()
|
try Actions.fixHomebrewPermissions()
|
||||||
} success: {
|
} success: {
|
||||||
@@ -195,13 +195,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
BetterAlert.show(for: error as! HomebrewPermissionError)
|
BetterAlert.show(for: error as! HomebrewPermissionError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func restartPhpFpm() {
|
@objc func restartPhpFpm() {
|
||||||
asyncExecution {
|
asyncExecution {
|
||||||
Actions.restartPhpFpm()
|
Actions.restartPhpFpm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func restartAllServices() {
|
@objc func restartAllServices() {
|
||||||
asyncExecution {
|
asyncExecution {
|
||||||
Actions.restartDnsMasq()
|
Actions.restartDnsMasq()
|
||||||
@@ -216,7 +216,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func stopAllServices() {
|
@objc func stopAllServices() {
|
||||||
asyncExecution {
|
asyncExecution {
|
||||||
Actions.stopAllServices()
|
Actions.stopAllServices()
|
||||||
@@ -229,79 +229,79 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func restartNginx() {
|
@objc func restartNginx() {
|
||||||
asyncExecution {
|
asyncExecution {
|
||||||
Actions.restartNginx()
|
Actions.restartNginx()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func restartDnsMasq() {
|
@objc func restartDnsMasq() {
|
||||||
asyncExecution {
|
asyncExecution {
|
||||||
Actions.restartDnsMasq()
|
Actions.restartDnsMasq()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func toggleXdebugMode(sender: XdebugMenuItem) {
|
@objc func toggleXdebugMode(sender: XdebugMenuItem) {
|
||||||
Log.info("Switching Xdebug to mode: \(sender.mode)")
|
Log.info("Switching Xdebug to mode: \(sender.mode)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
||||||
asyncExecution {
|
asyncExecution {
|
||||||
sender.phpExtension?.toggle()
|
sender.phpExtension?.toggle()
|
||||||
|
|
||||||
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
|
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
|
||||||
Actions.restartPhpFpm()
|
Actions.restartPhpFpm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openPhpInfo() {
|
@objc func openPhpInfo() {
|
||||||
var url: URL? = nil
|
var url: URL?
|
||||||
|
|
||||||
asyncWithBusyUI {
|
asyncWithBusyUI {
|
||||||
url = Actions.createTempPhpInfoFile()
|
url = Actions.createTempPhpInfoFile()
|
||||||
} completion: {
|
} completion: {
|
||||||
if url != nil { NSWorkspace.shared.open(url!) }
|
if url != nil { NSWorkspace.shared.open(url!) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func updateGlobalComposerDependencies() {
|
@objc func updateGlobalComposerDependencies() {
|
||||||
ComposerWindow().updateGlobalDependencies(
|
ComposerWindow().updateGlobalDependencies(
|
||||||
notify: true,
|
notify: true,
|
||||||
completion: { _ in }
|
completion: { _ in }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openActiveConfigFolder() {
|
@objc func openActiveConfigFolder() {
|
||||||
if (PhpEnv.phpInstall.version.error) {
|
if PhpEnv.phpInstall.version.error {
|
||||||
// php version was not identified
|
// php version was not identified
|
||||||
Actions.openGenericPhpConfigFolder()
|
Actions.openGenericPhpConfigFolder()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// php version was identified
|
// php version was identified
|
||||||
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
|
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openGlobalComposerFolder() {
|
@objc func openGlobalComposerFolder() {
|
||||||
Actions.openGlobalComposerFolder()
|
Actions.openGlobalComposerFolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openValetConfigFolder() {
|
@objc func openValetConfigFolder() {
|
||||||
Actions.openValetConfigFolder()
|
Actions.openValetConfigFolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func switchToPhpVersion(sender: PhpMenuItem) {
|
@objc func switchToPhpVersion(sender: PhpMenuItem) {
|
||||||
self.switchToPhpVersion(sender.version)
|
self.switchToPhpVersion(sender.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func switchToPhpVersion(_ version: String) {
|
@objc func switchToPhpVersion(_ version: String) {
|
||||||
setBusyImage()
|
setBusyImage()
|
||||||
PhpEnv.shared.isBusy = true
|
PhpEnv.shared.isBusy = true
|
||||||
PhpEnv.shared.delegate = self
|
PhpEnv.shared.delegate = self
|
||||||
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
|
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||||
updatePhpVersionInStatusBar()
|
updatePhpVersionInStatusBar()
|
||||||
rebuild()
|
rebuild()
|
||||||
@@ -313,38 +313,38 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Menu Item Functionality
|
// MARK: - Menu Item Functionality
|
||||||
|
|
||||||
@objc func openAbout() {
|
@objc func openAbout() {
|
||||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||||
NSApplication.shared.orderFrontStandardAboutPanel()
|
NSApplication.shared.orderFrontStandardAboutPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openPrefs() {
|
@objc func openPrefs() {
|
||||||
PrefsVC.show()
|
PrefsVC.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openDomainList() {
|
@objc func openDomainList() {
|
||||||
DomainListVC.show()
|
DomainListVC.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openDonate() {
|
@objc func openDonate() {
|
||||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func terminateApp() {
|
@objc func terminateApp() {
|
||||||
NSApplication.shared.terminate(nil)
|
NSApplication.shared.terminate(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Menu Delegate
|
// MARK: - Menu Delegate
|
||||||
|
|
||||||
func menuWillOpen(_ menu: NSMenu) {
|
func menuWillOpen(_ menu: NSMenu) {
|
||||||
// Make sure the shortcut key does not trigger this when the menu is open
|
// Make sure the shortcut key does not trigger this when the menu is open
|
||||||
App.shared.shortcutHotkey?.isPaused = true
|
App.shared.shortcutHotkey?.isPaused = true
|
||||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func menuDidClose(_ menu: NSMenu) {
|
func menuDidClose(_ menu: NSMenu) {
|
||||||
// When the menu is closed, allow the shortcut to work again
|
// When the menu is closed, allow the shortcut to work again
|
||||||
App.shared.shortcutHotkey?.isPaused = false
|
App.shared.shortcutHotkey?.isPaused = false
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ import Cocoa
|
|||||||
service information should also not happen in a view. Yet here we are.
|
service information should also not happen in a view. Yet here we are.
|
||||||
*/
|
*/
|
||||||
class ServicesView: NSView, XibLoadable {
|
class ServicesView: NSView, XibLoadable {
|
||||||
|
|
||||||
@IBOutlet weak var imageViewPhp: NSImageView!
|
@IBOutlet weak var imageViewPhp: NSImageView!
|
||||||
@IBOutlet weak var imageViewNginx: NSImageView!
|
@IBOutlet weak var imageViewNginx: NSImageView!
|
||||||
@IBOutlet weak var imageViewDnsmasq: NSImageView!
|
@IBOutlet weak var imageViewDnsmasq: NSImageView!
|
||||||
|
|
||||||
@IBOutlet weak var textFieldPhp: NSTextField!
|
@IBOutlet weak var textFieldPhp: NSTextField!
|
||||||
|
|
||||||
static var services: [String: HomebrewService] = [:]
|
static var services: [String: HomebrewService] = [:]
|
||||||
|
|
||||||
static func asMenuItem() -> NSMenuItem {
|
static func asMenuItem() -> NSMenuItem {
|
||||||
let view = Self.createFromXib()!
|
let view = Self.createFromXib()!
|
||||||
[view.imageViewPhp, view.imageViewNginx, view.imageViewDnsmasq].forEach { imageView in
|
[view.imageViewPhp, view.imageViewNginx, view.imageViewDnsmasq].forEach { imageView in
|
||||||
@@ -48,20 +48,20 @@ class ServicesView: NSView, XibLoadable {
|
|||||||
@objc func updateInformation() {
|
@objc func updateInformation() {
|
||||||
self.loadData()
|
self.loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadData() {
|
func loadData() {
|
||||||
self.applyAllInfoFieldsFromCachedValue()
|
self.applyAllInfoFieldsFromCachedValue()
|
||||||
HomebrewService.loadAll { services in
|
HomebrewService.loadAll { services in
|
||||||
ServicesView.services = Dictionary(uniqueKeysWithValues: services.map{ ($0.name, $0) })
|
ServicesView.services = Dictionary(uniqueKeysWithValues: services.map { ($0.name, $0) })
|
||||||
self.applyAllInfoFieldsFromCachedValue()
|
self.applyAllInfoFieldsFromCachedValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyAllInfoFieldsFromCachedValue() {
|
func applyAllInfoFieldsFromCachedValue() {
|
||||||
if ServicesView.services.keys.isEmpty {
|
if ServicesView.services.keys.isEmpty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.textFieldPhp.stringValue = PhpEnv.phpInstall.formula.uppercased()
|
self.textFieldPhp.stringValue = PhpEnv.phpInstall.formula.uppercased()
|
||||||
self.applyServiceStyling(PhpEnv.phpInstall.formula, self.imageViewPhp)
|
self.applyServiceStyling(PhpEnv.phpInstall.formula, self.imageViewPhp)
|
||||||
@@ -69,24 +69,24 @@ class ServicesView: NSView, XibLoadable {
|
|||||||
self.applyServiceStyling("dnsmasq", self.imageViewDnsmasq)
|
self.applyServiceStyling("dnsmasq", self.imageViewDnsmasq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyServiceStyling(_ serviceName: String, _ imageView: NSImageView) {
|
func applyServiceStyling(_ serviceName: String, _ imageView: NSImageView) {
|
||||||
if ServicesView.services[serviceName] == nil {
|
if ServicesView.services[serviceName] == nil {
|
||||||
imageView.image = NSImage(named: "ServiceLoading")
|
imageView.image = NSImage(named: "ServiceLoading")
|
||||||
imageView.contentTintColor = NSColor(named: "IconColorNormal")
|
imageView.contentTintColor = NSColor(named: "IconColorNormal")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ServicesView.services[serviceName]!.running {
|
if ServicesView.services[serviceName]!.running {
|
||||||
imageView.image = NSImage(named: "ServiceOn")
|
imageView.image = NSImage(named: "ServiceOn")
|
||||||
imageView.contentTintColor = NSColor(named: "IconColorNormal")
|
imageView.contentTintColor = NSColor(named: "IconColorNormal")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
imageView.image = NSImage(named: "ServiceOff")
|
imageView.image = NSImage(named: "ServiceOff")
|
||||||
imageView.contentTintColor = NSColor(named: "IconColorRed")
|
imageView.contentTintColor = NSColor(named: "IconColorRed")
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
NotificationCenter.default.removeObserver(self, name: Events.ServicesUpdated, object: nil)
|
NotificationCenter.default.removeObserver(self, name: Events.ServicesUpdated, object: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class StatsView: NSView, XibLoadable {
|
class StatsView: NSView, XibLoadable {
|
||||||
|
|
||||||
@IBOutlet weak var titleMemLimit: NSTextField!
|
@IBOutlet weak var titleMemLimit: NSTextField!
|
||||||
@IBOutlet weak var titleMaxPost: NSTextField!
|
@IBOutlet weak var titleMaxPost: NSTextField!
|
||||||
@IBOutlet weak var titleMaxUpload: NSTextField!
|
@IBOutlet weak var titleMaxUpload: NSTextField!
|
||||||
|
|
||||||
@IBOutlet weak var labelMemLimit: NSTextField!
|
@IBOutlet weak var labelMemLimit: NSTextField!
|
||||||
@IBOutlet weak var labelMaxPost: NSTextField!
|
@IBOutlet weak var labelMaxPost: NSTextField!
|
||||||
@IBOutlet weak var labelMaxUpload: NSTextField!
|
@IBOutlet weak var labelMaxUpload: NSTextField!
|
||||||
|
|
||||||
static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem {
|
static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem {
|
||||||
let view = Self.createFromXib()
|
let view = Self.createFromXib()
|
||||||
view!.titleMemLimit.stringValue = "mi_memory_limit".localized.uppercased()
|
view!.titleMemLimit.stringValue = "mi_memory_limit".localized.uppercased()
|
||||||
@@ -32,5 +32,5 @@ class StatsView: NSView, XibLoadable {
|
|||||||
item.target = self
|
item.target = self
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class StatusMenu : NSMenu {
|
class StatusMenu: NSMenu {
|
||||||
|
|
||||||
func addPhpVersionMenuItems() {
|
func addPhpVersionMenuItems() {
|
||||||
if PhpEnv.phpInstall.version.error {
|
if PhpEnv.phpInstall.version.error {
|
||||||
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
|
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
|
||||||
@@ -16,114 +16,134 @@ class StatusMenu : NSMenu {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let phpVersionText = "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)"
|
let phpVersionText = "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)"
|
||||||
addItem(HeaderView.asMenuItem(text: phpVersionText))
|
addItem(HeaderView.asMenuItem(text: phpVersionText))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPhpActionMenuItems() {
|
func addPhpActionMenuItems() {
|
||||||
if PhpEnv.shared.isBusy {
|
if PhpEnv.shared.isBusy {
|
||||||
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
|
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if PhpEnv.shared.availablePhpVersions.count == 0 {
|
if PhpEnv.shared.availablePhpVersions.isEmpty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addSwitchToPhpMenuItems()
|
self.addSwitchToPhpMenuItems()
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
self.addItem(ServicesView.asMenuItem())
|
self.addItem(ServicesView.asMenuItem())
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
func addValetMenuItems() {
|
func addValetMenuItems() {
|
||||||
self.addItem(HeaderView.asMenuItem(text: "mi_valet".localized))
|
self.addItem(HeaderView.asMenuItem(text: "mi_valet".localized))
|
||||||
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
|
self.addItem(NSMenuItem(
|
||||||
self.addItem(NSMenuItem(title: "mi_domain_list".localized, action: #selector(MainMenu.openDomainList), keyEquivalent: "l"))
|
title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
|
||||||
|
self.addItem(NSMenuItem(
|
||||||
|
title: "mi_domain_list".localized, action: #selector(MainMenu.openDomainList), keyEquivalent: "l"))
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
}
|
}
|
||||||
|
|
||||||
func addRemainingMenuItems() {
|
func addRemainingMenuItems() {
|
||||||
self.addConfigurationMenuItems()
|
self.addConfigurationMenuItems()
|
||||||
|
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
self.addComposerMenuItems()
|
self.addComposerMenuItems()
|
||||||
|
|
||||||
if (PhpEnv.shared.isBusy) {
|
if PhpEnv.shared.isBusy {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
self.addStatsMenuItem()
|
self.addStatsMenuItem()
|
||||||
|
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
self.addExtensionsMenuItems()
|
self.addExtensionsMenuItems()
|
||||||
|
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
// self.addXdebugMenuItem()
|
// self.addXdebugMenuItem()
|
||||||
|
|
||||||
self.addFirstAidAndServicesMenuItems()
|
self.addFirstAidAndServicesMenuItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addCoreMenuItems() {
|
func addCoreMenuItems() {
|
||||||
self.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(MainMenu.openPrefs), keyEquivalent: ","))
|
self.addItem(
|
||||||
self.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(MainMenu.openAbout), keyEquivalent: ""))
|
NSMenuItem(title: "mi_preferences".localized, action: #selector(MainMenu.openPrefs), keyEquivalent: ",")
|
||||||
self.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(MainMenu.terminateApp), keyEquivalent: "q"))
|
)
|
||||||
|
self.addItem(
|
||||||
|
NSMenuItem(title: "mi_about".localized, action: #selector(MainMenu.openAbout), keyEquivalent: "")
|
||||||
|
)
|
||||||
|
self.addItem(
|
||||||
|
NSMenuItem(title: "mi_quit".localized, action: #selector(MainMenu.terminateApp), keyEquivalent: "q")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Remaining Menu Items
|
// MARK: Remaining Menu Items
|
||||||
|
|
||||||
func addConfigurationMenuItems() {
|
func addConfigurationMenuItems() {
|
||||||
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
|
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
|
||||||
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
|
self.addItem(
|
||||||
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
|
NSMenuItem(title: "mi_php_config".localized,
|
||||||
|
action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c")
|
||||||
|
)
|
||||||
|
self.addItem(
|
||||||
|
NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addComposerMenuItems() {
|
func addComposerMenuItems() {
|
||||||
self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized))
|
self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized))
|
||||||
self.addItem(NSMenuItem(title: "mi_global_composer".localized, action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g"))
|
self.addItem(
|
||||||
|
NSMenuItem(title: "mi_global_composer".localized,
|
||||||
let composerMenuItem = NSMenuItem(title: "mi_update_global_composer".localized, action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies), keyEquivalent: "g")
|
action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g")
|
||||||
|
)
|
||||||
|
|
||||||
|
let composerMenuItem = NSMenuItem(
|
||||||
|
title: "mi_update_global_composer".localized,
|
||||||
|
action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies),
|
||||||
|
keyEquivalent: "g"
|
||||||
|
)
|
||||||
composerMenuItem.keyEquivalentModifierMask = .shift
|
composerMenuItem.keyEquivalentModifierMask = .shift
|
||||||
|
|
||||||
self.addItem(composerMenuItem)
|
self.addItem(composerMenuItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addStatsMenuItem() {
|
func addStatsMenuItem() {
|
||||||
guard let stats = PhpEnv.phpInstall.limits else { return }
|
guard let stats = PhpEnv.phpInstall.limits else { return }
|
||||||
|
|
||||||
self.addItem(StatsView.asMenuItem(
|
self.addItem(StatsView.asMenuItem(
|
||||||
memory: stats.memory_limit,
|
memory: stats.memory_limit,
|
||||||
post: stats.post_max_size,
|
post: stats.post_max_size,
|
||||||
upload: stats.upload_max_filesize)
|
upload: stats.upload_max_filesize)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addExtensionsMenuItems() {
|
func addExtensionsMenuItems() {
|
||||||
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
|
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
|
||||||
|
|
||||||
if (PhpEnv.phpInstall.extensions.count == 0) {
|
if PhpEnv.phpInstall.extensions.isEmpty {
|
||||||
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
|
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
var shortcutKey = 1
|
var shortcutKey = 1
|
||||||
for phpExtension in PhpEnv.phpInstall.extensions {
|
for phpExtension in PhpEnv.phpInstall.extensions {
|
||||||
self.addExtensionItem(phpExtension, shortcutKey)
|
self.addExtensionItem(phpExtension, shortcutKey)
|
||||||
shortcutKey += 1
|
shortcutKey += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addXdebugMenuItem() {
|
func addXdebugMenuItem() {
|
||||||
if !Xdebug.enabled {
|
if !Xdebug.enabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let xdebugSwitch = NSMenuItem(
|
let xdebugSwitch = NSMenuItem(
|
||||||
title: "mi_xdebug_mode".localized,
|
title: "mi_xdebug_mode".localized,
|
||||||
action: nil,
|
action: nil,
|
||||||
@@ -131,7 +151,7 @@ class StatusMenu : NSMenu {
|
|||||||
)
|
)
|
||||||
let xdebugModesMenu = NSMenu()
|
let xdebugModesMenu = NSMenu()
|
||||||
let xdebugMode = Xdebug.mode
|
let xdebugMode = Xdebug.mode
|
||||||
|
|
||||||
for mode in Xdebug.modes {
|
for mode in Xdebug.modes {
|
||||||
let item = XdebugMenuItem(
|
let item = XdebugMenuItem(
|
||||||
title: mode,
|
title: mode,
|
||||||
@@ -142,100 +162,119 @@ class StatusMenu : NSMenu {
|
|||||||
item.mode = mode
|
item.mode = mode
|
||||||
xdebugModesMenu.addItem(item)
|
xdebugModesMenu.addItem(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
for item in xdebugModesMenu.items {
|
for item in xdebugModesMenu.items {
|
||||||
item.target = MainMenu.shared
|
item.target = MainMenu.shared
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setSubmenu(xdebugModesMenu, for: xdebugSwitch)
|
self.setSubmenu(xdebugModesMenu, for: xdebugSwitch)
|
||||||
self.addItem(xdebugSwitch)
|
self.addItem(xdebugSwitch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFirstAidAndServicesMenuItems() {
|
func addFirstAidAndServicesMenuItems() {
|
||||||
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
|
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
|
||||||
let servicesMenu = NSMenu()
|
let servicesMenu = NSMenu()
|
||||||
|
|
||||||
let fixMyValetMenuItem = NSMenuItem(
|
let fixMyValetMenuItem = NSMenuItem(
|
||||||
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
|
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
|
||||||
action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
|
action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
|
||||||
)
|
)
|
||||||
fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized
|
fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized
|
||||||
servicesMenu.addItem(fixMyValetMenuItem)
|
servicesMenu.addItem(fixMyValetMenuItem)
|
||||||
|
|
||||||
let fixHomebrewMenuItem = NSMenuItem(
|
let fixHomebrewMenuItem = NSMenuItem(
|
||||||
title: "mi_fix_brew_permissions".localized(),
|
title: "mi_fix_brew_permissions".localized(),
|
||||||
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: ""
|
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: ""
|
||||||
)
|
)
|
||||||
fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized
|
fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized
|
||||||
servicesMenu.addItem(fixHomebrewMenuItem)
|
servicesMenu.addItem(fixHomebrewMenuItem)
|
||||||
|
|
||||||
servicesMenu.addItem(NSMenuItem.separator())
|
servicesMenu.addItem(NSMenuItem.separator())
|
||||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
|
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
|
||||||
|
|
||||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
|
|
||||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
|
|
||||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
|
|
||||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
|
|
||||||
servicesMenu.addItem(
|
servicesMenu.addItem(
|
||||||
NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
|
NSMenuItem(title: "mi_restart_dnsmasq".localized,
|
||||||
withKeyModifier: [.command, .shift])
|
action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d")
|
||||||
|
)
|
||||||
|
servicesMenu.addItem(
|
||||||
|
NSMenuItem(title: "mi_restart_php_fpm".localized,
|
||||||
|
action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p")
|
||||||
|
)
|
||||||
|
servicesMenu.addItem(
|
||||||
|
NSMenuItem(title: "mi_restart_nginx".localized,
|
||||||
|
action: #selector(MainMenu.restartNginx), keyEquivalent: "n")
|
||||||
|
)
|
||||||
|
servicesMenu.addItem(
|
||||||
|
NSMenuItem(title: "mi_restart_all_services".localized,
|
||||||
|
action: #selector(MainMenu.restartAllServices), keyEquivalent: "s")
|
||||||
|
)
|
||||||
|
servicesMenu.addItem(
|
||||||
|
NSMenuItem(title: "mi_stop_all_services".localized,
|
||||||
|
action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
|
||||||
|
withKeyModifier: [.command, .shift]
|
||||||
|
)
|
||||||
|
|
||||||
servicesMenu.addItem(NSMenuItem.separator())
|
servicesMenu.addItem(NSMenuItem.separator())
|
||||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized))
|
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized))
|
||||||
|
|
||||||
servicesMenu.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r"))
|
servicesMenu.addItem(
|
||||||
|
NSMenuItem(title: "mi_php_refresh".localized,
|
||||||
|
action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r")
|
||||||
|
)
|
||||||
|
|
||||||
for item in servicesMenu.items {
|
for item in servicesMenu.items {
|
||||||
item.target = MainMenu.shared
|
item.target = MainMenu.shared
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setSubmenu(servicesMenu, for: services)
|
self.setSubmenu(servicesMenu, for: services)
|
||||||
self.addItem(services)
|
self.addItem(services)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Private Helpers
|
// MARK: Private Helpers
|
||||||
|
|
||||||
private func addSwitchToPhpMenuItems() {
|
private func addSwitchToPhpMenuItems() {
|
||||||
var shortcutKey = 1
|
var shortcutKey = 1
|
||||||
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
|
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
|
||||||
|
|
||||||
// Get the short and long version
|
// Get the short and long version
|
||||||
let shortVersion = PhpEnv.shared.availablePhpVersions[index]
|
let shortVersion = PhpEnv.shared.availablePhpVersions[index]
|
||||||
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber
|
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber
|
||||||
|
|
||||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||||
let versionString = long ? longVersion.toString() : shortVersion
|
let versionString = long ? longVersion.toString() : shortVersion
|
||||||
|
|
||||||
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
||||||
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
|
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
|
||||||
let menuItem = PhpMenuItem(
|
let menuItem = PhpMenuItem(
|
||||||
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
|
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
|
||||||
action: (shortVersion == PhpEnv.phpInstall.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
|
action: (shortVersion == PhpEnv.phpInstall.version.short)
|
||||||
|
? nil
|
||||||
|
: action, keyEquivalent: "\(shortcutKey)"
|
||||||
)
|
)
|
||||||
|
|
||||||
menuItem.version = shortVersion
|
menuItem.version = shortVersion
|
||||||
shortcutKey = shortcutKey + 1
|
shortcutKey += 1
|
||||||
|
|
||||||
self.addItem(menuItem)
|
self.addItem(menuItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
||||||
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
||||||
|
|
||||||
let menuItem = ExtensionMenuItem(
|
let menuItem = ExtensionMenuItem(
|
||||||
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
|
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
|
||||||
action: #selector(MainMenu.toggleExtension),
|
action: #selector(MainMenu.toggleExtension),
|
||||||
keyEquivalent: keyEquivalent
|
keyEquivalent: keyEquivalent
|
||||||
)
|
)
|
||||||
|
|
||||||
if menuItem.keyEquivalent != "" {
|
if menuItem.keyEquivalent != "" {
|
||||||
menuItem.keyEquivalentModifierMask = [.option]
|
menuItem.keyEquivalentModifierMask = [.option]
|
||||||
}
|
}
|
||||||
|
|
||||||
menuItem.state = phpExtension.enabled ? .on : .off
|
menuItem.state = phpExtension.enabled ? .on : .off
|
||||||
menuItem.phpExtension = phpExtension
|
menuItem.phpExtension = phpExtension
|
||||||
|
|
||||||
self.addItem(menuItem)
|
self.addItem(menuItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,9 +290,9 @@ class XdebugMenuItem: NSMenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ExtensionMenuItem: NSMenuItem {
|
class ExtensionMenuItem: NSMenuItem {
|
||||||
var phpExtension: PhpExtension? = nil
|
var phpExtension: PhpExtension?
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditorMenuItem: NSMenuItem {
|
class EditorMenuItem: NSMenuItem {
|
||||||
var editor: Application? = nil
|
var editor: Application?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,47 +10,47 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class BetterAlert {
|
class BetterAlert {
|
||||||
|
|
||||||
var windowController: NSWindowController!
|
var windowController: NSWindowController!
|
||||||
|
|
||||||
var noticeVC: BetterAlertVC {
|
var noticeVC: BetterAlertVC {
|
||||||
return self.windowController.contentViewController as! BetterAlertVC
|
return self.windowController.contentViewController as! BetterAlertVC
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
|
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
self.windowController = storyboard.instantiateController(
|
self.windowController = storyboard.instantiateController(
|
||||||
withIdentifier: "noticeWindow"
|
withIdentifier: "noticeWindow"
|
||||||
) as? NSWindowController
|
) as? NSWindowController
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func make() -> BetterAlert {
|
public static func make() -> BetterAlert {
|
||||||
return BetterAlert()
|
return BetterAlert()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withPrimary(
|
public func withPrimary(
|
||||||
text: String,
|
text: String,
|
||||||
action: @escaping (BetterAlertVC) -> Void = {
|
action: @escaping (BetterAlertVC) -> Void = { vc in
|
||||||
vc in vc.close(with: .alertFirstButtonReturn)
|
vc.close(with: .alertFirstButtonReturn)
|
||||||
}
|
}
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.noticeVC.buttonPrimary.title = text
|
self.noticeVC.buttonPrimary.title = text
|
||||||
self.noticeVC.actionPrimary = action
|
self.noticeVC.actionPrimary = action
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withSecondary(
|
public func withSecondary(
|
||||||
text: String,
|
text: String,
|
||||||
action: ((BetterAlertVC) -> Void)? = {
|
action: ((BetterAlertVC) -> Void)? = { vc in
|
||||||
vc in vc.close(with: .alertSecondButtonReturn)
|
vc.close(with: .alertSecondButtonReturn)
|
||||||
}
|
}
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.noticeVC.buttonSecondary.title = text
|
self.noticeVC.buttonSecondary.title = text
|
||||||
self.noticeVC.actionSecondary = action
|
self.noticeVC.actionSecondary = action
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withTertiary(
|
public func withTertiary(
|
||||||
text: String = "",
|
text: String = "",
|
||||||
action: ((BetterAlertVC) -> Void)? = nil
|
action: ((BetterAlertVC) -> Void)? = nil
|
||||||
@@ -62,7 +62,7 @@ class BetterAlert {
|
|||||||
self.noticeVC.actionTertiary = action
|
self.noticeVC.actionTertiary = action
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withInformation(
|
public func withInformation(
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String,
|
subtitle: String,
|
||||||
@@ -71,15 +71,15 @@ class BetterAlert {
|
|||||||
self.noticeVC.labelTitle.stringValue = title
|
self.noticeVC.labelTitle.stringValue = title
|
||||||
self.noticeVC.labelSubtitle.stringValue = subtitle
|
self.noticeVC.labelSubtitle.stringValue = subtitle
|
||||||
self.noticeVC.labelDescription.stringValue = description
|
self.noticeVC.labelDescription.stringValue = description
|
||||||
|
|
||||||
// If the description is missing, handle the excess space and change the top margin
|
// If the description is missing, handle the excess space and change the top margin
|
||||||
if (description == "") {
|
if description == "" {
|
||||||
self.noticeVC.labelDescription.isHidden = true
|
self.noticeVC.labelDescription.isHidden = true
|
||||||
self.noticeVC.primaryButtonTopMargin.constant = 0
|
self.noticeVC.primaryButtonTopMargin.constant = 0
|
||||||
}
|
}
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Shows the modal and returns a ModalResponse.
|
Shows the modal and returns a ModalResponse.
|
||||||
If you wish to simply show the alert and disregard the outcome, use `show`.
|
If you wish to simply show the alert and disregard the outcome, use `show`.
|
||||||
@@ -88,12 +88,12 @@ class BetterAlert {
|
|||||||
if !Thread.isMainThread {
|
if !Thread.isMainThread {
|
||||||
fatalError("You should always present alerts on the main thread!")
|
fatalError("You should always present alerts on the main thread!")
|
||||||
}
|
}
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
windowController.window?.makeKeyAndOrderFront(nil)
|
windowController.window?.makeKeyAndOrderFront(nil)
|
||||||
return NSApplication.shared.runModal(for: windowController.window!)
|
return NSApplication.shared.runModal(for: windowController.window!)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shows the modal and returns true if the user pressed the primary button. */
|
/** Shows the modal and returns true if the user pressed the primary button. */
|
||||||
public func didSelectPrimary() -> Bool {
|
public func didSelectPrimary() -> Bool {
|
||||||
return self.runModal() == .alertFirstButtonReturn
|
return self.runModal() == .alertFirstButtonReturn
|
||||||
@@ -105,7 +105,7 @@ class BetterAlert {
|
|||||||
public func show() {
|
public func show() {
|
||||||
_ = self.runModal()
|
_ = self.runModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Shows the modal for a particular error.
|
Shows the modal for a particular error.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,30 +10,30 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class BetterAlertVC: NSViewController {
|
class BetterAlertVC: NSViewController {
|
||||||
|
|
||||||
// MARK: - Outlets
|
// MARK: - Outlets
|
||||||
|
|
||||||
@IBOutlet weak var labelTitle: NSTextField!
|
@IBOutlet weak var labelTitle: NSTextField!
|
||||||
@IBOutlet weak var labelSubtitle: NSTextField!
|
@IBOutlet weak var labelSubtitle: NSTextField!
|
||||||
@IBOutlet weak var labelDescription: NSTextField!
|
@IBOutlet weak var labelDescription: NSTextField!
|
||||||
|
|
||||||
@IBOutlet weak var buttonPrimary: NSButton!
|
@IBOutlet weak var buttonPrimary: NSButton!
|
||||||
@IBOutlet weak var buttonSecondary: NSButton!
|
@IBOutlet weak var buttonSecondary: NSButton!
|
||||||
@IBOutlet weak var buttonTertiary: NSButton!
|
@IBOutlet weak var buttonTertiary: NSButton!
|
||||||
|
|
||||||
var actionPrimary: (BetterAlertVC) -> Void = { _ in }
|
var actionPrimary: (BetterAlertVC) -> Void = { _ in }
|
||||||
var actionSecondary: ((BetterAlertVC) -> Void)?
|
var actionSecondary: ((BetterAlertVC) -> Void)?
|
||||||
var actionTertiary: ((BetterAlertVC) -> Void)?
|
var actionTertiary: ((BetterAlertVC) -> Void)?
|
||||||
|
|
||||||
@IBOutlet weak var imageView: NSImageView!
|
@IBOutlet weak var imageView: NSImageView!
|
||||||
|
|
||||||
@IBOutlet weak var primaryButtonTopMargin: NSLayoutConstraint!
|
@IBOutlet weak var primaryButtonTopMargin: NSLayoutConstraint!
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
override func viewWillAppear() {
|
override func viewWillAppear() {
|
||||||
imageView.image = NSApp.applicationIconImage
|
imageView.image = NSApp.applicationIconImage
|
||||||
|
|
||||||
if actionSecondary == nil {
|
if actionSecondary == nil {
|
||||||
buttonSecondary.isHidden = true
|
buttonSecondary.isHidden = true
|
||||||
}
|
}
|
||||||
@@ -41,22 +41,21 @@ class BetterAlertVC: NSViewController {
|
|||||||
buttonTertiary.isHidden = true
|
buttonTertiary.isHidden = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear() {
|
override func viewDidAppear() {
|
||||||
view.window?.makeFirstResponder(buttonPrimary)
|
view.window?.makeFirstResponder(buttonPrimary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Log.perf("A BetterAlert has been deinitialized.")
|
Log.perf("A BetterAlert has been deinitialized.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Outlet Actions
|
// MARK: Outlet Actions
|
||||||
|
|
||||||
@IBAction func primaryButtonAction(_ sender: Any) {
|
@IBAction func primaryButtonAction(_ sender: Any) {
|
||||||
self.actionPrimary(self)
|
self.actionPrimary(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func secondaryButtonAction(_ sender: Any) {
|
@IBAction func secondaryButtonAction(_ sender: Any) {
|
||||||
if self.actionSecondary != nil {
|
if self.actionSecondary != nil {
|
||||||
self.actionSecondary!(self)
|
self.actionSecondary!(self)
|
||||||
@@ -64,16 +63,16 @@ class BetterAlertVC: NSViewController {
|
|||||||
self.close(with: .alertSecondButtonReturn)
|
self.close(with: .alertSecondButtonReturn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func tertiaryButtonAction(_ sender: Any) {
|
@IBAction func tertiaryButtonAction(_ sender: Any) {
|
||||||
if self.actionSecondary != nil {
|
if self.actionSecondary != nil {
|
||||||
self.actionTertiary!(self)
|
self.actionTertiary!(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func close(with code: NSApplication.ModalResponse) {
|
public func close(with code: NSApplication.ModalResponse) {
|
||||||
self.view.window?.close()
|
self.view.window?.close()
|
||||||
NSApplication.shared.stopModal(withCode: code)
|
NSApplication.shared.stopModal(withCode: code)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension ActivePhpInstallation {
|
extension ActivePhpInstallation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
It is always possible that the system configuration for PHP-FPM has not been set up for Valet.
|
It is always possible that the system configuration for PHP-FPM has not been set up for Valet.
|
||||||
This can occur when a user manually installs a new PHP version, but does not run `valet install`.
|
This can occur when a user manually installs a new PHP version, but does not run `valet install`.
|
||||||
@@ -32,5 +32,5 @@ extension ActivePhpInstallation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,21 +9,21 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct GlobalKeybindPreference: Codable, CustomStringConvertible {
|
struct GlobalKeybindPreference: Codable, CustomStringConvertible {
|
||||||
|
|
||||||
// MARK: - Internal variables
|
// MARK: - Internal variables
|
||||||
|
|
||||||
let function : Bool
|
let function: Bool
|
||||||
let control : Bool
|
let control: Bool
|
||||||
let command : Bool
|
let command: Bool
|
||||||
let shift : Bool
|
let shift: Bool
|
||||||
let option : Bool
|
let option: Bool
|
||||||
let capsLock : Bool
|
let capsLock: Bool
|
||||||
let carbonFlags : UInt32
|
let carbonFlags: UInt32
|
||||||
let characters : String?
|
let characters: String?
|
||||||
let keyCode : UInt32
|
let keyCode: UInt32
|
||||||
|
|
||||||
// MARK: - How the keybind is display in Preferences
|
// MARK: - How the keybind is display in Preferences
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
var stringBuilder = ""
|
var stringBuilder = ""
|
||||||
if self.function {
|
if self.function {
|
||||||
@@ -49,19 +49,19 @@ struct GlobalKeybindPreference: Codable, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
return "\(stringBuilder)"
|
return "\(stringBuilder)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Persisting data to UserDefaults (as JSON)
|
// MARK: - Persisting data to UserDefaults (as JSON)
|
||||||
|
|
||||||
public func toJson() -> String {
|
public func toJson() -> String {
|
||||||
let jsonData = try! JSONEncoder().encode(self)
|
let jsonData = try! JSONEncoder().encode(self)
|
||||||
return String(data: jsonData, encoding: .utf8)!
|
return String(data: jsonData, encoding: .utf8)!
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func fromJson(_ string: String?) -> GlobalKeybindPreference? {
|
public static func fromJson(_ string: String?) -> GlobalKeybindPreference? {
|
||||||
if string == nil {
|
if string == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let jsonData = string!.data(using: .utf8) {
|
if let jsonData = string!.data(using: .utf8) {
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -39,24 +39,24 @@ enum InternalStats: String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Preferences {
|
class Preferences {
|
||||||
|
|
||||||
// MARK: - Singleton
|
// MARK: - Singleton
|
||||||
|
|
||||||
static var shared = Preferences()
|
static var shared = Preferences()
|
||||||
|
|
||||||
var customPreferences: CustomPrefs
|
var customPreferences: CustomPrefs
|
||||||
|
|
||||||
var cachedPreferences: [PreferenceName: Any?]
|
var cachedPreferences: [PreferenceName: Any?]
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
Preferences.handleFirstTimeLaunch()
|
Preferences.handleFirstTimeLaunch()
|
||||||
cachedPreferences = Self.cache()
|
cachedPreferences = Self.cache()
|
||||||
customPreferences = CustomPrefs(scanApps: [])
|
customPreferences = CustomPrefs(scanApps: [])
|
||||||
loadCustomPreferences()
|
loadCustomPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - First Time Run
|
// MARK: - First Time Run
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Note: macOS seems to cache plist values in memory as well as in files.
|
Note: macOS seems to cache plist values in memory as well as in files.
|
||||||
You can find the persisted configuration file in: ~/Library/Preferences/com.nicoverbruggen.phpmon.plist
|
You can find the persisted configuration file in: ~/Library/Preferences/com.nicoverbruggen.phpmon.plist
|
||||||
@@ -81,17 +81,17 @@ class Preferences {
|
|||||||
InternalStats.launchCount.rawValue: 0,
|
InternalStats.launchCount.rawValue: 0,
|
||||||
InternalStats.didSeeSponsorEncouragement.rawValue: false
|
InternalStats.didSeeSponsorEncouragement.rawValue: false
|
||||||
])
|
])
|
||||||
|
|
||||||
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
|
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
|
||||||
handleMigration()
|
handleMigration()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info("Saving first-time preferences!")
|
Log.info("Saving first-time preferences!")
|
||||||
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
|
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
|
||||||
UserDefaults.standard.synchronize()
|
UserDefaults.standard.synchronize()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Sometimes preferences will change, and a migration is required to take the user's previous preference
|
Sometimes preferences will change, and a migration is required to take the user's previous preference
|
||||||
and migrate it over to the new type. For example, the choice to disable the icon next to the version
|
and migrate it over to the new type. For example, the choice to disable the icon next to the version
|
||||||
@@ -99,26 +99,25 @@ class Preferences {
|
|||||||
*/
|
*/
|
||||||
static func handleMigration() {
|
static func handleMigration() {
|
||||||
// If the user chose the "no icon" option, migrate it over
|
// If the user chose the "no icon" option, migrate it over
|
||||||
if (
|
if
|
||||||
UserDefaults.standard.value(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue) != nil &&
|
UserDefaults.standard.value(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue) != nil &&
|
||||||
UserDefaults.standard.bool(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue) == false
|
UserDefaults.standard.bool(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue) == false {
|
||||||
) {
|
|
||||||
Log.info("The preference where the user chose no icon has been migrated over.")
|
Log.info("The preference where the user chose no icon has been migrated over.")
|
||||||
UserDefaults.standard.set(MenuBarIcon.noIcon.rawValue, forKey: PreferenceName.iconTypeToDisplay.rawValue)
|
UserDefaults.standard.set(MenuBarIcon.noIcon.rawValue, forKey: PreferenceName.iconTypeToDisplay.rawValue)
|
||||||
UserDefaults.standard.removeObject(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue)
|
UserDefaults.standard.removeObject(forKey: RetiredPreferenceName.shouldDisplayPhpHintInIcon.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - API
|
// MARK: - API
|
||||||
|
|
||||||
static var preferences: [PreferenceName: Any?] {
|
static var preferences: [PreferenceName: Any?] {
|
||||||
return Self.shared.cachedPreferences
|
return Self.shared.cachedPreferences
|
||||||
}
|
}
|
||||||
|
|
||||||
static var custom: CustomPrefs {
|
static var custom: CustomPrefs {
|
||||||
return Self.shared.customPreferences
|
return Self.shared.customPreferences
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine whether a particular preference is enabled.
|
Determine whether a particular preference is enabled.
|
||||||
- Important: Requires the preference to have a corresponding boolean value, or a fatal error will be thrown.
|
- Important: Requires the preference to have a corresponding boolean value, or a fatal error will be thrown.
|
||||||
@@ -130,38 +129,45 @@ class Preferences {
|
|||||||
fatalError("\(preference) is not a valid boolean preference!")
|
fatalError("\(preference) is not a valid boolean preference!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Internal Functionality
|
// MARK: - Internal Functionality
|
||||||
|
|
||||||
private static func cache() -> [PreferenceName: Any] {
|
private static func cache() -> [PreferenceName: Any] {
|
||||||
return [
|
return [
|
||||||
// Part 1: Always Booleans
|
// Part 1: Always Booleans
|
||||||
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
|
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(
|
||||||
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
|
forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
|
||||||
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
|
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(
|
||||||
.autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any,
|
forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
|
||||||
.allowProtocolForIntegrations: UserDefaults.standard.bool(forKey: PreferenceName.allowProtocolForIntegrations.rawValue) as Any,
|
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(
|
||||||
|
forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
|
||||||
|
.autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(
|
||||||
|
forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any,
|
||||||
|
.allowProtocolForIntegrations: UserDefaults.standard.bool(
|
||||||
|
forKey: PreferenceName.allowProtocolForIntegrations.rawValue) as Any,
|
||||||
|
|
||||||
// Part 2: Always Strings
|
// Part 2: Always Strings
|
||||||
.globalHotkey: UserDefaults.standard.string(forKey: PreferenceName.globalHotkey.rawValue) as Any,
|
.globalHotkey: UserDefaults.standard.string(
|
||||||
.iconTypeToDisplay: UserDefaults.standard.string(forKey: PreferenceName.iconTypeToDisplay.rawValue) as Any,
|
forKey: PreferenceName.globalHotkey.rawValue) as Any,
|
||||||
|
.iconTypeToDisplay: UserDefaults.standard.string(
|
||||||
|
forKey: PreferenceName.iconTypeToDisplay.rawValue) as Any
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
static func update(_ preference: PreferenceName, value: Any?) {
|
static func update(_ preference: PreferenceName, value: Any?) {
|
||||||
if (value == nil) {
|
if value == nil {
|
||||||
UserDefaults.standard.removeObject(forKey: preference.rawValue)
|
UserDefaults.standard.removeObject(forKey: preference.rawValue)
|
||||||
} else {
|
} else {
|
||||||
UserDefaults.standard.setValue(value, forKey: preference.rawValue)
|
UserDefaults.standard.setValue(value, forKey: preference.rawValue)
|
||||||
}
|
}
|
||||||
UserDefaults.standard.synchronize()
|
UserDefaults.standard.synchronize()
|
||||||
|
|
||||||
// Update the preferences cache in memory!
|
// Update the preferences cache in memory!
|
||||||
Preferences.shared.cachedPreferences = Preferences.cache()
|
Preferences.shared.cachedPreferences = Preferences.cache()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Custom Preferences
|
// MARK: - Custom Preferences
|
||||||
|
|
||||||
private func loadCustomPreferences() {
|
private func loadCustomPreferences() {
|
||||||
let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json")
|
let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json")
|
||||||
if Filesystem.fileExists(url.path) {
|
if Filesystem.fileExists(url.path) {
|
||||||
@@ -171,7 +177,7 @@ class Preferences {
|
|||||||
Log.info("There was no .phpmon.conf.json file to be loaded.")
|
Log.info("There was no .phpmon.conf.json file to be loaded.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadCustomPreferencesFile(_ url: URL) {
|
private func loadCustomPreferencesFile(_ url: URL) {
|
||||||
do {
|
do {
|
||||||
customPreferences = try JSONDecoder().decode(
|
customPreferences = try JSONDecoder().decode(
|
||||||
@@ -183,5 +189,5 @@ class Preferences {
|
|||||||
Log.warn("The .phpmon.conf.json file seems to be missing or malformed.")
|
Log.warn("The .phpmon.conf.json file seems to be missing or malformed.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,105 +10,133 @@ import Cocoa
|
|||||||
import Carbon
|
import Carbon
|
||||||
|
|
||||||
class PrefsVC: NSViewController {
|
class PrefsVC: NSViewController {
|
||||||
|
|
||||||
// MARK: - Window Identifier
|
// MARK: - Window Identifier
|
||||||
|
|
||||||
@IBOutlet weak var stackView: NSStackView!
|
@IBOutlet weak var stackView: NSStackView!
|
||||||
|
|
||||||
// MARK: - Display
|
// MARK: - Display
|
||||||
|
|
||||||
public static func create(delegate: NSWindowDelegate?) {
|
public static func create(delegate: NSWindowDelegate?) {
|
||||||
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
|
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||||
|
|
||||||
let windowController = storyboard.instantiateController(
|
let windowController = storyboard.instantiateController(
|
||||||
withIdentifier: "preferencesWindow"
|
withIdentifier: "preferencesWindow"
|
||||||
) as! PrefsWC
|
) as! PrefsWC
|
||||||
|
|
||||||
windowController.window!.title = "prefs.title".localized
|
windowController.window!.title = "prefs.title".localized
|
||||||
windowController.window!.subtitle = "prefs.subtitle".localized
|
windowController.window!.subtitle = "prefs.subtitle".localized
|
||||||
windowController.window!.delegate = delegate
|
windowController.window!.delegate = delegate
|
||||||
windowController.window!.styleMask = [.titled, .closable, .miniaturizable]
|
windowController.window!.styleMask = [.titled, .closable, .miniaturizable]
|
||||||
windowController.window!.delegate = windowController
|
windowController.window!.delegate = windowController
|
||||||
windowController.positionWindowInTopLeftCorner()
|
windowController.positionWindowInTopLeftCorner()
|
||||||
|
|
||||||
App.shared.preferencesWindowController = windowController
|
App.shared.preferencesWindowController = windowController
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func show(delegate: NSWindowDelegate? = nil) {
|
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||||
if (App.shared.preferencesWindowController == nil) {
|
if App.shared.preferencesWindowController == nil {
|
||||||
Self.create(delegate: delegate)
|
Self.create(delegate: delegate)
|
||||||
}
|
}
|
||||||
|
|
||||||
App.shared.preferencesWindowController!.showWindow(self)
|
App.shared.preferencesWindowController!.showWindow(self)
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
[
|
[
|
||||||
CheckboxPreferenceView.make(
|
getDynamicIconPreferenceView(),
|
||||||
sectionText: "prefs.dynamic_icon".localized,
|
getIconOptionsPreferenceView(),
|
||||||
descriptionText: "prefs.dynamic_icon_desc".localized,
|
getIconDensityPreferenceView(),
|
||||||
checkboxText: "prefs.dynamic_icon_title".localized,
|
getAutoRestartPreferenceView(),
|
||||||
preference: .shouldDisplayDynamicIcon,
|
getAutomaticComposerUpdatePreferenceView(),
|
||||||
action: {
|
getShortcutPreferenceView(),
|
||||||
MainMenu.shared.refreshIcon()
|
getIntegrationsPreferenceView()
|
||||||
}
|
|
||||||
),
|
|
||||||
SelectPreferenceView.make(
|
|
||||||
sectionText: "",
|
|
||||||
descriptionText: "prefs.icon_options_desc".localized,
|
|
||||||
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
|
||||||
localizationPrefix: "prefs.icon_options",
|
|
||||||
preference: .iconTypeToDisplay,
|
|
||||||
action: {
|
|
||||||
MainMenu.shared.refreshIcon()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
CheckboxPreferenceView.make(
|
|
||||||
sectionText: "prefs.info_density".localized,
|
|
||||||
descriptionText: "prefs.display_full_php_version_desc".localized,
|
|
||||||
checkboxText: "prefs.display_full_php_version".localized,
|
|
||||||
preference: .fullPhpVersionDynamicIcon,
|
|
||||||
action: {
|
|
||||||
MainMenu.shared.refreshIcon()
|
|
||||||
MainMenu.shared.rebuild()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
CheckboxPreferenceView.make(
|
|
||||||
sectionText: "prefs.services".localized,
|
|
||||||
descriptionText: "prefs.auto_restart_services_desc".localized,
|
|
||||||
checkboxText: "prefs.auto_restart_services_title".localized,
|
|
||||||
preference: .autoServiceRestartAfterExtensionToggle,
|
|
||||||
action: {}
|
|
||||||
),
|
|
||||||
CheckboxPreferenceView.make(
|
|
||||||
sectionText: "prefs.switcher".localized,
|
|
||||||
descriptionText: "prefs.auto_composer_update_desc".localized,
|
|
||||||
checkboxText: "prefs.auto_composer_update_title".localized,
|
|
||||||
preference: .autoComposerGlobalUpdateAfterSwitch,
|
|
||||||
action: {}
|
|
||||||
),
|
|
||||||
HotkeyPreferenceView.make(
|
|
||||||
sectionText: "prefs.global_shortcut".localized,
|
|
||||||
descriptionText: "prefs.shortcut_desc".localized,
|
|
||||||
self
|
|
||||||
),
|
|
||||||
CheckboxPreferenceView.make(
|
|
||||||
sectionText: "prefs.integrations".localized,
|
|
||||||
descriptionText: "prefs.open_protocol_desc".localized,
|
|
||||||
checkboxText: "prefs.open_protocol_title".localized,
|
|
||||||
preference: .allowProtocolForIntegrations,
|
|
||||||
action: {}
|
|
||||||
),
|
|
||||||
].forEach({ self.stackView.addArrangedSubview($0) })
|
].forEach({ self.stackView.addArrangedSubview($0) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getDynamicIconPreferenceView() -> NSView {
|
||||||
|
return CheckboxPreferenceView.make(
|
||||||
|
sectionText: "prefs.dynamic_icon".localized,
|
||||||
|
descriptionText: "prefs.dynamic_icon_desc".localized,
|
||||||
|
checkboxText: "prefs.dynamic_icon_title".localized,
|
||||||
|
preference: .shouldDisplayDynamicIcon,
|
||||||
|
action: {
|
||||||
|
MainMenu.shared.refreshIcon()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getIconOptionsPreferenceView() -> NSView {
|
||||||
|
return SelectPreferenceView.make(
|
||||||
|
sectionText: "",
|
||||||
|
descriptionText: "prefs.icon_options_desc".localized,
|
||||||
|
options: MenuBarIcon.allCases.map({ return $0.rawValue }),
|
||||||
|
localizationPrefix: "prefs.icon_options",
|
||||||
|
preference: .iconTypeToDisplay,
|
||||||
|
action: {
|
||||||
|
MainMenu.shared.refreshIcon()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getIconDensityPreferenceView() -> NSView {
|
||||||
|
return CheckboxPreferenceView.make(
|
||||||
|
sectionText: "prefs.info_density".localized,
|
||||||
|
descriptionText: "prefs.display_full_php_version_desc".localized,
|
||||||
|
checkboxText: "prefs.display_full_php_version".localized,
|
||||||
|
preference: .fullPhpVersionDynamicIcon,
|
||||||
|
action: {
|
||||||
|
MainMenu.shared.refreshIcon()
|
||||||
|
MainMenu.shared.rebuild()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getAutoRestartPreferenceView() -> NSView {
|
||||||
|
return CheckboxPreferenceView.make(
|
||||||
|
sectionText: "prefs.services".localized,
|
||||||
|
descriptionText: "prefs.auto_restart_services_desc".localized,
|
||||||
|
checkboxText: "prefs.auto_restart_services_title".localized,
|
||||||
|
preference: .autoServiceRestartAfterExtensionToggle,
|
||||||
|
action: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getAutomaticComposerUpdatePreferenceView() -> NSView {
|
||||||
|
CheckboxPreferenceView.make(
|
||||||
|
sectionText: "prefs.switcher".localized,
|
||||||
|
descriptionText: "prefs.auto_composer_update_desc".localized,
|
||||||
|
checkboxText: "prefs.auto_composer_update_title".localized,
|
||||||
|
preference: .autoComposerGlobalUpdateAfterSwitch,
|
||||||
|
action: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getShortcutPreferenceView() -> NSView {
|
||||||
|
return HotkeyPreferenceView.make(
|
||||||
|
sectionText: "prefs.global_shortcut".localized,
|
||||||
|
descriptionText: "prefs.shortcut_desc".localized,
|
||||||
|
self
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getIntegrationsPreferenceView() -> NSView {
|
||||||
|
return CheckboxPreferenceView.make(
|
||||||
|
sectionText: "prefs.integrations".localized,
|
||||||
|
descriptionText: "prefs.open_protocol_desc".localized,
|
||||||
|
checkboxText: "prefs.open_protocol_title".localized,
|
||||||
|
preference: .allowProtocolForIntegrations,
|
||||||
|
action: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Listening for hotkey delegate
|
// MARK: - Listening for hotkey delegate
|
||||||
|
|
||||||
var listeningForHotkeyView: HotkeyPreferenceView? = nil
|
var listeningForHotkeyView: HotkeyPreferenceView?
|
||||||
|
|
||||||
override func viewWillDisappear() {
|
override func viewWillDisappear() {
|
||||||
if listeningForHotkeyView !== nil {
|
if listeningForHotkeyView !== nil {
|
||||||
listeningForHotkeyView = nil
|
listeningForHotkeyView = nil
|
||||||
@@ -116,7 +144,7 @@ class PrefsVC: NSViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Deinitialization
|
// MARK: - Deinitialization
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Log.perf("PrefsVC deallocated")
|
Log.perf("PrefsVC deallocated")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ struct Keys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PrefsWC: PMWindowController {
|
class PrefsWC: PMWindowController {
|
||||||
|
|
||||||
// MARK: - Window Identifier
|
// MARK: - Window Identifier
|
||||||
|
|
||||||
override var windowName: String {
|
override var windowName: String {
|
||||||
return "Preferences"
|
return "Preferences"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Key Interaction
|
// MARK: - Key Interaction
|
||||||
|
|
||||||
override func keyDown(with event: NSEvent) {
|
override func keyDown(with event: NSEvent) {
|
||||||
super.keyDown(with: event)
|
super.keyDown(with: event)
|
||||||
|
|
||||||
if let vc = contentViewController as? PrefsVC {
|
if let vc = contentViewController as? PrefsVC {
|
||||||
if vc.listeningForHotkeyView != nil {
|
if vc.listeningForHotkeyView != nil {
|
||||||
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
|
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
|
||||||
@@ -37,5 +37,5 @@ class PrefsWC: PMWindowController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class Stats {
|
class Stats {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Keep track of how many times the app has been successfully launched.
|
Keep track of how many times the app has been successfully launched.
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class Stats {
|
|||||||
forKey: InternalStats.launchCount.rawValue
|
forKey: InternalStats.launchCount.rawValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Keep track of how many times the app has successfully switched
|
Keep track of how many times the app has successfully switched
|
||||||
between different PHP versions.
|
between different PHP versions.
|
||||||
@@ -37,7 +37,7 @@ class Stats {
|
|||||||
forKey: InternalStats.switchCount.rawValue
|
forKey: InternalStats.switchCount.rawValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Did the user see the sponsor encouragement / thank you message?
|
Did the user see the sponsor encouragement / thank you message?
|
||||||
Annoying the user is the worst, so let's not show the message twice.
|
Annoying the user is the worst, so let's not show the message twice.
|
||||||
@@ -47,7 +47,7 @@ class Stats {
|
|||||||
forKey: InternalStats.didSeeSponsorEncouragement.rawValue
|
forKey: InternalStats.didSeeSponsorEncouragement.rawValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Increment the successful launch count. This should only be
|
Increment the successful launch count. This should only be
|
||||||
called when the user has not encountered ANY issues starting
|
called when the user has not encountered ANY issues starting
|
||||||
@@ -59,7 +59,7 @@ class Stats {
|
|||||||
forKey: InternalStats.launchCount.rawValue
|
forKey: InternalStats.launchCount.rawValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Increment the successful switch count.
|
Increment the successful switch count.
|
||||||
*/
|
*/
|
||||||
@@ -69,7 +69,7 @@ class Stats {
|
|||||||
forKey: InternalStats.switchCount.rawValue
|
forKey: InternalStats.switchCount.rawValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Determine if the sponsor message should be displayed.
|
Determine if the sponsor message should be displayed.
|
||||||
|
|
||||||
@@ -86,19 +86,20 @@ class Stats {
|
|||||||
(see `didSeeSponsorEncouragement`)
|
(see `didSeeSponsorEncouragement`)
|
||||||
*/
|
*/
|
||||||
public static func evaluateSponsorMessageShouldBeDisplayed() {
|
public static func evaluateSponsorMessageShouldBeDisplayed() {
|
||||||
|
|
||||||
if Bundle.main.bundleIdentifier?.contains("beta") ?? false {
|
if Bundle.main.bundleIdentifier?.contains("beta") ?? false {
|
||||||
return Log.info("Sponsor messages never apply to beta builds.")
|
return Log.info("Sponsor messages never apply to beta builds.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if Stats.didSeeSponsorEncouragement {
|
if Stats.didSeeSponsorEncouragement {
|
||||||
return Log.info("Awesome, the user has already seen the sponsor message.")
|
return Log.info("Awesome, the user has already seen the sponsor message.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if Stats.successfulLaunchCount < 7 && Stats.successfulSwitchCount < 40 {
|
if Stats.successfulLaunchCount < 7 && Stats.successfulSwitchCount < 40 {
|
||||||
return Log.info("It is too soon to see the sponsor message (launched \(Stats.successfulLaunchCount) times, switched \(Stats.successfulSwitchCount) times).")
|
return Log.info("It is too soon to see the sponsor message (launched \(Stats.successfulLaunchCount) " +
|
||||||
|
"times, switched \(Stats.successfulSwitchCount) times).")
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let donate = BetterAlert()
|
let donate = BetterAlert()
|
||||||
.withInformation(
|
.withInformation(
|
||||||
@@ -117,9 +118,9 @@ class Stats {
|
|||||||
Log.info("The user is an absolute badass for choosing this option. Thank you.")
|
Log.info("The user is an absolute badass for choosing this option. Thank you.")
|
||||||
NSWorkspace.shared.open(Constants.Urls.DonationPayment)
|
NSWorkspace.shared.open(Constants.Urls.DonationPayment)
|
||||||
}
|
}
|
||||||
|
|
||||||
UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue)
|
UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,30 @@
|
|||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class CheckboxPreferenceView: NSView, XibLoadable {
|
class CheckboxPreferenceView: NSView, XibLoadable {
|
||||||
|
|
||||||
@IBOutlet weak var labelSection: NSTextField!
|
@IBOutlet weak var labelSection: NSTextField!
|
||||||
@IBOutlet weak var labelDescription: NSTextField!
|
@IBOutlet weak var labelDescription: NSTextField!
|
||||||
@IBOutlet weak var buttonCheckbox: NSButton!
|
@IBOutlet weak var buttonCheckbox: NSButton!
|
||||||
|
|
||||||
var action: (() -> Void)!
|
var action: (() -> Void)!
|
||||||
|
|
||||||
var preference: PreferenceName! {
|
var preference: PreferenceName! {
|
||||||
didSet {
|
didSet {
|
||||||
self.buttonCheckbox.state = Preferences.isEnabled(self.preference) ? .on : .off
|
self.buttonCheckbox.state = Preferences.isEnabled(self.preference) ? .on : .off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func make(sectionText: String, descriptionText: String, checkboxText: String, preference: PreferenceName, action: @escaping () -> Void) -> NSView {
|
static func make(
|
||||||
|
sectionText: String,
|
||||||
|
descriptionText: String,
|
||||||
|
checkboxText: String,
|
||||||
|
preference: PreferenceName,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) -> NSView {
|
||||||
let view = Self.createFromXib()!
|
let view = Self.createFromXib()!
|
||||||
view.labelSection.stringValue = sectionText
|
view.labelSection.stringValue = sectionText
|
||||||
view.labelDescription.stringValue = descriptionText
|
view.labelDescription.stringValue = descriptionText
|
||||||
@@ -34,10 +38,10 @@ class CheckboxPreferenceView: NSView, XibLoadable {
|
|||||||
view.action = action
|
view.action = action
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func toggled(_ sender: Any) {
|
@IBAction func toggled(_ sender: Any) {
|
||||||
Preferences.update(self.preference, value: buttonCheckbox.state == .on)
|
Preferences.update(self.preference, value: buttonCheckbox.state == .on)
|
||||||
self.action()
|
self.action()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user