1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-07 03:50:08 +02:00

Added linting

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

15
.swiftlint.yml Normal file
View File

@ -0,0 +1,15 @@
disabled_rules:
- todo
- identifier_name
- force_try
- force_cast
opt_in_rules:
- empty_count
included:
- phpmon
- phpmon-tests
excluded:
- phpmon/Vendor

View File

@ -230,6 +230,7 @@
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; }; C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; }; C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; };
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; }; C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; }; C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */; }; C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */; };
@ -405,6 +406,7 @@
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; }; C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = "<group>"; }; C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = "<group>"; };
C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = "<group>"; }; C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = "<group>"; };
C4F5FBCC28218C93001065C5 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; }; C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
@ -549,6 +551,7 @@
C4E713562570150F00007428 /* SECURITY.md */, C4E713562570150F00007428 /* SECURITY.md */,
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */, C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
54D9E0C027E4F5E9003B9AD9 /* LICENSE */, 54D9E0C027E4F5E9003B9AD9 /* LICENSE */,
C4F5FBCC28218C93001065C5 /* .swiftlint.yml */,
C4E713572570151400007428 /* docs */, C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */, C41C1B3522B0097F00E7CF16 /* phpmon */,
C4F7807A25D7F84B000DBC97 /* phpmon-tests */, C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
@ -974,6 +977,7 @@
C41C1B2F22B0097F00E7CF16 /* Sources */, C41C1B2F22B0097F00E7CF16 /* Sources */,
C41C1B3022B0097F00E7CF16 /* Frameworks */, C41C1B3022B0097F00E7CF16 /* Frameworks */,
C41C1B3122B0097F00E7CF16 /* Resources */, C41C1B3122B0097F00E7CF16 /* Resources */,
C4F5FBCB28216985001065C5 /* Run `swiftlint` */,
); );
buildRules = ( buildRules = (
); );
@ -1089,6 +1093,27 @@
}; };
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
C4F5FBCB28216985001065C5 /* Run `swiftlint` */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run `swiftlint`";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
C41C1B2F22B0097F00E7CF16 /* Sources */ = { C41C1B2F22B0097F00E7CF16 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
@ -1291,6 +1316,7 @@
C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */, C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */,
C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */, C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */,
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */, C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */,
C40B24F227A310770018C7D2 /* Events.swift in Sources */, C40B24F227A310770018C7D2 /* Events.swift in Sources */,
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */, C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
C4C0E8E027F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */, C4C0E8E027F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */,

View File

@ -12,33 +12,28 @@ 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",
@ -64,34 +59,30 @@ class Actions {
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])
@ -99,8 +90,7 @@ class Actions {
// 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)
@ -124,8 +114,7 @@ class Actions {
If this does not solve the issue, the user may need to install additional If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`. extensions and/or run `composer global update`.
*/ */
public static func fixMyValet(completed: @escaping () -> Void) public static func fixMyValet(completed: @escaping () -> Void) {
{
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: { InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
brew("services restart dnsmasq", sudo: true) brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true) brew("services restart php", sudo: true)

View File

@ -28,7 +28,7 @@ public class Command {
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")

View File

@ -11,24 +11,21 @@
/** /**
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: "\\/")
@ -45,8 +42,7 @@ func sed(file: String, original: String, replacement: String)
/** /**
Uses `grep` to determine whether a particular query string can be found in a particular file. Uses `grep` to determine whether a particular query string can be found in a particular file.
*/ */
func grepContains(file: String, query: String) -> Bool func grepContains(file: String, query: String) -> Bool {
{
return Shell.pipe(""" return Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO" grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""") """)

View File

@ -49,7 +49,7 @@ public class Paths {
// - 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

View File

@ -35,8 +35,11 @@ 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()

View File

@ -21,11 +21,11 @@ extension NSWindow {
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()

View File

@ -17,7 +17,7 @@ extension String {
} }
func countInstances(of stringToFind: String) -> Int { func countInstances(of stringToFind: String) -> Int {
if (stringToFind.isEmpty) { if stringToFind.isEmpty {
return 0 return 0
} }

View File

@ -26,10 +26,10 @@ extension XibLoadable where Self: NSView {
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? { static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
guard let xibName = xibName else { return nil } guard let xibName = xibName else { return nil }
var topLevelArray: NSArray? = nil var topLevelArray: NSArray?
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray) bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
guard let results = topLevelArray else { return nil } guard let results = topLevelArray else { return nil }
let views = Array<Any>(results).filter { $0 is Self } let views = [Any](results).filter { $0 is Self }
return views.last as? Self return views.last as? Self
} }

View File

@ -27,7 +27,7 @@ class Alert {
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

View File

@ -24,7 +24,7 @@ class MenuBarImageGenerator {
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)

View File

@ -23,7 +23,7 @@ class VersionExtractor {
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 {

View File

@ -35,7 +35,7 @@ class ActivePhpInstallation {
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
@ -60,9 +60,9 @@ class ActivePhpInstallation {
// 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)
} }
} }
} }
@ -71,12 +71,12 @@ class ActivePhpInstallation {
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
@ -112,13 +112,13 @@ class ActivePhpInstallation {
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"
} }

View File

@ -26,7 +26,7 @@ class Xdebug {
"debug", "debug",
"gcstats", "gcstats",
"profile", "profile",
"trace", "trace"
] ]
} }

View File

@ -15,7 +15,7 @@ class PhpEnv {
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,
@ -76,15 +76,14 @@ class PhpEnv {
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"))
@ -95,7 +94,7 @@ class PhpEnv {
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)
} }
@ -144,8 +143,7 @@ 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)
} }
} }

View File

@ -21,7 +21,8 @@ class PhpHelper {
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
} }
} }

View File

@ -134,7 +134,12 @@ public struct PhpVersionNumber: Equatable {
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(
@ -143,7 +148,7 @@ 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])
} }

View File

@ -23,7 +23,8 @@ class PhpExtension {
/// 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.
@ -34,6 +35,7 @@ class PhpExtension {
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,13 +49,14 @@ 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
@ -69,7 +72,8 @@ class PhpExtension {
} }
/** /**
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
@ -91,7 +95,7 @@ class PhpExtension {
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 []
} }

View File

@ -20,8 +20,7 @@ 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
@ -32,7 +31,7 @@ class InternalSwitcher: PhpSwitcher {
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)
} }
@ -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)
@ -93,7 +93,7 @@ class InternalSwitcher: PhpSwitcher {
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 {

View File

@ -38,7 +38,7 @@ extension App {
If there are no windows open, the app will be an accessory (toolbar) app. If there are no windows open, the app will be an accessory (toolbar) app.
*/ */
public func updateActivationPolicy() { public func updateActivationPolicy() {
NSApp.setActivationPolicy(openWindows.count > 0 ? .regular : .accessory) NSApp.setActivationPolicy(!openWindows.isEmpty ? .regular : .accessory)
} }
} }

View File

@ -41,10 +41,10 @@ class App {
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] = []
@ -57,7 +57,7 @@ class App {
/** /**
The shortcut the user has requested. The shortcut the user has requested.
*/ */
var shortcutHotkey: HotKey? = nil { var shortcutHotkey: HotKey? {
didSet { didSet {
setupGlobalHotkeyListener() setupGlobalHotkeyListener()
} }

View File

@ -44,4 +44,3 @@ extension AppDelegate {
} }
} }
} }

View File

@ -61,7 +61,7 @@ class InterApp {
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available." subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
).withPrimary(text: "OK").show() ).withPrimary(text: "OK").show()
} }
}), })
]} ]}
} }

View File

@ -17,8 +17,7 @@ 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)")

View File

@ -145,7 +145,7 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
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
} }

View File

@ -137,7 +137,7 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
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
} }

View File

@ -9,8 +9,7 @@
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!

View File

@ -9,8 +9,7 @@
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!

View File

@ -9,11 +9,10 @@
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!
@ -53,7 +52,7 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol
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

View File

@ -9,8 +9,7 @@
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!

View File

@ -9,8 +9,7 @@
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!

View File

@ -68,11 +68,11 @@ extension DomainListVC {
} }
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:)),
@ -85,7 +85,7 @@ 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),
@ -107,7 +107,11 @@ extension DomainListVC {
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)
} }

View File

@ -27,7 +27,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
} }
/// 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 = ""
@ -55,7 +55,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
return domains[tableView.selectedRow] return domains[tableView.selectedRow]
} }
var timer: Timer? = nil var timer: Timer?
// MARK: - Display // MARK: - Display
@ -80,7 +80,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
} }
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)
} }
@ -137,8 +137,7 @@ 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()
@ -168,17 +167,12 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
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
@ -199,7 +193,7 @@ 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()
} }
} }

View File

@ -95,7 +95,7 @@ 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)
} }

View File

@ -16,8 +16,8 @@ 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 {
@ -40,8 +40,7 @@ struct ComposerJson: Decodable {
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)
@ -76,5 +75,3 @@ struct ComposerJson: Decodable {
} }
} }

View File

@ -10,10 +10,10 @@ 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.
@ -80,7 +80,7 @@ class ComposerWindow {
// 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

View File

@ -37,7 +37,7 @@ 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",
@ -61,7 +61,7 @@ struct PhpFrameworks {
], ],
"Typo3": [ "Typo3": [
"/typo3", "/typo3",
"/public/typo3", "/public/typo3"
] ]
] ]

View File

@ -17,8 +17,7 @@ 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") {
@ -34,7 +33,8 @@ class HomebrewDiagnostics {
).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)
@ -55,12 +55,23 @@ class HomebrewDiagnostics {
} }
} }
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(

View File

@ -44,7 +44,7 @@ class NginxConfiguration {
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)!]
@ -60,7 +60,7 @@ class NginxConfiguration {
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)!],

View File

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

View File

@ -8,8 +8,7 @@
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

View File

@ -6,14 +6,16 @@
// 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,
path: "~/Code/tailwind/site", linked: true, constraint: "8.0"),
ValetSite(fakeWithName: "forge", tld: "test", secure: true, path: "~/Code/laravel/forge", linked: true), 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"),

View File

@ -8,8 +8,7 @@
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]

View File

@ -8,8 +8,7 @@
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

View File

@ -26,9 +26,9 @@ extension ValetSite {
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

View File

@ -36,7 +36,7 @@ class ValetSite: DomainListable {
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
@ -60,10 +60,10 @@ class ValetSite: DomainListable {
} }
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(
@ -103,8 +103,9 @@ class ValetSite: DomainListable {
*/ */
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]
@ -144,9 +145,9 @@ class ValetSite: DomainListable {
// 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)
} }

View File

@ -67,10 +67,28 @@ class Valet {
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.
@ -118,7 +136,7 @@ class Valet {
public func reloadSites() { public func reloadSites() {
loadConfiguration() loadConfiguration()
if (isBusy) { if isBusy {
return return
} }
@ -132,7 +150,7 @@ 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 {
@ -148,7 +166,7 @@ class Valet {
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()
@ -160,13 +178,17 @@ 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))")
} }
} }
@ -220,9 +242,11 @@ class Valet {
/// The default site that is served if the domain is not found. Optional. /// The default site that is served if the domain is not found. Optional.
let defaultSite: String? let defaultSite: String?
// swiftlint:disable nesting
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case tld, paths, loopback, defaultSite = "default" case tld, paths, loopback, defaultSite = "default"
} }
// swiftlint:enable nesting
} }
} }

View File

@ -54,7 +54,7 @@ extension MainMenu {
} }
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 }

View File

@ -26,8 +26,7 @@ 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
} }
@ -76,7 +75,7 @@ extension MainMenu {
MainMenu.shared.switchToPhpVersion(version) MainMenu.shared.switchToPhpVersion(version)
}) })
.withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnv.brewPhpVersion)) .withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnv.brewPhpVersion))
.withTertiary(text: "", action: { alert in .withTertiary(text: "", action: { _ in
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions) NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
}) })
.show() .show()

View File

@ -42,15 +42,7 @@ extension MainMenu {
// 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()
@ -60,22 +52,27 @@ extension MainMenu {
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
@ -85,15 +82,7 @@ extension MainMenu {
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)

View File

@ -11,7 +11,7 @@ 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.
@ -143,7 +143,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
/** 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 {
@ -257,7 +257,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
} }
@objc func openPhpInfo() { @objc func openPhpInfo() {
var url: URL? = nil var url: URL?
asyncWithBusyUI { asyncWithBusyUI {
url = Actions.createTempPhpInfoFile() url = Actions.createTempPhpInfoFile()
@ -274,7 +274,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
} }
@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

View File

@ -27,7 +27,7 @@ class StatusMenu : NSMenu {
return return
} }
if PhpEnv.shared.availablePhpVersions.count == 0 { if PhpEnv.shared.availablePhpVersions.isEmpty {
return return
} }
@ -40,8 +40,10 @@ class StatusMenu : NSMenu {
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())
} }
@ -52,7 +54,7 @@ class StatusMenu : NSMenu {
self.addComposerMenuItems() self.addComposerMenuItems()
if (PhpEnv.shared.isBusy) { if PhpEnv.shared.isBusy {
return return
} }
@ -72,24 +74,42 @@ class StatusMenu : NSMenu {
} }
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,
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") 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)
@ -108,7 +128,7 @@ class StatusMenu : NSMenu {
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: ""))
} }
@ -172,18 +192,35 @@ class StatusMenu : NSMenu {
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
@ -210,11 +247,13 @@ class StatusMenu : NSMenu {
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)
} }
@ -251,9 +290,9 @@ class XdebugMenuItem: NSMenuItem {
} }
class ExtensionMenuItem: NSMenuItem { class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension? = nil var phpExtension: PhpExtension?
} }
class EditorMenuItem: NSMenuItem { class EditorMenuItem: NSMenuItem {
var editor: Application? = nil var editor: Application?
} }

View File

@ -31,8 +31,8 @@ class 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
@ -42,8 +42,8 @@ class BetterAlert {
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
@ -73,7 +73,7 @@ class BetterAlert {
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
} }

View File

@ -46,7 +46,6 @@ class BetterAlertVC: NSViewController {
view.window?.makeFirstResponder(buttonPrimary) view.window?.makeFirstResponder(buttonPrimary)
} }
deinit { deinit {
Log.perf("A BetterAlert has been deinitialized.") Log.perf("A BetterAlert has been deinitialized.")
} }

View File

@ -99,10 +99,9 @@ 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)
@ -136,20 +135,27 @@ class Preferences {
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)

View File

@ -35,7 +35,7 @@ class PrefsVC: NSViewController {
} }
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)
} }
@ -47,7 +47,18 @@ class PrefsVC: NSViewController {
override func viewDidLoad() { override func viewDidLoad() {
[ [
CheckboxPreferenceView.make( getDynamicIconPreferenceView(),
getIconOptionsPreferenceView(),
getIconDensityPreferenceView(),
getAutoRestartPreferenceView(),
getAutomaticComposerUpdatePreferenceView(),
getShortcutPreferenceView(),
getIntegrationsPreferenceView()
].forEach({ self.stackView.addArrangedSubview($0) })
}
private func getDynamicIconPreferenceView() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.dynamic_icon".localized, sectionText: "prefs.dynamic_icon".localized,
descriptionText: "prefs.dynamic_icon_desc".localized, descriptionText: "prefs.dynamic_icon_desc".localized,
checkboxText: "prefs.dynamic_icon_title".localized, checkboxText: "prefs.dynamic_icon_title".localized,
@ -55,8 +66,11 @@ class PrefsVC: NSViewController {
action: { action: {
MainMenu.shared.refreshIcon() MainMenu.shared.refreshIcon()
} }
), )
SelectPreferenceView.make( }
private func getIconOptionsPreferenceView() -> NSView {
return SelectPreferenceView.make(
sectionText: "", sectionText: "",
descriptionText: "prefs.icon_options_desc".localized, descriptionText: "prefs.icon_options_desc".localized,
options: MenuBarIcon.allCases.map({ return $0.rawValue }), options: MenuBarIcon.allCases.map({ return $0.rawValue }),
@ -65,8 +79,11 @@ class PrefsVC: NSViewController {
action: { action: {
MainMenu.shared.refreshIcon() MainMenu.shared.refreshIcon()
} }
), )
CheckboxPreferenceView.make( }
private func getIconDensityPreferenceView() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.info_density".localized, sectionText: "prefs.info_density".localized,
descriptionText: "prefs.display_full_php_version_desc".localized, descriptionText: "prefs.display_full_php_version_desc".localized,
checkboxText: "prefs.display_full_php_version".localized, checkboxText: "prefs.display_full_php_version".localized,
@ -75,39 +92,50 @@ class PrefsVC: NSViewController {
MainMenu.shared.refreshIcon() MainMenu.shared.refreshIcon()
MainMenu.shared.rebuild() MainMenu.shared.rebuild()
} }
), )
CheckboxPreferenceView.make( }
private func getAutoRestartPreferenceView() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.services".localized, sectionText: "prefs.services".localized,
descriptionText: "prefs.auto_restart_services_desc".localized, descriptionText: "prefs.auto_restart_services_desc".localized,
checkboxText: "prefs.auto_restart_services_title".localized, checkboxText: "prefs.auto_restart_services_title".localized,
preference: .autoServiceRestartAfterExtensionToggle, preference: .autoServiceRestartAfterExtensionToggle,
action: {} action: {}
), )
}
private func getAutomaticComposerUpdatePreferenceView() -> NSView {
CheckboxPreferenceView.make( CheckboxPreferenceView.make(
sectionText: "prefs.switcher".localized, sectionText: "prefs.switcher".localized,
descriptionText: "prefs.auto_composer_update_desc".localized, descriptionText: "prefs.auto_composer_update_desc".localized,
checkboxText: "prefs.auto_composer_update_title".localized, checkboxText: "prefs.auto_composer_update_title".localized,
preference: .autoComposerGlobalUpdateAfterSwitch, preference: .autoComposerGlobalUpdateAfterSwitch,
action: {} action: {}
), )
HotkeyPreferenceView.make( }
private func getShortcutPreferenceView() -> NSView {
return HotkeyPreferenceView.make(
sectionText: "prefs.global_shortcut".localized, sectionText: "prefs.global_shortcut".localized,
descriptionText: "prefs.shortcut_desc".localized, descriptionText: "prefs.shortcut_desc".localized,
self self
), )
CheckboxPreferenceView.make( }
private func getIntegrationsPreferenceView() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.integrations".localized, sectionText: "prefs.integrations".localized,
descriptionText: "prefs.open_protocol_desc".localized, descriptionText: "prefs.open_protocol_desc".localized,
checkboxText: "prefs.open_protocol_title".localized, checkboxText: "prefs.open_protocol_title".localized,
preference: .allowProtocolForIntegrations, preference: .allowProtocolForIntegrations,
action: {} action: {}
), )
].forEach({ self.stackView.addArrangedSubview($0) })
} }
// 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 {

View File

@ -96,7 +96,8 @@ class Stats {
} }
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 {

View File

@ -6,8 +6,6 @@
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2021 Nico Verbruggen. All rights reserved.
// //
import Foundation
import Foundation import Foundation
import Cocoa import Cocoa
@ -25,7 +23,13 @@ class CheckboxPreferenceView: NSView, XibLoadable {
} }
} }
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

View File

@ -6,8 +6,6 @@
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2021 Nico Verbruggen. All rights reserved.
// //
import Foundation
import Foundation import Foundation
import Cocoa import Cocoa
@ -66,7 +64,7 @@ class HotkeyPreferenceView: NSView, XibLoadable {
func loadGlobalKeybindFromPreferences() { func loadGlobalKeybindFromPreferences() {
let globalKeybind = GlobalKeybindPreference.fromJson(Preferences.preferences[.globalHotkey] as! String?) let globalKeybind = GlobalKeybindPreference.fromJson(Preferences.preferences[.globalHotkey] as! String?)
if (globalKeybind != nil) { if globalKeybind != nil {
updateKeybindButton(globalKeybind!) updateKeybindButton(globalKeybind!)
} else { } else {
buttonSetShortcut.title = "prefs.shortcut_set".localized buttonSetShortcut.title = "prefs.shortcut_set".localized

View File

@ -16,7 +16,7 @@ class SelectPreferenceView: NSView, XibLoadable {
@IBOutlet weak var popupButton: NSPopUpButton! @IBOutlet weak var popupButton: NSPopUpButton!
var localizationPrefix: String = "" var localizationPrefix: String = ""
var imagePrefix: String? = nil var imagePrefix: String?
var options: [String] = [] { var options: [String] = [] {
didSet { didSet {
@ -50,6 +50,7 @@ class SelectPreferenceView: NSView, XibLoadable {
} }
} }
// swiftlint:disable function_parameter_count
static func make( static func make(
sectionText: String, sectionText: String,
descriptionText: String, descriptionText: String,
@ -57,8 +58,7 @@ class SelectPreferenceView: NSView, XibLoadable {
localizationPrefix: String, localizationPrefix: String,
imagePrefix: String? = nil, imagePrefix: String? = nil,
preference: PreferenceName, preference: PreferenceName,
action: @escaping () -> Void) -> NSView action: @escaping () -> Void) -> NSView {
{
let view = Self.createFromXib()! let view = Self.createFromXib()!
view.labelSection.stringValue = sectionText view.labelSection.stringValue = sectionText
@ -72,6 +72,7 @@ class SelectPreferenceView: NSView, XibLoadable {
return view return view
} }
// swiftlint:enable function_parameter_count
@IBAction func valueChanged(_ sender: Any) { @IBAction func valueChanged(_ sender: Any) {
let index = self.popupButton.indexOfSelectedItem let index = self.popupButton.indexOfSelectedItem

View File

@ -39,7 +39,7 @@ class ProgressWindowController: NSWindowController, NSWindowDelegate {
return return
} }
textView.string = textView.string + string textView.string += string
textView.scrollToEndOfDocument(nil) textView.scrollToEndOfDocument(nil)
} }

View File

@ -22,10 +22,10 @@ class Paths {
let optBrewFound = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew") let optBrewFound = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew")
let usrBrewFound = Shell.fileExists("\(HomebrewDir.usr.rawValue)/bin/brew") let usrBrewFound = Shell.fileExists("\(HomebrewDir.usr.rawValue)/bin/brew")
if (optBrewFound) { if optBrewFound {
// This is usually the case with Homebrew installed on Apple Silicon // This is usually the case with Homebrew installed on Apple Silicon
baseDir = .opt baseDir = .opt
} else if (usrBrewFound) { } else if usrBrewFound {
// This is usually the case with Homebrew installed on Intel (or Rosetta 2) // This is usually the case with Homebrew installed on Intel (or Rosetta 2)
baseDir = .usr baseDir = .usr
} else { } else {

View File

@ -16,7 +16,7 @@ class PhpConfigWatcher {
var didChange: ((URL) -> Void)? var didChange: ((URL) -> Void)?
var lastUpdate: TimeInterval? = nil var lastUpdate: TimeInterval?
var watchers: [FSWatcher] = [] var watchers: [FSWatcher] = []
@ -46,7 +46,11 @@ class PhpConfigWatcher {
})) }))
} }
func addWatcher(for url: URL, eventMask: DispatchSource.FileSystemEvent, behaviour: FSWatcherBehaviour = .reloadsMenu) { func addWatcher(
for url: URL,
eventMask: DispatchSource.FileSystemEvent,
behaviour: FSWatcherBehaviour = .reloadsMenu
) {
if !Filesystem.fileExists(url.path) { if !Filesystem.fileExists(url.path) {
Log.warn("No watcher was created for \(url.path) because the requested file does not exist.") Log.warn("No watcher was created for \(url.path) because the requested file does not exist.")
return return
@ -84,13 +88,21 @@ class FSWatcher {
let url: URL let url: URL
init(for url: URL, eventMask: DispatchSource.FileSystemEvent, parent: PhpConfigWatcher, behaviour: FSWatcherBehaviour = .reloadsMenu) { init(
for url: URL,
eventMask: DispatchSource.FileSystemEvent,
parent: PhpConfigWatcher,
behaviour: FSWatcherBehaviour = .reloadsMenu
) {
self.url = url self.url = url
self.parent = parent self.parent = parent
self.startMonitoring(eventMask, behaviour: behaviour) self.startMonitoring(eventMask, behaviour: behaviour)
} }
func startMonitoring(_ eventMask: DispatchSource.FileSystemEvent, behaviour: FSWatcherBehaviour) { func startMonitoring(
_ eventMask: DispatchSource.FileSystemEvent,
behaviour: FSWatcherBehaviour
) {
guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else { guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else {
return return
} }

View File

@ -91,7 +91,6 @@ final class HotKeysController {
hotKeys.removeValue(forKey: box.carbonHotKeyID) hotKeys.removeValue(forKey: box.carbonHotKeyID)
} }
// MARK: - Events // MARK: - Events
static func handleCarbonEvent(_ event: EventRef?) -> OSStatus { static func handleCarbonEvent(_ event: EventRef?) -> OSStatus {
@ -157,7 +156,6 @@ final class HotKeysController {
InstallEventHandler(GetEventDispatcherTarget(), hotKeyEventHandler, 2, eventSpec, nil, &eventHandler) InstallEventHandler(GetEventDispatcherTarget(), hotKeyEventHandler, 2, eventSpec, nil, &eventHandler)
} }
// MARK: - Querying // MARK: - Querying
private static func hotKey(for carbonHotKeyID: UInt32) -> HotKey? { private static func hotKey(for carbonHotKeyID: UInt32) -> HotKey? {