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

Compare commits

...

11 Commits
v1.1 ... v1.3

Author SHA1 Message Date
0fc7f2c905 📝 Minor tweaks, updated README 2019-07-15 08:14:51 +02:00
eebfa823f1 No more fixed width menu bar icon 2019-07-15 07:55:42 +02:00
edc23f97de 🐛 Fixes being unable to quit after setting target 2019-07-11 09:49:04 +02:00
7b79cb7fe8 ♻️ Move logic away from AppDelegate 2019-07-11 09:11:41 +02:00
88d81f343e ♻️ Move window logic to VC 2019-07-11 08:30:46 +02:00
8486997f01 🔥 Additional cleanup 2019-07-11 08:27:44 +02:00
af55e7fabc 🔥 Performance improvements, memory deallocation 2019-07-09 19:15:42 +02:00
2025c951e8 📝 Updated README 2019-07-09 18:39:52 +02:00
a71c6afc0d ♻️ Rework Shell as singleton, logging 2019-07-09 18:33:48 +02:00
1bfcdd546b 🐛 Fixes #3: Bad Gateway caused by valet use
Note: The terminal output work-in-progress is currently disabled in this
particular commit.
2019-07-08 10:43:56 +02:00
78702ae325 🚧 Adds window to view terminal output 2019-07-08 08:40:29 +02:00
20 changed files with 584 additions and 261 deletions

View File

@ -1,14 +1,28 @@
# phpmon
<img src="./docs/phpmon.png" width="306px" alt="phpmon screenshot"/>
phpmon is a macOS utility that runs on your Mac and displays the active PHP version in your status bar.
phpmon is a macOS utility that runs on your Mac and displays the active PHP version in your status bar. Handy if you're running multiple versions of PHP with Homebrew and wish to see which version is currently linked & active with Laravel Valet.
<img src="./docs/screenshot.png" width="278px" alt="phpmon screenshot"/>
For me, it comes in handy when running multiple versions of PHP with Homebrew and you wish to be able to see at a glance which version is currently linked & active with Laravel Valet, and switch between versions.
This version of phpmon was developed for:
* macOS Mojave (10.14)
* PHP 7.x with Homebrew 2.x (with support for PHP 5.6 and PHP 7.0 [as well](https://github.com/eXolnet/homebrew-deprecated))
* Laravel Valet 2.2
## Why I built this
I wanted to be able to see at a glance which version of PHP was linked, and handle dealing with Laravel Valet in a simple app without having to deal with the terminal every time.
Initially, I had an Alfred workflow for this. But this does the job as well, while also showing me at all times which version of PHP is linked (which is the main benefit over e.g. an Alfred workflow).
## How it works
### Version detection
This utility runs `php -r 'print phpversion();'` in the background periodically and extracts the version number.
This utility runs `php -r 'print phpversion();'` in the background periodically (every 60 seconds) and extracts the version number.
### Switching PHP versions
@ -27,4 +41,8 @@ The utility runs the following commands:
- Tell Valet to switch to a specific PHP version
- Link the desired version of PHP
If you want to know more about how this works, you can find the file Services.swift under the Helpers directory to learn more about how switching PHP versions works. In the end, this just executes some shell commands.
### Want to know more?
If you want to know more about how this works, I recommend you check out the source code.
This app isn't very complicated after all. In the end, this just (conveniently) executes some shell commands.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

BIN
docs/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

View File

@ -8,31 +8,41 @@
/* Begin PBXBuildFile section */
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C41C1B3922B0097F00E7CF16 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3822B0097F00E7CF16 /* ViewController.swift */; };
C41C1B3922B0097F00E7CF16 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3822B0097F00E7CF16 /* LogViewController.swift */; };
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
C41C1B4922B00A9800E7CF16 /* ImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* ImageGenerator.swift */; };
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
C41C1B4B22B019FF00E7CF16 /* PhpVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */; };
C41C1B4D22B0215A00E7CF16 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Services.swift */; };
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* BootChecks.swift */; };
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
C4F8C0A622D4FA41002EFE61 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = C4F8C0A522D4FA41002EFE61 /* README.md */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
C41C1B3322B0097F00E7CF16 /* phpmon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = phpmon.app; sourceTree = BUILT_PRODUCTS_DIR; };
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C41C1B3822B0097F00E7CF16 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
C41C1B3822B0097F00E7CF16 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C41C1B3D22B0098000E7CF16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
C41C1B3F22B0098000E7CF16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = "<group>"; };
C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
C41C1B4822B00A9800E7CF16 /* ImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGenerator.swift; sourceTree = "<group>"; };
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = "<group>"; };
C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpVersion.swift; sourceTree = "<group>"; };
C41C1B4C22B0215A00E7CF16 /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* BootChecks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootChecks.swift; sourceTree = "<group>"; };
C4811D2322D70A4700B5F6B3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = "<group>"; };
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
C4F8C0A522D4FA41002EFE61 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -49,6 +59,7 @@
C41C1B2A22B0097F00E7CF16 = {
isa = PBXGroup;
children = (
C4F8C0A522D4FA41002EFE61 /* README.md */,
C41C1B3522B0097F00E7CF16 /* phpmon */,
C41C1B3422B0097F00E7CF16 /* Products */,
);
@ -65,12 +76,12 @@
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup;
children = (
C41E181722CB61EB0072CF09 /* Classes */,
C41E181622CB61890072CF09 /* Startup */,
C41E181522CB614C0072CF09 /* Terminal */,
C41C1B4E22B024F100E7CF16 /* Helpers */,
C41E181822CB62200072CF09 /* View Controllers */,
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
C4EE188322D3386B00E126E5 /* Constants.swift */,
C4811D2622D70CEF00B5F6B3 /* Singletons */,
C41E181722CB61EB0072CF09 /* Classes */,
C41E181822CB62200072CF09 /* View Controllers */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
@ -78,36 +89,11 @@
path = phpmon;
sourceTree = "<group>";
};
C41C1B4E22B024F100E7CF16 /* Helpers */ = {
isa = PBXGroup;
children = (
C41C1B4822B00A9800E7CF16 /* ImageGenerator.swift */,
C476FF9722B0DD830098105B /* Alert.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
C41E181522CB614C0072CF09 /* Terminal */ = {
isa = PBXGroup;
children = (
C41C1B4622B009A400E7CF16 /* Shell.swift */,
C41C1B4C22B0215A00E7CF16 /* Services.swift */,
);
path = Terminal;
sourceTree = "<group>";
};
C41E181622CB61890072CF09 /* Startup */ = {
isa = PBXGroup;
children = (
C4D8016522B1584700C6DA1B /* BootChecks.swift */,
);
path = Startup;
sourceTree = "<group>";
};
C41E181722CB61EB0072CF09 /* Classes */ = {
isa = PBXGroup;
children = (
C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */,
C4811D2722D70D8E00B5F6B3 /* Commands */,
C4811D2822D70D9C00B5F6B3 /* Helpers */,
);
path = Classes;
sourceTree = "<group>";
@ -116,11 +102,48 @@
isa = PBXGroup;
children = (
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
C41C1B3822B0097F00E7CF16 /* ViewController.swift */,
C41C1B3822B0097F00E7CF16 /* LogViewController.swift */,
);
path = "View Controllers";
sourceTree = "<group>";
};
C4811D2622D70CEF00B5F6B3 /* Singletons */ = {
isa = PBXGroup;
children = (
C41C1B4622B009A400E7CF16 /* Shell.swift */,
C4811D2322D70A4700B5F6B3 /* App.swift */,
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */,
);
path = Singletons;
sourceTree = "<group>";
};
C4811D2722D70D8E00B5F6B3 /* Commands */ = {
isa = PBXGroup;
children = (
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
);
path = Commands;
sourceTree = "<group>";
};
C4811D2822D70D9C00B5F6B3 /* Helpers */ = {
isa = PBXGroup;
children = (
C476FF9722B0DD830098105B /* Alert.swift */,
C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */,
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
C4F8C0A222D4F100002EFE61 /* Extensions */ = {
isa = PBXGroup;
children = (
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -179,6 +202,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C4F8C0A622D4FA41002EFE61 /* README.md in Resources */,
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
);
@ -191,14 +215,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */,
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
C41C1B4D22B0215A00E7CF16 /* Services.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* ImageGenerator.swift in Sources */,
C41C1B3922B0097F00E7CF16 /* ViewController.swift in Sources */,
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */,
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
C41C1B3922B0097F00E7CF16 /* LogViewController.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* PhpVersion.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -12,141 +12,27 @@ import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Variables
let sharedShell : Shell
let state : App
let menu : MainMenu
let statusItem = NSStatusBar.system.statusItem(withLength: 32)
var timer: Timer?
var version: PhpVersion? = nil
var availablePhpVersions : [String] = []
var busy: Bool = false
// MARK: - Initializer
override init() {
self.sharedShell = Shell.user
self.state = App.shared
self.menu = MainMenu.shared
super.init()
}
// MARK: - Lifecycle
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Start with the icon
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
// Perform environment boot checks
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
)
}
self.menu.startup()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
// MARK: - UI related
func setStatusBarImage(version: String) {
self.setStatusBar(image: ImageGenerator.generateImageForStatusBar(width: 32.0, text: version))
}
func setStatusBar(image: NSImage) {
if let button = statusItem.button {
image.isTemplate = true
button.image = image
}
}
func updateMenu() {
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
let menu = NSMenu()
var string = "We are not sure what version of PHP you are running."
if (self.version != nil) {
string = "You are running PHP \(self.version!.long)"
}
menu.addItem(NSMenuItem(title: string, action: nil, keyEquivalent: ""))
if (self.version != nil) {
// Actions
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Open php.ini in Finder", action: #selector(self.openActiveConfigFolder), keyEquivalent: ""))
// menu.addItem(NSMenuItem(title: "Restart PHP \(self.version!.short) service", action: #selector(self.restartPhp), keyEquivalent: ""))
}
menu.addItem(NSMenuItem.separator())
if (self.availablePhpVersions.count > 0 && !self.busy) {
var shortcutKey = 1
for index in (0..<self.availablePhpVersions.count).reversed() {
let version = self.availablePhpVersions[index]
let action = #selector(self.switchToPhpVersion(sender:))
let menuItem = NSMenuItem(title: "Switch to PHP \(version)", action: (version == self.version?.short) ? nil : action, keyEquivalent: "\(shortcutKey)")
menuItem.tag = index
shortcutKey = shortcutKey + 1
menu.addItem(menuItem)
}
menu.addItem(NSMenuItem.separator())
}
if (self.busy) {
menu.addItem(NSMenuItem(title: "Switching PHP versions...", action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
}
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 {
self.statusItem.menu = menu
}
}
}
// MARK: - Callable via Obj-C (#selector)
@objc func updatePhpVersionInStatusBar() {
self.version = PhpVersion()
if (self.busy) {
DispatchQueue.main.async {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
}
} else {
DispatchQueue.main.async {
self.setStatusBarImage(version: self.version!.short)
}
}
self.updateMenu()
}
@objc public func openAbout() {
NSApplication.shared.activate(ignoringOtherApps: true)
NSApplication.shared.orderFrontStandardAboutPanel()
}
@objc public func openActiveConfigFolder() {
Services.openPhpConfigFolder(version: self.version!.short)
}
@objc public func restartPhp() {
Services.restartPhp(version: self.version!.short)
}
@objc public func switchToPhpVersion(sender: AnyObject) {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
let index = sender.tag!
let version = self.availablePhpVersions[index]
self.busy = true
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
// Update the PHP version in the status bar
self.updatePhpVersionInStatusBar()
// Update the menu
self.updateMenu()
// Switch the PHP version
Services.switchToPhpVersion(version: version, availableVersions: self.availablePhpVersions)
// Mark as no longer busy
self.busy = false
// Perform UI updates on main thread
DispatchQueue.main.async {
self.updatePhpVersionInStatusBar()
self.updateMenu()
}
}
}
}

View File

@ -9,19 +9,10 @@
import Foundation
import AppKit
class Services {
public static func mysqlIsRunning() -> Bool {
let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.mysql")
return (running != "")
}
public static func nginxIsRunning() -> Bool {
let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.nginx")
return (running != "")
}
class Actions {
public static func detectPhpVersions() -> [String] {
let files = Shell.execute(command: "ls /usr/local/opt | grep php@")
let files = Shell.user.pipe("ls /usr/local/opt | grep php@")
var versions = files.components(separatedBy: "\n")
// Remove all empty strings
versions.removeAll { (string) -> Bool in
@ -37,18 +28,18 @@ class Services {
public static func switchToPhpVersion(version: String, availableVersions: [String]) {
availableVersions.forEach { (version) in
_ = Shell.execute(command: "brew unlink php@\(version)")
Shell.user.run("brew unlink php@\(version)")
}
if (availableVersions.contains("7.3")) {
_ = Shell.execute(command: "brew link php@7.3")
_ = Shell.execute(command: "valet use php@\(version)")
Shell.user.run("brew link php@7.3")
if (version == Constants.LatestPhpVersion) {
Shell.user.run( "valet use php")
} else {
Shell.user.run("valet use php@\(version)")
}
}
}
public static func restartPhp(version: String) {
_ = Shell.execute(command: "brew services restart php@\(version)")
}
public static func openPhpConfigFolder(version: String) {
let files = [NSURL(fileURLWithPath: "/usr/local/etc/php/\(version)/php.ini")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL]);

View File

@ -8,36 +8,36 @@
import Foundation
class BootChecks {
class Startup {
public static func perform()
public static func checkEnvironment()
{
self.presentAlertOnMainThreadIf(
!Shell.execute(command: "which php").contains("/usr/local/bin/php"),
!Shell.user.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.user.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.user.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.user.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.user.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."
)

View File

@ -8,14 +8,15 @@
import Cocoa
class ImageGenerator {
class MenuBarImageGenerator {
public static func generateImageForStatusBar(width: CGFloat = 30.0, height: CGFloat = 20.0, text: String) -> NSImage {
let image = NSImage(size: NSMakeSize(width, height))
let font = NSFont.systemFont(ofSize: 14)
/**
Takes a string and converts it to an image that can be displayed in the menu bar.
The width of the NSImage depends on the length of the text.
*/
public static func textToImage(text: String) -> NSImage {
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let textRect = CGRect(x: 5, y: -1, width: image.size.width, height: image.size.height)
let font = NSFont.systemFont(ofSize: 14, weight: .medium)
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [
@ -24,8 +25,23 @@ class ImageGenerator {
NSAttributedString.Key.paragraphStyle: textStyle
]
let targetImage: NSImage = NSImage(size: image.size)
let padding : CGFloat = 2.0;
// Create an attributed string so we'll know how wide the item will need to be
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
let textSize = attributedString.size()
// Add padding to the width of the menu bar item
let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height)
let image = NSImage(size: size)
// Set the image rect with the appropriate dimensions
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
// Position the text inside the image rect
let textRect = CGRect(x: padding, y: 0, width: image.size.width, height: image.size.height)
let targetImage: NSImage = NSImage(size: image.size)
let rep: NSBitmapImageRep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(image.size.width),

View File

@ -14,11 +14,9 @@ class PhpVersion {
var long : String = "???"
init() {
let version = Shell
let version = Shell.user
// 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

26
phpmon/Constants.swift Normal file
View File

@ -0,0 +1,26 @@
//
// Constants.swift
// phpmon
//
// Created by Nico Verbruggen on 08/07/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Constants {
/**
* The PHP versions supported by this application.
*/
static let SupportedPhpVersions = [
"5.6", "7.0", "7.1", "7.2", "7.3"
]
/**
Which php version is aliased as `php` to brew?
This is usually the latest PHP version.
*/
static let LatestPhpVersion = "7.3"
}

View File

@ -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)
}
}

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.1</string>
<string>1.3</string>
<key>CFBundleVersion</key>
<string>10</string>
<key>LSApplicationCategoryType</key>

View File

@ -0,0 +1,40 @@
//
// StateManager.swift
// phpmon
//
// Created by Nico Verbruggen on 11/07/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class App {
static let shared = App()
/**
Whether the application is busy switching versions.
*/
var busy: Bool = false
/**
The currently active version of PHP.
*/
var currentVersion: PhpVersion? = nil
/**
All available versions of PHP.
*/
var availablePhpVersions : [String] = []
/**
The timer that will periodically fetch the PHP version that is currently active.
*/
var timer: Timer?
/**
The window controller that will show the log.
*/
var windowController: NSWindowController? = nil
}

View File

@ -0,0 +1,154 @@
//
// MainMenu.swift
// phpmon
//
// Created by Nico Verbruggen on 11/07/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class MainMenu: NSObject, NSWindowDelegate {
static let shared = MainMenu()
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
// MARK: - UI related
public func startup() {
// Start with the icon
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
// Perform environment boot checks
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
Startup.checkEnvironment()
App.shared.availablePhpVersions = Actions.detectPhpVersions()
self.updatePhpVersionInStatusBar()
// Schedule a request to fetch the PHP version every 60 seconds
DispatchQueue.main.async {
App.shared.timer = Timer.scheduledTimer(
timeInterval: 60,
target: self,
selector: #selector(self.updatePhpVersionInStatusBar),
userInfo: nil,
repeats: true
)
}
}
}
public func update() {
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
let menu = NSMenu()
var string = "We are not sure what version of PHP you are running."
if (App.shared.currentVersion != nil) {
string = "You are running PHP \(App.shared.currentVersion!.long)"
}
menu.addItem(NSMenuItem(title: string, action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
if (App.shared.availablePhpVersions.count > 0 && !App.shared.busy) {
var shortcutKey = 1
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
let version = App.shared.availablePhpVersions[index]
let action = #selector(self.switchToPhpVersion(sender:))
let menuItem = NSMenuItem(title: "Switch to PHP \(version)", action: (version == App.shared.currentVersion?.short) ? nil : action, keyEquivalent: "\(shortcutKey)")
menuItem.tag = index
shortcutKey = shortcutKey + 1
menu.addItem(menuItem)
}
menu.addItem(NSMenuItem.separator())
}
if (App.shared.busy) {
menu.addItem(NSMenuItem(title: "Switching PHP versions...", action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
}
if (App.shared.currentVersion != nil) {
menu.addItem(NSMenuItem(title: "PHP configuration file (php.ini)", action: #selector(self.openActiveConfigFolder), keyEquivalent: ""))
}
menu.addItem(NSMenuItem(title: "View shell output", action: #selector(self.openOutput), keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "About phpmon", action: #selector(self.openAbout), keyEquivalent: ""))
menu.items.forEach({ (item) in
item.target = self
})
menu.addItem(NSMenuItem(title: "Quit phpmon", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
DispatchQueue.main.async {
self.statusItem.menu = menu
}
}
}
func setStatusBarImage(version: String) {
self.setStatusBar(
image: MenuBarImageGenerator.textToImage(
text: version
)
)
}
func setStatusBar(image: NSImage) {
if let button = statusItem.button {
image.isTemplate = true
button.image = image
}
}
// MARK: - Callable via Obj-C (#selector)
@objc func openOutput() {
LogViewController.show(delegate: self)
}
@objc func updatePhpVersionInStatusBar() {
App.shared.currentVersion = PhpVersion()
if (App.shared.busy) {
DispatchQueue.main.async {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
}
} else {
DispatchQueue.main.async {
self.setStatusBarImage(version: App.shared.currentVersion!.short)
}
}
self.update()
}
@objc public func openAbout() {
NSApplication.shared.activate(ignoringOtherApps: true)
NSApplication.shared.orderFrontStandardAboutPanel()
}
@objc public func openActiveConfigFolder() {
Actions.openPhpConfigFolder(version: App.shared.currentVersion!.short)
}
@objc public func switchToPhpVersion(sender: AnyObject) {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
let index = sender.tag!
let version = App.shared.availablePhpVersions[index]
App.shared.busy = true
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
// Update the PHP version in the status bar
self.updatePhpVersionInStatusBar()
// Update the menu
self.update()
// Switch the PHP version
Actions.switchToPhpVersion(
version: version,
availableVersions: App.shared.availablePhpVersions
)
// Mark as no longer busy
App.shared.busy = false
// Perform UI updates on main thread
DispatchQueue.main.async {
self.updatePhpVersionInStatusBar()
self.update()
}
}
}
func windowWillClose(_ notification: Notification) {
App.shared.windowController = nil
Shell.user.delegate = nil
}
}

View File

@ -0,0 +1,70 @@
//
// Shell.swift
// phpmon
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
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)
static let user = 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]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(
data: data,
encoding: String.Encoding.utf8.rawValue
)!.replacingOccurrences(
of: "\u{1B}(B\u{1B}[m",
with: ""
) as String
let historyItem = ShellHistoryItem(command: command, output: output)
history.append(historyItem)
// Keep the last 100 items
history = history.suffix(100)
delegate?.didCompleteCommand(historyItem: historyItem)
return output
}
}

View File

@ -1,31 +0,0 @@
//
// Shell.swift
// phpmon
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Shell {
public static func execute(command: String) -> String {
let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["--login", "-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(
data: data,
encoding: String.Encoding.utf8.rawValue
)! as String
return output
}
}

View File

@ -52,20 +52,69 @@
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="0.0"/>
<point key="canvasLocation" x="134" y="40"/>
</scene>
<!--View Controller-->
<!--Log View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="phpmon" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="logWindow" id="XfG-lQ-9wD" customClass="LogViewController" customModule="phpmon" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" identifier="main" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="425" height="264"/>
<rect key="frame" x="0.0" y="0.0" width="662" height="475"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ICa-gx-jgq">
<rect key="frame" x="573" y="8" width="75" height="32"/>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="3md-FI-EWa">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="pressed:" target="XfG-lQ-9wD" id="fIC-Bz-vTK"/>
</connections>
</button>
<scrollView borderType="line" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vZy-5S-021">
<rect key="frame" x="20" y="46" width="622" height="409"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="s5L-AU-0fw">
<rect key="frame" x="1" y="1" width="620" height="407"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" smartInsertDelete="YES" id="tN6-Y9-1pA">
<rect key="frame" x="0.0" y="0.0" width="620" height="407"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="620" height="407"/>
<size key="maxSize" width="620" height="10000000"/>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</textView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="Kho-JF-NZJ">
<rect key="frame" x="-100" y="-100" width="240" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="qp7-7R-gTO">
<rect key="frame" x="605" y="1" width="16" height="407"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
</subviews>
<constraints>
<constraint firstItem="vZy-5S-021" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" constant="20" id="K0k-oE-r37"/>
<constraint firstAttribute="trailing" secondItem="ICa-gx-jgq" secondAttribute="trailing" constant="20" id="LFS-0E-Ibw"/>
<constraint firstItem="vZy-5S-021" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="20" id="Nec-oI-CjE"/>
<constraint firstAttribute="trailing" secondItem="vZy-5S-021" secondAttribute="trailing" constant="20" id="kBJ-O5-eYI"/>
<constraint firstAttribute="bottom" secondItem="ICa-gx-jgq" secondAttribute="bottom" constant="15" id="kYB-Fn-DSA"/>
<constraint firstItem="ICa-gx-jgq" firstAttribute="top" secondItem="vZy-5S-021" secondAttribute="bottom" constant="10" id="xdn-yU-LVb"/>
</constraints>
</view>
<connections>
<outlet property="textView" destination="tN6-Y9-1pA" id="z77-me-Od6"/>
</connections>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="74.5" y="217"/>
<point key="canvasLocation" x="-105" y="377.5"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,72 @@
//
// ViewController.swift
// phpmon
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class LogViewController: NSViewController, ShellDelegate {
public static func show(delegate: NSWindowDelegate? = nil) {
if (App.shared.windowController == nil) {
let vc = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "logWindow") as! LogViewController
Shell.user.delegate = vc
let window = NSWindow(contentViewController: vc)
window.title = "Terminal Output"
window.delegate = delegate
App.shared.windowController = NSWindowController(window: window)
}
App.shared.windowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
}
@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.user.history {
self.appendHistoryItem(entry)
}
}
func didCompleteCommand(historyItem: ShellHistoryItem) {
self.appendHistoryItem(historyItem)
}
@IBAction func pressed(_ sender: Any) {
self.view.window?.windowController?.close()
}
deinit {
print("VC deallocated")
}
}

View File

@ -1,13 +0,0 @@
//
// ViewController.swift
// phpmon
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class ViewController: NSViewController {
}