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

Compare commits

...

69 Commits
v3.0 ... v3.5

Author SHA1 Message Date
9bc8460cce 👌 Updated notification for Monterey 2021-10-19 21:42:17 +02:00
4cbd2fd6eb 📝 Updated documentation 2021-10-19 00:03:36 +02:00
6fef3fe37a 📝 Updated SECURITY.md 2021-10-19 00:02:13 +02:00
72a20d1ed9 🍱 New build screenshot of Xcode 13.1 2021-10-18 23:54:32 +02:00
73ed80434a 📝 Update README to reflect Monterey compatibility 2021-10-18 23:49:55 +02:00
a78672927b Support for upcoming releases of PHP 8.1 and 8.2 (dev) 2021-10-18 23:48:57 +02:00
4256eae442 👌 CS 2021-10-18 18:48:40 +02:00
76412b68f3 👌 Tests have OS X 10.14 as deployment target too 2021-08-31 11:08:00 +02:00
9153bb140a 👌 Code style fixes (empty line before class closes) 2021-08-31 11:03:55 +02:00
c9c15d10f9 👌 Improve handling of global hotkey load on startup 2021-08-31 10:52:49 +02:00
e8c2277ef5 🐛 Omit initial space (if uncommented, in #45) 2021-06-07 19:18:13 +02:00
23720c5dc9 🐛 Fix #45: Adjusted regex to support spaces 2021-06-07 19:13:52 +02:00
f881f07cba 👌 Cleanup 2021-05-07 15:29:47 +02:00
b072ee8dec 🚚 Improved project organisation, updated README 2021-05-03 16:52:51 +02:00
acfbc0b66f 👌 Clean up how file checks are done 2021-04-27 17:00:56 +02:00
c738a03934 📝 Update README & screenshot 2021-04-20 17:05:15 +02:00
84d62f3583 Ensure all tests pass 2021-04-20 17:05:05 +02:00
f9faa03b92 #41: Notify about broken PHP-FPM configuration 2021-04-19 20:30:37 +02:00
55f6c3c6cd Add "Locate global composer.json file" 2021-04-19 13:14:34 +02:00
a0c6753761 🐛 Use php_ini_scanned_files for .ini scan
Using `php-config --ini-dir` seems to fail on PHP 7.2 and below, likely
because said option was not available in these earlier versions. Because
all we need are the additional .ini files, calling php_ini_scanned_files
is a better solution since it is supported from PHP 5 and up.

This commit fixes the crash issue that was caused by running the failing
`php-config` command.

More information: https://www.php.net/manual/en/function.php-ini-scanned-files.php
2021-04-19 10:26:14 +02:00
327125608a 👌 Polish preferences screen 2021-04-15 23:16:42 +02:00
6c0045302b 📝 Updated README 2021-04-14 20:13:19 +02:00
9c85bebe72 Add option to turn all services off (#35) 2021-04-14 20:08:50 +02:00
fb56cd551e 👌 Improved parallelization 2021-04-14 19:40:07 +02:00
e83d507e79 Parallelize unlinking PHP versions 2021-04-14 18:46:33 +02:00
0c0e7fc87d Extension loading improvements (#31) and more
* The README has been updated with additional information
* The acknowledgements section has been added to the README
* The php@X.X/opt/bin/php-config binary is now used (#39)
* Extensions are now loaded from all possible .ini files
* PHP Monitor's preferences window can now be triggered via hotkey
* The first nine extensions can be triggered via hotkey
2021-04-07 16:58:05 +02:00
faf49fbe1d 👌 Prevent hotkey multi-fire (#33) 2021-04-02 18:32:07 +02:00
2925b0ff79 🏗 WIP: Global shortcut key (#33) 2021-04-02 17:50:46 +02:00
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
8a47cfd5f1 🐛 Remove which valet check 2021-03-17 14:50:26 +01:00
db4c0399fd 📝 Update README 2021-03-12 12:26:55 +01:00
b0f29b72cd 💰 Add funding option to GitHub 2021-03-12 12:24:57 +01:00
98a1622ccc 🔀 Merge branch 'develop'
* develop:
  💰 Add funding option to GitHub
  📝 Update SECURITY
2021-03-12 12:10:12 +01:00
a6ac737590 💰 Add funding option to GitHub 2021-03-12 12:09:49 +01:00
bba719012f 📝 Update SECURITY 2021-03-12 10:01:46 +01:00
cdc0cea01c 📝 Update SECURITY 2021-03-12 10:01:32 +01:00
d3a4f2091c 🚀 Version 3.1
* develop:
  📝 Tweak error message string
  🔧 Bump build version
  🐛 Handle `valet` install in `/opt/homebrew/bin`
  👌 Cleanup
  📝 Updated README
  🔧 Determine default shell based on macOS version
   Added tests
  🚚 Moved files around
  📝 Update copyright information
2021-03-12 09:54:40 +01:00
c7dc17dded 📝 Tweak error message string 2021-03-12 09:54:12 +01:00
d70f8627bb 🔧 Bump build version 2021-03-12 09:50:29 +01:00
7661f4643d 🐛 Handle valet install in /opt/homebrew/bin 2021-03-12 09:46:38 +01:00
11d74277e5 👌 Cleanup 2021-02-19 18:59:39 +01:00
e4096ab662 📝 Updated README 2021-02-19 17:35:48 +01:00
d367f1f353 🔧 Determine default shell based on macOS version
This fixes #21 (fixes the issue on Mojave) and closes #22 since the min
SDK does not need to be bumped.
2021-02-19 17:28:57 +01:00
79fbdef41b Added tests 2021-02-14 22:54:45 +01:00
37ede7e2a3 🚚 Moved files around 2021-02-14 22:54:38 +01:00
80eabe3bf3 📝 Update copyright information 2021-02-12 10:00:25 +01:00
63 changed files with 4242 additions and 411 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: ['https://nicoverbruggen.be/sponsor', 'https://paypal.me/nicoverbruggen']

View File

@ -3,10 +3,13 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 52;
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 */; };
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.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 */; };
@ -15,9 +18,16 @@
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 /* PhpInstall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstall.swift */; };
C41C1B4B22B019FF00E7CF16 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstallation.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 */; };
C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; };
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
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 */; };
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
@ -25,19 +35,61 @@
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 */; };
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C9925CC888B00CC7490 /* HeaderView.xib */; };
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
C4998F0626175E7200B2526E /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = C4998F0526175E7200B2526E /* HotKey */; };
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
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 */; };
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 */; };
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */; };
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4F780B425D80B51000DBC97 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
C4F780B725D80B5D000DBC97 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
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 */; };
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 */
C4F7807E25D7F84B000DBC97 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = C41C1B2B22B0097F00E7CF16 /* Project object */;
proxyType = 1;
remoteGlobalIDString = C41C1B3222B0097F00E7CF16;
remoteInfo = "PHP Monitor";
};
/* 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>"; };
@ -49,9 +101,15 @@
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 /* PhpInstall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstall.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; 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>"; };
C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = "<group>"; };
@ -64,18 +122,34 @@
C48D0C9525CC80B100CC7490 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
C48D0C9925CC888B00CC7490 /* HeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HeaderView.xib; sourceTree = "<group>"; };
C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = "<group>"; };
C49EAB45259FC305007F6C3B /* Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
C4F780A725D80AE8000DBC97 /* php.ini */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = php.ini; sourceTree = "<group>"; };
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 */
C41C1B3022B0097F00E7CF16 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C4998F0626175E7200B2526E /* HotKey in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C4F7807625D7F84B000DBC97 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
@ -85,6 +159,26 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5420395726135DB800FB00FA /* Preferences */ = {
isa = PBXGroup;
children = (
C4998F092617633900B2526E /* PrefsWC.swift */,
5420395826135DC100FB00FA /* PrefsVC.swift */,
5420395E2613607600FB00FA /* Preferences.swift */,
C41CD0272628D8E20065BBED /* Keybinds */,
);
path = Preferences;
sourceTree = "<group>";
};
54B20EDF263AA22C00D3250E /* PHP */ = {
isa = PBXGroup;
children = (
C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */,
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
);
path = PHP;
sourceTree = "<group>";
};
C405A4CD24B9B9070062FAFA /* IAP */ = {
isa = PBXGroup;
children = (
@ -99,8 +193,10 @@
children = (
C4F8C0A522D4FA41002EFE61 /* README.md */,
C4E713562570150F00007428 /* SECURITY.md */,
C4F7807425D7F7E5000DBC97 /* RELEASE.md */,
C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */,
C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
C41C1B3422B0097F00E7CF16 /* Products */,
);
sourceTree = "<group>";
@ -109,6 +205,7 @@
isa = PBXGroup;
children = (
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */,
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -120,6 +217,7 @@
C4EE188322D3386B00E126E5 /* Constants.swift */,
C41E181722CB61EB0072CF09 /* Domain */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
C4232EE42612526500158FC6 /* Credits.html */,
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
C473319E2470923A009A0597 /* Localizable.strings */,
@ -128,13 +226,22 @@
path = phpmon;
sourceTree = "<group>";
};
C41CD0272628D8E20065BBED /* Keybinds */ = {
isa = PBXGroup;
children = (
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */,
);
path = Keybinds;
sourceTree = "<group>";
};
C41E181722CB61EB0072CF09 /* Domain */ = {
isa = PBXGroup;
children = (
C4811D2622D70CEF00B5F6B3 /* Singletons */,
C4B13B1D25C4915000548C3A /* Core */,
54B20EDF263AA22C00D3250E /* PHP */,
C4F7808A25D7F918000DBC97 /* Terminal */,
C47331A0247093AC009A0597 /* Menu */,
C4811D2722D70D8E00B5F6B3 /* Commands */,
5420395726135DB800FB00FA /* Preferences */,
C4811D2822D70D9C00B5F6B3 /* Helpers */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
);
@ -144,6 +251,7 @@
C47331A0247093AC009A0597 /* Menu */ = {
isa = PBXGroup;
children = (
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */,
C47331A1247093B7009A0597 /* StatusMenu.swift */,
C48D0C9525CC80B100CC7490 /* HeaderView.swift */,
C48D0C9925CC888B00CC7490 /* HeaderView.xib */,
@ -153,33 +261,13 @@
path = Menu;
sourceTree = "<group>";
};
C4811D2622D70CEF00B5F6B3 /* Singletons */ = {
isa = PBXGroup;
children = (
C49EAB45259FC305007F6C3B /* Paths.swift */,
C41C1B4622B009A400E7CF16 /* Shell.swift */,
C42295DC2358D02000E263B2 /* Command.swift */,
C4811D2322D70A4700B5F6B3 /* App.swift */,
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */,
);
path = Singletons;
sourceTree = "<group>";
};
C4811D2722D70D8E00B5F6B3 /* Commands */ = {
isa = PBXGroup;
children = (
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
);
path = Commands;
sourceTree = "<group>";
};
C4811D2822D70D9C00B5F6B3 /* Helpers */ = {
isa = PBXGroup;
children = (
C476FF9722B0DD830098105B /* Alert.swift */,
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -188,19 +276,45 @@
isa = PBXGroup;
children = (
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
C41C1B4A22B019FF00E7CF16 /* PhpInstall.swift */,
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
C4811D2322D70A4700B5F6B3 /* App.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
);
path = Core;
sourceTree = "<group>";
};
C4F7807A25D7F84B000DBC97 /* phpmon-tests */ = {
isa = PBXGroup;
children = (
C43A8A1F25D9D1D700591B77 /* brew.json */,
C4F780A725D80AE8000DBC97 /* php.ini */,
C4F7807D25D7F84B000DBC97 /* Info.plist */,
C4F7809B25D80344000DBC97 /* CommandTest.swift */,
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
C43A8A1925D9CD1000591B77 /* Utility.swift */,
);
path = "phpmon-tests";
sourceTree = "<group>";
};
C4F7808A25D7F918000DBC97 /* Terminal */ = {
isa = PBXGroup;
children = (
C49EAB45259FC305007F6C3B /* Paths.swift */,
C42295DC2358D02000E263B2 /* Command.swift */,
C41C1B4622B009A400E7CF16 /* Shell.swift */,
);
path = Terminal;
sourceTree = "<group>";
};
C4F8C0A222D4F100002EFE61 /* Extensions */ = {
isa = PBXGroup;
children = (
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */,
C46FA23E246C358E00944F05 /* StringExtension.swift */,
C48D0C9225CC804200CC7490 /* XibLoadable.swift */,
C42759662627662800093CAE /* NSMenuExtension.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -221,23 +335,48 @@
dependencies = (
);
name = "PHP Monitor";
packageProductDependencies = (
C4998F0526175E7200B2526E /* HotKey */,
);
productName = phpmon;
productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */;
productType = "com.apple.product-type.application";
};
C4F7807825D7F84B000DBC97 /* phpmon-tests */ = {
isa = PBXNativeTarget;
buildConfigurationList = C4F7808025D7F84B000DBC97 /* Build configuration list for PBXNativeTarget "phpmon-tests" */;
buildPhases = (
C4F7807525D7F84B000DBC97 /* Sources */,
C4F7807625D7F84B000DBC97 /* Frameworks */,
C4F7807725D7F84B000DBC97 /* Resources */,
);
buildRules = (
);
dependencies = (
C4F7807F25D7F84B000DBC97 /* PBXTargetDependency */,
);
name = "phpmon-tests";
productName = "phpmon-tests";
productReference = C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
C41C1B2B22B0097F00E7CF16 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1020;
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1220;
ORGANIZATIONNAME = "Nico Verbruggen";
TargetAttributes = {
C41C1B3222B0097F00E7CF16 = {
CreatedOnToolsVersion = 10.2.1;
};
C4F7807825D7F84B000DBC97 = {
CreatedOnToolsVersion = 12.4;
TestTargetID = C41C1B3222B0097F00E7CF16;
};
};
};
buildConfigurationList = C41C1B2E22B0097F00E7CF16 /* Build configuration list for PBXProject "PHP Monitor" */;
@ -249,11 +388,15 @@
Base,
);
mainGroup = C41C1B2A22B0097F00E7CF16;
packageReferences = (
C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */,
);
productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
C41C1B3222B0097F00E7CF16 /* PHP Monitor */,
C4F7807825D7F84B000DBC97 /* phpmon-tests */,
);
};
/* End PBXProject section */
@ -267,12 +410,22 @@
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 */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C4F7807725D7F84B000DBC97 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
C43A8A2025D9D1D700591B77 /* brew.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -282,18 +435,23 @@
files = (
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */,
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C4998F0A2617633900B2526E /* PrefsWC.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 */,
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.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 */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* PhpInstall.swift in Sources */,
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* PhpInstallation.swift in Sources */,
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
@ -304,8 +462,54 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C4F7807525D7F84B000DBC97 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
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 */,
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */,
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */,
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */,
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */,
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */,
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */,
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 */,
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */,
C4F780B425D80B51000DBC97 /* Actions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
C4F7807F25D7F84B000DBC97 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C41C1B3222B0097F00E7CF16 /* PHP Monitor */;
targetProxy = C4F7807E25D7F84B000DBC97 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
C41C1B3C22B0098000E7CF16 /* Main.storyboard */ = {
isa = PBXVariantGroup;
@ -443,7 +647,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 45;
CURRENT_PROJECT_VERSION = 60;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -451,7 +655,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 3.0;
MARKETING_VERSION = 3.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -467,7 +671,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 45;
CURRENT_PROJECT_VERSION = 60;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -475,7 +679,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 3.0;
MARKETING_VERSION = 3.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -483,6 +687,48 @@
};
name = Release;
};
C4F7808125D7F84B000DBC97 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
INFOPLIST_FILE = "phpmon-tests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PHP Monitor.app/Contents/MacOS/PHP Monitor";
};
name = Debug;
};
C4F7808225D7F84B000DBC97 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
INFOPLIST_FILE = "phpmon-tests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PHP Monitor.app/Contents/MacOS/PHP Monitor";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -504,7 +750,35 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C4F7808025D7F84B000DBC97 /* Build configuration list for PBXNativeTarget "phpmon-tests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C4F7808125D7F84B000DBC97 /* Debug */,
C4F7808225D7F84B000DBC97 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/soffes/HotKey";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.1.3;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
C4998F0526175E7200B2526E /* HotKey */ = {
isa = XCSwiftPackageProductDependency;
package = C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */;
productName = HotKey;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = C41C1B2B22B0097F00E7CF16 /* Project object */;
}

View File

@ -28,6 +28,16 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
BuildableName = "phpmon-tests.xctest"
BlueprintName = "phpmon-tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

113
README.md
View File

@ -1,24 +1,27 @@
# 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.<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/screenshot34.png" width="412px" 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
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
* macOS 10.15 Catalina 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
@ -40,10 +43,6 @@ To upgrade your existing installation, run:
_The app is signed and notarized, meaning all you have to do is approve its first launch._
## ⭐️ Star me!
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. 😃
## 👨‍💻 Why build this?
I wanted to be able to **see at a glance** which version of PHP was linked, and handle dealing with Laravel Valet in a simple app without having to deal with the terminal every time.
@ -60,7 +59,26 @@ PHP Monitor performs some integrity checks to ensure a good experience when usin
> If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. To upgrade Valet, run `composer global update`. Don't forget to run `valet install` after upgrading.
If you're still having issues, here's a few common issues and solutions:
If you're still having issues, here's a few common questions & answers, as well as issues and solutions:
<details>
<summary><strong>Which versions of PHP are supported?</strong></summary>
<ul>
<li>PHP 5.6</li>
<li>PHP 7.0</li>
<li>PHP 7.1</li>
<li>PHP 7.2</li>
<li>PHP 7.3</li>
<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.
</details>
<details>
<summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary>
@ -136,6 +154,35 @@ This problem is usually resolved by upgrading Valet and running `valet install`
valet install
</details>
<details>
<summary><strong>PHP Monitor tells me my installation is broken, but I don't see why!</strong></summary>
PHP Monitor tells you that a PHP installation is broken, if the configuration is causing warnings or errors when determining the version number.
Since PHP Monitor changes the linked version via Homebrew, both Valet *and* your terminal (CLI) should use the new PHP version.
However, this might not be the case on your system. You _might_ have a specific version of PHP linked if that is not the case. In that case, you may need to change your `.bashrc` or `.zshrc` file where the PATH is set (depending on the terminal you use).
You can find out which version of PHP is being used by running `which php`.
You can find out what exactly is causing the issue by running a command. On Intel, you can run (replace `7.4` with the version that is broken):
```
/usr/local/opt/php@7.4/bin/php -r "print phpversion();"
```
On Apple Silicon, you can run (replace `7.4` with the version that is broken):
```
/opt/homebrew/opt/php@7.4/bin/php -r "print phpversion();"
```
You should see an error or a warning here in the output.
Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling).
</details>
<details>
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
@ -151,7 +198,7 @@ You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` i
<details>
<summary><strong>One of my commented out extensions is not being detected...</strong></summary>
The app searches in the relevant `php.ini` file for a specific pattern. For regular extensions:
The app searches in the relevant `.ini` files for a specific pattern. For regular extensions:
* `extension="*.so"`
* `; extension="*.so"`
@ -162,6 +209,9 @@ 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.
Since v3.4 all of the loaded .ini files are sourced to determine which extensions are enabled.
</details>
<details>
@ -185,23 +235,47 @@ PHP Monitor itself doesn't do any network requests. Feel free to check the sourc
This is a security feature of Brew. When you start a service as an administrator, the root user becomes the owner of relevant binaries.
You will need to manually clean up those folders yourself using `rm -rf` or by manually removing those folders via Finder.
You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder).
</details>
## 📝 Another issue?
<details>
<summary><strong>The app has crashed!</strong></summary>
Please get in touch and open an issue. PHP Monitor shouldn't crash :)
</details>
## 📝 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!).
## 😎 Acknowledgements
While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks:
* My colleagues at [DIVE](https://dive.be)
* The [Homebrew](https://brew.sh/) team who maintain
* The [developers & maintainers of Valet](https://github.com/laravel/valet/graphs/contributors)
* Everyone in the Laravel community who shared the app (thanks!)
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot)
* Everyone who left feedback via issues
Thank you very much for your contributions, kind words and support.
## 🚜 How it works
### Loading info about PHP in the background
This utility runs `php -r 'print phpversion()'` 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.
@ -217,11 +291,8 @@ This means:
The utility runs the following commands:
- Unlink all detected PHP versions
- Switch to whatever version of PHP `php` is at (this is done to ensure that Valet works, even when attempting to use PHP 5.6)
- Stop all relevant services (`php`, `nginx`)
- Link the desired version of PHP
- Start the correct `php` service for the desired PHP version
- Unlink all detected PHP versions & stop the respective `php@X.X` services
- Link the desired version of PHP, and start the associated service
### Want to know more?
@ -231,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:

View File

@ -4,13 +4,14 @@
Generally speaking, only the latest version of **PHP Monitor** is supported:
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target |
| ------- | ------------- | ------------------ | ----- | ----- |
| 3.0 | ✅ 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 |
| ------- | ------------- | ------------------ | ----- | ----- | ----- |
| 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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

BIN
docs/screenshot34.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@ -0,0 +1,28 @@
//
// BrewJsonParserTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class BrewJsonParserTest: XCTestCase {
static var jsonBrewFile: URL {
return Bundle(for: Self.self).url(forResource: "brew", withExtension: "json")!
}
func testCanLoadExtension() throws {
let json = try? String(contentsOf: Self.jsonBrewFile, encoding: .utf8)
let package = try! JSONDecoder().decode(
[HomebrewPackage].self, from: json!.data(using: .utf8)!
).first!
XCTAssertEqual(package.name, "php")
XCTAssertEqual(package.full_name, "php")
XCTAssertEqual(package.aliases.first!, "php@8.0")
}
}

View File

@ -0,0 +1,25 @@
//
// CommandTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class CommandTest: XCTestCase {
func testDeterminePhpVersion() {
let version = Command.execute(
path: Paths.php,
arguments: ["-v"]
)
XCTAssert(version.contains("(cli)"))
XCTAssert(version.contains("NTS"))
XCTAssert(version.contains("built"))
XCTAssert(version.contains("Zend"))
}
}

View File

@ -0,0 +1,72 @@
//
// ExtensionParserTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class ExtensionParserTest: XCTestCase {
static var phpIniFileUrl: URL {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
}
func testCanLoadExtension() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
XCTAssertGreaterThan(extensions.count, 0)
}
func testExtensionNameIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensionNames = extensions.map { (ext) -> String in
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 {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
// xdebug should be enabled
XCTAssertEqual(extensions[0].enabled, true)
// imagick should be disabled
XCTAssertEqual(extensions[1].enabled, false)
}
func testToggleWorksAsExpected() throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.load(from: destination)
XCTAssertEqual(extensions.count, 6)
// Try to disable xdebug (should be detected first)!
let xdebug = extensions.first!
XCTAssertTrue(xdebug.name == "xdebug")
XCTAssertEqual(xdebug.enabled, true)
xdebug.toggle()
XCTAssertEqual(xdebug.enabled, false)
// Check if the file contains the appropriate data
let file = try! String(contentsOf: destination, encoding: .utf8)
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
// Make sure if we load the data again, it's disabled
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
}
}

22
phpmon-tests/Info.plist Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

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"])
}
}

View File

@ -0,0 +1,28 @@
//
// Utility.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class Utility {
public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? {
if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) {
let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)")
do {
try FileManager.default.copyItem(at: bundleURL, to: targetURL)
return targetURL
} catch let error {
print("Unable to copy file: \(error)")
}
}
return nil
}
}

332
phpmon-tests/brew.json Normal file
View File

@ -0,0 +1,332 @@
[
{
"name":"php",
"full_name":"php",
"tap":"homebrew/core",
"oldname":null,
"aliases":[
"php@8.0"
],
"versioned_formulae":[
"php@7.4",
"php@7.3",
"php@7.2"
],
"desc":"General-purpose scripting language",
"license":"PHP-3.01",
"homepage":"https://www.php.net/",
"versions":{
"stable":"8.0.2",
"head":"HEAD",
"bottle":true
},
"urls":{
"stable":{
"url":"https://www.php.net/distributions/php-8.0.2.tar.xz",
"tag":null,
"revision":null
}
},
"revision":0,
"version_scheme":0,
"bottle":{
"stable":{
"rebuild":0,
"cellar":"/opt/homebrew/Cellar",
"prefix":"/opt/homebrew",
"root_url":"https://homebrew.bintray.com/bottles",
"files":{
"arm64_big_sur":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.arm64_big_sur.bottle.tar.gz",
"sha256":"cbefa1db73d08b9af4593a44512b8d727e43033ee8517736bae5f16315501b12"
},
"big_sur":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.big_sur.bottle.tar.gz",
"sha256":"6857142e12254b15da4e74c2986dd24faca57dac8d467b04621db349e277dd63"
},
"catalina":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.catalina.bottle.tar.gz",
"sha256":"b651611134c18f93fdf121a4277b51b197a896a19ccb8020289b4e19e0638349"
},
"mojave":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.mojave.bottle.tar.gz",
"sha256":"9583a51fcc6f804aadbb14e18f770d4fb4973deaed6ddc4770342e62974ffbca"
}
}
}
},
"keg_only":false,
"bottle_disabled":false,
"options":[
],
"build_dependencies":[
"httpd",
"pkg-config"
],
"dependencies":[
"apr",
"apr-util",
"argon2",
"aspell",
"autoconf",
"curl",
"freetds",
"gd",
"gettext",
"glib",
"gmp",
"icu4c",
"krb5",
"libffi",
"libpq",
"libsodium",
"libzip",
"oniguruma",
"openldap",
"openssl@1.1",
"pcre2",
"sqlite",
"tidy-html5",
"unixodbc"
],
"recommended_dependencies":[
],
"optional_dependencies":[
],
"uses_from_macos":[
{
"xz":"build"
},
"bzip2",
"libedit",
"libxml2",
"libxslt",
"zlib"
],
"requirements":[
],
"conflicts_with":[
],
"caveats":"To enable PHP in Apache add the following to httpd.conf and restart Apache:\n LoadModule php_module $(brew --prefix)/opt/php/lib/httpd/modules/libphp.so\n\n <FilesMatch \\.php$>\n SetHandler application/x-httpd-php\n </FilesMatch>\n\nFinally, check DirectoryIndex includes index.php\n DirectoryIndex index.php index.html\n\nThe php.ini and php-fpm.ini file can be found in:\n $(brew --prefix)/etc/php/8.0/\n",
"installed":[
{
"version":"8.0.2",
"used_options":[
],
"built_as_bottle":true,
"poured_from_bottle":true,
"runtime_dependencies":[
{
"full_name":"apr",
"version":"1.7.0"
},
{
"full_name":"openssl@1.1",
"version":"1.1.1i"
},
{
"full_name":"apr-util",
"version":"1.6.1"
},
{
"full_name":"argon2",
"version":"20190702"
},
{
"full_name":"aspell",
"version":"0.60.8"
},
{
"full_name":"autoconf",
"version":"2.69"
},
{
"full_name":"brotli",
"version":"1.0.9"
},
{
"full_name":"gettext",
"version":"0.21"
},
{
"full_name":"libunistring",
"version":"0.9.10"
},
{
"full_name":"libidn2",
"version":"2.3.0"
},
{
"full_name":"libmetalink",
"version":"0.1.3"
},
{
"full_name":"libssh2",
"version":"1.9.0"
},
{
"full_name":"c-ares",
"version":"1.17.1"
},
{
"full_name":"jemalloc",
"version":"5.2.1"
},
{
"full_name":"libev",
"version":"4.33"
},
{
"full_name":"nghttp2",
"version":"1.43.0"
},
{
"full_name":"openldap",
"version":"2.4.57"
},
{
"full_name":"rtmpdump",
"version":"2.4+20151223"
},
{
"full_name":"zstd",
"version":"1.4.8"
},
{
"full_name":"curl",
"version":"7.75.0"
},
{
"full_name":"libtool",
"version":"2.4.6"
},
{
"full_name":"unixodbc",
"version":"2.3.9"
},
{
"full_name":"freetds",
"version":"1.2.18"
},
{
"full_name":"libpng",
"version":"1.6.37"
},
{
"full_name":"freetype",
"version":"2.10.4"
},
{
"full_name":"fontconfig",
"version":"2.13.1"
},
{
"full_name":"jpeg",
"version":"9d"
},
{
"full_name":"libtiff",
"version":"4.2.0"
},
{
"full_name":"webp",
"version":"1.2.0"
},
{
"full_name":"gd",
"version":"2.3.1"
},
{
"full_name":"libffi",
"version":"3.3"
},
{
"full_name":"pcre",
"version":"8.44"
},
{
"full_name":"gdbm",
"version":"1.18.1"
},
{
"full_name":"readline",
"version":"8.1"
},
{
"full_name":"sqlite",
"version":"3.34.0"
},
{
"full_name":"tcl-tk",
"version":"8.6.11"
},
{
"full_name":"xz",
"version":"5.2.5"
},
{
"full_name":"python@3.9",
"version":"3.9.1"
},
{
"full_name":"glib",
"version":"2.66.6"
},
{
"full_name":"gmp",
"version":"6.2.1"
},
{
"full_name":"icu4c",
"version":"67.1"
},
{
"full_name":"krb5",
"version":"1.19"
},
{
"full_name":"libpq",
"version":"13.1"
},
{
"full_name":"libsodium",
"version":"1.0.18"
},
{
"full_name":"libzip",
"version":"1.7.3"
},
{
"full_name":"oniguruma",
"version":"6.9.6"
},
{
"full_name":"pcre2",
"version":"10.36"
},
{
"full_name":"tidy-html5",
"version":"5.6.0"
}
],
"installed_as_dependency":false,
"installed_on_request":true
}
],
"linked_keg":"8.0.2",
"pinned":false,
"outdated":false,
"deprecated":false,
"deprecation_date":null,
"deprecation_reason":null,
"disabled":false,
"disable_date":null,
"disable_reason":null
}
]

1957
phpmon-tests/php.ini Normal file

File diff suppressed because it is too large Load Diff

View File

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

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

View File

@ -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"
]
}

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

@ -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()
@ -57,6 +78,13 @@ class Actions {
brew("services restart dnsmasq", sudo: true)
}
public static func stopAllServices()
{
brew("services stop \(App.phpInstall!.formula)", sudo: true)
brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true)
}
/**
Switching to a new PHP version involves:
- unlinking the current version
@ -66,17 +94,40 @@ class Actions {
Please note that depending on which version is installed,
the version that is switched to may or may not be identical to `php` (without @version).
*/
public static func switchToPhpVersion(version: String, availableVersions: [String])
{
public static func switchToPhpVersion(
version: String,
availableVersions: [String],
completed: @escaping () -> Void
) {
print("Switching to \(version), unlinking all versions...")
let group = DispatchGroup()
availableVersions.forEach { (available) in
let formula = (available == App.shared.brewPhpVersion) ? "php" : "php@\(available)"
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
group.enter()
DispatchQueue.global(qos: .userInitiated).async {
let formula = (available == App.shared.brewPhpVersion)
? "php" : "php@\(available)"
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
group.leave()
}
}
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
brew("link \(formula) --overwrite --force")
brew("services start \(formula)", sudo: true)
group.notify(queue: .global(qos: .userInitiated)) {
print("All versions have been unlinked!")
print("Linking the new version!")
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
brew("link \(formula) --overwrite --force")
brew("services start \(formula)", sudo: true)
print("The new version has been linked!")
completed()
}
}
// MARK: - Finding Config Files
@ -87,6 +138,13 @@ class Actions {
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder()
{
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpConfigFolder(version: String)
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
@ -95,8 +153,9 @@ class Actions {
public static func openValetConfigFolder()
{
let files = [NSURL(fileURLWithPath: NSString(string: "~/.config/valet").expandingTildeInPath)];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Quick Fix
@ -110,7 +169,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)")
@ -140,7 +199,11 @@ class Actions {
*/
public static func sed(file: String, original: String, replacement: String)
{
Shell.run("sed -i '' 's/\(original)/\(replacement)/g' \(file)")
// Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacment = replacement.replacingOccurrences(of: "/", with: "\\/")
Shell.run("sed -i '' 's/\(e_original)/\(e_replacment)/g' \(file)")
}
/**
@ -154,4 +217,5 @@ class Actions {
.trimmingCharacters(in: .whitespacesAndNewlines)
.contains("YES")
}
}

View File

@ -0,0 +1,125 @@
//
// StateManager.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
import HotKey
class App {
static let shared = App()
init() {
loadGlobalHotkey()
}
/** Information about the currently linked PHP installation. */
static var phpInstall: PhpInstallation? {
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
}
/** 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.
*/
var busy: Bool = false
/**
The currently active installation of PHP.
*/
var currentInstall: PhpInstallation? = nil
/**
All available versions of PHP.
*/
var availablePhpVersions : [String] = []
/**
The timer that will periodically fetch the PHP version that is currently active.
*/
var timer: Timer?
/**
Information we were able to discern from the Homebrew info command (as JSON).
*/
var brewPhpPackage: HomebrewPackage? = nil {
didSet {
brewPhpVersion = brewPhpPackage!.version
}
}
/**
The version that the `php` formula via Brew is aliased to on the current system.
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 the version in Constants.swift, but the information
should always be loaded from Homebrew itself upon startup.
*/
var brewPhpVersion: String = Constants.LatestStablePhpVersion
/**
The shortcut the user has requested.
*/
var shortcutHotkey: HotKey? = nil {
didSet {
self.setupGlobalHotkeyListener()
}
}
// 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() {
// 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
}
// 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 {
return
}
hotkey.keyDownHandler = {
MainMenu.shared.statusItem.button?.performClick(nil)
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
}

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,172 @@
</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" customClass="PrefsWC" customModule="PHP_Monitor" customModuleProvider="target" 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" wantsLayer="YES" id="Pf1-A5-3Xz">
<rect key="frame" x="0.0" y="0.0" width="574" height="189"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="GSr-K5-3yw">
<rect key="frame" x="485" 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"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</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="148" y="152" 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"/>
</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="148" y="131" 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"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="V7b-jv-oCB">
<rect key="frame" x="143" y="75" width="184" height="32"/>
<constraints>
<constraint firstAttribute="width" constant="170" id="9jD-Bf-T2M"/>
</constraints>
<backgroundFilters>
<ciFilter name="CIDotScreen">
<configuration>
<real key="inputAngle" value="0.0"/>
<ciVector key="inputCenter">
<real value="150"/>
<real value="150"/>
</ciVector>
<null key="inputImage"/>
<real key="inputSharpness" value="0.69999999999999996"/>
<real key="inputWidth" value="6"/>
</configuration>
</ciFilter>
</backgroundFilters>
<buttonCell key="cell" type="push" title="SET_SHORTCUT" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="R63-tN-KVQ">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="register:" target="AW2-rV-rbS" id="4Mj-eM-4eW"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YsQ-AZ-Aei">
<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"/>
</buttonCell>
<connections>
<action selector="unregister:" target="AW2-rV-rbS" id="2RI-4w-6Td"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5ZK-BG-o1t">
<rect key="frame" x="42" y="85" width="100" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="PREF_GLOSHO:" id="xiD-8H-p5s">
<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>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="31d-gd-auR">
<rect key="frame" x="18" y="153" width="124" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="120" id="8dt-Pg-wFI"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="PREF_DYN_ICON:" id="E10-ss-Cdz">
<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>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1TO-9H-z2d">
<rect key="frame" x="148" y="60" width="101" height="14"/>
<textFieldCell key="cell" title="SHORTCUT_DESC" id="nYP-yi-DBf">
<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 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="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="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="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="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="GSr-K5-3yw" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="mTE-WD-54L"/>
<constraint firstItem="31d-gd-auR" firstAttribute="leading" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="o0J-yT-TDX"/>
<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="buttonClearShortcut" destination="YsQ-AZ-Aei" id="1xo-hk-HgM"/>
<outlet property="buttonClose" destination="GSr-K5-3yw" id="d4I-Cf-gXD"/>
<outlet property="buttonDynamicIcon" destination="MEf-MN-oXt" id="qEN-Vg-EZS"/>
<outlet property="buttonSetShortcut" destination="V7b-jv-oCB" id="2aS-S4-cKR"/>
<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"/>
</connections>
</viewController>
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="264" y="399.5"/>
</scene>
</scenes>
</document>

View File

@ -1,114 +0,0 @@
//
// PhpInstall.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpInstall {
var version: Version
var configuration: Configuration
var extensions: [PhpExtension]
// MARK: - Computed
var formula: String {
return (self.version.short == App.shared.brewPhpVersion) ? "php" : "php@\(self.version.short)"
}
// MARK: - Initializer
init() {
// Show information about the current version
self.version = Self.getVersion()
// If an error occurred, exit early
if (self.version.error) {
self.configuration = Configuration()
self.extensions = []
return
}
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(self.version.short)/php.ini")
self.extensions = PhpExtension.load(from: path)
// Get configuration values
self.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")
)
}
/**
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
*/
private static func getVersion() -> Version {
var versionStruct = Version()
let version = Command.execute(path: Paths.php, arguments: ["-r", "print phpversion();"])
if (version == "" || version.contains("Warning") || version.contains("Error")) {
versionStruct.short = "💩 BROKEN"
versionStruct.long = "";
versionStruct.error = true
return versionStruct;
}
// That's the long version
versionStruct.long = version
// Next up, let's strip away the minor version number
let segments = versionStruct.long.components(separatedBy: ".")
// Get the first two elements
versionStruct.short = segments[0...1].joined(separator: ".")
return versionStruct
}
/**
Retrieves the display value for a specific key in the `.ini` file.
The following values are valid:
* -1: unlimited (show the infinity icon)
* 10000: an integer = amount of bytes
* 1K, 1M, 1G = shorthand for kilobytes, megabytes and gigabytes
If none of these notations are used, the _fallback_ value is used. We'll show an emoji to indicate something has gone wrong here.
To clarify, B gets appended to valid values. As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes .
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
*/
private static func getByteCount(key: String) -> String {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
// Check if the value is unlimited
if (value == "-1") {
return ""
}
// Check if the syntax is valid otherwise
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
return (match == nil) ? "⚠️" : "\(value)B"
}
// MARK: - Structs
struct Version {
var short = "???"
var long = "???"
var error = false
}
struct Configuration {
var memory_limit = "???"
var upload_max_filesize = "???"
var post_max_size = "???"
}
}

View File

@ -21,53 +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(
!Shell.pipe("which valet").contains("/usr/local/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(
!Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"),
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(".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()
}
}
@ -107,13 +115,14 @@ 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

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

View File

@ -0,0 +1,18 @@
//
// NSMenuExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 14/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension NSMenu {
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
newItem.keyEquivalentModifierMask = modifier
self.addItem(newItem)
}
}

View File

@ -33,4 +33,5 @@ extension String {
let end = r.upperBound
return String(self[start ..< end])
}
}

View File

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

View File

@ -8,6 +8,7 @@
import Foundation
struct HomebrewPackage : Decodable {
let name: String
let full_name: String
let aliases: [String]
@ -15,4 +16,5 @@ struct HomebrewPackage : Decodable {
public var version: String {
return aliases.first!.replacingOccurrences(of: "php@", with: "")
}
}

View File

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

View File

@ -7,10 +7,12 @@
import Cocoa
class MainMenu: NSObject, NSWindowDelegate {
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
static let shared = MainMenu()
weak var menuDelegate: NSMenuDelegate? = nil
/**
The status bar item with variable length.
*/
@ -25,11 +27,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 +42,17 @@ class MainMenu: NSObject, NSWindowDelegate {
*/
private func onEnvironmentPass() {
App.shared.availablePhpVersions = Actions.detectPhpVersions()
self.updatePhpVersionInStatusBar()
updatePhpVersionInStatusBar()
let installation = App.phpInstall!
installation.notifyAboutBrokenPhpFpm()
// 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 +63,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 +75,7 @@ class MainMenu: NSObject, NSWindowDelegate {
exit(1)
}
self.startup()
startup()
}
}
@ -77,7 +84,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 +101,17 @@ 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
statusItem.menu?.delegate = self
}
}
@ -110,7 +119,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 +145,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()
}
}
@ -155,81 +165,123 @@ class MainMenu: NSObject, NSWindowDelegate {
// MARK: - User Interface
@objc func updatePhpVersionInStatusBar() {
App.shared.currentInstall = PhpInstall()
DispatchQueue.main.async {
if (App.shared.busy) {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
App.shared.currentInstall = PhpInstallation()
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] as! Bool == 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()
})
} completion: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_restarted".localized,
subtitle: "notification.services_restarted_desc".localized
)
}
}
}
@objc func stopAllServices() {
waitAndExecute {
Actions.stopAllServices()
} completion: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_stopped".localized,
subtitle: "notification.services_stopped_desc".localized
)
}
}
}
@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() {
@ -243,42 +295,50 @@ class MainMenu: NSObject, NSWindowDelegate {
Actions.openPhpConfigFolder(version: App.phpInstall!.version.short)
}
@objc func openGlobalComposerFolder() {
Actions.openGlobalComposerFolder()
}
@objc func openValetConfigFolder() {
Actions.openValetConfigFolder()
}
@objc func switchToPhpVersion(sender: PhpMenuItem) {
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()
let completion = {
// Mark as no longer busy
App.shared.busy = false
// Perform UI updates on main thread
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),
subtitle: String(format: "notification.version_changed_desc".localized, sender.version)
)
App.phpInstall?.notifyAboutBrokenPhpFpm()
}
}
// Switch the PHP version
Actions.switchToPhpVersion(
version: sender.version,
availableVersions: App.shared.availablePhpVersions
availableVersions: App.shared.availablePhpVersions,
completed: completion
)
// Mark as no longer busy
App.shared.busy = false
// Perform UI updates on main thread
DispatchQueue.main.async {
self.updatePhpVersionInStatusBar()
self.update()
// Send a notification that the switch has been completed
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, sender.version),
subtitle: String(format: "notification.version_changed_desc".localized, sender.version)
)
}
}
}
@ -287,7 +347,23 @@ class MainMenu: NSObject, NSWindowDelegate {
NSApplication.shared.orderFrontStandardAboutPanel()
}
@objc func openPrefs() {
PrefsVC.show()
}
@objc func terminateApp() {
NSApplication.shared.terminate(nil)
}
// MARK: - Menu Delegate
func menuWillOpen(_ menu: NSMenu) {
// Make sure the shortcut key does not trigger this when the menu is open
App.shared.shortcutHotkey?.isPaused = true
}
func menuDidClose(_ menu: NSMenu) {
// When the menu is closed, allow the shortcut to work again
App.shared.shortcutHotkey?.isPaused = false
}
}

View File

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

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
}
@ -58,11 +58,15 @@ class StatusMenu : NSMenu {
private func addServicesMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_active_services".localized))
let services = NSMenuItem(title: "mi_restart_specific".localized, action: nil, keyEquivalent: "")
let services = NSMenuItem(title: "mi_manage_services".localized, action: nil, keyEquivalent: "")
let servicesMenu = NSMenu()
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
servicesMenu.addItem(
NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
withKeyModifier: [.command, .shift]
)
for item in servicesMenu.items {
item.target = MainMenu.shared
}
@ -81,6 +85,7 @@ class StatusMenu : NSMenu {
// Configuration
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
self.addItem(NSMenuItem(title: "mi_global_composer".localized, action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g"))
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
@ -93,9 +98,9 @@ class StatusMenu : NSMenu {
// Stats
self.addItem(NSMenuItem.separator())
self.addItem(StatsView.asMenuItem(
memory: stats.memory_limit,
post: stats.post_max_size,
upload: stats.upload_max_filesize)
memory: stats!.memory_limit,
post: stats!.post_max_size,
upload: stats!.upload_max_filesize)
)
// Extensions
@ -106,18 +111,33 @@ class StatusMenu : NSMenu {
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
}
var shortcutKey = 1
for phpExtension in App.phpInstall!.extensions {
self.addExtensionItem(phpExtension)
self.addExtensionItem(phpExtension, shortcutKey)
shortcutKey += 1
}
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r"))
}
private func addExtensionItem(_ phpExtension: PhpExtension) {
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
let menuItem = ExtensionMenuItem(
title: "\(phpExtension.name.capitalized) (php.ini)",
action: #selector(MainMenu.toggleExtension), keyEquivalent: ""
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
action: #selector(MainMenu.toggleExtension),
keyEquivalent: keyEquivalent
)
if menuItem.keyEquivalent != "" {
menuItem.keyEquivalentModifierMask = [.option]
}
menuItem.state = phpExtension.enabled ? .on : .off
menuItem.phpExtension = phpExtension
self.addItem(menuItem)
}
}

View File

@ -29,6 +29,11 @@ class PhpExtension {
/// Whether the extension has been enabled.
var enabled: Bool
/// The file where this extension was located, but only the filename, not the full path to the .ini file.
var fileNameOnly: String {
return String(file.split(separator: "/").last ?? "php.ini")
}
/**
This regular expression will allow us to identify lines which activate an extension.
@ -41,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>[a-zA-Z]*).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.
@ -52,7 +57,13 @@ class PhpExtension {
let range = Range(match!.range(withName: "name"), in: line)!
self.line = line
self.name = line[range]
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(";")
self.file = file
}
@ -61,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: self.file,
original: self.line,
replacement: self.enabled ? "; \(self.line)" : self.line.replacingOccurrences(of: "; ", with: "")
)
self.enabled = !self.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
@ -83,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)
}
}
}

View File

@ -0,0 +1,173 @@
//
// PhpInstallation.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
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 PhpInstallation {
var version: Version!
var configuration: Configuration!
var extensions: [PhpExtension]!
// MARK: - Computed
var formula: String {
return (version.short == App.shared.brewPhpVersion) ? "php" : "php@\(version.short)"
}
// MARK: - Initializer
init() {
// Show information about the current version
self.getVersion()
// If an error occurred, exit early
if (version.error) {
configuration = Configuration()
extensions = []
return
}
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path)
// Get configuration values
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")
)
// Return a list of .ini files parsed after php.ini
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
.replacingOccurrences(of: "\n", with: "")
.split(separator: ",")
.map { String($0) }
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
let extensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if extensions.count > 0 {
self.extensions.append(contentsOf: extensions)
}
}
}
/**
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
*/
private func getVersion() -> Void {
self.version = Version()
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
if (version == "" || version.contains("Warning") || version.contains("Error")) {
self.version.short = "💩 BROKEN"
self.version.long = ""
self.version.error = true
return
}
// That's the long version
self.version.long = version
// Next up, let's strip away the minor version number
let segments = self.version.long.components(separatedBy: ".")
// Get the first two elements
self.version.short = segments[0...1].joined(separator: ".")
}
/**
Retrieves the display value for a specific key in the `.ini` file.
The following values are valid:
* -1: unlimited (show the infinity icon)
* 10000: an integer = amount of bytes
* 1K, 1M, 1G = shorthand for kilobytes, megabytes and gigabytes
If none of these notations are used, the _fallback_ value is used. We'll show an emoji to indicate something has gone wrong here.
To clarify, B gets appended to valid values. As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes .
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
*/
private func getByteCount(key: String) -> String {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
// Check if the value is unlimited
if (value == "-1") {
return ""
}
// Check if the syntax is valid otherwise
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
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
)
}
}
}
/**
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?
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
return Shell.pipe("cat \(fileName)").contains("valet.sock")
}
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
return Shell.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
}
// MARK: - Structs
struct Version {
var short = "???"
var long = "???"
var error = false
}
struct Configuration {
var memory_limit = "???"
var upload_max_filesize = "???"
var post_max_size = "???"
}
}

View File

@ -0,0 +1,75 @@
//
// GlobalKeybindPreference.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
struct GlobalKeybindPreference: Codable, CustomStringConvertible {
// MARK: - Internal variables
let function : Bool
let control : Bool
let command : Bool
let shift : Bool
let option : Bool
let capsLock : Bool
let carbonFlags : UInt32
let characters : String?
let keyCode : UInt32
// MARK: - How the keybind is display in Preferences
var description: String {
var stringBuilder = ""
if self.function {
stringBuilder += "Fn"
}
if self.control {
stringBuilder += ""
}
if self.option {
stringBuilder += ""
}
if self.command {
stringBuilder += ""
}
if self.shift {
stringBuilder += ""
}
if self.capsLock {
stringBuilder += ""
}
if let characters = self.characters {
stringBuilder += characters.uppercased()
}
return "\(stringBuilder)"
}
// MARK: - Persisting data to UserDefaults (as JSON)
public func toJson() -> String {
let jsonData = try! JSONEncoder().encode(self)
return String(data: jsonData, encoding: .utf8)!
}
public static func fromJson(_ string: String?) -> GlobalKeybindPreference? {
if string == nil {
return nil
}
if let jsonData = string!.data(using: .utf8) {
let decoder = JSONDecoder()
do {
return try decoder.decode(GlobalKeybindPreference.self, from: jsonData)
} catch {
return nil
}
}
return nil
}
}

View File

@ -0,0 +1,56 @@
//
// 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"
case globalHotkey = "global_hotkey"
}
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: Any] {
Preferences.handleFirstTimeLaunch()
return [
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(
forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
.globalHotkey: UserDefaults.standard.string(
forKey: PreferenceName.globalHotkey.rawValue) as Any
]
}
static var preferences: [PreferenceName: Any?] {
return Preferences.retrieve()
}
static func update(_ preference: PreferenceName, value: Any?) {
if (value == nil) {
UserDefaults.standard.removeObject(forKey: preference.rawValue)
} else {
UserDefaults.standard.setValue(value, forKey: preference.rawValue)
}
UserDefaults.standard.synchronize()
}
}

View File

@ -0,0 +1,185 @@
//
// PrefsVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
import HotKey
import Carbon
class PrefsVC: NSViewController {
@IBOutlet weak var leftLabelDynamicIcon: NSTextField!
@IBOutlet weak var leftLabelGlobalShortcut: NSTextField!
@IBOutlet weak var buttonDynamicIcon: NSButton!
@IBOutlet weak var labelDynamicIcon: NSTextField!
@IBOutlet weak var buttonClose: NSButton!
@IBOutlet weak var buttonSetShortcut: NSButton!
@IBOutlet weak var buttonClearShortcut: NSButton!
@IBOutlet weak var labelShortcut: NSTextField!
// MARK: - Display
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 = PrefsWC(window: window)
}
App.shared.windowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
}
// MARK: - Lifecycle
override func viewWillAppear() {
loadLocalization()
loadDynamicIconFromPreferences()
loadGlobalKeybindFromPreferences()
}
override func viewWillDisappear() {
if self.listeningForGlobalHotkey {
listeningForGlobalHotkey = false
}
}
private func loadLocalization() {
// Dynamic icon
leftLabelDynamicIcon.stringValue = "prefs.dynamic_icon".localized
labelDynamicIcon.stringValue = "prefs.dynamic_icon_desc".localized
buttonDynamicIcon.title = "prefs.dynamic_icon_title".localized
// Global Shortcut
leftLabelGlobalShortcut.stringValue = "prefs.global_shortcut".localized
labelShortcut.stringValue = "prefs.shortcut_desc".localized
buttonSetShortcut.title = "prefs.shortcut_set".localized
buttonClearShortcut.title = "prefs.shortcut_clear".localized
// Close button
buttonClose.title = "prefs.close".localized
}
// MARK: - Dynamic Icon Preference
func loadDynamicIconFromPreferences() {
let shouldDisplay = Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == true
self.buttonDynamicIcon.state = shouldDisplay ? .on : .off
}
// MARK: - Shortcut Preference
// Adapted from: https://dev.to/mitchartemis/creating-a-global-configurable-shortcut-for-macos-apps-in-swift-25e9
var listeningForGlobalHotkey = false {
didSet {
if listeningForGlobalHotkey {
DispatchQueue.main.async { [weak self] in
self?.buttonSetShortcut.highlight(true)
self?.buttonSetShortcut.title = "prefs.shortcut_listening".localized
}
} else {
DispatchQueue.main.async { [weak self] in
self?.buttonSetShortcut.highlight(false)
self?.loadGlobalKeybindFromPreferences()
}
}
}
}
func loadGlobalKeybindFromPreferences() {
let globalKeybind = GlobalKeybindPreference.fromJson(Preferences.preferences[.globalHotkey] as! String?)
if (globalKeybind != nil) {
updateKeybindButton(globalKeybind!)
} else {
buttonSetShortcut.title = "prefs.shortcut_set".localized
}
buttonClearShortcut.isEnabled = globalKeybind != nil
}
func updateGlobalShortcut(_ event : NSEvent) {
self.listeningForGlobalHotkey = false
if let characters = event.charactersIgnoringModifiers {
let newGlobalKeybind = GlobalKeybindPreference.init(
function: event.modifierFlags.contains(.function),
control: event.modifierFlags.contains(.control),
command: event.modifierFlags.contains(.command),
shift: event.modifierFlags.contains(.shift),
option: event.modifierFlags.contains(.option),
capsLock: event.modifierFlags.contains(.capsLock),
carbonFlags: event.modifierFlags.carbonFlags,
characters: characters,
keyCode: UInt32(event.keyCode)
)
Preferences.update(.globalHotkey, value: newGlobalKeybind.toJson())
updateKeybindButton(newGlobalKeybind)
buttonClearShortcut.isEnabled = true
App.shared.shortcutHotkey = HotKey(
keyCombo: KeyCombo(
carbonKeyCode: UInt32(event.keyCode),
carbonModifiers: event.modifierFlags.carbonFlags
)
)
}
}
@IBAction func register(_ sender: Any) {
unregister(nil)
listeningForGlobalHotkey = true
view.window?.makeFirstResponder(nil)
}
@IBAction func unregister(_ sender: Any?) {
listeningForGlobalHotkey = false
App.shared.shortcutHotkey = nil
buttonSetShortcut.title = ""
Preferences.update(.globalHotkey, value: nil)
}
func updateClearButton(_ globalKeybindPreference: GlobalKeybindPreference?) {
if globalKeybindPreference != nil {
buttonClearShortcut.isEnabled = true
} else {
buttonClearShortcut.isEnabled = false
}
}
func updateKeybindButton(_ globalKeybindPreference: GlobalKeybindPreference) {
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()
}
// MARK: - Deinitialization
deinit {
print("VC deallocated")
}
}

View File

@ -0,0 +1,37 @@
//
// PrefsWC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 02/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
struct Keys {
static let Escape = 53
static let Space = 49
}
class PrefsWC: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
}
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)
if let vc = self.contentViewController as? PrefsVC {
if vc.listeningForGlobalHotkey {
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
print("A blacklisted key was pressed, canceling listen")
vc.listeningForGlobalHotkey = false
} else {
vc.updateGlobalShortcut(event)
}
}
}
}
}

View File

@ -1,61 +0,0 @@
//
// StateManager.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class App {
static let shared = App()
static var phpInstall: PhpInstall? {
return App.shared.currentInstall
}
static var busy: Bool {
return App.shared.busy
}
/**
Whether the application is busy switching versions.
*/
var busy: Bool = false
/**
The currently active installation of PHP.
*/
var currentInstall: PhpInstall? = nil
/**
All available versions of PHP.
*/
var availablePhpVersions : [String] = []
/**
The timer that will periodically fetch the PHP version that is currently active.
*/
var timer: Timer?
/**
Information we were able to discern from the Homebrew info command (as JSON).
*/
var brewPhpPackage: HomebrewPackage? = nil {
didSet {
self.brewPhpVersion = self.brewPhpPackage!.version
}
}
/**
The version that the `php` formula via Brew is aliased to on the current system.
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.
*/
var brewPhpVersion: String = "8.0"
}

View File

@ -14,8 +14,9 @@ class Command {
- Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on.
- Parameter trimNewlines: Removes empty new line output.
*/
public static func execute(path: String, arguments: [String]) -> String {
public static func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String {
let task = Process()
task.launchPath = path
task.arguments = arguments
@ -26,7 +27,14 @@ class Command {
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
return output;
if (trimNewlines) {
return output.components(separatedBy: .newlines)
.filter({ !$0.isEmpty })
.joined(separator: "\n")
}
return output
}
}

View File

@ -23,43 +23,46 @@ 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"
}
public static var phpConfig: String {
return "\(binPath)/php-config"
}
// - 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

@ -15,12 +15,30 @@ class Shell {
Shell.user.run(command)
}
public static func pipe(_ command: String, shell: String = "/bin/sh") -> String {
Shell.user.pipe(command, shell: shell)
public static func pipe(_ command: String) -> String {
return Shell.user.pipe(command)
}
// MARK: - Singleton
var shell: String
init() {
// Determine if we're using macOS Catalina or newer (that support /bin/zsh as default shell)
let at_least_10_15 = ProcessInfo.processInfo.isOperatingSystemAtLeast(
.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): defaulting to /bin/bash"
)
}
/**
Singleton to access a user shell (with --login)
*/
@ -34,7 +52,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)
}
/**
@ -43,11 +61,11 @@ class Shell {
- Parameter command: The command to run
- Parameter shell: Path to the shell to invoke
*/
func pipe(_ command: String, shell: String = "/bin/sh") -> String {
func pipe(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.launchPath = shell
task.launchPath = self.shell
task.arguments = ["--login", "-c", command]
task.standardOutput = pipe
task.launch()
@ -60,10 +78,10 @@ class Shell {
/**
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"
}
}

View File

@ -27,7 +27,7 @@
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2020 Nico Verbruggen. All rights reserved.</string>
<string>Copyright © 2021 Nico Verbruggen. All rights reserved.</string>
<key>NSMainStoryboardFile</key>
<string>Main</string>
<key>NSPrincipalClass</key>

View File

@ -22,9 +22,11 @@
"mi_restart_php_fpm" = "Restart service: php";
"mi_restart_nginx" = "Restart service: nginx";
"mi_restart_dnsmasq" = "Restart service: dnsmasq";
"mi_restart_specific" = "Restart specific service";
"mi_manage_services" = "Manage services";
"mi_restart_all_services" = "Restart all services";
"mi_stop_all_services" = "Stop all services";
"mi_force_load_latest" = "Force load latest PHP version";
"mi_php_refresh" = "Refresh information";
"mi_configuration" = "Configuration";
"mi_limits" = "Limits Configuration";
@ -34,17 +36,41 @@
"mi_valet_config" = "Locate Valet folder (.config/valet)";
"mi_php_config" = "Locate PHP configuration file (php.ini)";
"mi_global_composer" = "Locate global composer.json file (.composer)";
"mi_phpinfo" = "Show current configuration (phpinfo)";
"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" = "PHP Monitor";
"prefs.close" = "Close";
"prefs.global_shortcut" = "Global shortcut:";
"prefs.dynamic_icon" = "Dynamic icon:";
"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.shortcut_set" = "Set global shortcut";
"prefs.shortcut_listening" = "<listening for keypress>";
"prefs.shortcut_clear" = "Clear";
"prefs.shortcut_desc" = "If a shortcut combination is set up, you can toggle PHP Monitor\nwherever you are by pressing the key combination you chose.\n(Cancel choosing a shortcut by pressing the spacebar.)";
// 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.services_stopped" = "Valet services stopped";
"notification.services_stopped_desc" = "All services have been successfully stopped.";
"notification.services_restarted" = "Valet services restarted";
"notification.services_restarted_desc" = "All services have been successfully restarted.";
// ALERTS
@ -56,6 +82,10 @@
"alert.force_reload_done.title" = "PHP has been force reloaded";
"alert.force_reload_done.info" = "All appropriate services have been restarted, and the latest version of PHP is now active. You can now try switching to another version of PHP. If visiting sites still does not work, you may try running `valet install` again, this can fix a 502 issue (Bad Gateway).";
// PHP FPM Broken
"alert.php_fpm_broken.title" = "PHP-FPM configuration is incorrect";
"alert.php_fpm_broken.info" = "PHP Monitor has determined that there are issues with your PHP-FPM config: it's not pointing to the Valet socket. This will result in 502 Bad Gateway if you visit websites linked via Valet.\n\nYou can usually fix this by running\n`valet install`, which updates your\n PHP-FPM configuration.";
// PHP Monitor Cannot Start
"alert.cannot_start.title" = "PHP Monitor cannot start";
"alert.cannot_start.info" = "The issue you were just notified about is keeping PHP Monitor from functioning correctly. Please fix the issue and restart PHP Monitor. After clicking on OK, PHP Monitor will close.\n\nIf you have fixed the issue (or don't remember what the exact issue is) you can click on Retry, which will have PHP Monitor retry the startup checks.";
@ -70,11 +100,11 @@
/// 2. PHP not found in /usr/local/opt or /opt/homebrew/opt
"startup.errors.php_opt.title" = "PHP is not correctly installed";
"startup.errors.php_opt.desc" = "PHP alias was not found in `/usr/local/opt` (or `/opt/homebrew/opt`). The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php` in order for PHP Monitor to detect this installation.";
"startup.errors.php_opt.desc" = "PHP alias was not found in `/usr/local/opt` or `/opt/homebrew/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php` in order for PHP Monitor to detect this installation.";
/// 3. Valet not installed
"startup.errors.valet_executable.title" = "Laravel Valet is not correctly installed";
"startup.errors.valet_executable.desc" = "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue.";
"startup.errors.valet_executable.desc" = "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet` or `/opt/homebrew/bin/valet`. The app will not work correctly until you resolve this issue. (PHP Monitor checks for the existence of `valet` in either of these paths.)";
/// 4. Brew & sudoers
"startup.errors.sudoers_brew.title" = "Brew has not been added to sudoers.d";