mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-07 20:10:08 +02:00
♻️ Change app detection, detect apps upfront
This commit is contained in:
@ -9,6 +9,8 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
|
||||
5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
|
||||
54AB03262763858F00A29D5F /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AB03252763858F00A29D5F /* Timer.swift */; };
|
||||
54AB03272763858F00A29D5F /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AB03252763858F00A29D5F /* Timer.swift */; };
|
||||
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
|
||||
54B48B60275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
|
||||
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
|
||||
@ -120,6 +122,7 @@
|
||||
/* Begin PBXFileReference section */
|
||||
5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = "<group>"; };
|
||||
5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
54AB03252763858F00A29D5F /* Timer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timer.swift; sourceTree = "<group>"; };
|
||||
54B48B5E275F66AE006D90C5 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = "<group>"; };
|
||||
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
|
||||
@ -328,6 +331,7 @@
|
||||
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
|
||||
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
|
||||
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */,
|
||||
54AB03252763858F00A29D5F /* Timer.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@ -552,6 +556,7 @@
|
||||
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
|
||||
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
|
||||
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
|
||||
54AB03262763858F00A29D5F /* Timer.swift in Sources */,
|
||||
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
|
||||
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
|
||||
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */,
|
||||
@ -577,6 +582,7 @@
|
||||
files = (
|
||||
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
|
||||
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
|
||||
54AB03272763858F00A29D5F /* Timer.swift in Sources */,
|
||||
54B48B60275F66AE006D90C5 /* Application.swift in Sources */,
|
||||
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
|
||||
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
|
||||
|
@ -62,6 +62,9 @@ class App {
|
||||
/** Cached information about the PHP installations. */
|
||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||
|
||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||
var detectedApplications: [Application] = []
|
||||
|
||||
/** Timer that will periodically reload info about the user's PHP installation. */
|
||||
var timer: Timer?
|
||||
|
||||
|
@ -8,43 +8,26 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
/// An application that is capable of opening a particular directory (usually of a PHP project).
|
||||
/// In most cases this is going to be a code editor, but it could also be another application
|
||||
/// that supports opening those directories, like a visual Git client or a terminal app.
|
||||
class Application {
|
||||
|
||||
/// Name of the app. Used for display purposes.
|
||||
enum AppType {
|
||||
case editor, browser, gitgui, terminal
|
||||
}
|
||||
|
||||
/// Name of the app. Used for display purposes and to determine `name.app` exists.
|
||||
let name: String
|
||||
|
||||
/// Paths to check whether the application is actually installed.
|
||||
/// If the app finds any of these, the app is considered installed.
|
||||
let pathsToVerifyInstalled: [String]
|
||||
/// Application type. Depending on the type, a different action might occur.
|
||||
let type: AppType
|
||||
|
||||
/// Path to the binary that actually opens the directory.
|
||||
let pathToBinary: String
|
||||
|
||||
/// Instruction that needs to be followed in order to ensure that the app can open with this editor.
|
||||
var missingBinaryInstruction: String? = nil
|
||||
|
||||
/// Callback that is executed to open a particular folder.
|
||||
/// Must open the directory in the requested app, usually by using `pathToBinary`.
|
||||
@objc let openCallback: (String) -> Void
|
||||
|
||||
/**
|
||||
- Parameter name: Name of the application.
|
||||
- Parameter installPath: Files to verify, if any file exists here the app is considered present on the system.
|
||||
- Parameter binaryPath: Additional file that is used to open a specific path.
|
||||
- Parameter open: Callback used to open a specific directory in the editor in question.
|
||||
- Parameter instruction: Instruction for end user that needs to be followed in order to ensure the `binaryPath exists.
|
||||
*/
|
||||
init(name: String, installPaths: [String], binaryPath: String, open: @escaping ((String) -> Void), instruction: String? = nil) {
|
||||
/// Initializer.
|
||||
init(_ name: String, _ type: AppType) {
|
||||
self.name = name
|
||||
self.pathsToVerifyInstalled = installPaths.map({ path in
|
||||
return path.replacingOccurrences(of: " ", with: "\\ ")
|
||||
})
|
||||
self.pathToBinary = binaryPath.replacingOccurrences(of: " ", with: "\\ ")
|
||||
self.openCallback = open
|
||||
self.missingBinaryInstruction = instruction
|
||||
self.type = type
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,22 +35,17 @@ class Application {
|
||||
This will open the editor if it isn't open yet.
|
||||
*/
|
||||
@objc public func openDirectory(file: String) {
|
||||
self.openCallback(file)
|
||||
return Shell.run("/usr/bin/open -a \(self.name) \(file)")
|
||||
}
|
||||
|
||||
/** Checks if the app is installed. */
|
||||
func isInstalled() -> Bool {
|
||||
// TODO: Alternative way to detect if an app is installed:
|
||||
// mdfind "kMDItemKind == 'Application'" | grep AppName.app
|
||||
// This will return the path to the application. Worth a refactor?
|
||||
self.pathsToVerifyInstalled.map({ path in
|
||||
Shell.fileExists(path)
|
||||
}).contains(true)
|
||||
}
|
||||
|
||||
/** Checks if the correct binary required to open directories and/or files exists. */
|
||||
func hasBinary() -> Bool {
|
||||
return Shell.fileExists(self.pathToBinary)
|
||||
// If this script does not complain, the app exists!
|
||||
return Shell.user.execute(
|
||||
"/usr/bin/open -Ra \"\(self.name)\"",
|
||||
requiresPath: false,
|
||||
waitUntilExit: true
|
||||
).task.terminationStatus == 0
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,66 +53,13 @@ class Application {
|
||||
*/
|
||||
static public func detectPresetApplications() -> [Application] {
|
||||
return [
|
||||
Application(
|
||||
name: "PhpStorm",
|
||||
installPaths: [
|
||||
"/Applications/PhpStorm.app/Contents/Info.plist",
|
||||
"/usr/local/bin/pstorm"
|
||||
],
|
||||
binaryPath: "/usr/local/bin/pstorm",
|
||||
open: { path in
|
||||
Shell.run("/usr/local/bin/pstorm \(path)")
|
||||
},
|
||||
instruction: "editors.pstorm_binary_not_linked.desc".localized
|
||||
),
|
||||
Application(
|
||||
name: "PhpStorm (via Toolbox)",
|
||||
installPaths: [
|
||||
"~/Applications/JetBrains Toolbox/PhpStorm.app/Contents/Info.plist",
|
||||
"/usr/local/bin/phpstorm"
|
||||
],
|
||||
binaryPath: "/usr/local/bin/phpstorm",
|
||||
open: { path in
|
||||
Shell.run("/usr/local/bin/phpstorm \(path)")
|
||||
},
|
||||
instruction: "editors.phpstorm_binary_not_linked.desc".localized
|
||||
),
|
||||
Application(
|
||||
name: "Visual Studio Code",
|
||||
installPaths: [
|
||||
"/Applications/Visual Studio Code.app/Contents/Info.plist",
|
||||
"/usr/local/bin/code"
|
||||
],
|
||||
binaryPath: "/usr/local/bin/code",
|
||||
open: { path in
|
||||
Shell.run("/usr/local/bin/code \(path)")
|
||||
},
|
||||
instruction: "editors.code_binary_not_linked.desc".localized
|
||||
),
|
||||
Application(
|
||||
name: "Sublime Text",
|
||||
installPaths: ["/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl"],
|
||||
binaryPath: "/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl",
|
||||
open: { path in
|
||||
Shell.run("/Applications/Sublime\\ Text.app/Contents/SharedSupport/bin/subl \(path)")
|
||||
}
|
||||
),
|
||||
Application(
|
||||
name: "Sublime Merge",
|
||||
installPaths: ["/Applications/Sublime Merge.app/Contents/SharedSupport/bin/smerge"],
|
||||
binaryPath: "/Applications/Sublime Merge.app/Contents/SharedSupport/bin/smerge",
|
||||
open: { path in
|
||||
Shell.run("/Applications/Sublime\\ Merge.app/Contents/SharedSupport/bin/smerge \(path)")
|
||||
}
|
||||
),
|
||||
Application(
|
||||
name: "iTerm",
|
||||
installPaths: ["/Applications/iTerm.app/Contents/Info.plist"],
|
||||
binaryPath: "/Applications/iTerm.app/Contents/Info.plist",
|
||||
open: { path in
|
||||
Shell.run("open -a iTerm \(path)")
|
||||
}
|
||||
)
|
||||
].filter { return $0.isInstalled() }
|
||||
Application("PhpStorm", .editor),
|
||||
Application("Visual Studio Code", .editor),
|
||||
Application("Sublime Text", .editor),
|
||||
Application("Sublime Merge", .gitgui),
|
||||
Application("iTerm", .terminal)
|
||||
].filter {
|
||||
return $0.isInstalled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
phpmon/Domain/Helpers/Timer.swift
Normal file
32
phpmon/Domain/Helpers/Timer.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// BenchmarkTimer.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 10/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class BenchmarkTimer {
|
||||
let startTime: CFAbsoluteTime
|
||||
var endTime: CFAbsoluteTime?
|
||||
|
||||
init() {
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
|
||||
func stop() -> CFAbsoluteTime {
|
||||
endTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
return duration!
|
||||
}
|
||||
|
||||
var duration: CFAbsoluteTime? {
|
||||
if let endTime = endTime {
|
||||
return endTime - startTime
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
@ -55,10 +55,19 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
|
||||
|
||||
updatePhpVersionInStatusBar()
|
||||
|
||||
print("Determining broken PHP-FPM...")
|
||||
// Attempt to find out if PHP-FPM is broken
|
||||
let installation = App.phpInstall!
|
||||
installation.notifyAboutBrokenPhpFpm()
|
||||
|
||||
print("Detecting applications...")
|
||||
// Attempt to load list of applications
|
||||
App.shared.detectedApplications = Application.detectPresetApplications()
|
||||
let appNames = App.shared.detectedApplications.map { app in
|
||||
return app.name
|
||||
}
|
||||
print("Detected applications: \(appNames)")
|
||||
|
||||
// Attempt to find out more info about Valet
|
||||
print("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version)")
|
||||
print("PHP Monitor is ready to serve!")
|
||||
|
@ -41,7 +41,9 @@ class SiteListCell: NSTableCellView
|
||||
|
||||
// Show the green or red lock based on whether the site was secured
|
||||
imageViewLock.image = NSImage(named: site.secured ? "Lock" : "LockUnlocked")
|
||||
imageViewLock.contentTintColor = site.secured ? NSColor.systemGreen : NSColor.systemRed
|
||||
imageViewLock.contentTintColor = site.secured ?
|
||||
NSColor.init(red: 63/255, green: 195/255, blue: 128/255, alpha: 1.0) // green
|
||||
: NSColor.init(red: 246/255, green: 71/255, blue: 71/255, alpha: 1.0) // red
|
||||
|
||||
// Show the current driver
|
||||
labelDriver.stringValue = site.driver ?? "???"
|
||||
|
@ -23,7 +23,9 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||
var sites: [Valet.Site] = []
|
||||
|
||||
/// Array that contains various apps that might open a particular site directory.
|
||||
var editors: [Application] = Application.detectPresetApplications()
|
||||
var applications: [Application] {
|
||||
return App.shared.detectedApplications
|
||||
}
|
||||
|
||||
/// String that was last searched for. Empty by default.
|
||||
var lastSearchedFor = ""
|
||||
@ -283,11 +285,11 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||
keyEquivalent: "L"
|
||||
)
|
||||
|
||||
if (editors.count > 0) {
|
||||
if (applications.count > 0) {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
menu.addItem(withTitle: "site_list.detected_apps".localized, action: nil, keyEquivalent: "")
|
||||
|
||||
for (index, editor) in editors.enumerated() {
|
||||
for (index, editor) in applications.enumerated() {
|
||||
let editorMenuItem = EditorMenuItem(
|
||||
title: "Open with \(editor.name)",
|
||||
action: #selector(self.openWithEditor(sender:)),
|
||||
@ -296,10 +298,9 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||
editorMenuItem.editor = editor
|
||||
menu.addItem(editorMenuItem)
|
||||
}
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
menu.addItem(withTitle: "site_list.system_apps".localized, action: nil, keyEquivalent: "")
|
||||
menu.addItem(
|
||||
withTitle: "site_list.open_in_finder".localized,
|
||||
@ -322,29 +323,8 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||
|
||||
@objc func openWithEditor(sender: EditorMenuItem) {
|
||||
guard let editor = sender.editor else { return }
|
||||
|
||||
if editor.hasBinary() {
|
||||
editor.openDirectory(file: selectedSite!.absolutePath!)
|
||||
} else {
|
||||
presentAlertForMissingEditorBinary(editor, sender)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentAlertForMissingEditorBinary(_ editor: Application, _ sender: EditorMenuItem) {
|
||||
Alert.confirm(
|
||||
onWindow: self.view.window!,
|
||||
messageText: "editors.binary_missing.title".localized(editor.pathToBinary),
|
||||
informativeText:
|
||||
editor.missingBinaryInstruction
|
||||
?? "editors.binary_missing.desc".localized(editor.pathToBinary),
|
||||
buttonTitle: "editors.alert.try_again".localized,
|
||||
secondButtonTitle: "editors.alert.cancel".localized,
|
||||
onFirstButtonPressed: {
|
||||
self.openWithEditor(sender: sender)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Deinitialization
|
||||
|
||||
deinit {
|
||||
|
@ -62,6 +62,26 @@ class Shell {
|
||||
_ command: String,
|
||||
requiresPath: Bool = false
|
||||
) -> String {
|
||||
let shellOutput = self.execute(command, requiresPath: requiresPath)
|
||||
let hasError = (
|
||||
shellOutput.standardOutput == ""
|
||||
&& shellOutput.errorOutput.lengthOfBytes(using: .utf8) > 0
|
||||
)
|
||||
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
|
||||
}
|
||||
|
||||
/**
|
||||
Runs the command and returns a `ShellOutput` object, which contains info about the process.
|
||||
|
||||
- Parameter command: The command to run
|
||||
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
|
||||
- Parameter waitUntilExit: Waits for the command to complete before returning the `ShellOutput`
|
||||
*/
|
||||
func execute(
|
||||
_ command: String,
|
||||
requiresPath: Bool = false,
|
||||
waitUntilExit: Bool = false
|
||||
) -> ShellOutput {
|
||||
let task = Process()
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
@ -76,14 +96,19 @@ class Shell {
|
||||
task.standardError = errorPipe
|
||||
task.launch()
|
||||
|
||||
let error = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
let output = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
|
||||
if (output == "" && error.lengthOfBytes(using: .utf8) > 0) {
|
||||
return error
|
||||
if waitUntilExit {
|
||||
task.waitUntilExit()
|
||||
}
|
||||
|
||||
return output
|
||||
return ShellOutput(
|
||||
standardOutput: String(
|
||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8
|
||||
)!,
|
||||
errorOutput: String(
|
||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8
|
||||
)!,
|
||||
task: task
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,3 +119,17 @@ class Shell {
|
||||
return Shell.pipe("if [ -f \(path) ]; then /bin/echo -n \"0\"; fi") == "0"
|
||||
}
|
||||
}
|
||||
|
||||
class ShellOutput {
|
||||
let standardOutput: String
|
||||
let errorOutput: String
|
||||
let task: Process
|
||||
|
||||
init(standardOutput: String,
|
||||
errorOutput: String,
|
||||
task: Process) {
|
||||
self.standardOutput = standardOutput
|
||||
self.errorOutput = errorOutput
|
||||
self.task = task
|
||||
}
|
||||
}
|
||||
|
@ -75,45 +75,10 @@
|
||||
"site_list.system_apps" = "System Applications";
|
||||
|
||||
// EDITORS
|
||||
"editors.binary_missing.title" = "`%@` missing";
|
||||
"editors.binary_missing.desc" = "The associated binary (`%@`) is missing. Because of this, PHP Monitor cannot open the domain’s folder.";
|
||||
|
||||
"editors.alert.try_again" = "Try Again";
|
||||
"editors.alert.cancel" = "Cancel";
|
||||
|
||||
// - PHPSTORM + TOOLBOX
|
||||
"editors.phpstorm_binary_not_linked.desc" =
|
||||
"PHP Monitor makes use of PhpStorm’s launcher to open a specific directory.
|
||||
|
||||
The required launcher does not seem to be found in the usual location. In JetBrains Toolbox go to Settings, and select 'Generate shell scripts'.
|
||||
|
||||
As the directory, enter `/usr/local/bin`.
|
||||
|
||||
After this is done, PHP Monitor should be able to open the domain’s folder in PhpStorm.";
|
||||
|
||||
// - PHPSTORM (standalone)
|
||||
"editors.pstorm_binary_not_linked.desc" =
|
||||
"PHP Monitor makes use of PhpStorm’s launcher to open a specific directory.
|
||||
|
||||
The required launcher does not seem to be found in the usual location. Please set up the launcher by going to 'Tools > Create command-line launcher' in PhpStorm.
|
||||
|
||||
After this is done, PHP Monitor should be able to open the domain’s folder in PhpStorm.";
|
||||
"editors.code_binary_not_linked.desc" =
|
||||
"PHP Monitor makes use of Visual Studio Code‘s helper binary, `code`, but it seems not to exist on your system.
|
||||
|
||||
You can fix this by selecting 'Install `code` command in PATH' in Visual Studio Code’s command palette.
|
||||
(Type `>code` to find it.)
|
||||
|
||||
After this is done, PHP Monitor should be able to open the domain’s folder in PhpStorm.";
|
||||
|
||||
// - VS CODE
|
||||
"editors.code_binary_not_linked.desc" =
|
||||
"PHP Monitor makes use of Visual Studio Code‘s helper binary, `code`, but it seems not to exist on your system.
|
||||
|
||||
You can fix this by selecting 'Install `code` command in PATH' in Visual Studio Code’s command palette.
|
||||
(Type `>code` to find it.)
|
||||
|
||||
After this is done, PHP Monitor should be able to open the domain’s folder in PhpStorm.";
|
||||
|
||||
// PREFERENCES
|
||||
|
||||
"prefs.title" = "PHP Monitor";
|
||||
|
Reference in New Issue
Block a user