1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-08 12:29:54 +02:00

Compare commits

...

24 Commits
v3.2 ... v3.3

Author SHA1 Message Date
acb18474c8 👌 Fine-tuning the README 2021-04-01 21:17:35 +02:00
ed61490398 📝 Update README 2021-04-01 21:14:03 +02:00
abb76273c9 📝 New screenshot 2021-04-01 20:56:28 +02:00
2f15af4ff8 Improved test 2021-04-01 20:44:53 +02:00
e29e8416d5 Fix tests, add test for version detection 2021-04-01 20:38:24 +02:00
5d423210dd 👌 Improved PHP version filter 2021-04-01 20:19:55 +02:00
340c36fdf8 🔧 Bump version & build number 2021-04-01 00:18:54 +02:00
3085158b80 🐛 Validate detected PHP version (#30) 2021-04-01 00:18:33 +02:00
c2585f9bf4 👌 Cleanup, add separator before refresh item 2021-03-31 23:57:17 +02:00
d478137742 🔧 Bump version & build number 2021-03-31 19:50:38 +02:00
827bd182b1 🐛 Avoid duplicates (#30) 2021-03-31 19:38:30 +02:00
f7500637fe 🐛 Be more lenient about Valet checks (#27) 2021-03-31 12:21:06 +02:00
7c884610b1 👌 Use snake case for all key names 2021-03-30 16:38:09 +02:00
d3d219751e 🍱 Updated layout constraints 2021-03-30 16:35:13 +02:00
16d2e7d06f Add preferences dialog to enable static icon (#25) 2021-03-30 16:31:28 +02:00
47b86ff9fa 🚩 New prerelease build 2021-03-29 22:29:44 +02:00
6e574b9154 👌 Make sure 'refresh' reopens the menu afterwards (#24) 2021-03-29 22:24:15 +02:00
485001403d Add refresh button (#24)
By pressing Command-R while the menu is open, the information about the
current PHP installation will be refreshed. 

You'll need to open the menu again, but now it will contain up-to-date 
information. You could also just press the menu item, of course.
2021-03-29 21:50:40 +02:00
694c5e7f7d Add credits 2021-03-29 20:49:55 +02:00
d9ff26385a 🍱 Optimized assets 2021-03-29 20:04:08 +02:00
0cfb7c65bb 👌 Avoid self 2021-03-19 16:15:05 +01:00
3cbc2a0367 👌 Avoid self. (and capture using [self] in) 2021-03-19 16:01:03 +01:00
7733c90206 👌 Whitespace, remove print() 2021-03-19 15:50:43 +01:00
0ad6e5cb1c 👌 Multiple trailing closures 2021-03-19 15:44:26 +01:00
35 changed files with 504 additions and 129 deletions

View File

@@ -7,6 +7,8 @@
objects = {
/* 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 */; };
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */; };
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; };
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
@@ -18,6 +20,7 @@
C41C1B4B22B019FF00E7CF16 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */; };
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
C42295DD2358D02000E263B2 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; };
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; };
C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; };
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */; };
@@ -28,6 +31,8 @@
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C8F25CC7FD000CC7490 /* StatsView.xib */; };
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
@@ -62,6 +67,7 @@
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; };
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -75,6 +81,8 @@
/* End PBXContainerItemProxy section */
/* 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>"; };
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>"; };
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = "<group>"; };
@@ -89,6 +97,7 @@
C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
C43A8A1925D9CD1000591B77 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = "<group>"; };
C43A8A1F25D9D1D700591B77 /* brew.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = brew.json; sourceTree = "<group>"; };
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewJsonParserTest.swift; sourceTree = "<group>"; };
@@ -118,6 +127,7 @@
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionParserTest.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>"; };
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionDetectionTest.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -138,6 +148,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5420395726135DB800FB00FA /* Preferences */ = {
isa = PBXGroup;
children = (
5420395826135DC100FB00FA /* PrefsVC.swift */,
5420395E2613607600FB00FA /* Preferences.swift */,
);
path = Preferences;
sourceTree = "<group>";
};
C405A4CD24B9B9070062FAFA /* IAP */ = {
isa = PBXGroup;
children = (
@@ -176,6 +195,7 @@
C4EE188322D3386B00E126E5 /* Constants.swift */,
C41E181722CB61EB0072CF09 /* Domain */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
C4232EE42612526500158FC6 /* Credits.html */,
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
C473319E2470923A009A0597 /* Localizable.strings */,
@@ -187,6 +207,7 @@
C41E181722CB61EB0072CF09 /* Domain */ = {
isa = PBXGroup;
children = (
5420395726135DB800FB00FA /* Preferences */,
C4F7808A25D7F918000DBC97 /* Terminal */,
C4B13B1D25C4915000548C3A /* Core */,
C47331A0247093AC009A0597 /* Menu */,
@@ -240,6 +261,7 @@
C4F7809B25D80344000DBC97 /* CommandTest.swift */,
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
C43A8A1925D9CD1000591B77 /* Utility.swift */,
);
path = "phpmon-tests";
@@ -352,6 +374,7 @@
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */,
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
C4232EE52612526500158FC6 /* Credits.html in Resources */,
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */,
@@ -377,12 +400,14 @@
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */,
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */,
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */,
C42295DD2358D02000E263B2 /* Command.swift in Sources */,
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
5420395F2613607600FB00FA /* Preferences.swift in Sources */,
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
@@ -407,12 +432,14 @@
C4F780CC25D80B75000DBC97 /* PhpInstallation.swift in Sources */,
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */,
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */,
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */,
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */,
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */,
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */,
@@ -422,6 +449,7 @@
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */,
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */,
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */,
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */,
@@ -577,7 +605,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 49;
CURRENT_PROJECT_VERSION = 54;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@@ -585,7 +613,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 3.2;
MARKETING_VERSION = 3.3;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -601,7 +629,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 49;
CURRENT_PROJECT_VERSION = 54;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@@ -609,7 +637,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 3.2;
MARKETING_VERSION = 3.3;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -1,20 +1,21 @@
# PHP Monitor
> ⭐️ If this software has been useful to you, all I ask is that you **please star the repository**, so I know that the software is being used. You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy. Thank you! 😃
> If this software has been useful to you, all I ask is that you **please star the repository**, so I know that the software is being used.
> You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy.<br>**Thank you!** ⭐️
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
PHP Monitor (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar.
**PHP Monitor** (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so you need to have it set up before you can use this.
<img src="./docs/screenshot.png" width="389px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot33.png" width="376px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: A menu showing all of the functionality of PHP Monitor.</i></small>
It's also super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
It also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
## 🖥 System requirements
@@ -160,6 +161,7 @@ For Zend extensions:
* `; zend_extension="*.so"`
The `*` is a wildcard and the name of the extension. If you've commented out the extension, make sure you've commented it out with a semicolon (;) and a single space after the semicolon for PHP Monitor to detect it.
</details>
<details>
@@ -187,13 +189,17 @@ You will need to manually clean up those folders yourself using `rm -rf` or by m
</details>
## 📝 Another issue?
## 📝 Having another issue?
I did not include any tracking or analytics software, so if you encounter issues, let me know [via an issue](https://github.com/nicoverbruggen/phpmon/issues/new).
## 💵 Support me?
I usually develop this application in my spare time, after work. If you find the application useful and you have a bit of money to spare, feel free to send me [a tip via PayPal](https://paypal.me/nicoverbruggen).
PHP Monitor is available entirely **free of charge**, but if you can afford it a donation helps keep the project alive and the app maintained.
You can find a [sponsor](https://nicoverbruggen.be/sponsor) link at the top of this repo or you could click the link here to be taken to my sponsorship page.
Donations really help with the Apple Developer Program cost, and keep me motivated to keep working on PHP Monitor outside of work hours (I do have a day job!).
## 🚜 How it works

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

BIN
docs/screenshot33.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

@@ -0,0 +1,30 @@
//
// PhpVersionDetectionTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 01/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class PhpVersionDetectionTest: XCTestCase {
func testCanDetectValidPhpVersions() throws {
let outcome = Actions.extractPhpVersions(from: [
"", // empty lines should be omitted
"php@8.0",
"php@8.0", // should only be detected once
"meta-php@8.0", // should be omitted, invalid
"php@8.0-coolio", // should be omitted, invalid
"php@7.0",
"",
"unrelatedphp@1.0", // should be omitted, invalid
"php@5.6",
"php@5.4" // should be omitted, not supported
], checkBinaries: false)
XCTAssertEqual(outcome, ["8.0", "7.0", "5.6"])
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 500 B

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "phpmon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "phpmon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

22
phpmon/Credits.html Normal file
View File

@@ -0,0 +1,22 @@
<html>
<head>
<style>
body {
background-color: #FFF;
color: #000;
font-family: -apple-system;
font-size: 11px;
padding: 5px;
margin: 5px;
}
</style>
</head>
<body>
<br>
<p><b>Want to spread the love?</b> Leave a <a href="https://github.com/nicoverbruggen/phpmon">star on GitHub</a>!</p>
<p><b>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p>
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p>
<br>
</body>
</html>

View File

@@ -18,6 +18,14 @@ class App {
return App.shared.busy
}
/** The list of preferences that are currently active. */
var preferences: [PreferenceName: Bool]!
/**
The window controller of the currently active window.
*/
var windowController: NSWindowController? = nil
/**
Whether the application is busy switching versions.
*/
@@ -43,7 +51,7 @@ class App {
*/
var brewPhpPackage: HomebrewPackage? = nil {
didSet {
self.brewPhpVersion = self.brewPhpPackage!.version
brewPhpVersion = brewPhpPackage!.version
}
}

View File

@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17506"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Application-->
@@ -54,5 +55,89 @@
</objects>
<point key="canvasLocation" x="-343" y="-16"/>
</scene>
<!--Window Controller-->
<scene sceneID="PQa-AT-b2a">
<objects>
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="h4c-3b-nko">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="372" y="403" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/>
<view key="contentView" id="2yL-50-11x">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/>
</connections>
</window>
<connections>
<segue destination="AW2-rV-rbS" kind="relationship" relationship="window.shadowedContentViewController" id="3dX-9V-eA0"/>
</connections>
</windowController>
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="327"/>
</scene>
<!--Preferences-->
<scene sceneID="iyi-IS-7Ps">
<objects>
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="Pf1-A5-3Xz">
<rect key="frame" x="0.0" y="0.0" width="462" height="139"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="GSr-K5-3yw">
<rect key="frame" x="373" y="13" width="76" height="32"/>
<buttonCell key="cell" type="push" title="CLOSE" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ocw-Rx-gyh">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="pressed:" target="AW2-rV-rbS" id="8dA-y4-voq"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MEf-MN-oXt">
<rect key="frame" x="18" y="102" width="424" height="18"/>
<buttonCell key="cell" type="check" title="DYN_ICON" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="m5s-qp-Iaj">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="toggledDynamicIcon:" target="AW2-rV-rbS" id="cuJ-mt-agf"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JrH-aa-AzL">
<rect key="frame" x="18" y="81" width="426" height="14"/>
<textFieldCell key="cell" title="DYN_ICON_DESC" id="MHA-Xt-xgF">
<font key="font" metaFont="system" size="11"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="JrH-aa-AzL" secondAttribute="trailing" constant="20" symbolic="YES" id="8iM-Xf-ShU"/>
<constraint firstAttribute="trailing" secondItem="GSr-K5-3yw" secondAttribute="trailing" constant="20" symbolic="YES" id="AT9-5F-6g1"/>
<constraint firstItem="MEf-MN-oXt" firstAttribute="top" secondItem="Pf1-A5-3Xz" secondAttribute="top" constant="20" symbolic="YES" id="FJC-Lx-L8a"/>
<constraint firstItem="MEf-MN-oXt" firstAttribute="leading" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="Imd-YJ-Ae7"/>
<constraint firstItem="JrH-aa-AzL" firstAttribute="top" secondItem="MEf-MN-oXt" secondAttribute="bottom" constant="8" symbolic="YES" id="Vf8-fx-H50"/>
<constraint firstAttribute="bottom" secondItem="GSr-K5-3yw" secondAttribute="bottom" constant="20" symbolic="YES" id="dAS-yW-vua"/>
<constraint firstItem="JrH-aa-AzL" firstAttribute="leading" secondItem="MEf-MN-oXt" secondAttribute="leading" id="dzR-S7-M6U"/>
<constraint firstItem="GSr-K5-3yw" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="mTE-WD-54L"/>
<constraint firstAttribute="trailing" secondItem="MEf-MN-oXt" secondAttribute="trailing" constant="20" symbolic="YES" id="pJg-zj-cBs"/>
</constraints>
</view>
<connections>
<outlet property="buttonClose" destination="GSr-K5-3yw" id="d4I-Cf-gXD"/>
<outlet property="buttonDynamicIcon" destination="MEf-MN-oXt" id="qEN-Vg-EZS"/>
<outlet property="labelDynamicIcon" destination="JrH-aa-AzL" id="CFc-fF-oPq"/>
</connections>
</viewController>
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="216" y="319"/>
</scene>
</scenes>
</document>

View File

@@ -62,11 +62,11 @@ class PhpExtension {
*/
func toggle() {
Actions.sed(
file: self.file,
original: self.line,
replacement: self.enabled ? "; \(self.line)" : self.line.replacingOccurrences(of: "; ", with: "")
file: file,
original: line,
replacement: enabled ? "; \(line)" : line.replacingOccurrences(of: "; ", with: "")
)
self.enabled = !self.enabled
enabled = !enabled
}
// MARK: - Static Methods

View File

@@ -16,28 +16,29 @@ class PhpInstallation {
// MARK: - Computed
var formula: String {
return (self.version.short == App.shared.brewPhpVersion) ? "php" : "php@\(self.version.short)"
return (version.short == App.shared.brewPhpVersion) ? "php" : "php@\(version.short)"
}
// MARK: - Initializer
init() {
// Show information about the current version
self.version = Self.getVersion()
version = Self.getVersion()
// If an error occurred, exit early
if (self.version.error) {
self.configuration = Configuration()
self.extensions = []
if (version.error) {
configuration = Configuration()
extensions = []
return
}
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(self.version.short)/php.ini")
self.extensions = PhpExtension.load(from: path)
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path)
// Get configuration values
self.configuration = Configuration(
configuration = Configuration(
memory_limit: Self.getByteCount(key: "memory_limit"),
upload_max_filesize: Self.getByteCount(key: "upload_max_filesize"),
post_max_size: Self.getByteCount(key: "post_max_size")

View File

@@ -25,11 +25,12 @@ class MainMenu: NSObject, NSWindowDelegate {
*/
func startup() {
// Start with the icon
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
// Perform environment boot checks
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
Startup().checkEnvironment(success: { self.onEnvironmentPass() },
failure: { self.onEnvironmentFail() }
Startup().checkEnvironment(success: { onEnvironmentPass() },
failure: { onEnvironmentFail() }
)
}
}
@@ -39,13 +40,14 @@ class MainMenu: NSObject, NSWindowDelegate {
*/
private func onEnvironmentPass() {
App.shared.availablePhpVersions = Actions.detectPhpVersions()
self.updatePhpVersionInStatusBar()
updatePhpVersionInStatusBar()
// Schedule a request to fetch the PHP version every 60 seconds
DispatchQueue.main.async {
DispatchQueue.main.async { [self] in
App.shared.timer = Timer.scheduledTimer(
timeInterval: 60,
target: self,
selector: #selector(self.updatePhpVersionInStatusBar),
selector: #selector(updatePhpVersionInStatusBar),
userInfo: nil,
repeats: true
)
@@ -56,7 +58,7 @@ class MainMenu: NSObject, NSWindowDelegate {
When the environment is not OK, present an alert to inform the user.
*/
private func onEnvironmentFail() {
DispatchQueue.main.async {
DispatchQueue.main.async { [self] in
let close = Alert.present(
messageText: "alert.cannot_start.title".localized,
informativeText: "alert.cannot_start.info".localized,
@@ -68,7 +70,7 @@ class MainMenu: NSObject, NSWindowDelegate {
exit(1)
}
self.startup()
startup()
}
}
@@ -77,7 +79,7 @@ class MainMenu: NSObject, NSWindowDelegate {
*/
func update() {
// Update the menu item on the main thread
DispatchQueue.main.async {
DispatchQueue.main.async { [self] in
// Create a new menu
let menu = StatusMenu()
@@ -94,15 +96,16 @@ class MainMenu: NSObject, NSWindowDelegate {
menu.addItem(NSMenuItem.separator())
// Add about & quit menu items
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"))
menu.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(openPrefs), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(openAbout), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(terminateApp), keyEquivalent: "q"))
// Make sure every item can be interacted with
menu.items.forEach({ (item) in
item.target = self
})
self.statusItem.menu = menu
statusItem.menu = menu
}
}
@@ -110,7 +113,7 @@ class MainMenu: NSObject, NSWindowDelegate {
Sets the status bar image based on a version string.
*/
func setStatusBarImage(version: String) {
self.setStatusBar(
setStatusBar(
image: MenuBarImageGenerator.textToImage(text: version)
)
}
@@ -136,17 +139,18 @@ class MainMenu: NSObject, NSWindowDelegate {
- Parameter execute: 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 = {})
private func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
{
App.shared.busy = true
self.setBusyImage()
setBusyImage()
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
self.update()
update()
execute()
App.shared.busy = false
DispatchQueue.main.async {
self.updatePhpVersionInStatusBar()
self.update()
DispatchQueue.main.async { [self] in
updatePhpVersionInStatusBar()
update()
completion()
}
}
@@ -156,80 +160,102 @@ class MainMenu: NSObject, NSWindowDelegate {
@objc func updatePhpVersionInStatusBar() {
App.shared.currentInstall = PhpInstallation()
DispatchQueue.main.async {
if (App.shared.busy) {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
refreshIcon()
update()
}
func refreshIcon() {
DispatchQueue.main.async { [self] in
if (App.busy) {
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
} else {
self.setStatusBarImage(version: App.phpInstall!.version.short)
if Preferences.preferences[.shouldDisplayDynamicIcon] == false {
// Static icon has been requested
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
} else {
// The dynamic icon has been requested
setStatusBarImage(version: App.phpInstall!.version.short)
}
}
}
}
@objc func reloadPhpMonitorMenu() {
waitAndExecute {
// This automatically reloads the menu
print("Reloading information about the PHP installation...")
} completion: {
// Add a slight delay to make sure it loads the new menu
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// Open the menu again
MainMenu.shared.statusItem.button?.performClick(nil)
}
}
self.update()
}
@objc func setBusyImage() {
DispatchQueue.main.async {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
DispatchQueue.main.async { [self] in
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
}
}
// MARK: - Actions
@objc func restartPhpFpm() {
self.waitAndExecute({
waitAndExecute {
Actions.restartPhpFpm()
})
}
}
@objc func restartAllServices() {
self.waitAndExecute({
waitAndExecute {
Actions.restartDnsMasq()
Actions.restartPhpFpm()
Actions.restartNginx()
})
}
}
@objc func restartNginx() {
self.waitAndExecute({
waitAndExecute {
Actions.restartNginx()
})
}
}
@objc func restartDnsMasq() {
self.waitAndExecute({
waitAndExecute {
Actions.restartDnsMasq()
})
}
}
@objc func toggleExtension(sender: ExtensionMenuItem) {
self.waitAndExecute({
// Toggle that extension
print("Toggling extension '\(sender.phpExtension!.name)'")
waitAndExecute {
sender.phpExtension?.toggle()
})
}
}
@objc func openPhpInfo() {
self.waitAndExecute({
waitAndExecute {
// Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
// Tell php-cgi to run the PHP and output as an .html file
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
}, {
} completion: {
// When this has been completed, open the URL to the file in the browser
NSWorkspace.shared.open(URL(string: "file:///private/tmp/phpmon_phpinfo.html")!)
})
}
}
@objc func forceRestartLatestPhp() {
// Tell the user the switch is about to occur
Alert.notify(message: "alert.force_reload.title".localized, info: "alert.force_reload.info".localized)
// Start switching
self.waitAndExecute(
{ Actions.fixMyPhp() },
{ Alert.notify(
message: "alert.force_reload_done.title".localized,
info: "alert.force_reload_done.info".localized
) }
)
waitAndExecute {
Actions.fixMyPhp()
} completion: {
Alert.notify(message: "alert.force_reload_done.title".localized, info: "alert.force_reload_done.info".localized)
}
}
@objc func openActiveConfigFolder() {
@@ -248,17 +274,17 @@ class MainMenu: NSObject, NSWindowDelegate {
}
@objc func switchToPhpVersion(sender: PhpMenuItem) {
print("Switching to: PHP \(sender.version)")
// print("Switching to: PHP \(sender.version)")
self.setBusyImage()
setBusyImage()
App.shared.busy = true
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
// Update the PHP version in the status bar
self.updatePhpVersionInStatusBar()
updatePhpVersionInStatusBar()
// Update the menu
self.update()
update()
// Switch the PHP version
Actions.switchToPhpVersion(
@@ -270,9 +296,10 @@ class MainMenu: NSObject, NSWindowDelegate {
App.shared.busy = false
// Perform UI updates on main thread
DispatchQueue.main.async {
self.updatePhpVersionInStatusBar()
self.update()
DispatchQueue.main.async { [self] in
updatePhpVersionInStatusBar()
update()
// Send a notification that the switch has been completed
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, sender.version),
@@ -287,6 +314,10 @@ class MainMenu: NSObject, NSWindowDelegate {
NSApplication.shared.orderFrontStandardAboutPanel()
}
@objc func openPrefs() {
PrefsVC.show()
}
@objc func terminateApp() {
NSApplication.shared.terminate(nil)
}

View File

@@ -15,18 +15,18 @@ class StatusMenu : NSMenu {
if App.phpInstall!.version.error {
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
self.addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
}
return
}
let phpVersionText = "\("mi_php_version".localized) \(App.phpInstall!.version.long)"
self.addItem(HeaderView.asMenuItem(text: phpVersionText))
addItem(HeaderView.asMenuItem(text: phpVersionText))
}
func addPhpActionMenuItems() {
if App.busy {
self.addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
return
}
@@ -109,6 +109,10 @@ class StatusMenu : NSMenu {
for phpExtension in App.phpInstall!.extensions {
self.addExtensionItem(phpExtension)
}
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r"))
}
private func addExtensionItem(_ phpExtension: PhpExtension) {
@@ -118,6 +122,7 @@ class StatusMenu : NSMenu {
)
menuItem.state = phpExtension.enabled ? .on : .off
menuItem.phpExtension = phpExtension
self.addItem(menuItem)
}
}

View File

@@ -0,0 +1,50 @@
//
// Preferences.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
enum PreferenceName: String {
case shouldDisplayDynamicIcon = "use_dynamic_icon"
}
class Preferences {
static func handleFirstTimeLaunch() {
let launchedBefore = UserDefaults.standard.bool(forKey: "launched_before")
if launchedBefore {
return
}
UserDefaults.standard.setValue(true, forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue)
UserDefaults.standard.setValue(true, forKey: "launched_before")
UserDefaults.standard.synchronize()
print("Saving first-time preferences!")
}
static func retrieve() -> [PreferenceName: Bool] {
Preferences.handleFirstTimeLaunch()
return [
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(
forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue
)
]
}
static var preferences: [PreferenceName: Bool] {
return Preferences.retrieve()
}
static func update(_ preference: PreferenceName, value: Bool) {
UserDefaults.standard.setValue(value, forKey: preference.rawValue)
UserDefaults.standard.synchronize()
}
}

View File

@@ -0,0 +1,51 @@
//
// PrefsVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class PrefsVC: NSViewController {
@IBOutlet weak var buttonDynamicIcon: NSButton!
@IBOutlet weak var labelDynamicIcon: NSTextField!
@IBOutlet weak var buttonClose: NSButton!
public static func show(delegate: NSWindowDelegate? = nil) {
if (App.shared.windowController == nil) {
let vc = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "preferences") as! PrefsVC
let window = NSWindow(contentViewController: vc)
window.title = "prefs.title".localized
window.delegate = delegate
window.styleMask = [.titled, .closable]
App.shared.windowController = NSWindowController(window: window)
}
App.shared.windowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func toggledDynamicIcon(_ sender: Any) {
Preferences.update(.shouldDisplayDynamicIcon, value: buttonDynamicIcon.state == .on)
MainMenu.shared.refreshIcon()
}
override func viewWillAppear() {
buttonDynamicIcon.title = "prefs.dynamic_icon_title".localized
labelDynamicIcon.stringValue = "prefs.dynamic_icon_desc".localized
buttonClose.title = "prefs.close".localized
let prefs = Preferences.preferences
self.buttonDynamicIcon.state = (prefs[.shouldDisplayDynamicIcon] == true) ? .on : .off
}
@IBAction func pressed(_ sender: Any) {
self.view.window?.windowController?.close()
}
deinit {
print("VC deallocated")
}
}

View File

@@ -15,31 +15,52 @@ class Actions {
public static func detectPhpVersions() -> [String]
{
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versions = files.components(separatedBy: "\n")
// Remove all empty strings
versions.removeAll { (string) -> Bool in
return (string == "")
}
// Get a list of versions only
var versionsOnly : [String] = []
versions.forEach { (string) in
versionsOnly.append(string.components(separatedBy: "php@")[1])
}
var versionsOnly = Self.extractPhpVersions(from: files.components(separatedBy: "\n"))
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed
let phpAlias = App.shared.brewPhpVersion
// Avoid inserting a duplicate
if (!versionsOnly.contains(phpAlias)) {
versionsOnly.append(phpAlias);
}
print("The PHP versions that were detected are: \(versionsOnly)")
return versionsOnly
}
/**
Extracts valid PHP versions from an array of strings.
This array of strings is usually retrieved from `grep`.
*/
public static func extractPhpVersions(
from versions: [String],
checkBinaries: Bool = true
) -> [String] {
var output : [String] = []
versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected)
return version.starts(with: "php@")
}.forEach { (string) in
let version = string.components(separatedBy: "php@")[1]
// Only append the version if it doesn't already exist (avoid dupes),
// is supported and where the binary exists (avoids broken installs)
if !output.contains(version)
&& Constants.SupportedPhpVersions.contains(version)
&& (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
{
output.append(version)
}
}
return output
}
// MARK: - Services
public static func restartPhpFpm()
@@ -75,6 +96,7 @@ class Actions {
}
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
brew("link \(formula) --overwrite --force")
brew("services start \(formula)", sudo: true)
}
@@ -110,7 +132,7 @@ class Actions {
{
brew("services restart dnsmasq", sudo: true)
self.detectPhpVersions().forEach { (version) in
detectPhpVersions().forEach { (version) in
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink php@\(version)")
brew("services stop \(formula)")

View File

@@ -23,41 +23,41 @@ class Paths {
if (optBrewFound) {
// This is usually the case with Homebrew installed on Apple Silicon
self.baseDir = .opt
baseDir = .opt
} else if (usrBrewFound) {
// This is usually the case with Homebrew installed on Intel (or Rosetta 2)
self.baseDir = .usr
baseDir = .usr
} else {
// Falling back to default "legacy" Homebrew location (for Intel)
print("Seems like we couldn't determine the Homebrew directory.")
print("This usually means we're in trouble... (no Homebrew?)")
self.baseDir = .usr
baseDir = .usr
}
print("Homebrew directory: \(self.baseDir)")
print("Homebrew directory: \(baseDir)")
}
// - MARK: Binaries
public static var brew: String {
return "\(self.binPath)/brew"
return "\(binPath)/brew"
}
public static var php: String {
return "\(self.binPath)/php"
return "\(binPath)/php"
}
// - MARK: Paths
public static var binPath: String {
return "\(self.shared.baseDir.rawValue)/bin"
return "\(shared.baseDir.rawValue)/bin"
}
public static var optPath: String {
return "\(self.shared.baseDir.rawValue)/opt"
return "\(shared.baseDir.rawValue)/opt"
}
public static var etcPath: String {
return "\(self.shared.baseDir.rawValue)/etc"
return "\(shared.baseDir.rawValue)/etc"
}
}

View File

@@ -29,7 +29,7 @@ class Shell {
.init(majorVersion: 10, minorVersion: 15, patchVersion: 0))
// If macOS Mojave is being used, we'll default to /bin/bash
self.shell = at_least_10_15 ? "/bin/sh" : "/bin/bash"
shell = at_least_10_15 ? "/bin/sh" : "/bin/bash"
print(at_least_10_15 ? "Detected recent macOS (> 10.15): defaulting to /bin/sh"
: "Detected older macOS (< 10.15): so defaulting to /bin/bash")
}
@@ -47,7 +47,7 @@ class Shell {
*/
func run(_ command: String) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = self.pipe(command)
_ = pipe(command)
}
/**

View File

@@ -21,56 +21,61 @@ class Startup {
*/
func checkEnvironment(success: () -> Void, failure: @escaping () -> Void)
{
self.failureCallback = failure
failureCallback = failure
self.performEnvironmentCheck(
performEnvironmentCheck(
!Shell.fileExists("\(Paths.binPath)/php"),
messageText: "startup.errors.php_binary.title".localized,
informativeText: "startup.errors.php_binary_desc".localized,
breaking: true
)
self.performEnvironmentCheck(
performEnvironmentCheck(
!Shell.pipe("ls \(Paths.optPath) | grep php").contains("php"),
messageText: "startup.errors.php_opt.title".localized,
informativeText: "startup.errors.php_opt.desc".localized,
breaking: true
)
self.performEnvironmentCheck(
// Older versions of Valet might be located in `/usr/local/bin` regardless of Homebrew prefix
!(Shell.fileExists("/usr/local/bin/valet") || Shell.fileExists("/opt/homebrew/bin/valet")),
performEnvironmentCheck(
// Check for Valet; it can be symlinked or in .composer/vendor/bin
!(Shell.fileExists("/usr/local/bin/valet")
|| Shell.fileExists("/opt/homebrew/bin/valet")
|| Shell.fileExists("~/.composer/vendor/bin/valet")
),
messageText: "startup.errors.valet_executable.title".localized,
informativeText: "startup.errors.valet_executable.desc".localized,
breaking: true
)
self.performEnvironmentCheck(
performEnvironmentCheck(
!Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"),
messageText: "startup.errors.sudoers_brew.title".localized,
informativeText: "startup.errors.sudoers_brew.desc".localized,
breaking: true
)
self.performEnvironmentCheck(
// Older versions of Valet might be located in `/usr/local/bin` regardless of Homebrew prefix
performEnvironmentCheck(
// Check for Valet; it can be symlinked or in .composer/vendor/bin
!(Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet")
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/opt/homebrew/bin/valet")),
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/opt/homebrew/bin/valet")
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains(".composer/vendor/bin/valet")
),
messageText: "startup.errors.sudoers_valet.title".localized,
informativeText: "startup.errors.sudoers_valet.desc".localized,
breaking: true
)
let services = Shell.pipe("\(Paths.brew) services list | grep php")
self.performEnvironmentCheck(
performEnvironmentCheck(
(services.countInstances(of: "started") > 1),
messageText: "startup.errors.services.title".localized,
informativeText: "startup.errors.services.desc".localized,
breaking: false
)
if (!self.failed) {
self.determineBrewAliasVersion()
if (!failed) {
determineBrewAliasVersion()
success()
}
}
@@ -110,13 +115,13 @@ class Startup {
) {
if (!condition) { return }
self.failed = breaking
failed = breaking
DispatchQueue.main.async {
DispatchQueue.main.async { [self] in
// Present the information to the user
Alert.notify(message: messageText, info: informativeText)
// Only breaking issues will throw the extra retry modal
breaking ? self.failureCallback() : ()
breaking ? failureCallback() : ()
}
}
}

View File

@@ -25,6 +25,7 @@
"mi_restart_specific" = "Restart specific service";
"mi_restart_all_services" = "Restart all services";
"mi_force_load_latest" = "Force load latest PHP version";
"mi_php_refresh" = "Refresh information";
"mi_configuration" = "Configuration";
"mi_limits" = "Limits Configuration";
@@ -38,9 +39,17 @@
"mi_detected_extensions" = "Detected Extensions";
"mi_no_extensions_detected" = "No additional extensions detected.";
"mi_preferences" = "Preferences...";
"mi_quit" = "Quit PHP Monitor";
"mi_about" = "About PHP Monitor";
// PREFERENCES
"prefs.title" = "Preferences";
"prefs.close" = "Close";
"prefs.dynamic_icon_title" = "Show a dynamic icon in the menu bar";
"prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible.\nIf checked, it will display the major version number of the currently linked PHP version.";
// NOTIFICATIONS
"notification.version_changed_title" = "PHP %@ now active";