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

Compare commits

..

51 Commits
v5.0b1 ... v5.0

Author SHA1 Message Date
94da7fb255 🚀 Version 5.0
This release brings a plethora of new features to PHP Monitor.
Please check out the release notes for more details.

🔀 Merge branch 'dev/5.x'
2022-02-01 19:59:56 +01:00
46d2d35c1a 👌 Detect Drupal and WordPress projects (#112) 2022-02-01 18:36:37 +01:00
cba41b29bc 🐛 Fix storyboard issues 2022-02-01 17:58:06 +01:00
7a8f47b995 👌 Quality of life changes
- Moved DonationUrl to Constants
- Added additional menu items (visible if window is open)
- Fixed capitalisation of "WordPress" in PhpFrameworks
- Cleanup Stats
- Add new translation strings for menu items
2022-02-01 17:51:23 +01:00
40062c5091 🔧 Bump version number for new RC 2022-01-31 18:17:15 +01:00
6081ef6b02 👌 Verify switch succeeded (#111)
- Verify switch was successful
- Suggest "Fix My Valet"
- Restart nginx when switching PHP versions
2022-01-31 17:56:20 +01:00
4cffe5a662 👌 Rename "Force Load PHP" to "Fix My Valet" (#111) 2022-01-31 12:41:41 +01:00
813aec2b42 👌 Disable message in beta builds 2022-01-30 19:32:32 +01:00
f2452bbc70 👌 Keep track of times (successfully) switched
Some users might not reboot their computer and in that situation they
will never see the message in bba961269c.

This has been remedied by also checking how many times the version
switch has occurred.

The thresholds for the alert are now:

- Must have launched the app at least 7 times
OR
- Must have switched PHP versions at least 40 times

If the alert has been seen, it'll never be shown again. For more info
please consult the linked commit for the rationale behind this change.
2022-01-30 19:27:44 +01:00
28fb685bfc 🐛 Fix refreshing of PHP version 2022-01-30 00:40:11 +01:00
0661ca00ff 📝 Update README and SECURITY 2022-01-29 23:03:19 +01:00
0b04619003 ✏️ Update TODO for 5.1 2022-01-29 22:49:30 +01:00
85d7b6aa57 🔧 Switch to regular version (release candidate) 2022-01-29 21:36:55 +01:00
bba961269c Add successful launch count, sponsor alert
Okay, so this commit adds a sponsor alert. I wanted to elaborate.

Why? At this point I've invested so much of my free time in the app that
any and all donations would be incredibly welcome. Of course, phpmon
as it exists today must always remain free and open source.

(I dislike it when an app goes open source and then becomes paid.)

Obviously, I don't want to take useful features away from users:

1) usage of the old version is the only option for those who won't pay
2) piracy is an alternative and I don't want to deal with that
3) the positive sentiment around the app disappears ("sellout!")

Instead, I will nicely ask for donations once the app has been
successfully launched 7 times or more. This alert should only
appear once.

Fun fact: PHP Monitor started  as a single menu item with only
options to switch between version numbers.

Thanks to all the support, it has now become so much more.

To those who have already contributed: thank you very much.
I hope you continue to use and enjoy the app.

Cheers!
2022-01-29 21:29:51 +01:00
d0f7d2c5e9 Handle >= and > constraints 2022-01-29 19:10:17 +01:00
eeeb3eb184 👌 Tweak strings to be completely accurate 2022-01-29 18:05:26 +01:00
f00f8d26f6 🐛 Enforce readable Valet version 2022-01-29 17:46:56 +01:00
74817beec6 🐛 Fix First Aid not working 2022-01-29 17:46:37 +01:00
7b6809245c 🐛 loopback might not exist (#104) 2022-01-29 17:05:00 +01:00
5b40a8fd41 👌 Updated goals, new asset, SwiftUI integration 2022-01-29 14:15:39 +01:00
193f459be1 ✏️ New comments 2022-01-29 14:13:38 +01:00
c4c19a5b47 📝 Updated README, new promo shot 2022-01-29 14:13:05 +01:00
7d103c70e7 📝 Updated README 2022-01-29 13:22:47 +01:00
2ffe90948e Add preference to disable integrations
I like the idea of the exposed phpmon:// protocol, but for those who
care about security it should be possible to disable the integrations.
2022-01-29 12:52:33 +01:00
8e61aaacde 🔧 Bump build 2022-01-29 00:12:38 +01:00
29c8fcbde2 👌 Force composer from /usr/local/bin (#102) 2022-01-29 00:11:12 +01:00
8dd21f46aa 🍱 Fix colors for dark mode (#101) 2022-01-28 23:40:00 +01:00
e688dde2aa 👌 Add example Alfred workflow 2022-01-28 22:12:35 +01:00
987e1e1bdb 🔧 Bump version number 2022-01-28 22:06:53 +01:00
510257c436 👌 Complete work on inter app handler
Allowed commands:

phpmon://list
phpmon://services/stop
phpmon://services/restart/all
phpmon://services/restart/nginx
phpmon://services/restart/php
phpmon://services/restart/dnsmasq
phpmon://locate/config
phpmon://locate/composer
phpmon://locate/valet
phpmon://phpinfo
phpmon://switch/php/{version}
2022-01-28 22:05:53 +01:00
bb1572f32a Allow switching PHP versions via callback 2022-01-28 17:42:40 +01:00
45276034b1 Add initial start for scheme integration 2022-01-28 17:01:40 +01:00
0d4a144524 👌 Cleanup HomebrewDiagnostics 2022-01-28 16:42:46 +01:00
a0e5102ca7 👌 Add some comments for curious code readers 2022-01-28 16:30:07 +01:00
69c0f5ace9 Have all tests pass, refactor comparison logic 2022-01-27 19:39:02 +01:00
d0962c2387 🐛 Show question mark if service not found 2022-01-27 18:23:12 +01:00
4670894cfd 👌 Driver not detected (localised) 2022-01-27 01:15:54 +01:00
a2f6c70a03 🔀 Merge branch 'dev/4.x' 2022-01-27 00:46:12 +01:00
ef469868d8 📝 Update README (no more Valet switcher in 5.0) 2022-01-27 00:44:36 +01:00
c9ba872529 ️ Fix laggy scrolling and search
(Partial backport for the stable build.)
2022-01-27 00:43:08 +01:00
1e15042be2 🐛 Start a "clean" terminal every time (#99)
(Backported for the stable build.)
2022-01-27 00:34:54 +01:00
7647978da5 🐛 Start a "clean" terminal every time (#99) 2022-01-26 23:49:32 +01:00
f82f3052f2 Add Flarum to framework list (#95) 2022-01-26 21:56:27 +01:00
10b299ff65 🐛 Check if services command can run 2022-01-26 21:00:52 +01:00
e4ff0418fd ️ Faster search, faster scrolling 2022-01-26 20:31:37 +01:00
a2b25e31ca 👌 Delayed loading of config.json 2022-01-26 19:47:00 +01:00
c4772db808 👌 Determine "driver" by reading composer file
This is much faster than checking the actual driver, which might take
a while if you have many sites. If we're just checking the actual
composer file (which is already parsed) this should be much faster.
2022-01-26 19:06:57 +01:00
38c2d9131b 📝 PR template 2022-01-04 20:49:57 +01:00
1566323fca 📝 PR template 2022-01-04 20:49:44 +01:00
bf0a923eb2 👌 Add more detail to full PHP version setting name (#78) 2022-01-04 19:41:01 +01:00
e372480249 🐛 Fix issue with Valet precedence (#77) 2022-01-03 17:01:51 +01:00
48 changed files with 1329 additions and 229 deletions

View File

@ -24,6 +24,11 @@
54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */; };
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */; };
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; };
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C40B24F327A310780018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
C40B24F527A3108B0018C7D2 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; };
C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
C40C7F202772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
@ -39,6 +44,8 @@
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
C40C7F3227722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; };
C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; };
C415D3B72770F294005EF286 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; };
C415D3B82770F294005EF286 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; };
C415D3E12770F34D005EF286 /* AllowedArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3DE2770F34D005EF286 /* AllowedArguments.swift */; };
@ -101,6 +108,7 @@
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 */; };
C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49E171E27A5736E00787921 /* PMServicesView.swift */; };
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4AF9F71275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
@ -142,19 +150,20 @@
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4D9ADCA277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; };
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C4EC1E6D279DF87A0010F296 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; };
C4EC1E6E279DF87A0010F296 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; };
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C4EC1E74279DFCF40010F296 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A627708B9E001DF387 /* PMHeaderView.swift */; };
C4EE55AA27708B9E001DF387 /* PMHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A627708B9E001DF387 /* PMHeaderView.swift */; };
C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A727708B9E001DF387 /* Preview.swift */; };
C4EE55AC27708B9E001DF387 /* Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A727708B9E001DF387 /* Preview.swift */; };
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A827708B9E001DF387 /* PMStatsView.swift */; };
C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A827708B9E001DF387 /* PMStatsView.swift */; };
C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EED88827A48778006D7272 /* InterAppHandler.swift */; };
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EED88827A48778006D7272 /* InterAppHandler.swift */; };
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
@ -179,7 +188,6 @@
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 */; };
@ -228,6 +236,7 @@
C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActivePhpInstallation+Checks.swift"; sourceTree = "<group>"; };
C40C7F2F27722E8D00DDDCDC /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = "<group>"; };
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFrameworks.swift; sourceTree = "<group>"; };
C415D3B62770F294005EF286 /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
C415D3D62770F341005EF286 /* phpmon-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "phpmon-cli"; sourceTree = BUILT_PRODUCTS_DIR; };
C415D3DE2770F34D005EF286 /* AllowedArguments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllowedArguments.swift; sourceTree = "<group>"; };
@ -273,6 +282,7 @@
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionNumberTest.swift; sourceTree = "<group>"; };
C4930849279F331F009C240B /* AddSiteVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteVC.swift; sourceTree = "<group>"; };
C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = "<group>"; };
C49E171E27A5736E00787921 /* PMServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMServicesView.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
C4AF9F70275445FF00D44ED0 /* valet-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "valet-config.json"; sourceTree = "<group>"; };
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigParserTest.swift; sourceTree = "<group>"; };
@ -295,6 +305,7 @@
C4D89BC52783C99400A02B68 /* ComposerJson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerJson.swift; sourceTree = "<group>"; };
C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpSwitcher.swift; sourceTree = "<group>"; };
C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalSwitcher.swift; sourceTree = "<group>"; };
C4DEB7D327A5D60B00834718 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.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>"; };
C4EC1E65279DE0380010F296 /* ServicesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ServicesView.xib; sourceTree = "<group>"; };
@ -305,6 +316,7 @@
C4EE55A627708B9E001DF387 /* PMHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMHeaderView.swift; sourceTree = "<group>"; };
C4EE55A727708B9E001DF387 /* Preview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preview.swift; sourceTree = "<group>"; };
C4EE55A827708B9E001DF387 /* PMStatsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMStatsView.swift; sourceTree = "<group>"; };
C4EED88827A48778006D7272 /* InterAppHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterAppHandler.swift; sourceTree = "<group>"; };
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = "<group>"; };
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = "<group>"; };
@ -353,6 +365,7 @@
5420395826135DC100FB00FA /* PrefsVC.swift */,
5420395E2613607600FB00FA /* Preferences.swift */,
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */,
C4DEB7D327A5D60B00834718 /* Stats.swift */,
C41CD0272628D8E20065BBED /* Keybinds */,
54FCFD28276C88C0004CE748 /* Views */,
);
@ -594,6 +607,7 @@
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */,
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
C4EED88827A48778006D7272 /* InterAppHandler.swift */,
);
path = Core;
sourceTree = "<group>";
@ -627,6 +641,7 @@
isa = PBXGroup;
children = (
C4D89BC52783C99400A02B68 /* ComposerJson.swift */,
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */,
);
path = Composer;
sourceTree = "<group>";
@ -651,6 +666,7 @@
C4EE55B027708BB2001DF387 /* SwiftUI */ = {
isa = PBXGroup;
children = (
C49E171E27A5736E00787921 /* PMServicesView.swift */,
C4EE55A627708B9E001DF387 /* PMHeaderView.swift */,
C4EE55A827708B9E001DF387 /* PMStatsView.swift */,
C4EE55A727708B9E001DF387 /* Preview.swift */,
@ -846,6 +862,7 @@
C40C7F2627721FA200DDDCDC /* Constants.swift in Sources */,
C4B585402770FE3900DA4FBE /* Paths.swift in Sources */,
C415D3E62770F540005EF286 /* main.swift in Sources */,
C40B24F327A310780018C7D2 /* Events.swift in Sources */,
C40C7F2227721F8200DDDCDC /* PhpInstallation.swift in Sources */,
C4B585432770FE3900DA4FBE /* Shell.swift in Sources */,
C4D9ADC1277610E1007277F4 /* PhpSwitcher.swift in Sources */,
@ -854,7 +871,6 @@
C40C7F2327721F8200DDDCDC /* ActivePhpInstallation.swift in Sources */,
C4B585462770FE3900DA4FBE /* Command.swift in Sources */,
C4D9ADCA277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4EC1E74279DFCF40010F296 /* Events.swift in Sources */,
C48D6C72279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C40C7F2527721F9800DDDCDC /* HomebrewPackage.swift in Sources */,
C417DC76277614690015E6EE /* Helpers.swift in Sources */,
@ -878,6 +894,7 @@
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */,
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */,
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */,
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
@ -897,6 +914,7 @@
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */,
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */,
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */,
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */,
@ -921,6 +939,7 @@
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */,
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */,
C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
@ -936,6 +955,7 @@
C464ADB2275A87CA003FCD53 /* SiteListCell.swift in Sources */,
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
C493084A279F331F009C240B /* AddSiteVC.swift in Sources */,
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -950,7 +970,6 @@
54AB03272763858F00A29D5F /* Timer.swift in Sources */,
54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
C415D3B82770F294005EF286 /* Actions.swift in Sources */,
C4EE55AC27708B9E001DF387 /* Preview.swift in Sources */,
54B48B60275F66AE006D90C5 /* Application.swift in Sources */,
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
C493084B279F331F009C240B /* AddSiteVC.swift in Sources */,
@ -967,11 +986,12 @@
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
C40B24F527A3108B0018C7D2 /* Async.swift in Sources */,
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */,
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */,
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */,
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
@ -984,6 +1004,7 @@
C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */,
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */,
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */,
C417DC75277614690015E6EE /* Helpers.swift in Sources */,
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
@ -995,12 +1016,15 @@
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */,
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */,
C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */,
C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */,
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
C40B24F227A310770018C7D2 /* Events.swift in Sources */,
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */,
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
C4B585452770FE3900DA4FBE /* Command.swift in Sources */,
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */,
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
@ -1189,12 +1213,12 @@
C41C1B4422B0098000E7CF16 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconBeta;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 500;
CURRENT_PROJECT_VERSION = 560;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -1203,8 +1227,8 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "5.0-b1";
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.beta;
MARKETING_VERSION = 5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
@ -1214,12 +1238,12 @@
C41C1B4522B0098000E7CF16 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconBeta;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 500;
CURRENT_PROJECT_VERSION = 560;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -1228,8 +1252,8 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "5.0-b1";
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.beta;
MARKETING_VERSION = 5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;

101
README.md
View File

@ -1,15 +1,17 @@
# PHP Monitor
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project.
> 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" />
<h1 align="center"><b>PHP Monitor</b> (phpmon)</h1>
**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.
<p align="center">
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
</p>
<img src="./docs/screenshot41.jpg" width="800px" alt="phpmon screenshot (menu bar app)"/>
**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 <u>you need to have it set up before you can use this</u>.
<small><i>Screenshot: A menu showing all of the functionality of PHP Monitor.</i></small>
<img src="./docs/screenshot50.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: Showing the key functionality of PHP Monitor. You can also add new domains as links, manage various services, and perform First Aid to fix all kinds of common PHP link issues.</i></small>
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)!
@ -19,7 +21,7 @@ PHP Monitor also gives you quick access to various useful functionality (like ac
## 🖥 System requirements
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
@ -30,7 +32,7 @@ _You may need to update your Valet installation to keep everything working if a
## 🚀 How to install
You can install via Homebrew (recommended), or may download the latest release on GitHub.
Again, make sure you have **Laravel Valet** installed first. Once that's done, you can install via Homebrew (recommended), or may download the latest release on GitHub.
To install via Homebrew, run:
@ -41,7 +43,11 @@ To upgrade your existing installation, run:
brew upgrade phpmon
_The app is signed and notarized, meaning all you have to do is approve its first launch._
(You may need to run `brew update` first in order to update the cask file if you ran a Homebrew operation recently.)
## 🔑 Is the app signed & notarized?
Yes, the app is signed and notarized, meaning all you have to do is approve its first launch (or whenever it updates).
## 👨‍💻 Why build this?
@ -51,10 +57,12 @@ Initially, I had an Alfred workflow for this — but it has now been replaced wi
## 🤬 The app won't start?!
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
**Follow instructions as specified in the alert in order to resolve any issues.**
(If the app crashes at launch without showing you any of these messages, you might have a non-standard Homebrew and Valet setup. Those are not supported.)
## 🙋‍♂️ FAQ & Troubleshooting
> 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.
@ -258,12 +266,63 @@ You can add your own apps by creating and editing a `~/.phpmon.conf.json` file,
You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary.
</details>
<details>
<summary><strong>How can the app integrate with third party tools, like Alfred?</strong></summary>
There's an Alfred workflow usually included in the release list, you can grab it by going to releases and downloading the asset `phpmon.alfredworkflow`.
If you'd like to integrate something yourself, all you need to to is use the `phpmon://` protocol and ensure that third party app integrations are enabled in Preferences (in PHP Monitor).
Using app callbacks, macOS and PHP Monitor allow for the following to be called:
* phpmon://list
* phpmon://services/stop
* phpmon://services/restart/all
* phpmon://services/restart/nginx
* phpmon://services/restart/php
* phpmon://services/restart/dnsmasq
* phpmon://locate/config
* phpmon://locate/composer
* phpmon://locate/valet
* phpmon://phpinfo
* phpmon://switch/php/{version}
</details>
<details>
<summary><strong>How does the app know what PHP version is required for my app?</strong></summary>
The `composer.json` file in the root of the folder (if it exists) is scanned and interpreted.
If the version is set in `platform`, it takes precendence.
If the version is not set in `platform` but it is in `require` (most common) then that version is used.
</details>
<details>
<summary><strong>What do the checkmarks next to the PHP version mean in the site list?</strong></summary>
You'll see a checkmark next to the version number if the currently enabled PHP version is compatible with the version required to run the site.
This is determined by evaluating the PHP requirement constraint (e.g. `^8.0`, `~8.0` or a specific version: `8.0`).
</details>
<details>
<summary><strong>Why can't I see the driver type any more? It says "Project Type" now.</strong></summary>
PHP Monitor currently checks your `composer.json` file to try to figure out what project you are running.
This approach is a lot faster than asking for a driver when you have many sites linked, but is slightly less reliable since the framework or type of project inferred via `composer.json` might not be 100% accurate.
You can always still ask Valet using the command line, should it be necessary. In my experience fetching the drivers slowed down the app unnecessarily.
</details>
<details>
<summary><strong>After running PHP Monitor, Homebrew sometimes has issues with `brew upgrade` or `brew cleanup`!</strong></summary>
<strike>This is a security feature of Homebrew. 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).</strike>
This is a security feature of Homebrew. 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).
**Update**: If you are using the Valet switcher (currently not available in the latest stable build) you will not encounter this issue. For more information on this, see [this issue](https://github.com/nicoverbruggen/phpmon/issues/17) and also [this issue about switching to Valet's switcher](https://github.com/nicoverbruggen/phpmon/issues/34).
If you would like to know more, consult [this issue](https://github.com/nicoverbruggen/phpmon/issues/85) for more information.
</details>
@ -272,6 +331,10 @@ You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor
Please get in touch and open an issue. PHP Monitor shouldn't crash... (unless you are actually removing PHP *while* the app is running, thats considered normal behaviour!)
If you would like to report a crash, please include the associated **log files** so I can find out what exactly went wrong.
To find the logs, take a look in `~/Library/Logs/DiagnosticReports` (in Finder) and see if there's any (log) files that start with "PHP Monitor".
</details>
## 📝 Having another issue?
@ -291,12 +354,10 @@ Donations really help with the Apple Developer Program cost, and keep me motivat
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)
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors)
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so
* 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
* Everyone who donated to keep the project up and running
* Everyone who left feedback via issues & who donated to keep the project up and running
Thank you very much for your contributions, kind words and support.
@ -326,7 +387,7 @@ If an extension or other process writes to a single file a bunch of times in a s
1. **Location of your sites**: PHP Monitor uses the Valet configuration file to determine which folders to look into. Each folder is scanned and then PHP Monitor will validate if a composer.json file exists to determine the desired PHP version.
1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name.
1. **Site drivers**: PHP Monitor runs `valet which` to determine which driver is currently in use for each individual site. This command is executed once for each site whenever the site list is refreshed.
1. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all.
*Note*: If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
@ -334,6 +395,10 @@ If an extension or other process writes to a single file a bunch of times in a s
If you want to know more about how this works, I recommend you check out the source code.
I have done my best to annotate as much as humanly possible, and have avoided using an overly complicated architecture to keep the code as easy to maintain as possible. The code isn't perfect by a long shot (lots of cleanup can still happen!) but the application works well.
I also have a few tests for key parts of the application that I found needed to be tested. In the future, I would like to add even more tests for some of the UI stuff, but for now the tests are more unit tests than feature tests.
## 🔧 Build instructions
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>

View File

@ -4,15 +4,15 @@
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | TBD |
| 5.0 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
## Legacy versions
These versions of PHP Monitor are no longer supported, but if youre using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

BIN
docs/screenshot50.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

View File

@ -88,7 +88,7 @@ class Actions {
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
}
// MARK: - Quick Fix
// MARK: - Fix My Valet
/**
Detects all currently available PHP versions,
@ -100,7 +100,7 @@ class Actions {
If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`.
*/
public static func fixMyPhp()
public static func fixMyValet()
{
brew("services restart dnsmasq", sudo: true)
@ -111,11 +111,14 @@ class Actions {
brew("services stop \(formula)", sudo: true)
}
brew("services stop dnsmasq")
brew("services stop php")
brew("services stop nginx")
brew("link php")
brew("link php --overwrite --force")
brew("services restart dnsmasq", sudo: true)
brew("services stop php", sudo: true)
brew("services stop nginx", sudo: true)
brew("services restart php", sudo: true)
brew("services restart nginx", sudo: true)
}
}

View File

@ -51,4 +51,9 @@ class Constants {
"8.2"
]
/**
The URL that people can visit if they wish to help support the project.
*/
static let DonationUrl = URL(string: "https://nicoverbruggen.be/sponsor#pay-now")!
}

View File

@ -8,8 +8,8 @@
import Foundation
/**
The `Paths` class is used to locate various binaries on the system,
and provides a full
The `Paths` class is used to locate various binaries on the system.
The path to the Homebrew directory and the user's name are fetched only once, at boot.
*/
public class Paths {
@ -17,8 +17,11 @@ public class Paths {
private var baseDir : Paths.HomebrewDir
private var userName : String
init() {
baseDir = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
}
// - MARK: Binaries
@ -42,7 +45,7 @@ public class Paths {
// - MARK: Paths
public static var whoami: String {
return String(Shell.pipe("whoami").split(separator: "\n")[0])
return shared.userName
}
public static var binPath: String {

View File

@ -123,7 +123,7 @@ public class Shell {
let task = Process()
task.launchPath = self.shell
task.arguments = ["--login", "-c", tailoredCommand]
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
return task
}

View File

@ -155,4 +155,16 @@ class PhpEnv {
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
/**
Validates whether the currently running version matches the provided version.
*/
public func validate(_ version: String) -> Bool {
if self.currentInstall.version.short == version {
print("Switching to version \(version) seems to have succeeded. Validation passed.")
return true
}
return false
}
}

View File

@ -32,7 +32,7 @@ public struct PhpVersionNumberCollection: Equatable {
https://getcomposer.org/doc/articles/versions.md#writing-version-constraints
- Parameter constraint: The full constraint as a string (e.g. "^7.0")
- Parameter strict: Whether the minor version check is strict. See more below.
- Parameter strict: Whether the patch version check is strict. See more below.
The strict mode does not matter if a patch version is provided for all versions in the collection.
@ -45,7 +45,8 @@ public struct PhpVersionNumberCollection: Equatable {
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will
be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK).
When checking against actual PHP versions installed by the user, use strict mode.
When checking against actual PHP versions installed by the user (with patch precision), use
strict mode.
**NON-STRICT MODE (= patch precision off)**
@ -58,43 +59,34 @@ public struct PhpVersionNumberCollection: Equatable {
public func matching(constraint: String, strict: Bool = false) -> [PhpVersionNumber] {
if let version = PhpVersionNumber.make(from: constraint, type: .versionOnly) {
// Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter {
$0.major == version.major
&& $0.minor == version.minor
&& (strict ? $0.patch(strict, version) == version.patch(strict) : true)
}
return self.versions.filter { $0.isSameAs(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
// Caret range means that the major version is never higher but minor version can be higher
// ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter {
$0.major == version.major &&
(
// Either the minor version is the same and the patch is higher or equal
$0.minor == version.minor && $0.patch(strict) >= version.patch(strict, $0)
// or the minor version number has been bumped
|| $0.minor > version.minor
)
}
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis.
if version.patch != nil {
return self.versions.filter {
version.patch != nil
// If a patch is provided then the minor version cannot be bumped.
return self.versions.filter {
$0.major == version.major && $0.minor == version.minor
&& $0.patch(strict, version) >= version.patch!
}
} else {
? $0.hasSameMajorAndMinorButNewerOrSamePatch(version, strict)
// If a patch is not provided then the major version cannot be bumped.
return self.versions.filter {
$0.major == version.major && $0.minor >= version.minor
}
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
}
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) }
}
return []
}
}
@ -104,7 +96,7 @@ public struct PhpVersionNumber: Equatable {
let minor: Int
let patch: Int?
public func patch(_ strictFallback: Bool, _ constraint: PhpVersionNumber? = nil) -> Int {
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
}
@ -116,6 +108,14 @@ public struct PhpVersionNumber: Equatable {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
// TODO: (5.1) Handle these cases (even though I suspect these are uncommon)
/*
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
*/
}
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
@ -138,4 +138,39 @@ public struct PhpVersionNumber: Equatable {
return nil
}
// MARK: Comparison Logic
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
}
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return (
self.major > version.major ||
self.major == version.major && self.minor > version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) > version.patch(strict)
)
}
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major &&
(
(self.minor == version.minor && self.patch(strict) >= version.patch(strict, self))
|| self.minor > version.minor
)
}
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict)
}
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor >= version.minor
}
}

View File

@ -50,6 +50,9 @@ class InternalSwitcher: PhpSwitcher {
brew("link \(formula) --overwrite --force")
brew("services start \(formula)", sudo: true)
Log.info("Restarting nginx, just to be sure!")
brew("services restart nginx", sudo: true)
Log.info("The new version has been linked!")
completion()
}

View File

@ -191,4 +191,86 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
}
func testCanCheckGreaterThanOrEqualConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">=7.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">=7.0.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">=7.2.5", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all
)
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">=7.2.5", strict: false),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2"]).all
)
}
func testCanCheckGreaterThanConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">7.0"),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">7.2.5"),
// 7.2 will be valid due to non-strict mode (resolves to 7.2.999)
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">7.2.5", strict: true),
// 7.2 will not be valid due to strict mode (resolves to 7.2.0)
PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
.matching(constraint: ">7.2.8"),
// 7.2 will be valid due to non-strict mode (resolves to 7.2.999)
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
.matching(constraint: ">7.2.8", strict: true),
// 7.2 will not be valid due to strict mode (resolves to 7.2.0)
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9"]).all
)
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.501",
"green" : "0.697",
"red" : "0.247"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.501",
"green" : "0.765",
"red" : "0.247"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.180",
"green" : "0.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.426",
"green" : "0.363",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -16,6 +16,7 @@
<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>
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
<br>
</body>
</html>

View File

@ -11,9 +11,37 @@ import Foundation
extension AppDelegate {
/**
This is an entry point for future development for integrating with the PHP Monitor
application URL. You can use the `phpmon://` protocol to communicate with the app.
At this time you can trigger the site list using Alfred (or some other application)
by opening the following URL: `phpmon://list`.
Please note that PHP Monitor needs to be running in the background for this to work.
*/
func application(_ application: NSApplication, open urls: [URL]) {
print(urls)
if !Preferences.isEnabled(.allowProtocolForIntegrations) {
Log.info("Acting on commands via phpmon:// has been disabled.")
return
}
guard let url = urls.first else { return }
self.interpretCommand(
url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""),
commands: InterApp.getCommands()
)
}
private func interpretCommand(_ command: String, commands: [InterApp.Action]) {
commands.forEach { action in
if command.starts(with: action.command) {
let lastElement = String(command.split(separator: "/").last!)
action.action(lastElement)
}
}
}
}

View File

@ -7,6 +7,7 @@
//
import Foundation
import AppKit
/**
Any outlets connected to the app's main menu (not the menu that shows when the icon in
@ -24,6 +25,13 @@ extension AppDelegate {
// MARK: - Menu Interactions
@IBAction func addSiteLinkPressed(_ sender: Any) {
SiteListVC.show()
guard let windowController = App.shared.siteListWindowController else { return }
windowController.pressedAddLink(nil)
}
@IBAction func reloadSiteListPressed(_ sender: Any) {
let vc = App.shared.siteListWindowController?
.window?.contentViewController as? SiteListVC
@ -37,4 +45,11 @@ extension AppDelegate {
}
}
@IBAction func focusSearchField(_ sender: Any) {
SiteListVC.show()
guard let windowController = App.shared.siteListWindowController else { return }
windowController.searchToolbarItem.searchField.becomeFirstResponder()
}
}

View File

@ -13,6 +13,10 @@ extension AppDelegate {
// MARK: - Notifications
/**
Sets up notifications. That does mean we need to ask for permission first.
If we cannot get permission, we should log this.
*/
public func setupNotifications() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self

View File

@ -4,6 +4,7 @@
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -49,11 +50,31 @@
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Sites" id="YTZ-bb-TOG">
<items>
<menuItem title="Reload Site List" keyEquivalent="r" id="Ema-AU-Nbr">
<menuItem title="add-as-link" keyEquivalent="n" id="du1-bO-N2U" userLabel="Add Link" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_add_folder_as_link"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="addSiteLinkPressed:" target="Voe-Tx-rLC" id="DzS-MY-6g0"/>
</connections>
</menuItem>
<menuItem title="reload-list" keyEquivalent="r" id="Ema-AU-Nbr" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_site_list"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="reloadSiteListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="2ux-8Q-UjK"/>
<menuItem title="focus-find" keyEquivalent="f" id="I95-fb-EL7" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_site_list"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
@ -298,7 +319,7 @@
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="-484" y="32"/>
<point key="canvasLocation" x="-495" y="-44"/>
</scene>
<!--Window Controller-->
<scene sceneID="PQa-AT-b2a">
@ -474,7 +495,7 @@ Gw
</string>
</buttonCell>
<connections>
<action selector="pressedCancel:" target="glS-wF-sEU" id="MZS-Vg-Vjf"/>
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
@ -544,6 +565,7 @@ Gw
<constraint firstAttribute="bottom" secondItem="SwS-o8-pbl" secondAttribute="bottom" constant="20" symbolic="YES" id="24Z-vC-4E8"/>
<constraint firstItem="900-Z2-tID" firstAttribute="centerY" secondItem="PVw-cM-qAB" secondAttribute="centerY" id="578-2f-4x8"/>
<constraint firstItem="ZX9-s1-23i" firstAttribute="leading" secondItem="6JT-Vt-3q0" secondAttribute="trailing" constant="-440" id="6eF-GS-Xcn"/>
<constraint firstItem="SwS-o8-pbl" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="900-Z2-tID" secondAttribute="trailing" constant="10" id="9uc-R7-CZk"/>
<constraint firstItem="6JT-Vt-3q0" firstAttribute="top" secondItem="P0B-Ht-R8n" secondAttribute="bottom" constant="8" symbolic="YES" id="DGN-4k-X0h"/>
<constraint firstItem="P0B-Ht-R8n" firstAttribute="top" secondItem="JJJ-T9-Yuv" secondAttribute="top" constant="20" symbolic="YES" id="F2r-6E-qxh"/>
<constraint firstItem="mmQ-7e-dlb" firstAttribute="top" secondItem="KZf-b0-9cm" secondAttribute="bottom" constant="8" symbolic="YES" id="G21-Vd-tgl"/>
@ -560,16 +582,18 @@ Gw
<constraint firstAttribute="trailing" secondItem="mmQ-7e-dlb" secondAttribute="trailing" constant="20" symbolic="YES" id="hjv-Xq-cxV"/>
<constraint firstItem="6JT-Vt-3q0" firstAttribute="leading" secondItem="P0B-Ht-R8n" secondAttribute="leading" id="jxP-vM-eA9"/>
<constraint firstItem="P0B-Ht-R8n" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="msC-eG-Fop"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="P0B-Ht-R8n" secondAttribute="trailing" constant="20" symbolic="YES" id="nvj-Ij-dcd"/>
<constraint firstItem="VzR-5a-cmT" firstAttribute="top" secondItem="ZX9-s1-23i" secondAttribute="bottom" constant="8" symbolic="YES" id="sVP-EV-07F"/>
<constraint firstAttribute="trailing" secondItem="ZX9-s1-23i" secondAttribute="trailing" constant="20" symbolic="YES" id="tZ3-2X-JC9"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="KZf-b0-9cm" secondAttribute="trailing" constant="20" symbolic="YES" id="zq0-Ce-sCs"/>
</constraints>
</view>
<connections>
<outlet property="buttonCancel" destination="SwS-o8-pbl" id="N1v-uy-2Mi"/>
<outlet property="buttonCreateLink" destination="PVw-cM-qAB" id="0Oo-xW-He7"/>
<outlet property="buttonSecure" destination="KZf-b0-9cm" id="5A7-Bn-NB7"/>
<outlet property="linkName" destination="ZX9-s1-23i" id="yT6-80-Zr1"/>
<outlet property="pathControl" destination="6JT-Vt-3q0" id="f5K-8h-VOd"/>
<outlet property="pressedCancel" destination="SwS-o8-pbl" id="cLR-Yn-TSs"/>
<outlet property="previewText" destination="VzR-5a-cmT" id="qwd-wX-645"/>
<outlet property="textFieldError" destination="900-Z2-tID" id="qUk-FE-IKW"/>
<outlet property="textFieldSecure" destination="mmQ-7e-dlb" id="LeA-YS-hRM"/>
@ -588,6 +612,10 @@ Gw
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<customView id="j65-Lf-0lG">
<rect key="frame" x="9" y="0.0" width="581" height="203"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</customView>
<scrollView autohidesScrollers="YES" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/>
<clipView key="contentView" id="6IL-DW-37w">
@ -690,7 +718,7 @@ Gw
<constraint firstAttribute="width" constant="14" id="wrl-lJ-3eN"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Checkmark" id="R5o-Cd-a91"/>
<color key="contentTintColor" name="systemGreenColor" catalog="System" colorSpace="catalog"/>
<color key="contentTintColor" name="IconColorGreen"/>
</imageView>
</subviews>
<constraints>
@ -723,6 +751,7 @@ Gw
<outlet property="imageViewPhpVersionOK" destination="5aN-ZI-D7U" id="ePz-Cb-dWk"/>
<outlet property="imageViewType" destination="0NQ-ZD-CqD" id="Cph-FN-LaY"/>
<outlet property="labelDriver" destination="TbX-e2-3QL" id="qJh-Ak-Dge"/>
<outlet property="labelDriverType" destination="jKi-Ls-7FZ" id="ZTq-pP-qUC"/>
<outlet property="labelPathName" destination="CXK-Q9-CpO" id="iVZ-cL-azB"/>
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
</connections>
@ -774,7 +803,7 @@ Gw
</viewController>
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="251" y="742"/>
<point key="canvasLocation" x="251" y="741.5"/>
</scene>
</scenes>
<resources>
@ -783,5 +812,8 @@ Gw
<image name="Lock" width="30" height="30"/>
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
<image name="plus" catalog="system" width="14" height="13"/>
<namedColor name="IconColorGreen">
<color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -0,0 +1,64 @@
//
// InterAppHandler.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 28/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class InterApp {
public static var bindings: [Action] = []
public static func register(_ action: Action) {
self.bindings.append(action)
}
public struct Action {
let command: String
let action: (String) -> Void
}
static func getCommands() -> [InterApp.Action] { return [
InterApp.Action(command: "list", action: { _ in
SiteListVC.show()
}),
InterApp.Action(command: "services/stop", action: { _ in
MainMenu.shared.stopAllServices()
}),
InterApp.Action(command: "services/restart/all", action: { _ in
MainMenu.shared.restartAllServices()
}),
InterApp.Action(command: "services/restart/nginx", action: { _ in
MainMenu.shared.restartNginx()
}),
InterApp.Action(command: "services/restart/php", action: { _ in
MainMenu.shared.restartPhpFpm()
}),
InterApp.Action(command: "services/restart/dnsmasq", action: { _ in
MainMenu.shared.restartDnsMasq()
}),
InterApp.Action(command: "locate/config", action: { _ in
MainMenu.shared.openActiveConfigFolder()
}),
InterApp.Action(command: "locate/composer", action: { _ in
MainMenu.shared.openGlobalComposerFolder()
}),
InterApp.Action(command: "locate/valet", action: { _ in
MainMenu.shared.openValetConfigFolder()
}),
InterApp.Action(command: "phpinfo", action: { _ in
MainMenu.shared.openPhpInfo()
}),
InterApp.Action(command: "switch/php/", action: { version in
if PhpEnv.shared.availablePhpVersions.contains(version) {
MainMenu.shared.switchToPhpVersion(version)
} else {
Alert.notify(message: "Unsupported version", info: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available.")
}
}),
]}
}

View File

@ -49,6 +49,21 @@ class Startup {
breaking: true
)
Valet.shared.version = VersionExtractor.from(valet("--version"))
performEnvironmentCheck(
Valet.shared.version == nil,
messageText: "startup.errors.valet_version_unknown.title".localized,
informativeText: "startup.errors.valet_version_unknown.desc".localized,
breaking: true
)
performEnvironmentCheck(
HomebrewDiagnostics.cannotLoadService(),
messageText: "startup.errors.services_json_error.title".localized,
informativeText: "startup.errors.services_json_error.desc".localized,
breaking: true
)
performEnvironmentCheck(
!Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"),
messageText: "startup.errors.sudoers_brew.title".localized,

View File

@ -16,3 +16,14 @@ extension NSMenu {
}
}
@IBDesignable class LocalizedMenuItem: NSMenuItem {
@IBInspectable
var localizationKey: String? {
didSet {
self.title = localizationKey?.localized ?? self.title
}
}
}

View File

@ -8,6 +8,15 @@
import Foundation
/**
This generic async helper is something I'd like to use in more places.
The `DispatchQueue.global` into `DispatchQueue.main.async` logic is common in the app.
I could also use try `async` support which was introduced in Swift but that would
require too much refactoring at this time to consider. I also need to read up on async
in order to properly grasp all the gotchas. Looking into that later at some point.
*/
public func runAsync(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
{
DispatchQueue.global(qos: .userInitiated).async {

View File

@ -65,6 +65,9 @@ class MenuBarImageGenerator {
return targetImage
}
/**
The same as before, but also attempts to add an icon to the left.
*/
public static func textToImageWithIcon(text: String) -> NSImage {
let textImage = self.textToImage(text: text)
let iconImage = NSImage(named: "StatusBarPHP")!

View File

@ -10,28 +10,35 @@ import Foundation
class VersionExtractor {
/**
This attempts to extract the version number from the command line output of Valet.
*/
public static func from(_ string: String) -> String? {
let regex = try! NSRegularExpression(
pattern: #"Laravel Valet (?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
options: []
)
let match = regex.matches(
in: string,
options: [],
range: NSMakeRange(0, string.count)
).first
guard let match = match else {
do {
let regex = try NSRegularExpression(
pattern: #"Laravel Valet (?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
options: []
)
let match = regex.matches(
in: string,
options: [],
range: NSMakeRange(0, string.count)
).first
guard let match = match else {
return nil
}
let range = Range(
match.range(withName: "version"),
in: string
)!
return String(string[range])
} catch {
return nil
}
let range = Range(
match.range(withName: "version"),
in: string
)!
return String(string[range])
}
}

View File

@ -8,11 +8,37 @@
import Foundation
/**
This `Decodable` class is used to directly map `composer.json`
to this object.
*/
struct ComposerJson: Decodable {
// MARK: - JSON structure
let dependencies: Dictionary<String, String>?
let devDependencies: Dictionary<String, String>?
let configuration: Config?
private enum CodingKeys: String, CodingKey {
case dependencies = "require"
case devDependencies = "require-dev"
case configuration = "config"
}
struct Config: Decodable {
let platform: Platform?
}
struct Platform: Decodable {
let php: String?
}
// MARK: - Helpers
/**
Checks what the PHP version constraint is.
Returns a tuple (constraint, location of constraint).
*/
public func getPhpVersion() -> (String, String) {
// Check if in platform
if configuration?.platform?.php != nil {
@ -25,12 +51,18 @@ struct ComposerJson: Decodable {
}
// Unknown!
return ("", "unknown")
return ("???", "unknown")
}
/**
Checks if any notable dependencies can be resolved.
Only notable dependencies are saved.
*/
public func getNotableDependencies() -> [String: String] {
var notable: [String: String] = [:]
let scan = ["php", "laravel/framework"]
var scan = Array(PhpFrameworks.DependencyList.keys)
scan.append("php")
scan.forEach { dependency in
if dependencies?[dependency] != nil {
@ -41,19 +73,6 @@ struct ComposerJson: Decodable {
return notable
}
private enum CodingKeys: String, CodingKey {
case dependencies = "require"
case devDependencies = "require-dev"
case configuration = "config"
}
struct Config: Decodable {
let platform: Platform?
}
struct Platform: Decodable {
let php: String?
}
}

View File

@ -0,0 +1,82 @@
//
// Frameworks.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 26/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
struct PhpFrameworks {
/**
This list should probably be reversed when checked, because some of these
will also require either `laravel/framework` or `symfony/symfony`.
*/
public static let DependencyList = [
// COMMON FRAMEWORKS
"laravel/framework" : "Laravel",
"symfony/symfony": "Symfony",
"laravel/lumen": "Lumen",
// VARIOUS CMS
"roots/bedrock": "Bedrock",
"cakephp/app": "CakePHP",
"craftcms/craft": "Craft",
"drupal/core": "Drupal",
"flarum/core": "Flarum",
"tightenco/jigsaw": "Jigsaw",
"joomla/uri": "Joomla",
"themsaid/katana": "Katana",
"getkirby/cms": "Kirby",
"october/october": "OctoberCMS",
"sculpin/sculpin": "Sculpin",
"statamic/cms": "Statamic",
"johnpbloch/wordpress-core": "WordPress",
"zendframework/zendframework": "Zend",
"zendframework/zend-mvc": "Zend"
// TODO (5.1): Handle these in v5.1
// "magento/*": "Magento",
// "concrete5/*": "Concrete5",
// "contao/*": "Contao",
// "slim/*": "Slim",
]
public static let FileMapping: [String: [String]] = [
"Drupal": [
// Legacy installations
"/misc/drupal.js",
"/core/lib/Drupal.php",
// The default (new) installation w/ Composer puts the modules in /web
"/web/misc/drupal.js",
"/web/core/lib/Drupal.php"
],
"WordPress": [
"/wp-config.php",
"/wp-config-sample.php"
],
]
/**
There are two cases where users are unlikely to use `composer`,
when setting up a Drupal or a WordPress project. For performance
reasons, we only check that here!
*/
public static func detectFallbackDependency(_ basePath: String) -> String? {
for entry in Self.FileMapping {
let found = entry.value
.map { path in return Filesystem.fileExists(basePath + path) }
.contains(true)
if found {
return entry.key
}
}
return nil
}
}

View File

@ -10,19 +10,6 @@ import Foundation
class HomebrewDiagnostics {
enum Errors: String {
case aliasConflict = "alias_conflict"
}
static let shared = HomebrewDiagnostics()
var errors: [HomebrewDiagnostics.Errors] = []
init() {
if determineAliasConflicts() {
errors.append(.aliasConflict)
}
}
/**
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
This will then result in two different aliases claiming to point to the same formula (`php`).
@ -30,7 +17,7 @@ class HomebrewDiagnostics {
This check only needs to be performed if the `shivammathur/php` tap is active.
*/
public func determineAliasConflicts() -> Bool
public static func hasAliasConflict() -> Bool
{
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
@ -67,4 +54,21 @@ class HomebrewDiagnostics {
return false
}
}
/**
In order to see if we support the --json syntax, we'll query nginx.
If the JSON response cannot be parsed, Homebrew is probably out of date.
*/
public static func cannotLoadService(_ name: String = "nginx") -> Bool
{
let serviceInfo = try? JSONDecoder().decode(
[HomebrewService].self,
from: Shell.pipe(
"sudo \(Paths.brew) services info \(name) --json",
requiresPath: true
).data(using: .utf8)!
)
return serviceInfo == nil
}
}

View File

@ -13,31 +13,48 @@ class Valet {
static let shared = Valet()
/// The version of Valet that was detected.
var version: String
var version: String! = nil
/// The Valet configuration file.
var config: Valet.Configuration
var config: Valet.Configuration!
/// A cached list of sites that were detected after analyzing the paths set up for Valet.
var sites: [Site] = []
/// Whether we're busy with some blocking operation.
var isBusy: Bool = false
/// When initialising the Valet singleton, extract the Valet version and assume no sites loaded.
init() {
version = VersionExtractor.from(valet("--version"))
?? "UNKNOWN"
self.version = nil
self.sites = []
}
/**
We don't want to load the initial config.json file as soon as the class is initialised.
Instead, we'll defer the loading of the configuration file once the initial app checks
have passed: if the user does not have Valet installed, we'll crash the app because we
force unwrap the file. Currently, this does also mean that if the JSON is invalid or
incompatible with the `Decodable` `Valet.Configuration` class, that the app will crash.
*/
public func loadConfiguration() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet/config.json")
// TODO: (5.1) Fix loading of invalid JSON: do not crash the app
config = try! JSONDecoder().decode(
Valet.Configuration.self,
from: try! String(contentsOf: file, encoding: .utf8).data(using: .utf8)!
)
self.sites = []
}
/**
Starts the preload of sites, but only if the maximum amount of sites is 30.
For users with more sites, the site list is loaded when they bring up the site list window.
(This is done to keep the startup speed as fast as possible.)
*/
public func startPreloadingSites() {
let maximumPreload = 10
let maximumPreload = 30
let foundSites = self.countPaths()
if foundSites <= maximumPreload {
// Preload the sites and their drivers
@ -48,23 +65,32 @@ class Valet {
}
}
/**
Reloads the list of sites, assuming that the list isn't being reloaded at the time.
We don't want to do duplicate or parallel work!
*/
public func reloadSites() {
if (isBusy) {
return
}
resolvePaths(tld: config.tld)
}
/**
Checks if the version of Valet is more recent than the minimum version required for PHP Monitor to function.
Should this procedure fail, the user will get an alert notifying them that the version of Valet they have
installed is not recent enough.
*/
public func validateVersion() -> Void {
if version == "UNKNOWN" {
return Log.warn("The Valet version could not be extracted... that does not bode well.")
}
if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending {
let version = version
Log.warn("Valet version \(version) is too old! (recommended: \(Constants.MinimumRecommendedValetVersion))")
Log.warn("Valet version \(version!) is too old! (recommended: \(Constants.MinimumRecommendedValetVersion))")
DispatchQueue.main.async {
Alert.notify(message: "alert.min_valet_version.title".localized, info: "alert.min_valet_version.info".localized(version, Constants.MinimumRecommendedValetVersion))
Alert.notify(message: "alert.min_valet_version.title".localized, info: "alert.min_valet_version.info".localized(version!, Constants.MinimumRecommendedValetVersion))
}
} else {
Log.info("Valet version \(version) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
Log.info("Valet version \(version!) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
}
}
@ -88,6 +114,8 @@ class Valet {
Resolves all paths and creates linked or parked site instances that can be referenced later.
*/
private func resolvePaths(tld: String) {
isBusy = true
sites = []
for path in config.paths {
@ -98,6 +126,8 @@ class Valet {
}
sites = sites.sorted { $0.absolutePath < $1.absolutePath }
isBusy = false
}
/**
@ -153,6 +183,13 @@ class Valet {
/// The absolute path to the directory that is served.
var absolutePath: String!
/// The absolute path to the directory that is served,
/// replacing the user's home folder with ~.
lazy var absolutePathRelative: String = {
return self.absolutePath
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
}()
/// Location of the alias. If set, this is a linked domain.
var aliasPath: String?
@ -162,12 +199,18 @@ class Valet {
/// What driver is currently in use. If not detected, defaults to nil.
var driver: String? = nil
/// Whether the driver was determined by checking the Composer file.
var driverDeterminedByComposer: Bool = false
/// A list of notable Composer dependencies.
var notableComposerDependencies: [String: String] = [:]
/// The PHP version as discovered in composer.json.
/// The PHP version as discovered in `composer.json`.
var composerPhp: String = "???"
/// Check whether the PHP version is valid for the currently linked version.
var composerPhpCompatibleWithLinked: Bool = false
/// How the PHP version was determined.
var composerPhpSource: String = "unknown"
@ -179,8 +222,8 @@ class Valet {
self.name = URL(fileURLWithPath: absolutePath).lastPathComponent
self.aliasPath = nil
determineSecured(tld)
determineDriver()
determineComposerPhpVersion()
determineDriver()
}
convenience init(aliasPath: String, tld: String) {
@ -189,28 +232,31 @@ class Valet {
self.name = URL(fileURLWithPath: aliasPath).lastPathComponent
self.aliasPath = aliasPath
determineSecured(tld)
determineDriver()
determineComposerPhpVersion()
determineDriver()
}
/**
Checks if a certificate file can be found in the `valet/Certificates` directory.
- Note: The file is not validated, only its presence is checked.
*/
public func determineSecured(_ tld: String) {
secured = Shell.fileExists("~/.config/valet/Certificates/\(self.name!).\(tld).key")
}
public func determineDriver() {
let driver = Shell.pipe("cd '\(absolutePath!)' && valet which", requiresPath: true)
if driver.contains("This site is served by") {
self.driver = driver
// TODO: Use a regular expression to retrieve the driver instead?
.replacingOccurrences(of: "This site is served by [", with: "")
.replacingOccurrences(of: "ValetDriver].\n", with: "")
} else {
self.driver = nil
}
}
/**
Checks if `composer.json` exists in the folder, and extracts notable information:
- The PHP version required (the constraint, so it could be `^8.0`, for example)
- Where the PHP version was found (`require` or `platform`)
- Notable PHP dependencies (determined via `PhpFrameworks.DependencyList`)
The method then also checks if the determined constraint (if found) is compatible
with the currently linked version of PHP (see `composerPhpMatchesSystem`).
*/
public func determineComposerPhpVersion() {
let path = "\(absolutePath!)/composer.json"
do {
if Filesystem.fileExists(path) {
let decoded = try JSONDecoder().decode(
@ -224,6 +270,60 @@ class Valet {
} catch {
Log.err("Something went wrong reading the composer JSON file.")
}
if self.composerPhp == "???" {
return
}
// Split the composer list (on "|") to evaluate multiple constraints
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
self.composerPhpCompatibleWithLinked =
self.composerPhp.split(separator: "|").map { string in
return PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.count > 0
}.contains(true)
}
/**
Determine the driver to be displayed in the list of sites. In v5.0, this has been changed
to load the "framework" or "project type" instead.
*/
public func determineDriver() {
self.determineDriverViaComposer()
if self.driver == nil {
self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath)
}
}
/**
Check the dependency list and see if a particular dependency can't be found.
We'll revert the dependency list so that Laravel and Symfony are detected last.
(Some other frameworks might use Laravel, so if we found it first the detection would be incorrect:
this would happen with Statamic, for example.)
*/
private func determineDriverViaComposer() {
self.driverDeterminedByComposer = true
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
if self.notableComposerDependencies.keys.contains(key) {
self.driver = value
}
}
}
@available(*, deprecated, renamed: "determineDriver")
private func determineDriverViaValet() {
let driver = Shell.pipe("cd '\(absolutePath!)' && valet which", requiresPath: true)
if driver.contains("This site is served by") {
self.driver = driver
.replacingOccurrences(of: "This site is served by [", with: "")
.replacingOccurrences(of: "ValetDriver].\n", with: "")
} else {
self.driver = nil
}
}
}
@ -235,8 +335,8 @@ class Valet {
/// The paths that need to be checked.
let paths: [String]
/// The loopback address.
let loopback: String
/// The loopback address. Optional.
let loopback: String?
/// The default site that is served if the domain is not found. Optional.
let defaultSite: String?

View File

@ -30,7 +30,7 @@ extension MainMenu {
private func onEnvironmentPass() {
PhpEnv.detectPhpVersions()
if HomebrewDiagnostics.shared.errors.contains(.aliasConflict) {
if HomebrewDiagnostics.hasAliasConflict() {
DispatchQueue.main.async {
Alert.notify(
message: "alert.php_alias_conflict.title".localized,
@ -69,8 +69,11 @@ extension MainMenu {
App.shared.loadGlobalHotkey()
// Attempt to find out more info about Valet
Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version)")
if Valet.shared.version != nil {
Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)")
}
Valet.shared.loadConfiguration()
Valet.shared.validateVersion()
Valet.shared.startPreloadingSites()
@ -88,6 +91,9 @@ extension MainMenu {
repeats: true
)
}
Stats.incrementSuccessfulLaunchCount()
Stats.evaluateSponsorMessageShouldBeDisplayed()
}
/**

View File

@ -104,6 +104,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
PhpEnv.shared.isBusy = false
DispatchQueue.main.async { [self] in
PhpEnv.shared.currentInstall = ActivePhpInstallation()
updatePhpVersionInStatusBar()
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
completion()
@ -233,21 +234,26 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
}
}
@objc func forceRestartLatestPhp() {
@objc func fixMyValet() {
// 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
waitAndExecute {
Actions.fixMyPhp()
} completion: {
Alert.notify(
message: "alert.force_reload_done.title".localized,
info: "alert.force_reload_done.info".localized
)
if Alert.present(
messageText: "alert.fix_my_valet.title".localized,
informativeText: "alert.fix_my_valet.info".localized(PhpEnv.brewPhpVersion),
buttonTitle: "alert.fix_my_valet.ok".localized,
secondButtonTitle: "alert.fix_my_valet.cancel".localized,
style: .warning
) {
// Start the fix
waitAndExecute {
Actions.fixMyValet()
} completion: {
Alert.notify(
message: "alert.fix_my_valet_done.title".localized,
info: "alert.fix_my_valet_done.info".localized
)
}
} else {
Log.info("The user has chosen to abort Fix My Valet")
}
}
@ -278,6 +284,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
self.switchToPhpVersion(sender.version)
}
// TODO (5.1): Investigate if `waitAndExecute` cannot be used here
@objc func switchToPhpVersion(_ version: String) {
setBusyImage()
PhpEnv.shared.isBusy = true
@ -289,7 +296,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
// Update the menu
rebuild()
let sendLocalNotification = {
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, version),
subtitle: String(format: "notification.version_changed_desc".localized, version)
)
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
}
let completion = {
// Fire off the delegate method
PhpEnv.shared.delegate?.switcherDidCompleteSwitch()
// Mark as no longer busy
@ -300,12 +316,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
updatePhpVersionInStatusBar()
rebuild()
let sendLocalNotification = {
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, version),
subtitle: String(format: "notification.version_changed_desc".localized, version)
)
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
if !PhpEnv.shared.validate(version) {
let outcome = Alert.present(
messageText: "alert.php_switch_failed.title".localized(version),
informativeText: "alert.php_switch_failed.info".localized(version),
buttonTitle: "alert.php_switch_failed.confirm".localized,
secondButtonTitle: "alert.php_switch_failed.cancel".localized, style: .informational)
if outcome {
MainMenu.shared.fixMyValet()
}
return
}
// Run composer updates
@ -314,6 +334,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
} else {
sendLocalNotification()
}
// Update stats
Stats.incrementSuccessfulSwitchCount()
Stats.evaluateSponsorMessageShouldBeDisplayed()
}
}
@ -337,6 +361,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
SiteListVC.show()
}
@objc func openDonate() {
NSWorkspace.shared.open(Constants.DonationUrl)
}
@objc func terminateApp() {
NSApplication.shared.terminate(nil)
}
@ -356,9 +384,17 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
// MARK: - Private Methods
/**
Updates the global dependencies and runs the completion callback when done.
*/
private func updateGlobalDependencies(notify: Bool, completion: @escaping (Bool) -> Void) {
if !Shell.fileExists("/usr/local/bin/composer") {
Alert.notify(
message: "alert.composer_missing.title".localized,
info: "alert.composer_missing.info".localized
)
return
}
PhpEnv.shared.isBusy = true
setBusyImage()
self.rebuild()
@ -378,14 +414,12 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
window?.setType(info: true)
DispatchQueue.global(qos: .userInitiated).async {
let output = Shell.user.executeSynchronously(
"composer global update", requiresPath: true
let task = Shell.user.createTask(
for: "/usr/local/bin/composer global update", requiresPath: true
)
let task = Shell.user.createTask(for: "composer global update", requiresPath: true)
DispatchQueue.main.async {
window?.addToConsole("composer global update\n")
window?.addToConsole("/usr/local/bin/composer global update\n")
}
Shell.captureOutput(
@ -409,7 +443,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
Shell.haltCapturingOutput(task)
DispatchQueue.main.async {
if output.task.terminationStatus <= 0 {
if task.terminationStatus <= 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
window?.close()
if (notify) {

View File

@ -9,6 +9,16 @@
import Foundation
import Cocoa
/**
The ServicesView is an example of a view that I consider to be "poorly" set up.
Why ship it like this, then? Well, it works that's reason number one, really.
However, I do believe this should be refactored at some point. Here's why:
this view is responsible for retaining the information about the services status.
The status of the services should live somewhere else, and the fetching of said
service information should also not happen in a view. Yet here we are.
*/
class ServicesView: NSView, XibLoadable {
@IBOutlet weak var imageViewPhp: NSImageView!
@ -21,6 +31,9 @@ class ServicesView: NSView, XibLoadable {
static func asMenuItem() -> NSMenuItem {
let view = Self.createFromXib()!
[view.imageViewPhp, view.imageViewNginx, view.imageViewDnsmasq].forEach { imageView in
imageView?.contentTintColor = NSColor(named: "IconColorNormal")
}
let item = NSMenuItem()
item.view = view
item.target = self
@ -41,6 +54,7 @@ class ServicesView: NSView, XibLoadable {
self.loadData()
}
// TODO: (5.1) Move data fetching, caching & retrieval somewhere else
func loadData() {
// Use stale data
self.applyAllInfoFieldsFromCachedValue()
@ -78,13 +92,20 @@ class ServicesView: NSView, XibLoadable {
}
func applyServiceStyling(_ serviceName: String, _ imageView: NSImageView) {
if ServicesView.services[serviceName] != nil && ServicesView.services[serviceName]!.running {
imageView.image = NSImage(named: "ServiceOn")
imageView.contentTintColor = NSColor.black
} else {
imageView.image = NSImage(named: "ServiceOff")
imageView.contentTintColor = NSColor.init(red: 246/255, green: 71/255, blue: 71/255, alpha: 1.0)
if ServicesView.services[serviceName] == nil {
imageView.image = NSImage(named: "ServiceLoading")
imageView.contentTintColor = NSColor(named: "IconColorNormal")
return
}
if ServicesView.services[serviceName]!.running {
imageView.image = NSImage(named: "ServiceOn")
imageView.contentTintColor = NSColor(named: "IconColorNormal")
return
}
imageView.image = NSImage(named: "ServiceOff")
imageView.contentTintColor = NSColor(named: "IconColorRed")
}
deinit {

View File

@ -45,13 +45,13 @@ class StatusMenu : NSMenu {
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
servicesMenu.addItem(NSMenuItem(
title: "mi_force_load_latest_unavailable".localized(PhpEnv.brewPhpVersion),
title: "mi_fix_my_valet_unavailable".localized(PhpEnv.brewPhpVersion),
action: nil, keyEquivalent: "f"
))
} else {
servicesMenu.addItem(NSMenuItem(
title: "mi_force_load_latest".localized(PhpEnv.brewPhpVersion),
action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: "f"))
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
action: #selector(MainMenu.fixMyValet), keyEquivalent: "f"))
}
servicesMenu.addItem(NSMenuItem(title: "mi_services".localized, action: nil, keyEquivalent: ""))
@ -95,7 +95,11 @@ class StatusMenu : NSMenu {
self.addItem(NSMenuItem.separator())
self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized))
self.addItem(NSMenuItem(title: "mi_global_composer".localized, action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g"))
self.addItem(NSMenuItem(title: "mi_update_global_composer".localized, action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies), keyEquivalent: ""))
let composerMenuItem = NSMenuItem(title: "mi_update_global_composer".localized, action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies), keyEquivalent: "g")
composerMenuItem.keyEquivalentModifierMask = .shift
self.addItem(composerMenuItem)
if (PhpEnv.shared.isBusy) {
return

View File

@ -8,6 +8,9 @@
import Foundation
/**
These are the keys used for every preference in the app.
*/
enum PreferenceName: String {
case wasLaunchedBefore = "launched_before"
case shouldDisplayDynamicIcon = "use_dynamic_icon"
@ -15,9 +18,19 @@ enum PreferenceName: String {
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch"
case allowProtocolForIntegrations = "allow_protocol_for_integrations"
case globalHotkey = "global_hotkey"
}
/**
These are internal stats. They NEVER get shared.
*/
enum InternalStats: String {
case launchCount = "times_launched"
case switchCount = "times_switched_versions"
case didSeeSponsorEncouragement = "did_see_sponsor_encouragement"
}
class Preferences {
// MARK: - Singleton
@ -49,16 +62,23 @@ class Preferences {
*/
static func handleFirstTimeLaunch() {
UserDefaults.standard.register(defaults: [
/// Preferences
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
PreferenceName.shouldDisplayPhpHintInIcon.rawValue: true,
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true,
PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue: false
PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue: false,
PreferenceName.allowProtocolForIntegrations.rawValue: true,
/// Stats
InternalStats.switchCount.rawValue: 0,
InternalStats.launchCount.rawValue: 0,
InternalStats.didSeeSponsorEncouragement.rawValue: false
])
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
return
}
Log.info("Saving first-time preferences!")
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
UserDefaults.standard.synchronize()
@ -96,6 +116,7 @@ class Preferences {
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
.autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any,
.allowProtocolForIntegrations: UserDefaults.standard.bool(forKey: PreferenceName.allowProtocolForIntegrations.rawValue) as Any,
// Part 2: Always Strings
.globalHotkey: UserDefaults.standard.string(forKey: PreferenceName.globalHotkey.rawValue) as Any,

View File

@ -94,7 +94,14 @@ class PrefsVC: NSViewController {
sectionText: "prefs.global_shortcut".localized,
descriptionText: "prefs.shortcut_desc".localized,
self
)
),
CheckboxPreferenceView.make(
sectionText: "prefs.integrations".localized,
descriptionText: "prefs.open_protocol_desc".localized,
checkboxText: "prefs.open_protocol_title".localized,
preference: .allowProtocolForIntegrations,
action: {}
),
].forEach({ self.stackView.addArrangedSubview($0) })
}

View File

@ -0,0 +1,116 @@
//
// Stats.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class Stats {
/**
Keep track of how many times the app has been successfully launched.
This is used to determine whether it is time to show the sponsor
encouragement alert, but I'd like to include this stat somewhere
else as well.
*/
public static var successfulLaunchCount: Int {
UserDefaults.standard.integer(
forKey: InternalStats.launchCount.rawValue
)
}
/**
Keep track of how many times the app has successfully switched
between different PHP versions.
This is used to determine whether it is time to show the sponsor
encouragement alert, but I'd like to include this stat somewhere
else as well.
*/
public static var successfulSwitchCount: Int {
UserDefaults.standard.integer(
forKey: InternalStats.switchCount.rawValue
)
}
/**
Did the user see the sponsor encouragement / thank you message?
Annoying the user is the worst, so let's not show the message twice.
*/
public static var didSeeSponsorEncouragement: Bool {
UserDefaults.standard.bool(
forKey: InternalStats.didSeeSponsorEncouragement.rawValue
)
}
/**
Increment the successful launch count. This should only be
called when the user has not encountered ANY issues starting
up the application.
*/
public static func incrementSuccessfulLaunchCount() {
UserDefaults.standard.set(
Stats.successfulLaunchCount + 1,
forKey: InternalStats.launchCount.rawValue
)
}
/**
Increment the successful switch count.
*/
public static func incrementSuccessfulSwitchCount() {
UserDefaults.standard.set(
Stats.successfulSwitchCount + 1,
forKey: InternalStats.switchCount.rawValue
)
}
/**
Determine if the sponsor message should be displayed.
The rationale behind this is simple, some of the stats
increasing beyond a certain point indicate the app
is being used.
We evaluate, first:
- Successful version switches
OR
- Successful starts of the application
AND, of course, you must never have seen the alert before.
(see `didSeeSponsorEncouragement`)
*/
public static func evaluateSponsorMessageShouldBeDisplayed() {
if Bundle.main.bundleIdentifier?.contains("beta") ?? false {
return Log.info("Sponsor messages never apply to beta builds.")
}
if Stats.didSeeSponsorEncouragement {
return Log.info("Awesome, the user has already seen the sponsor message.")
}
if Stats.successfulLaunchCount < 7 && Stats.successfulSwitchCount < 40 {
return Log.info("It is too soon to see the sponsor message (launched \(Stats.successfulLaunchCount) times, switched \(Stats.successfulSwitchCount) times).")
}
DispatchQueue.main.async {
let donate = Alert.present(
messageText: "startup.sponsor_encouragement.title".localized,
informativeText: "startup.sponsor_encouragement.desc".localized,
buttonTitle: "startup.sponsor_encouragement.accept".localized,
secondButtonTitle: "startup.sponsor_encouragement.skip".localized,
style: .informational)
if donate {
Log.info("The user is an absolute badass for choosing this option. Thank you.")
NSWorkspace.shared.open(Constants.DonationUrl)
}
UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue)
}
}
}

View File

@ -16,6 +16,7 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
@IBOutlet weak var previewText: NSTextField!
@IBOutlet weak var buttonSecure: NSButton!
@IBOutlet weak var buttonCreateLink: NSButton!
@IBOutlet weak var buttonCancel: NSButton!
@IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var textFieldSecure: NSTextField!
@ -39,6 +40,7 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
textFieldTitle.stringValue = "site_list.add.link_folder".localized
linkName.placeholderString = "site_list.add.domain_name_placeholder".localized
textFieldSecure.stringValue = "site_list.add.secure_description".localized
buttonCancel.stringValue = "site_list.add.cancel".localized
}
// MARK: - Outlet Interactions

View File

@ -15,6 +15,7 @@ class SiteListCell: NSTableCellView
@IBOutlet weak var labelSiteName: NSTextField!
@IBOutlet weak var labelPathName: NSTextField!
@IBOutlet weak var labelDriverType: NSTextField!
@IBOutlet weak var imageViewLock: NSImageView!
@IBOutlet weak var imageViewType: NSImageView!
@ -35,8 +36,7 @@ class SiteListCell: NSTableCellView
labelSiteName.stringValue = "\(site.name!).\(Valet.shared.config.tld)"
// Show the absolute path, except make sure to replace the /Users/username segment with ~ for readability
labelPathName.stringValue = site.absolutePath
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
labelPathName.stringValue = site.absolutePathRelative
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
imageViewType.image = NSImage(
@ -47,13 +47,17 @@ class SiteListCell: NSTableCellView
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
// Show the green or red lock based on whether the site was secured
imageViewLock.image = NSImage(named: site.secured ? "Lock" : "LockUnlocked")
// imageViewLock.image = NSImage(named: site.secured ? "Lock" : "LockUnlocked")
imageViewLock.contentTintColor = site.secured ?
NSColor.init(red: 63/255, green: 195/255, blue: 128/255, alpha: 1.0) // green
: NSColor.init(red: 246/255, green: 71/255, blue: 71/255, alpha: 1.0) // red
NSColor(named: "IconColorGreen") // green
: NSColor(named: "IconColorRed")
// Show the current driver
labelDriver.stringValue = "\(site.driver ?? "???")"
labelDriverType.stringValue = site.driverDeterminedByComposer
? "Project Type".uppercased()
: "Driver Type".uppercased()
labelDriver.stringValue = site.driver ?? "driver.not_detected".localized
// Determine the Laravel version
if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") {
@ -65,15 +69,8 @@ class SiteListCell: NSTableCellView
buttonPhpVersion.title = " PHP \(site.composerPhp) "
buttonPhpVersion.isHidden = (site.composerPhp == "???")
// Split the composer list (on "|") to evaluate multiple constraints
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
let matchesConstraint = site.composerPhp.split(separator: "|").map { string in
return PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.count > 0
}.contains(true)
imageViewPhpVersionOK.isHidden = (site.composerPhp == "???" || !matchesConstraint)
imageViewPhpVersionOK.isHidden = (site.composerPhp == "???" || !site.composerPhpCompatibleWithLinked)
}
@IBAction func pressedPhpVersion(_ sender: Any) {

View File

@ -93,6 +93,7 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
progressIndicator.startAnimation(nil)
tableView.alphaValue = 0.3
tableView.isEnabled = false
tableView.selectRowIndexes([], byExtendingSelection: true)
}
/**
@ -201,8 +202,14 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
return
}
let splitSearchString: [String] = searchString
.split(separator: " ")
.map { return String($0) }
sites = Valet.shared.sites.filter({ site in
return site.name.lowercased().contains(searchString)
return !splitSearchString.map { searchString in
return site.name.lowercased().contains(searchString)
}.contains(false)
})
DispatchQueue.main.async {

View File

@ -43,7 +43,7 @@ class SiteListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
self.searchTimer?.invalidate()
searchTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false, block: { _ in
searchTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false, block: { _ in
self.contentVC.searchedFor(text: searchField.stringValue)
})
}
@ -78,7 +78,7 @@ class SiteListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
}
func showSitePopup(_ folder: String) {
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
let storyboard = NSStoryboard(name: "Main", bundle : nil)
let windowController = storyboard.instantiateController(
withIdentifier: "addSiteWindow"

View File

@ -0,0 +1,25 @@
//
// PMHeaderView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import SwiftUI
@available(OSX 11.0, *)
struct PMServicesView: View {
var body: some View {
PMServices().frame(minWidth: 0, maxWidth: 450, minHeight: 0, maxHeight: 50)
}
}
@available(OSX 11.0, *)
struct PMServices: NSViewRepresentable {
func makeNSView(context: Context) -> some NSView {
return ServicesView.asMenuItem().view!
}
func updateNSView(_ nsView: NSViewType, context: Context) {}
}

View File

@ -15,5 +15,6 @@ struct Preview_Previews: PreviewProvider {
PMHeaderView(content: "You are running PHP 8.1")
PMStatsView(content: "15 MB")
PMStatsView(content: "2 GB")
PMServicesView() // uses live services data!
}
}

View File

@ -0,0 +1,75 @@
//
// Paths.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
enum HomebrewDir: String {
case opt = "/opt/homebrew"
case usr = "/usr/local"
}
class Paths {
static let shared = Paths()
var baseDir : HomebrewDir
var userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
init() {
let optBrewFound = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew")
let usrBrewFound = Shell.fileExists("\(HomebrewDir.usr.rawValue)/bin/brew")
if (optBrewFound) {
// This is usually the case with Homebrew installed on Apple Silicon
baseDir = .opt
} else if (usrBrewFound) {
// This is usually the case with Homebrew installed on Intel (or Rosetta 2)
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?)")
baseDir = .usr
}
}
// - MARK: Binaries
public static var valet: String {
return "\(binPath)/valet"
}
public static var brew: String {
return "\(binPath)/brew"
}
public static var php: String {
return "\(binPath)/php"
}
public static var phpConfig: String {
return "\(binPath)/php-config"
}
// - MARK: Paths
public static var whoami: String {
return shared.userName
}
public static var binPath: String {
return "\(shared.baseDir.rawValue)/bin"
}
public static var optPath: String {
return "\(shared.baseDir.rawValue)/opt"
}
public static var etcPath: String {
return "\(shared.baseDir.rawValue)/etc"
}
}

View File

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

View File

@ -25,8 +25,8 @@
"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 %@";
"mi_force_load_latest_unavailable" = "Force Load Unavailable (PHP %@ Not Installed)";
"mi_fix_my_valet" = "Fix My Valet (PHP & Services)";
"mi_fix_my_valet_unavailable" = "Fix My Valet Unavailable";
"mi_php_refresh" = "Refresh Information";
"mi_configuration" = "PHP Configuration";
@ -52,9 +52,16 @@
"mi_sitelist" = "View Linked and Parked Domains...";
"mi_preferences" = "Preferences...";
"mi_donate" = "Donate...";
"mi_quit" = "Quit PHP Monitor";
"mi_about" = "About PHP Monitor";
// MENU ITEMS (if window is open)
"mm_add_folder_as_link" = "Add Folder as Link...";
"mm_reload_site_list" = "Reload Site List";
"mm_find_in_site_list" = "Search in Site List";
// SITE LIST
"site_list.title" = "Domains";
@ -92,7 +99,6 @@
"site_list.alert.folder_missing.cancel" = "Cancel Link";
"site_list.alert.folder_missing.return" = "OK";
"site_list.add.modal_description" = "First, select which folder you would like to link.";
// SITE LIST ACTIONS
@ -111,6 +117,10 @@
"site_list.alert.invalid_folder_name" = "Invalid folder name";
"site_list.alert.invalid_folder_name_desc" = "This folder could not be resolved to a valid URL. This is usually because theres a space in the folder name. Please rename the folder, reload the list of sites, and try again.";
// DRIVERS
"driver.not_detected" = "Other";
// EDITORS
"editors.alert.try_again" = "Try Again";
@ -127,6 +137,7 @@
"prefs.info_density" = "Info density:";
"prefs.services" = "Services:";
"prefs.switcher" = "Switcher:";
"prefs.integrations" = "Integrations:";
"prefs.auto_restart_services_title" = "Auto-restart PHP-FPM";
"prefs.auto_restart_services_desc" = "When checked, will automatically restart PHP-FPM when you check or uncheck an extension. Slightly slower when enabled, but this applies the extension change immediately for all sites you're serving, no need to restart PHP-FPM manually.";
@ -142,6 +153,9 @@
"prefs.auto_composer_update_title" = "Automatically update global dependencies";
"prefs.auto_composer_update_desc" = "When checked, will automatically ask Composer to run `composer global update` whenever you switch between different PHP versions. You will be able to see what changes are being made, or should this fail.";
"prefs.open_protocol_title" = "Allow third-party integrations";
"prefs.open_protocol_desc" = "When checked, this will allow the interaction with third party utilities to work (e.g. Alfred, Raycast). If you disable this, PHP Monitor will still receive the commands, but will not act upon them.";
"prefs.shortcut_set" = "Set global shortcut";
"prefs.shortcut_listening" = "<listening for keypress>";
@ -165,6 +179,9 @@
// ALERTS
// Composer Update
"alert.composer_missing.title" = "Composer not found!";
"alert.composer_missing.info" = "Make sure you have Composer available in `/usr/local/bin/composer`. If Composer is located somewhere else, please create a symlink, like so (make sure to use the correct path):\n\n`ln -s /path/to/composer /user/local/bin`.";
"alert.composer_progress.title" = "Updating global dependencies...";
"alert.composer_progress.info" = "You can see the progress in the terminal output below.";
@ -180,13 +197,21 @@ problem manually, using your own Terminal app (this just shows you the output)."
"alert.composer_php_requirement.title" = "`%@` has the following PHP requirement: \"php\":\n\"%@\".";
"alert.composer_php_requirement.info" = "This required PHP version was determined by checking the `%@` field in the `composer.json` file when the site list was last refreshed.";
// Force Reload Started
"alert.force_reload.title" = "PHP Monitor will force reload the latest version of PHP";
"alert.force_reload.info" = "This can take a while. You'll get another alert when the force reload has completed.";
// Suggest Fix My Valet
"alert.php_switch_failed.title" = "Switching to PHP %@ seems to have failed.";
"alert.php_switch_failed.info" = "PHP Monitor has detected that PHP %@ is not active after completing its switching procedure. You can try to run \"Fix My Valet\" and switch again after that. Do you want to try \"Fix My Valet\"?";
"alert.php_switch_failed.confirm" = "Yes, run \"Fix My Valet\"";
"alert.php_switch_failed.cancel" = "Do Not Run";
// Force Reload Done
"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).";
// Fix My Valet Started
"alert.fix_my_valet.title" = "Having issues? Fix My Valet is ready to commence!";
"alert.fix_my_valet.info" = "This can take a while. Please be patient.\n\nWhen this is done, all other services will be halted and PHP %@ will be linked. You will be able to switch to your desired version of PHP once this operation has completed.\n\n(You'll get another alert once Fix My Valet is done.)";
"alert.fix_my_valet.ok" = "Continue";
"alert.fix_my_valet.cancel" = "Abort";
// Fix My Valet Done
"alert.fix_my_valet_done.title" = "Fix My Valet has completed its operations.";
"alert.fix_my_valet_done.info" = "All appropriate services have been stopped and the correct ones restarted, and the latest version of PHP should now be active. You can now try switching to another version of PHP.\n\nIf visiting sites still does not work, you may try running `valet install` again, this can fix a 502 issue (Bad Gateway).\n\nIf Valet is broken and you cannot run `valet install`, you may need to run `composer global update`. Please consult the FAQ on GitHub if you have further issues.";
// PHP FPM Broken
"alert.php_fpm_broken.title" = "PHP-FPM configuration is incorrect";
@ -219,10 +244,18 @@ You can do this by running `composer global update` in your terminal. After that
"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.";
/// 3. Valet not installed
/// 3a. 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` 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.)";
/// 3b. Valet configuration file missing [currently not enabled]
"startup.errors.valet_config.title" = "Laravel Valet configuration file missing";
"startup.errors.valet_config.desc" = "PHP Monitor needs to be able to read the configuration file in `~/.config/valet/config.json`.";
/// 3c. Valet version not readable
"startup.errors.valet_version_unknown.title" = "Your Valet version could not be read (`valet --version` failed)";
"startup.errors.valet_version_unknown.desc" = "Make sure your Valet installation works and is up-to-date.\n\nTry running `valet --version` in a terminal to find out what's going on.";
/// 4. Brew & sudoers
"startup.errors.sudoers_brew.title" = "Brew has not been added to sudoers.d";
"startup.errors.sudoers_brew.desc" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
@ -231,6 +264,17 @@ You can do this by running `composer global update` in your terminal. After that
"startup.errors.sudoers_valet.title" = "Valet has not been added to sudoers.d";
"startup.errors.sudoers_valet.desc" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue. If you did this before, please run `sudo valet trust` again.";
/// 6. Multiple services active
/// 6. Cannot retrieve services
"startup.errors.services_json_error.title" = "Cannot determine services status";
"startup.errors.services_json_error.desc" = "PHP Monitor usually queries `brew` using the following command to test if the services can be retrieved: `sudo brew services info nginx --json`.\n\nPHP Monitor could not interpret this response. This can happen if your Homebrew installation is out of date, in which case Homebrew won't return JSON yet.\n\nYou can usually fix this by running `brew update`.";
/// 7. Multiple services active
"startup.errors.services.title" = "Multiple PHP services are active";
"startup.errors.services.desc" = "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes.\n\nThe easiest solution is to choose the option 'Force load latest PHP version' in the menu bar.\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies).\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue.\n\nFor more information about this issue, please see the README.md file in the repository on GitHub.";
"startup.errors.services.desc" = "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes.\n\nThe easiest solution is to choose the option 'First Aid & Services > Fix My Valet' in the menu bar.\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies).\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue.\n\nFor more information about this issue, please see the README.md file in the repository on GitHub.";
// SPONSOR ENCOURAGEMENT
"startup.sponsor_encouragement.title" = "If PHP Monitor has been useful to you or your company, please consider leaving a tip.";
"startup.sponsor_encouragement.desc" = "If you have already donated, then YOU are the reason why the app was able to get all these new features. In that case, this is a THANK YOU message to you.\n\nTo be 100% transparent: I plan to keep PHP Monitor open source and free. Your support makes this decision very easy.\n\n(You will only see this prompt once.)";
"startup.sponsor_encouragement.accept" = "Yes, I would like to sponsor";
"startup.sponsor_encouragement.skip" = "Nevermind";