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

Compare commits

...

149 Commits

Author SHA1 Message Date
2e58bbb5e8 🚀 Version 5.0.1
🔀 Merge branch 'dev/5.x'
2022-02-08 23:40:10 +01:00
bb6abc5e9a 👌 Fix print statements 2022-02-08 23:37:17 +01:00
e4f9d491c3 🔥 Cleanup .afdesign file 2022-02-08 23:29:55 +01:00
1ba7ee37db 👌 Ensure target ownership is OK 2022-02-08 23:20:48 +01:00
34cfbae3a9 👌 Fix inconsistent capitalisation 2022-02-08 23:12:55 +01:00
9fabda545f ♻️ Big Cleanup 2022-02-08 23:07:23 +01:00
7cd50aed7b 🐛 Fix retain cycle due to threading issue 2022-02-08 22:19:32 +01:00
cdc082071d ♻️ Menu item method reordering 2022-02-08 21:52:17 +01:00
d1479672c8 ♻️ Rework site list loading UI
* 0.2 second delay for smooth UI (minimum)
* > 0.5 second delay for operation = show spinner
2022-02-08 21:51:59 +01:00
ffb112cfb2 🚛 Reorganise Errors structs 2022-02-08 21:26:06 +01:00
f5af33c098 🐛 Correctly parse RC PHP version (#132)
It'll be a while before a new release candidate is available, but this
bug has now been resolved.

A new `PhpVersionNumber.parse` method has been added which can throw.

The `VersionExtractor` class is now capable of extracting version
numbers from all strings now too, and isn't just used to determine
the Valet version number.

New tests have been added to handle these scenarios.

This commit also removes the phpmon-cli component, which wasn't being
updated or maintained (it was an experiment).
2022-02-08 18:51:16 +01:00
0f464f5814 🔥 Cleanup 2022-02-07 18:00:35 +01:00
776c2095e6 🚛 Move files around 2022-02-07 17:38:44 +01:00
82a2833161 👌 Improve separators (#128) 2022-02-07 17:19:57 +01:00
b14d7bdf07 Add multiple options for icons (#106)
- PHP next to icon (default)
- PHP Elephant
- No icon

If you chose "no PHP text next to icon" earlier, that preference is
remembered and migrated over.
2022-02-07 00:46:00 +01:00
ffa22eea25 👌 Cleanup, ensure all tests pass 2022-02-06 17:56:16 +01:00
8220e6409d 🚛 Move async helpers to MainMenu+Async 2022-02-06 13:18:12 +01:00
927e1c02fa ♻️ Refactor waitAndExecuteasyncExecution
This change allows for errors to be thrown during the `asyncExecution`
initial callback, and if one is thrown, allows a `failure` callback to
be used. Existing instances of `waitAndExecute` have been replaced.

I also added the ability to tweak the behaviours of the actions that are
always performed when the asyncExecution method is called: you can now
specify limited behaviours (e.g. only set busy icon). For that use case
I have already created a new method: `asyncWithBusyUI`.

With this change, the handling of the Homebrew Permissions flow has also
been modified: when the user does not get administrative permissions
an error is now thrown which results in an alert being presented to
the user once the error occurs.

There is now an opportunity to further refactor other parts of the app
to more gracefully handle failure states using the Error and
AlertableError protocols.
2022-02-06 13:15:03 +01:00
a13a17b106 👌 Various improvements
- Properly draw the menu bar icon
  (now with extra documentation)
- Resolve Paths via FileManager.default
- Updated DEV readme
- Add PHP Elephant icon (TBD)
2022-02-05 00:42:10 +01:00
18d6d73f94 🐛 Fix for #124 ('valet --version' failing) 2022-02-04 20:17:10 +01:00
54d101acbe 🍱 Updated icons file (Affinity) 2022-02-04 18:29:52 +01:00
76f720cb71 📝 Updated README 2022-02-04 18:24:56 +01:00
033e485ce1 🔥 Cleanup icons 2022-02-04 18:20:56 +01:00
82ee830de1 🔧 New dev build for bugfixing purposes 2022-02-04 18:20:24 +01:00
55cd1cccee 🐛 Do not watch files that do not exist (#123) 2022-02-04 18:10:30 +01:00
5f13ba3d1b 📝 Add section on symbolication 2022-02-04 17:54:30 +01:00
298aa0aa8d 🔧 Updated project 2022-02-04 17:30:41 +01:00
5d49be6f7e 🍱 Updated assets (#120) 2022-02-04 17:30:22 +01:00
ab81bf5875 📝 Updated FUNDING.yml 2022-02-04 15:35:53 +01:00
5d8765683a 🔧 Use beta icon for 5.1 betas 2022-02-03 20:38:02 +01:00
6c22bc0145 🏗 Fix Homebrew Permissions (#86) 2022-02-03 20:34:57 +01:00
5c1908668f ✏️ Adjust path in composer symlink recommendation (#115) 2022-02-03 17:07:22 +01:00
94da7fb255 🚀 Version 5.0
This release brings a plethora of new features to PHP Monitor.
Please check out the release notes for more details.

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

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

The thresholds for the alert are now:

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

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

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

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

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

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

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

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

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

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

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

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

3
.github/FUNDING.yml vendored
View File

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

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)

46
DEVELOPER.md Normal file
View File

@ -0,0 +1,46 @@
# DEVELOPER README
## 🔧 Build instructions
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
If you'd like to build PHP Monitor yourself, you need:
* Xcode (usually the latest version)
* The contents of this repository
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to immediately build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
## 🐛 Symbolication of crashes
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.
For example, given the following crash (from an .ips file):
```
Thread 2 Crashed:: Dispatch queue: com.apple.root.user-initiated-qos
0 libswiftDispatch.dylib 0x7ff82aa3ab8c static OS_dispatch_source.makeProcessSource(identifier:eventMask:queue:) + 28
1 PHP Monitor 0x1096907d8 0x10965e000 + 206808
| |
address load address
2 PHP Monitor 0x1096903ac 0x10965e000 + 205740
3 PHP Monitor 0x10968f88b 0x10965e000 + 202891
```
You must use the correct order for the the address and load address in the command below:
```
$ atos -arch x86_64 -o '/path/to/PHP Monitor.app.dSYM/Contents/Resources/DWARF/PHP Monitor' -l 0x10965e000 0x1096907d8
| | | |
architecture path to DSYM load address address
```
This will return the relevant information, for example:
```
FSWatcher.startMonitoring(_:behaviour:) (in PHP Monitor) (PhpConfigWatcher.swift:95)
```
For more information, see [Apple's documentation](https://developer.apple.com/documentation/xcode/adding-identifiable-symbol-names-to-a-crash-report).

View File

@ -9,13 +9,11 @@
/* Begin PBXBuildFile section */
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
54AB03262763858F00A29D5F /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AB03252763858F00A29D5F /* Timer.swift */; };
54AB03272763858F00A29D5F /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AB03252763858F00A29D5F /* Timer.swift */; };
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
54B48B60275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
54FCFD26276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */; };
54FCFD27276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */; };
54FCFD26276C883F004CE748 /* SelectPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */; };
54FCFD27276C883F004CE748 /* SelectPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */; };
54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */; };
54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */; };
54FCFD2D276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */; };
@ -24,22 +22,43 @@
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 */; };
C4068CA427B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */; };
C4068CA527B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */; };
C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */; };
C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */; };
C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */; };
C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */; };
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
C40B24F527A3108B0018C7D2 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; };
C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
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 */; };
C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; };
C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; };
C415D3B72770F294005EF286 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; };
C415D3B82770F294005EF286 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; };
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 */; };
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 */; };
@ -48,6 +67,14 @@
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 */; };
C44CCD4027AFE2FC00CE40E5 /* AlertableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */; };
C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */; };
C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; };
C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; };
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 */; };
@ -68,10 +95,17 @@
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 */; };
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */; };
C4927F0B27B2DFC200C55AFD /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4927F0A27B2DFC200C55AFD /* Errors.swift */; };
C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4927F0A27B2DFC200C55AFD /* Errors.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 */; };
C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49E171E27A5736E00787921 /* PMServicesView.swift */; };
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4AF9F71275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
@ -82,28 +116,65 @@
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 */; };
C4B585412770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; };
C4B585422770FE3900DA4FBE /* 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 */; };
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 */; };
C4CE3BB827B31F2E0086CA49 /* MainMenu+Switcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */; };
C4CE3BBA27B31F670086CA49 /* MainMenu+Composer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* MainMenu+Composer.swift */; };
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */; };
C4CE3BBC27B324250086CA49 /* MainMenu+Composer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* MainMenu+Composer.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 */; };
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; };
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C4EC1E6D279DF87A0010F296 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; };
C4EC1E73279DFCF40010F296 /* 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 */; };
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A827708B9E001DF387 /* PMStatsView.swift */; };
C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A827708B9E001DF387 /* PMStatsView.swift */; };
C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EED88827A48778006D7272 /* InterAppHandler.swift */; };
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EED88827A48778006D7272 /* InterAppHandler.swift */; };
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
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 */; };
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 */; };
C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.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 */; };
@ -112,7 +183,6 @@
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
@ -137,15 +207,25 @@
/* 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>"; };
54AB03252763858F00A29D5F /* Timer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timer.swift; sourceTree = "<group>"; };
54B48B5E275F66AE006D90C5 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CheckboxPreferenceView.xib; sourceTree = "<group>"; };
54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SelectPreferenceView.xib; sourceTree = "<group>"; };
54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxPreferenceView.swift; sourceTree = "<group>"; };
54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HotkeyPreferenceView.xib; sourceTree = "<group>"; };
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>"; };
C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CheckboxPreferenceView.xib; sourceTree = "<group>"; };
C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectPreferenceView.swift; sourceTree = "<group>"; };
C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarIcons.swift; 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>"; };
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFrameworks.swift; sourceTree = "<group>"; };
C415D3B62770F294005EF286 /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+InterApp.swift"; sourceTree = "<group>"; };
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPER.md; 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>"; };
@ -153,20 +233,21 @@
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>"; };
C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableError.swift; sourceTree = "<group>"; };
C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Async.swift"; 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>"; };
@ -182,8 +263,12 @@
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>"; };
C4927F0A27B2DFC200C55AFD /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.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>"; };
C49E171E27A5736E00787921 /* PMServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMServicesView.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
C4AF9F70275445FF00D44ED0 /* valet-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "valet-config.json"; sourceTree = "<group>"; };
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigParserTest.swift; sourceTree = "<group>"; };
@ -191,16 +276,39 @@
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>"; };
C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Switcher.swift"; sourceTree = "<group>"; };
C4CE3BB927B31F670086CA49 /* MainMenu+Composer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Composer.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>"; };
C4DEB7D327A5D60B00834718 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = "<group>"; };
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
C4EC1E65279DE0380010F296 /* ServicesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ServicesView.xib; sourceTree = "<group>"; };
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>"; };
C4EED88827A48778006D7272 /* InterAppHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterAppHandler.swift; sourceTree = "<group>"; };
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = "<group>"; };
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = "<group>"; };
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>"; };
@ -237,6 +345,9 @@
C4998F092617633900B2526E /* PrefsWC.swift */,
5420395826135DC100FB00FA /* PrefsVC.swift */,
5420395E2613607600FB00FA /* Preferences.swift */,
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */,
C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */,
C4DEB7D327A5D60B00834718 /* Stats.swift */,
C41CD0272628D8E20065BBED /* Keybinds */,
54FCFD28276C88C0004CE748 /* Views */,
);
@ -246,6 +357,9 @@
54B20EDF263AA22C00D3250E /* PHP */ = {
isa = PBXGroup;
children = (
C48D6C6E279CD29C00F26D7E /* PHP Version */,
C4D9ADC2277610E4007277F4 /* Switcher */,
C4F30B01278E169B00755FCE /* Homebrew */,
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */,
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */,
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
@ -256,8 +370,10 @@
54FCFD28276C88C0004CE748 /* Views */ = {
isa = PBXGroup;
children = (
54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */,
C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */,
54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */,
54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */,
C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */,
54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */,
54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */,
);
@ -273,16 +389,44 @@
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>";
};
C41C1B2A22B0097F00E7CF16 = {
isa = PBXGroup;
children = (
C4F8C0A522D4FA41002EFE61 /* README.md */,
C4E713562570150F00007428 /* SECURITY.md */,
C4F7807425D7F7E5000DBC97 /* RELEASE.md */,
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */,
C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
C41C1B3422B0097F00E7CF16 /* Products */,
C4D309E72770EF2F00958BCF /* Frameworks */,
);
sourceTree = "<group>";
};
@ -298,7 +442,7 @@
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup;
children = (
C4EE188322D3386B00E126E5 /* Constants.swift */,
C4B5853A2770FE2500DA4FBE /* Common */,
C41E181722CB61EB0072CF09 /* Domain */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
C4232EE42612526500158FC6 /* Credits.html */,
@ -322,18 +466,36 @@
isa = PBXGroup;
children = (
C4AF9F6B275445D300D44ED0 /* Integrations */,
C4B13B1D25C4915000548C3A /* Core */,
54B20EDF263AA22C00D3250E /* PHP */,
C4F7808A25D7F918000DBC97 /* Terminal */,
C4B13B1D25C4915000548C3A /* App */,
C4D9ADBD27761084007277F4 /* PHP */,
C47331A0247093AC009A0597 /* Menu */,
C464ADAA275A7A25003FCD53 /* SiteList */,
5420395726135DB800FB00FA /* Preferences */,
C4811D2822D70D9C00B5F6B3 /* Helpers */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
C44C198F276E3A380072762D /* Progress */,
C4C8E81D276F5686003AC782 /* Watcher */,
C4EE55B027708BB2001DF387 /* SwiftUI */,
);
path = Domain;
sourceTree = "<group>";
};
C44C198F276E3A380072762D /* Progress */ = {
isa = PBXGroup;
children = (
C44C198C276E3A1C0072762D /* ProgressWindow.swift */,
C44C1990276E44CB0072762D /* ProgressWindow.storyboard */,
);
path = Progress;
sourceTree = "<group>";
};
C44CCD4327AFE93300CE40E5 /* Errors */ = {
isa = PBXGroup;
children = (
C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */,
C4927F0A27B2DFC200C55AFD /* Errors.swift */,
);
path = Errors;
sourceTree = "<group>";
};
C464ADAA275A7A25003FCD53 /* SiteList */ = {
isa = PBXGroup;
children = (
@ -341,6 +503,7 @@
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */,
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */,
C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */,
C4930849279F331F009C240B /* AddSiteVC.swift */,
C464ADB1275A87CA003FCD53 /* SiteListCell.swift */,
);
path = SiteList;
@ -350,11 +513,17 @@
isa = PBXGroup;
children = (
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */,
C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */,
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */,
C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */,
C4CE3BB927B31F670086CA49 /* MainMenu+Composer.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>";
@ -368,12 +537,21 @@
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
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 = (
@ -385,6 +563,7 @@
C4AF9F6B275445D300D44ED0 /* Integrations */ = {
isa = PBXGroup;
children = (
C4D89BC42783C98800A02B68 /* Composer */,
C4AF9F6C275445D900D44ED0 /* Homebrew */,
C4AF9F6A275445C900D44ED0 /* Valet */,
);
@ -395,38 +574,111 @@
isa = PBXGroup;
children = (
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */,
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
);
path = Homebrew;
sourceTree = "<group>";
};
C4B13B1D25C4915000548C3A /* Core */ = {
C4B13B1D25C4915000548C3A /* App */ = {
isa = PBXGroup;
children = (
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
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 */,
C4EED88827A48778006D7272 /* InterAppHandler.swift */,
);
path = Core;
path = App;
sourceTree = "<group>";
};
C4B5853A2770FE2500DA4FBE /* Common */ = {
isa = PBXGroup;
children = (
C40C7F2127721F7300DDDCDC /* Core */,
54B20EDF263AA22C00D3250E /* PHP */,
C44CCD4327AFE93300CE40E5 /* Errors */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
C4811D2822D70D9C00B5F6B3 /* Helpers */,
);
path = 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 */,
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.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 = (
C49E171E27A5736E00787921 /* PMServicesView.swift */,
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 */,
@ -435,16 +687,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 = (
@ -503,8 +745,8 @@
C41C1B2B22B0097F00E7CF16 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1220;
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1320;
ORGANIZATIONNAME = "Nico Verbruggen";
TargetAttributes = {
C41C1B3222B0097F00E7CF16 = {
@ -548,9 +790,13 @@
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 */,
54FCFD26276C883F004CE748 /* SelectPreferenceView.xib in Resources */,
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
C4F30B07278E195800755FCE /* brew-services.json in Resources */,
C4068CA427B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */,
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */,
54FCFD2D276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */,
@ -561,11 +807,14 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54FCFD27276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */,
54FCFD27276C883F004CE748 /* SelectPreferenceView.xib in Resources */,
54FCFD2E276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
C4068CA527B0780A00544CD5 /* CheckboxPreferenceView.xib 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;
};
@ -578,49 +827,81 @@
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 */,
C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
C49E171F27A5736E00787921 /* PMServicesView.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 */,
C4CE3BB827B31F2E0086CA49 /* MainMenu+Switcher.swift in Sources */,
C415937F27A1B54F00D2E1B7 /* PhpFrameworks.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 */,
54AB03262763858F00A29D5F /* Timer.swift in Sources */,
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */,
C4068CAA27B0890D00544CD5 /* MenuBarIcons.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 */,
C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */,
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */,
C4927F0B27B2DFC200C55AFD /* Errors.swift in Sources */,
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */,
C44CCD4027AFE2FC00CE40E5 /* AlertableError.swift in Sources */,
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */,
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */,
C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
C4CE3BBA27B31F670086CA49 /* MainMenu+Composer.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 */,
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -629,58 +910,89 @@
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 */,
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 */,
C4068CA827B07A1300544CD5 /* SelectPreferenceView.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 */,
C4F319C927B034A500AFF46F /* Stats.swift in Sources */,
C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */,
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
C40B24F527A3108B0018C7D2 /* Async.swift in Sources */,
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */,
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */,
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */,
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
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 */,
C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */,
C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */,
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */,
C4CE3BBC27B324250086CA49 /* MainMenu+Composer.swift in Sources */,
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */,
C417DC75277614690015E6EE /* Helpers.swift in Sources */,
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */,
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
C44CCD4127AFE2FC00CE40E5 /* AlertableError.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 */,
C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */,
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
C40B24F227A310770018C7D2 /* Events.swift in Sources */,
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */,
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
C4B585452770FE3900DA4FBE /* Command.swift in Sources */,
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */,
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */,
C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.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;
};
@ -831,7 +1143,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 610;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -840,7 +1152,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 4.1.1;
MARKETING_VERSION = 5.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -856,7 +1168,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 610;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -865,7 +1177,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 4.1.1;
MARKETING_VERSION = 5.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

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

145
README.md
View File

@ -1,15 +1,17 @@
# PHP Monitor
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project.
> You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy.<br>**Thank you!** ⭐️
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
<h1 align="center"><b>PHP Monitor</b> (phpmon)</h1>
**PHP Monitor** (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so you need to have it set up before you can use this.
<p align="center">
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
</p>
<img src="./docs/screenshot41.jpg" width="800px" alt="phpmon screenshot (menu bar app)"/>
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this</u>.
<small><i>Screenshot: A menu showing all of the functionality of PHP Monitor.</i></small>
<img src="./docs/screenshot50.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: Showing the key functionality of PHP Monitor. You can also add new domains as links, manage various services, and perform First Aid to fix all kinds of common PHP link issues.</i></small>
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
@ -19,7 +21,7 @@ PHP Monitor also gives you quick access to various useful functionality (like ac
## 🖥 System requirements
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
@ -30,7 +32,7 @@ _You may need to update your Valet installation to keep everything working if a
## 🚀 How to install
You can install via Homebrew (recommended), or may download the latest release on GitHub.
Again, make sure you have **Laravel Valet** installed first. Once that's done, you can install via Homebrew (recommended), or may download the latest release on GitHub.
To install via Homebrew, run:
@ -41,7 +43,11 @@ To upgrade your existing installation, run:
brew upgrade phpmon
_The app is signed and notarized, meaning all you have to do is approve its first launch._
(You may need to run `brew update` first in order to update the cask file if you ran a Homebrew operation recently.)
## 🔑 Is the app signed & notarized?
Yes, the app is signed and notarized, meaning all you have to do is approve its first launch (or whenever it updates).
## 👨‍💻 Why build this?
@ -51,10 +57,12 @@ Initially, I had an Alfred workflow for this — but it has now been replaced wi
## 🤬 The app won't start?!
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
**Follow instructions as specified in the alert in order to resolve any issues.**
(If the app crashes at launch without showing you any of these messages, you might have a non-standard Homebrew and Valet setup. Those are not supported.)
## 🙋‍♂️ FAQ & Troubleshooting
> If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. To upgrade Valet, run `composer global update`. Don't forget to run `valet install` after upgrading.
@ -111,9 +119,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,14 +254,75 @@ 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>
<summary><strong>How can the app integrate with third party tools, like Alfred?</strong></summary>
There's an Alfred workflow usually included in the release list, you can grab it by going to releases and downloading the asset `phpmon.alfredworkflow`.
If you'd like to integrate something yourself, all you need to to is use the `phpmon://` protocol and ensure that third party app integrations are enabled in Preferences (in PHP Monitor).
Using app callbacks, macOS and PHP Monitor allow for the following to be called:
* phpmon://list
* phpmon://services/stop
* phpmon://services/restart/all
* phpmon://services/restart/nginx
* phpmon://services/restart/php
* phpmon://services/restart/dnsmasq
* phpmon://locate/config
* phpmon://locate/composer
* phpmon://locate/valet
* phpmon://phpinfo
* phpmon://switch/php/{version}
</details>
<details>
<summary><strong>How does the app know what PHP version is required for my app?</strong></summary>
The `composer.json` file in the root of the folder (if it exists) is scanned and interpreted.
If the version is set in `platform`, it takes precendence.
If the version is not set in `platform` but it is in `require` (most common) then that version is used.
</details>
<details>
<summary><strong>What do the checkmarks next to the PHP version mean in the site list?</strong></summary>
You'll see a checkmark next to the version number if the currently enabled PHP version is compatible with the version required to run the site.
This is determined by evaluating the PHP requirement constraint (e.g. `^8.0`, `~8.0` or a specific version: `8.0`).
</details>
<details>
<summary><strong>Why can't I see the driver type any more? It says "Project Type" now.</strong></summary>
PHP Monitor currently checks your `composer.json` file to try to figure out what project you are running.
This approach is a lot faster than asking for a driver when you have many sites linked, but is slightly less reliable since the framework or type of project inferred via `composer.json` might not be 100% accurate.
You can always still ask Valet using the command line, should it be necessary. In my experience fetching the drivers slowed down the app unnecessarily.
</details>
<details>
<summary><strong>After running PHP Monitor, Homebrew sometimes has issues with `brew upgrade` or `brew cleanup`!</strong></summary>
<strike>This is a security feature of Homebrew. When you start a service as an administrator, the root user becomes the owner of relevant binaries. You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder).</strike>
You can now use **First Aid & Services > Restore Homebrew Permissions** to (temporarily) resolve this issue and allow for a clean and painless `brew upgrade` or `brew cleanup` process.
**Update**: If you are using the Valet switcher (currently not available in the latest stable build) you will not encounter this issue. For more information on this, see [this issue](https://github.com/nicoverbruggen/phpmon/issues/17) and also [this issue about switching to Valet's switcher](https://github.com/nicoverbruggen/phpmon/issues/34).
If you would like to know more, consult [this issue](https://github.com/nicoverbruggen/phpmon/issues/85) for more information about why this is needed.
</details>
@ -256,6 +331,10 @@ To see which files are checked to determine availability, see [this file](./phpm
Please get in touch and open an issue. PHP Monitor shouldn't crash... (unless you are actually removing PHP *while* the app is running, thats considered normal behaviour!)
If you would like to report a crash, please include the associated **log files** so I can find out what exactly went wrong.
To find the logs, take a look in `~/Library/Logs/DiagnosticReports` (in Finder) and see if there's any (log) files that start with "PHP Monitor".
</details>
## 📝 Having another issue?
@ -275,11 +354,10 @@ Donations really help with the Apple Developer Program cost, and keep me motivat
While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks:
* My colleagues at [DIVE](https://dive.be)
* The [Homebrew](https://brew.sh/) team who maintain
* The [developers & maintainers of Valet](https://github.com/laravel/valet/graphs/contributors)
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors)
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so
* Everyone in the Laravel community who shared the app (thanks!)
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot)
* Everyone who left feedback via issues
* Everyone who left feedback via issues & who donated to keep the project up and running
Thank you very much for your contributions, kind words and support.
@ -297,21 +375,28 @@ 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. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all.
*Note*: If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
### Want to know more?
If you want to know more about how this works, I recommend you check out the source code.
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.
I have done my best to annotate as much as humanly possible, and have avoided using an overly complicated architecture to keep the code as easy to maintain as possible. The code isn't perfect by a long shot (lots of cleanup can still happen!) but the application works well.
## 🔧 Build instructions
I also have a few tests for key parts of the application that I found needed to be tested. In the future, I would like to add even more tests for some of the UI stuff, but for now the tests are more unit tests than feature tests.
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
If you'd like to build PHP Monitor yourself, you need:
* Xcode (usually the latest version)
* The contents of this repository
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to immediately build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
For more detailed information for developers, please see [the documentation file for developers](./DEVS.md).

View File

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

BIN
assets/icons.afdesign Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

BIN
docs/screenshot50.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

View File

@ -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,286 @@
//
// 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(
try! PhpVersionNumber.parse("PHP 8.1.0RC5-dev"),
PhpVersionNumber(major: 8, minor: 1, patch: 0)
)
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 testPhpVersionNumberParse() throws {
XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
XCTAssertTrue(error is VersionParseError)
}
}
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
)
}
func testCanCheckGreaterThanOrEqualConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">=7.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">=7.0.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">=7.2.5", strict: true),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all
)
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">=7.2.5", strict: false),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2"]).all
)
}
func testCanCheckGreaterThanConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">7.0"),
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">7.2.5"),
// 7.2 will be valid due to non-strict mode (resolves to 7.2.999)
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: ">7.2.5", strict: true),
// 7.2 will not be valid due to strict mode (resolves to 7.2.0)
PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
.matching(constraint: ">7.2.8"),
// 7.2 will be valid due to non-strict mode (resolves to 7.2.999)
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
.matching(constraint: ">7.2.8", strict: true),
// 7.2 will not be valid due to strict mode (resolves to 7.2.0)
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9"]).all
)
}
}

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
{
"images" : [
{
"filename" : "Linked.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "link.svg",
"filename" : "Linked@2x.png",
"idiom" : "universal",
"scale" : "2x"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,11 +1,12 @@
{
"images" : [
{
"filename" : "Parked.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "car-alt.svg",
"filename" : "Parked@2x.png",
"idiom" : "universal",
"scale" : "2x"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M438.66 212.33l-11.24-28.1-19.93-49.83C390.38 91.63 349.57 64 303.5 64h-127c-46.06 0-86.88 27.63-103.99 70.4l-19.93 49.83-11.24 28.1C17.22 221.5 0 244.66 0 272v48c0 16.12 6.16 30.67 16 41.93V416c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-32h256v32c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-54.07c9.84-11.25 16-25.8 16-41.93v-48c0-27.34-17.22-50.5-41.34-59.67zm-306.73-54.16c7.29-18.22 24.94-30.17 44.57-30.17h127c19.63 0 37.28 11.95 44.57 30.17L368 208H112l19.93-49.83zM80 319.8c-19.2 0-32-12.76-32-31.9S60.8 256 80 256s48 28.71 48 47.85-28.8 15.95-48 15.95zm320 0c-19.2 0-48 3.19-48-15.95S380.8 256 400 256s32 12.76 32 31.9-12.8 31.9-32 31.9z"/></svg>

Before

Width:  |  Height:  |  Size: 918 B

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,11 +1,12 @@
{
"images" : [
{
"filename" : "Menu Bar.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "php@2x.png",
"filename" : "Menu Bar@2x.png",
"idiom" : "universal",
"scale" : "2x"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 940 B

View File

@ -0,0 +1,146 @@
//
// 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)
}
public static func fixHomebrewPermissions() throws
{
var servicesCommands = [
"\(Paths.brew) services stop nginx",
"\(Paths.brew) services stop dnsmasq",
]
var cellarCommands = [
"chown -R \(Paths.whoami):staff \(Paths.cellarPath)/nginx",
"chown -R \(Paths.whoami):staff \(Paths.cellarPath)/dnsmasq"
]
PhpEnv.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnv.brewPhpVersion
? "php"
: "php@\(version)"
servicesCommands.append("\(Paths.brew) services stop \(formula)")
cellarCommands.append("chown -R \(Paths.whoami):staff \(Paths.cellarPath)/\(formula)")
}
let script =
servicesCommands.joined(separator: " && ")
+ " && "
+ cellarCommands.joined(separator: " && ")
let appleScript = NSAppleScript(
source: "do shell script \"\(script)\" with administrator privileges"
)
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
if (eventResult == nil) {
throw HomebrewPermissionError(kind: .applescriptNilError)
}
}
// 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: - Fix My Valet
/**
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 fixMyValet()
{
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 dnsmasq")
brew("services stop php")
brew("services stop nginx")
brew("link php --overwrite --force")
brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true)
brew("services restart nginx", sudo: true)
}
}

View File

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

View File

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

View File

@ -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("[ERR] \(item)")
}
}
static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() {
print("[WRN] \(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,74 @@
//
// 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.
The path to the Homebrew directory and the user's name are fetched only once, at boot.
*/
public class Paths {
public static let shared = Paths()
private var baseDir : Paths.HomebrewDir
private var userName : String
init() {
baseDir = FileManager.default.fileExists(atPath: "\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
}
// - MARK: Binaries
public static var valet: String {
return "\(binPath)/valet"
}
public static var brew: String {
return "\(binPath)/brew"
}
public static var php: String {
return "\(binPath)/php"
}
public static var phpConfig: String {
return "\(binPath)/php-config"
}
// - MARK: Paths
public static var whoami: String {
return shared.userName
}
public static var cellarPath: String {
return "\(shared.baseDir.rawValue)/Cellar"
}
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

@ -0,0 +1,180 @@
//
// Shell.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
public class Shell {
// MARK: - Invoke static functions
public static func run(
_ command: String,
requiresPath: Bool = false
) {
Shell.user.run(command, requiresPath: requiresPath)
}
public static func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
return Shell.user.pipe(command, requiresPath: requiresPath)
}
// MARK: - Singleton
/**
We now require macOS 11, so no need to detect which terminal to use.
*/
public var shell: String = "/bin/sh"
/**
Singleton to access a user shell (with --login)
*/
public static let user = Shell()
/**
Runs a shell command without using the output.
Uses the default shell.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
private func run(
_ command: String,
requiresPath: Bool = false
) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = Shell.pipe(command, requiresPath: requiresPath)
}
/**
Runs a shell command and returns the output.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
private func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
let shellOutput = self.executeSynchronously(command, requiresPath: requiresPath)
let hasError = (
shellOutput.standardOutput == ""
&& shellOutput.errorOutput.lengthOfBytes(using: .utf8) > 0
)
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
}
/**
Runs the command and returns a `ShellOutput` object, which contains info about the process.
- Parameter command: The command to run
- 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`
*/
public func executeSynchronously(
_ command: String,
requiresPath: Bool = false
) -> Shell.Output {
let outputPipe = Pipe()
let errorPipe = Pipe()
let task = self.createTask(for: command, requiresPath: requiresPath)
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
return Shell.Output(
standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
errorOutput: String(
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
task: task
)
}
/**
Checks if a file exists at a certain path.
Used to be done with a shell command, now uses the native FileManager class instead.
*/
public static func fileExists(_ path: String) -> Bool {
let fullPath = path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
return FileManager.default.fileExists(atPath: fullPath)
}
/**
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 = ["--noprofile", "-norc", "--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

@ -0,0 +1,13 @@
//
// Errors.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 06/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol AlertableError {
func getErrorMessageKey() -> String
}

View File

@ -0,0 +1,29 @@
//
// VersionParseError.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
// MARK: - Alertable Errors
// These errors must be resolved by the user.
struct HomebrewPermissionError: Error, AlertableError {
enum Kind: String {
case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
}
let kind: Kind
func getErrorMessageKey() -> String {
return "alert.errors.\(self.kind.rawValue)"
}
}
// MARK: - Errors that do not have an associated alert message
// The errors must be resolved by the developer.
struct VersionParseError: Error {}

View File

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

View File

@ -51,6 +51,9 @@ class Alert {
}
}
/**
Notify the user about something by showing an alert.
*/
public static func notify(message: String, info: String, style: NSAlert.Style = .informational) {
_ = present(
messageText: message,
@ -61,4 +64,18 @@ class Alert {
)
}
/**
Notify the user about a particular error (which must be `Alertable`)
by showing an alert.
*/
public static func notify(about error: Error & AlertableError) {
let key = error.getErrorMessageKey()
_ = present(
messageText: "\(key).title".localized,
informativeText: "\(key).description".localized,
buttonTitle: "OK",
secondButtonTitle: "",
style: .critical
)
}
}

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.
@ -40,10 +40,9 @@ class Application {
/** 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,29 @@
//
// Async.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
This generic async helper is something I'd like to use in more places.
The `DispatchQueue.global` into `DispatchQueue.main.async` logic is common in the app.
I could also use try `async` support which was introduced in Swift but that would
require too much refactoring at this time to consider. I also need to read up on async
in order to properly grasp all the gotchas. Looking into that later at some point.
*/
public func runAsync(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
{
DispatchQueue.global(qos: .userInitiated).async {
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

@ -42,7 +42,7 @@ class MenuBarImageGenerator {
let targetImage: NSImage = NSImage(size: image.size)
let rep: NSBitmapImageRep = NSBitmapImageRep(
let representation: NSBitmapImageRep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(image.size.width),
pixelsHigh: Int(image.size.height),
@ -55,7 +55,7 @@ class MenuBarImageGenerator {
bitsPerPixel: 0
)!
targetImage.addRepresentation(rep)
targetImage.addRepresentation(representation)
targetImage.lockFocus()
image.draw(in: imageRect)
@ -65,33 +65,73 @@ class MenuBarImageGenerator {
return targetImage
}
/**
The same as before, but also attempts to add an icon to the left.
*/
public static func textToImageWithIcon(text: String) -> NSImage {
let textImage = self.textToImage(text: text)
let iconImage = NSImage(named: "StatusBarPHP")!
let iconWidthSize = iconImage.size.width
let divider = iconWidthSize
// We'll start out with the image containing the text
let textImage = self.textToImage(text: text)
// Then we'll fetch the image we want on the left
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
if iconType == nil {
Log.warn("Invalid icon type found, using the default")
iconType = MenuBarIcon.iconPhp.rawValue
}
let iconImage = NSImage(named: "MenuBar_\(iconType!)")!
// We'll need to reference the width of the icon a bunch of times
let iconWidthSize = iconImage.size.width
// There will also be an additional divider between the image and the text (image)
let divider: CGFloat = 3
// Use a fixed size for the height of the menu bar (18pt)
let imageRect = CGRect(
x: 0,
y: 0,
width: textImage.size.width + divider,
height: textImage.size.height
width: textImage.size.width + iconWidthSize + divider,
height: 18
)
// Create a new image, we'll draw the text and our icon in there
let image: NSImage = NSImage(size: imageRect.size)
image.lockFocus()
// Calculate the offset between the image and the text
let offset = imageRect.size.width - textImage.size.width
let difference = imageRect.size.width - textImage.size.width
// Draw the text with a negative x offset (so there is room on the left for the icon)
textImage.draw(
in: imageRect,
from: NSRect(
x: -offset,
y: 0,
width: textImage.size.width + offset,
height: textImage.size.height
),
operation: .overlay,
fraction: 1
)
textImage.draw(in: imageRect, from: NSRect(
x: -difference,
y: 0, width: textImage.size.width + difference,
height: textImage.size.height
), operation: .overlay, fraction: 1)
iconImage.draw(in: imageRect, from: NSRect(x: 0, y: 0, width: imageRect.size.width * 1.6, height: imageRect.size.height * 2.0), operation: .overlay, fraction: 1)
// Draw the icon directly in the left of the imageRect (where we left space)
iconImage.draw(
in: imageRect,
from: NSRect(
x: 0,
y: 0,
width: imageRect.size.width,
height: imageRect.size.height
),
operation: .overlay,
fraction: 1
)
// We're done with this image
image.unlockFocus()
return image
}

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,44 @@
//
// VersionExtractor.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class VersionExtractor {
/**
This attempts to extract the version number from the command line output of Valet.
*/
public static func from(_ string: String) -> String? {
do {
let regex = try NSRegularExpression(
pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
options: []
)
let match = regex.matches(
in: string,
options: [],
range: NSMakeRange(0, string.count)
).first
guard let match = match else {
return nil
}
let range = Range(
match.range(withName: "version"),
in: string
)!
return String(string[range])
} catch {
return nil
}
}
}

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,165 @@
//
// PhpSwitcher.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
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))
}
}
/**
Validates whether the currently running version matches the provided version.
*/
public func validate(_ version: String) -> Bool {
if self.currentInstall.version.short == version {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
return true
}
return false
}
}

View File

@ -0,0 +1,184 @@
//
// 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 patch version check is strict. See more below.
The strict mode does not matter if a patch version is provided for all versions in the collection.
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 (with patch precision), 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.isSameAs(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
// Caret range means that the major version is never higher but minor version can be higher
// ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis.
return self.versions.filter {
version.patch != nil
// If a patch is provided then the minor version cannot be bumped.
? $0.hasSameMajorAndMinorButNewerOrSamePatch(version, strict)
// If a patch is not provided then the major version cannot be bumped.
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
}
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) }
}
return []
}
}
public struct PhpVersionNumber: Equatable {
let major: Int
let minor: Int
let patch: Int?
public func patch(_ strictFallback: Bool = true, _ 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"#
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
// TODO: (5.1) Handle these cases (even though I suspect these are uncommon)
/*
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
*/
}
public static func parse(_ text: String) throws -> Self {
guard let versionText = VersionExtractor.from(text) else {
throw VersionParseError()
}
return Self.make(from: versionText)!
}
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
}
// MARK: Comparison Logic
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
}
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return (
self.major > version.major ||
self.major == version.major && self.minor > version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) > version.patch(strict)
)
}
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major &&
(
(self.minor == version.minor && self.patch(strict) >= version.patch(strict, self))
|| self.minor > version.minor
)
}
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict)
}
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor >= version.minor
}
}

View File

@ -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,27 @@ 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)
// The parser should always work, or the string has to be very unusual.
// If so, the app SHOULD crash, so that the users report what's up.
// TODO: Alert the user that the version number could not be parsed.
self.longVersion = try! PhpVersionNumber.parse(longVersionString)
}
}

View File

@ -0,0 +1,61 @@
//
// 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("Restarting nginx, just to be sure!")
brew("services restart nginx", sudo: true)
Log.info("The new version has been linked!")
completion()
}
}
}

View File

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

View File

@ -16,6 +16,7 @@
<p><b>Want to spread the love?</b> Leave a <a href="https://github.com/nicoverbruggen/phpmon">star on GitHub</a>!</p>
<p><b>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p>
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p>
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
<br>
</body>
</html>

View File

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

@ -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,10 @@ 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!
}

View File

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

View File

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

View File

@ -13,16 +13,20 @@ extension AppDelegate {
// MARK: - Notifications
/**
Sets up notifications. That does mean we need to ask for permission first.
If we cannot get permission, we should log this.
*/
public func setupNotifications() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
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,30 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
*/
let valet: Valet
/**
The PhpEnv singleton that handles PHP version
detection, as well as switching. It is initialized
when the app is ready and passed all checks.
*/
var phpEnvironment: PhpEnv! = nil
/**
The logger is responsible for different levels of logging.
You can tweak the verbosity in the `init` method here.
*/
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 = .info
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 +76,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
super.init()
}
func initializeSwitcher() {
self.phpEnvironment = PhpEnv.shared
}
// MARK: - Lifecycle
/**

View File

@ -4,6 +4,7 @@
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -49,11 +50,31 @@
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Sites" id="YTZ-bb-TOG">
<items>
<menuItem title="Reload Site List" keyEquivalent="r" id="Ema-AU-Nbr">
<menuItem title="add-as-link" keyEquivalent="n" id="du1-bO-N2U" userLabel="Add Link" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_add_folder_as_link"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="addSiteLinkPressed:" target="Voe-Tx-rLC" id="DzS-MY-6g0"/>
</connections>
</menuItem>
<menuItem title="reload-list" keyEquivalent="r" id="Ema-AU-Nbr" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_site_list"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="reloadSiteListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="2ux-8Q-UjK"/>
<menuItem title="focus-find" keyEquivalent="f" id="I95-fb-EL7" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_site_list"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
@ -298,7 +319,7 @@
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="-484" y="32"/>
<point key="canvasLocation" x="-495" y="-44"/>
</scene>
<!--Window Controller-->
<scene sceneID="PQa-AT-b2a">
@ -334,11 +355,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 +395,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 +421,7 @@
</searchToolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="GsC-ra-40U"/>
<toolbarItem reference="YtK-vM-5y7"/>
<searchToolbarItem reference="Q7Z-fw-lB9"/>
</defaultToolbarItems>
@ -406,30 +437,199 @@
</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="q0L-YZ-F3J"/>
</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="SwS-o8-pbl" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="900-Z2-tID" secondAttribute="trailing" constant="10" id="9uc-R7-CZk"/>
<constraint firstItem="6JT-Vt-3q0" firstAttribute="top" secondItem="P0B-Ht-R8n" secondAttribute="bottom" constant="8" symbolic="YES" id="DGN-4k-X0h"/>
<constraint firstItem="P0B-Ht-R8n" firstAttribute="top" secondItem="JJJ-T9-Yuv" secondAttribute="top" constant="20" symbolic="YES" id="F2r-6E-qxh"/>
<constraint firstItem="mmQ-7e-dlb" firstAttribute="top" secondItem="KZf-b0-9cm" secondAttribute="bottom" constant="8" symbolic="YES" id="G21-Vd-tgl"/>
<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 firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="P0B-Ht-R8n" secondAttribute="trailing" constant="20" symbolic="YES" id="nvj-Ij-dcd"/>
<constraint firstItem="VzR-5a-cmT" firstAttribute="top" secondItem="ZX9-s1-23i" secondAttribute="bottom" constant="8" symbolic="YES" id="sVP-EV-07F"/>
<constraint firstAttribute="trailing" secondItem="ZX9-s1-23i" secondAttribute="trailing" constant="20" symbolic="YES" id="tZ3-2X-JC9"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="KZf-b0-9cm" secondAttribute="trailing" constant="20" symbolic="YES" id="zq0-Ce-sCs"/>
</constraints>
</view>
<connections>
<outlet property="buttonCancel" destination="SwS-o8-pbl" id="N1v-uy-2Mi"/>
<outlet property="buttonCreateLink" destination="PVw-cM-qAB" id="0Oo-xW-He7"/>
<outlet property="buttonSecure" destination="KZf-b0-9cm" id="5A7-Bn-NB7"/>
<outlet property="linkName" destination="ZX9-s1-23i" id="yT6-80-Zr1"/>
<outlet property="pathControl" destination="6JT-Vt-3q0" id="f5K-8h-VOd"/>
<outlet property="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>
<customView id="j65-Lf-0lG">
<rect key="frame" x="9" y="0.0" width="581" height="203"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</customView>
<scrollView autohidesScrollers="YES" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
<rect key="frame" x="0.0" y="0.0" width="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 +642,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 +670,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 +678,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 +686,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,59 +697,63 @@
<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 translatesAutoresizingMaskIntoConstraints="NO" id="ypa-iv-wLD">
<rect key="frame" x="211" y="18" width="18" height="18"/>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="3xt-wC-hUJ">
<rect key="frame" x="363" y="18" width="75" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="18" id="jKJ-Xn-BPA"/>
<constraint firstAttribute="height" constant="18" id="lSH-of-WzD"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="75" id="VI8-MP-7Hv"/>
</constraints>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSCaution" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="9XB-KO-aSI">
<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="system"/>
<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>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="150" translatesAutoresizingMaskIntoConstraints="NO" id="MD8-ef-Ht8">
<rect key="frame" x="235" y="16" width="182" height="22"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Warning: This is a warning message. Please take this into account." id="iub-KH-clf">
<font key="font" metaFont="system" size="9"/>
<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="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="IconColorGreen"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" secondItem="MD8-ef-Ht8" secondAttribute="trailing" constant="20" id="1Rb-Or-Nnn"/>
<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"/>
<constraint firstItem="syz-LF-l6P" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" id="8QK-nf-Fiw"/>
<constraint firstItem="QPX-eu-eV8" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="top" id="9QB-jZ-k1V"/>
<constraint firstItem="ypa-iv-wLD" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="9d8-P2-iSk"/>
<constraint firstItem="MD8-ef-Ht8" firstAttribute="leading" secondItem="ypa-iv-wLD" secondAttribute="trailing" constant="8" symbolic="YES" id="C90-wQ-3Gf"/>
<constraint firstItem="QPX-eu-eV8" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" constant="10" id="GOj-sw-ZlZ"/>
<constraint firstItem="TbX-e2-3QL" firstAttribute="top" secondItem="jKi-Ls-7FZ" secondAttribute="bottom" constant="-1" id="J29-wT-Uex"/>
<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="ypa-iv-wLD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="30" id="koV-Sj-tO8"/>
<constraint firstItem="MD8-ef-Ht8" firstAttribute="centerY" secondItem="ypa-iv-wLD" secondAttribute="centerY" id="lIN-pm-mCo"/>
<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="buttonWarning" destination="ypa-iv-wLD" id="NwX-H3-8um"/>
<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="labelDriverType" destination="jKi-Ls-7FZ" id="ZTq-pP-qUC"/>
<outlet property="labelPathName" destination="CXK-Q9-CpO" id="iVZ-cL-azB"/>
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
<outlet property="labelWarning" destination="MD8-ef-Ht8" id="Faw-CY-9R5"/>
</connections>
</tableCellView>
</prototypeCellViews>
@ -564,10 +768,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="292" width="598" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
@ -576,7 +780,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"/>
@ -599,13 +803,17 @@
</viewController>
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="288" y="764.5"/>
<point key="canvasLocation" x="251" y="741.5"/>
</scene>
</scenes>
<resources>
<image name="IconLinked" width="512" height="512"/>
<image name="Checkmark" width="512" height="512"/>
<image name="IconLinked" width="25" height="25"/>
<image name="Lock" width="30" height="30"/>
<image name="NSCaution" width="32" height="32"/>
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
<image name="plus" catalog="system" width="14" height="13"/>
<namedColor name="IconColorGreen">
<color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

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

View File

@ -6,6 +6,7 @@
//
import Foundation
import AppKit
class Startup {
@ -48,6 +49,13 @@ class Startup {
breaking: true
)
performEnvironmentCheck(
HomebrewDiagnostics.cannotLoadService(),
messageText: "startup.errors.services_json_error.title".localized,
informativeText: "startup.errors.services_json_error.desc".localized,
breaking: true
)
performEnvironmentCheck(
!Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"),
messageText: "startup.errors.sudoers_brew.title".localized,
@ -56,56 +64,49 @@ 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,
breaking: true
)
let services = Shell.pipe("\(Paths.brew) services list | grep php")
// Determine the Valet version only AFTER confirming the correct permission is in place
Valet.shared.version = VersionExtractor.from(valet("--version"))
performEnvironmentCheck(
(services.countInstances(of: "started") > 1),
messageText: "startup.errors.services.title".localized,
informativeText: "startup.errors.services.desc".localized,
breaking: false
Valet.shared.version == nil,
messageText: "startup.errors.valet_version_unknown.title".localized,
informativeText: "startup.errors.valet_version_unknown.desc".localized,
breaking: true
)
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

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

@ -1,32 +0,0 @@
//
// BenchmarkTimer.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class BenchmarkTimer {
let startTime: CFAbsoluteTime
var endTime: CFAbsoluteTime?
init() {
startTime = CFAbsoluteTimeGetCurrent()
}
func stop() -> CFAbsoluteTime {
endTime = CFAbsoluteTimeGetCurrent()
return duration!
}
var duration: CFAbsoluteTime? {
if let endTime = endTime {
return endTime - startTime
} else {
return nil
}
}
}

View File

@ -1,37 +0,0 @@
//
// VersionExtractor.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class VersionExtractor {
public static func from(_ string: String) -> String? {
let regex = try! NSRegularExpression(
pattern: #"Laravel Valet (?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
options: []
)
let match = regex.matches(
in: string,
options: [],
range: NSMakeRange(0, string.count)
).first
guard let match = match else {
return nil
}
let range = Range(
match.range(withName: "version"),
in: string
)!
return String(string[range])
}
}

View File

@ -0,0 +1,78 @@
//
// ComposerJson.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
This `Decodable` class is used to directly map `composer.json`
to this object.
*/
struct ComposerJson: Decodable {
// MARK: - JSON structure
let dependencies: Dictionary<String, String>?
let devDependencies: Dictionary<String, String>?
let configuration: Config?
private enum CodingKeys: String, CodingKey {
case dependencies = "require"
case devDependencies = "require-dev"
case configuration = "config"
}
struct Config: Decodable {
let platform: Platform?
}
struct Platform: Decodable {
let php: String?
}
// MARK: - Helpers
/**
Checks what the PHP version constraint is.
Returns a tuple (constraint, location of constraint).
*/
public func getPhpVersion() -> (String, String) {
// Check if in platform
if configuration?.platform?.php != nil {
return (configuration!.platform!.php!, "platform")
}
// Check if in dependencies
if dependencies?["php"] != nil {
return (dependencies!["php"]!, "require")
}
// Unknown!
return ("???", "unknown")
}
/**
Checks if any notable dependencies can be resolved.
Only notable dependencies are saved.
*/
public func getNotableDependencies() -> [String: String] {
var notable: [String: String] = [:]
var scan = Array(PhpFrameworks.DependencyList.keys)
scan.append("php")
scan.forEach { dependency in
if dependencies?[dependency] != nil {
notable[dependency] = dependencies![dependency]
}
}
return notable
}
}

View File

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

View File

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

View File

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

View File

@ -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,7 +10,7 @@
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="HeaderView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="350" height="24"/>
<rect key="frame" x="0.0" y="0.0" width="270" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ddg-VQ-cOT">
@ -29,7 +29,7 @@
<connections>
<outlet property="textField" destination="ddg-VQ-cOT" id="aaQ-Xb-o2X"/>
</connections>
<point key="canvasLocation" x="-75" y="38"/>
<point key="canvasLocation" x="177" y="105"/>
</customView>
</objects>
</document>

Some files were not shown because too many files have changed in this diff Show More