diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 3a76475..c3e04e0 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -52,6 +52,8 @@ C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; }; C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; }; C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; }; + C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; }; + C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; }; C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; }; C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; }; C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; }; @@ -208,6 +210,7 @@ C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = ""; }; C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = ""; }; C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = ""; }; + C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+Actions.swift"; sourceTree = ""; }; C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = ""; }; C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.swift"; sourceTree = ""; }; C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = ""; }; @@ -314,6 +317,7 @@ C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */, C40C7F1D2772136000DDDCDC /* PhpSwitcher.swift */, C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */, + C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */, C4F2E4392752F7D00020E974 /* PhpInstallation.swift */, C4ACA38E25C754C100060C66 /* PhpExtension.swift */, ); @@ -363,14 +367,6 @@ path = Core; sourceTree = ""; }; - C40C7F2C2772204700DDDCDC /* PHP */ = { - isa = PBXGroup; - children = ( - C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */, - ); - path = PHP; - sourceTree = ""; - }; C415D3D72770F341005EF286 /* phpmon-cli */ = { isa = PBXGroup; children = ( @@ -433,7 +429,6 @@ children = ( C4AF9F6B275445D300D44ED0 /* Integrations */, C4B13B1D25C4915000548C3A /* Core */, - C40C7F2C2772204700DDDCDC /* PHP */, C47331A0247093AC009A0597 /* Menu */, C464ADAA275A7A25003FCD53 /* SiteList */, 5420395726135DB800FB00FA /* Preferences */, @@ -461,6 +456,7 @@ C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */, C464ADAE275A7A69003FCD53 /* SiteListVC.swift */, C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */, + C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */, C464ADB1275A87CA003FCD53 /* SiteListCell.swift */, ); path = SiteList; @@ -793,6 +789,7 @@ 54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */, C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */, C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */, + C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */, C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */, 54AB03262763858F00A29D5F /* Timer.swift in Sources */, C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */, @@ -824,6 +821,7 @@ files = ( 54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */, C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */, + C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */, C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */, 54AB03272763858F00A29D5F /* Timer.swift in Sources */, 54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */, diff --git a/phpmon-common/Core/Constants.swift b/phpmon-common/Core/Constants.swift index 50f5c27..6bb3f62 100644 --- a/phpmon-common/Core/Constants.swift +++ b/phpmon-common/Core/Constants.swift @@ -50,7 +50,5 @@ class Constants { // dev release. In this case, that means that the version below is detected. "8.2" ] - - } diff --git a/phpmon-common/Core/Shell.swift b/phpmon-common/Core/Shell.swift index 59aa4b9..2dfcdf9 100644 --- a/phpmon-common/Core/Shell.swift +++ b/phpmon-common/Core/Shell.swift @@ -109,7 +109,8 @@ public class Shell { Uses `/bin/echo` instead of the `builtin` (which does not support `-n`). */ public static func fileExists(_ path: String) -> Bool { - return Shell.pipe("if [ -f \(path) ]; then /bin/echo -n \"0\"; fi") == "0" + let escapedPath = path.replacingOccurrences(of: " ", with: "\\ ") + return Shell.pipe("if [ -f \(escapedPath) ]; then /bin/echo -n \"0\"; fi") == "0" } /** diff --git a/phpmon/Domain/PHP/ActivePhpInstallation+Checks.swift b/phpmon-common/PHP/ActivePhpInstallation+Checks.swift similarity index 100% rename from phpmon/Domain/PHP/ActivePhpInstallation+Checks.swift rename to phpmon-common/PHP/ActivePhpInstallation+Checks.swift diff --git a/phpmon/Domain/Core/Base.lproj/Main.storyboard b/phpmon/Domain/Core/Base.lproj/Main.storyboard index 05e731f..acc1270 100644 --- a/phpmon/Domain/Core/Base.lproj/Main.storyboard +++ b/phpmon/Domain/Core/Base.lproj/Main.storyboard @@ -500,13 +500,35 @@ + + + + + + + + + + + + @@ -518,15 +540,19 @@ + + + + @@ -576,12 +602,13 @@ - + + diff --git a/phpmon/Domain/Helpers/Application.swift b/phpmon/Domain/Helpers/Application.swift index e337ed5..6617a01 100644 --- a/phpmon/Domain/Helpers/Application.swift +++ b/phpmon/Domain/Helpers/Application.swift @@ -34,7 +34,7 @@ class Application { (This will open the app if it isn't open yet.) */ @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. */ diff --git a/phpmon/Domain/Integrations/Valet/Valet.swift b/phpmon/Domain/Integrations/Valet/Valet.swift index f42ff61..a40295b 100644 --- a/phpmon/Domain/Integrations/Valet/Valet.swift +++ b/phpmon/Domain/Integrations/Valet/Valet.swift @@ -37,10 +37,14 @@ class Valet { } public func startPreloadingSites() { - if self.sites.count <= 10 { + let maximumPreload = 10 + let foundSites = self.countPaths() + if foundSites <= maximumPreload { // Preload the sites and their drivers - Log.info("Fewer than or 11 sites found, preloading list of sites...") + Log.info("Fewer than or \(maximumPreload) sites found, preloading list of sites...") self.reloadSites() + } else { + Log.info("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!") } } @@ -64,6 +68,25 @@ class Valet { } } + /** + Returns a count of how many sites are linked and parked. + */ + private func countPaths() -> Int { + var count = 0 + for path in config.paths { + let entries = try! FileManager.default.contentsOfDirectory(atPath: path) + for entry in entries { + if resolveSite(entry, forPath: path) { + count += 1 + } + } + } + return count + } + + /** + Resolves all paths and creates linked or parked site instances that can be referenced later. + */ private func resolvePaths(tld: String) { sites = [] @@ -75,6 +98,28 @@ class Valet { } } + /** + Determines whether the site can be resolved as a symbolic link or as a directory. + Regular files are ignored. Returns true if the path can be parsed. + */ + private func resolveSite(_ entry: String, forPath path: String) -> Bool { + let siteDir = path + "/" + entry + + let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir) + + let type = attrs[FileAttributeKey.type] as! FileAttributeType + + if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory { + return true + } + + return false + } + + /** + 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. + */ private func resolvePath(_ entry: String, forPath path: String, tld: String) { let siteDir = path + "/" + entry @@ -84,6 +129,12 @@ class Valet { // We can also determine whether the thing at the path is a directory, too let type = attrs[FileAttributeKey.type] as! FileAttributeType + // We should also check that we can interpret the path correctly + if URL(fileURLWithPath: siteDir).lastPathComponent == "" { + print("Warning: could not parse the site: \(siteDir), skipping!") + return + } + if type == FileAttributeType.typeSymbolicLink { sites.append(Site(aliasPath: siteDir, tld: tld)) } else if type == FileAttributeType.typeDirectory { @@ -114,7 +165,7 @@ class Valet { convenience init(absolutePath: String, tld: String) { self.init() self.absolutePath = absolutePath - self.name = URL(string: absolutePath)!.lastPathComponent + self.name = URL(fileURLWithPath: absolutePath).lastPathComponent self.aliasPath = nil determineSecured(tld) determineDriver() @@ -123,7 +174,7 @@ class Valet { convenience init(aliasPath: String, tld: String) { self.init() self.absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath) - self.name = URL(string: aliasPath)!.lastPathComponent + self.name = URL(fileURLWithPath: aliasPath).lastPathComponent self.aliasPath = aliasPath determineSecured(tld) determineDriver() @@ -134,7 +185,7 @@ class Valet { } public func determineDriver() { - let driver = Shell.pipe("cd \(absolutePath!) && valet which", requiresPath: true) + let driver = Shell.pipe("cd '\(absolutePath!)' && valet which", requiresPath: true) if driver.contains("This site is served by") { self.driver = driver // TODO: Use a regular expression to retrieve the driver instead? diff --git a/phpmon/Domain/SiteList/SiteListCell.swift b/phpmon/Domain/SiteList/SiteListCell.swift index 1bcf0f3..4af0450 100644 --- a/phpmon/Domain/SiteList/SiteListCell.swift +++ b/phpmon/Domain/SiteList/SiteListCell.swift @@ -19,6 +19,9 @@ class SiteListCell: NSTableCellView @IBOutlet weak var labelDriver: NSTextField! + @IBOutlet weak var buttonWarning: NSButton! + @IBOutlet weak var labelWarning: NSTextField! + override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) } @@ -27,6 +30,11 @@ class SiteListCell: NSTableCellView // Make sure to show the TLD labelSiteName.stringValue = "\(site.name!).\(Valet.shared.config.tld)" + let isProblematic = site.name.contains(" ") + buttonWarning.isHidden = !isProblematic + labelWarning.isHidden = !isProblematic + labelWarning.stringValue = "site_list.warning.spaces".localized + // Show the absolute path, except make sure to replace the /Users/username segment with ~ for readability labelPathName.stringValue = site.absolutePath .replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~") diff --git a/phpmon/Domain/SiteList/SiteListVC+Actions.swift b/phpmon/Domain/SiteList/SiteListVC+Actions.swift new file mode 100644 index 0000000..b386571 --- /dev/null +++ b/phpmon/Domain/SiteList/SiteListVC+Actions.swift @@ -0,0 +1,97 @@ +// +// SiteListVC+Actions.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation +import Cocoa + +extension SiteListVC { + + @objc func toggleSecure() { + let rowToReload = tableView.selectedRow + let originalSecureStatus = selectedSite!.secured + let action = selectedSite!.secured ? "unsecure" : "secure" + let selectedSite = selectedSite! + let command = "cd '\(selectedSite.absolutePath!)' && sudo \(Paths.valet) \(action) && exit;" + + waitAndExecute { + Shell.run(command, requiresPath: true) + } completion: { [self] in + selectedSite.determineSecured(Valet.shared.config.tld) + if selectedSite.secured == originalSecureStatus { + Alert.notify( + message: "site_list.alerts_status_not_changed.title".localized, + info: "site_list.alerts_status_not_changed.desc".localized(command) + ) + } else { + let newState = selectedSite.secured ? "secured" : "unsecured" + LocalNotification.send( + title: "site_list.alerts_status_changed.title".localized, + subtitle: "site_list.alerts_status_changed.desc" + .localized( + "\(selectedSite.name!).\(Valet.shared.config.tld)", + newState + ) + ) + } + + tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0]) + tableView.deselectRow(rowToReload) + tableView.selectRowIndexes([rowToReload], byExtendingSelection: true) + } + } + + @objc func openInBrowser() { + let prefix = selectedSite!.secured ? "https://" : "http://" + let url = URL(string: "\(prefix)\(selectedSite!.name!).\(Valet.shared.config.tld)") + if url != nil { + NSWorkspace.shared.open(url!) + } else { + _ = Alert.present( + messageText: "site_list.alert.invalid_folder_name".localized, + informativeText: "site_list.alert.invalid_folder_name_desc".localized + ) + } + } + + @objc func openInFinder() { + Shell.run("open '\(selectedSite!.absolutePath!)'") + } + + @objc func openInTerminal() { + Shell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath!)'") + } + + @objc func openWithEditor(sender: EditorMenuItem) { + guard let editor = sender.editor else { return } + editor.openDirectory(file: selectedSite!.absolutePath!) + } + + @objc func unlinkSite() { + guard let site = selectedSite else { + return + } + + if site.aliasPath == nil { + return + } + + Alert.confirm( + onWindow: view.window!, + messageText: "site_list.confirm_unlink".localized(site.name), + informativeText: "site_link.confirm_link".localized, + buttonTitle: "site_list.unlink".localized, + secondButtonTitle: "Cancel", + style: .critical, + onFirstButtonPressed: { + Shell.run("valet unlink '\(site.name!)'", requiresPath: true) + self.reloadSites() + } + ) + } + +} diff --git a/phpmon/Domain/SiteList/SiteListVC.swift b/phpmon/Domain/SiteList/SiteListVC.swift index b56d4c6..2caeaf5 100644 --- a/phpmon/Domain/SiteList/SiteListVC.swift +++ b/phpmon/Domain/SiteList/SiteListVC.swift @@ -112,12 +112,11 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource { - Parameter execute: Callback of the work that needs to happen. - Parameter completion: Callback that is fired when the work is done. */ - private func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {}) + internal func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {}) { setUIBusy() DispatchQueue.global(qos: .userInitiated).async { [unowned self] in execute() - DispatchQueue.main.async { [self] in completion() setUINotBusy() @@ -164,81 +163,6 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource { self.openInBrowser() } - // MARK: Secure & Unsecure - - @objc public func toggleSecure() { - let rowToReload = tableView.selectedRow - let originalSecureStatus = selectedSite!.secured - let action = selectedSite!.secured ? "unsecure" : "secure" - let selectedSite = selectedSite! - let command = "cd \(selectedSite.absolutePath!) && sudo \(Paths.valet) \(action) && exit;" - - waitAndExecute { - Shell.run(command, requiresPath: true) - } completion: { [self] in - selectedSite.determineSecured(Valet.shared.config.tld) - if selectedSite.secured == originalSecureStatus { - Alert.notify( - message: "site_list.alerts_status_not_changed.title".localized, - info: "site_list.alerts_status_not_changed.desc".localized(command) - ) - } else { - let newState = selectedSite.secured ? "secured" : "unsecured" - LocalNotification.send( - title: "site_list.alerts_status_changed.title".localized, - subtitle: "site_list.alerts_status_changed.desc" - .localized( - "\(selectedSite.name!).\(Valet.shared.config.tld)", - newState - ) - ) - } - - tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0]) - tableView.deselectRow(rowToReload) - tableView.selectRowIndexes([rowToReload], byExtendingSelection: true) - } - } - - // MARK: Open in Browser & Finder - - @objc public func openInBrowser() { - let prefix = selectedSite!.secured ? "https://" : "http://" - let url = "\(prefix)\(selectedSite!.name!).\(Valet.shared.config.tld)" - NSWorkspace.shared.open(URL(string: url)!) - } - - @objc public func openInFinder() { - Shell.run("open \(selectedSite!.absolutePath!)") - } - - @objc public func openInTerminal() { - Shell.run("open -b com.apple.terminal \(selectedSite!.absolutePath!)") - } - - @objc public func unlinkSite() { - guard let site = selectedSite else { - return - } - - if site.aliasPath == nil { - return - } - - Alert.confirm( - onWindow: view.window!, - messageText: "site_list.confirm_unlink".localized(site.name), - informativeText: "site_link.confirm_link".localized, - buttonTitle: "site_list.unlink".localized, - secondButtonTitle: "Cancel", - style: .critical, - onFirstButtonPressed: { - Shell.run("valet unlink \(site.name!)", requiresPath: true) - self.reloadSites() - } - ) - } - // MARK: - (Search) Text Field Delegate func searchedFor(text: String) { @@ -258,13 +182,7 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource { tableView.reloadData() } - - // MARK: - Context Menu - - @objc func openWithEditor(sender: EditorMenuItem) { - guard let editor = sender.editor else { return } - editor.openDirectory(file: selectedSite!.absolutePath!) - } + // MARK: - Deinitialization deinit { diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 7e95f30..4371640 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -80,6 +80,11 @@ "site_list.detected_apps" = "Detected Applications"; "site_list.system_apps" = "System Applications"; +"site_list.warning.spaces" = "Warning! This site has a space in its folder.\nThe site will not be reachable via the browser."; + +"site_list.alert.invalid_folder_name" = "Invalid folder name"; +"site_list.alert.invalid_folder_name_desc" = "This folder could not be resolved to a valid URL. This is usually because there’s a space in the folder name. Please rename the folder, reload the list of sites, and try again."; + // EDITORS "editors.alert.try_again" = "Try Again";