diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index cf5887b..bda503f 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -405,7 +405,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 25; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = phpmon/Info.plist; @@ -413,7 +413,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.2; + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -429,7 +429,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 25; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = phpmon/Info.plist; @@ -437,7 +437,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.2; + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme index c21a19b..e93bdab 100644 --- a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme @@ -39,6 +39,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/phpmon/AppDelegate.swift b/phpmon/AppDelegate.swift index f43e0b4..3bdecee 100644 --- a/phpmon/AppDelegate.swift +++ b/phpmon/AppDelegate.swift @@ -10,16 +10,34 @@ import Cocoa import UserNotifications @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate { +class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate { // MARK: - Variables + /** + The Shell singleton that keeps track of the history of all + (invoked by PHP Monitor) shell commands. It is used to + invoke all commands in this application. + */ let sharedShell : Shell + + /** + The App singleton contains information about the state of + the application and global variables. + */ let state : App + + /** + The MainMenu singleton is responsible for rendering the + menu bar item and its menu, as well as its actions. + */ let menu : MainMenu // MARK: - Initializer + /** + When the application initializes, create all singletons. + */ override init() { self.sharedShell = Shell.user self.state = App.shared @@ -29,20 +47,28 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - Lifecycle + /** + When the application has finished launching, we'll want to set up + the user notification center delegate, and kickoff the menu + startup procedure. + */ func applicationDidFinishLaunching(_ aNotification: Notification) { NSUserNotificationCenter.default.delegate = self self.menu.startup() } - func applicationWillTerminate(_ aNotification: Notification) { - self.state.windowController = nil - } -} - -extension AppDelegate: NSUserNotificationCenterDelegate { - func userNotificationCenter(_ center: NSUserNotificationCenter, - shouldPresent notification: NSUserNotification) -> Bool { + // MARK: - NSUserNotificationCenterDelegate + + /** + When a notification is sent, the delegate of the notification center + is asked whether the notification should be presented or not. Since + the user can now disable notifications per application since macOS + Catalina, any and all notifications should be displayed. + */ + func userNotificationCenter( + _ center: NSUserNotificationCenter, + shouldPresent notification: NSUserNotification + ) -> Bool { return true } } - diff --git a/phpmon/Classes/Menu/StatusMenu.swift b/phpmon/Classes/Menu/StatusMenu.swift index d49e66f..c160543 100644 --- a/phpmon/Classes/Menu/StatusMenu.swift +++ b/phpmon/Classes/Menu/StatusMenu.swift @@ -12,17 +12,18 @@ class StatusMenu : NSMenu { public func addPhpVersionMenuItems() { - var string = "We are not sure what version of PHP you are running." + var string = "mi_unsure".localized if (App.shared.currentVersion != nil) { if (!App.shared.currentVersion!.error) { - string = "You are running PHP \(App.shared.currentVersion!.long)" + // in case the php version loaded without issue + string = "\("mi_php_version".localized) \(App.shared.currentVersion!.long)" self.addItem(NSMenuItem(title: string, action: nil, keyEquivalent: "")) } else { // in case of an error show the error message - self.addItem(NSMenuItem(title: "Oof! It appears your PHP installation is broken...", action: nil, keyEquivalent: "")) - self.addItem(NSMenuItem(title: "Try running `php -v` in your terminal.", action: nil, keyEquivalent: "")) - self.addItem(NSMenuItem(title: "You could also try switching to another version.", action: nil, keyEquivalent: "")) - self.addItem(NSMenuItem(title: "Running `brew reinstall php` (or for the equivalent version) might help.", action: nil, keyEquivalent: "")) + ["mi_php_broken_1", "mi_php_broken_2", + "mi_php_broken_3", "mi_php_broken_4"].forEach { (message) in + self.addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: "")) + } } } } @@ -34,30 +35,30 @@ class StatusMenu : NSMenu { for index in (0.. String { let task = Process() task.launchPath = path diff --git a/phpmon/Singletons/MainMenu.swift b/phpmon/Singletons/MainMenu.swift index eba7700..3d2c456 100644 --- a/phpmon/Singletons/MainMenu.swift +++ b/phpmon/Singletons/MainMenu.swift @@ -12,10 +12,18 @@ class MainMenu: NSObject, NSWindowDelegate { static let shared = MainMenu() - let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + /** + The status bar item with variable length. + */ + let statusItem = NSStatusBar.system.statusItem( + withLength: NSStatusItem.variableLength + ) // MARK: - UI related + /** + Kick off the startup of the rendering of the main menu. + */ public func startup() { // Start with the icon self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) @@ -29,6 +37,9 @@ class MainMenu: NSObject, NSWindowDelegate { } } + /** + When the environment is all clear and the app can run, let's go. + */ private func onEnvironmentPass() { App.shared.availablePhpVersions = Actions.detectPhpVersions() self.updatePhpVersionInStatusBar() @@ -44,13 +55,16 @@ class MainMenu: NSObject, NSWindowDelegate { } } + /** + When the environment is not OK, present an alert to inform the user. + */ private func onEnvironmentFail() { DispatchQueue.main.async { let close = Alert.present( - messageText: "PHP Monitor cannot start", - informativeText: "The issue you were just notified about is keeping PHP Monitor from functioning correctly. Please fix the issue and restart PHP Monitor. After clicking on OK, PHP Monitor will close.\n\nIf you have fixed the issue (or don't remember what the exact issue is) you can click on Retry, which will have PHP Monitor retry the startup checks.", - buttonTitle: "Close", - secondButtonTitle: "Retry" + messageText: "alert.cannot_start.title".localized, + informativeText: "alert.cannot_start.info".localized, + buttonTitle: "alert.cannot_start.close".localized, + secondButtonTitle: "alert.cannot_start.retry".localized ) if (!close) { self.startup() @@ -60,6 +74,9 @@ class MainMenu: NSObject, NSWindowDelegate { } } + /** + Update the menu's contents, based on what's going on. + */ public func update() { DispatchQueue.global(qos: .userInitiated).async { [unowned self] in // Create a new menu @@ -78,8 +95,8 @@ class MainMenu: NSObject, NSWindowDelegate { menu.addItem(NSMenuItem.separator()) // Add about & quit menu items - menu.addItem(NSMenuItem(title: "About PHP Monitor", action: #selector(self.openAbout), keyEquivalent: "")) - menu.addItem(NSMenuItem(title: "Quit PHP Monitor", action: #selector(self.terminateApp), keyEquivalent: "q")) + menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(self.openAbout), keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(self.terminateApp), keyEquivalent: "q")) // Make sure every item can be interacted with menu.items.forEach({ (item) in @@ -93,10 +110,19 @@ class MainMenu: NSObject, NSWindowDelegate { } } + /** + Sets the status bar image based on a version string. + */ func setStatusBarImage(version: String) { - self.setStatusBar(image: MenuBarImageGenerator.textToImage(text: version)) + self.setStatusBar( + image: MenuBarImageGenerator.textToImage(text: version) + ) } + /** + Sets the status bar image, based on the provided NSImage. + The image will be used as a template image. + */ func setStatusBar(image: NSImage) { if let button = statusItem.button { image.isTemplate = true @@ -106,6 +132,14 @@ class MainMenu: NSObject, NSWindowDelegate { // MARK: - Nicer callbacks + /** + Executes a specific callback and fires the completion callback, + while updating the UI as required. As long as the completion callback + does not fire, the app is presumed to be busy and the UI reflects this. + + - Parameter execute: Escaping 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 = {}) { App.shared.busy = true @@ -122,7 +156,7 @@ class MainMenu: NSObject, NSWindowDelegate { } } - // MARK: - Actions + // MARK: - User Interface @objc func updatePhpVersionInStatusBar() { App.shared.currentVersion = PhpVersion() @@ -144,6 +178,8 @@ class MainMenu: NSObject, NSWindowDelegate { } } + // MARK: - Actions + @objc public func restartPhpFpm() { self.waitAndExecute({ Actions.restartPhpFpm() @@ -163,10 +199,12 @@ class MainMenu: NSObject, NSWindowDelegate { } @objc public func forceRestartLatestPhp() { + // Tell the user the switch is about to occur _ = Alert.present( messageText: "alert.force_reload.title".localized, informativeText: "alert.force_reload.info".localized ) + // Start switching self.waitAndExecute({ Actions.fixMyPhp() }, { _ = Alert.present( messageText: "alert.force_reload_done.title".localized, @@ -192,6 +230,7 @@ class MainMenu: NSObject, NSWindowDelegate { @objc public func switchToPhpVersion(sender: AnyObject) { self.setBusyImage() + // TODO: A wise man once said: using tags is not good. Fix this. let index = sender.tag! let version = App.shared.availablePhpVersions[index] App.shared.busy = true @@ -211,6 +250,7 @@ class MainMenu: NSObject, NSWindowDelegate { DispatchQueue.main.async { self.updatePhpVersionInStatusBar() self.update() + // Send a notification that the switch has been completed LocalNotification.send( title: "PHP \(version) is now active", subtitle: "PHP Monitor has finished switching to PHP \(version)." @@ -227,11 +267,4 @@ class MainMenu: NSObject, NSWindowDelegate { @objc public func terminateApp() { NSApplication.shared.terminate(nil) } - - // MARK: - Cleanup when window closes - - func windowWillClose(_ notification: Notification) { - App.shared.windowController = nil - Shell.user.delegate = nil - } } diff --git a/phpmon/Singletons/Shell.swift b/phpmon/Singletons/Shell.swift index 61729cd..9ed509c 100644 --- a/phpmon/Singletons/Shell.swift +++ b/phpmon/Singletons/Shell.swift @@ -8,38 +8,30 @@ import Cocoa -protocol ShellDelegate: class { - func didCompleteCommand(historyItem: ShellHistoryItem) -} - -class ShellHistoryItem { - var command: String - var output: String - var date: Date - - init(command: String, output: String) { - self.command = command - self.output = output - self.date = Date() - } -} - class Shell { - // Singleton to access a user shell (with --login) + /** + Singleton to access a user shell (with --login) + */ static let user = Shell() - var history : [ShellHistoryItem] = [] - - var delegate : ShellDelegate? - - /// Runs a shell command without using the description. + /** + Runs a shell command without using the output. + Uses the default shell. + + - Parameter command: The command to run + */ public func run(_ command: String) { // Equivalent of piping to /dev/null; don't do anything with the string _ = self.pipe(command) } - /// Runs a shell command and returns the output. + /** + Runs a shell command and returns the output. + + - Parameter command: The command to run + - Parameter shell: Path to the shell to invoke + */ public func pipe(_ command: String, shell: String = "/bin/sh") -> String { let task = Process() task.launchPath = shell @@ -51,17 +43,10 @@ class Shell { let data = pipe.fileHandleForReading.readDataToEndOfFile() - let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String - - let historyItem = ShellHistoryItem(command: command, output: output) - - DispatchQueue.global(qos: .userInitiated).async { [unowned self] in - self.history.append(historyItem) - // Keep the last 100 items - self.history = self.history.suffix(100) - } - - delegate?.didCompleteCommand(historyItem: historyItem) + let output: String = NSString( + data: data, + encoding: String.Encoding.utf8.rawValue + )! as String return output }