From bbbdce6b4408a95bc8f07262f9f88c76d2f3a4d9 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Sun, 14 Aug 2022 23:57:46 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Reworked=20helper=20script?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'Welcome Tour' to First Aid menu - Updated 'Welcome Tour' - Helpers are now always written to ~/.config/phpmon/bin - Updated helpers (now symlinked) - Updated checks for when to symlink helpers --- PHP Monitor.xcodeproj/project.pbxproj | 4 +- phpmon/Common/Core/Paths.swift | 7 +++ phpmon/Common/Helpers/Filesystem.swift | 9 ++++ phpmon/Common/PHP/PHP Version/PhpHelper.swift | 44 ++++++++++++++++++- phpmon/Domain/Menu/MainMenu+Startup.swift | 7 ++- phpmon/Domain/Menu/MainMenu.swift | 10 +++++ phpmon/Domain/Menu/StatusMenu+Items.swift | 29 ++++++------ .../SwiftUI/Onboarding/OnboardingView.swift | 27 ++++++++---- phpmon/Domain/Warnings/WarningManager.swift | 11 +++-- phpmon/Localizable.strings | 19 +++++--- 10 files changed, 124 insertions(+), 43 deletions(-) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 9a94c35..bd55d39 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -1677,7 +1677,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 920; + CURRENT_PROJECT_VERSION = 950; DEBUG = YES; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; @@ -1704,7 +1704,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 920; + CURRENT_PROJECT_VERSION = 950; DEBUG = NO; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; diff --git a/phpmon/Common/Core/Paths.swift b/phpmon/Common/Core/Paths.swift index 465951b..6a0a4ba 100644 --- a/phpmon/Common/Core/Paths.swift +++ b/phpmon/Common/Core/Paths.swift @@ -19,9 +19,12 @@ public class Paths { private var userName: String + private var PATH: String + init() { baseDir = App.architecture != "x86_64" ? .opt : .usr userName = String(Shell.pipe("whoami").split(separator: "\n")[0]) + PATH = String(Shell.pipe("echo $PATH")).trimmingCharacters(in: .whitespacesAndNewlines) } public func detectBinaryPaths() { @@ -57,6 +60,10 @@ public class Paths { return shared.userName } + public static var PATH: String { + return shared.PATH + } + public static var cellarPath: String { return "\(shared.baseDir.rawValue)/Cellar" } diff --git a/phpmon/Common/Helpers/Filesystem.swift b/phpmon/Common/Helpers/Filesystem.swift index a54f665..652ae44 100644 --- a/phpmon/Common/Helpers/Filesystem.swift +++ b/phpmon/Common/Helpers/Filesystem.swift @@ -33,6 +33,15 @@ class Filesystem { return exists && !isDirectory.boolValue } + public static func fileIsSymlink(_ path: String) -> Bool { + do { + let attribs = try FileManager.default.attributesOfItem(atPath: path) + return attribs[.type] as! FileAttributeType == FileAttributeType.typeSymbolicLink + } catch { + return false + } + } + /** Checks if a directory exists at the provided path. */ diff --git a/phpmon/Common/PHP/PHP Version/PhpHelper.swift b/phpmon/Common/PHP/PHP Version/PhpHelper.swift index 33935fa..cc5c9bb 100644 --- a/phpmon/Common/PHP/PHP Version/PhpHelper.swift +++ b/phpmon/Common/PHP/PHP Version/PhpHelper.swift @@ -16,8 +16,18 @@ class PhpHelper { // Take the PHP version (e.g. "7.2") and generate a dotless version let dotless = version.replacingOccurrences(of: ".", with: "") + // Determine the dotless name for this PHP version + let destination = "/Users/\(Paths.whoami)/.config/phpmon/bin/pm\(dotless)" + + // Check if the ~/.config/phpmon/bin directory is in the PATH + let inPath = Paths.PATH.contains("/Users/\(Paths.whoami)/.config/phpmon/bin") + + // Check if we can create symlinks (`/usr/local/bin` must be writable) + let canWriteSymlinks = FileManager.default.isWritableFile(atPath: "/usr/local/bin/") + do { - let destination = "/usr/local/bin/pm\(dotless)" + Shell.run("mkdir -p ~/.config/phpmon/bin") + if FileManager.default.fileExists(atPath: destination) { let contents = try String(contentsOfFile: destination) if !contents.contains(keyPhrase) { @@ -52,10 +62,40 @@ class PhpHelper { // Make sure the file is executable Shell.run("chmod +x \(destination)") + + // Create a symlink if the folder is not in the PATH + if !inPath { + // First, check if we can create symlinks at all + if !canWriteSymlinks { + Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.") + return + } + + // Write the symlink + self.createSymlink(dotless) + } } catch { print(error) - Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)") + Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))") } } + private static func createSymlink(_ dotless: String) { + let source = "/Users/\(Paths.whoami)/.config/phpmon/bin/pm\(dotless)" + let destination = "/usr/local/bin/pm\(dotless)" + + if !Filesystem.fileExists(destination) { + Log.info("Creating new symlink: \(destination)") + Shell.run("ln -s \(source) \(destination)") + return + } + + if !Filesystem.fileIsSymlink(destination) { + Log.info("Overwriting existing file with new symlink: \(destination)") + Shell.run("ln -fs \(source) \(destination)") + return + } + + Log.info("Symlink in \(destination) already exists, OK.") + } } diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift index 868c00f..6cc8e4b 100644 --- a/phpmon/Domain/Menu/MainMenu+Startup.swift +++ b/phpmon/Domain/Menu/MainMenu+Startup.swift @@ -57,6 +57,9 @@ extension MainMenu { let installation = PhpEnv.phpInstall installation.notifyAboutBrokenPhpFpm() + // Check for other problems + WarningManager.shared.evaluateWarnings() + // Set up the config watchers on launch (updated automatically when switching) Log.info("Setting up watchers...") App.shared.handlePhpConfigWatcher() @@ -85,15 +88,11 @@ extension MainMenu { // Start the background refresh timer startSharedTimer() - // Check warnings - WarningManager.shared.evaluateWarnings() - // Update the stats Stats.incrementSuccessfulLaunchCount() Stats.evaluateSponsorMessageShouldBeDisplayed() // Present first launch screen if needed - #warning("You should definitely tweak this view again") if Stats.successfulLaunchCount == 0 && !isRunningSwiftUIPreview { Log.info("Should present the first launch screen!") DispatchQueue.main.async { diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index e70cdb1..8e4cfff 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -126,6 +126,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate ServicesManager.shared.loadData() } + /** + Shows the Welcome Tour screen, again. + Did this need a comment? No, probably not. + */ + @objc func showWelcomeTour() { + DispatchQueue.main.async { + OnboardingWindowController.show() + } + } + /** Reloads the menu in the background, using `asyncExecution`. */ @objc func reloadPhpMonitorMenuInBackground() { asyncExecution({ diff --git a/phpmon/Domain/Menu/StatusMenu+Items.swift b/phpmon/Domain/Menu/StatusMenu+Items.swift index 58c95be..98aaf86 100644 --- a/phpmon/Domain/Menu/StatusMenu+Items.swift +++ b/phpmon/Domain/Menu/StatusMenu+Items.swift @@ -199,6 +199,10 @@ extension StatusMenu { let servicesMenu = NSMenu() servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_first_aid".localized)) + + servicesMenu.addItem(NSMenuItem(title: "mi_view_onboarding".localized, + action: #selector(MainMenu.showWelcomeTour), keyEquivalent: "")) + let fixMyValetMenuItem = NSMenuItem( title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion), action: #selector(MainMenu.fixMyValet), keyEquivalent: "" @@ -216,22 +220,15 @@ extension StatusMenu { servicesMenu.addItem(NSMenuItem.separator()) servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized)) - servicesMenu.addItem( - NSMenuItem(title: "mi_restart_dnsmasq".localized, - action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d") - ) - servicesMenu.addItem( - NSMenuItem(title: "mi_restart_php_fpm".localized, - action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p") - ) - servicesMenu.addItem( - NSMenuItem(title: "mi_restart_nginx".localized, - action: #selector(MainMenu.restartNginx), keyEquivalent: "n") - ) - servicesMenu.addItem( - NSMenuItem(title: "mi_restart_valet_services".localized, - action: #selector(MainMenu.restartValetServices), keyEquivalent: "s") - ) + 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_valet_services".localized, + action: #selector(MainMenu.restartValetServices), keyEquivalent: "s")) servicesMenu.addItem( NSMenuItem(title: "mi_stop_valet_services".localized, action: #selector(MainMenu.stopValetServices), keyEquivalent: "s"), diff --git a/phpmon/Domain/SwiftUI/Onboarding/OnboardingView.swift b/phpmon/Domain/SwiftUI/Onboarding/OnboardingView.swift index cf81c48..34a2c0f 100644 --- a/phpmon/Domain/SwiftUI/Onboarding/OnboardingView.swift +++ b/phpmon/Domain/SwiftUI/Onboarding/OnboardingView.swift @@ -25,18 +25,17 @@ struct OnboardingTextItem: View { Text(title.localizedForSwiftUI) .font(.system(size: 14)) .lineLimit(3) - HStack { - Text(description.localizedForSwiftUI) - .foregroundColor(Color.secondary) - .font(.system(size: 13)) - .lineLimit(3) - .fixedSize(horizontal: false, vertical: true) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - } + Text(description.localizedForSwiftUI) + .foregroundColor(Color.secondary) + .font(.system(size: 13)) + .lineLimit(6) + .fixedSize(horizontal: false, vertical: true) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) } } .padding() - .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.gray.opacity(0.3), lineWidth: 1)) + .overlay(RoundedRectangle(cornerRadius: 5) + .stroke(Color.gray.opacity(0.3), lineWidth: 1)) } } @@ -57,9 +56,13 @@ struct OnboardingView: View { .padding(.bottom, 5) Text("onboarding.explore".localized) .padding(.bottom) + .padding(.trailing) } .padding(.top, 10) } + .padding(.leading) + .padding(.trailing) + VStack { VStack(alignment: .leading, spacing: 10) { OnboardingTextItem( @@ -67,6 +70,11 @@ struct OnboardingView: View { title: "onboarding.tour.menu_bar.title", description: "onboarding.tour.menu_bar" ) + OnboardingTextItem( + icon: "checkmark.circle.fill", + title: "onboarding.tour.services.title", + description: "onboarding.tour.services" + ) OnboardingTextItem( icon: "list.bullet.circle.fill", title: "onboarding.tour.domains.title", @@ -79,6 +87,7 @@ struct OnboardingView: View { ) } }.padding() + VStack(spacing: 20) { HStack { Image(systemName: "questionmark.circle.fill") diff --git a/phpmon/Domain/Warnings/WarningManager.swift b/phpmon/Domain/Warnings/WarningManager.swift index 1365fcc..9f65c4a 100644 --- a/phpmon/Domain/Warnings/WarningManager.swift +++ b/phpmon/Domain/Warnings/WarningManager.swift @@ -32,11 +32,16 @@ class WarningManager { ), Warning( command: { + !Paths.PATH.contains("/Users/\(Paths.whoami)/.config/phpmon/bin") && !FileManager.default.isWritableFile(atPath: "/usr/local/bin/") }, - name: "`/usr/local/bin` not writable", + name: "Helpers cannot be symlinked and not in PATH", title: "warnings.helper_permissions.title", - paragraphs: ["warnings.helper_permissions.description", "warnings.helper_permissions.unavailable"], + paragraphs: [ + "warnings.helper_permissions.description", + "warnings.helper_permissions.unavailable", + "warnings.helper_permissions.symlink" + ], url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries" ) ] @@ -59,7 +64,7 @@ class WarningManager { for check in self.evaluations { if await check.applies() { - Log.info("[WARNING] \(check.name)") + Log.info("[DOCTOR] \(check.name) (!)") self.warnings.append(check) continue } diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 39c0a1d..c090432 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -74,6 +74,8 @@ "mi_no_presets" = "No presets available."; "mi_set_up_presets" = "Learn more about presets..."; +"mi_view_onboarding" = "Show Welcome Tour..."; + "mi_xdebug_available_modes" = "Available Modes"; "mi_xdebug_actions" = "Actions"; "mi_xdebug_disable_all" = "Disable All Modes"; @@ -518,7 +520,8 @@ If you are seeing this message but are confused why this folder has gone missing "warnings.helper_permissions.title" = "PHP Monitor’s helpers are currently unavailable."; "warnings.helper_permissions.description" = "PHP Monitor comes with various helper binaries. Using these binaries allows you to easily invoke a specific version of PHP without switching the linked PHP version."; -"warnings.helper_permissions.unavailable" = "However, these helpers are currently *unavailable* because PHP Monitor could not create the required symlinks (alternatively, you could add PHP Monitor's helper directory to your `PATH` variable to make this warning go away as well)."; +"warnings.helper_permissions.unavailable" = "However, these helpers are potentially *unavailable* because PHP Monitor cannot currently create or update the required symlinks."; +"warnings.helper_permissions.symlink" = "If you do not wish to make `/usr/local/bin` writable, you can add PHP Monitor's helper directory to your `PATH` variable to make this warning go away. (Click on ”Learn More” to find out how to fix this issue.)"; "warnings.arm_compatibility.title" = "You are running PHP Monitor using Rosetta on Apple Silicon, which means your PHP environment is also running via Rosetta."; "warnings.arm_compatibility.description" = "You appear to be running an ARM-compatible version of macOS, but you are currently running PHP Monitor using Rosetta. While this will work correctly, it is recommended that you use the native version of Homebrew."; @@ -527,13 +530,15 @@ If you are seeing this message but are confused why this folder has gone missing "onboarding.title" = "Welcome Tour"; "onboarding.welcome" = "Welcome to PHP Monitor!"; -"onboarding.explore" = "Learn more about some of the features that PHP Monitor has to offer."; +"onboarding.explore" = "Learn more about some of the features that PHP Monitor has to offer. You can find a more comprehensive list of features on GitHub."; "onboarding.tour.menu_bar.title" = "Get Started"; "onboarding.tour.menu_bar" = "PHP Monitor lives in your menu bar. From here, you can switch the globally linked PHP version, start or stop services, locate config files, and more."; "onboarding.tour.faq_hint" = "I recommend that you check out the [README](https://github.com/nicoverbruggen/phpmon/blob/main/README.md) on GitHub: it contains a comprehensive FAQ with various tips and common questions and answers."; -"onboarding.tour.domains.title" = "Domains"; -"onboarding.tour.domains" = "By opening the Domains window via the Menu Bar item, you can view which domains are linked and parked."; -"onboarding.tour.isolation.title" = "Isolation"; -"onboarding.tour.isolation" = "If you have Valet 3 installed, you can even use domain isolation by right-clicking on a given domain in the Domains window. This allows you to pick a specific version of PHP to use for that domain!"; -"onboarding.tour.once" = "You will only see the Welcome Tour once. You can re-open the Welcome Tour later via the menu bar icon."; +"onboarding.tour.services.title" = "Manage Services"; +"onboarding.tour.services" = "Once you click on the menu bar item, you can see at a glance based on the checkmarks or crosses if all of the Homebrew services are up and running. You can also click on a service to quickly toggle it. You can also add your own!"; +"onboarding.tour.domains.title" = "Manage Domains"; +"onboarding.tour.domains" = "By opening the Domains window via the menu bar item, you can view which domains are linked and parked, as well as active nginx proxies."; +"onboarding.tour.isolation.title" = "Isolate Domains"; +"onboarding.tour.isolation" = "If you have Valet 3 installed, you can even use domain isolation by right-clicking on a given domain in the Domains window. This allows you to pick a specific version of PHP to use for that domain, and that domain only!"; +"onboarding.tour.once" = "You will only see the Welcome Tour once. You can re-open the Welcome Tour later via the menu bar icon (under First Aid & Services)."; "onboarding.tour.close" = "Close Tour";