mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-08 04:20:07 +02:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
015f406ddf | |||
e1a97672b5 | |||
493b5945f9 | |||
52606aae8b | |||
2d6ca0f841 | |||
34900f929f | |||
5dbd05fdfb | |||
fe3cf9adb1 | |||
9bc8460cce | |||
4cbd2fd6eb | |||
6fef3fe37a | |||
72a20d1ed9 | |||
73ed80434a | |||
a78672927b | |||
4256eae442 | |||
76412b68f3 | |||
9153bb140a | |||
c9c15d10f9 | |||
e8c2277ef5 | |||
23720c5dc9 | |||
f881f07cba | |||
b072ee8dec | |||
acfbc0b66f |
@ -18,7 +18,7 @@
|
||||
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
|
||||
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
|
||||
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
|
||||
C41C1B4B22B019FF00E7CF16 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */; };
|
||||
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
|
||||
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
|
||||
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
|
||||
C42295DD2358D02000E263B2 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
|
||||
@ -49,6 +49,10 @@
|
||||
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
|
||||
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
|
||||
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
|
||||
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
|
||||
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
|
||||
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
|
||||
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
|
||||
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
|
||||
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
|
||||
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
|
||||
@ -70,7 +74,7 @@
|
||||
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
|
||||
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
|
||||
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
|
||||
C4F780CC25D80B75000DBC97 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */; };
|
||||
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
|
||||
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 */; };
|
||||
@ -101,7 +105,7 @@
|
||||
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 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = "<group>"; };
|
||||
C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
|
||||
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = "<group>"; };
|
||||
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
|
||||
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = "<group>"; };
|
||||
C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
|
||||
@ -129,6 +133,8 @@
|
||||
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
|
||||
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
|
||||
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = "<group>"; };
|
||||
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
|
||||
C4F7807425D7F7E5000DBC97 /* RELEASE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = RELEASE.md; sourceTree = "<group>"; };
|
||||
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@ -170,6 +176,16 @@
|
||||
path = Preferences;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54B20EDF263AA22C00D3250E /* PHP */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */,
|
||||
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */,
|
||||
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
|
||||
);
|
||||
path = PHP;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C405A4CD24B9B9070062FAFA /* IAP */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -228,10 +244,11 @@
|
||||
C41E181722CB61EB0072CF09 /* Domain */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5420395726135DB800FB00FA /* Preferences */,
|
||||
C4F7808A25D7F918000DBC97 /* Terminal */,
|
||||
C4B13B1D25C4915000548C3A /* Core */,
|
||||
54B20EDF263AA22C00D3250E /* PHP */,
|
||||
C4F7808A25D7F918000DBC97 /* Terminal */,
|
||||
C47331A0247093AC009A0597 /* Menu */,
|
||||
5420395726135DB800FB00FA /* Preferences */,
|
||||
C4811D2822D70D9C00B5F6B3 /* Helpers */,
|
||||
C4F8C0A222D4F100002EFE61 /* Extensions */,
|
||||
);
|
||||
@ -257,6 +274,8 @@
|
||||
C476FF9722B0DD830098105B /* Alert.swift */,
|
||||
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
|
||||
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
|
||||
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */,
|
||||
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@ -266,9 +285,8 @@
|
||||
children = (
|
||||
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
|
||||
C4811D2322D70A4700B5F6B3 /* App.swift */,
|
||||
C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */,
|
||||
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
|
||||
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
|
||||
C4D8016522B1584700C6DA1B /* Startup.swift */,
|
||||
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
@ -294,8 +312,6 @@
|
||||
C49EAB45259FC305007F6C3B /* Paths.swift */,
|
||||
C42295DC2358D02000E263B2 /* Command.swift */,
|
||||
C41C1B4622B009A400E7CF16 /* Shell.swift */,
|
||||
C4D8016522B1584700C6DA1B /* Startup.swift */,
|
||||
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
|
||||
);
|
||||
path = Terminal;
|
||||
sourceTree = "<group>";
|
||||
@ -431,8 +447,10 @@
|
||||
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
|
||||
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
|
||||
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
|
||||
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
|
||||
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */,
|
||||
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */,
|
||||
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */,
|
||||
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
|
||||
C42295DD2358D02000E263B2 /* Command.swift in Sources */,
|
||||
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
|
||||
@ -443,7 +461,7 @@
|
||||
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
|
||||
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
|
||||
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
|
||||
C41C1B4B22B019FF00E7CF16 /* PhpInstallation.swift in Sources */,
|
||||
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
|
||||
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */,
|
||||
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
|
||||
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
|
||||
@ -461,13 +479,14 @@
|
||||
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
|
||||
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
|
||||
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
|
||||
C4F780CC25D80B75000DBC97 /* PhpInstallation.swift in Sources */,
|
||||
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.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 */,
|
||||
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
|
||||
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
|
||||
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */,
|
||||
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
|
||||
@ -477,6 +496,7 @@
|
||||
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
|
||||
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
|
||||
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */,
|
||||
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */,
|
||||
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */,
|
||||
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */,
|
||||
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */,
|
||||
@ -639,7 +659,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 56;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = phpmon/Info.plist;
|
||||
@ -647,7 +667,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.4;
|
||||
MARKETING_VERSION = 4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -663,7 +683,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 56;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = phpmon/Info.plist;
|
||||
@ -671,7 +691,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.4;
|
||||
MARKETING_VERSION = 4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -692,7 +712,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.1;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -713,7 +733,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.1;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
@ -21,7 +21,7 @@ PHP Monitor also gives you quick access to various useful functionality (like ac
|
||||
|
||||
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
|
||||
|
||||
* macOS 10.14 Mojave or higher (works on macOS 11 Big Sur)
|
||||
* macOS 10.14 Mojave or higher (works on macOS 11 Big Sur and macOS 12 Monterey)
|
||||
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
|
||||
* The brew formula `php` has to be installed (which version is detected)
|
||||
* Laravel Valet 2.13 or higher
|
||||
@ -30,7 +30,7 @@ _You may need to update your Valet installation to keep everything working if a
|
||||
|
||||
## 🚀 How to install
|
||||
|
||||
You can install via Homebrew, or may download the latest [release](https://github.com/nicoverbruggen/phpmon/releases).
|
||||
You can install via Homebrew (recommended), or may download the latest release on GitHub.
|
||||
|
||||
To install via Homebrew, run:
|
||||
|
||||
@ -73,6 +73,7 @@ If you're still having issues, here's a few common questions & answers, as well
|
||||
<li>PHP 7.4</li>
|
||||
<li>PHP 8.0</li>
|
||||
<li>PHP 8.1</li>
|
||||
<li>PHP 8.2 (experimental)</li>
|
||||
</ul>
|
||||
|
||||
For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Constants.swift#L16) file to see which versions are supported.
|
||||
@ -274,7 +275,7 @@ Thank you very much for your contributions, kind words and support.
|
||||
|
||||
### Loading info about PHP in the background
|
||||
|
||||
This utility runs `php-config --version'` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
|
||||
This utility runs `php-config --version` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
|
||||
|
||||
In order to save power, this only happens once every 60 seconds.
|
||||
|
||||
@ -301,7 +302,7 @@ This app isn't very complicated after all. In the end, this just (conveniently)
|
||||
|
||||
## 🔧 Build instructions
|
||||
|
||||
<img src="./docs/build.png" width="320px" alt="build button in Xcode"/>
|
||||
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
|
||||
|
||||
If you'd like to build PHP Monitor yourself, you need:
|
||||
|
||||
|
17
SECURITY.md
17
SECURITY.md
@ -4,13 +4,16 @@
|
||||
|
||||
Generally speaking, only the latest version of **PHP Monitor** is supported:
|
||||
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- |
|
||||
| 3.x | ✅ Universal binary | ✅ | Big Sur (11.0) | macOS 10.14+ |
|
||||
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ |
|
||||
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
|
||||
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
|
||||
| < 2.4 | ❌ Intel binary<br/>`/usr/local/homebrew` installations only | ❌ | Catalina (10.15) | macOS 10.14+ |
|
||||
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- |
|
||||
| 4.0 | ✅ Universal binary | ✅ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 |
|
||||
| 3.5 | ✅ Universal binary | ✅ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 |
|
||||
| 3.5 | ✅ Universal binary | ✅ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 |
|
||||
| 3.0—3.4 | ✅ Universal binary | ✅ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 |
|
||||
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 |
|
||||
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable |
|
||||
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable |
|
||||
| < 2.4 | ❌ Intel binary<br/>`/usr/local/homebrew` installations only | ❌ | Catalina (10.15) | macOS 10.14+ | not applicable |
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
|
BIN
docs/build.png
BIN
docs/build.png
Binary file not shown.
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 18 KiB |
@ -23,6 +23,9 @@ class BrewJsonParserTest: XCTestCase {
|
||||
XCTAssertEqual(package.name, "php")
|
||||
XCTAssertEqual(package.full_name, "php")
|
||||
XCTAssertEqual(package.aliases.first!, "php@8.0")
|
||||
XCTAssertEqual(package.installed.contains(where: { installed in
|
||||
installed.version.starts(with: "8.0")
|
||||
}), true)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,11 +27,16 @@ class ExtensionParserTest: XCTestCase {
|
||||
return ext.name
|
||||
}
|
||||
|
||||
// These 6 should be found
|
||||
XCTAssertTrue(extensionNames.contains("xdebug"))
|
||||
XCTAssertTrue(extensionNames.contains("imagick"))
|
||||
XCTAssertTrue(extensionNames.contains("sodium-next"))
|
||||
XCTAssertTrue(extensionNames.contains("opcache"))
|
||||
XCTAssertTrue(extensionNames.contains("yaml"))
|
||||
XCTAssertTrue(extensionNames.contains("custom"))
|
||||
|
||||
XCTAssertFalse(extensionNames.contains("fake"))
|
||||
XCTAssertFalse(extensionNames.contains("nice"))
|
||||
}
|
||||
|
||||
func testExtensionStatusIsCorrect() throws {
|
||||
@ -47,7 +52,7 @@ class ExtensionParserTest: XCTestCase {
|
||||
func testToggleWorksAsExpected() throws {
|
||||
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
||||
let extensions = PhpExtension.load(from: destination)
|
||||
XCTAssertEqual(extensions.count, 4)
|
||||
XCTAssertEqual(extensions.count, 6)
|
||||
|
||||
// Try to disable xdebug (should be detected first)!
|
||||
let xdebug = extensions.first!
|
||||
|
@ -1,8 +1,16 @@
|
||||
# These should be detected
|
||||
|
||||
zend_extension="xdebug.so"
|
||||
; zend_extension="imagick.so"
|
||||
zend_extension=/opt/homebrew/opt/php/lib/php/20200930/opcache.so
|
||||
zend_extension="/opt/homebrew/opt/php/lib/php/20200930/yaml.so"
|
||||
#zend_extension="/opt/homebrew/opt/php/lib/php/20200930/fake.so"
|
||||
;zend_extension="sodium-next.so"
|
||||
extension = custom.so
|
||||
|
||||
# These should not be detected
|
||||
|
||||
#zend_extension="/opt/homebrew/opt/php/lib/php/20200930/commented.so"
|
||||
hextension = nice.so
|
||||
|
||||
[PHP]
|
||||
|
||||
|
@ -18,25 +18,25 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
|
||||
(invoked by PHP Monitor) shell commands. It is used to
|
||||
invoke all commands in this application.
|
||||
*/
|
||||
let sharedShell : Shell
|
||||
let sharedShell: Shell
|
||||
|
||||
/**
|
||||
The App singleton contains information about the state of
|
||||
the application and global variables.
|
||||
*/
|
||||
let state : App
|
||||
let state: App
|
||||
|
||||
/**
|
||||
The MainMenu singleton is responsible for rendering the
|
||||
menu bar item and its menu, as well as its actions.
|
||||
*/
|
||||
let menu : MainMenu
|
||||
let menu: MainMenu
|
||||
|
||||
/**
|
||||
The paths singleton that determines where Homebrew is installed,
|
||||
and where to look for binaries.
|
||||
*/
|
||||
let paths : Paths
|
||||
let paths: Paths
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
@ -77,4 +77,5 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
|
||||
) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,11 +9,21 @@ import Cocoa
|
||||
|
||||
class Constants {
|
||||
|
||||
/**
|
||||
* The latest PHP version that is considered to be stable at the time of release.
|
||||
* This version number is currently not used (only as a default fallback).
|
||||
*/
|
||||
static let LatestStablePhpVersion = "8.1"
|
||||
|
||||
/**
|
||||
* The PHP versions supported by this application.
|
||||
* Versions that do not appear in this array are omitted from the list.
|
||||
*/
|
||||
static let SupportedPhpVersions = [
|
||||
// ====================
|
||||
// STABLE RELEASES
|
||||
// ====================
|
||||
// Versions of PHP that are stable and are supported.
|
||||
"5.6",
|
||||
"7.0",
|
||||
"7.1",
|
||||
@ -21,7 +31,16 @@ class Constants {
|
||||
"7.3",
|
||||
"7.4",
|
||||
"8.0",
|
||||
"8.1"
|
||||
"8.1",
|
||||
|
||||
// ====================
|
||||
// EXPERIMENTAL SUPPORT
|
||||
// ====================
|
||||
// Every release that supports the next release will always support the next
|
||||
// dev release. In this case, that means that the version below is detected.
|
||||
"8.2"
|
||||
]
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -29,9 +29,26 @@ class Actions {
|
||||
|
||||
print("The PHP versions that were detected are: \(versionsOnly)")
|
||||
|
||||
App.shared.availablePhpVersions = versionsOnly
|
||||
Actions.extractPhpLongVersions()
|
||||
|
||||
return versionsOnly
|
||||
}
|
||||
|
||||
/**
|
||||
This method extracts the PHP full version number after finding the php installation folders.
|
||||
To be refactored at some later point, I'd like to cache the `PhpInstallation` objects instead of just the version number at some point.
|
||||
*/
|
||||
public static func extractPhpLongVersions()
|
||||
{
|
||||
var mappedVersions: [String: PhpInstallation] = [:]
|
||||
App.shared.availablePhpVersions.forEach { version in
|
||||
mappedVersions[version] = PhpInstallation(version)
|
||||
}
|
||||
|
||||
App.shared.cachedPhpInstallations = mappedVersions
|
||||
}
|
||||
|
||||
/**
|
||||
Extracts valid PHP versions from an array of strings.
|
||||
This array of strings is usually retrieved from `grep`.
|
||||
@ -201,9 +218,14 @@ class Actions {
|
||||
{
|
||||
// Escape slashes (or `sed` won't work)
|
||||
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
|
||||
let e_replacment = replacement.replacingOccurrences(of: "/", with: "\\/")
|
||||
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
|
||||
|
||||
Shell.run("sed -i '' 's/\(e_original)/\(e_replacment)/g' \(file)")
|
||||
// Check if gsed exists; it is able to follow symlinks, which we want to do to toggle the extension
|
||||
if Shell.fileExists("\(Paths.binPath)/gsed") {
|
||||
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
} else {
|
||||
Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -217,4 +239,5 @@ class Actions {
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.contains("YES")
|
||||
}
|
||||
|
||||
}
|
@ -16,10 +16,12 @@ class App {
|
||||
loadGlobalHotkey()
|
||||
}
|
||||
|
||||
static var phpInstall: PhpInstallation? {
|
||||
/** Information about the currently linked PHP installation. */
|
||||
static var phpInstall: ActivePhpInstallation? {
|
||||
return App.shared.currentInstall
|
||||
}
|
||||
|
||||
/** Whether the app is busy doing something. Used to determine what UI to display. */
|
||||
static var busy: Bool {
|
||||
return App.shared.busy
|
||||
}
|
||||
@ -40,13 +42,18 @@ class App {
|
||||
/**
|
||||
The currently active installation of PHP.
|
||||
*/
|
||||
var currentInstall: PhpInstallation? = nil
|
||||
var currentInstall: ActivePhpInstallation? = nil
|
||||
|
||||
/**
|
||||
All available versions of PHP.
|
||||
*/
|
||||
var availablePhpVersions : [String] = []
|
||||
|
||||
/**
|
||||
Cached information about the PHP installations; contains only the full version number at this point.
|
||||
*/
|
||||
var cachedPhpInstallations : [String: PhpInstallation] = [:]
|
||||
|
||||
/**
|
||||
The timer that will periodically fetch the PHP version that is currently active.
|
||||
*/
|
||||
@ -55,7 +62,7 @@ class App {
|
||||
/**
|
||||
Information we were able to discern from the Homebrew info command (as JSON).
|
||||
*/
|
||||
var brewPhpPackage: HomebrewPackage? = nil {
|
||||
var brewPhpPackage: HomebrewPackage! = nil {
|
||||
didSet {
|
||||
brewPhpVersion = brewPhpPackage!.version
|
||||
}
|
||||
@ -67,10 +74,10 @@ class App {
|
||||
If you're up to date, `php` will be aliased to the latest version,
|
||||
but that might not be the case.
|
||||
|
||||
We'll technically default to version 8.0, but the information should always be loaded
|
||||
from Homebrew itself upon starting the application.
|
||||
We'll technically default to the version in Constants.swift, but the information
|
||||
should always be loaded from Homebrew itself upon startup.
|
||||
*/
|
||||
var brewPhpVersion: String = "8.0"
|
||||
var brewPhpVersion: String = Constants.LatestStablePhpVersion
|
||||
|
||||
/**
|
||||
The shortcut the user has requested.
|
||||
@ -83,29 +90,38 @@ class App {
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
/**
|
||||
On startup, the preferences should be loaded from the .plist, and we'll enable the shortcut if it is set.
|
||||
*/
|
||||
private func loadGlobalHotkey() {
|
||||
let hotkey = Preferences.preferences[.globalHotkey] as! String?
|
||||
if hotkey == nil {
|
||||
// Make sure we can retrieve the hotkey from preferences; if we cannot, no hotkey is set
|
||||
guard let hotkey = Preferences.preferences[.globalHotkey] as? String else {
|
||||
print("No global hotkey loaded")
|
||||
return
|
||||
}
|
||||
|
||||
let keybindPref = GlobalKeybindPreference.fromJson(hotkey!)
|
||||
|
||||
if (keybindPref != nil) {
|
||||
self.shortcutHotkey = HotKey(keyCombo: KeyCombo(
|
||||
carbonKeyCode: keybindPref!.keyCode,
|
||||
carbonModifiers: keybindPref!.carbonFlags
|
||||
))
|
||||
} else {
|
||||
// Make sure we can parse the JSON into the desired format; if we cannot, no hotkey is set
|
||||
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
|
||||
print("No global hotkey loaded, could not be parsed!")
|
||||
self.shortcutHotkey = nil
|
||||
return
|
||||
}
|
||||
|
||||
self.shortcutHotkey = HotKey(keyCombo: KeyCombo(
|
||||
carbonKeyCode: keybindPref.keyCode,
|
||||
carbonModifiers: keybindPref.carbonFlags
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
Sets up the action that needs to occur when the shortcut key is pressed (open the menu).
|
||||
*/
|
||||
private func setupGlobalHotkeyListener() {
|
||||
guard let hotKey = self.shortcutHotkey else {
|
||||
guard let hotkey = self.shortcutHotkey else {
|
||||
return
|
||||
}
|
||||
hotKey.keyDownHandler = {
|
||||
|
||||
hotkey.keyDownHandler = {
|
||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19455"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@ -84,8 +84,8 @@
|
||||
<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" wantsLayer="YES" id="Pf1-A5-3Xz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="574" height="189"/>
|
||||
<view key="view" wantsLayer="YES" misplaced="YES" id="Pf1-A5-3Xz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="574" height="311"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="GSr-K5-3yw">
|
||||
@ -102,7 +102,7 @@ Gw
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MEf-MN-oXt">
|
||||
<rect key="frame" x="148" y="152" width="406" height="18"/>
|
||||
<rect key="frame" x="148" y="274" width="406" 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"/>
|
||||
@ -112,7 +112,7 @@ Gw
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JrH-aa-AzL">
|
||||
<rect key="frame" x="148" y="131" width="408" height="14"/>
|
||||
<rect key="frame" x="148" y="253" width="408" height="14"/>
|
||||
<textFieldCell key="cell" title="DYN_ICON_DESC" id="MHA-Xt-xgF">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
@ -150,7 +150,7 @@ Gw
|
||||
<rect key="frame" x="325" y="75" width="138" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="CLEAR_SHORTCUT" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nvE-5d-VOS">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system" size="11"/>
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="unregister:" target="AW2-rV-rbS" id="2RI-4w-6Td"/>
|
||||
@ -165,7 +165,7 @@ Gw
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="31d-gd-auR">
|
||||
<rect key="frame" x="18" y="153" width="124" height="16"/>
|
||||
<rect key="frame" x="18" y="275" width="124" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="120" id="8dt-Pg-wFI"/>
|
||||
</constraints>
|
||||
@ -183,44 +183,106 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="vSc-oQ-NC5">
|
||||
<rect key="frame" x="148" y="220" width="121" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="FULL_PHP_VER" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="eCd-ja-EwE">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="toggledFullPhpVersion:" target="AW2-rV-rbS" id="RCY-Ah-sLM"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="t24-LR-wKz">
|
||||
<rect key="frame" x="148" y="199" width="123" height="14"/>
|
||||
<textFieldCell key="cell" title="FULL_PHP_VER_DESC" id="8gG-qs-mHR">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ogC-wz-ZfO">
|
||||
<rect key="frame" x="18" y="153" width="124" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="120" id="i9O-6m-Gr9"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="PREF_SERVICES:" id="bm4-rf-kCF">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="47u-9B-eDu">
|
||||
<rect key="frame" x="148" y="152" width="126" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="AUTO_RESTART" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="n1d-l4-inL">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="toggledAutoRestartServices:" target="AW2-rV-rbS" id="THn-nu-IiJ"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ObP-GE-ejZ">
|
||||
<rect key="frame" x="148" y="131" width="126" height="14"/>
|
||||
<textFieldCell key="cell" title="AUTO_RESTART_DESC" id="F9P-iQ-gBk">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ogC-wz-ZfO" firstAttribute="trailing" secondItem="31d-gd-auR" secondAttribute="trailing" id="2Lr-Ht-qKI"/>
|
||||
<constraint firstItem="t24-LR-wKz" firstAttribute="leading" secondItem="vSc-oQ-NC5" secondAttribute="leading" id="3tK-kp-q5R"/>
|
||||
<constraint firstItem="t24-LR-wKz" firstAttribute="top" secondItem="vSc-oQ-NC5" secondAttribute="bottom" constant="8" symbolic="YES" id="4Ft-lN-vwA"/>
|
||||
<constraint firstAttribute="trailing" secondItem="JrH-aa-AzL" secondAttribute="trailing" constant="20" symbolic="YES" id="8iM-Xf-ShU"/>
|
||||
<constraint firstItem="ObP-GE-ejZ" firstAttribute="leading" secondItem="47u-9B-eDu" secondAttribute="leading" id="ASF-WR-A3X"/>
|
||||
<constraint firstAttribute="trailing" secondItem="GSr-K5-3yw" secondAttribute="trailing" constant="20" symbolic="YES" id="AT9-5F-6g1"/>
|
||||
<constraint firstItem="YsQ-AZ-Aei" firstAttribute="leading" secondItem="V7b-jv-oCB" secondAttribute="trailing" constant="12" symbolic="YES" id="Bk6-4V-GLk"/>
|
||||
<constraint firstItem="31d-gd-auR" firstAttribute="top" secondItem="Pf1-A5-3Xz" secondAttribute="top" constant="20" symbolic="YES" id="C3K-NX-BBl"/>
|
||||
<constraint firstItem="YsQ-AZ-Aei" firstAttribute="top" secondItem="V7b-jv-oCB" secondAttribute="top" id="DY5-za-saX"/>
|
||||
<constraint firstItem="vSc-oQ-NC5" firstAttribute="leading" secondItem="JrH-aa-AzL" secondAttribute="leading" id="FVa-vu-VGJ"/>
|
||||
<constraint firstItem="MEf-MN-oXt" firstAttribute="leading" secondItem="31d-gd-auR" secondAttribute="trailing" constant="10" id="G5S-JV-re3"/>
|
||||
<constraint firstItem="V7b-jv-oCB" firstAttribute="firstBaseline" secondItem="5ZK-BG-o1t" secondAttribute="firstBaseline" id="H5D-2D-DLH"/>
|
||||
<constraint firstItem="1TO-9H-z2d" firstAttribute="leading" secondItem="V7b-jv-oCB" secondAttribute="leading" id="Imk-o0-2fS"/>
|
||||
<constraint firstItem="ObP-GE-ejZ" firstAttribute="top" secondItem="47u-9B-eDu" secondAttribute="bottom" constant="8" symbolic="YES" id="JqR-Jd-SoR"/>
|
||||
<constraint firstItem="JrH-aa-AzL" firstAttribute="leading" secondItem="MEf-MN-oXt" secondAttribute="leading" id="K2H-Af-2qK"/>
|
||||
<constraint firstItem="5ZK-BG-o1t" firstAttribute="top" secondItem="JrH-aa-AzL" secondAttribute="bottom" constant="30" id="NMk-yt-fha"/>
|
||||
<constraint firstItem="5ZK-BG-o1t" firstAttribute="top" secondItem="ObP-GE-ejZ" secondAttribute="bottom" constant="30" id="LO4-8j-ihp"/>
|
||||
<constraint firstItem="47u-9B-eDu" firstAttribute="top" secondItem="ogC-wz-ZfO" secondAttribute="top" id="T9j-v2-fSW"/>
|
||||
<constraint firstItem="JrH-aa-AzL" firstAttribute="top" secondItem="MEf-MN-oXt" secondAttribute="bottom" constant="8" symbolic="YES" id="Vf8-fx-H50"/>
|
||||
<constraint firstItem="MEf-MN-oXt" firstAttribute="firstBaseline" secondItem="31d-gd-auR" secondAttribute="firstBaseline" id="W36-bE-iAT"/>
|
||||
<constraint firstItem="1TO-9H-z2d" firstAttribute="firstBaseline" secondItem="V7b-jv-oCB" secondAttribute="baseline" constant="25" id="bJG-ed-pch"/>
|
||||
<constraint firstItem="V7b-jv-oCB" firstAttribute="leading" secondItem="JrH-aa-AzL" secondAttribute="leading" id="bUY-uH-N7A"/>
|
||||
<constraint firstItem="5ZK-BG-o1t" firstAttribute="trailing" secondItem="31d-gd-auR" secondAttribute="trailing" id="c4g-jO-JUm"/>
|
||||
<constraint firstAttribute="bottom" secondItem="GSr-K5-3yw" secondAttribute="bottom" constant="20" symbolic="YES" id="dAS-yW-vua"/>
|
||||
<constraint firstItem="vSc-oQ-NC5" firstAttribute="top" secondItem="JrH-aa-AzL" secondAttribute="bottom" constant="16" id="hQf-4s-iHn"/>
|
||||
<constraint firstItem="GSr-K5-3yw" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="mTE-WD-54L"/>
|
||||
<constraint firstItem="47u-9B-eDu" firstAttribute="leading" secondItem="MEf-MN-oXt" secondAttribute="leading" id="n8B-C8-dXs"/>
|
||||
<constraint firstItem="31d-gd-auR" firstAttribute="leading" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="o0J-yT-TDX"/>
|
||||
<constraint firstItem="ogC-wz-ZfO" firstAttribute="top" secondItem="t24-LR-wKz" secondAttribute="bottom" constant="30" id="oXh-LE-sRS"/>
|
||||
<constraint firstAttribute="trailing" secondItem="MEf-MN-oXt" secondAttribute="trailing" constant="20" symbolic="YES" id="pJg-zj-cBs"/>
|
||||
<constraint firstItem="GSr-K5-3yw" firstAttribute="top" secondItem="1TO-9H-z2d" secondAttribute="bottom" constant="20" id="pMZ-Gx-Jmm"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="buttonAutoRestartServices" destination="47u-9B-eDu" id="kyg-BX-PQK"/>
|
||||
<outlet property="buttonClearShortcut" destination="YsQ-AZ-Aei" id="1xo-hk-HgM"/>
|
||||
<outlet property="buttonClose" destination="GSr-K5-3yw" id="d4I-Cf-gXD"/>
|
||||
<outlet property="buttonDisplayFullPhpVersion" destination="vSc-oQ-NC5" id="ZLa-Vf-4Dq"/>
|
||||
<outlet property="buttonDynamicIcon" destination="MEf-MN-oXt" id="qEN-Vg-EZS"/>
|
||||
<outlet property="buttonSetShortcut" destination="V7b-jv-oCB" id="2aS-S4-cKR"/>
|
||||
<outlet property="labelAutoRestartServices" destination="ObP-GE-ejZ" id="uwY-D7-Uve"/>
|
||||
<outlet property="labelDisplayFullPhpVersion" destination="t24-LR-wKz" id="wYj-Z0-a3h"/>
|
||||
<outlet property="labelDynamicIcon" destination="JrH-aa-AzL" id="CFc-fF-oPq"/>
|
||||
<outlet property="labelShortcut" destination="1TO-9H-z2d" id="paF-hK-78x"/>
|
||||
<outlet property="leftLabelDynamicIcon" destination="31d-gd-auR" id="ANZ-Zs-4d7"/>
|
||||
<outlet property="leftLabelGlobalShortcut" destination="5ZK-BG-o1t" id="73E-9i-cg8"/>
|
||||
<outlet property="leftLabelServices" destination="ogC-wz-ZfO" id="BYx-Gv-N1p"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="264" y="399.5"/>
|
||||
<point key="canvasLocation" x="264" y="457"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
@ -119,9 +119,14 @@ class Startup {
|
||||
|
||||
DispatchQueue.main.async { [self] in
|
||||
// Present the information to the user
|
||||
Alert.notify(message: messageText, info: informativeText)
|
||||
Alert.notify(
|
||||
message: messageText,
|
||||
info: informativeText,
|
||||
style: breaking ? .critical : .warning
|
||||
)
|
||||
// Only breaking issues will throw the extra retry modal
|
||||
breaking ? failureCallback() : ()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -7,11 +7,12 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension Date
|
||||
{
|
||||
extension Date {
|
||||
|
||||
func toString() -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return dateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -33,4 +33,5 @@ extension String {
|
||||
let end = r.upperBound
|
||||
return String(self[start ..< end])
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,17 +7,19 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Cocoa
|
||||
|
||||
// Adapted from: https://stackoverflow.com/a/46268778
|
||||
|
||||
protocol XibLoadable {
|
||||
|
||||
static var xibName: String? { get }
|
||||
static func createFromXib(in bundle: Bundle) -> Self?
|
||||
|
||||
}
|
||||
|
||||
extension XibLoadable where Self: NSView {
|
||||
|
||||
static var xibName: String? {
|
||||
return String(describing: Self.self)
|
||||
}
|
||||
@ -30,4 +32,5 @@ extension XibLoadable where Self: NSView {
|
||||
let views = Array<Any>(results).filter { $0 is Self }
|
||||
return views.last as? Self
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,9 +13,11 @@ class Alert {
|
||||
messageText: String,
|
||||
informativeText: String,
|
||||
buttonTitle: String = "OK",
|
||||
secondButtonTitle: String = ""
|
||||
secondButtonTitle: String = "",
|
||||
style: NSAlert.Style = .informational
|
||||
) -> Bool {
|
||||
let alert = NSAlert.init()
|
||||
alert.alertStyle = style
|
||||
alert.messageText = messageText
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: buttonTitle)
|
||||
@ -25,8 +27,8 @@ class Alert {
|
||||
return alert.runModal() == .alertFirstButtonReturn
|
||||
}
|
||||
|
||||
public static func notify(message: String, info: String) {
|
||||
_ = self.present(messageText: message, informativeText: info, buttonTitle: "OK", secondButtonTitle: "")
|
||||
public static func notify(message: String, info: String, style: NSAlert.Style = .informational) {
|
||||
_ = self.present(messageText: message, informativeText: info, buttonTitle: "OK", secondButtonTitle: "", style: style)
|
||||
}
|
||||
|
||||
}
|
||||
|
70
phpmon/Domain/Helpers/HomebrewDiagnostics.swift
Normal file
70
phpmon/Domain/Helpers/HomebrewDiagnostics.swift
Normal file
@ -0,0 +1,70 @@
|
||||
//
|
||||
// AliasConflict.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 28/11/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class HomebrewDiagnostics {
|
||||
|
||||
enum Errors: String {
|
||||
case aliasConflict = "alias_conflict"
|
||||
}
|
||||
|
||||
static let shared = HomebrewDiagnostics()
|
||||
var errors: [HomebrewDiagnostics.Errors] = []
|
||||
|
||||
init() {
|
||||
if self.determineAliasConflicts() {
|
||||
self.errors.append(.aliasConflict)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||
This will break all linking functionality in PHP Monitor, and the user needs to be informed of this.
|
||||
|
||||
This check only needs to be performed if the `shivammathur/php` tap is active.
|
||||
*/
|
||||
public func determineAliasConflicts() -> Bool
|
||||
{
|
||||
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
|
||||
|
||||
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
|
||||
print("The user does not appear to have tapped: shivammathur/php")
|
||||
return false
|
||||
} else {
|
||||
print("The user DOES have the following tapped: shivammathur/php")
|
||||
print("Checking for `php` formula conflicts...")
|
||||
|
||||
let tapPhp = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self,
|
||||
from: tapAlias.data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
if tapPhp.version != App.shared.brewPhpVersion {
|
||||
print("The `php` formula alias seems to be the different between the tap and core. This could be a problem!")
|
||||
print("Determining whether both of these versions are installed...")
|
||||
|
||||
let bothInstalled = App.shared.availablePhpVersions.contains(tapPhp.version)
|
||||
&& App.shared.availablePhpVersions.contains(App.shared.brewPhpVersion)
|
||||
|
||||
if bothInstalled {
|
||||
print("Both conflicting aliases seem to be installed, warning the user!")
|
||||
} else {
|
||||
print("Conflicting aliases are not both installed, seems fine!")
|
||||
}
|
||||
|
||||
return bothInstalled
|
||||
}
|
||||
|
||||
print("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -7,12 +7,23 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct HomebrewPackage : Decodable {
|
||||
struct HomebrewPackage: Decodable {
|
||||
|
||||
let name: String
|
||||
let full_name: String
|
||||
let aliases: [String]
|
||||
let installed: [HomebrewInstalled]
|
||||
let linked_keg: String?
|
||||
|
||||
public var version: String {
|
||||
return aliases.first!.replacingOccurrences(of: "php@", with: "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct HomebrewInstalled: Decodable {
|
||||
let version: String
|
||||
let built_as_bottle: Bool
|
||||
let installed_as_dependency: Bool
|
||||
let installed_on_request: Bool
|
||||
}
|
@ -10,6 +10,7 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class HeaderView: NSView, XibLoadable {
|
||||
|
||||
@IBOutlet weak var textField: NSTextField!
|
||||
|
||||
static func asMenuItem(text: String) -> NSMenuItem {
|
||||
@ -20,4 +21,5 @@ class HeaderView: NSView, XibLoadable {
|
||||
item.target = self
|
||||
return item
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -41,7 +41,18 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
|
||||
When the environment is all clear and the app can run, let's go.
|
||||
*/
|
||||
private func onEnvironmentPass() {
|
||||
App.shared.availablePhpVersions = Actions.detectPhpVersions()
|
||||
_ = Actions.detectPhpVersions()
|
||||
|
||||
if HomebrewDiagnostics.shared.errors.contains(.aliasConflict) {
|
||||
DispatchQueue.main.async {
|
||||
Alert.notify(
|
||||
message: "alert.php_alias_conflict.title".localized,
|
||||
info: "alert.php_alias_conflict.info".localized,
|
||||
style: .critical
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
updatePhpVersionInStatusBar()
|
||||
|
||||
let installation = App.phpInstall!
|
||||
@ -165,7 +176,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
|
||||
// MARK: - User Interface
|
||||
|
||||
@objc func updatePhpVersionInStatusBar() {
|
||||
App.shared.currentInstall = PhpInstallation()
|
||||
App.shared.currentInstall = ActivePhpInstallation()
|
||||
refreshIcon()
|
||||
update()
|
||||
}
|
||||
@ -180,7 +191,8 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
||||
} else {
|
||||
// The dynamic icon has been requested
|
||||
setStatusBarImage(version: App.phpInstall!.version.short)
|
||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||
setStatusBarImage(version: long ? App.phpInstall!.version.long : App.phpInstall!.version.short)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,6 +268,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
|
||||
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
||||
waitAndExecute {
|
||||
sender.phpExtension?.toggle()
|
||||
|
||||
if Preferences.preferences[.autoServiceRestartAfterExtensionToggle] as! Bool == true {
|
||||
Actions.restartPhpFpm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class StatsView: NSView, XibLoadable {
|
||||
|
||||
@IBOutlet weak var titleMemLimit: NSTextField!
|
||||
@IBOutlet weak var titleMaxPost: NSTextField!
|
||||
@IBOutlet weak var titleMaxUpload: NSTextField!
|
||||
@ -31,4 +32,5 @@ class StatsView: NSView, XibLoadable {
|
||||
item.target = self
|
||||
return item
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -42,15 +42,24 @@ class StatusMenu : NSMenu {
|
||||
private func addSwitchToPhpMenuItems() {
|
||||
var shortcutKey = 1
|
||||
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
|
||||
let version = App.shared.availablePhpVersions[index]
|
||||
|
||||
// Get the short and long version
|
||||
let shortVersion = App.shared.availablePhpVersions[index]
|
||||
let longVersion = App.shared.cachedPhpInstallations[shortVersion]!.longVersion
|
||||
|
||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||
let versionString = long ? longVersion : shortVersion
|
||||
|
||||
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
||||
let brew = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
let brew = (shortVersion == App.shared.brewPhpVersion) ? "php" : "php@\(shortVersion)"
|
||||
let menuItem = PhpMenuItem(
|
||||
title: "\("mi_php_switch".localized) \(version) (\(brew))",
|
||||
action: (version == App.phpInstall?.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
|
||||
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
|
||||
action: (shortVersion == App.phpInstall?.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
|
||||
)
|
||||
menuItem.version = version
|
||||
|
||||
menuItem.version = shortVersion
|
||||
shortcutKey = shortcutKey + 1
|
||||
|
||||
self.addItem(menuItem)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// PhpInstallation.swift
|
||||
// ActivePhpInstallation.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
@ -7,7 +7,15 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpInstallation {
|
||||
/**
|
||||
An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory.
|
||||
|
||||
When initialized, that version's .ini files are also scanned (for active or inactive extensions).
|
||||
Integrity checks can be performed to determine whether PHP-FPM is configured correctly.
|
||||
|
||||
- Note: Each installation has a separate version number. Using `version.short` is advisable if you want to interact with Homebrew.
|
||||
*/
|
||||
class ActivePhpInstallation {
|
||||
|
||||
var version: Version!
|
||||
var configuration: Configuration!
|
||||
@ -111,17 +119,33 @@ class PhpInstallation {
|
||||
return (match == nil) ? "⚠️" : "\(value)B"
|
||||
}
|
||||
|
||||
/**
|
||||
It is always possible that the system configuration for PHP-FPM has not been set up for Valet.
|
||||
This can occur when a user manually installs a new PHP version, but does not run `valet install`.
|
||||
In that case, we should alert the user!
|
||||
|
||||
- Important: The underlying check is `checkPhpFpmStatus`, which can be run multiple times.
|
||||
This method actively presents a modal if said checks fails, so don't call this method too many times.
|
||||
*/
|
||||
public func notifyAboutBrokenPhpFpm() {
|
||||
if !self.checkPhpFpmStatus() {
|
||||
DispatchQueue.main.async {
|
||||
Alert.notify(
|
||||
message: "alert.php_fpm_broken.title".localized,
|
||||
info: "alert.php_fpm_broken.info".localized
|
||||
info: "alert.php_fpm_broken.info".localized,
|
||||
style: .critical
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Determine if PHP-FPM is configured correctly.
|
||||
|
||||
For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent
|
||||
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
|
||||
that means that Valet won't work properly.
|
||||
*/
|
||||
private func checkPhpFpmStatus() -> Bool {
|
||||
if self.version.short == "5.6" {
|
||||
// The main PHP config file should contain `valet.sock` and then we're probably fine?
|
||||
@ -146,4 +170,5 @@ class PhpInstallation {
|
||||
var upload_max_filesize = "???"
|
||||
var post_max_size = "???"
|
||||
}
|
||||
|
||||
}
|
@ -46,7 +46,7 @@ class PhpExtension {
|
||||
|
||||
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
||||
*/
|
||||
static let extensionRegex = #"^(extension=|zend_extension=|; extension=|; zend_extension=)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
||||
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
||||
|
||||
/**
|
||||
When registering an extension, we do that based on the line found inside the .ini file.
|
||||
@ -61,6 +61,7 @@ class PhpExtension {
|
||||
let fullPath = String(line[range])
|
||||
.replacingOccurrences(of: "\"", with: "") // replace excess "
|
||||
.replacingOccurrences(of: ".so", with: "") // replace excess .so
|
||||
|
||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||
|
||||
self.enabled = !line.contains(";")
|
||||
@ -71,12 +72,15 @@ class PhpExtension {
|
||||
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply.
|
||||
*/
|
||||
func toggle() {
|
||||
Actions.sed(
|
||||
file: file,
|
||||
original: line,
|
||||
replacement: enabled ? "; \(line)" : line.replacingOccurrences(of: "; ", with: "")
|
||||
)
|
||||
enabled = !enabled
|
||||
let newLine = enabled
|
||||
// DISABLED: Commented out line
|
||||
? "; \(line)"
|
||||
// ENABLED: Line where the comment delimiter (;) is removed
|
||||
: line.replacingOccurrences(of: "; ", with: "")
|
||||
|
||||
Actions.sed(file: file, original: line, replacement: newLine)
|
||||
|
||||
enabled.toggle()
|
||||
}
|
||||
|
||||
// MARK: - Static Methods
|
||||
@ -93,11 +97,12 @@ class PhpExtension {
|
||||
}
|
||||
|
||||
return file!.components(separatedBy: "\n")
|
||||
.filter({ (line) -> Bool in
|
||||
return line.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||
})
|
||||
.map { (line) -> PhpExtension in
|
||||
return PhpExtension(line, file: path.path)
|
||||
.filter {
|
||||
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||
}
|
||||
.map {
|
||||
return PhpExtension($0, file: path.path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
33
phpmon/Domain/PHP/PhpInstallation.swift
Normal file
33
phpmon/Domain/PHP/PhpInstallation.swift
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// BrewPhpInstallation.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 28/11/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpInstallation {
|
||||
|
||||
var longVersion: String
|
||||
var homebrewInfo: HomebrewPackage
|
||||
|
||||
init(_ version: String) {
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
||||
self.longVersion = version
|
||||
if Shell.fileExists(phpConfigExecutablePath) {
|
||||
self.longVersion = Command.execute(
|
||||
path: phpConfigExecutablePath,
|
||||
arguments: ["--version"]
|
||||
)
|
||||
}
|
||||
|
||||
let info = Shell.pipe("\(Paths.brew) info php@\(version) --json")
|
||||
self.homebrewInfo = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self,
|
||||
from: info.data(using: .utf8)!
|
||||
).first!
|
||||
}
|
||||
|
||||
}
|
@ -9,39 +9,71 @@
|
||||
import Foundation
|
||||
|
||||
enum PreferenceName: String {
|
||||
case wasLaunchedBefore = "launched_before"
|
||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
|
||||
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
|
||||
case globalHotkey = "global_hotkey"
|
||||
}
|
||||
|
||||
class Preferences {
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
static var shared = Preferences()
|
||||
|
||||
var cachedPreferences: [PreferenceName: Any?]
|
||||
|
||||
public init() {
|
||||
Preferences.handleFirstTimeLaunch()
|
||||
self.cachedPreferences = Self.cache()
|
||||
}
|
||||
|
||||
// MARK: - First Time Run
|
||||
|
||||
/**
|
||||
Note: macOS seems to cache plist values in memory as well as in files.
|
||||
You can find the persisted configuration file in: ~/Library/Preferences/com.nicoverbruggen.phpmon.plist
|
||||
|
||||
To clear the cache, and get a first-run experience you may need to run:
|
||||
```
|
||||
defaults delete com.nicoverbruggen.phpmon
|
||||
killall cfprefsd
|
||||
```
|
||||
*/
|
||||
static func handleFirstTimeLaunch() {
|
||||
let launchedBefore = UserDefaults.standard.bool(forKey: "launched_before")
|
||||
UserDefaults.standard.register(defaults: [
|
||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
|
||||
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true
|
||||
])
|
||||
|
||||
if launchedBefore {
|
||||
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
|
||||
return
|
||||
}
|
||||
|
||||
UserDefaults.standard.setValue(true, forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue)
|
||||
UserDefaults.standard.setValue(true, forKey: "launched_before")
|
||||
UserDefaults.standard.synchronize()
|
||||
|
||||
print("Saving first-time preferences!")
|
||||
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
|
||||
static func retrieve() -> [PreferenceName: Any] {
|
||||
Preferences.handleFirstTimeLaunch()
|
||||
|
||||
return [
|
||||
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
|
||||
.globalHotkey: UserDefaults.standard.string(
|
||||
forKey: PreferenceName.globalHotkey.rawValue) as Any
|
||||
]
|
||||
}
|
||||
// MARK: - API
|
||||
|
||||
static var preferences: [PreferenceName: Any?] {
|
||||
return Preferences.retrieve()
|
||||
return Self.shared.cachedPreferences
|
||||
}
|
||||
|
||||
// MARK: - Internal Functionality
|
||||
|
||||
static func cache() -> [PreferenceName: Any] {
|
||||
return [
|
||||
// Part 1: Always Booleans
|
||||
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
|
||||
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
|
||||
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
|
||||
|
||||
// Part 2: Always Strings
|
||||
.globalHotkey: UserDefaults.standard.string(forKey: PreferenceName.globalHotkey.rawValue) as Any,
|
||||
]
|
||||
}
|
||||
|
||||
static func update(_ preference: PreferenceName, value: Any?) {
|
||||
@ -51,6 +83,9 @@ class Preferences {
|
||||
UserDefaults.standard.setValue(value, forKey: preference.rawValue)
|
||||
}
|
||||
UserDefaults.standard.synchronize()
|
||||
|
||||
// Update the preferences cache in memory!
|
||||
Preferences.shared.cachedPreferences = Preferences.cache()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,17 +12,31 @@ import Carbon
|
||||
|
||||
class PrefsVC: NSViewController {
|
||||
|
||||
// Labels on the left
|
||||
@IBOutlet weak var leftLabelDynamicIcon: NSTextField!
|
||||
@IBOutlet weak var leftLabelServices: NSTextField!
|
||||
@IBOutlet weak var leftLabelGlobalShortcut: NSTextField!
|
||||
|
||||
// Dynamic icon
|
||||
@IBOutlet weak var buttonDynamicIcon: NSButton!
|
||||
@IBOutlet weak var labelDynamicIcon: NSTextField!
|
||||
@IBOutlet weak var buttonClose: NSButton!
|
||||
|
||||
// Full PHP version
|
||||
@IBOutlet weak var buttonDisplayFullPhpVersion: NSButton!
|
||||
@IBOutlet weak var labelDisplayFullPhpVersion: NSTextField!
|
||||
|
||||
// Auto-restart services
|
||||
@IBOutlet weak var buttonAutoRestartServices: NSButton!
|
||||
@IBOutlet weak var labelAutoRestartServices: NSTextField!
|
||||
|
||||
// Shortcut
|
||||
@IBOutlet weak var buttonSetShortcut: NSButton!
|
||||
@IBOutlet weak var buttonClearShortcut: NSButton!
|
||||
@IBOutlet weak var labelShortcut: NSTextField!
|
||||
|
||||
// Close button (bottom right)
|
||||
@IBOutlet weak var buttonClose: NSButton!
|
||||
|
||||
// MARK: - Display
|
||||
|
||||
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||
@ -47,6 +61,7 @@ class PrefsVC: NSViewController {
|
||||
override func viewWillAppear() {
|
||||
loadLocalization()
|
||||
loadDynamicIconFromPreferences()
|
||||
loadFullPhpVersionFromPreferences()
|
||||
loadGlobalKeybindFromPreferences()
|
||||
}
|
||||
|
||||
@ -62,6 +77,15 @@ class PrefsVC: NSViewController {
|
||||
labelDynamicIcon.stringValue = "prefs.dynamic_icon_desc".localized
|
||||
buttonDynamicIcon.title = "prefs.dynamic_icon_title".localized
|
||||
|
||||
// Full PHP version
|
||||
buttonDisplayFullPhpVersion.title = "prefs.display_full_php_version".localized
|
||||
labelDisplayFullPhpVersion.stringValue = "prefs.display_full_php_version_desc".localized
|
||||
|
||||
// Services
|
||||
leftLabelServices.stringValue = "prefs.services".localized
|
||||
buttonAutoRestartServices.title = "prefs.auto_restart_services_title".localized
|
||||
labelAutoRestartServices.stringValue = "prefs_auto_restart_services_desc".localized
|
||||
|
||||
// Global Shortcut
|
||||
leftLabelGlobalShortcut.stringValue = "prefs.global_shortcut".localized
|
||||
labelShortcut.stringValue = "prefs.shortcut_desc".localized
|
||||
@ -72,13 +96,40 @@ class PrefsVC: NSViewController {
|
||||
buttonClose.title = "prefs.close".localized
|
||||
}
|
||||
|
||||
// MARK: - Dynamic Icon Preference
|
||||
// MARK: - Loading Preferences
|
||||
|
||||
func loadDynamicIconFromPreferences() {
|
||||
let shouldDisplay = Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == true
|
||||
self.buttonDynamicIcon.state = shouldDisplay ? .on : .off
|
||||
}
|
||||
|
||||
func loadFullPhpVersionFromPreferences() {
|
||||
let shouldDisplay = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool == true
|
||||
self.buttonDisplayFullPhpVersion.state = shouldDisplay ? .on : .off
|
||||
}
|
||||
|
||||
func loadAutoRestartServicesFromPreferences() {
|
||||
let shouldDisplay = Preferences.preferences[.autoServiceRestartAfterExtensionToggle] as! Bool == true
|
||||
self.buttonAutoRestartServices.state = shouldDisplay ? .on : .off
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func toggledDynamicIcon(_ sender: Any) {
|
||||
Preferences.update(.shouldDisplayDynamicIcon, value: buttonDynamicIcon.state == .on)
|
||||
MainMenu.shared.refreshIcon()
|
||||
}
|
||||
|
||||
@IBAction func toggledFullPhpVersion(_ sender: Any) {
|
||||
Preferences.update(.fullPhpVersionDynamicIcon, value: buttonDisplayFullPhpVersion.state == .on)
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.update()
|
||||
}
|
||||
|
||||
@IBAction func toggledAutoRestartServices(_ sender: Any) {
|
||||
Preferences.update(.autoServiceRestartAfterExtensionToggle, value: buttonAutoRestartServices.state == .on)
|
||||
}
|
||||
|
||||
// MARK: - Shortcut Preference
|
||||
// Adapted from: https://dev.to/mitchartemis/creating-a-global-configurable-shortcut-for-macos-apps-in-swift-25e9
|
||||
|
||||
@ -166,13 +217,6 @@ class PrefsVC: NSViewController {
|
||||
buttonSetShortcut.title = globalKeybindPreference.description
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func toggledDynamicIcon(_ sender: Any) {
|
||||
Preferences.update(.shouldDisplayDynamicIcon, value: buttonDynamicIcon.state == .on)
|
||||
MainMenu.shared.refreshIcon()
|
||||
}
|
||||
|
||||
@IBAction func pressed(_ sender: Any) {
|
||||
self.view.window?.windowController?.close()
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ struct Keys {
|
||||
}
|
||||
|
||||
class PrefsWC: NSWindowController {
|
||||
|
||||
override func windowDidLoad() {
|
||||
super.windowDidLoad()
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class Command {
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
return output;
|
||||
return output
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -64,4 +64,5 @@ class Paths {
|
||||
public static var etcPath: String {
|
||||
return "\(shared.baseDir.rawValue)/etc"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class Shell {
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
var shell = "/bin/sh"
|
||||
var shell: String
|
||||
|
||||
init() {
|
||||
// Determine if we're using macOS Catalina or newer (that support /bin/zsh as default shell)
|
||||
@ -29,9 +29,14 @@ class Shell {
|
||||
.init(majorVersion: 10, minorVersion: 15, patchVersion: 0))
|
||||
|
||||
// If macOS Mojave is being used, we'll default to /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")
|
||||
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): defaulting to /bin/bash"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,25 +63,31 @@ class Shell {
|
||||
*/
|
||||
func pipe(_ command: String) -> String {
|
||||
let task = Process()
|
||||
let pipe = Pipe()
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
task.launchPath = self.shell
|
||||
task.arguments = ["--login", "-c", command]
|
||||
task.standardOutput = pipe
|
||||
task.standardOutput = outputPipe
|
||||
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
|
||||
}
|
||||
|
||||
return String(
|
||||
data: pipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if a file exists at the provided path.
|
||||
Uses `/bin/echo` instead of the `builtin` (which does not support `-n`).
|
||||
*/
|
||||
public static func fileExists(_ path: String) -> Bool {
|
||||
return Shell.pipe(
|
||||
"if [ -f \(path) ]; then echo \"PHP_Y_FE\"; fi"
|
||||
).contains("PHP_Y_FE")
|
||||
return Shell.pipe("if [ -f \(path) ]; then /bin/echo -n \"0\"; fi") == "0"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -52,10 +52,17 @@
|
||||
|
||||
"prefs.global_shortcut" = "Global shortcut:";
|
||||
"prefs.dynamic_icon" = "Dynamic icon:";
|
||||
"prefs.services" = "Services:";
|
||||
|
||||
"prefs.auto_restart_services_title" = "Auto-restart PHP-FPM";
|
||||
"prefs_auto_restart_services_desc" = "When checked, will automatically restart PHP-FPM when\nyou check or uncheck an extension. Slightly slower when enabled, \nbut this applies the extension change immediately for all sites \nyou're serving, no need to restart PHP-FPM manually.";
|
||||
|
||||
"prefs.dynamic_icon_title" = "Display dynamic icon in 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\ncurrently linked PHP version.";
|
||||
|
||||
"prefs.display_full_php_version" = "Display full PHP version in menu bar";
|
||||
"prefs.display_full_php_version_desc" = "Display the full version instead of the major version only.\n(This may be undesirable on smaller displays,\nso this is disabled by default.)";
|
||||
|
||||
"prefs.shortcut_set" = "Set global shortcut";
|
||||
"prefs.shortcut_listening" = "<listening for keypress>";
|
||||
"prefs.shortcut_clear" = "Clear";
|
||||
@ -64,7 +71,10 @@
|
||||
// NOTIFICATIONS
|
||||
|
||||
"notification.version_changed_title" = "PHP %@ now active";
|
||||
"notification.version_changed_desc" = "PHP Monitor has finished the switch to PHP %@.";
|
||||
"notification.version_changed_desc" = "PHP Monitor has switched to PHP %@.";
|
||||
|
||||
"notification.php_fpm_restarted" = "PHP-FPM automatically restarted";
|
||||
"notification.php_fpm_restarted_desc" = "You toggled an extension, so PHP-FPM was automatically restarted.";
|
||||
|
||||
"notification.services_stopped" = "Valet services stopped";
|
||||
"notification.services_stopped_desc" = "All services have been successfully stopped.";
|
||||
@ -92,6 +102,10 @@
|
||||
"alert.cannot_start.close" = "Close";
|
||||
"alert.cannot_start.retry" = "Retry";
|
||||
|
||||
// PHP alias issue
|
||||
"alert.php_alias_conflict.title" = "Homebrew `php` formula alias conflict detected";
|
||||
"alert.php_alias_conflict.info" = "PHP Monitor has detected conflicting `php` aliases in your Homebrew setup, both of which have been detected as installed.\n\nThis will likely result in failed linking when switching PHP versions, and will break PHP Monitor functionality.\n\nFor more information, please visit: https://github.com/nicoverbruggen/phpmon/issues/54";
|
||||
|
||||
// STARTUP
|
||||
|
||||
/// 1. PHP binary not found
|
||||
|
Reference in New Issue
Block a user