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

Compare commits

...

70 Commits
v4.1 ... v5.0b1

Author SHA1 Message Date
b59a5d31a5 👌 Save window size and position for site list 2022-01-25 21:40:11 +01:00
e4b1f75c53 👌 Handle errors when adding moved folder 2022-01-25 21:02:21 +01:00
338a87d503 👌 Show what prevents creation of link
- Site name already exists?
- Site name empty?
2022-01-25 18:50:35 +01:00
0f0e91273e 👌 Localisation improvements 2022-01-25 18:28:10 +01:00
20959501c9 👌 Fix flickering of incorrect data on first load 2022-01-25 00:09:50 +01:00
aeeecd6996 👌 Suggestions should also check all constraints 2022-01-25 00:09:36 +01:00
e9ae989200 👌 Check multiple constraints (e.g. "^7.3|^8.0") 2022-01-24 23:56:26 +01:00
f6378e7b73 Add option to add a linked site 2022-01-24 23:42:22 +01:00
d7e8652f5f 👌 Populate new ServicesView with stale data
This means that the user cannot tell we swapped out the view for another
view. The services are re-fetched upon creating the new view, but there
is a slight delay. This change conveniently "hides" this delay.

BEFORE
- Upon creating ServicesView2, ServicesView is deinitialized
- ServicesView2 shows question marks (no services data persisted)
- ServicesView2 async loads services, when done question marks removed

AFTER
- Upon creating ServicesView2, ServicesView is deinitialized
- ServicesView2 loads stale data (services data was persisted)
- ServicesView2 async loads services, when done stale data replaced
2022-01-24 23:41:47 +01:00
5293c437d1 🔧 Prepare for 5.0 beta 1 2022-01-24 01:22:13 +01:00
f75bfc9c4a Initial version of #72 2022-01-24 01:16:17 +01:00
42fc0e3698 🔥 Due to closing #34, removed switcher pref 2022-01-23 20:06:20 +01:00
626b7a735d 👌 Checkmark on site list 2022-01-23 19:37:10 +01:00
567373f8da Version constraint checks (#84)
The version constraint checks will also be used in the future to
evaluate whether any given site's PHP constraint (if set) is
valid for the currently linked version of PHP.

For example, assuming you have PHP 8.1.2 linked, we could evaluate:

* A site requires "8.0" -> invalid
* A site requires "^8.0" -> valid
* A site requires "^8.0.0" -> valid
* A site requires "~8.0" -> valid
* A site requires "~8.0.0" -> invalid

Currently, this constraint check is used to determine which versions
that are currently installed are good suggestions to switch to.

If you have a site with constraint "^8.0" for example, and you have
PHP 8.0 and 8.1 installed (with 8.1 linked), then you will get a
suggestion to switch back to 8.0.
2022-01-23 03:59:29 +01:00
32e8878a68 🏗 WIP: UI for switch to version options (#84) 2022-01-22 22:08:45 +01:00
46005a3c68 👌 Load notable dependencies (incl. laravel, #80) 2022-01-22 21:35:32 +01:00
03a409281a 👌 Sort site list by absolute path (#81) 2022-01-22 20:54:59 +01:00
e0dd778bb3 👌 Add debounce to site search (#82) 2022-01-22 20:50:17 +01:00
c3f8a53ac3 Updated preferences (added option to disable PHP hint next to icon)
- Only works with dynamic icon enabled
- Preference defaults to true on new or existing installs
  (because we want to display PHP next to the version number by default)

For those who love a minimal menu bar setup but still want to see what
PHP version is currently enabled, this is perfect.
2022-01-16 13:14:54 +01:00
d8579bd7d1 🐛 Fix incorrect change in AppDelegate 2022-01-11 21:30:18 +01:00
d2cd567fd2 Filter only needed services (#72) 2022-01-11 21:22:17 +01:00
a5212b436e Tweaked test copy (#72) 2022-01-11 21:22:17 +01:00
b16250c2be Added tests for Homebrew service parsing (#72) 2022-01-11 21:22:17 +01:00
3b4a1a0654 Enable parsing of Homebrew services JSON (#72) 2022-01-11 21:22:17 +01:00
9ab6231337 📝 PR template 2022-01-04 20:50:06 +01:00
dc44538a7b 👌 Adjust preference description (see also #78) 2022-01-04 20:17:26 +01:00
04bf5a3251 📝 Updated README 2022-01-04 02:56:00 +01:00
23f3204fa8 📝 Phrasing fix 2022-01-04 02:52:33 +01:00
6dc74e94aa 📝 Document functionality in README 2022-01-04 02:51:33 +01:00
422aefe831 Read composer.json for version requirement 2022-01-04 02:33:53 +01:00
3c3a0c8b45 ♻️ Rework loading of custom preferences 2022-01-04 00:32:30 +01:00
0cdeeec0a4 ♻️ Load the custom preferences file (#73) 2022-01-04 00:30:27 +01:00
e4c3e78a8a 📝 Adjust README 2022-01-04 00:21:52 +01:00
7f320897be 👌 Make user-supplied apps available (#73) 2022-01-04 00:20:47 +01:00
9ef184331e 🐛 Fix issue with Valet path 2022-01-03 17:08:45 +01:00
3bca3117f9 Load custom preferences file 2022-01-03 16:49:50 +01:00
722e082526 👌 Rename method from update to rebuild 2022-01-03 16:20:53 +01:00
40a0bd6cab 👌 Prevent crash when refresh and switcher run at same time 2022-01-03 16:19:18 +01:00
78510ea3fe 👌 Even more cleanup 2022-01-03 16:09:49 +01:00
8624573e74 👌 Cleanup 2022-01-03 16:02:18 +01:00
dd251936b9 ♻️ Refactor PhpSwitcher into PhpEnv 2021-12-24 16:09:51 +01:00
c647aee8ea 🔀 Merge branch 'main' into dev/5.x 2021-12-23 20:19:21 +01:00
b7766aeec2 👌 Improve UI and warn about spaces in folder names 2021-12-23 20:09:23 +01:00
5af1f09ee1 🐛 Ensure app can handle interactions with path w/ spaces (#74) 2021-12-23 13:27:19 +01:00
6646ceda76 🐛 Fix preloaded site logic 2021-12-23 12:52:10 +01:00
0b05bb44a2 🐛 Fix initialization of Site objects (#74) 2021-12-23 12:29:12 +01:00
1fbb1a8aa8 🔀 Merge branch 'main' into dev/5.x 2021-12-23 00:16:19 +01:00
665bba86dd 🔧 Tweak default verbosity 2021-12-22 16:55:32 +01:00
0d29fbf796 Add phpmon-cli fix command 2021-12-22 16:54:27 +01:00
69042042ea 👌 Cleanup 2021-12-21 18:00:07 +01:00
63f4f8b078 Added prototype binary to switch quickly 2021-12-21 17:52:13 +01:00
e76c6e14e4 ♻️ Added logger class 2021-12-21 17:06:03 +01:00
ceb168c6cf ♻️ Rework various common classes 2021-12-21 16:00:27 +01:00
a6387e96e7 ♻️ Separate some of the PHP config logic from the app 2021-12-21 15:30:50 +01:00
2dbf775ad6 🔥 Remove Swift Package for common data 2021-12-20 19:10:58 +01:00
acdcce7f7a Add scheme to support inter-app communication (#59) 2021-12-20 18:39:59 +01:00
7a3dc9a145 👌 Fix incorrect main.swift file 2021-12-20 18:31:16 +01:00
1ca49f6cbc ♻️ Reorganise code for optimal code sharing, add phpmon-cli
- Moved over common functionality to package
- Added phpmon-cli target (for fast switching via the terminal)
2021-12-20 18:25:52 +01:00
fa2de1f77c 🔀 Merge in changes from SwiftUI previews branch 2021-12-20 17:20:43 +01:00
fe695bb026 👌 Nicer logging of multiple paths 2021-12-19 14:16:49 +01:00
f82a3bb008 🔀 Merge in remaining changes from 4.x 2021-12-19 14:14:43 +01:00
e7df254dcc Make sure that watchers reload if the .conf.d dir contents change 2021-12-19 14:11:25 +01:00
ea9538f116 Ensure watcher does not fire too many times 2021-12-19 13:58:25 +01:00
0d75e4c3b2 🔀 Merge WIP changes from feature/config-watcher into 5.x 2021-12-19 12:56:16 +01:00
267a1dac94 👌 Various QoL improvements
- Ensure composer global update cannot run twice (#71)
- Set busy status when updating dependencies (#71)
- Further reorganized menu items (#69)
- Use consistent capitals in menu items
- Fix preferences screen layout (auto newlines due to fixed width)
2021-12-19 12:27:34 +01:00
ed49362291 Add option to automatically run composer global update 2021-12-18 19:45:50 +01:00
3f0f070245 👌 Clear out extra newlines 2021-12-18 16:17:59 +01:00
bd79f42e96 👌 Cleanup 2021-12-18 16:06:39 +01:00
35ae681c2d ♻️ Rework how output is handled 2021-12-18 15:53:04 +01:00
313e806414 Added menu item to run composer global update 2021-12-17 16:10:45 +01:00
86 changed files with 4141 additions and 950 deletions

35
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,35 @@
Hello there! Thank you for considering a pull request for PHP Monitor.
Please read the text below first before you submit your PR.
## Do not PR unless...
In order to make development and maintenance of PHP Monitor easier, I ask that you _avoid_ making a pull request in the following situations:
* No issue has been associated with the changes youd like to merge
* You have not announced you will be addressing a particular issue
* The PR is a low effort change: e.g. commits that only fix typos or phrasing may not be accepted
(If you believe the phrasing of particular text in the app is unclear or incorrect, please open an issue first.)
In short: It is usually best to *get in touch first* if you are making substantial changes.
## About destination branches
Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Merge requests that target `main` will be closed without mercy.**
Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released.
There may be a newer branch available, which is an appropriate place for bigger changes, but please keep in mind that it is usually best to announce youll be working on such a change before you spend the time, since as the lead contributor I might not even want said change in the app. Thank you.
## Your changes
(feel free to remove the disclaimer above)
* Affected parts of the app: shared code / UI code / CLI (remove what does not apply)
* Estimated impact on performance: none / low / high (remove what does not apply)
* Made a new build with Xcode and tested this: yes / no (remove what does not apply)
* Tested on macOS version + architecture: (e.g. "Monterey on M1" or "Big Sur on Intel")
* References issue(s): (please reference the issue here, using # and the number of the issue)
(please describe what you have changed here)

View File

@ -24,20 +24,42 @@
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 */; };
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 */; };
C40C7F2227721F8200DDDCDC /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
C40C7F2327721F8200DDDCDC /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
C40C7F2427721F8200DDDCDC /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C40C7F2527721F9800DDDCDC /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C40C7F2627721FA200DDDCDC /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; };
C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; };
C40C7F2B2772201C00DDDCDC /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; };
C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
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 */; };
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 */; };
C415D3E62770F540005EF286 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3E52770F540005EF286 /* main.swift */; };
C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */; };
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */; };
C417DC74277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; };
C417DC75277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; };
C417DC76277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; };
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; };
C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; };
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; };
C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; };
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; };
C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; };
C42295DD2358D02000E263B2 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; };
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
@ -46,6 +68,10 @@
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; };
C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; };
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */; };
C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* ProgressWindow.swift */; };
C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* ProgressWindow.swift */; };
C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C44C1990276E44CB0072762D /* ProgressWindow.storyboard */; };
C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C44C1990276E44CB0072762D /* ProgressWindow.storyboard */; };
C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */; };
C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */; };
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* SiteListVC.swift */; };
@ -66,10 +92,15 @@
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C9925CC888B00CC7490 /* HeaderView.xib */; };
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */; };
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */; };
C48D6C72279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */; };
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */; };
C493084A279F331F009C240B /* AddSiteVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4930849279F331F009C240B /* AddSiteVC.swift */; };
C493084B279F331F009C240B /* AddSiteVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4930849279F331F009C240B /* AddSiteVC.swift */; };
C4998F0626175E7200B2526E /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = C4998F0526175E7200B2526E /* HotKey */; };
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
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 */; };
@ -80,28 +111,66 @@
C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; };
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; };
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */; };
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; };
C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; };
C4B585402770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; };
C4B585412770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; };
C4B585422770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; };
C4B585432770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; };
C4B585442770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
C4B585452770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
C4B585462770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; };
C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; };
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */; };
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */; };
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; };
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; };
C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; };
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; };
C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; };
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; };
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; };
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
C4D9ADC1277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
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 */; };
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 */; };
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; };
C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; };
C4F30B05278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; };
C4F30B07278E195800755FCE /* brew-services.json in Resources */ = {isa = PBXBuildFile; fileRef = C4F30B06278E195800755FCE /* brew-services.json */; };
C4F30B08278E195800755FCE /* brew-services.json in Resources */ = {isa = PBXBuildFile; fileRef = C4F30B06278E195800755FCE /* brew-services.json */; };
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; };
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */; };
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4F780B425D80B51000DBC97 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
C4F780B725D80B5D000DBC97 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
@ -132,6 +201,18 @@
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
C415D3D42770F341005EF286 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = "<group>"; };
5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
@ -143,7 +224,16 @@
54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotkeyPreferenceView.swift; sourceTree = "<group>"; };
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = "<group>"; };
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
C40C7F1D2772136000DDDCDC /* PhpEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpEnv.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
C415D3E52770F540005EF286 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+InterApp.swift"; sourceTree = "<group>"; };
C417DC73277614690015E6EE /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
C4188988275FE8CB001EF227 /* Filesystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filesystem.swift; sourceTree = "<group>"; };
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; };
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -151,19 +241,19 @@
C41C1B3D22B0098000E7CF16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
C41C1B3F22B0098000E7CF16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = "<group>"; };
C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = "<group>"; };
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = "<group>"; };
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+Actions.swift"; sourceTree = "<group>"; };
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = "<group>"; };
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.swift"; sourceTree = "<group>"; };
C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = "<group>"; };
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.swift"; sourceTree = "<group>"; };
C43A8A1925D9CD1000591B77 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = "<group>"; };
C43A8A1F25D9D1D700591B77 /* brew.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = brew.json; sourceTree = "<group>"; };
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewJsonParserTest.swift; sourceTree = "<group>"; };
C44C198C276E3A1C0072762D /* ProgressWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressWindow.swift; sourceTree = "<group>"; };
C44C1990276E44CB0072762D /* ProgressWindow.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ProgressWindow.storyboard; sourceTree = "<group>"; };
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListWC.swift; sourceTree = "<group>"; };
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListVC.swift; sourceTree = "<group>"; };
C464ADB1275A87CA003FCD53 /* SiteListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListCell.swift; sourceTree = "<group>"; };
@ -179,8 +269,10 @@
C48D0C9525CC80B100CC7490 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
C48D0C9925CC888B00CC7490 /* HeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HeaderView.xib; sourceTree = "<group>"; };
C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpVersionNumber.swift; sourceTree = "<group>"; };
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>"; };
C49EAB45259FC305007F6C3B /* Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
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>"; };
@ -188,16 +280,35 @@
C4AF9F7C275454A900D44ED0 /* ValetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetTest.swift; sourceTree = "<group>"; };
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.swift; sourceTree = "<group>"; };
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionExtractorTest.swift; sourceTree = "<group>"; };
C4B5853B2770FE3900DA4FBE /* Paths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
C4B5853C2770FE3900DA4FBE /* Shell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
C4B5853D2770FE3900DA4FBE /* Command.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MenuOutlets.swift"; sourceTree = "<group>"; };
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+ActivationPolicy.swift"; sourceTree = "<group>"; };
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+GlobalHotkey.swift"; sourceTree = "<group>"; };
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Startup.swift"; sourceTree = "<group>"; };
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = "<group>"; };
C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = "<group>"; };
C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpConfigWatcher.swift; sourceTree = "<group>"; };
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
C4EC1E67279DE0540010F296 /* ServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServicesView.swift; sourceTree = "<group>"; };
C4EC1E6C279DF87A0010F296 /* Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Async.swift; sourceTree = "<group>"; };
C4EC1E72279DFCF40010F296 /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = "<group>"; };
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = "<group>"; };
C4F7807425D7F7E5000DBC97 /* RELEASE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = RELEASE.md; sourceTree = "<group>"; };
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -210,6 +321,13 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
C415D3D32770F341005EF286 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C41C1B3022B0097F00E7CF16 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -234,6 +352,7 @@
C4998F092617633900B2526E /* PrefsWC.swift */,
5420395826135DC100FB00FA /* PrefsVC.swift */,
5420395E2613607600FB00FA /* Preferences.swift */,
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */,
C41CD0272628D8E20065BBED /* Keybinds */,
54FCFD28276C88C0004CE748 /* Views */,
);
@ -243,6 +362,9 @@
54B20EDF263AA22C00D3250E /* PHP */ = {
isa = PBXGroup;
children = (
C48D6C6E279CD29C00F26D7E /* PHP Version */,
C4D9ADC2277610E4007277F4 /* Switcher */,
C4F30B01278E169B00755FCE /* Homebrew */,
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */,
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */,
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
@ -270,6 +392,41 @@
path = IAP;
sourceTree = "<group>";
};
C40C7F1C27720E1400DDDCDC /* Test Files */ = {
isa = PBXGroup;
children = (
C4AF9F70275445FF00D44ED0 /* valet-config.json */,
C43A8A1F25D9D1D700591B77 /* brew.json */,
C4F30B06278E195800755FCE /* brew-services.json */,
C4F780A725D80AE8000DBC97 /* php.ini */,
);
path = "Test Files";
sourceTree = "<group>";
};
C40C7F2127721F7300DDDCDC /* Core */ = {
isa = PBXGroup;
children = (
C415D3B62770F294005EF286 /* Actions.swift */,
C4EE188322D3386B00E126E5 /* Constants.swift */,
C4EC1E72279DFCF40010F296 /* Events.swift */,
C4B5853D2770FE3900DA4FBE /* Command.swift */,
C4B5853B2770FE3900DA4FBE /* Paths.swift */,
C4B5853C2770FE3900DA4FBE /* Shell.swift */,
C40C7F2F27722E8D00DDDCDC /* Logger.swift */,
C417DC73277614690015E6EE /* Helpers.swift */,
);
path = Core;
sourceTree = "<group>";
};
C415D3D72770F341005EF286 /* phpmon-cli */ = {
isa = PBXGroup;
children = (
C415D3E52770F540005EF286 /* main.swift */,
C415D3DE2770F34D005EF286 /* AllowedArguments.swift */,
);
path = "phpmon-cli";
sourceTree = "<group>";
};
C41C1B2A22B0097F00E7CF16 = {
isa = PBXGroup;
children = (
@ -279,7 +436,10 @@
C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */,
C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
C415D3D72770F341005EF286 /* phpmon-cli */,
C4B5853A2770FE2500DA4FBE /* phpmon-common */,
C41C1B3422B0097F00E7CF16 /* Products */,
C4D309E72770EF2F00958BCF /* Frameworks */,
);
sourceTree = "<group>";
};
@ -288,6 +448,7 @@
children = (
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */,
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */,
C415D3D62770F341005EF286 /* phpmon-cli */,
);
name = Products;
sourceTree = "<group>";
@ -295,7 +456,6 @@
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup;
children = (
C4EE188322D3386B00E126E5 /* Constants.swift */,
C41E181722CB61EB0072CF09 /* Domain */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
C4232EE42612526500158FC6 /* Credits.html */,
@ -320,23 +480,36 @@
children = (
C4AF9F6B275445D300D44ED0 /* Integrations */,
C4B13B1D25C4915000548C3A /* Core */,
54B20EDF263AA22C00D3250E /* PHP */,
C4F7808A25D7F918000DBC97 /* Terminal */,
C4D9ADBD27761084007277F4 /* PHP */,
C47331A0247093AC009A0597 /* Menu */,
C464ADAA275A7A25003FCD53 /* SiteList */,
5420395726135DB800FB00FA /* Preferences */,
C44C198F276E3A380072762D /* Progress */,
C4C8E81D276F5686003AC782 /* Watcher */,
C4811D2822D70D9C00B5F6B3 /* Helpers */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
C4EE55B027708BB2001DF387 /* SwiftUI */,
);
path = Domain;
sourceTree = "<group>";
};
C44C198F276E3A380072762D /* Progress */ = {
isa = PBXGroup;
children = (
C44C198C276E3A1C0072762D /* ProgressWindow.swift */,
C44C1990276E44CB0072762D /* ProgressWindow.storyboard */,
);
path = Progress;
sourceTree = "<group>";
};
C464ADAA275A7A25003FCD53 /* SiteList */ = {
isa = PBXGroup;
children = (
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */,
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */,
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */,
C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */,
C4930849279F331F009C240B /* AddSiteVC.swift */,
C464ADB1275A87CA003FCD53 /* SiteListCell.swift */,
);
path = SiteList;
@ -346,11 +519,14 @@
isa = PBXGroup;
children = (
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */,
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */,
C47331A1247093B7009A0597 /* StatusMenu.swift */,
C48D0C9525CC80B100CC7490 /* HeaderView.swift */,
C48D0C9925CC888B00CC7490 /* HeaderView.xib */,
C48D0CA225CC992000CC7490 /* StatsView.swift */,
C48D0C8F25CC7FD000CC7490 /* StatsView.xib */,
C4EC1E67279DE0540010F296 /* ServicesView.swift */,
C4EC1E65279DE0380010F296 /* ServicesView.xib */,
);
path = Menu;
sourceTree = "<group>";
@ -366,10 +542,20 @@
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */,
54AB03252763858F00A29D5F /* Timer.swift */,
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */,
C4EC1E6C279DF87A0010F296 /* Async.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
C48D6C6E279CD29C00F26D7E /* PHP Version */ = {
isa = PBXGroup;
children = (
C40C7F1D2772136000DDDCDC /* PhpEnv.swift */,
C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */,
);
path = "PHP Version";
sourceTree = "<group>";
};
C4AF9F6A275445C900D44ED0 /* Valet */ = {
isa = PBXGroup;
children = (
@ -381,6 +567,7 @@
C4AF9F6B275445D300D44ED0 /* Integrations */ = {
isa = PBXGroup;
children = (
C4D89BC42783C98800A02B68 /* Composer */,
C4AF9F6C275445D900D44ED0 /* Homebrew */,
C4AF9F6A275445C900D44ED0 /* Valet */,
);
@ -391,7 +578,6 @@
isa = PBXGroup;
children = (
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */,
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
);
path = Homebrew;
sourceTree = "<group>";
@ -403,26 +589,94 @@
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */,
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */,
C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */,
C4811D2322D70A4700B5F6B3 /* App.swift */,
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */,
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
);
path = Core;
sourceTree = "<group>";
};
C4B5853A2770FE2500DA4FBE /* phpmon-common */ = {
isa = PBXGroup;
children = (
C40C7F2127721F7300DDDCDC /* Core */,
54B20EDF263AA22C00D3250E /* PHP */,
);
path = "phpmon-common";
sourceTree = "<group>";
};
C4C8E81D276F5686003AC782 /* Watcher */ = {
isa = PBXGroup;
children = (
C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */,
C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */,
);
path = Watcher;
sourceTree = "<group>";
};
C4D309E72770EF2F00958BCF /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
C4D89BC42783C98800A02B68 /* Composer */ = {
isa = PBXGroup;
children = (
C4D89BC52783C99400A02B68 /* ComposerJson.swift */,
);
path = Composer;
sourceTree = "<group>";
};
C4D9ADBD27761084007277F4 /* PHP */ = {
isa = PBXGroup;
children = (
C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */,
);
path = PHP;
sourceTree = "<group>";
};
C4D9ADC2277610E4007277F4 /* Switcher */ = {
isa = PBXGroup;
children = (
C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */,
C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */,
);
path = Switcher;
sourceTree = "<group>";
};
C4EE55B027708BB2001DF387 /* SwiftUI */ = {
isa = PBXGroup;
children = (
C4EE55A627708B9E001DF387 /* PMHeaderView.swift */,
C4EE55A827708B9E001DF387 /* PMStatsView.swift */,
C4EE55A727708B9E001DF387 /* Preview.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
};
C4F30B01278E169B00755FCE /* Homebrew */ = {
isa = PBXGroup;
children = (
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
C4F30B02278E16BA00755FCE /* HomebrewService.swift */,
);
path = Homebrew;
sourceTree = "<group>";
};
C4F7807A25D7F84B000DBC97 /* phpmon-tests */ = {
isa = PBXGroup;
children = (
C4AF9F70275445FF00D44ED0 /* valet-config.json */,
C43A8A1F25D9D1D700591B77 /* brew.json */,
C4F780A725D80AE8000DBC97 /* php.ini */,
C4F7807D25D7F84B000DBC97 /* Info.plist */,
C40C7F1C27720E1400DDDCDC /* Test Files */,
C4F7809B25D80344000DBC97 /* CommandTest.swift */,
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */,
C43A8A1925D9CD1000591B77 /* Utility.swift */,
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */,
C4AF9F7C275454A900D44ED0 /* ValetTest.swift */,
@ -431,16 +685,6 @@
path = "phpmon-tests";
sourceTree = "<group>";
};
C4F7808A25D7F918000DBC97 /* Terminal */ = {
isa = PBXGroup;
children = (
C49EAB45259FC305007F6C3B /* Paths.swift */,
C42295DC2358D02000E263B2 /* Command.swift */,
C41C1B4622B009A400E7CF16 /* Shell.swift */,
);
path = Terminal;
sourceTree = "<group>";
};
C4F8C0A222D4F100002EFE61 /* Extensions */ = {
isa = PBXGroup;
children = (
@ -455,6 +699,23 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
C415D3D52770F341005EF286 /* phpmon-cli */ = {
isa = PBXNativeTarget;
buildConfigurationList = C415D3DA2770F341005EF286 /* Build configuration list for PBXNativeTarget "phpmon-cli" */;
buildPhases = (
C415D3D22770F341005EF286 /* Sources */,
C415D3D32770F341005EF286 /* Frameworks */,
C415D3D42770F341005EF286 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = "phpmon-cli";
productName = "phpmon-cli";
productReference = C415D3D62770F341005EF286 /* phpmon-cli */;
productType = "com.apple.product-type.tool";
};
C41C1B3222B0097F00E7CF16 /* PHP Monitor */ = {
isa = PBXNativeTarget;
buildConfigurationList = C41C1B4322B0098000E7CF16 /* Build configuration list for PBXNativeTarget "PHP Monitor" */;
@ -499,10 +760,13 @@
C41C1B2B22B0097F00E7CF16 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1220;
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1320;
ORGANIZATIONNAME = "Nico Verbruggen";
TargetAttributes = {
C415D3D52770F341005EF286 = {
CreatedOnToolsVersion = 13.2.1;
};
C41C1B3222B0097F00E7CF16 = {
CreatedOnToolsVersion = 10.2.1;
};
@ -530,6 +794,7 @@
targets = (
C41C1B3222B0097F00E7CF16 /* PHP Monitor */,
C4F7807825D7F84B000DBC97 /* phpmon-tests */,
C415D3D52770F341005EF286 /* phpmon-cli */,
);
};
/* End PBXProject section */
@ -544,9 +809,12 @@
C4AF9F71275445FF00D44ED0 /* valet-config.json in Resources */,
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */,
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */,
C4232EE52612526500158FC6 /* Credits.html in Resources */,
54FCFD26276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */,
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
C4F30B07278E195800755FCE /* brew-services.json in Resources */,
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */,
54FCFD2D276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */,
@ -562,60 +830,112 @@
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
C43A8A2025D9D1D700591B77 /* brew.json in Resources */,
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */,
C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */,
C4F30B08278E195800755FCE /* brew-services.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C415D3D22770F341005EF286 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C40C7F2427721F8200DDDCDC /* PhpExtension.swift in Sources */,
C40C7F2627721FA200DDDCDC /* Constants.swift in Sources */,
C4B585402770FE3900DA4FBE /* Paths.swift in Sources */,
C415D3E62770F540005EF286 /* main.swift in Sources */,
C40C7F2227721F8200DDDCDC /* PhpInstallation.swift in Sources */,
C4B585432770FE3900DA4FBE /* Shell.swift in Sources */,
C4D9ADC1277610E1007277F4 /* PhpSwitcher.swift in Sources */,
C4F30B05278E16BA00755FCE /* HomebrewService.swift in Sources */,
C4EC1E6E279DF87A0010F296 /* Async.swift in Sources */,
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 */,
C40C7F3227722E8D00DDDCDC /* Logger.swift in Sources */,
C40C7F2B2772201C00DDDCDC /* Actions.swift in Sources */,
C415D3E12770F34D005EF286 /* AllowedArguments.swift in Sources */,
C40C7F202772136000DDDCDC /* PhpEnv.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C41C1B2F22B0097F00E7CF16 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */,
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C4B585412770FE3900DA4FBE /* Shell.swift in Sources */,
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */,
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */,
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */,
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */,
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */,
C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */,
C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */,
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */,
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */,
C4B585442770FE3900DA4FBE /* Command.swift in Sources */,
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
C42295DD2358D02000E263B2 /* Command.swift in Sources */,
C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */,
C415D3B72770F294005EF286 /* Actions.swift in Sources */,
C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */,
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */,
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */,
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */,
5420395F2613607600FB00FA /* Preferences.swift in Sources */,
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */,
C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */,
54AB03262763858F00A29D5F /* Timer.swift in Sources */,
C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */,
C417DC74277614690015E6EE /* Helpers.swift in Sources */,
C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */,
C4EC1E6D279DF87A0010F296 /* Async.swift in Sources */,
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */,
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */,
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */,
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */,
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */,
C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */,
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */,
C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
C464ADB2275A87CA003FCD53 /* SiteListCell.swift in Sources */,
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
C493084A279F331F009C240B /* AddSiteVC.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -624,17 +944,26 @@
buildActionMask = 2147483647;
files = (
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */,
C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
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 */,
C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */,
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */,
C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */,
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */,
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */,
C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */,
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
@ -642,39 +971,51 @@
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */,
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */,
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */,
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */,
C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */,
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */,
C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */,
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */,
C417DC75277614690015E6EE /* Helpers.swift in Sources */,
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */,
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */,
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */,
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */,
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */,
C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */,
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */,
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
C4B585452770FE3900DA4FBE /* Command.swift in Sources */,
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */,
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */,
C4B585422770FE3900DA4FBE /* Shell.swift in Sources */,
C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */,
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */,
C464ADB0275A7A6A003FCD53 /* SiteListVC.swift in Sources */,
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */,
C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */,
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */,
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */,
C4F780B425D80B51000DBC97 /* Actions.swift in Sources */,
C4EE55AA27708B9E001DF387 /* PMHeaderView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -700,6 +1041,34 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C415D3DB2770F341005EF286 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 11.0;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C415D3DC2770F341005EF286 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 11.0;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Release;
};
C41C1B4122B0098000E7CF16 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -820,12 +1189,12 @@
C41C1B4422B0098000E7CF16 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconBeta;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 135;
CURRENT_PROJECT_VERSION = 500;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -834,8 +1203,8 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 4.1;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
MARKETING_VERSION = "5.0-b1";
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
@ -845,12 +1214,12 @@
C41C1B4522B0098000E7CF16 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconBeta;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 135;
CURRENT_PROJECT_VERSION = 500;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -859,8 +1228,8 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 4.1;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
MARKETING_VERSION = "5.0-b1";
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
@ -912,6 +1281,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
C415D3DA2770F341005EF286 /* Build configuration list for PBXNativeTarget "phpmon-cli" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C415D3DB2770F341005EF286 /* Debug */,
C415D3DC2770F341005EF286 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C41C1B2E22B0097F00E7CF16 /* Build configuration list for PBXProject "PHP Monitor" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C415D3D52770F341005EF286"
BuildableName = "phpmon-cli"
BlueprintName = "phpmon-cli"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C415D3D52770F341005EF286"
BuildableName = "phpmon-cli"
BlueprintName = "phpmon-cli"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "help"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C415D3D52770F341005EF286"
BuildableName = "phpmon-cli"
BlueprintName = "phpmon-cli"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1220"
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -111,9 +111,15 @@ If you're on an Apple Silicon-based Mac, you'll need to add:
# on an M1 Mac
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
and add the following to your .zshrc:
and add the following to your .zshrc, but add this BEFORE the homebrew PATH additions:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
If you're adding composer and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
export PATH=$HOME/bin:/usr/local/bin:$PATH
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
Make sure PHP is linked correctly:
@ -240,6 +246,16 @@ The supported apps are: <i>PhpStorm, Visual Studio Code, Sublime Text, Sublime M
All of these apps should just be detected correctly, no matter their location on your system. If you can open it using `open -a "appname"`, the app should be detected and work. If you have renamed the app, there might be an issue getting it detected.
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
You can add your own apps by creating and editing a `~/.phpmon.conf.json` file, with the following entry:
<pre>
{
"scan_apps": ["Xcode", "Kraken"]
}
</pre>
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>
@ -280,6 +296,7 @@ While I did make this application during my own free time, I have been lucky eno
* 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
Thank you very much for your contributions, kind words and support.
@ -297,11 +314,25 @@ This utility will detect which PHP versions you have installed via Homebrew, and
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be much faster than Valets switcher.
### Config change detection
PHP Monitor watches your filesystem in the relevant `conf.d` directory for the currently linked PHP version.
Whenever an .ini file is modified, PHP Monitor will attempt to reload the current information about the active PHP installation.
If an extension or other process writes to a single file a bunch of times in a short span of time (&lt; 1 sec), PHP Monitor will only reload the active configuration information after a while (with a slight delay).
### Site detection
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.
*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.
### Want to know more?
If you want to know more about how this works, I recommend you check out the source code.
This app isn't very complicated after all. In the end, this just (conveniently) executes some shell commands.
If you want to know more about how this works, I recommend you check out the source code.
## 🔧 Build instructions

View File

@ -6,7 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 4.1 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | TBD |
## Legacy versions
@ -14,6 +14,7 @@ These versions of PHP Monitor are no longer supported, but if youre using an
| 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 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 |

View File

@ -0,0 +1,26 @@
//
// AllowedArguments.swift
// phpmon-cli
//
// Created by Nico Verbruggen on 20/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
enum AllowedArguments: String, CaseIterable {
case use = "use"
case performSwitch = "switch"
case fix = "fix"
case help = "help"
static func has(_ string: String) -> Bool {
return Self.allCases.contains { arg in
return arg.rawValue == string
}
}
static var rawValues: [String] {
return Self.allCases.map { $0.rawValue }
}
}

103
phpmon-cli/main.swift Normal file
View File

@ -0,0 +1,103 @@
//
// main.swift
// phpmon-cli
//
// Created by Nico Verbruggen on 20/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
let toolver = "0.1 (early access)"
let log = Log.shared
log.verbosity = .info
if CommandLine.arguments.contains("-q") || CommandLine.arguments.contains("--quiet") {
Log.shared.verbosity = .warning
}
if CommandLine.arguments.contains("-p") || CommandLine.arguments.contains("--performance") {
Log.shared.verbosity = .performance
}
var argument = "help"
if CommandLine.arguments.count > 1 {
argument = CommandLine.arguments[1]
}
if !AllowedArguments.has(argument) {
Log.err("The supported arguments are: \(AllowedArguments.rawValues)")
exit(1)
}
let action = AllowedArguments.init(rawValue: argument)
switch action {
case .use, .performSwitch:
if !Shell.fileExists("\(Paths.binPath)/php") {
Log.err("PHP is currently not linked. Attempting quick fix...")
_ = Shell.user.executeSynchronously("brew link php", requiresPath: true)
}
let phpenv = PhpEnv.shared
PhpEnv.detectPhpVersions()
if CommandLine.arguments.count < 3 {
Log.err("You must enter at least two additional arguments when using this command.")
exit(1)
}
let version = CommandLine.arguments[2].replacingOccurrences(of: "php@", with: "")
if phpenv.availablePhpVersions.contains(version) {
Log.info("Switching to PHP \(version)...")
Actions.switchToPhpVersion(
version: version,
availableVersions: phpenv.availablePhpVersions,
completed: {
Log.info("The switch has been completed.")
exit(0)
}
)
} else {
Log.err("A PHP installation with version \(version) is not installed.")
Log.err("The installed versions are: \(phpenv.availablePhpVersions.joined(separator: ", ")).")
Log.err("If this version is available, you may be able to install it by using `brew install php@\(version)`.")
exit(1)
}
case .fix:
Log.info("Fixing your PHP installation...")
Actions.fixMyPhp()
Log.info("All operations completed. You can check which version of PHP is linked by using `php -v`.")
exit(0)
case .help:
print("""
===============================================================
PHP MONITOR CLI \(toolver)
by Nico Verbruggen
===============================================================
Gives access to the quick version switcher from PHP Monitor,
but without the GUI and 100% of the speed!
SUPPORTED COMMANDS
* use {version}: Switch to a specific version of PHP.
(e.g. `phpmon-cli use 8.0`)
* switch {version}: Alias for the `use` command.
* fix Attempts to unlink all PHP versions,
and link the latest version of PHP.
* help: Show this help.
SUPPORTED FLAGS
* `-q / --quiet`: Silences all logs except for warnings and exceptions.
* `-p / --perf`: Enables performance mode.
""")
exit(0)
case .none:
Log.err("Action not recognized!")
exit(1)
}
RunLoop.main.run()

View File

@ -0,0 +1,121 @@
//
// Services.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class Actions {
// MARK: - Services
public static func restartPhpFpm()
{
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
}
public static func restartNginx()
{
brew("services restart nginx", sudo: true)
}
public static func restartDnsMasq()
{
brew("services restart dnsmasq", sudo: true)
}
public static func stopAllServices()
{
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true)
}
/**
Kindly asks Valet to switch to a specific PHP version.
*/
public static func switchToPhpVersionUsingValet(
version: String,
availableVersions: [String],
completed: @escaping () -> Void
) {
Log.info("Switching to \(version) using Valet")
Log.info(valet("use php@\(version)"))
completed()
}
// MARK: - Finding Config Files
public static func openGenericPhpConfigFolder()
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder()
{
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpConfigFolder(version: String)
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openValetConfigFolder()
{
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL
{
// Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
// Tell php-cgi to run the PHP and output as an .html file
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
}
// MARK: - Quick Fix
/**
Detects all currently available PHP versions,
and unlinks each and every one of them.
After this, the brew services are also stopped,
the latest PHP version is linked, and php + nginx are restarted.
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()
{
brew("services restart dnsmasq", sudo: true)
PhpEnv.shared.detectPhpVersions().forEach { (version) in
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink php@\(version)")
brew("services stop \(formula)")
brew("services stop \(formula)", sudo: true)
}
brew("services stop php")
brew("services stop nginx")
brew("link php")
brew("services restart dnsmasq", sudo: true)
brew("services stop php", sudo: true)
brew("services stop nginx", sudo: true)
}
}

View File

@ -7,7 +7,7 @@
import Cocoa
class Command {
public class Command {
/**
Immediately executes a command.

View File

@ -50,7 +50,5 @@ class Constants {
// dev release. In this case, that means that the version below is detected.
"8.2"
]
}

View File

@ -0,0 +1,15 @@
//
// Events.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class Events {
static let ServicesUpdated = Notification.Name("ServicesUpdated")
}

View File

@ -0,0 +1,55 @@
//
// Helpers.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
// MARK: Common Shell Commands
/**
Runs a `valet` command.
*/
func valet(_ command: String) -> String
{
return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true)
}
/**
Runs a `brew` command. Can run as superuser.
*/
func brew(_ command: String, sudo: Bool = false)
{
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
}
/**
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
*/
func sed(file: String, original: String, replacement: String)
{
// Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
// Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension
if Shell.fileExists("\(Paths.binPath)/gsed") {
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else {
Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
}
}
/**
Uses `grep` to determine whether a particular query string can be found in a particular file.
*/
func grepContains(file: String, query: String) -> Bool
{
return Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""")
.trimmingCharacters(in: .whitespacesAndNewlines)
.contains("YES")
}

View File

@ -0,0 +1,52 @@
//
// Logger.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class Log {
static var shared = Log()
enum Verbosity: Int {
case error = 1,
warning = 2,
info = 3,
performance = 4
public func isApplicable() -> Bool {
return Log.shared.verbosity.rawValue >= self.rawValue
}
}
var verbosity: Verbosity = .warning
static func err(_ item: Any) {
if Verbosity.error.isApplicable() {
print(item)
}
}
static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() {
print(item)
}
}
static func info(_ item: Any) {
if Verbosity.info.isApplicable() {
print(item)
}
}
static func perf(_ item: Any) {
if Verbosity.performance.isApplicable() {
print(item)
}
}
}

View File

@ -0,0 +1,67 @@
//
// Paths.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
The `Paths` class is used to locate various binaries on the system,
and provides a full
*/
public class Paths {
public static let shared = Paths()
private var baseDir : Paths.HomebrewDir
init() {
baseDir = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .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 String(Shell.pipe("whoami").split(separator: "\n")[0])
}
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"
}
// MARK: - Enum
public enum HomebrewDir: String {
case opt = "/opt/homebrew"
case usr = "/usr/local"
}
}

View File

@ -7,7 +7,7 @@
import Cocoa
class Shell {
public class Shell {
// MARK: - Invoke static functions
@ -30,12 +30,12 @@ class Shell {
/**
We now require macOS 11, so no need to detect which terminal to use.
*/
var shell: String = "/bin/sh"
public var shell: String = "/bin/sh"
/**
Singleton to access a user shell (with --login)
*/
static let user = Shell()
public static let user = Shell()
/**
Runs a shell command without using the output.
@ -44,7 +44,7 @@ class Shell {
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
func run(
private func run(
_ command: String,
requiresPath: Bool = false
) {
@ -58,11 +58,11 @@ class Shell {
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
func pipe(
private func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
let shellOutput = self.execute(command, requiresPath: requiresPath)
let shellOutput = self.executeSynchronously(command, requiresPath: requiresPath)
let hasError = (
shellOutput.standardOutput == ""
&& shellOutput.errorOutput.lengthOfBytes(using: .utf8) > 0
@ -77,35 +77,28 @@ class Shell {
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
- Parameter waitUntilExit: Waits for the command to complete before returning the `ShellOutput`
*/
func execute(
public func executeSynchronously(
_ command: String,
requiresPath: Bool = false,
waitUntilExit: Bool = false
) -> ShellOutput {
let task = Process()
requiresPath: Bool = false
) -> Shell.Output {
let outputPipe = Pipe()
let errorPipe = Pipe()
let tailoredCommand = requiresPath
? "export PATH=\(Paths.binPath):$PATH && \(command)"
: command
task.launchPath = self.shell
task.arguments = ["--login", "-c", tailoredCommand]
let task = self.createTask(for: command, requiresPath: requiresPath)
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
if waitUntilExit {
task.waitUntilExit()
}
task.waitUntilExit()
return ShellOutput(
return Shell.Output(
standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
errorOutput: String(
data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
task: task
)
@ -116,20 +109,72 @@ class Shell {
Uses `/bin/echo` instead of the `builtin` (which does not support `-n`).
*/
public static func fileExists(_ path: String) -> Bool {
return Shell.pipe("if [ -f \(path) ]; then /bin/echo -n \"0\"; fi") == "0"
let escapedPath = path.replacingOccurrences(of: " ", with: "\\ ")
return Shell.pipe("if [ -f \(escapedPath) ]; then /bin/echo -n \"0\"; fi") == "0"
}
}
class ShellOutput {
let standardOutput: String
let errorOutput: String
let task: Process
init(standardOutput: String,
errorOutput: String,
task: Process) {
self.standardOutput = standardOutput
self.errorOutput = errorOutput
self.task = task
/**
Creates a new process with the correct PATH and shell.
*/
public func createTask(for command: String, requiresPath: Bool) -> Process {
let tailoredCommand = requiresPath
? "export PATH=\(Paths.binPath):$PATH && \(command)"
: command
let task = Process()
task.launchPath = self.shell
task.arguments = ["--login", "-c", tailoredCommand]
return task
}
public static func captureOutput(
_ task: Process,
didReceiveStdOutData: @escaping (String) -> Void,
didReceiveStdErrData: @escaping (String) -> Void
) {
let outputPipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = errorPipe
[(outputPipe, didReceiveStdOutData), (errorPipe, didReceiveStdErrData)].forEach {
(pipe: Pipe, callback: @escaping (String) -> Void) in
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
NotificationCenter.default.addObserver(
forName: NSNotification.Name.NSFileHandleDataAvailable,
object: pipe.fileHandleForReading,
queue: nil
) { notification in
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
callback(outputString)
}
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
}
}
}
public static func haltCapturingOutput(_ task: Process) {
if let pipe = task.standardOutput as? Pipe {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
if let pipe = task.standardError as? Pipe {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
}
public class Output {
public let standardOutput: String
public let errorOutput: String
public let task: Process
init(standardOutput: String,
errorOutput: String,
task: Process) {
self.standardOutput = standardOutput
self.errorOutput = errorOutput
self.task = task
}
}
}

View File

@ -25,7 +25,7 @@ class ActivePhpInstallation {
// MARK: - Computed
var formula: String {
return (version.short == App.shared.brewPhpVersion) ? "php" : "php@\(version.short)"
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
}
// MARK: - Initializer
@ -122,26 +122,6 @@ class ActivePhpInstallation {
return (match == nil) ? "⚠️" : "\(value)B"
}
/**
It is always possible that the system configuration for PHP-FPM has not been set up for Valet.
This can occur when a user manually installs a new PHP version, but does not run `valet install`.
In that case, we should alert the user!
- Important: The underlying check is `checkPhpFpmStatus`, which can be run multiple times.
This method actively presents a modal if said checks fails, so don't call this method too many times.
*/
public func notifyAboutBrokenPhpFpm() {
if !self.checkPhpFpmStatus() {
DispatchQueue.main.async {
Alert.notify(
message: "alert.php_fpm_broken.title".localized,
info: "alert.php_fpm_broken.info".localized,
style: .critical
)
}
}
}
/**
Determine if PHP-FPM is configured correctly.
@ -149,7 +129,7 @@ class ActivePhpInstallation {
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
that means that Valet won't work properly.
*/
private func checkPhpFpmStatus() -> Bool {
func checkPhpFpmStatus() -> Bool {
if self.version.short == "5.6" {
// The main PHP config file should contain `valet.sock` and then we're probably fine?
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"

View File

@ -0,0 +1,21 @@
//
// HomebrewService.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
struct HomebrewService: Decodable, Equatable {
let name: String
let service_name: String
let running: Bool
let loaded: Bool
let pid: Int?
let user: String?
let status: String?
let log_path: String?
let error_log_path: String?
}

View File

@ -0,0 +1,158 @@
//
// PhpSwitcher.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol PhpSwitcherDelegate: AnyObject {
func switcherDidStartSwitching()
func switcherDidCompleteSwitch()
}
class PhpEnv {
// MARK: - Initializer
init() {
self.currentInstall = ActivePhpInstallation()
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
}
// MARK: - Properties
/** The delegate that is informed of updates. */
weak var delegate: PhpSwitcherDelegate?
/** The static app instance. Accessible at any time. */
static let shared = PhpEnv()
/** Whether the switcher is busy performing any actions. */
var isBusy: Bool = false
/** All available versions of PHP. */
var availablePhpVersions: [String] = []
/** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */
var currentInstall: ActivePhpInstallation
/**
The version that the `php` formula via Brew is aliased to on the current system.
If you're up to date, `php` will be aliased to the latest version,
but that might not be the case since not everyone keeps their
software up-to-date.
As such, we take that information from Homebrew.
*/
static var brewPhpVersion: String {
return Self.shared.homebrewPackage.version
}
/**
The currently linked and active PHP installation.
*/
static var phpInstall: ActivePhpInstallation {
return Self.shared.currentInstall
}
/**
Information we were able to discern from the Homebrew info command.
*/
var homebrewPackage: HomebrewPackage! = nil
// MARK: - Methods
public static var switcher: PhpSwitcher {
return InternalSwitcher()
}
public static func detectPhpVersions() -> Void {
_ = Self.shared.detectPhpVersions()
}
/**
Detects which versions of PHP are installed.
*/
public func detectPhpVersions() -> [String]
{
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed
let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate
if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) {
versionsOnly.append(phpAlias)
}
Log.info("The PHP versions that were detected are: \(versionsOnly)")
availablePhpVersions = versionsOnly
var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version)
}
cachedPhpInstallations = mappedVersions
return versionsOnly
}
/**
Extracts valid PHP versions from an array of strings.
This array of strings is usually retrieved from `grep`.
*/
public func extractPhpVersions(
from versions: [String],
checkBinaries: Bool = true
) -> [String] {
var output : [String] = []
versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected)
return version.starts(with: "php@")
}.forEach { (string) in
let version = string.components(separatedBy: "php@")[1]
// Only append the version if it doesn't already exist (avoid dupes),
// is supported and where the binary exists (avoids broken installs)
if !output.contains(version)
&& Constants.SupportedPhpVersions.contains(version)
&& (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
{
output.append(version)
}
}
return output
}
public func validVersions(for constraint: String) -> [PhpVersionNumber] {
constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection
.make(from: self.availablePhpVersions)
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
}

View File

@ -0,0 +1,141 @@
//
// PhpVersionNumber.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
public struct PhpVersionNumberCollection: Equatable {
let versions: [PhpVersionNumber]
public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection(
versions: versions.map { PhpVersionNumber.make(from: $0)! }
)
}
public var first: PhpVersionNumber? {
return self.versions.first
}
public var all: [PhpVersionNumber] {
return self.versions
}
/**
Checks if any versions of PHP are valid for the constraint provided.
Due to the complexity of evaluating these, a important test is maintained.
More information on these constraints can be found here:
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.
The strict mode does not matter if a patch version is provided for all versions in the collection.
Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred
from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise,
assumes that the patch version is .999, which means that in all cases the patch version check is
always going to pass.
**STRICT MODE (= patch precision on)**
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.
**NON-STRICT MODE (= patch precision off)**
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0
is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version.
In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde).
If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since
the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.)
*/
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)
}
}
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
)
}
}
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 {
// 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 {
// 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
}
}
}
return []
}
}
public struct PhpVersionNumber: Equatable {
let major: Int
let minor: Int
let patch: Int?
public func patch(_ strictFallback: Bool, _ constraint: PhpVersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
}
public var homebrewVersion: String {
return "\(major).\(minor)"
}
public enum MatchType: String {
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"#
}
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
if match != nil {
let major = Int(
versionString[Range(match!.range(withName: "major"), in: versionString)!]
)!
let minor = Int(
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
)!
var patch: Int? = nil
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
patch = Int(versionString[minorRange])
}
return Self(major: major, minor: minor, patch: patch)
}
return nil
}
}

View File

@ -78,7 +78,7 @@ class PhpExtension {
// ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "")
Actions.sed(file: file, original: line, replacement: newLine)
sed(file: file, original: line, replacement: newLine)
enabled.toggle()
}
@ -92,7 +92,7 @@ class PhpExtension {
let file = try? String(contentsOf: path, encoding: .utf8)
if (file == nil) {
print("There was an issue reading the file. Assuming no extensions were found.")
Log.err("There was an issue reading the file. Assuming no extensions were found.")
return []
}

View File

@ -10,20 +10,26 @@ import Foundation
class PhpInstallation {
var longVersion: String
var longVersion: PhpVersionNumber
/**
In order to determine details about a PHP installation, well simply run `php-config --version`
in the relevant directory.
*/
init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
self.longVersion = version
self.longVersion = PhpVersionNumber.make(from: version)!
if Shell.fileExists(phpConfigExecutablePath) {
self.longVersion = Command.execute(
let longVersionString = Command.execute(
path: phpConfigExecutablePath,
arguments: ["--version"]
)
).trimmingCharacters(in: .whitespacesAndNewlines)
self.longVersion = PhpVersionNumber.make(
from: String(longVersionString.split(separator: "-")[0])
)!
}
}

View File

@ -0,0 +1,58 @@
//
// InternalSwitcher.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class InternalSwitcher: PhpSwitcher {
/**
Switching to a new PHP version involves:
- unlinking the current version
- stopping the active services
- linking the new desired version
Please note that depending on which version is installed,
the version that is switched to may or may not be identical to `php`
(without @version).
*/
func performSwitch(to version: String, completion: @escaping () -> Void)
{
Log.info("Switching to \(version), unlinking all versions...")
let group = DispatchGroup()
PhpEnv.shared.availablePhpVersions.forEach { (available) in
group.enter()
DispatchQueue.global(qos: .userInitiated).async {
let formula = (available == PhpEnv.brewPhpVersion)
? "php" : "php@\(available)"
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
Log.perf("Unlinked and stopped services for \(formula)")
group.leave()
}
}
group.notify(queue: .global(qos: .userInitiated)) {
Log.info("All versions have been unlinked!")
Log.info("Linking the new version!")
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
brew("link \(formula) --overwrite --force")
brew("services start \(formula)", sudo: true)
Log.info("The new version has been linked!")
completion()
}
}
}

View File

@ -0,0 +1,15 @@
//
// PhpVersionSwitchContract.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol PhpSwitcher {
func performSwitch(to version: String, completion: @escaping () -> Void)
}

View File

@ -9,15 +9,18 @@
import XCTest
class BrewJsonParserTest: XCTestCase {
// - MARK: SYNTHETIC TESTS
static var jsonBrewFile: URL {
return Bundle(for: Self.self).url(forResource: "brew", withExtension: "json")!
return Bundle(for: Self.self)
.url(forResource: "brew", withExtension: "json")!
}
func testCanLoadExtension() throws {
let json = try? String(contentsOf: Self.jsonBrewFile, encoding: .utf8)
func testCanLoadExtensionJson() throws {
let json = try! String(contentsOf: Self.jsonBrewFile, encoding: .utf8)
let package = try! JSONDecoder().decode(
[HomebrewPackage].self, from: json!.data(using: .utf8)!
[HomebrewPackage].self, from: json.data(using: .utf8)!
).first!
XCTAssertEqual(package.name, "php")
@ -27,5 +30,56 @@ class BrewJsonParserTest: XCTestCase {
installed.version.starts(with: "8.0")
}), true)
}
static var jsonBrewServicesFile: URL {
return Bundle(for: Self.self)
.url(forResource: "brew-services", withExtension: "json")!
}
func testCanParseServicesJson() throws {
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
let services = try! JSONDecoder().decode(
[HomebrewService].self, from: json.data(using: .utf8)!
)
XCTAssertGreaterThan(services.count, 0)
XCTAssertEqual(services.first?.name, "dnsmasq")
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
}
// - MARK: LIVE TESTS
/// This test requires that you have a valid Homebrew installation set up,
/// and requires the Valet services to be installed: php, nginx and dnsmasq.
/// If this test fails, there is an issue with your Homebrew installation
/// or the JSON API of the Homebrew output may have changed.
func testCanParseServicesJsonFromCliOutput() throws {
let services = try! JSONDecoder().decode(
[HomebrewService].self,
from: Shell.pipe(
"sudo \(Paths.brew) services info --all --json",
requiresPath: true
).data(using: .utf8)!
).filter({ service in
return ["php", "nginx", "dnsmasq"].contains(service.name)
})
XCTAssertTrue(services.contains(where: {$0.name == "php"} ))
XCTAssertTrue(services.contains(where: {$0.name == "nginx"} ))
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} ))
XCTAssertEqual(services.count, 3)
}
/// This test requires that you have a valid Homebrew installation set up,
/// and requires the `php` formula to be installed.
/// If this test fails, there is an issue with your Homebrew installation
/// or the JSON API of the Homebrew output may have changed.
func testCanLoadExtensionJsonFromCliOutput() throws {
let package = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
).first!
XCTAssertTrue(package.name == "php")
}
}

View File

@ -11,7 +11,7 @@ import XCTest
class PhpVersionDetectionTest: XCTestCase {
func testCanDetectValidPhpVersions() throws {
let outcome = Actions.extractPhpVersions(from: [
let outcome = PhpEnv.shared.extractPhpVersions(from: [
"", // empty lines should be omitted
"php@8.0",
"php@8.0", // should only be detected once
@ -26,5 +26,4 @@ class PhpVersionDetectionTest: XCTestCase {
XCTAssertEqual(outcome, ["8.0", "7.0", "5.6"])
}
}

View File

@ -0,0 +1,194 @@
//
// PhpVersionNumberTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class PhpVersionNumberTest: XCTestCase {
func testCanDeconstructPhpVersion() throws {
XCTAssertEqual(
PhpVersionNumber.make(from: "8.0.11"),
PhpVersionNumber(major: 8, minor: 0, patch: 11)
)
XCTAssertEqual(
PhpVersionNumber.make(from: "7.4.2"),
PhpVersionNumber(major: 7, minor: 4, patch: 2)
)
XCTAssertEqual(
PhpVersionNumber.make(from: "7.4"),
PhpVersionNumber(major: 7, minor: 4, patch: nil)
)
XCTAssertEqual(
PhpVersionNumber.make(from: "7"),
nil
)
}
func testCanCheckFixedConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "7.0"),
PhpVersionNumberCollection
.make(from: ["7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"])
.matching(constraint: "7.0.3"),
PhpVersionNumberCollection
.make(from: ["7.0.3"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "7.0.3", strict: false),
PhpVersionNumberCollection
.make(from: ["7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "7.0.3", strict: true),
PhpVersionNumberCollection
.make(from: []).all
)
}
func testCanCheckCaretConstraints() throws {
// 1. Imprecise checks
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
)
// 2. Imprecise check with precise constraint (lenient AKA not strict)
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "^7.0.1", strict: false),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
// 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "^7.0.1", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
)
// 4. Precise members and constraint all around
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
.matching(constraint: "^7.0.1", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
// 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
.matching(constraint: "^7.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
// 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
.matching(constraint: "^7.0", strict: false),
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
}
func testCanCheckTildeConstraints() throws {
// 1. Imprecise checks
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
)
// 2. Imprecise check with precise constraint (lenient AKA not strict)
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "~7.0.1", strict: false),
// One result because 7.0.1 to 7.0.x is expected.
// 7.0.999 (assumed due to no strictness) is valid.
// 7.1.0 and up are not valid (minor version is too high).
PhpVersionNumberCollection
.make(from: ["7.0"]).all
)
// 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "~7.0.1", strict: true),
// No results because 7.0.1 to 7.0.x is expected.
// 7.0.0 (assumed due to strictness) is not valid.
// 7.1.0 and up are also not valid (minor version is too high).
PhpVersionNumberCollection
.make(from: []).all
)
// 4. Precise members and constraint all around
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
.matching(constraint: "~7.0.1", strict: true),
// Only 7.0 with a patch version of .1 or higher is OK.
// In this example, 7.0.10 is OK but all other versions are too new.
PhpVersionNumberCollection
.make(from: ["7.0.10"]).all
)
// 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0.
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
.matching(constraint: "~7.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
// 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal.
// (Strictness does not make any difference here, but both should be tested.)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
.matching(constraint: "~7.0", strict: false),
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
}
}

View File

@ -0,0 +1,135 @@
[
{
"name": "dnsmasq",
"service_name": "homebrew.mxcl.dnsmasq",
"running": true,
"loaded": true,
"schedulable": false,
"pid": 106,
"exit_code": 0,
"user": "root",
"status": "started",
"file": "/Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist",
"command": "/opt/homebrew/opt/dnsmasq/sbin/dnsmasq --keep-in-foreground -C /opt/homebrew/etc/dnsmasq.conf -7 /opt/homebrew/etc/dnsmasq.d,*.conf",
"working_dir": null,
"root_dir": null,
"log_path": null,
"error_log_path": null,
"interval": null,
"cron": null
},
{
"name": "httpd",
"service_name": "homebrew.mxcl.httpd",
"running": false,
"loaded": false,
"schedulable": false,
"pid": null,
"exit_code": null,
"user": null,
"status": "none",
"file": "/opt/homebrew/opt/httpd/homebrew.mxcl.httpd.plist",
"command": "/opt/homebrew/opt/httpd/bin/httpd -D FOREGROUND",
"working_dir": null,
"root_dir": null,
"log_path": null,
"error_log_path": null,
"interval": null,
"cron": null
},
{
"name": "mailhog",
"service_name": "homebrew.mxcl.mailhog",
"running": false,
"loaded": false,
"schedulable": false,
"pid": null,
"exit_code": null,
"user": null,
"status": "none",
"file": "/opt/homebrew/opt/mailhog/homebrew.mxcl.mailhog.plist",
"command": "/opt/homebrew/opt/mailhog/bin/MailHog",
"working_dir": null,
"root_dir": null,
"log_path": "/opt/homebrew/var/log/mailhog.log",
"error_log_path": "/opt/homebrew/var/log/mailhog.log",
"interval": null,
"cron": null
},
{
"name": "nginx",
"service_name": "homebrew.mxcl.nginx",
"running": true,
"loaded": true,
"schedulable": false,
"pid": 116,
"exit_code": 0,
"user": "root",
"status": "started",
"file": "/Library/LaunchDaemons/homebrew.mxcl.nginx.plist",
"command": "/opt/homebrew/opt/nginx/bin/nginx -g daemon off;",
"working_dir": "/opt/homebrew",
"root_dir": null,
"log_path": null,
"error_log_path": null,
"interval": null,
"cron": null
},
{
"name": "php",
"service_name": "homebrew.mxcl.php",
"running": true,
"loaded": true,
"schedulable": false,
"pid": 142,
"exit_code": 0,
"user": "root",
"status": "started",
"file": "/Library/LaunchDaemons/homebrew.mxcl.php.plist",
"command": "/opt/homebrew/opt/php/sbin/php-fpm --nodaemonize",
"working_dir": "/opt/homebrew/var",
"root_dir": null,
"log_path": null,
"error_log_path": "/opt/homebrew/var/log/php-fpm.log",
"interval": null,
"cron": null
},
{
"name": "php@8.0",
"service_name": "homebrew.mxcl.php@8.0",
"running": false,
"loaded": false,
"schedulable": false,
"pid": null,
"exit_code": null,
"user": null,
"status": "none",
"file": "/opt/homebrew/opt/php@8.0/homebrew.mxcl.php@8.0.plist",
"command": "/opt/homebrew/opt/php@8.0/sbin/php-fpm --nodaemonize",
"working_dir": "/opt/homebrew/var",
"root_dir": null,
"log_path": null,
"error_log_path": "/opt/homebrew/var/log/php-fpm.log",
"interval": null,
"cron": null
},
{
"name": "unbound",
"service_name": "homebrew.mxcl.unbound",
"running": false,
"loaded": false,
"schedulable": false,
"pid": null,
"exit_code": null,
"user": null,
"status": "none",
"file": "/opt/homebrew/opt/unbound/homebrew.mxcl.unbound.plist",
"command": "/opt/homebrew/opt/unbound/sbin/unbound -d -c /opt/homebrew/etc/unbound/unbound.conf",
"working_dir": null,
"root_dir": null,
"log_path": null,
"error_log_path": null,
"interval": null,
"cron": null
}
]

View File

@ -19,7 +19,7 @@ class Utility {
try FileManager.default.copyItem(at: bundleURL, to: targetURL)
return targetURL
} catch let error {
print("Unable to copy file: \(error)")
Log.err("Unable to copy file: \(error)")
}
}

View File

@ -11,7 +11,7 @@ import XCTest
class ValetTest: XCTestCase {
func testDetermineValetVersion() {
let version = Actions.valet("--version")
let version = valet("--version")
XCTAssert(version.contains("Laravel Valet 2."))
}

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "check.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M435.848 83.466L172.804 346.51l-96.652-96.652c-4.686-4.686-12.284-4.686-16.971 0l-28.284 28.284c-4.686 4.686-4.686 12.284 0 16.971l133.421 133.421c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-28.284-28.284c-4.686-4.686-12.284-4.686-16.97 0z"/></svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "question-circle.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 448c-110.532 0-200-89.431-200-200 0-110.495 89.472-200 200-200 110.491 0 200 89.471 200 200 0 110.53-89.431 200-200 200zm107.244-255.2c0 67.052-72.421 68.084-72.421 92.863V300c0 6.627-5.373 12-12 12h-45.647c-6.627 0-12-5.373-12-12v-8.659c0-35.745 27.1-50.034 47.579-61.516 17.561-9.845 28.324-16.541 28.324-29.579 0-17.246-21.999-28.693-39.784-28.693-23.189 0-33.894 10.977-48.942 29.969-4.057 5.12-11.46 6.071-16.666 2.124l-27.824-21.098c-5.107-3.872-6.251-11.066-2.644-16.363C184.846 131.491 214.94 112 261.794 112c49.071 0 101.45 38.304 101.45 88.8zM298 368c0 23.159-18.841 42-42 42s-42-18.841-42-42 18.841-42 42-42 42 18.841 42 42z"/></svg>

After

Width:  |  Height:  |  Size: 966 B

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "times-circle.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z"/></svg>

After

Width:  |  Height:  |  Size: 685 B

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "check-circle.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z"/></svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@ -1,270 +0,0 @@
//
// Services.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class Actions {
// MARK: - Detect PHP Versions
public static func detectPhpVersions() -> [String]
{
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versionsOnly = Self.extractPhpVersions(from: files.components(separatedBy: "\n"))
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed
let phpAlias = App.shared.brewPhpVersion
// Avoid inserting a duplicate
if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) {
versionsOnly.append(phpAlias);
}
print("The PHP versions that were detected are: \(versionsOnly)")
App.shared.availablePhpVersions = versionsOnly
Actions.extractPhpLongVersions()
return versionsOnly
}
/**
This method extracts the PHP full version number after finding the php installation folders.
To be refactored at some later point, I'd like to cache the `PhpInstallation` objects instead of just the version number at some point.
*/
public static func extractPhpLongVersions()
{
var mappedVersions: [String: PhpInstallation] = [:]
App.shared.availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version)
}
App.shared.cachedPhpInstallations = mappedVersions
}
/**
Extracts valid PHP versions from an array of strings.
This array of strings is usually retrieved from `grep`.
*/
public static func extractPhpVersions(
from versions: [String],
checkBinaries: Bool = true
) -> [String] {
var output : [String] = []
versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected)
return version.starts(with: "php@")
}.forEach { (string) in
let version = string.components(separatedBy: "php@")[1]
// Only append the version if it doesn't already exist (avoid dupes),
// is supported and where the binary exists (avoids broken installs)
if !output.contains(version)
&& Constants.SupportedPhpVersions.contains(version)
&& (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
{
output.append(version)
}
}
return output
}
// MARK: - Services
public static func restartPhpFpm()
{
brew("services restart \(App.phpInstall!.formula)", sudo: true)
}
public static func restartNginx()
{
brew("services restart nginx", sudo: true)
}
public static func restartDnsMasq()
{
brew("services restart dnsmasq", sudo: true)
}
public static func stopAllServices()
{
brew("services stop \(App.phpInstall!.formula)", sudo: true)
brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true)
}
/**
Kindly asks Valet to switch to a specific PHP version.
*/
public static func switchToPhpVersionUsingValet(
version: String,
availableVersions: [String],
completed: @escaping () -> Void
) {
print("Switching to \(version) using Valet")
print(valet("use php@\(version)"))
completed()
}
/**
Switching to a new PHP version involves:
- unlinking the current version
- stopping the active services
- linking the new desired version
Please note that depending on which version is installed,
the version that is switched to may or may not be identical to `php` (without @version).
*/
public static func switchToPhpVersion(
version: String,
availableVersions: [String],
completed: @escaping () -> Void
) {
print("Switching to \(version), unlinking all versions...")
let group = DispatchGroup()
availableVersions.forEach { (available) in
group.enter()
DispatchQueue.global(qos: .userInitiated).async {
let formula = (available == App.shared.brewPhpVersion)
? "php" : "php@\(available)"
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
group.leave()
}
}
group.notify(queue: .global(qos: .userInitiated)) {
print("All versions have been unlinked!")
print("Linking the new version!")
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
brew("link \(formula) --overwrite --force")
brew("services start \(formula)", sudo: true)
print("The new version has been linked!")
completed()
}
}
// MARK: - Finding Config Files
public static func openGenericPhpConfigFolder()
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder()
{
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpConfigFolder(version: String)
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openValetConfigFolder()
{
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Quick Fix
/**
Detects all currently available PHP versions,
and unlinks each and every one of them.
After this, the brew services are also stopped,
the latest PHP version is linked, and php + nginx are restarted.
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()
{
brew("services restart dnsmasq", sudo: true)
detectPhpVersions().forEach { (version) in
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink php@\(version)")
brew("services stop \(formula)")
brew("services stop \(formula)", sudo: true)
}
brew("services stop php")
brew("services stop nginx")
brew("link php")
brew("services restart dnsmasq", sudo: true)
brew("services stop php", sudo: true)
brew("services stop nginx", sudo: true)
}
// MARK: Common Shell Commands
/**
Runs a `valet` command.
*/
public static func valet(_ command: String) -> String
{
return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true)
}
/**
Runs a `brew` command. Can run as superuser.
*/
public static func brew(_ command: String, sudo: Bool = false)
{
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
}
/**
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
*/
public static func sed(file: String, original: String, replacement: String)
{
// Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
// Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension
if Shell.fileExists("\(Paths.binPath)/gsed") {
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else {
Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
}
}
/**
Uses `grep` to determine whether a particular query string can be found in a particular file.
*/
public static func grepContains(file: String, query: String) -> Bool
{
return Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""")
.trimmingCharacters(in: .whitespacesAndNewlines)
.contains("YES")
}
}

View File

@ -20,13 +20,13 @@ extension App {
func loadGlobalHotkey() {
// Make sure we can retrieve the hotkey from preferences
guard let hotkey = Preferences.preferences[.globalHotkey] as? String else {
print("No global hotkey was saved in preferences. None set.")
Log.info("No global hotkey was saved in preferences. None set.")
return
}
// Make sure we can parse the JSON into the desired format
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
print("No global hotkey loaded, could not be parsed!")
Log.err("No global hotkey loaded, could not be parsed!")
shortcutHotkey = nil
return
}

View File

@ -8,7 +8,7 @@
import Cocoa
import HotKey
class App {
class App: PhpSwitcherDelegate {
// MARK: Static Vars
@ -22,14 +22,9 @@ class App {
return "\(version) (\(build))"
}
/** Information about the currently linked PHP installation. */
static var phpInstall: ActivePhpInstallation? {
return App.shared.currentInstall
}
/** Whether the app is busy doing something. Used to determine what UI to display. */
static var busy: Bool {
return App.shared.busy
return PhpEnv.shared.isBusy
}
// MARK: Variables
@ -43,42 +38,12 @@ class App {
/** The window controller of the currently active site list window. */
var siteListWindowController: SiteListWC? = nil
/** Whether the application is busy switching versions. */
var busy: Bool = false
/** The currently active installation of PHP. */
var currentInstall: ActivePhpInstallation? = nil
/** All available versions of PHP. */
var availablePhpVersions: [String] = []
/** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = []
/** Timer that will periodically reload info about the user's PHP installation. */
var timer: Timer?
/** Information we were able to discern from the Homebrew info command (as JSON). */
var brewPhpPackage: HomebrewPackage! = nil {
didSet {
brewPhpVersion = brewPhpPackage!.version
}
}
/**
The version that the `php` formula via Brew is aliased to on the current system.
If you're up to date, `php` will be aliased to the latest version,
but that might not be the case.
We'll technically default to the version in Constants.swift, but the information
should always be loaded from Homebrew itself upon startup.
*/
var brewPhpVersion: String = Constants.LatestStablePhpVersion
// MARK: - Global Hotkey
/**
@ -102,4 +67,26 @@ class App {
*/
var openWindows: [String] = []
// MARK: - App Watchers
/**
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
*/
var watcher: PhpConfigWatcher!
// MARK: - PhpSwitcherDelegate
func switcherDidStartSwitching() {
}
func switcherDidCompleteSwitch() {
PhpEnv.shared.currentInstall = ActivePhpInstallation()
handlePhpConfigWatcher()
if let window = siteListWindowController {
DispatchQueue.main.async {
window.contentVC.reloadSites()
}
}
}
}

View File

@ -0,0 +1,19 @@
//
// AppDelegate+InterApp.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 20/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
import Foundation
extension AppDelegate {
func application(_ application: NSApplication, open urls: [URL]) {
print(urls)
}
}

View File

@ -18,11 +18,11 @@ extension AppDelegate {
notificationCenter.delegate = self
notificationCenter.requestAuthorization(options: [.alert], completionHandler: { granted, error in
if !granted {
print("PHP Monitor does not have permission to show notifications.")
Log.warn("PHP Monitor does not have permission to show notifications.")
}
if let error = error {
print("PHP Monitor encounted an error determining notification permissions:")
print(error)
Log.err("PHP Monitor encounted an error determining notification permissions:")
Log.err(error)
}
})
}

View File

@ -44,16 +44,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
*/
let valet: Valet
/**
The PhpSwitcher singleton that handles PHP version
detection, as well as switching. It is initialized
when the app is ready and passed all checks.
*/
var switcher: PhpEnv! = nil
var logger = Log.shared
// MARK: - Initializer
/**
When the application initializes, create all singletons.
*/
override init() {
print("==================================")
print("PHP MONITOR by Nico Verbruggen")
print("Version \(App.version)")
print("==================================")
logger.verbosity = .performance
Log.info("==================================")
Log.info("PHP MONITOR by Nico Verbruggen")
Log.info("Version \(App.version)")
Log.info("==================================")
self.sharedShell = Shell.user
self.state = App.shared
self.menu = MainMenu.shared
@ -62,6 +72,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
super.init()
}
func initializeSwitcher() {
self.switcher = PhpEnv.shared
self.switcher.delegate = self.state
}
// MARK: - Lifecycle
/**

View File

@ -334,11 +334,14 @@
<objects>
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" wantsLayer="YES" id="Pf1-A5-3Xz">
<rect key="frame" x="0.0" y="0.0" width="574" height="498"/>
<rect key="frame" x="0.0" y="0.0" width="550" height="498"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView distribution="fillEqually" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="k57-O3-Yyj">
<rect key="frame" x="0.0" y="15" width="574" height="468"/>
<rect key="frame" x="0.0" y="15" width="550" height="468"/>
<constraints>
<constraint firstAttribute="width" constant="550" id="eOC-yS-nl6"/>
</constraints>
</stackView>
</subviews>
<constraints>
@ -371,6 +374,12 @@
</view>
<toolbar key="toolbar" implicitIdentifier="594015E3-8428-4926-9341-4B8CE4C7E373" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconOnly" sizeMode="regular" id="OOz-oZ-vlN">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="5B9DBBA8-D173-4EAF-807C-C6EA0B4D806A" label="Add Link" paletteLabel="Add Link" tag="-1" bordered="YES" sizingBehavior="auto" id="GsC-ra-40U">
<imageReference key="image" image="plus" catalog="system" symbolScale="medium"/>
<connections>
<action selector="pressedAddLink:" target="8Ec-9q-82s" id="H0o-No-x4M"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="B734CDE2-70E9-45A8-B1B3-5A5DE156621D" label="Reload" paletteLabel="Reload" tag="-1" bordered="YES" sizingBehavior="auto" id="YtK-vM-5y7">
<imageReference key="image" image="arrow.clockwise" catalog="system" symbolScale="medium"/>
<connections>
@ -391,6 +400,7 @@
</searchToolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="GsC-ra-40U"/>
<toolbarItem reference="YtK-vM-5y7"/>
<searchToolbarItem reference="Q7Z-fw-lB9"/>
</defaultToolbarItems>
@ -406,30 +416,192 @@
</windowController>
<customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="773.5"/>
<point key="canvasLocation" x="-374" y="758"/>
</scene>
<!--Window Controller-->
<scene sceneID="HTI-x5-rOp">
<objects>
<windowController storyboardIdentifier="addSiteWindow" id="N1O-Nj-C2V" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="yLy-XT-fuq">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="7Is-aK-lDv">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="N1O-Nj-C2V" id="CvY-PZ-Y6C"/>
</connections>
</window>
<connections>
<segue destination="glS-wF-sEU" kind="relationship" relationship="window.shadowedContentViewController" id="6Sa-w0-Uov"/>
</connections>
</windowController>
<customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-339" y="1147"/>
</scene>
<!--Add SiteVC-->
<scene sceneID="6JC-H6-u4K">
<objects>
<viewController storyboardIdentifier="newSiteLink" id="glS-wF-sEU" customClass="AddSiteVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="JJJ-T9-Yuv">
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
<rect key="frame" x="13" y="13" width="103" height="32"/>
<buttonCell key="cell" type="push" title="Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="pressedCreateLink:" target="glS-wF-sEU" id="Vh7-K5-ubM"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
<rect key="frame" x="391" y="13" width="76" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="pressedCancel:" target="glS-wF-sEU" id="MZS-Vg-Vjf"/>
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
<rect key="frame" x="20" y="156" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a potential domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="glS-wF-sEU" id="Dyf-0M-Gwj"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
<rect key="frame" x="18" y="134" width="444" height="14"/>
<textFieldCell key="cell" title="FOLDER_AVAILABLE" id="bJr-s6-tdP">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KZf-b0-9cm">
<rect key="frame" x="18" y="101" width="227" height="18"/>
<buttonCell key="cell" type="check" title="Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
<rect key="frame" x="18" y="66" width="444" height="28"/>
<textFieldCell key="cell" title="Securing a site requires administrative privileges. You will be prompted for your password or Touch ID." id="4gd-KM-5Fu">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6JT-Vt-3q0">
<rect key="frame" x="20" y="185" width="440" height="22"/>
<pathCell key="cell" selectable="YES" refusesFirstResponder="YES" alignment="left" id="m8d-XF-kh9">
<font key="font" metaFont="system"/>
<url key="url" string="file:///Users/nicoverbruggen/Code/nicoverbruggen.be/"/>
</pathCell>
</pathControl>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
<rect key="frame" x="18" y="215" width="87" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Link a Folder" id="S4j-ZC-ddT">
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
<rect key="frame" x="115" y="23" width="128" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That link already exists." id="jOt-n6-TQf">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="VzR-5a-cmT" firstAttribute="trailing" secondItem="ZX9-s1-23i" secondAttribute="trailing" id="06B-dj-IBU"/>
<constraint firstItem="ZX9-s1-23i" firstAttribute="top" secondItem="6JT-Vt-3q0" secondAttribute="bottom" constant="8" symbolic="YES" id="0QU-nI-sYv"/>
<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="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"/>
<constraint firstItem="900-Z2-tID" firstAttribute="leading" secondItem="PVw-cM-qAB" secondAttribute="trailing" constant="8" symbolic="YES" id="QzV-vP-fbq"/>
<constraint firstItem="VzR-5a-cmT" firstAttribute="leading" secondItem="ZX9-s1-23i" secondAttribute="leading" id="UPN-Ad-j3X"/>
<constraint firstItem="KZf-b0-9cm" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="Vab-wq-9Nc"/>
<constraint firstAttribute="bottom" secondItem="PVw-cM-qAB" secondAttribute="bottom" constant="20" symbolic="YES" id="VsP-Q0-zRW"/>
<constraint firstItem="ZX9-s1-23i" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="bJ4-Yr-4ah"/>
<constraint firstItem="KZf-b0-9cm" firstAttribute="top" secondItem="VzR-5a-cmT" secondAttribute="bottom" constant="16" id="bdw-P7-FLz"/>
<constraint firstAttribute="trailing" secondItem="SwS-o8-pbl" secondAttribute="trailing" constant="20" symbolic="YES" id="bkx-g2-WCM"/>
<constraint firstAttribute="trailing" secondItem="6JT-Vt-3q0" secondAttribute="trailing" constant="20" symbolic="YES" id="ctg-Gt-34Y"/>
<constraint firstItem="PVw-cM-qAB" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="fE5-T7-e8z"/>
<constraint firstItem="mmQ-7e-dlb" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="fKH-1r-MIf"/>
<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 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"/>
</constraints>
</view>
<connections>
<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"/>
<outlet property="textFieldTitle" destination="P0B-Ht-R8n" id="Qh8-qv-6iR"/>
</connections>
</viewController>
<customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="277" y="1137.5"/>
</scene>
<!--Site ListVC-->
<scene sceneID="aZt-6w-TFl">
<objects>
<viewController storyboardIdentifier="siteList" id="JZI-Vd-9oq" customClass="SiteListVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<viewController identifier="siteList" storyboardIdentifier="siteList" id="JZI-Vd-9oq" customClass="SiteListVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="rIZ-4U-bhj">
<rect key="frame" x="0.0" y="0.0" width="550" height="309"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<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="550" height="309"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/>
<clipView key="contentView" id="6IL-DW-37w">
<rect key="frame" x="1" y="1" width="548" height="307"/>
<rect key="frame" x="1" y="1" width="598" height="307"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" rowHeight="54" rowSizeStyle="automatic" viewBased="YES" id="cp3-34-pQj">
<rect key="frame" x="0.0" y="0.0" width="548" height="307"/>
<rect key="frame" x="0.0" y="0.0" width="598" height="307"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn width="536" minWidth="40" maxWidth="10000" id="oeH-B2-0rA">
<tableColumn width="586" minWidth="40" maxWidth="10000" id="oeH-B2-0rA">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
@ -442,7 +614,7 @@
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteItem" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="531" height="54"/>
<rect key="frame" x="8" y="0.0" width="581" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
@ -470,7 +642,7 @@
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Lock" id="aJ0-ia-YrZ"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jKi-Ls-7FZ">
<rect key="frame" x="459" y="28" width="64" height="11"/>
<rect key="frame" x="474" y="28" width="64" height="11"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="DRIVER TYPE" id="fjd-eb-itv">
<font key="font" metaFont="miniSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
@ -478,7 +650,7 @@
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TbX-e2-3QL">
<rect key="frame" x="459" y="15" width="36" height="14"/>
<rect key="frame" x="474" y="15" width="36" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Driver" id="GMt-SG-vFl">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -486,10 +658,10 @@
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="syz-LF-l6P">
<rect key="frame" x="0.0" y="-2" width="531" height="5"/>
<rect key="frame" x="0.0" y="-2" width="581" height="5"/>
</box>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0NQ-ZD-CqD">
<rect key="frame" x="435" y="18" width="18" height="18"/>
<rect key="frame" x="450" y="18" width="18" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="18" id="Suw-gm-AEi"/>
<constraint firstAttribute="height" constant="18" id="qO6-vg-5nC"/>
@ -497,8 +669,33 @@
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="IconLinked" id="2ng-pK-kvv"/>
<color key="contentTintColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/>
</imageView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="3xt-wC-hUJ">
<rect key="frame" x="363" y="18" width="75" height="18"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="75" id="VI8-MP-7Hv"/>
</constraints>
<buttonCell key="cell" type="inline" title=" PHP X.X" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="anZ-hP-G0R">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystemBold"/>
</buttonCell>
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<connections>
<action selector="pressedPhpVersion:" target="5GY-nN-BWd" id="mB5-WD-aZy"/>
</connections>
</button>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5aN-ZI-D7U">
<rect key="frame" x="341" y="20" width="14" height="14"/>
<constraints>
<constraint firstAttribute="height" constant="14" id="NKD-Pc-okU"/>
<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"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" secondItem="3xt-wC-hUJ" secondAttribute="trailing" constant="12" id="2G8-Ow-FTu"/>
<constraint firstItem="3xt-wC-hUJ" firstAttribute="leading" secondItem="5aN-ZI-D7U" secondAttribute="trailing" constant="8" symbolic="YES" id="39Z-nB-kXx"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="TbX-e2-3QL" secondAttribute="trailing" constant="20" symbolic="YES" id="3vE-LR-S7N"/>
<constraint firstItem="TbX-e2-3QL" firstAttribute="leading" secondItem="0NQ-ZD-CqD" secondAttribute="trailing" constant="8" symbolic="YES" id="4cb-D9-8d1"/>
<constraint firstItem="XJL-Uw-frD" firstAttribute="leading" secondItem="QPX-eu-eV8" secondAttribute="trailing" constant="10" id="55y-3V-RYt"/>
@ -509,17 +706,21 @@
<constraint firstItem="CXK-Q9-CpO" firstAttribute="leading" secondItem="XJL-Uw-frD" secondAttribute="leading" id="Ojw-VZ-3EG"/>
<constraint firstAttribute="trailing" secondItem="syz-LF-l6P" secondAttribute="trailing" id="PWd-5k-AlD"/>
<constraint firstItem="XJL-Uw-frD" firstAttribute="top" secondItem="5GY-nN-BWd" secondAttribute="top" constant="12" id="QeE-c7-I9U"/>
<constraint firstAttribute="trailing" secondItem="jKi-Ls-7FZ" secondAttribute="trailing" constant="10" id="Uhk-Dy-c65"/>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="Utr-aa-tqX"/>
<constraint firstItem="CXK-Q9-CpO" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="bottom" id="VKg-Vq-sYa"/>
<constraint firstItem="5aN-ZI-D7U" firstAttribute="centerY" secondItem="3xt-wC-hUJ" secondAttribute="centerY" id="a6n-E2-i2x"/>
<constraint firstItem="TbX-e2-3QL" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" constant="5" id="cN8-zO-fnc"/>
<constraint firstAttribute="bottom" secondItem="syz-LF-l6P" secondAttribute="bottom" id="gj7-cJ-Lle"/>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="CXK-Q9-CpO" secondAttribute="trailing" constant="8" symbolic="YES" id="iEd-Y3-zhp"/>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="8" symbolic="YES" id="lLA-Jx-Q4W"/>
<constraint firstItem="3xt-wC-hUJ" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="vhb-WC-3NC"/>
<constraint firstAttribute="trailing" secondItem="jKi-Ls-7FZ" secondAttribute="trailing" constant="45" id="vwD-Sg-Lzc"/>
<constraint firstItem="jKi-Ls-7FZ" firstAttribute="leading" secondItem="TbX-e2-3QL" secondAttribute="leading" id="zjN-s3-2Ww"/>
</constraints>
<connections>
<outlet property="buttonPhpVersion" destination="3xt-wC-hUJ" id="LpB-7n-qUr"/>
<outlet property="imageViewLock" destination="QPX-eu-eV8" id="Nnh-kB-adG"/>
<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="labelPathName" destination="CXK-Q9-CpO" id="iVZ-cL-azB"/>
@ -538,10 +739,10 @@
</clipView>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="300" id="R3Z-g3-tYQ"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="550" id="iRQ-sz-oyv"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="600" id="iRQ-sz-oyv"/>
</constraints>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
<rect key="frame" x="1" y="293" width="548" height="15"/>
<rect key="frame" x="1" y="293" width="598" height="15"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
@ -550,7 +751,7 @@
</scroller>
</scrollView>
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
<rect key="frame" x="260" y="150" width="30" height="30"/>
<rect key="frame" x="285" y="150" width="30" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
@ -573,12 +774,14 @@
</viewController>
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="288" y="765"/>
<point key="canvasLocation" x="251" y="742"/>
</scene>
</scenes>
<resources>
<image name="Checkmark" width="512" height="512"/>
<image name="IconLinked" width="512" height="512"/>
<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"/>
</resources>
</document>

View File

@ -6,6 +6,7 @@
//
import Foundation
import AppKit
class Startup {
@ -56,10 +57,9 @@ class Startup {
)
performEnvironmentCheck(
// Check for Valet; it can be symlinked or in .composer/vendor/bin
// Check for Valet; it MUST be symlinked thanks to sudoers
!(Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet")
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/opt/homebrew/bin/valet")
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains(".composer/vendor/bin/valet")
),
messageText: "startup.errors.sudoers_valet.title".localized,
informativeText: "startup.errors.sudoers_valet.desc".localized,
@ -75,37 +75,30 @@ class Startup {
)
if (!failed) {
determineBrewAliasVersion()
initializeSwitcher()
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
success()
}
}
/**
* In order to avoid having to hard-code which version of PHP is aliased to what specific subversion,
* PHP Monitor now determines the alias by checking the user's system.
Because the Switcher requires various environment guarantees, the switcher is only
initialized when it is done working.
*/
private func determineBrewAliasVersion()
{
print("PHP Monitor has determined the application has successfully passed all checks.")
print("Determining which version of PHP is aliased to `php` via Homebrew...")
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
App.shared.brewPhpPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
print("When on your system, the `php` formula means version \(App.shared.brewPhpVersion)!")
private func initializeSwitcher() {
DispatchQueue.main.async {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
appDelegate.initializeSwitcher()
}
}
/**
* Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
*
* - Parameter condition: Fail condition to check for; if this returns `true`, the alert will be shown
* - Parameter messageText: Short description of what is wrong
* - Parameter informativeText: Expanded description of the environment check that failed
* - Parameter breaking: If the application should terminate afterwards
Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
- Parameter condition: Fail condition to check for; if this returns `true`, the alert will be shown
- Parameter messageText: Short description of what is wrong
- Parameter informativeText: Expanded description of the environment check that failed
- Parameter breaking: If the application should terminate afterwards
*/
private func performEnvironmentCheck(
_ condition: Bool,

View File

@ -59,6 +59,5 @@ class Alert {
secondButtonTitle: "",
style: style
)
}
}
}

View File

@ -14,7 +14,7 @@ import Foundation
class Application {
enum AppType {
case editor, browser, git_gui, terminal
case editor, browser, git_gui, terminal, user_supplied
}
/// Name of the app. Used for display purposes and to determine `name.app` exists.
@ -34,16 +34,15 @@ class Application {
(This will open the app if it isn't open yet.)
*/
@objc public func openDirectory(file: String) {
return Shell.run("/usr/bin/open -a \"\(name)\" \(file)")
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
}
/** Checks if the app is installed. */
func isInstalled() -> Bool {
// If this script does not complain, the app exists!
return Shell.user.execute(
return Shell.user.executeSynchronously(
"/usr/bin/open -Ra \"\(name)\"",
requiresPath: false,
waitUntilExit: true
requiresPath: false
).task.terminationStatus == 0
}

View File

@ -0,0 +1,20 @@
//
// Async.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
public func runAsync(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
{
DispatchQueue.global(qos: .userInitiated).async {
execute()
DispatchQueue.main.async {
completion()
}
}
}

View File

@ -25,7 +25,7 @@ class LocalNotification {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) { (error) in
if error != nil {
print(error!)
Log.err(error!)
}
}
}

View File

@ -25,6 +25,18 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
App.shared.register(window: windowName)
}
func windowWillClose(_ notification: Notification) {
App.shared.remove(window: windowName)
}
deinit {
Log.perf("Window controller '\(windowName)' was deinitialized")
}
}
extension NSWindowController {
public func positionWindowInTopLeftCorner() {
guard let frame = NSScreen.main?.frame else { return }
guard let window = self.window else { return }
@ -37,12 +49,4 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
), display: true)
}
func windowWillClose(_ notification: Notification) {
App.shared.remove(window: windowName)
}
deinit {
print("Window controller '\(windowName)' was deinitialized")
}
}

View File

@ -0,0 +1,59 @@
//
// ComposerJson.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
struct ComposerJson: Decodable {
let dependencies: Dictionary<String, String>?
let devDependencies: Dictionary<String, String>?
let configuration: Config?
public func getPhpVersion() -> (String, String) {
// Check if in platform
if configuration?.platform?.php != nil {
return (configuration!.platform!.php!, "platform")
}
// Check if in dependencies
if dependencies?["php"] != nil {
return (dependencies!["php"]!, "require")
}
// Unknown!
return ("", "unknown")
}
public func getNotableDependencies() -> [String: String] {
var notable: [String: String] = [:]
let scan = ["php", "laravel/framework"]
scan.forEach { dependency in
if dependencies?[dependency] != nil {
notable[dependency] = dependencies![dependency]
}
}
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

@ -35,34 +35,34 @@ class HomebrewDiagnostics {
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
print("The user does not appear to have tapped: shivammathur/php")
Log.info("The user does not appear to have tapped: shivammathur/php")
return false
} else {
print("The user DOES have the following tapped: shivammathur/php")
print("Checking for `php` formula conflicts...")
Log.info("The user DOES have the following tapped: shivammathur/php")
Log.info("Checking for `php` formula conflicts...")
let tapPhp = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: tapAlias.data(using: .utf8)!
).first!
if tapPhp.version != App.shared.brewPhpVersion {
print("The `php` formula alias seems to be the different between the tap and core. This could be a problem!")
print("Determining whether both of these versions are installed...")
if tapPhp.version != PhpEnv.brewPhpVersion {
Log.warn("The `php` formula alias seems to be the different between the tap and core. This could be a problem!")
Log.info("Determining whether both of these versions are installed...")
let bothInstalled = App.shared.availablePhpVersions.contains(tapPhp.version)
&& App.shared.availablePhpVersions.contains(App.shared.brewPhpVersion)
let bothInstalled = PhpEnv.shared.availablePhpVersions.contains(tapPhp.version)
&& PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion)
if bothInstalled {
print("Both conflicting aliases seem to be installed, warning the user!")
Log.warn("Both conflicting aliases seem to be installed, warning the user!")
} else {
print("Conflicting aliases are not both installed, seems fine!")
Log.info("Conflicting aliases are not both installed, seems fine!")
}
return bothInstalled
}
print("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
Log.info("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
return false
}

View File

@ -22,7 +22,7 @@ class Valet {
var sites: [Site] = []
init() {
version = VersionExtractor.from(Actions.valet("--version"))
version = VersionExtractor.from(valet("--version"))
?? "UNKNOWN"
let file = FileManager.default.homeDirectoryForCurrentUser
@ -37,10 +37,14 @@ class Valet {
}
public func startPreloadingSites() {
if self.sites.count <= 10 {
let maximumPreload = 10
let foundSites = self.countPaths()
if foundSites <= maximumPreload {
// Preload the sites and their drivers
print("Fewer than or 11 sites found, preloading list of sites...")
Log.info("Fewer than or \(maximumPreload) sites found, preloading list of sites...")
self.reloadSites()
} else {
Log.info("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!")
}
}
@ -50,20 +54,39 @@ class Valet {
public func validateVersion() -> Void {
if version == "UNKNOWN" {
return print("The Valet version could not be extracted... that does not bode well.")
return Log.warn("The Valet version could not be extracted... that does not bode well.")
}
if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending {
let version = version
print("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))
}
} else {
print("Valet version \(version) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
Log.info("Valet version \(version) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
}
}
/**
Returns a count of how many sites are linked and parked.
*/
private func countPaths() -> Int {
var count = 0
for path in config.paths {
let entries = try! FileManager.default.contentsOfDirectory(atPath: path)
for entry in entries {
if resolveSite(entry, forPath: path) {
count += 1
}
}
}
return count
}
/**
Resolves all paths and creates linked or parked site instances that can be referenced later.
*/
private func resolvePaths(tld: String) {
sites = []
@ -73,8 +96,32 @@ class Valet {
resolvePath(entry, forPath: path, tld: tld)
}
}
sites = sites.sorted { $0.absolutePath < $1.absolutePath }
}
/**
Determines whether the site can be resolved as a symbolic link or as a directory.
Regular files are ignored. Returns true if the path can be parsed.
*/
private func resolveSite(_ entry: String, forPath path: String) -> Bool {
let siteDir = path + "/" + entry
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
let type = attrs[FileAttributeKey.type] as! FileAttributeType
if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory {
return true
}
return false
}
/**
Determines whether the site can be resolved as a symbolic link or as a directory.
Regular files are ignored, and the site is added to Valet's list of sites.
*/
private func resolvePath(_ entry: String, forPath path: String, tld: String) {
let siteDir = path + "/" + entry
@ -84,6 +131,12 @@ class Valet {
// We can also determine whether the thing at the path is a directory, too
let type = attrs[FileAttributeKey.type] as! FileAttributeType
// We should also check that we can interpret the path correctly
if URL(fileURLWithPath: siteDir).lastPathComponent == "" {
print("Warning: could not parse the site: \(siteDir), skipping!")
return
}
if type == FileAttributeType.typeSymbolicLink {
sites.append(Site(aliasPath: siteDir, tld: tld))
} else if type == FileAttributeType.typeDirectory {
@ -109,24 +162,35 @@ class Valet {
/// What driver is currently in use. If not detected, defaults to nil.
var driver: String? = nil
/// A list of notable Composer dependencies.
var notableComposerDependencies: [String: String] = [:]
/// The PHP version as discovered in composer.json.
var composerPhp: String = "???"
/// How the PHP version was determined.
var composerPhpSource: String = "unknown"
init() {}
convenience init(absolutePath: String, tld: String) {
self.init()
self.absolutePath = absolutePath
self.name = URL(string: absolutePath)!.lastPathComponent
self.name = URL(fileURLWithPath: absolutePath).lastPathComponent
self.aliasPath = nil
determineSecured(tld)
determineDriver()
determineComposerPhpVersion()
}
convenience init(aliasPath: String, tld: String) {
self.init()
self.absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
self.name = URL(string: aliasPath)!.lastPathComponent
self.name = URL(fileURLWithPath: aliasPath).lastPathComponent
self.aliasPath = aliasPath
determineSecured(tld)
determineDriver()
determineComposerPhpVersion()
}
public func determineSecured(_ tld: String) {
@ -134,7 +198,7 @@ class Valet {
}
public func determineDriver() {
let driver = Shell.pipe("cd \(absolutePath!) && valet which", requiresPath: true)
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?
@ -144,6 +208,23 @@ class Valet {
self.driver = nil
}
}
public func determineComposerPhpVersion() {
let path = "\(absolutePath!)/composer.json"
do {
if Filesystem.fileExists(path) {
let decoded = try JSONDecoder().decode(
ComposerJson.self,
from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)!
)
(self.composerPhp, self.composerPhpSource) = decoded.getPhpVersion()
self.notableComposerDependencies = decoded.getNotableDependencies()
}
} catch {
Log.err("Something went wrong reading the composer JSON file.")
}
}
}
struct Configuration: Decodable {

View File

@ -0,0 +1,112 @@
//
// MainMenu+Startup.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 03/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension MainMenu {
/**
Kick off the startup of the rendering of the main menu.
*/
func startup() {
// Start with the icon
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
// Perform environment boot checks
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
Startup().checkEnvironment(success: { onEnvironmentPass() },
failure: { onEnvironmentFail() }
)
}
}
/**
When the environment is all clear and the app can run, let's go.
*/
private func onEnvironmentPass() {
PhpEnv.detectPhpVersions()
if HomebrewDiagnostics.shared.errors.contains(.aliasConflict) {
DispatchQueue.main.async {
Alert.notify(
message: "alert.php_alias_conflict.title".localized,
info: "alert.php_alias_conflict.info".localized,
style: .critical
)
}
}
updatePhpVersionInStatusBar()
Log.info("Determining broken PHP-FPM...")
// Attempt to find out if PHP-FPM is broken
let installation = PhpEnv.phpInstall
installation.notifyAboutBrokenPhpFpm()
// Set up the config watchers on launch (these are automatically updated via delegate methods if the user switches)
Log.info("Setting up watchers...")
App.shared.handlePhpConfigWatcher()
// Detect applications (preset + custom)
Log.info("Detecting applications...")
App.shared.detectedApplications = Application.detectPresetApplications()
let customApps = Preferences.custom.scanApps.map { appName in
return Application(appName, .user_supplied)
}.filter { app in
return app.isInstalled()
}
App.shared.detectedApplications.append(contentsOf: customApps)
let appNames = App.shared.detectedApplications.map { app in
return app.name
}
Log.info("Detected applications: \(appNames)")
// Load the global hotkey
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)")
Valet.shared.validateVersion()
Valet.shared.startPreloadingSites()
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
Log.info("PHP Monitor is ready to serve!")
// Schedule a request to fetch the PHP version every 60 seconds
DispatchQueue.main.async { [self] in
App.shared.timer = Timer.scheduledTimer(
timeInterval: 60,
target: self,
selector: #selector(refreshActiveInstallation),
userInfo: nil,
repeats: true
)
}
}
/**
When the environment is not OK, present an alert to inform the user.
*/
private func onEnvironmentFail() {
DispatchQueue.main.async { [self] in
let close = Alert.present(
messageText: "alert.cannot_start.title".localized,
informativeText: "alert.cannot_start.info".localized,
buttonTitle: "alert.cannot_start.close".localized,
secondButtonTitle: "alert.cannot_start.retry".localized
)
if (close) {
exit(1)
}
startup()
}
}
}

View File

@ -22,97 +22,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
// MARK: - UI related
/**
Kick off the startup of the rendering of the main menu.
*/
func startup() {
// Start with the icon
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
// Perform environment boot checks
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
Startup().checkEnvironment(success: { onEnvironmentPass() },
failure: { onEnvironmentFail() }
)
}
}
/**
When the environment is all clear and the app can run, let's go.
*/
private func onEnvironmentPass() {
_ = Actions.detectPhpVersions()
if HomebrewDiagnostics.shared.errors.contains(.aliasConflict) {
DispatchQueue.main.async {
Alert.notify(
message: "alert.php_alias_conflict.title".localized,
info: "alert.php_alias_conflict.info".localized,
style: .critical
)
}
}
updatePhpVersionInStatusBar()
print("Determining broken PHP-FPM...")
// Attempt to find out if PHP-FPM is broken
let installation = App.phpInstall!
installation.notifyAboutBrokenPhpFpm()
print("Detecting applications...")
// Attempt to load list of applications
App.shared.detectedApplications = Application.detectPresetApplications()
let appNames = App.shared.detectedApplications.map { app in
return app.name
}
print("Detected applications: \(appNames)")
// Load the global hotkey
App.shared.loadGlobalHotkey()
// Attempt to find out more info about Valet
print("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version)")
Valet.shared.validateVersion()
Valet.shared.startPreloadingSites()
print("PHP Monitor is ready to serve!")
// Schedule a request to fetch the PHP version every 60 seconds
DispatchQueue.main.async { [self] in
App.shared.timer = Timer.scheduledTimer(
timeInterval: 60,
target: self,
selector: #selector(updatePhpVersionInStatusBar),
userInfo: nil,
repeats: true
)
}
}
/**
When the environment is not OK, present an alert to inform the user.
*/
private func onEnvironmentFail() {
DispatchQueue.main.async { [self] in
let close = Alert.present(
messageText: "alert.cannot_start.title".localized,
informativeText: "alert.cannot_start.info".localized,
buttonTitle: "alert.cannot_start.close".localized,
secondButtonTitle: "alert.cannot_start.retry".localized
)
if (close) {
exit(1)
}
startup()
}
}
/**
Update the menu's contents, based on what's going on.
This will rebuild the entire menu, so this can take a few moments.
*/
func update() {
func rebuild() {
// Update the menu item on the main thread
DispatchQueue.main.async { [self] in
// Create a new menu
@ -131,10 +45,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
menu.addItem(NSMenuItem.separator())
// Add services
menu.addServicesMenuItems()
menu.addItem(NSMenuItem.separator())
// Add information about services & actions
menu.addPhpConfigurationMenuItems()
menu.addItem(NSMenuItem.separator())
@ -158,7 +68,9 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
*/
func setStatusBarImage(version: String) {
setStatusBar(
image: MenuBarImageGenerator.textToImageWithIcon(text: version)
image: Preferences.isEnabled(.shouldDisplayPhpHintInIcon)
? MenuBarImageGenerator.textToImageWithIcon(text: version)
: MenuBarImageGenerator.textToImage(text: version)
)
}
@ -185,16 +97,15 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
*/
private func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
{
App.shared.busy = true
PhpEnv.shared.isBusy = true
setBusyImage()
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
update()
execute()
App.shared.busy = false
PhpEnv.shared.isBusy = false
DispatchQueue.main.async { [self] in
updatePhpVersionInStatusBar()
update()
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
completion()
}
}
@ -202,10 +113,18 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
// MARK: - User Interface
@objc func refreshActiveInstallation() {
if !PhpEnv.shared.isBusy {
PhpEnv.shared.currentInstall = ActivePhpInstallation()
updatePhpVersionInStatusBar()
} else {
Log.perf("Skipping version refresh due to busy status")
}
}
@objc func updatePhpVersionInStatusBar() {
App.shared.currentInstall = ActivePhpInstallation()
refreshIcon()
update()
rebuild()
}
func refreshIcon() {
@ -219,22 +138,23 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
} else {
// The dynamic icon has been requested
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
setStatusBarImage(version: long ? App.phpInstall!.version.long : App.phpInstall!.version.short)
setStatusBarImage(version: long ? PhpEnv.phpInstall.version.long : PhpEnv.phpInstall.version.short)
}
}
}
}
@objc func reloadPhpMonitorMenuInBackground() {
waitAndExecute {
// This automatically reloads the menu
Log.info("Reloading information about the PHP installation (in the background)...")
}
}
@objc func reloadPhpMonitorMenu() {
waitAndExecute {
// This automatically reloads the menu
print("Reloading information about the PHP installation...")
} completion: {
// Add a slight delay to make sure it loads the new menu
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// Open the menu again
MainMenu.shared.statusItem.button?.performClick(nil)
}
Log.info("Reloading information about the PHP installation...")
}
}
@ -296,46 +216,54 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
waitAndExecute {
sender.phpExtension?.toggle()
if Preferences.preferences[.autoServiceRestartAfterExtensionToggle] as! Bool == true {
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
Actions.restartPhpFpm()
}
}
}
@objc func openPhpInfo() {
var url: URL? = nil
waitAndExecute {
// Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
// Tell php-cgi to run the PHP and output as an .html file
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
url = Actions.createTempPhpInfoFile()
} completion: {
// When this has been completed, open the URL to the file in the browser
NSWorkspace.shared.open(URL(string: "file:///private/tmp/phpmon_phpinfo.html")!)
NSWorkspace.shared.open(url!)
}
}
@objc func forceRestartLatestPhp() {
// Tell the user the switch is about to occur
Alert.notify(message: "alert.force_reload.title".localized, info: "alert.force_reload.info".localized)
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)
Alert.notify(
message: "alert.force_reload_done.title".localized,
info: "alert.force_reload_done.info".localized
)
}
}
@objc func updateGlobalComposerDependencies() {
self.updateGlobalDependencies(notify: true, completion: { _ in })
}
@objc func openActiveConfigFolder() {
if (App.phpInstall!.version.error) {
if (PhpEnv.phpInstall.version.error) {
// php version was not identified
Actions.openGenericPhpConfigFolder()
return
}
// php version was identified
Actions.openPhpConfigFolder(version: App.phpInstall!.version.short)
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
}
@objc func openGlobalComposerFolder() {
@ -347,53 +275,52 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
}
@objc func switchToPhpVersion(sender: PhpMenuItem) {
self.switchToPhpVersion(sender.version)
}
@objc func switchToPhpVersion(_ version: String) {
setBusyImage()
App.shared.busy = true
PhpEnv.shared.isBusy = true
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
// Update the PHP version in the status bar
updatePhpVersionInStatusBar()
// Update the menu
update()
rebuild()
let completion = {
PhpEnv.shared.delegate?.switcherDidCompleteSwitch()
// Mark as no longer busy
App.shared.busy = false
PhpEnv.shared.isBusy = false
// Perform UI updates on main thread
DispatchQueue.main.async { [self] in
updatePhpVersionInStatusBar()
update()
rebuild()
// Send a notification that the switch has been completed
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, sender.version),
subtitle: String(format: "notification.version_changed_desc".localized, sender.version)
)
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()
}
App.phpInstall?.notifyAboutBrokenPhpFpm()
// Run composer updates
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
self.updateGlobalDependencies(notify: false, completion: { _ in sendLocalNotification() })
} else {
sendLocalNotification()
}
}
}
/* DISABLED UNTIL VALET SWITCHING IS OK (see #34)
if Preferences.preferences[.useInternalSwitcher] as! Bool == false {
// 1. Default switcher using Valet
// Will cause less issues, but is slower
Actions.switchToPhpVersionUsingValet(
version: sender.version,
availableVersions: App.shared.availablePhpVersions,
completed: completion
)
} else { */
// 2. Custom switcher (internal)
// Will cause more issues with Homebrew and is faster
Actions.switchToPhpVersion(
version: sender.version,
availableVersions: App.shared.availablePhpVersions,
completed: completion
)
/* } */
PhpEnv.switcher.performSwitch(
to: version,
completion: completion
)
}
}
@ -425,4 +352,85 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
// When the menu is closed, allow the shortcut to work again
App.shared.shortcutHotkey?.isPaused = false
}
// MARK: - Private Methods
/**
*/
private func updateGlobalDependencies(notify: Bool, completion: @escaping (Bool) -> Void) {
PhpEnv.shared.isBusy = true
setBusyImage()
self.rebuild()
let noLongerBusy = {
PhpEnv.shared.isBusy = false
DispatchQueue.main.async { [self] in
self.updatePhpVersionInStatusBar()
self.rebuild()
}
}
var window: ProgressWindowController? = ProgressWindowController.display(
title: "alert.composer_progress.title".localized,
description: "alert.composer_progress.info".localized
)
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: "composer global update", requiresPath: true)
DispatchQueue.main.async {
window?.addToConsole("composer global update\n")
}
Shell.captureOutput(
task,
didReceiveStdOutData: { string in
DispatchQueue.main.async {
window?.addToConsole(string)
}
Log.perf("\(string.trimmingCharacters(in: .newlines))")
},
didReceiveStdErrData: { string in
DispatchQueue.main.async {
window?.addToConsole(string)
}
Log.perf("\(string.trimmingCharacters(in: .newlines))")
}
)
task.launch()
task.waitUntilExit()
Shell.haltCapturingOutput(task)
DispatchQueue.main.async {
if output.task.terminationStatus <= 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
window?.close()
if (notify) {
LocalNotification.send(
title: "alert.composer_success.title".localized,
subtitle: "alert.composer_success.info".localized
)
}
window = nil
noLongerBusy()
completion(true)
}
} else {
window?.setType(info: false)
window?.progressView?.labelTitle.stringValue = "alert.composer_failure.title".localized
window?.progressView?.labelDescription.stringValue = "alert.composer_failure.info".localized
window = nil
noLongerBusy()
completion(false)
}
}
}
}
}

View File

@ -0,0 +1,93 @@
//
// StatsView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class ServicesView: NSView, XibLoadable {
@IBOutlet weak var imageViewPhp: NSImageView!
@IBOutlet weak var imageViewNginx: NSImageView!
@IBOutlet weak var imageViewDnsmasq: NSImageView!
@IBOutlet weak var textFieldPhp: NSTextField!
static var services: [String: HomebrewService] = [:]
static func asMenuItem() -> NSMenuItem {
let view = Self.createFromXib()!
let item = NSMenuItem()
item.view = view
item.target = self
NotificationCenter.default.addObserver(
view, selector: #selector(self.updateInformation),
name: Events.ServicesUpdated,
object: nil
)
return item
}
override func viewWillDraw() {
super.viewWillDraw()
self.loadData()
}
@objc func updateInformation() {
self.loadData()
}
func loadData() {
// Use stale data
self.applyAllInfoFieldsFromCachedValue()
// Re-fetch services
runAsync {
let servicesList = try! JSONDecoder().decode(
[HomebrewService].self,
from: Shell.pipe(
"sudo \(Paths.brew) services info --all --json",
requiresPath: true
).data(using: .utf8)!
).filter({ service in
return [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"].contains(service.name)
})
ServicesView.services = Dictionary(uniqueKeysWithValues: servicesList.map{ ($0.name, $0) })
} completion: {
// Use fresh data
self.applyAllInfoFieldsFromCachedValue()
}
}
func applyAllInfoFieldsFromCachedValue() {
if ServicesView.services.keys.isEmpty {
return
}
DispatchQueue.main.async {
self.textFieldPhp.stringValue = PhpEnv.phpInstall.formula.uppercased()
self.applyServiceStyling(PhpEnv.phpInstall.formula, self.imageViewPhp)
self.applyServiceStyling("nginx", self.imageViewNginx)
self.applyServiceStyling("dnsmasq", self.imageViewDnsmasq)
}
}
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)
}
}
deinit {
NotificationCenter.default.removeObserver(self, name: Events.ServicesUpdated, object: nil)
}
}

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="ServicesView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="340" height="46"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
<rect key="frame" x="30" y="3" width="280" height="40"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
<rect key="frame" x="0.0" y="4" width="80" height="32"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="At1-ch-qv2">
<rect key="frame" x="25" y="18" width="31" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="PHP" id="LKe-C4-jxo">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="tko-cP-XSz">
<rect key="frame" x="28" y="0.0" width="24" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="Fxu-6h-A2h"/>
<constraint firstAttribute="width" constant="24" id="hOc-Ur-dmA"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="vjB-6Z-3xR"/>
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
</imageView>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fillEqually" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g4d-4N-NkC">
<rect key="frame" x="100" y="4" width="80" height="32"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7um-XA-djV">
<rect key="frame" x="20" y="18" width="40" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="NGINX" id="Qfq-Bl-yuh">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ZqW-6d-vpe">
<rect key="frame" x="32" y="0.0" width="16" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="EPG-jm-7Xs"/>
<constraint firstAttribute="width" constant="16" id="iif-kT-phn"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="JmQ-dU-ip7"/>
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
</imageView>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
<rect key="frame" x="200" y="4" width="80" height="32"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
<rect key="frame" x="9" y="18" width="62" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="DNSMASQ" id="lGh-MT-TgI">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="DcG-x3-lvy">
<rect key="frame" x="32" y="0.0" width="16" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="16" id="AKl-Gq-RtM"/>
<constraint firstAttribute="height" constant="16" id="q2g-Ua-eIJ"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="Ign-Cq-DKf"/>
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
</imageView>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="40" id="2EU-Fd-hMg"/>
<constraint firstItem="nWj-33-m8Q" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="CAY-Pw-B8n"/>
<constraint firstAttribute="bottom" secondItem="doH-ww-BDw" secondAttribute="bottom" constant="4" id="Dq4-M6-1Wf"/>
<constraint firstItem="g4d-4N-NkC" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="bls-fM-H4b"/>
<constraint firstAttribute="bottom" secondItem="nWj-33-m8Q" secondAttribute="bottom" constant="4" id="f6j-eI-wiH"/>
<constraint firstAttribute="bottom" secondItem="g4d-4N-NkC" secondAttribute="bottom" constant="4" id="faS-Mo-Qa2"/>
<constraint firstItem="doH-ww-BDw" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="gL3-5S-OKo"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="TnH-dX-qaQ" secondAttribute="trailing" constant="30" id="3dD-wf-5pS"/>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="3" id="JmY-D0-uAy"/>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="30" id="S8i-CD-j3h"/>
<constraint firstAttribute="bottom" secondItem="TnH-dX-qaQ" secondAttribute="bottom" constant="3" id="fDc-OY-YL0"/>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="fFF-rl-3s4"/>
</constraints>
<connections>
<outlet property="imageViewDnsmasq" destination="DcG-x3-lvy" id="XxJ-kZ-bdO"/>
<outlet property="imageViewNginx" destination="ZqW-6d-vpe" id="Wil-Ug-8Kb"/>
<outlet property="imageViewPhp" destination="tko-cP-XSz" id="q7L-HK-7Pj"/>
<outlet property="textFieldPhp" destination="At1-ch-qv2" id="Guk-hr-f1T"/>
</connections>
<point key="canvasLocation" x="-64" y="195"/>
</customView>
</objects>
<resources>
<image name="ServiceLoading" width="512" height="512"/>
</resources>
</document>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -10,11 +10,11 @@
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="StatsView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="341" height="55"/>
<rect key="frame" x="0.0" y="0.0" width="340" height="55"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
<rect key="frame" x="30" y="6" width="281" height="43"/>
<rect key="frame" x="30" y="6" width="280" height="43"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
<rect key="frame" x="0.0" y="4" width="87" height="35"/>
@ -75,10 +75,10 @@
</customSpacing>
</stackView>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
<rect key="frame" x="204" y="4" width="77" height="35"/>
<rect key="frame" x="204" y="4" width="76" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
<rect key="frame" x="-1" y="21" width="79" height="14"/>
<rect key="frame" x="-2" y="21" width="79" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX UPLOAD" id="lGh-MT-TgI">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
@ -86,7 +86,7 @@
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eHT-tr-Kwx">
<rect key="frame" x="11" y="0.0" width="55" height="19"/>
<rect key="frame" x="10" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="1iA-Ri-zYY">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -138,7 +138,7 @@
<outlet property="titleMaxUpload" destination="Oef-6n-9QI" id="Q61-JI-RJq"/>
<outlet property="titleMemLimit" destination="At1-ch-qv2" id="SQT-B9-sWS"/>
</connections>
<point key="canvasLocation" x="-84.5" y="44"/>
<point key="canvasLocation" x="139" y="168"/>
</customView>
</objects>
</document>

View File

@ -9,18 +9,14 @@ import Cocoa
class StatusMenu : NSMenu {
func addPhpVersionMenuItems() {
if App.shared.currentInstall == nil {
return
}
if App.phpInstall!.version.error {
if PhpEnv.phpInstall.version.error {
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
}
return
}
let phpVersionText = "\("mi_php_version".localized) \(App.phpInstall!.version.long)"
let phpVersionText = "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)"
addItem(HeaderView.asMenuItem(text: phpVersionText))
}
@ -30,47 +26,56 @@ class StatusMenu : NSMenu {
return
}
if App.shared.availablePhpVersions.count == 0 {
if PhpEnv.shared.availablePhpVersions.count == 0 {
return
}
self.addSwitchToPhpMenuItems()
self.addItem(NSMenuItem.separator())
self.addItem(ServicesView.asMenuItem())
self.addItem(NSMenuItem.separator())
}
func addServicesMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_active_services".localized))
let services = NSMenuItem(title: "mi_manage_services".localized, action: nil, keyEquivalent: "")
func addOtherMenuItems() {
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
let servicesMenu = NSMenu()
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
servicesMenu.addItem(
NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
withKeyModifier: [.command, .shift])
servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
for item in servicesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(servicesMenu, for: services)
self.addForceLoadLatestVersion()
self.addItem(services)
}
func addForceLoadLatestVersion() {
if !App.shared.availablePhpVersions.contains(App.shared.brewPhpVersion) {
self.addItem(NSMenuItem(
title: "mi_force_load_latest_unavailable".localized(App.shared.brewPhpVersion),
servicesMenu.addItem(NSMenuItem(title: "mi_help".localized, action: nil, keyEquivalent: ""))
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
servicesMenu.addItem(NSMenuItem(
title: "mi_force_load_latest_unavailable".localized(PhpEnv.brewPhpVersion),
action: nil, keyEquivalent: "f"
))
} else {
self.addItem(NSMenuItem(
title: "mi_force_load_latest".localized(App.shared.brewPhpVersion),
servicesMenu.addItem(NSMenuItem(
title: "mi_force_load_latest".localized(PhpEnv.brewPhpVersion),
action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: "f"))
}
servicesMenu.addItem(NSMenuItem(title: "mi_services".localized, action: nil, keyEquivalent: ""))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
servicesMenu.addItem(
NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
withKeyModifier: [.command, .shift])
servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
servicesMenu.addItem(NSMenuItem(title: "mi_manual_actions".localized, action: nil, keyEquivalent: ""))
servicesMenu.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r"))
for item in servicesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(servicesMenu, for: services)
self.addItem(services)
}
func addValetMenuItems() {
@ -81,21 +86,22 @@ class StatusMenu : NSMenu {
}
func addPhpConfigurationMenuItems() {
if App.shared.currentInstall == nil {
return
}
// Configuration
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
self.addItem(NSMenuItem(title: "mi_global_composer".localized, action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g"))
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
if (App.shared.busy) {
// Composer
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: ""))
if (PhpEnv.shared.isBusy) {
return
}
let stats = App.phpInstall!.limits
let stats = PhpEnv.phpInstall.limits
// Stats
self.addItem(NSMenuItem.separator())
@ -109,37 +115,37 @@ class StatusMenu : NSMenu {
self.addItem(NSMenuItem.separator())
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
if (App.phpInstall!.extensions.count == 0) {
if (PhpEnv.phpInstall.extensions.count == 0) {
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
}
var shortcutKey = 1
for phpExtension in App.phpInstall!.extensions {
for phpExtension in PhpEnv.phpInstall.extensions {
self.addExtensionItem(phpExtension, shortcutKey)
shortcutKey += 1
}
// Other
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r"))
self.addOtherMenuItems()
}
private func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
// Get the short and long version
let shortVersion = App.shared.availablePhpVersions[index]
let longVersion = App.shared.cachedPhpInstallations[shortVersion]!.longVersion
let shortVersion = PhpEnv.shared.availablePhpVersions[index]
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.longVersion
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
let versionString = long ? longVersion : shortVersion
let versionString = long ? longVersion.homebrewVersion : shortVersion
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (shortVersion == App.shared.brewPhpVersion) ? "php" : "php@\(shortVersion)"
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
let menuItem = PhpMenuItem(
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
action: (shortVersion == App.phpInstall?.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
action: (shortVersion == PhpEnv.phpInstall.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
)
menuItem.version = shortVersion

View File

@ -0,0 +1,33 @@
//
// ActivePhpInstallation.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
extension ActivePhpInstallation {
/**
It is always possible that the system configuration for PHP-FPM has not been set up for Valet.
This can occur when a user manually installs a new PHP version, but does not run `valet install`.
In that case, we should alert the user!
- Important: The underlying check is `checkPhpFpmStatus`, which can be run multiple times.
This method actively presents a modal if said checks fails, so don't call this method too many times.
*/
public func notifyAboutBrokenPhpFpm() {
if !self.checkPhpFpmStatus() {
DispatchQueue.main.async {
Alert.notify(
message: "alert.php_fpm_broken.title".localized,
info: "alert.php_fpm_broken.info".localized,
style: .critical
)
}
}
}
}

View File

@ -0,0 +1,17 @@
//
// CustomPrefs.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 03/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
struct CustomPrefs: Decodable {
let scanApps: [String]
private enum CodingKeys: String, CodingKey {
case scanApps = "scan_apps"
}
}

View File

@ -11,9 +11,10 @@ import Foundation
enum PreferenceName: String {
case wasLaunchedBefore = "launched_before"
case shouldDisplayDynamicIcon = "use_dynamic_icon"
case shouldDisplayPhpHintInIcon = "add_php_to_icon"
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
case useInternalSwitcher = "use_phpmon_switcher"
case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch"
case globalHotkey = "global_hotkey"
}
@ -23,11 +24,15 @@ class Preferences {
static var shared = Preferences()
var customPreferences: CustomPrefs
var cachedPreferences: [PreferenceName: Any?]
public init() {
Preferences.handleFirstTimeLaunch()
cachedPreferences = Self.cache()
customPreferences = CustomPrefs(scanApps: [])
loadCustomPreferences()
}
// MARK: - First Time Run
@ -45,15 +50,16 @@ class Preferences {
static func handleFirstTimeLaunch() {
UserDefaults.standard.register(defaults: [
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
PreferenceName.shouldDisplayPhpHintInIcon.rawValue: true,
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true,
PreferenceName.useInternalSwitcher.rawValue: false
PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue: false
])
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
return
}
print("Saving first-time preferences!")
Log.info("Saving first-time preferences!")
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
UserDefaults.standard.synchronize()
}
@ -64,15 +70,32 @@ class Preferences {
return Self.shared.cachedPreferences
}
static var custom: CustomPrefs {
return Self.shared.customPreferences
}
/**
Determine whether a particular preference is enabled.
- Important: Requires the preference to have a corresponding boolean value, or a fatal error will be thrown.
*/
static func isEnabled(_ preference: PreferenceName) -> Bool {
if let bool = Preferences.preferences[preference] as? Bool {
return bool == true
} else {
fatalError("\(preference) is not a valid boolean preference!")
}
}
// MARK: - Internal Functionality
static func cache() -> [PreferenceName: Any] {
private static func cache() -> [PreferenceName: Any] {
return [
// Part 1: Always Booleans
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
.shouldDisplayPhpHintInIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayPhpHintInIcon.rawValue) as Any,
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
.useInternalSwitcher: UserDefaults.standard.bool(forKey: PreferenceName.useInternalSwitcher.rawValue) as Any,
.autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any,
// Part 2: Always Strings
.globalHotkey: UserDefaults.standard.string(forKey: PreferenceName.globalHotkey.rawValue) as Any,
@ -91,4 +114,28 @@ class Preferences {
Preferences.shared.cachedPreferences = Preferences.cache()
}
// MARK: - Custom Preferences
private func loadCustomPreferences() {
let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json")
if Filesystem.fileExists(url.path) {
Log.info("A custom .phpmon.conf.json file was found. Attempting to parse...")
loadCustomPreferencesFile(url)
} else {
Log.info("There was no .phpmon.conf.json file to be loaded.")
}
}
private func loadCustomPreferencesFile(_ url: URL) {
do {
customPreferences = try JSONDecoder().decode(
CustomPrefs.self,
from: try! String(contentsOf: url, encoding: .utf8).data(using: .utf8)!
)
Log.info("The .phpmon.conf.json file was successfully parsed.")
} catch {
Log.warn("The .phpmon.conf.json file seems to be missing or malformed.")
}
}
}

View File

@ -59,12 +59,21 @@ class PrefsVC: NSViewController {
),
CheckboxPreferenceView.make(
sectionText: "",
descriptionText: "prefs.icon_hint_desc".localized,
checkboxText: "prefs.icon_hint_title".localized,
preference: .shouldDisplayPhpHintInIcon,
action: {
MainMenu.shared.refreshIcon()
}
),
CheckboxPreferenceView.make(
sectionText: "prefs.info_density".localized,
descriptionText: "prefs.display_full_php_version_desc".localized,
checkboxText: "prefs.display_full_php_version".localized,
preference: .fullPhpVersionDynamicIcon,
action: {
MainMenu.shared.refreshIcon()
MainMenu.shared.update()
MainMenu.shared.rebuild()
}
),
CheckboxPreferenceView.make(
@ -74,14 +83,13 @@ class PrefsVC: NSViewController {
preference: .autoServiceRestartAfterExtensionToggle,
action: {}
),
/* DISABLED UNTIL VALET SWITCHING IS OK (see #34)
CheckboxPreferenceView.make(
sectionText: "",
descriptionText: "prefs.use_internal_switcher_desc".localized,
checkboxText: "prefs.use_internal_switcher".localized,
preference: .useInternalSwitcher,
sectionText: "prefs.switcher".localized,
descriptionText: "prefs.auto_composer_update_desc".localized,
checkboxText: "prefs.auto_composer_update_title".localized,
preference: .autoComposerGlobalUpdateAfterSwitch,
action: {}
), */
),
HotkeyPreferenceView.make(
sectionText: "prefs.global_shortcut".localized,
descriptionText: "prefs.shortcut_desc".localized,
@ -90,7 +98,7 @@ class PrefsVC: NSViewController {
].forEach({ self.stackView.addArrangedSubview($0) })
}
// MARK: - Listening for hotkey dleegate
// MARK: - Listening for hotkey delegate
var listeningForHotkeyView: HotkeyPreferenceView? = nil
@ -103,6 +111,6 @@ class PrefsVC: NSViewController {
// MARK: - Deinitialization
deinit {
print("VC deallocated")
Log.perf("PrefsVC deallocated")
}
}

View File

@ -21,12 +21,6 @@ class PrefsWC: PMWindowController {
return "Preferences"
}
// MARK: - Window Lifecycle
override func windowDidLoad() {
super.windowDidLoad()
}
// MARK: - Key Interaction
override func keyDown(with event: NSEvent) {
@ -35,7 +29,7 @@ class PrefsWC: PMWindowController {
if let vc = contentViewController as? PrefsVC {
if vc.listeningForHotkeyView != nil {
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
print("A blacklisted key was pressed, canceling listen")
Log.info("A blacklisted key was pressed, canceling listen!")
vc.listeningForHotkeyView = nil
} else {
vc.listeningForHotkeyView!.updateShortcut(event)

View File

@ -21,8 +21,7 @@ class CheckboxPreferenceView: NSView, XibLoadable {
var preference: PreferenceName! {
didSet {
let shouldDisplay = Preferences.preferences[self.preference] as! Bool == true
self.buttonCheckbox.state = shouldDisplay ? .on : .off
self.buttonCheckbox.state = Preferences.isEnabled(self.preference) ? .on : .off
}
}

View File

@ -63,7 +63,7 @@
<constraint firstItem="B8f-nb-Y0A" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="5" id="2Zu-h3-qb0"/>
<constraint firstItem="iUx-vA-jg4" firstAttribute="leading" secondItem="gBj-K1-Q2I" secondAttribute="trailing" constant="12" symbolic="YES" id="3fW-pY-HBu"/>
<constraint firstItem="gBj-K1-Q2I" firstAttribute="top" secondItem="B8f-nb-Y0A" secondAttribute="top" id="7JI-pU-DnQ"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="iUx-vA-jg4" secondAttribute="trailing" constant="112" id="AVQ-1M-kE4"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="iUx-vA-jg4" secondAttribute="trailing" constant="50" id="AVQ-1M-kE4"/>
<constraint firstItem="iUx-vA-jg4" firstAttribute="top" secondItem="gBj-K1-Q2I" secondAttribute="top" id="O2C-aI-XFS"/>
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="gBj-K1-Q2I" secondAttribute="bottom" constant="8" id="Sly-aj-yUl"/>
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Window Controller-->
<scene sceneID="pUZ-6w-gUX">
<objects>
<windowController storyboardIdentifier="progressWindow" id="LSr-Iw-X1T" customClass="ProgressWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="none" frameAutosaveName="" titlebarAppearsTransparent="YES" titleVisibility="hidden" id="PD9-0p-i0S" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES"/>
<windowPositionMask key="initialPositionMask" rightStrut="YES" topStrut="YES"/>
<rect key="contentRect" x="2080" y="1145" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="QOA-qf-m1l">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="LSr-Iw-X1T" id="uEN-dQ-Jv4"/>
</connections>
</window>
<connections>
<segue destination="c01-Tm-OtR" kind="relationship" relationship="window.shadowedContentViewController" id="uNS-tY-qB9"/>
</connections>
</windowController>
<customObject id="aGV-xt-u13" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-313" y="-6"/>
</scene>
<!--Progress View Controller-->
<scene sceneID="XgN-R6-44T">
<objects>
<viewController id="c01-Tm-OtR" customClass="ProgressViewController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="0fd-qq-0ME">
<rect key="frame" x="0.0" y="0.0" width="591" height="270"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" horizontalScrollElasticity="none" findBarPosition="aboveHorizontalRuler" translatesAutoresizingMaskIntoConstraints="NO" id="JK7-kL-1L3">
<rect key="frame" x="0.0" y="0.0" width="591" height="210"/>
<clipView key="contentView" drawsBackground="NO" id="2Mc-oy-AzN">
<rect key="frame" x="0.0" y="0.0" width="591" height="210"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" smartInsertDelete="YES" id="d1T-N1-CRe">
<rect key="frame" x="0.0" y="0.0" width="591" height="210"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<size key="minSize" width="591" height="210"/>
<size key="maxSize" width="757" height="10000000"/>
<attributedString key="textStorage">
<fragment content="$ ">
<attributes>
<color key="NSColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<font key="NSFont" size="10" name="Menlo-Regular"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0"/>
</attributes>
</fragment>
</attributedString>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</textView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="51v-CN-AuA">
<rect key="frame" x="-100" y="-100" width="225" height="15"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="lSO-JG-QOf">
<rect key="frame" x="-100" y="-100" width="15" height="173"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9c0-5U-sVK">
<rect key="frame" x="69" y="242" width="504" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="TITLE" id="oyI-D5-kEd">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7Be-eK-EcJ">
<rect key="frame" x="69" y="226" width="504" height="14"/>
<constraints>
<constraint firstAttribute="width" constant="500" id="0kb-4B-ZF3"/>
</constraints>
<textFieldCell key="cell" title="DESC" id="V0K-KF-leA">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="750" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="LiM-ZV-F8j">
<rect key="frame" x="20" y="224" width="36" height="36"/>
<constraints>
<constraint firstAttribute="height" constant="36" id="ddK-Ha-wqT"/>
<constraint firstAttribute="width" constant="36" id="pHp-9H-nhF"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSInfo" id="GWi-hE-LOJ"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="9c0-5U-sVK" firstAttribute="top" secondItem="LiM-ZV-F8j" secondAttribute="top" constant="2" id="0YJ-RT-kYE"/>
<constraint firstAttribute="bottom" secondItem="JK7-kL-1L3" secondAttribute="bottom" id="5Yv-2h-nea"/>
<constraint firstItem="9c0-5U-sVK" firstAttribute="leading" secondItem="LiM-ZV-F8j" secondAttribute="trailing" constant="15" id="EUV-hs-udj"/>
<constraint firstItem="LiM-ZV-F8j" firstAttribute="top" secondItem="0fd-qq-0ME" secondAttribute="top" constant="10" id="Ovb-Nu-ftR"/>
<constraint firstItem="JK7-kL-1L3" firstAttribute="top" secondItem="7Be-eK-EcJ" secondAttribute="bottom" constant="16" id="PYn-gt-Akk"/>
<constraint firstAttribute="trailing" secondItem="9c0-5U-sVK" secondAttribute="trailing" constant="20" symbolic="YES" id="TgO-ax-DGp"/>
<constraint firstItem="LiM-ZV-F8j" firstAttribute="leading" secondItem="0fd-qq-0ME" secondAttribute="leading" constant="20" symbolic="YES" id="ang-xM-Zmh"/>
<constraint firstAttribute="trailing" secondItem="JK7-kL-1L3" secondAttribute="trailing" id="atA-67-BQF"/>
<constraint firstItem="JK7-kL-1L3" firstAttribute="leading" secondItem="0fd-qq-0ME" secondAttribute="leading" id="gwR-eH-CmM"/>
<constraint firstItem="7Be-eK-EcJ" firstAttribute="top" secondItem="9c0-5U-sVK" secondAttribute="bottom" constant="2" id="jdR-1x-xta"/>
<constraint firstItem="7Be-eK-EcJ" firstAttribute="leading" secondItem="LiM-ZV-F8j" secondAttribute="trailing" constant="15" id="loj-L6-5NK"/>
<constraint firstAttribute="trailing" secondItem="7Be-eK-EcJ" secondAttribute="trailing" constant="20" symbolic="YES" id="sgd-u4-k0O"/>
</constraints>
</view>
<connections>
<outlet property="imageViewType" destination="LiM-ZV-F8j" id="yIU-aW-YFL"/>
<outlet property="labelDescription" destination="7Be-eK-EcJ" id="qui-Ub-y5U"/>
<outlet property="labelTitle" destination="9c0-5U-sVK" id="Iy3-Ym-pA9"/>
<outlet property="textView" destination="d1T-N1-CRe" id="pWp-W1-aus"/>
</connections>
</viewController>
<customObject id="oqT-7w-frK" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="266.5" y="18"/>
</scene>
</scenes>
<resources>
<image name="NSInfo" width="32" height="32"/>
</resources>
</document>

View File

@ -0,0 +1,76 @@
//
// ProgressView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 18/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class ProgressWindowController: NSWindowController, NSWindowDelegate {
static func display(title: String, description: String) -> ProgressWindowController {
let storyboard = NSStoryboard(name: "ProgressWindow" , bundle : nil)
let windowController = storyboard.instantiateController(
withIdentifier: "progressWindow"
) as! ProgressWindowController
windowController.showWindow(windowController)
windowController.window?.makeKeyAndOrderFront(nil)
windowController.positionWindowInTopLeftCorner()
windowController.progressView?.labelTitle.stringValue = title
windowController.progressView?.labelDescription.stringValue = description
NSApp.activate(ignoringOtherApps: true)
return windowController
}
var progressView: ProgressViewController? {
return self.contentViewController as? ProgressViewController
}
public func addToConsole(_ string: String) {
guard let textView = self.progressView?.textView else {
return
}
textView.string = textView.string + string
textView.scrollToEndOfDocument(nil)
}
public func setType(info: Bool = true) {
guard let imageView = self.progressView?.imageViewType else {
return
}
imageView.image = NSImage(named: info ? "NSInfo" : "NSCaution")
}
func windowWillClose(_ notification: Notification) {
self.contentViewController = nil
}
deinit {
Log.perf("Deinitializing ProgressWindowController")
}
}
class ProgressViewController: NSViewController {
@IBOutlet weak var labelTitle: NSTextField!
@IBOutlet weak var labelDescription: NSTextField!
@IBOutlet var textView: NSTextView!
@IBOutlet weak var imageViewType: NSImageView!
deinit {
Log.perf("Deinitializing ProgressViewController")
}
}

View File

@ -0,0 +1,137 @@
//
// AddSiteVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 24/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class AddSiteVC: NSViewController, NSTextFieldDelegate {
@IBOutlet weak var pathControl: NSPathControl!
@IBOutlet weak var linkName: NSTextField!
@IBOutlet weak var previewText: NSTextField!
@IBOutlet weak var buttonSecure: NSButton!
@IBOutlet weak var buttonCreateLink: NSButton!
@IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var textFieldSecure: NSTextField!
@IBOutlet weak var textFieldError: NSTextField!
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
loadStaticLocalisedStrings()
}
private func dismissView(outcome: NSApplication.ModalResponse) {
guard let window = self.view.window, let parent = window.sheetParent else { return }
parent.endSheet(window, returnCode: outcome)
}
// MARK: - Localisation
func loadStaticLocalisedStrings() {
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
}
// MARK: - Outlet Interactions
@IBAction func pressedCreateLink(_ sender: Any) {
let path = self.pathControl.url!.path
let name = self.linkName.stringValue
if !FileManager.default.fileExists(atPath: path) {
Alert.confirm(
onWindow: self.view.window!,
messageText: "site_list.alert.folder_missing.title".localized,
informativeText: "site_list.alert.folder_missing.desc".localized,
buttonTitle: "site_list.alert.folder_missing.cancel".localized,
secondButtonTitle: "site_list.alert.folder_missing.return".localized,
onFirstButtonPressed: {
self.dismissView(outcome: .cancel)
}
)
return
}
Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)'", requiresPath: true)
self.dismissView(outcome: .OK)
// Reset search
App.shared.siteListWindowController?
.searchToolbarItem
.searchField.stringValue = ""
// Add the new item and scrolls to it
App.shared.siteListWindowController?
.contentVC
.addedNewSite(
name: name,
secure: buttonSecure.state == .on
)
}
@IBAction func pressedCancel(_ sender: Any) {
self.dismissView(outcome: .cancel)
}
@IBAction func pressedSecure(_ sender: Any) {
updatePreview()
}
// MARK: - Text Field Delegate
func controlTextDidChange(_ obj: Notification) {
updateTextField()
}
// MARK: - Helper Methods
private func isValidLinkName(_ name: String) -> Bool {
if self.linkName.stringValue.isEmpty {
self.textFieldError.isHidden = false
self.textFieldError.stringValue = "site_list.add.errors.empty".localized
return false
}
if Valet.shared.sites.contains(where: { $0.name == name }) {
self.textFieldError.isHidden = false
self.textFieldError.stringValue = "site_list.add.errors.already_exists".localized
return false
}
self.textFieldError.isHidden = true
return true
}
func updateTextField() {
self.linkName.stringValue = self.linkName.stringValue
.replacingOccurrences(of: " ", with: "-")
buttonCreateLink.isEnabled = isValidLinkName(self.linkName.stringValue)
self.updatePreview()
}
func updatePreview() {
buttonSecure.title = "site_list.add.secure_after_creation"
.localized(
self.linkName.stringValue,
Valet.shared.config.tld
)
previewText.stringValue = "site_list.add.folder_available"
.localized(
self.buttonSecure.state == .on ? "https" : "http",
self.linkName.stringValue,
Valet.shared.config.tld
)
}
}

View File

@ -11,6 +11,8 @@ import AppKit
class SiteListCell: NSTableCellView
{
var site: Valet.Site? = nil
@IBOutlet weak var labelSiteName: NSTextField!
@IBOutlet weak var labelPathName: NSTextField!
@ -19,11 +21,16 @@ class SiteListCell: NSTableCellView
@IBOutlet weak var labelDriver: NSTextField!
@IBOutlet weak var buttonPhpVersion: NSButton!
@IBOutlet weak var imageViewPhpVersionOK: NSImageView!
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
func populateCell(with site: Valet.Site) {
self.site = site
// Make sure to show the TLD
labelSiteName.stringValue = "\(site.name!).\(Valet.shared.config.tld)"
@ -46,6 +53,63 @@ class SiteListCell: NSTableCellView
: NSColor.init(red: 246/255, green: 71/255, blue: 71/255, alpha: 1.0) // red
// Show the current driver
labelDriver.stringValue = site.driver ?? "???"
labelDriver.stringValue = "\(site.driver ?? "???")"
// Determine the Laravel version
if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") {
let constraint = site.notableComposerDependencies["laravel/framework"]!
labelDriver.stringValue = "Laravel (\(constraint))"
}
// Show the PHP version
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)
}
@IBAction func pressedPhpVersion(_ sender: Any) {
guard let site = self.site else { return }
let alert = NSAlert.init()
alert.alertStyle = .informational
alert.messageText = "alert.composer_php_requirement.title"
.localized("\(site.name!).\(Valet.shared.config.tld)", site.composerPhp)
alert.informativeText = "alert.composer_php_requirement.info"
.localized(site.composerPhpSource)
alert.addButton(withTitle: "site_link.close".localized)
var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue
var map: [Int: String] = [:]
// Determine which installed versions would be ideal to switch to,
// but make sure to exclude the currently linked version
PhpEnv.shared.validVersions(for: site.composerPhp).filter({ version in
version.homebrewVersion != PhpEnv.phpInstall.version.short
}).forEach { version in
alert.addButton(withTitle: "site_link.switch_to_php".localized(version.homebrewVersion))
map[mapIndex] = version.homebrewVersion
mapIndex += 1
}
alert.beginSheetModal(for: App.shared.siteListWindowController!.window!) { response in
if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue {
if map.keys.contains(response.rawValue) {
let version = map[response.rawValue]!
print("Pressed button to switch to \(version)")
MainMenu.shared.switchToPhpVersion(version)
}
}
}
}
}

View File

@ -0,0 +1,97 @@
//
// SiteListVC+Actions.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
extension SiteListVC {
@objc func toggleSecure() {
let rowToReload = tableView.selectedRow
let originalSecureStatus = selectedSite!.secured
let action = selectedSite!.secured ? "unsecure" : "secure"
let selectedSite = selectedSite!
let command = "cd '\(selectedSite.absolutePath!)' && sudo \(Paths.valet) \(action) && exit;"
waitAndExecute {
Shell.run(command, requiresPath: true)
} completion: { [self] in
selectedSite.determineSecured(Valet.shared.config.tld)
if selectedSite.secured == originalSecureStatus {
Alert.notify(
message: "site_list.alerts_status_not_changed.title".localized,
info: "site_list.alerts_status_not_changed.desc".localized(command)
)
} else {
let newState = selectedSite.secured ? "secured" : "unsecured"
LocalNotification.send(
title: "site_list.alerts_status_changed.title".localized,
subtitle: "site_list.alerts_status_changed.desc"
.localized(
"\(selectedSite.name!).\(Valet.shared.config.tld)",
newState
)
)
}
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0])
tableView.deselectRow(rowToReload)
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
}
}
@objc func openInBrowser() {
let prefix = selectedSite!.secured ? "https://" : "http://"
let url = URL(string: "\(prefix)\(selectedSite!.name!).\(Valet.shared.config.tld)")
if url != nil {
NSWorkspace.shared.open(url!)
} else {
_ = Alert.present(
messageText: "site_list.alert.invalid_folder_name".localized,
informativeText: "site_list.alert.invalid_folder_name_desc".localized
)
}
}
@objc func openInFinder() {
Shell.run("open '\(selectedSite!.absolutePath!)'")
}
@objc func openInTerminal() {
Shell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath!)'")
}
@objc func openWithEditor(sender: EditorMenuItem) {
guard let editor = sender.editor else { return }
editor.openDirectory(file: selectedSite!.absolutePath!)
}
@objc func unlinkSite() {
guard let site = selectedSite else {
return
}
if site.aliasPath == nil {
return
}
Alert.confirm(
onWindow: view.window!,
messageText: "site_list.confirm_unlink".localized(site.name),
informativeText: "site_link.confirm_link".localized,
buttonTitle: "site_list.unlink".localized,
secondButtonTitle: "Cancel",
style: .critical,
onFirstButtonPressed: {
Shell.run("valet unlink '\(site.name!)'", requiresPath: true)
self.reloadSites()
}
)
}
}

View File

@ -56,7 +56,7 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
]
windowController.window!.minSize = NSSize(width: 550, height: 200)
windowController.window!.delegate = windowController
windowController.positionWindowInTopLeftCorner()
windowController.window!.setFrameAutosaveName("siteListWindow")
App.shared.siteListWindowController = windowController
}
@ -112,12 +112,11 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
- Parameter execute: Callback of the work that needs to happen.
- Parameter completion: Callback that is fired when the work is done.
*/
private func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
internal func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
{
setUIBusy()
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
execute()
DispatchQueue.main.async { [self] in
completion()
setUINotBusy()
@ -136,6 +135,28 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
}
}
func addedNewSite(name: String, secure: Bool) {
waitAndExecute {
Valet.shared.reloadSites()
} completion: { [self] in
find(name, secure)
}
}
private func find(_ name: String, _ secure: Bool = false) {
sites = Valet.shared.sites
searchedFor(text: "")
if let site = sites.enumerated().first(where: { $0.element.name == name }) {
DispatchQueue.main.async {
self.tableView.selectRowIndexes([site.offset], byExtendingSelection: false)
self.tableView.scrollRowToVisible(site.offset)
if (secure && !site.element.secured) {
self.toggleSecure()
}
}
}
}
// MARK: - Table View Delegate
func numberOfRows(in tableView: NSTableView) -> Int {
@ -164,81 +185,6 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
self.openInBrowser()
}
// MARK: Secure & Unsecure
@objc public func toggleSecure() {
let rowToReload = tableView.selectedRow
let originalSecureStatus = selectedSite!.secured
let action = selectedSite!.secured ? "unsecure" : "secure"
let selectedSite = selectedSite!
let command = "cd \(selectedSite.absolutePath!) && sudo \(Paths.valet) \(action) && exit;"
waitAndExecute {
Shell.run(command, requiresPath: true)
} completion: { [self] in
selectedSite.determineSecured(Valet.shared.config.tld)
if selectedSite.secured == originalSecureStatus {
Alert.notify(
message: "site_list.alerts_status_not_changed.title".localized,
info: "site_list.alerts_status_not_changed.desc".localized(command)
)
} else {
let newState = selectedSite.secured ? "secured" : "unsecured"
LocalNotification.send(
title: "site_list.alerts_status_changed.title".localized,
subtitle: "site_list.alerts_status_changed.desc"
.localized(
"\(selectedSite.name!).\(Valet.shared.config.tld)",
newState
)
)
}
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0])
tableView.deselectRow(rowToReload)
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
}
}
// MARK: Open in Browser & Finder
@objc public func openInBrowser() {
let prefix = selectedSite!.secured ? "https://" : "http://"
let url = "\(prefix)\(selectedSite!.name!).\(Valet.shared.config.tld)"
NSWorkspace.shared.open(URL(string: url)!)
}
@objc public func openInFinder() {
Shell.run("open \(selectedSite!.absolutePath!)")
}
@objc public func openInTerminal() {
Shell.run("open -b com.apple.terminal \(selectedSite!.absolutePath!)")
}
@objc public func unlinkSite() {
guard let site = selectedSite else {
return
}
if site.aliasPath == nil {
return
}
Alert.confirm(
onWindow: view.window!,
messageText: "site_list.confirm_unlink".localized(site.name),
informativeText: "site_link.confirm_link".localized,
buttonTitle: "site_list.unlink".localized,
secondButtonTitle: "Cancel",
style: .critical,
onFirstButtonPressed: {
Shell.run("valet unlink \(site.name!)", requiresPath: true)
self.reloadSites()
}
)
}
// MARK: - (Search) Text Field Delegate
func searchedFor(text: String) {
@ -248,7 +194,10 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
if searchString.isEmpty {
sites = Valet.shared.sites
tableView.reloadData()
DispatchQueue.main.async {
self.tableView.reloadData()
}
return
}
@ -256,18 +205,14 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
return site.name.lowercased().contains(searchString)
})
tableView.reloadData()
}
// MARK: - Context Menu
@objc func openWithEditor(sender: EditorMenuItem) {
guard let editor = sender.editor else { return }
editor.openDirectory(file: selectedSite!.absolutePath!)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
// MARK: - Deinitialization
deinit {
print("VC deallocated")
Log.perf("SiteListVC deallocated")
}
}

View File

@ -34,17 +34,61 @@ class SiteListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
return self.contentViewController as! SiteListVC
}
var searchTimer: Timer?
func controlTextDidChange(_ notification: Notification) {
guard let searchField = notification.object as? NSSearchField else {
return
}
contentVC.searchedFor(text: searchField.stringValue)
self.searchTimer?.invalidate()
searchTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false, block: { _ in
self.contentVC.searchedFor(text: searchField.stringValue)
})
}
// MARK: - Reload functionality
@IBAction func pressedReload(_ sender: Any) {
@IBAction func pressedReload(_ sender: Any?) {
contentVC.reloadSites()
}
@IBAction func pressedAddLink(_ sender: Any?) {
selectFolder()
}
// MARK: - Add a new site
func selectFolder() {
let dialog = NSOpenPanel()
dialog.message = "site_list.add.modal_description".localized
dialog.showsResizeIndicator = true
dialog.showsHiddenFiles = false
dialog.allowsMultipleSelection = false
dialog.canChooseDirectories = true
dialog.canChooseFiles = false
dialog.beginSheetModal(for: self.window!) { response in
let result = dialog.url
if (result != nil && response == .OK) {
let path: String = result!.path
self.showSitePopup(path)
}
}
}
func showSitePopup(_ folder: String) {
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
let windowController = storyboard.instantiateController(
withIdentifier: "addSiteWindow"
) as! NSWindowController
let viewController = windowController.window!.contentViewController as! AddSiteVC
viewController.pathControl.url = URL(fileURLWithPath: folder)
viewController.linkName.stringValue = String(folder.split(separator: "/").last!)
viewController.updateTextField()
self.window?.beginSheet(windowController.window!)
}
}

View File

@ -0,0 +1,29 @@
//
// 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 PMHeaderView: View {
@State var content: String = "Your Title Here"
var body: some View {
PMHeader(labelText: $content).frame(minWidth: 0, maxWidth: 450, minHeight: 0, maxHeight: 50)
}
}
@available(OSX 11.0, *)
struct PMHeader: NSViewRepresentable {
@Binding var labelText: String
func makeNSView(context: Context) -> some NSView {
return HeaderView.asMenuItem(text: labelText).view!
}
func updateNSView(_ nsView: NSViewType, context: Context) {}
}

View File

@ -0,0 +1,29 @@
//
// PMStatsView.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 PMStats: NSViewRepresentable {
@Binding var labelText: String
func makeNSView(context: Context) -> some NSView {
return StatsView.asMenuItem(memory: labelText, post: labelText, upload: labelText).view!
}
func updateNSView(_ nsView: NSViewType, context: Context) {}
}
@available(OSX 11.0, *)
struct PMStatsView: View {
@State var content: String = "5 MB"
var body: some View {
PMStats(labelText: $content).frame(minWidth: 0, maxWidth: 450, minHeight: 0, maxHeight: 80)
}
}

View File

@ -0,0 +1,19 @@
//
// Preview.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import SwiftUI
import Cocoa
@available(OSX 11.0, *)
struct Preview_Previews: PreviewProvider {
static var previews: some View {
PMHeaderView(content: "You are running PHP 8.1")
PMStatsView(content: "15 MB")
PMStatsView(content: "2 GB")
}
}

View File

@ -1,74 +0,0 @@
//
// 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
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 "/Users/\(whoami)/.composer/vendor/bin/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 String(Shell.pipe("whoami").split(separator: "\n")[0])
}
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

@ -0,0 +1,47 @@
//
// App+ConfigWatch.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
extension App {
func startWatcher(_ url: URL) {
Log.info("No watcher currently active...")
self.watcher = PhpConfigWatcher(for: url)
self.watcher.didChange = { url in
Log.info("Something has changed in: \(url)")
// Check if the watcher has last updated the menu less than 0.75s ago
let distance = self.watcher.lastUpdate?.distance(to: Date().timeIntervalSince1970)
if distance == nil || distance != nil && distance! > 0.75 {
Log.info("Refreshing menu...")
MainMenu.shared.reloadPhpMonitorMenuInBackground()
self.watcher.lastUpdate = Date().timeIntervalSince1970
}
}
}
func handlePhpConfigWatcher(forceReload: Bool = false) {
let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(PhpEnv.phpInstall.version.short)")
// Watcher needs to be created
if self.watcher == nil {
startWatcher(url)
}
// Watcher needs to be updated
if self.watcher.url != url || forceReload {
self.watcher.disable()
self.watcher = nil
Log.info("Watcher has stopped watching files. Starting new one...")
startWatcher(url)
}
}
}

View File

@ -0,0 +1,130 @@
//
// FolderWatcher.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpConfigWatcher {
let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent)
let url: URL
var didChange: ((URL) -> Void)?
var lastUpdate: TimeInterval? = nil
var watchers: [FSWatcher] = []
init(for url: URL) {
self.url = url
// Add a watcher for php.ini
self.addWatcher(for: self.url.appendingPathComponent("php.ini"), eventMask: .write)
// Add a watcher for conf.d (in case a new file is added or a file is deleted)
// This watcher, when triggered, will restart all watchers
self.addWatcher(for: self.url.appendingPathComponent("conf.d"), eventMask: .all, behaviour: .reloadsWatchers)
// Scan the conf.d folder for .ini files, and add a watcher for each file
let enumerator = FileManager.default.enumerator(atPath: self.url.appendingPathComponent("conf.d").path)
let filePaths = enumerator?.allObjects as! [String]
// Loop over the .ini files that we discovered
filePaths.filter { $0.contains(".ini") }.forEach { (file) in
// Add a watcher for each file we have discovered
self.addWatcher(for: self.url.appendingPathComponent("conf.d/\(file)"), eventMask: .write)
}
Log.info("A watcher exists for the following config paths:")
Log.info(self.watchers.map({ watcher in
return watcher.url.relativePath
}))
}
func addWatcher(for url: URL, eventMask: DispatchSource.FileSystemEvent, behaviour: FSWatcherBehaviour = .reloadsMenu) {
let watcher = FSWatcher(for: url, eventMask: eventMask, parent: self, behaviour: behaviour)
self.watchers.append(watcher)
}
func disable() {
Log.info("Turning off existing watchers...")
self.watchers.forEach { (watcher) in
watcher.stopMonitoring()
}
}
deinit {
Log.perf("A PhpConfigWatcher has been deinitialized.")
}
}
enum FSWatcherBehaviour {
case reloadsMenu
case reloadsWatchers
}
class FSWatcher {
private var parent: PhpConfigWatcher!
private var monitoredFolderFileDescriptor: CInt = -1
private var folderMonitorSource: DispatchSourceFileSystemObject?
let url: URL
init(for url: URL, eventMask: DispatchSource.FileSystemEvent, parent: PhpConfigWatcher, behaviour: FSWatcherBehaviour = .reloadsMenu) {
self.url = url
self.parent = parent
self.startMonitoring(eventMask, behaviour: behaviour)
}
func startMonitoring(_ eventMask: DispatchSource.FileSystemEvent, behaviour: FSWatcherBehaviour) {
guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else {
return
}
// Open the file or folder referenced by URL for monitoring only.
monitoredFolderFileDescriptor = open(url.path, O_EVTONLY)
folderMonitorSource = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: monitoredFolderFileDescriptor,
eventMask: eventMask,
queue: parent.folderMonitorQueue
)
// Define the block to call when a file change is detected.
folderMonitorSource?.setEventHandler { [weak self] in
// The default behaviour is to reload the menu
switch behaviour {
case .reloadsMenu:
// Default behaviour: reload the menu items
self?.parent.didChange?(self!.url)
case .reloadsWatchers:
// Alternative behaviour: reload all watchers
App.shared.handlePhpConfigWatcher(forceReload: true)
}
}
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
folderMonitorSource?.setCancelHandler { [weak self] in
guard let self = self else { return }
close(self.monitoredFolderFileDescriptor)
self.monitoredFolderFileDescriptor = -1
self.folderMonitorSource = nil
}
// Start monitoring the directory via the source.
folderMonitorSource?.resume()
}
func stopMonitoring() {
folderMonitorSource?.cancel()
self.parent = nil
}
}

View File

@ -18,6 +18,19 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>com.nicoverbruggen.phpmon</string>
<key>CFBundleURLSchemes</key>
<array>
<string>phpmon</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSApplicationCategoryType</key>

View File

@ -19,31 +19,37 @@
"mi_diagnostics" = "Diagnostics";
"mi_active_services" = "Active Services";
"mi_restart_php_fpm" = "Restart service: php";
"mi_restart_nginx" = "Restart service: nginx";
"mi_restart_dnsmasq" = "Restart service: dnsmasq";
"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_php_refresh" = "Refresh information";
"mi_restart_php_fpm" = "Restart Service: php";
"mi_restart_nginx" = "Restart Service: nginx";
"mi_restart_dnsmasq" = "Restart Service: dnsmasq";
"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_php_refresh" = "Refresh Information";
"mi_configuration" = "Configuration";
"mi_configuration" = "PHP Configuration";
"mi_limits" = "Limits Configuration";
"mi_memory_limit" = "Memory Limit";
"mi_post_max_size" = "Max POST";
"mi_upload_max_filesize" = "Max Upload";
"mi_manual_actions" = "Manual Actions";
"mi_services" = "Services";
"mi_help" = "First Aid";
"mi_other" = "First Aid & Services";
"mi_composer" = "Composer";
"mi_valet_config" = "Locate Valet folder (.config/valet)";
"mi_php_config" = "Locate PHP configuration file (php.ini)";
"mi_global_composer" = "Locate global composer.json file (.composer)";
"mi_phpinfo" = "Show current configuration (phpinfo)";
"mi_php_config" = "Locate PHP Configuration File (php.ini)";
"mi_global_composer" = "Locate Global Composer File (.composer)";
"mi_phpinfo" = "Show Current Configuration (phpinfo)";
"mi_update_global_composer" = "Update Global Composer Dependencies...";
"mi_detected_extensions" = "Detected Extensions";
"mi_no_extensions_detected" = "No additional extensions detected.";
"mi_valet" = "Laravel Valet";
"mi_sitelist" = "View linked & parked domains...";
"mi_sitelist" = "View Linked and Parked Domains...";
"mi_preferences" = "Preferences...";
"mi_quit" = "Quit PHP Monitor";
@ -61,7 +67,33 @@
"site_list.alerts_status_changed.desc" = "The domain '%@' is now %@.";
"site_list.confirm_unlink" = "Are you sure you want to unlink '%@'?";
"site_link.confirm_link" = "No files will be removed. If needed, the site will need to be relinked via the command line.";
"site_link.confirm_link" = "No files will be removed. You can always link the folder again by clicking on the + button and selecting the original folder.";
"site_link.close" = "Close";
"site_link.switch_to_php" = "Switch to PHP %@";
// ADD SITE TO SITE LIST
"site_list.add.link_folder" = "Link a Folder";
"site_list.add.domain_name_placeholder" = "Enter a domain name here";
"site_list.add.secure_after_creation" = "Secure %@.%@ after creation";
"site_list.add.secure_description" = "Securing a site requires administrative privileges.\nYou will be prompted for your password or Touch ID.";
"site_list.add.create_link" = "Create Link";
"site_list.add.cancel" = "Cancel";
"site_list.add.folder_available" = "This site will be available via the following URL: %@://%@.%@";
"site_list.add.errors.empty" = "You must enter a name.";
"site_list.add.errors.already_exists" = "A link with that name already exists.";
// ADD SITE ERROR: FOLDER MISSING SINCE SELECTION
"site_list.alert.folder_missing.desc" = "The folder you chose no longer seems to exist. Do you want to cancel adding this folder? If you moved the folder, you could always put it back and try again.";
"site_list.alert.folder_missing.title" = "Folder missing!";
"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
@ -74,6 +106,11 @@
"site_list.detected_apps" = "Detected Applications";
"site_list.system_apps" = "System Applications";
"site_list.warning.spaces" = "Warning! This site has a space in its folder.\nThe site will not be reachable via the browser.";
"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.";
// EDITORS
"editors.alert.try_again" = "Try Again";
@ -86,31 +123,30 @@
"prefs.close" = "Close";
"prefs.global_shortcut" = "Global shortcut:";
"prefs.dynamic_icon" = "Dynamic icon:";
"prefs.dynamic_icon" = "Icon customization:";
"prefs.info_density" = "Info density:";
"prefs.services" = "Services:";
"prefs.switcher" = "Switcher:";
"prefs.auto_restart_services_title" = "Auto-restart PHP-FPM";
"prefs.auto_restart_services_desc" = "When checked, will automatically restart PHP-FPM when\nyou check or uncheck an extension. Slightly slower when enabled, \nbut this applies the extension change immediately for all sites \nyou're serving, no need to restart PHP-FPM manually.";
"prefs.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.";
"prefs.dynamic_icon_title" = "Display dynamic icon in menu bar";
"prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible.\nIf checked, it will display the major version number of the\ncurrently linked PHP version.";
"prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible. If checked, it will display the major version number of the currently linked PHP version.";
"prefs.display_full_php_version" = "Display full PHP version in menu bar";
"prefs.display_full_php_version_desc" = "Display the full version instead of the major version only.\n(This may be undesirable on smaller displays,\nso this is disabled by default.)";
"prefs.icon_hint_title" = "Display PHP hint next to version number";
"prefs.icon_hint_desc" = "If you uncheck this box, the icon in the menu bar will only show the version number and not the PHP hint. This preference does not do anything if the dynamic icon has been disabled.";
"prefs.use_internal_switcher" = "Use PHP Monitors own version switcher";
"prefs.use_internal_switcher_desc" =
"By default, PHP Monitor will attempt to use Laravel Valet
in order to switch PHP versions. If you prefer a different
switcher or are having issues with `valet use php`, you may
use PHP Monitor's own switcher which is slightly faster,
but might cause issues with permissions in your Homebrew
directories, since PHP Monitor controls the services.";
"prefs.display_full_php_version" = "Display full PHP version everywhere";
"prefs.display_full_php_version_desc" = "Display the full version instead of the major version displayed in the menu bar and the dropdown menu. (This may be undesirable on smaller displays, so this is disabled by default.)";
"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.shortcut_set" = "Set global shortcut";
"prefs.shortcut_listening" = "<listening for keypress>";
"prefs.shortcut_clear" = "Clear";
"prefs.shortcut_desc" = "If a shortcut combination is set up, you can toggle PHP Monitor\nwherever you are by pressing the key combination you chose.\n(Cancel choosing a shortcut by pressing the spacebar.)";
"prefs.shortcut_desc" = "If a shortcut combination is set up, you can toggle PHP Monitor wherever you are by pressing the key combination you chose. (Cancel choosing a shortcut by pressing the spacebar.)";
// NOTIFICATIONS
@ -128,6 +164,22 @@ directories, since PHP Monitor controls the services.";
// ALERTS
// Composer Update
"alert.composer_progress.title" = "Updating global dependencies...";
"alert.composer_progress.info" = "You can see the progress in the terminal output below.";
"alert.composer_failure.title" = "Something went wrong!";
"alert.composer_failure.info" = "Your global Composer dependencies could not be updated.
You can find more information in the terminal output below. Youll have to fix this
problem manually, using your own Terminal app (this just shows you the output).";
"alert.composer_success.title" = "Composers done updating!";
"alert.composer_success.info" = "Your global Composer dependencies have been successfully updated.";
// Composer Version
"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.";
@ -177,7 +229,7 @@ You can do this by running `composer global update` in your terminal. After that
/// 5. Valet & sudoers
"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.";
"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
"startup.errors.services.title" = "Multiple PHP services are active";