diff --git a/phpmon.xcodeproj/project.pbxproj b/phpmon.xcodeproj/project.pbxproj index 431756a..98c8ab0 100644 --- a/phpmon.xcodeproj/project.pbxproj +++ b/phpmon.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; }; C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* BootChecks.swift */; }; C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; }; + C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -35,6 +36,7 @@ C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; C4D8016522B1584700C6DA1B /* BootChecks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootChecks.swift; sourceTree = ""; }; C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -73,6 +75,7 @@ C41E181522CB614C0072CF09 /* Terminal */, C41C1B4E22B024F100E7CF16 /* Helpers */, C41E181822CB62200072CF09 /* View Controllers */, + C4F8C0A222D4F100002EFE61 /* Extensions */, C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */, C41C1B3F22B0098000E7CF16 /* Info.plist */, C41C1B4022B0098000E7CF16 /* phpmon.entitlements */, @@ -124,6 +127,14 @@ path = "View Controllers"; sourceTree = ""; }; + C4F8C0A222D4F100002EFE61 /* Extensions */ = { + isa = PBXGroup; + children = ( + C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -195,6 +206,7 @@ buildActionMask = 2147483647; files = ( C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */, + C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */, C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */, C41C1B4D22B0215A00E7CF16 /* Services.swift in Sources */, C41C1B4922B00A9800E7CF16 /* ImageGenerator.swift in Sources */, diff --git a/phpmon/AppDelegate.swift b/phpmon/AppDelegate.swift index 8620e10..ed904ad 100644 --- a/phpmon/AppDelegate.swift +++ b/phpmon/AppDelegate.swift @@ -13,6 +13,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - Variables + let sharedShell : Shell let statusItem = NSStatusBar.system.statusItem(withLength: 32) var timer: Timer? var version: PhpVersion? = nil @@ -21,6 +22,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { var log: String = "" var windowController: NSWindowController? = nil + override init() { + self.sharedShell = Shell.shared + super.init() + } + // MARK: - Lifecycle func applicationDidFinishLaunching(_ aNotification: Notification) { @@ -30,17 +36,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { DispatchQueue.global(qos: .userInitiated).async { [unowned self] in BootChecks.perform() self.availablePhpVersions = Services.detectPhpVersions() - print("The following PHP versions were detected:") - print(self.availablePhpVersions) self.updatePhpVersionInStatusBar() - // Schedule a request to fetch the PHP version every 15 seconds - Timer.scheduledTimer( - timeInterval: 15, - target: self, - selector: #selector(self.updatePhpVersionInStatusBar), - userInfo: nil, - repeats: true - ) + // Schedule a request to fetch the PHP version every 60 seconds + DispatchQueue.main.async { + self.timer = Timer.scheduledTimer( + timeInterval: 60, + target: self, + selector: #selector(self.updatePhpVersionInStatusBar), + userInfo: nil, + repeats: true + ) + } } } @@ -94,7 +100,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { menu.addItem(NSMenuItem.separator()) } // TODO: Enable when implementation is complete - // menu.addItem(NSMenuItem(title: "View terminal output", action: #selector(self.openOutput), keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "View terminal output", action: #selector(self.openOutput), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "About phpmon", action: #selector(self.openAbout), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "Quit phpmon", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) DispatchQueue.main.async { @@ -108,16 +114,22 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func openOutput() { if (self.windowController == nil) { let vc = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "logWindow") as! LogViewController + Shell.shared.delegate = vc let window = NSWindow(contentViewController: vc) window.title = "Terminal Output" self.windowController = NSWindowController(window: window) } self.windowController!.showWindow(self) + NSApp.activate(ignoringOtherApps: true) + // TODO: Send window to front (if possible) } @objc func updatePhpVersionInStatusBar() { self.version = PhpVersion() + if (Shell.shared.history.count > 0) { + _ = Shell.shared.history.popLast() + } if (self.busy) { DispatchQueue.main.async { self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) diff --git a/phpmon/Classes/PhpVersion.swift b/phpmon/Classes/PhpVersion.swift index f2886a7..493c6dc 100644 --- a/phpmon/Classes/PhpVersion.swift +++ b/phpmon/Classes/PhpVersion.swift @@ -14,11 +14,9 @@ class PhpVersion { var long : String = "???" init() { - let version = Shell + let version = Shell.shared // Get the version directly from PHP - .execute(command: "php -r 'print phpversion();'") - // also remove any colors - .replacingOccurrences(of: "\u{1b}(B\u{1b}[m", with: "") + .pipe("php -r 'print phpversion();'") // That's the long version self.long = version diff --git a/phpmon/Extensions/DateExtension.swift b/phpmon/Extensions/DateExtension.swift new file mode 100644 index 0000000..8622f0e --- /dev/null +++ b/phpmon/Extensions/DateExtension.swift @@ -0,0 +1,19 @@ +// +// Date.swift +// phpmon +// +// Created by Nico Verbruggen on 09/07/2019. +// Copyright © 2019 Nico Verbruggen. All rights reserved. +// + +import Cocoa + +extension Date +{ + func toString() -> String + { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + return dateFormatter.string(from: self) + } +} diff --git a/phpmon/Startup/BootChecks.swift b/phpmon/Startup/BootChecks.swift index 684204f..ec30034 100644 --- a/phpmon/Startup/BootChecks.swift +++ b/phpmon/Startup/BootChecks.swift @@ -13,31 +13,31 @@ class BootChecks { public static func perform() { self.presentAlertOnMainThreadIf( - !Shell.execute(command: "which php").contains("/usr/local/bin/php"), + !Shell.shared.pipe("which php").contains("/usr/local/bin/php"), messageText: "PHP is not correctly installed", informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue." ) self.presentAlertOnMainThreadIf( - !Shell.execute(command: "ls /usr/local/opt | grep php@7.3").contains("php@7.3"), + !Shell.shared.pipe("ls /usr/local/opt | grep php@7.3").contains("php@7.3"), messageText: "PHP 7.3 is not correctly installed", informativeText: "PHP 7.3 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue." ) self.presentAlertOnMainThreadIf( - !Shell.execute(command: "which valet").contains("/usr/local/bin/valet"), + !Shell.shared.pipe("which valet").contains("/usr/local/bin/valet"), messageText: "Laravel Valet is not correctly installed", informativeText: "You must install Valet via brew. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue." ) self.presentAlertOnMainThreadIf( - !Shell.execute(command: "cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"), + !Shell.shared.pipe("cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"), messageText: "Brew has not been added to sudoers.d", informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue." ) self.presentAlertOnMainThreadIf( - !Shell.execute(command: "cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"), + !Shell.shared.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"), messageText: "Valet has not been added to sudoers.d", informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue." ) diff --git a/phpmon/Terminal/Services.swift b/phpmon/Terminal/Services.swift index 6591947..7007d02 100644 --- a/phpmon/Terminal/Services.swift +++ b/phpmon/Terminal/Services.swift @@ -11,17 +11,17 @@ import AppKit class Services { public static func mysqlIsRunning() -> Bool { - let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.mysql") + let running = Shell.shared.pipe( "launchctl list | grep homebrew.mxcl.mysql") return (running != "") } public static func nginxIsRunning() -> Bool { - let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.nginx") + let running = Shell.shared.pipe( "launchctl list | grep homebrew.mxcl.nginx") return (running != "") } public static func detectPhpVersions() -> [String] { - let files = Shell.execute(command: "ls /usr/local/opt | grep php@") + let files = Shell.shared.pipe( "ls /usr/local/opt | grep php@") var versions = files.components(separatedBy: "\n") // Remove all empty strings versions.removeAll { (string) -> Bool in @@ -37,20 +37,20 @@ class Services { public static func switchToPhpVersion(version: String, availableVersions: [String]) { availableVersions.forEach { (version) in - _ = Shell.execute(command: "brew unlink php@\(version)") + Shell.shared.run( "brew unlink php@\(version)") } if (availableVersions.contains("7.3")) { - _ = Shell.execute(command: "brew link php@7.3") + Shell.shared.run( "brew link php@7.3") if (version == Constants.LatestPhpVersion) { - _ = Shell.execute(command: "valet use php") + Shell.shared.run( "valet use php") } else { - _ = Shell.execute(command: "valet use php@\(version)") + Shell.shared.run( "valet use php@\(version)") } } } public static func restartPhp(version: String) { - _ = Shell.execute(command: "brew services restart php@\(version)") + Shell.shared.run( "brew services restart php@\(version)") } public static func openPhpConfigFolder(version: String) { diff --git a/phpmon/Terminal/Shell.swift b/phpmon/Terminal/Shell.swift index 7349f61..ed7e282 100644 --- a/phpmon/Terminal/Shell.swift +++ b/phpmon/Terminal/Shell.swift @@ -8,9 +8,38 @@ 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 { - public static func execute(command: String) -> String { + static let shared = Shell() + + var history : [ShellHistoryItem] = [] + + var delegate : ShellDelegate? + + /// Runs a shell command without using the description. + 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. + public func pipe(_ command: String) -> String { let task = Process() task.launchPath = "/bin/bash" task.arguments = ["--login", "-c", command] @@ -24,7 +53,14 @@ class Shell { let output: String = NSString( data: data, encoding: String.Encoding.utf8.rawValue - )! as String + )!.replacingOccurrences( + of: "\u{1B}(B\u{1B}[m", + with: "" + ) as String + + let historyItem = ShellHistoryItem(command: command, output: output) + history.append(historyItem) + delegate?.didCompleteCommand(historyItem: historyItem) return output } diff --git a/phpmon/View Controllers/Base.lproj/Main.storyboard b/phpmon/View Controllers/Base.lproj/Main.storyboard index 7716961..39a390e 100644 --- a/phpmon/View Controllers/Base.lproj/Main.storyboard +++ b/phpmon/View Controllers/Base.lproj/Main.storyboard @@ -59,11 +59,11 @@ - + - + - + - + - - + + @@ -94,7 +94,7 @@ - + @@ -104,14 +104,17 @@ - + + + + - + diff --git a/phpmon/View Controllers/LogViewController.swift b/phpmon/View Controllers/LogViewController.swift index 13bab06..11f648a 100644 --- a/phpmon/View Controllers/LogViewController.swift +++ b/phpmon/View Controllers/LogViewController.swift @@ -8,10 +8,48 @@ import Cocoa -class LogViewController: NSViewController { +class LogViewController: NSViewController, ShellDelegate { + + @IBOutlet var textView: NSTextView! + + public func appendHistoryItem(_ historyItem: ShellHistoryItem) { + self.append( + """ + ====== + @ \(historyItem.date.toString()) + ------ + $ \(historyItem.command) + ------ + > \(historyItem.output) + + """ + ) + } + + public func append(_ text : String) { + self.textView.textStorage?.append( + NSAttributedString( + string: text, + attributes: [ + NSAttributedString.Key.font: NSFont(name: "Menlo", size: 12.0)! + ] + ) + ) + self.textView.scrollToEndOfDocument(nil) + } + + override func viewDidLoad() { + self.textView.isEditable = false + for entry in Shell.shared.history { + self.appendHistoryItem(entry) + } + } + + func didCompleteCommand(historyItem: ShellHistoryItem) { + self.appendHistoryItem(historyItem) + } @IBAction func pressed(_ sender: Any) { self.view.window?.windowController?.close() } - }