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

Compare commits

..

122 Commits

Author SHA1 Message Date
4eea13f059 🚀 Version 5.2.2
Merge branch 'dev/5.2'
2022-04-22 19:50:57 +02:00
3cff2d6469 🐛 Fix issue w/ flag evaluation order (#165) 2022-04-20 22:31:20 +02:00
df506e4128 🐛 Fix various minor issues (discovered via #162)
- Renaming the configuration files from `www.conf` to the backup
  (`disabled-by-phpmon`) will now succeed if the `disabled-by-phpmon`
  file already exists. This would fail if the `disabled-by-phpmon` file
  already existed in previous builds.

- The PHP-FPM alert when there's an issue with a missing socket
  configuration file has been tweaked and now contains a workaround if
  you want to run a newer version of PHP (e.g. PHP 8.2) that is not
  officially supported by Valet yet.

- When attempting to list the PHP version numbers, the `parse()` method
  is now used, as opposed to `PhpVersionNumber.make()`, which couldn't
  correctly handle pre-release versions of PHP.

- Updated tests to reflect these changes to `PhpVersionNumber`.
2022-04-20 12:19:02 +02:00
f3b1172d0e 🚀 Version 5.2.1
This is a bugfix release that fixes the isolation command (#158).
2022-03-31 13:51:46 +02:00
32278533bf 🚀 Version 5.2
Merge branch 'dev/5.x'
2022-03-29 17:32:26 +02:00
39908f7fc5 🔧 Bump build 2022-03-29 17:32:09 +02:00
347d79c88d 👌 Cleanup 2022-03-29 17:25:47 +02:00
9bc117e9f5 🔥 Remove icon 2022-03-29 17:18:44 +02:00
ba5fbed9be 📝 Add logo 2022-03-29 17:18:26 +02:00
211556d5ce 📝 Include system administrator requirement 2022-03-29 14:16:28 +02:00
066d7bc217 🐛 Restoring Homebrew permissions uses admin group now 2022-03-29 14:14:39 +02:00
f072ceae37 📝 Updated FAQ 2022-03-29 09:55:38 +02:00
273dac1ca7 📝 Updated README with PHP upgrade instructions 2022-03-29 09:51:56 +02:00
f3b170ba14 🔧 Prepare for release 2022-03-29 09:23:57 +02:00
78d8030ed6 Fix tests 2022-03-25 23:55:41 +01:00
a300d2f4cf 👌 Show isolation is unavailable 2022-03-25 23:54:32 +01:00
96a658073e 📝 Update README and SECURITY 2022-03-25 17:26:58 +01:00
8395ba407d 👌 Use new, cleaner layout for screenshot 2022-03-25 17:11:40 +01:00
f80e3fed2b 👌 Disable border on table 2022-03-25 16:40:54 +01:00
b48edf7409 📝 New screenshot in README 2022-03-23 19:50:58 +01:00
e0a0eb089d 🔧 Prepare for a new dev build 2022-03-23 18:52:54 +01:00
60b126333d 👌 Extra startup check for invalid config.json
- Up to 50 sites are now preloaded (up from 30)
- No longer crash when invalid config.json is found (only at launch)
- Added `evaluateFeatureSupport` to Valet.swift
- Load configuration during launch checks instead
2022-03-23 18:46:35 +01:00
649a3f4fb5 👌 Handle correct version number 2022-03-23 18:15:17 +01:00
9a326928f3 🐛 Ensure unknown driver is at bottom 2022-03-21 18:45:41 +01:00
52f87ca18a 👌 Add tooltips to First Aid menu items 2022-03-21 18:33:25 +01:00
ad2d2cb57f Allow sorting of site list 2022-03-21 18:33:12 +01:00
22c0021ada 🐛 Workaround broken JSON file (AddSiteVC.swift:66) 2022-03-20 15:56:07 +01:00
2cfc5731fb 👌 Re-enable debugger, site loading 2022-03-20 15:07:34 +01:00
1d74e1536c Add default site to site list (#125) 2022-03-20 14:38:58 +01:00
e6b2ddf2ad 👌 Fix "WordPress" name in fake site window 2022-03-19 15:54:17 +01:00
62fda6224e 👌 Updated fake sites 2022-03-19 15:21:50 +01:00
083f8ebec8 Add marketing mode 2022-03-19 15:15:03 +01:00
aec8fb1168 🔧 Prepare for a new dev build 2022-03-18 18:38:11 +01:00
d5d9b38ed6 ♻️ Reworked "Fix My Valet" to use built-in switcher 2022-03-18 18:36:49 +01:00
9a6975e3d9 ♻️ Include HotKey source code 2022-03-18 18:21:55 +01:00
d1c40f2eb5 🐛 Fix issue with generator 2022-03-18 15:54:53 +01:00
ad58661449 🔥 Cleanup 2022-03-17 23:32:40 +01:00
1d6cfd419e Fix tests 2022-03-17 23:32:13 +01:00
15182ea15a 👌 Cleanup writing helper files 2022-03-17 23:28:03 +01:00
c43e00c88d 👌 Add source_php{version} helpers 2022-03-17 21:21:05 +01:00
25c7004368 🔥 Remove impossible functionality 2022-03-17 20:20:23 +01:00
02ba57cd64 🏗 TODO: Automatic isolated PHP handling 2022-03-17 19:54:13 +01:00
c2dc6302c0 ️ Fix performance issue with async 2022-03-17 19:18:48 +01:00
af9f30a123 ️ Fix performance bottleneck related to view drawing 2022-03-17 18:38:26 +01:00
28c5754800 🐛 Disable www.conf file when switching versions (#152) 2022-03-17 18:03:00 +01:00
48c1d48573 👌 Add support for Typo3 (#153) 2022-03-17 17:49:10 +01:00
582bf0e12c 🐛 Fix storyboard 2022-03-17 17:48:50 +01:00
46b30bbff4 🐛 Ensure isolation is available 2022-03-16 23:36:53 +01:00
372011ca08 Add site isolation option to context menu 2022-03-16 23:30:26 +01:00
7255792910 ♻️ WIP: Various changes 2022-03-16 22:17:17 +01:00
0c96b11b05 ♻️ WIP: Modified layout 2022-03-16 19:40:01 +01:00
ea4da12d3b ♻️ WIP: Cell rendering in site list 2022-03-16 19:15:57 +01:00
8419ebad10 👌 Adjust internal switcher 2022-03-16 18:04:45 +01:00
09a5cf836a 📝 Added TODO comments 2022-03-15 22:40:40 +01:00
1a1a53b472 ♻️ Preliminary refactor for Valet 3.0 (#148) 2022-03-15 22:39:46 +01:00
a8bad8447a 👌 isolatedVersion as a lazy property 2022-03-15 18:27:38 +01:00
ca8f5a8fbe Speed up nginx parsing 2022-03-15 18:16:28 +01:00
a0e7aec228 Parse nginx files + tests 2022-03-14 23:35:08 +01:00
26badc759e 🏗 Conditional PHP 5.6 support 2022-03-14 21:31:30 +01:00
e21c2168ea 🏗 Add isolation icon 2022-03-14 19:39:04 +01:00
589ab3664e 🏗 Add feature flag based on Valet version 2022-03-14 19:19:14 +01:00
48b4f9b160 🏗 Added initial TODO items for #148 2022-03-14 19:13:38 +01:00
139e416c3b 🚀 Version 5.1.1
Merge branch 'dev/5.x'
2022-03-09 17:52:07 +01:00
ba4ed3b365 🔧 Prepare for maintenance release 2022-03-09 17:51:19 +01:00
06a8022265 📝 Annotate environment checks (#146) 2022-03-09 16:41:47 +01:00
3b297e07dc 🐛 Adjust the order of startup checks (#146) 2022-03-09 16:19:41 +01:00
68fa8e523e 👌 Fix string that made it into 5.1 2022-03-03 14:49:33 +01:00
768bf06a9d 🚀 Version 5.1
Merge branch 'dev/5.x'
2022-03-02 19:53:52 +01:00
6a8d66758a 🔧 Build 715 for the final 5.1 release 2022-03-02 19:52:24 +01:00
078e6e6f23 📝 Update README with Raycast extension 2022-03-01 09:32:01 +01:00
3f80bfb641 👌 Detect binary paths (#144)
This change introduces binary detection to the app. PHP Monitor does not
rely on the user's PATH because the output of a user's terminal can
cause issues, so we will scan the two common locations for the Composer
binary file. The text in the alert has been modified to accommodate the
change (you could still not have Composer installed).
2022-02-26 18:24:02 +01:00
a34389c3a9 🔧 New dev build (RC) 2022-02-23 14:07:14 +01:00
692d3c143f ♻️ Refactor Composer window logic 2022-02-23 13:39:49 +01:00
bc86a45925 Run tests in isolation 2022-02-23 13:03:04 +01:00
2a412b794a Fix tests 2022-02-22 21:16:50 +01:00
bf673263d8 👌 Extract method 2022-02-22 21:16:17 +01:00
ce498d3019 ♻️ Cleanup 2022-02-22 20:49:13 +01:00
e398f089af ♻️ Refactor Valet structure, add #143 2022-02-22 20:39:35 +01:00
e8ba24e48b 👌 Improved alerts 2022-02-20 16:39:06 +01:00
e0f40be188 🐛 Fix issue with "Fix Homebrew Permissions" 2022-02-20 16:33:56 +01:00
42848764cf 👌 New icon for DEV builds 2022-02-20 16:08:04 +01:00
46ac0c339c 👌 Warn about custom TLD (#126) 2022-02-20 15:46:51 +01:00
a0fe68f3ab 👌 Constants as struct 2022-02-20 15:29:24 +01:00
c952c3d031 👌 Link to FAQ 2022-02-20 15:29:15 +01:00
c05cdeda72 👌 Offer to switch back to prev version (#141) 2022-02-20 15:12:30 +01:00
ae7b285eb0 🐛 Fix delay due to use of async 2022-02-20 14:26:56 +01:00
6b3c562af2 🐛 Fixes full PHP version (#142) 2022-02-20 13:48:37 +01:00
e3ae878cae 👌 Various alerts updated 2022-02-18 22:01:05 +01:00
dd43c94e6e 👌 Handle errors 2022-02-18 20:38:40 +01:00
0e8fe1fcfb 👌 Updated all environment alerts 2022-02-18 20:13:43 +01:00
293b7f564e 🏗 Rearrange button order 2022-02-17 19:45:56 +01:00
634ffb4c57 🏗 Preparing for additional refactoring 2022-02-17 19:23:23 +01:00
9468a2e9f8 🚩 Break compilation
Now missing labels will come up as Compiler Errors. This, along with the
deprecations in the previous alert should allow me to replace all the
alerts everywhere.
2022-02-17 19:21:03 +01:00
921ecd99d6 👌 Improve BetterAlert performance notices 2022-02-17 19:19:02 +01:00
d06f92051d 👌 Improved alert for sponsor encouragement 2022-02-17 19:07:20 +01:00
97d68f89f1 👌 Animate custom modal, fix constraints 2022-02-17 18:43:48 +01:00
115863f1ee 👌 Improve initial alert 2022-02-17 09:59:50 +01:00
bc27a4d25a 👌 Cleanup 2022-02-17 00:14:40 +01:00
5a0b2f319b 👌 Use BetterAlert API 2022-02-16 23:51:06 +01:00
50f003a2bd New notice alert API 2022-02-16 22:42:43 +01:00
bc0b50f5bf 👌 Fixed UI and added new notice view 2022-02-16 21:11:33 +01:00
dd20b25900 📝 Add issue templates 2022-02-14 13:56:01 +01:00
d0469467ac 📝 Add issue templates (#138) 2022-02-14 13:55:16 +01:00
61427ec505 👌 Cleanup App.busy -> PhpEnv.shared.isBusy 2022-02-12 15:08:00 +01:00
6cd1d78572 👌 Tweak logger verbosity 2022-02-12 14:58:49 +01:00
0ad5049785 Add separator to Log 2022-02-12 14:52:10 +01:00
b5c1960260 👌 Async / await support for loading services 2022-02-12 14:47:29 +01:00
e6fbe7c4a4 👀 First attempt at using async code in Swift 2022-02-12 13:17:17 +01:00
537536d443 👌 Improve initial logging during startup 2022-02-11 23:51:58 +01:00
f702d14749 👌 Move check list to bottom 2022-02-11 23:39:43 +01:00
74bb544f3c ♻️ Refactor (part 2) 2022-02-11 23:37:44 +01:00
dae47e3779 ♻️ Refactor startup procedure 2022-02-11 23:24:38 +01:00
dc91d0e00c 👌 Path based on architecture (#134) 2022-02-11 18:58:07 +01:00
6187eb3e4e 👌 Communicate why Fix My Valet is disabled (#135)
A quick 5-minute fix.

As raised in #135, it is not super obvious why Fix My Valet might be
disabled. From now on, Fix My Valet is now always enabled, but it might
throw up an alert if the required formula is missing.
2022-02-11 14:19:52 +01:00
0fa6d337f2 👌 Wrote down some notes during Deep Dive 2022-02-09 22:53:37 +01:00
a10e1cad11 🔥 Remove RELEASE.md (and move it to DEVELOPER.md) 2022-02-09 21:49:30 +01:00
7c252deede 📝 Update version to not specify minor version 2022-02-09 21:38:40 +01:00
224c5c4fe2 📝 Fix developers doc link (#133) 2022-02-09 21:33:38 +01:00
e945b5fe94 👌 Add version to PhpSwitcherDelegate 2022-02-09 21:32:55 +01:00
23e534c5c9 📝 Fix developers doc link (#133) 2022-02-09 21:31:50 +01:00
dcddf74830 🔧 New dev build 2022-02-08 23:51:16 +01:00
9baf69394e 🚩 Re-enabled "Restore Homebrew Permissions" 2022-02-08 23:50:45 +01:00
118 changed files with 4112 additions and 1183 deletions

33
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,33 @@
---
name: Bug report
about: Something going wrong? File a bug report!
title: ''
labels: bug
assignees: nicoverbruggen
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Required information**
- Did you consult the FAQ in the README? [yes/no]
- Did you try "Fix My Valet"? [yes/no]
- OS: [e.g. macOS Monterey]
- PHP Monitor version [e.g. v5.0.1]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,22 @@
---
name: Feature request
about: Suggest an enhancement.
title: ''
labels: enhancement
assignees: nicoverbruggen
---
_Enhancement requests that are not immediately approved will be moved to a discussion instead._
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -13,6 +13,20 @@ Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive. If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
## 🚀 Release procedure
1. Merge into `main`
2. Create tag
3. Add changes to changelog + update security document
4. Archive
5. Notarize and prepare for own distribution
6. After notarization, export .app
7. Create zipped version
8. Calculate SHA256: `openssl dgst -sha256 phpmon.zip`
9. Upload to GitHub and add to tagged release
10. Update Cask with new version + hash
11. Check new version can be installed via Cask
## 🐛 Symbolication of crashes ## 🐛 Symbolication of crashes
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs. If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 Nico Verbruggen Copyright (c) 2019-2022 Nico Verbruggen
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 52; objectVersion = 50;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -11,6 +11,16 @@
5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; }; 5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; }; 54B48B5F275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
54B48B60275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; }; 54B48B60275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
54D9E0B227E4F51E003B9AD9 /* HotKeysController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AC27E4F51E003B9AD9 /* HotKeysController.swift */; };
54D9E0B327E4F51E003B9AD9 /* HotKeysController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AC27E4F51E003B9AD9 /* HotKeysController.swift */; };
54D9E0B427E4F51E003B9AD9 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AD27E4F51E003B9AD9 /* Key.swift */; };
54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AD27E4F51E003B9AD9 /* Key.swift */; };
54D9E0B627E4F51E003B9AD9 /* HotKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AE27E4F51E003B9AD9 /* HotKey.swift */; };
54D9E0B727E4F51E003B9AD9 /* HotKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AE27E4F51E003B9AD9 /* HotKey.swift */; };
54D9E0B827E4F51E003B9AD9 /* KeyCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AF27E4F51E003B9AD9 /* KeyCombo.swift */; };
54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AF27E4F51E003B9AD9 /* KeyCombo.swift */; };
54D9E0BA27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0B027E4F51E003B9AD9 /* ModifierFlagsExtension.swift */; };
54D9E0BB27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0B027E4F51E003B9AD9 /* ModifierFlagsExtension.swift */; };
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; }; 54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
54FCFD26276C883F004CE748 /* SelectPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */; }; 54FCFD26276C883F004CE748 /* SelectPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */; };
54FCFD27276C883F004CE748 /* SelectPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */; }; 54FCFD27276C883F004CE748 /* SelectPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */; };
@ -28,10 +38,13 @@
C4068CA827B07A1300544CD5 /* 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 */; }; C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */; };
C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */; }; C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */; };
C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */; };
C4080FF727BD8C6400BF2C6B /* BetterAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */; };
C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */; };
C4080FFB27BD956700BF2C6B /* BetterAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */; };
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; }; C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; }; C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.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 */; }; C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
C40C7F1F2772136000DDDCDC /* 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 */; }; C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; };
@ -49,6 +62,10 @@
C417DC75277614690015E6EE /* 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 */; }; C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; };
C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; }; C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; };
C41C02A627E60D7A009F26CB /* SiteScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C02A527E60D7A009F26CB /* SiteScanner.swift */; };
C41C02A927E61A65009F26CB /* ValetSite+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C02A827E61A65009F26CB /* ValetSite+Fake.swift */; };
C41C02AA27E61CA3009F26CB /* SiteScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C02A527E60D7A009F26CB /* SiteScanner.swift */; };
C41C02AB27E61CB3009F26CB /* ValetSite+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C02A827E61A65009F26CB /* ValetSite+Fake.swift */; };
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; }; C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; }; C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; }; C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
@ -62,11 +79,24 @@
C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; }; C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; };
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; }; C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; }; C42759682627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */; };
C42CFB1627DFDE7900862737 /* nicoverbruggen.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1527DFDE7900862737 /* nicoverbruggen.test */; };
C42CFB1827DFDFDC00862737 /* nicoverbruggen_isolated.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1727DFDFDC00862737 /* nicoverbruggen_isolated.test */; };
C42CFB1A27DFE8BD00862737 /* NginxConfigParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42CFB1927DFE8BD00862737 /* NginxConfigParserTest.swift */; };
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; }; C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; };
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; }; C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; };
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; }; C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; };
C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; }; C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; };
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */; }; C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */; };
C44067F527E2582B0045BD4E /* SiteListNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F427E2582B0045BD4E /* SiteListNameCell.swift */; };
C44067F727E258410045BD4E /* SiteListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* SiteListPhpCell.swift */; };
C44067F927E2585E0045BD4E /* SiteListTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F827E2585E0045BD4E /* SiteListTypeCell.swift */; };
C44067FB27E25FD70045BD4E /* SiteListTLSCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067FA27E25FD70045BD4E /* SiteListTLSCell.swift */; };
C449B4F027EE7FB800C47E8A /* SiteListTLSCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067FA27E25FD70045BD4E /* SiteListTLSCell.swift */; };
C449B4F127EE7FC200C47E8A /* SiteListNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F427E2582B0045BD4E /* SiteListNameCell.swift */; };
C449B4F227EE7FC400C47E8A /* SiteListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* SiteListPhpCell.swift */; };
C449B4F327EE7FC600C47E8A /* SiteListTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F827E2585E0045BD4E /* SiteListTypeCell.swift */; };
C449B4F427EE7FC800C47E8A /* SiteListKindCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC51FB27E27F47008528CA /* SiteListKindCell.swift */; };
C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* ProgressWindow.swift */; }; C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* ProgressWindow.swift */; };
C44C198E276E3A1C0072762D /* 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 */; }; C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C44C1990276E44CB0072762D /* ProgressWindow.storyboard */; };
@ -79,8 +109,7 @@
C464ADAD275A7A3F003FCD53 /* 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 */; }; C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* SiteListVC.swift */; };
C464ADB0275A7A6A003FCD53 /* SiteListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* SiteListVC.swift */; }; C464ADB0275A7A6A003FCD53 /* SiteListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* SiteListVC.swift */; };
C464ADB2275A87CA003FCD53 /* SiteListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* SiteListCell.swift */; }; C464ADB2275A87CA003FCD53 /* SiteListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* SiteListCellProtocol.swift */; };
C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* SiteListCell.swift */; };
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; }; C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; }; C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; }; C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
@ -102,17 +131,17 @@
C4927F0C27B2DFC200C55AFD /* 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 */; }; C493084A279F331F009C240B /* AddSiteVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4930849279F331F009C240B /* AddSiteVC.swift */; };
C493084B279F331F009C240B /* 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 */; }; C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; }; C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49E171E27A5736E00787921 /* PMServicesView.swift */; }; C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49E171E27A5736E00787921 /* PMServicesView.swift */; };
C4AC51FC27E27F47008528CA /* SiteListKindCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC51FB27E27F47008528CA /* SiteListKindCell.swift */; };
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.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 */; }; 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 */; }; C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */; }; C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */; };
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; }; C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; };
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; }; C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; };
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F7C275454A900D44ED0 /* ValetTest.swift */; }; C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */; };
C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; }; C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; };
C4B5635F276AB09000F12CCB /* 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 */; }; C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */; };
@ -128,6 +157,9 @@
C4B97B79275CF1B5003F3378 /* 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 */; }; 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 */; }; C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; };
C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */; };
C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C1019A27C65C6F001FACC2 /* Process.swift */; };
C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C1019A27C65C6F001FACC2 /* Process.swift */; };
C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.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 */; }; C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; };
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
@ -137,19 +169,27 @@
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; }; C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; };
C4CCBA6D275C567B008C7055 /* 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 */; }; 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 */; }; C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */; };
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.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 */; }; C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */; };
C4D5CFCA27E0F9CD00035329 /* NginxConfigParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigParser.swift */; };
C4D5CFCB27E0F9CD00035329 /* NginxConfigParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigParser.swift */; };
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; }; C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; }; C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
C4D936C927E3EB6100BD69FE /* PhpHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D936C827E3EB6100BD69FE /* PhpHelper.swift */; };
C4D936CA27E3EB6100BD69FE /* PhpHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D936C827E3EB6100BD69FE /* PhpHelper.swift */; };
C4D936CB27E3EE4A00BD69FE /* SiteListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* SiteListCellProtocol.swift */; };
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; }; C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
C4D9ADC0277610E1007277F4 /* 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 */; }; C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4D9ADC9277611A0007277F4 /* 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 */; }; C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; };
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; };
C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* ValetSite.swift */; };
C4E4404727C56F4700D225E1 /* ValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* ValetSite.swift */; };
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; }; C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; };
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; }; 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 */; }; C4EC1E73279DFCF40010F296 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; }; C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A627708B9E001DF387 /* PMHeaderView.swift */; }; C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A627708B9E001DF387 /* PMHeaderView.swift */; };
@ -208,6 +248,13 @@
5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = "<group>"; }; 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>"; }; 5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
54B48B5E275F66AE006D90C5 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; }; 54B48B5E275F66AE006D90C5 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
54D9E0AC27E4F51E003B9AD9 /* HotKeysController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKeysController.swift; sourceTree = "<group>"; };
54D9E0AD27E4F51E003B9AD9 /* Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Key.swift; sourceTree = "<group>"; };
54D9E0AE27E4F51E003B9AD9 /* HotKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKey.swift; sourceTree = "<group>"; };
54D9E0AF27E4F51E003B9AD9 /* KeyCombo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyCombo.swift; sourceTree = "<group>"; };
54D9E0B027E4F51E003B9AD9 /* ModifierFlagsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifierFlagsExtension.swift; sourceTree = "<group>"; };
54D9E0BF27E4F5D9003B9AD9 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
54D9E0C027E4F5E9003B9AD9 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SelectPreferenceView.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>"; }; 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>"; }; 54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HotkeyPreferenceView.xib; sourceTree = "<group>"; };
@ -217,6 +264,8 @@
C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CheckboxPreferenceView.xib; 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>"; }; 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>"; }; C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarIcons.swift; sourceTree = "<group>"; };
C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlert.swift; sourceTree = "<group>"; };
C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlertVC.swift; sourceTree = "<group>"; };
C40C7F1D2772136000DDDCDC /* PhpEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpEnv.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>"; }; 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>"; }; C40C7F2F27722E8D00DDDCDC /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
@ -227,6 +276,8 @@
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPER.md; 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>"; }; 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>"; }; C4188988275FE8CB001EF227 /* Filesystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filesystem.swift; sourceTree = "<group>"; };
C41C02A527E60D7A009F26CB /* SiteScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteScanner.swift; sourceTree = "<group>"; };
C41C02A827E61A65009F26CB /* ValetSite+Fake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValetSite+Fake.swift"; sourceTree = "<group>"; };
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 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>"; }; C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -240,17 +291,25 @@
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.swift"; sourceTree = "<group>"; }; C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.swift"; sourceTree = "<group>"; };
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; 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>"; }; C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = "<group>"; };
C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+FixMyValet.swift"; sourceTree = "<group>"; };
C42CFB1527DFDE7900862737 /* nicoverbruggen.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = nicoverbruggen.test; sourceTree = "<group>"; };
C42CFB1727DFDFDC00862737 /* nicoverbruggen_isolated.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = nicoverbruggen_isolated.test; sourceTree = "<group>"; };
C42CFB1927DFE8BD00862737 /* NginxConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigParserTest.swift; sourceTree = "<group>"; };
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.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>"; }; 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>"; }; 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>"; }; C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewJsonParserTest.swift; sourceTree = "<group>"; };
C44067F427E2582B0045BD4E /* SiteListNameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListNameCell.swift; sourceTree = "<group>"; };
C44067F627E258410045BD4E /* SiteListPhpCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListPhpCell.swift; sourceTree = "<group>"; };
C44067F827E2585E0045BD4E /* SiteListTypeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteListTypeCell.swift; sourceTree = "<group>"; };
C44067FA27E25FD70045BD4E /* SiteListTLSCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteListTLSCell.swift; sourceTree = "<group>"; };
C44C198C276E3A1C0072762D /* ProgressWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressWindow.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; C464ADB1275A87CA003FCD53 /* SiteListCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListCellProtocol.swift; sourceTree = "<group>"; };
C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; }; C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; }; C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = "<group>"; }; C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = "<group>"; };
@ -269,11 +328,12 @@
C4930849279F331F009C240B /* AddSiteVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteVC.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>"; }; C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = "<group>"; };
C49E171E27A5736E00787921 /* PMServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMServicesView.swift; sourceTree = "<group>"; }; C49E171E27A5736E00787921 /* PMServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMServicesView.swift; sourceTree = "<group>"; };
C4AC51FB27E27F47008528CA /* SiteListKindCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteListKindCell.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.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>"; }; 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>"; }; C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigParserTest.swift; sourceTree = "<group>"; };
C4AF9F792754499000D44ED0 /* Valet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.swift; sourceTree = "<group>"; }; C4AF9F792754499000D44ED0 /* Valet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.swift; sourceTree = "<group>"; };
C4AF9F7C275454A900D44ED0 /* ValetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetTest.swift; sourceTree = "<group>"; }; C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetVersionExtractorTest.swift; sourceTree = "<group>"; };
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.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>"; }; 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>"; }; C4B5853B2770FE3900DA4FBE /* Paths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
@ -282,23 +342,27 @@
C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MenuOutlets.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>"; }; 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>"; }; C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+GlobalHotkey.swift"; sourceTree = "<group>"; };
C4C1019A27C65C6F001FACC2 /* Process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Process.swift; sourceTree = "<group>"; };
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Startup.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerWindow.swift; sourceTree = "<group>"; };
C4D5CFC927E0F9CD00035329 /* NginxConfigParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigParser.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.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>"; }; C4D89BC52783C99400A02B68 /* ComposerJson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerJson.swift; sourceTree = "<group>"; };
C4D936C827E3EB6100BD69FE /* PhpHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpHelper.swift; sourceTree = "<group>"; };
C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpSwitcher.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>"; }; 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>"; }; C4DEB7D327A5D60B00834718 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = "<group>"; };
C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWindowExtension.swift; sourceTree = "<group>"; };
C4E4404527C56F4700D225E1 /* ValetSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetSite.swift; sourceTree = "<group>"; };
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; C4EE55A627708B9E001DF387 /* PMHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMHeaderView.swift; sourceTree = "<group>"; };
@ -309,7 +373,6 @@
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.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>"; }; 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>"; }; 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; }; 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>"; }; C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; }; C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
@ -325,7 +388,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C4998F0626175E7200B2526E /* HotKey in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -367,6 +429,27 @@
path = PHP; path = PHP;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
54D9E0AB27E4F502003B9AD9 /* HotKey */ = {
isa = PBXGroup;
children = (
54D9E0BF27E4F5D9003B9AD9 /* LICENSE */,
54D9E0AE27E4F51E003B9AD9 /* HotKey.swift */,
54D9E0AC27E4F51E003B9AD9 /* HotKeysController.swift */,
54D9E0AD27E4F51E003B9AD9 /* Key.swift */,
54D9E0AF27E4F51E003B9AD9 /* KeyCombo.swift */,
54D9E0B027E4F51E003B9AD9 /* ModifierFlagsExtension.swift */,
);
path = HotKey;
sourceTree = "<group>";
};
54D9E0BE27E4F5C0003B9AD9 /* Vendor */ = {
isa = PBXGroup;
children = (
54D9E0AB27E4F502003B9AD9 /* HotKey */,
);
path = Vendor;
sourceTree = "<group>";
};
54FCFD28276C88C0004CE748 /* Views */ = { 54FCFD28276C88C0004CE748 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -389,9 +472,20 @@
path = IAP; path = IAP;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C4080FF827BD955900BF2C6B /* Notice */ = {
isa = PBXGroup;
children = (
C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */,
C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */,
);
path = Notice;
sourceTree = "<group>";
};
C40C7F1C27720E1400DDDCDC /* Test Files */ = { C40C7F1C27720E1400DDDCDC /* Test Files */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C42CFB1527DFDE7900862737 /* nicoverbruggen.test */,
C42CFB1727DFDFDC00862737 /* nicoverbruggen_isolated.test */,
C4AF9F70275445FF00D44ED0 /* valet-config.json */, C4AF9F70275445FF00D44ED0 /* valet-config.json */,
C43A8A1F25D9D1D700591B77 /* brew.json */, C43A8A1F25D9D1D700591B77 /* brew.json */,
C4F30B06278E195800755FCE /* brew-services.json */, C4F30B06278E195800755FCE /* brew-services.json */,
@ -409,6 +503,7 @@
C4B5853D2770FE3900DA4FBE /* Command.swift */, C4B5853D2770FE3900DA4FBE /* Command.swift */,
C4B5853B2770FE3900DA4FBE /* Paths.swift */, C4B5853B2770FE3900DA4FBE /* Paths.swift */,
C4B5853C2770FE3900DA4FBE /* Shell.swift */, C4B5853C2770FE3900DA4FBE /* Shell.swift */,
C4C1019A27C65C6F001FACC2 /* Process.swift */,
C40C7F2F27722E8D00DDDCDC /* Logger.swift */, C40C7F2F27722E8D00DDDCDC /* Logger.swift */,
C417DC73277614690015E6EE /* Helpers.swift */, C417DC73277614690015E6EE /* Helpers.swift */,
); );
@ -420,8 +515,8 @@
children = ( children = (
C4F8C0A522D4FA41002EFE61 /* README.md */, C4F8C0A522D4FA41002EFE61 /* README.md */,
C4E713562570150F00007428 /* SECURITY.md */, C4E713562570150F00007428 /* SECURITY.md */,
C4F7807425D7F7E5000DBC97 /* RELEASE.md */,
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */, C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
54D9E0C027E4F5E9003B9AD9 /* LICENSE */,
C4E713572570151400007428 /* docs */, C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */, C41C1B3522B0097F00E7CF16 /* phpmon */,
C4F7807A25D7F84B000DBC97 /* phpmon-tests */, C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
@ -444,6 +539,7 @@
children = ( children = (
C4B5853A2770FE2500DA4FBE /* Common */, C4B5853A2770FE2500DA4FBE /* Common */,
C41E181722CB61EB0072CF09 /* Domain */, C41E181722CB61EB0072CF09 /* Domain */,
54D9E0BE27E4F5C0003B9AD9 /* Vendor */,
C41C1B3F22B0098000E7CF16 /* Info.plist */, C41C1B3F22B0098000E7CF16 /* Info.plist */,
C4232EE42612526500158FC6 /* Credits.html */, C4232EE42612526500158FC6 /* Credits.html */,
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */, C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
@ -465,6 +561,7 @@
C41E181722CB61EB0072CF09 /* Domain */ = { C41E181722CB61EB0072CF09 /* Domain */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4080FF827BD955900BF2C6B /* Notice */,
C4AF9F6B275445D300D44ED0 /* Integrations */, C4AF9F6B275445D300D44ED0 /* Integrations */,
C4B13B1D25C4915000548C3A /* App */, C4B13B1D25C4915000548C3A /* App */,
C4D9ADBD27761084007277F4 /* PHP */, C4D9ADBD27761084007277F4 /* PHP */,
@ -478,6 +575,19 @@
path = Domain; path = Domain;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C44067F327E256560045BD4E /* Cells */ = {
isa = PBXGroup;
children = (
C464ADB1275A87CA003FCD53 /* SiteListCellProtocol.swift */,
C44067FA27E25FD70045BD4E /* SiteListTLSCell.swift */,
C44067F427E2582B0045BD4E /* SiteListNameCell.swift */,
C44067F627E258410045BD4E /* SiteListPhpCell.swift */,
C44067F827E2585E0045BD4E /* SiteListTypeCell.swift */,
C4AC51FB27E27F47008528CA /* SiteListKindCell.swift */,
);
path = Cells;
sourceTree = "<group>";
};
C44C198F276E3A380072762D /* Progress */ = { C44C198F276E3A380072762D /* Progress */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -499,12 +609,12 @@
C464ADAA275A7A25003FCD53 /* SiteList */ = { C464ADAA275A7A25003FCD53 /* SiteList */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C44067F327E256560045BD4E /* Cells */,
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */, C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */,
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */, C464ADAE275A7A69003FCD53 /* SiteListVC.swift */,
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */, C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */,
C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */, C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */,
C4930849279F331F009C240B /* AddSiteVC.swift */, C4930849279F331F009C240B /* AddSiteVC.swift */,
C464ADB1275A87CA003FCD53 /* SiteListCell.swift */,
); );
path = SiteList; path = SiteList;
sourceTree = "<group>"; sourceTree = "<group>";
@ -513,10 +623,10 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */, C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */,
C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */,
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */, C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */,
C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */,
C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */, C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */,
C4CE3BB927B31F670086CA49 /* MainMenu+Composer.swift */, C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */,
C47331A1247093B7009A0597 /* StatusMenu.swift */, C47331A1247093B7009A0597 /* StatusMenu.swift */,
C48D0C9525CC80B100CC7490 /* HeaderView.swift */, C48D0C9525CC80B100CC7490 /* HeaderView.swift */,
C48D0C9925CC888B00CC7490 /* HeaderView.xib */, C48D0C9925CC888B00CC7490 /* HeaderView.xib */,
@ -538,7 +648,6 @@
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */, C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */, C4CCBA6B275C567B008C7055 /* PMWindowController.swift */,
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */, C4B5635D276AB09000F12CCB /* VersionExtractor.swift */,
C4EC1E6C279DF87A0010F296 /* Async.swift */,
); );
path = Helpers; path = Helpers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -548,6 +657,7 @@
children = ( children = (
C40C7F1D2772136000DDDCDC /* PhpEnv.swift */, C40C7F1D2772136000DDDCDC /* PhpEnv.swift */,
C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */, C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */,
C4D936C827E3EB6100BD69FE /* PhpHelper.swift */,
); );
path = "PHP Version"; path = "PHP Version";
sourceTree = "<group>"; sourceTree = "<group>";
@ -556,6 +666,10 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4AF9F792754499000D44ED0 /* Valet.swift */, C4AF9F792754499000D44ED0 /* Valet.swift */,
C4E4404527C56F4700D225E1 /* ValetSite.swift */,
C41C02A827E61A65009F26CB /* ValetSite+Fake.swift */,
C41C02A527E60D7A009F26CB /* SiteScanner.swift */,
C4D5CFC927E0F9CD00035329 /* NginxConfigParser.swift */,
); );
path = Valet; path = Valet;
sourceTree = "<group>"; sourceTree = "<group>";
@ -589,8 +703,8 @@
C4811D2322D70A4700B5F6B3 /* App.swift */, C4811D2322D70A4700B5F6B3 /* App.swift */,
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */, C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */,
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */, C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
C4EED88827A48778006D7272 /* InterAppHandler.swift */, C4EED88827A48778006D7272 /* InterAppHandler.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
); );
path = App; path = App;
sourceTree = "<group>"; sourceTree = "<group>";
@ -607,6 +721,36 @@
path = Common; path = Common;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C4C1019727C65A11001FACC2 /* Parsers */ = {
isa = PBXGroup;
children = (
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */,
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
C42CFB1927DFE8BD00862737 /* NginxConfigParserTest.swift */,
);
path = Parsers;
sourceTree = "<group>";
};
C4C1019827C65A1A001FACC2 /* Versions */ = {
isa = PBXGroup;
children = (
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */,
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */,
C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */,
);
path = Versions;
sourceTree = "<group>";
};
C4C1019927C65A4D001FACC2 /* Commands */ = {
isa = PBXGroup;
children = (
C4F7809B25D80344000DBC97 /* CommandTest.swift */,
);
path = Commands;
sourceTree = "<group>";
};
C4C8E81D276F5686003AC782 /* Watcher */ = { C4C8E81D276F5686003AC782 /* Watcher */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -626,6 +770,7 @@
C4D89BC42783C98800A02B68 /* Composer */ = { C4D89BC42783C98800A02B68 /* Composer */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */,
C4D89BC52783C99400A02B68 /* ComposerJson.swift */, C4D89BC52783C99400A02B68 /* ComposerJson.swift */,
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */, C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */,
); );
@ -673,16 +818,11 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4F7807D25D7F84B000DBC97 /* Info.plist */, C4F7807D25D7F84B000DBC97 /* Info.plist */,
C40C7F1C27720E1400DDDCDC /* Test Files */,
C4F7809B25D80344000DBC97 /* CommandTest.swift */,
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */,
C43A8A1925D9CD1000591B77 /* Utility.swift */, C43A8A1925D9CD1000591B77 /* Utility.swift */,
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */, C40C7F1C27720E1400DDDCDC /* Test Files */,
C4AF9F7C275454A900D44ED0 /* ValetTest.swift */, C4C1019927C65A4D001FACC2 /* Commands */,
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */, C4C1019827C65A1A001FACC2 /* Versions */,
C4C1019727C65A11001FACC2 /* Parsers */,
); );
path = "phpmon-tests"; path = "phpmon-tests";
sourceTree = "<group>"; sourceTree = "<group>";
@ -694,6 +834,7 @@
C46FA23E246C358E00944F05 /* StringExtension.swift */, C46FA23E246C358E00944F05 /* StringExtension.swift */,
C48D0C9225CC804200CC7490 /* XibLoadable.swift */, C48D0C9225CC804200CC7490 /* XibLoadable.swift */,
C42759662627662800093CAE /* NSMenuExtension.swift */, C42759662627662800093CAE /* NSMenuExtension.swift */,
C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -715,7 +856,6 @@
); );
name = "PHP Monitor"; name = "PHP Monitor";
packageProductDependencies = ( packageProductDependencies = (
C4998F0526175E7200B2526E /* HotKey */,
); );
productName = phpmon; productName = phpmon;
productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */; productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */;
@ -735,6 +875,8 @@
C4F7807F25D7F84B000DBC97 /* PBXTargetDependency */, C4F7807F25D7F84B000DBC97 /* PBXTargetDependency */,
); );
name = "phpmon-tests"; name = "phpmon-tests";
packageProductDependencies = (
);
productName = "phpmon-tests"; productName = "phpmon-tests";
productReference = C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */; productReference = C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */;
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
@ -754,7 +896,6 @@
}; };
C4F7807825D7F84B000DBC97 = { C4F7807825D7F84B000DBC97 = {
CreatedOnToolsVersion = 12.4; CreatedOnToolsVersion = 12.4;
TestTargetID = C41C1B3222B0097F00E7CF16;
}; };
}; };
}; };
@ -768,7 +909,6 @@
); );
mainGroup = C41C1B2A22B0097F00E7CF16; mainGroup = C41C1B2A22B0097F00E7CF16;
packageReferences = ( packageReferences = (
C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */,
); );
productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */; productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -809,12 +949,14 @@
files = ( files = (
54FCFD27276C883F004CE748 /* SelectPreferenceView.xib in Resources */, 54FCFD27276C883F004CE748 /* SelectPreferenceView.xib in Resources */,
54FCFD2E276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */, 54FCFD2E276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C42CFB1827DFDFDC00862737 /* nicoverbruggen_isolated.test in Resources */,
C4F780A825D80AE8000DBC97 /* php.ini in Resources */, C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
C4068CA527B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */, C4068CA527B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */,
C43A8A2025D9D1D700591B77 /* brew.json in Resources */, C43A8A2025D9D1D700591B77 /* brew.json in Resources */,
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */, C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */,
C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */, C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */,
C4F30B08278E195800755FCE /* brew-services.json in Resources */, C4F30B08278E195800755FCE /* brew-services.json in Resources */,
C42CFB1627DFDE7900862737 /* nicoverbruggen.test in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -827,6 +969,7 @@
files = ( files = (
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */, C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */,
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */, C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */,
C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C4B585412770FE3900DA4FBE /* Shell.swift in Sources */, C4B585412770FE3900DA4FBE /* Shell.swift in Sources */,
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */, C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */,
@ -835,23 +978,31 @@
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */, 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */, C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */,
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */, C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */,
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */, C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */,
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, 54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */,
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */, C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */,
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */, C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */, C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */, C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */,
C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */, C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */,
C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */, C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */,
C41C02A927E61A65009F26CB /* ValetSite+Fake.swift in Sources */,
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */, C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */,
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */, C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */,
C4B585442770FE3900DA4FBE /* Command.swift in Sources */, C4B585442770FE3900DA4FBE /* Command.swift in Sources */,
C44067F527E2582B0045BD4E /* SiteListNameCell.swift in Sources */,
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */, C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */, C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */,
C44067F727E258410045BD4E /* SiteListPhpCell.swift in Sources */,
C415D3B72770F294005EF286 /* Actions.swift in Sources */, C415D3B72770F294005EF286 /* Actions.swift in Sources */,
C4AC51FC27E27F47008528CA /* SiteListKindCell.swift in Sources */,
C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */, C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */,
54D9E0B827E4F51E003B9AD9 /* KeyCombo.swift in Sources */,
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */, C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */,
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */, 54B48B5F275F66AE006D90C5 /* Application.swift in Sources */,
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */, C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
@ -860,9 +1011,11 @@
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */, C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */, C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */, C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */,
54D9E0B427E4F51E003B9AD9 /* Key.swift in Sources */,
5420395F2613607600FB00FA /* Preferences.swift in Sources */, 5420395F2613607600FB00FA /* Preferences.swift in Sources */,
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */, C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */, 54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
54D9E0B227E4F51E003B9AD9 /* HotKeysController.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */, C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */, C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */,
C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */, C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
@ -875,9 +1028,10 @@
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */, C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */, C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */, C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */,
C4EC1E6D279DF87A0010F296 /* Async.swift in Sources */,
C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */, C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */,
C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */,
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */, C4EC1E73279DFCF40010F296 /* Events.swift in Sources */,
C44067FB27E25FD70045BD4E /* SiteListTLSCell.swift in Sources */,
C4927F0B27B2DFC200C55AFD /* Errors.swift in Sources */, C4927F0B27B2DFC200C55AFD /* Errors.swift in Sources */,
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */, C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */, C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
@ -889,16 +1043,23 @@
C476FF9822B0DD830098105B /* Alert.swift in Sources */, C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */, C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */, C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
C4CE3BBA27B31F670086CA49 /* MainMenu+Composer.swift in Sources */, C4D5CFCA27E0F9CD00035329 /* NginxConfigParser.swift in Sources */,
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */,
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */, C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */,
C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */, C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */,
54D9E0B627E4F51E003B9AD9 /* HotKey.swift in Sources */,
C4D936C927E3EB6100BD69FE /* PhpHelper.swift in Sources */,
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */, C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
C44067F927E2585E0045BD4E /* SiteListTypeCell.swift in Sources */,
54D9E0BA27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */,
C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */, C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */,
C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */, C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */,
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */, C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */,
C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */, C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C41C02A627E60D7A009F26CB /* SiteScanner.swift in Sources */,
C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */, C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
C464ADB2275A87CA003FCD53 /* SiteListCell.swift in Sources */, C464ADB2275A87CA003FCD53 /* SiteListCellProtocol.swift in Sources */,
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */, C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
C493084A279F331F009C240B /* AddSiteVC.swift in Sources */, C493084A279F331F009C240B /* AddSiteVC.swift in Sources */,
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */, C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */,
@ -909,9 +1070,11 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C449B4F427EE7FC800C47E8A /* SiteListKindCell.swift in Sources */,
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */, 54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */, C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */,
C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */, C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
54D9E0B727E4F51E003B9AD9 /* HotKey.swift in Sources */,
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */, C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */, 54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
C415D3B82770F294005EF286 /* Actions.swift in Sources */, C415D3B82770F294005EF286 /* Actions.swift in Sources */,
@ -919,29 +1082,38 @@
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */, C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
C493084B279F331F009C240B /* AddSiteVC.swift in Sources */, C493084B279F331F009C240B /* AddSiteVC.swift in Sources */,
C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */, C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */,
C41C02AA27E61CA3009F26CB /* SiteScanner.swift in Sources */,
C4080FFB27BD956700BF2C6B /* BetterAlertVC.swift in Sources */,
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */, C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
54D9E0BB27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */,
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */, C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
C4D5CFCB27E0F9CD00035329 /* NginxConfigParser.swift in Sources */,
C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */, C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */, C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */,
C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */, C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */,
C449B4F027EE7FB800C47E8A /* SiteListTLSCell.swift in Sources */,
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */, C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */, C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */,
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */, C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */, C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */,
C4F319C927B034A500AFF46F /* Stats.swift in Sources */, C4F319C927B034A500AFF46F /* Stats.swift in Sources */,
C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */, C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */,
54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */,
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
C40B24F527A3108B0018C7D2 /* Async.swift in Sources */,
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */, C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */,
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */, C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */, C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */,
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */, C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */,
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */, C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */,
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */, C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
C4D936CB27E3EE4A00BD69FE /* SiteListCellProtocol.swift in Sources */,
C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */, C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */, C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */,
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */, C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */,
@ -949,17 +1121,20 @@
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */, C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */,
C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */, C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */, C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */,
C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */,
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */, C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */, C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */,
C4CE3BBC27B324250086CA49 /* MainMenu+Composer.swift in Sources */, C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */,
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */, C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */,
C417DC75277614690015E6EE /* Helpers.swift in Sources */, C417DC75277614690015E6EE /* Helpers.swift in Sources */,
C4080FF727BD8C6400BF2C6B /* BetterAlert.swift in Sources */,
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */, C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
54D9E0B327E4F51E003B9AD9 /* HotKeysController.swift in Sources */,
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */, C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */, C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */,
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */, C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */, C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */,
C4D936CA27E3EB6100BD69FE /* PhpHelper.swift in Sources */,
C449B4F127EE7FC200C47E8A /* SiteListNameCell.swift in Sources */,
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */, C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, 54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */, C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
@ -969,21 +1144,27 @@
C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */, C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */,
C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */, C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */,
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */, C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C449B4F227EE7FC400C47E8A /* SiteListPhpCell.swift in Sources */,
C42CFB1A27DFE8BD00862737 /* NginxConfigParserTest.swift in Sources */,
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */, C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
C40B24F227A310770018C7D2 /* Events.swift in Sources */, C40B24F227A310770018C7D2 /* Events.swift in Sources */,
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */, C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */, C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */,
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */, C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
C4B585452770FE3900DA4FBE /* Command.swift in Sources */, C4B585452770FE3900DA4FBE /* Command.swift in Sources */,
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */, C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */,
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */, C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */, C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */, C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */,
C4E4404727C56F4700D225E1 /* ValetSite.swift in Sources */,
C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */, C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */,
C449B4F327EE7FC600C47E8A /* SiteListTypeCell.swift in Sources */,
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C41C02AB27E61CB3009F26CB /* ValetSite+Fake.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */, C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */, C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */,
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */, C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */,
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
C4B585422770FE3900DA4FBE /* Shell.swift in Sources */, C4B585422770FE3900DA4FBE /* Shell.swift in Sources */,
C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */, C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */, C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */,
@ -1143,7 +1324,8 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 610; CURRENT_PROJECT_VERSION = 761;
DEBUG = YES;
DEVELOPMENT_TEAM = 8M54J5J787; DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist; INFOPLIST_FILE = phpmon/Info.plist;
@ -1152,7 +1334,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 5.0.1; MARKETING_VERSION = 5.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -1168,7 +1350,8 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 610; CURRENT_PROJECT_VERSION = 761;
DEBUG = NO;
DEVELOPMENT_TEAM = 8M54J5J787; DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist; INFOPLIST_FILE = phpmon/Info.plist;
@ -1177,7 +1360,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 5.0.1; MARKETING_VERSION = 5.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -1188,7 +1371,6 @@
C4F7808125D7F84B000DBC97 /* Debug */ = { C4F7808125D7F84B000DBC97 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 8M54J5J787; DEVELOPMENT_TEAM = 8M54J5J787;
@ -1202,14 +1384,12 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests"; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PHP Monitor.app/Contents/MacOS/PHP Monitor";
}; };
name = Debug; name = Debug;
}; };
C4F7808225D7F84B000DBC97 /* Release */ = { C4F7808225D7F84B000DBC97 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 8M54J5J787; DEVELOPMENT_TEAM = 8M54J5J787;
@ -1223,7 +1403,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests"; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PHP Monitor.app/Contents/MacOS/PHP Monitor";
}; };
name = Release; name = Release;
}; };
@ -1258,25 +1437,6 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/soffes/HotKey";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.1.3;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
C4998F0526175E7200B2526E /* HotKey */ = {
isa = XCSwiftPackageProductDependency;
package = C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */;
productName = HotKey;
};
/* End XCSwiftPackageProductDependency section */
}; };
rootObject = C41C1B2B22B0097F00E7CF16 /* Project object */; rootObject = C41C1B2B22B0097F00E7CF16 /* Project object */;
} }

View File

@ -61,6 +61,13 @@
ReferencedContainer = "container:PHP Monitor.xcodeproj"> ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "PHPMON_MARKETING_MODE"
value = "YES"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -1,17 +1,12 @@
> 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. > 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, as this is something I make in my free time. **Thank you!** ⭐️
> You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy.<br>**Thank you!** ⭐️
<h1 align="center"><b>PHP Monitor</b> (phpmon)</h1> <p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p>
<p align="center"> **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 app</u> (consult the FAQ below with info about how to set up your environment).
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
</p>
**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>. <img src="./docs/screenshot.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot50.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/> <small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
<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)! 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,16 +14,19 @@ It's super convenient to switch between different versions of PHP. You'll even g
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more). PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
You can also add new domains as links, isolate sites, manage various services, and perform First Aid to fix all kinds of common PHP link issues.
## 🖥 System requirements ## 🖥 System requirements
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs. PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
* macOS 11 Big Sur or higher (supports macOS 12 Monterey) * macOS 11 Big Sur or higher (supports macOS 12 Monterey)
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew` * Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* The brew formula `php` has to be installed (which version is detected) * Homebrew `php` formula is installed
* Laravel Valet 2.16.2 or higher (older versions might be compatible but are not supported) * Laravel Valet 2.16 or newer (supports Valet 3)
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`._ _You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
## 🚀 How to install ## 🚀 How to install
@ -43,7 +41,13 @@ To upgrade your existing installation, run:
brew upgrade phpmon brew upgrade phpmon
(You may need to run `brew update` first in order to update the cask file if you ran a Homebrew operation recently.) (You may need to run `brew update` or `brew update-reset` first in order to update the cask file if you ran a Homebrew operation recently.)
## ⚡️ Launchers (Alfred, Raycast)
If you would like to integrate with your launcher of choice, you can also download an [Alfred workflow](https://github.com/nicoverbruggen/phpmon/raw/main/integrations/phpmon.alfredworkflow) or [Raycast extension](https://www.raycast.com/nicoverbruggen/php-monitor) that works with PHP Monitor.
The app must be running in the background for these to work, and the _Allow third-party integrations_ checkbox must be enabled in Preferences (it is by default).
## 🔑 Is the app signed & notarized? ## 🔑 Is the app signed & notarized?
@ -73,7 +77,7 @@ If you're still having issues, here's a few common questions & answers, as well
<summary><strong>Which versions of PHP are supported?</strong></summary> <summary><strong>Which versions of PHP are supported?</strong></summary>
<ul> <ul>
<li>PHP 5.6</li> <li>PHP 5.6 (only if you are running Valet 2)</li>
<li>PHP 7.0</li> <li>PHP 7.0</li>
<li>PHP 7.1</li> <li>PHP 7.1</li>
<li>PHP 7.2</li> <li>PHP 7.2</li>
@ -84,7 +88,7 @@ If you're still having issues, here's a few common questions & answers, as well
<li>PHP 8.2 (experimental)</li> <li>PHP 8.2 (experimental)</li>
</ul> </ul>
For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Constants.swift#L16) file to see which versions are supported. For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Common/Core/Constants.swift#L16) file to see which versions are supported.
</details> </details>
@ -145,6 +149,29 @@ This should install `dnsmasq` and set up Valet. Great, almost there!
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
</details> </details>
<details>
<summary><strong>I have PHP Monitor installed, and it works. I want to upgrade my PHP installations to the latest version, what's the best way to do this?</strong></summary>
It's easy to make a mistake here, and end up with an unlinked version of PHP or have versions missing from PHP Monitor.
Here's what I usually do:
* Open PHP Monitor and select **First Aid & Services** > **Restore Homebrew Permissions**.
* Close PHP Monitor after the pop-up tells you the permissions were restored.
* Run `brew update-reset`
* Run `brew upgrade`
If after this, any PHP versions are missing in PHP Monitor, please run the following for the versions that are missing:
* Run `brew uninstall php@x.x` (where `x.x` is the version)
* Run `brew cleanup` (if you get any permission issues you may need to manually clean up the folder)
* Run `brew install php@x.x` (where `x.x` is the version)
You may still need to run `brew link php` after upgrading, too.
That's it. Now start up PHP Monitor again and you should be golden!
</details>
<details> <details>
<summary><strong>PHP Monitor tells me `php` is not installed...</strong></summary> <summary><strong>PHP Monitor tells me `php` is not installed...</strong></summary>
@ -197,6 +224,12 @@ You should see an error or a warning here in the output.
Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling). Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling).
</details> </details>
<details>
<summary><strong>The option to isolate a site is disabled! What's going on?</strong></summary>
Make sure you have at least **Valet 3.0** installed, since support for isolation was added in this version of Valet. (Please note that this version of Valet drops support for PHP 5.6.)
</details>
<details> <details>
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary> <summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
@ -231,8 +264,15 @@ Since v3.4 all of the loaded .ini files are sourced to determine which extension
<details> <details>
<summary><strong>I've got two Homebrew installations on my Apple Silicon Mac, can I choose which installation to use with PHP Monitor?</strong></summary> <summary><strong>I've got two Homebrew installations on my Apple Silicon Mac, can I choose which installation to use with PHP Monitor?</strong></summary>
Not at this time, no. PHP Monitor will prefer the `/opt/homebrew` installation over the classic installation directory. If you are using PHP Monitor on an Intel machine or on an Apple Silicon machine with Rosetta enabled, PHP Monitor expects the main Homebrew binary in `/usr/local/bin/brew`.
If you are using PHP Monitor on Apple Silicon without Rosetta, PHP Monitor expects the main Homebrew binary in `/opt/homebrew/bin/brew`.
If there's an issue here, you'll get an alert at launch.
Make sure that the version of Homebrew that you are running normally is the same as the one that PHP Monitor expects. If you are on M1 hardware for example, but still using Rosetta for Homebrew, you'll need to run PHP Monitor under Rosetta as well.
PHP Monitor is a universal app and supports both architectures, so [find out here](https://support.apple.com/en-us/HT211861) how to enable Rosetta with PHP Monitor.
</details> </details>
<details> <details>
@ -267,9 +307,11 @@ You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor
</details> </details>
<details> <details>
<summary><strong>How can the app integrate with third party tools, like Alfred?</strong></summary> <summary><strong>How can the app integrate with third party tools, like Alfred or Raycast?</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`. PHP Monitor supports third party app integrations by default, and this feature is enabled in Preferences unless you disable it.
You can grab the official [Alfred workflow](https://github.com/nicoverbruggen/phpmon/raw/main/integrations/phpmon.alfredworkflow) or [Raycast extension](https://www.raycast.com/nicoverbruggen/php-monitor).
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). 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).
@ -351,13 +393,14 @@ Donations really help with the Apple Developer Program cost, and keep me motivat
## 😎 Acknowledgements ## 😎 Acknowledgements
While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks: While I did make this application during my own free time, PHP Monitor started out from various learning experiments during work hours at my employer, DIVE. I'd also like to shout out the following folks:
* My colleagues at [DIVE](https://dive.be) * My colleagues at [DIVE](https://dive.be)
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](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 * 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
* My [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen) and those who have donated
* Everyone who has left feedback and reported bugs (appreciate it!)
* Everyone in the Laravel community who shared the app (thanks!) * Everyone in the Laravel community who shared the app (thanks!)
* 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. Thank you very much for your contributions, kind words and support.
@ -373,7 +416,9 @@ In order to save power, this only happens once every 60 seconds.
This utility will detect which PHP versions you have installed via Homebrew, and then allows you to switch between them. This utility will detect which PHP versions you have installed via Homebrew, and then allows you to switch between them.
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. 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 a bit faster than Valets switcher.
If you're using Valet 3, versions of PHP-FPM required to keep isolated sites up and running will also be started or stopped as needed.
### Config change detection ### Config change detection
@ -399,4 +444,4 @@ I have done my best to annotate as much as humanly possible, and have avoided us
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. 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.
For more detailed information for developers, please see [the documentation file for developers](./DEVS.md). For more detailed information for developers, please see [the documentation file for developers](./DEVELOPER.md).

View File

@ -1,13 +0,0 @@
# Release Procedure
1. Merge into `main`
2. Create tag
3. Add changes to changelog + update security document
4. Archive
5. Notarize and prepare for own distribution
6. After notarization, export .app
7. Create zipped version
8. Calculate SHA256: `openssl dgst -sha256 phpmon.zip`
9. Upload to GitHub and add to tagged release
10. Update Cask with new version + hash
11. Check new version can be installed via Cask

View File

@ -4,9 +4,11 @@
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up): 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 | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- | ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 5.0 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 | | 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 (*) | 3.0 (2.16.2 minimum) |
_(*) Support for PHP 5.6 is only included if you are using Valet 2.x, since support for PHP 5.6 was dropped in Valet 3.0._
## Legacy versions ## Legacy versions

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docs/screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

View File

@ -0,0 +1,32 @@
//
// NginxConfigParserTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class NginxConfigParserTest: XCTestCase {
static var regularUrl: URL {
return Bundle(for: Self.self).url(forResource: "nicoverbruggen", withExtension: "test")!
}
static var isolatedUrl: URL {
return Bundle(for: Self.self).url(forResource: "nicoverbruggen_isolated", withExtension: "test")!
}
func testCanDetermineIsolation() throws {
XCTAssertNil(
NginxConfigParser(filePath: NginxConfigParserTest.regularUrl.path).isolatedVersion
)
XCTAssertEqual(
"8.1",
NginxConfigParser(filePath: NginxConfigParserTest.isolatedUrl.path).isolatedVersion
)
}
}

View File

@ -32,6 +32,7 @@ class ValetConfigParserTest: XCTestCase {
"/Users/username/.config/valet/Sites", "/Users/username/.config/valet/Sites",
"/Users/username/Sites" "/Users/username/Sites"
]) ])
XCTAssertEqual(config.defaultSite, "/Users/username/default-site")
XCTAssertEqual(config.loopback, "127.0.0.1") XCTAssertEqual(config.loopback, "127.0.0.1")
} }

View File

@ -0,0 +1,93 @@
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:443 ssl http2;
#listen 127.0.0.1:443 ssl http2; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
root /;
charset utf-8;
client_max_body_size 512M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/nicoverbruggen/.config/valet/Certificates/nicoverbruggen.test.crt";
ssl_certificate_key "/Users/nicoverbruggen/.config/valet/Certificates/nicoverbruggen.test.key";
location / {
rewrite ^ "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php" last;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/nginx-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass "unix:/Users/nicoverbruggen/.config/valet/valet.sock";
fastcgi_index "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
}
server {
listen 127.0.0.1:60;
#listen 127.0.0.1:60; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
root /;
charset utf-8;
client_max_body_size 128M;
add_header X-Robots-Tag 'noindex, nofollow, nosnippet, noarchive';
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
location / {
rewrite ^ "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php" last;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/nginx-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass "unix:/Users/nicoverbruggen/.config/valet/valet.sock";
fastcgi_index "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -0,0 +1,94 @@
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:443 ssl http2;
#listen 127.0.0.1:443 ssl http2; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
root /;
charset utf-8;
client_max_body_size 512M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/nicoverbruggen/.config/valet/Certificates/nicoverbruggen.test.crt";
ssl_certificate_key "/Users/nicoverbruggen/.config/valet/Certificates/nicoverbruggen.test.key";
location / {
rewrite ^ "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php" last;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/nginx-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# ISOLATED_PHP_VERSION=php@8.1
fastcgi_pass "unix:/Users/nicoverbruggen/.config/valet/valet81.sock";
fastcgi_index "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
}
server {
listen 127.0.0.1:60;
#listen 127.0.0.1:60; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
root /;
charset utf-8;
client_max_body_size 128M;
add_header X-Robots-Tag 'noindex, nofollow, nosnippet, noarchive';
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
location / {
rewrite ^ "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php" last;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/nginx-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass "unix:/Users/nicoverbruggen/.config/valet/valet.sock";
fastcgi_index "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -4,5 +4,6 @@
"/Users/username/.config/valet/Sites", "/Users/username/.config/valet/Sites",
"/Users/username/Sites" "/Users/username/Sites"
], ],
"loopback": "127.0.0.1" "loopback": "127.0.0.1",
"default": "/Users/username/default-site"
} }

View File

@ -22,8 +22,8 @@ class PhpVersionDetectionTest: XCTestCase {
"unrelatedphp@1.0", // should be omitted, invalid "unrelatedphp@1.0", // should be omitted, invalid
"php@5.6", "php@5.6",
"php@5.4" // should be omitted, not supported "php@5.4" // should be omitted, not supported
], checkBinaries: false) ], checkBinaries: false, generateHelpers: false)
XCTAssertEqual(outcome, ["8.0", "7.0", "5.6"]) XCTAssertEqual(outcome, ["8.0", "7.0"])
} }
} }

View File

@ -11,20 +11,24 @@ import XCTest
class PhpVersionNumberTest: XCTestCase { class PhpVersionNumberTest: XCTestCase {
func testCanDeconstructPhpVersion() throws { func testCanDeconstructPhpVersion() throws {
XCTAssertEqual(
try! PhpVersionNumber.parse("PHP 8.2.0-dev"),
PhpVersionNumber(major: 8, minor: 2, patch: 0)
)
XCTAssertEqual( XCTAssertEqual(
try! PhpVersionNumber.parse("PHP 8.1.0RC5-dev"), try! PhpVersionNumber.parse("PHP 8.1.0RC5-dev"),
PhpVersionNumber(major: 8, minor: 1, patch: 0) PhpVersionNumber(major: 8, minor: 1, patch: 0)
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumber.make(from: "8.0.11"), try! PhpVersionNumber.parse("8.0.11"),
PhpVersionNumber(major: 8, minor: 0, patch: 11) PhpVersionNumber(major: 8, minor: 0, patch: 11)
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumber.make(from: "7.4.2"), try! PhpVersionNumber.parse("7.4.2"),
PhpVersionNumber(major: 7, minor: 4, patch: 2) PhpVersionNumber(major: 7, minor: 4, patch: 2)
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumber.make(from: "7.4"), try! PhpVersionNumber.parse("7.4"),
PhpVersionNumber(major: 7, minor: 4, patch: nil) PhpVersionNumber(major: 7, minor: 4, patch: nil)
) )
XCTAssertEqual( XCTAssertEqual(

View File

@ -8,11 +8,11 @@
import XCTest import XCTest
class ValetTest: XCTestCase { class ValetVersionExtractorTest: XCTestCase {
func testDetermineValetVersion() { func testDetermineValetVersion() {
let version = valet("--version") let version = valet("--version", sudo: false)
XCTAssert(version.contains("Laravel Valet 2.")) XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3"))
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"filename" : "Default.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Default@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: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"filename" : "Isolated.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Isolated@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: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

View File

@ -41,8 +41,8 @@ class Actions {
"\(Paths.brew) services stop dnsmasq", "\(Paths.brew) services stop dnsmasq",
] ]
var cellarCommands = [ var cellarCommands = [
"chown -R \(Paths.whoami):staff \(Paths.cellarPath)/nginx", "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
"chown -R \(Paths.whoami):staff \(Paths.cellarPath)/dnsmasq" "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq"
] ]
PhpEnv.shared.availablePhpVersions.forEach { version in PhpEnv.shared.availablePhpVersions.forEach { version in
@ -50,7 +50,7 @@ class Actions {
? "php" ? "php"
: "php@\(version)" : "php@\(version)"
servicesCommands.append("\(Paths.brew) services stop \(formula)") servicesCommands.append("\(Paths.brew) services stop \(formula)")
cellarCommands.append("chown -R \(Paths.whoami):staff \(Paths.cellarPath)/\(formula)") cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
} }
let script = let script =
@ -116,31 +116,21 @@ class Actions {
Detects all currently available PHP versions, Detects all currently available PHP versions,
and unlinks each and every one of them. and unlinks each and every one of them.
This all happens in sequence, nothing runs in parallel.
After this, the brew services are also stopped, After this, the brew services are also stopped,
the latest PHP version is linked, and php + nginx are restarted. 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 If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`. extensions and/or run `composer global update`.
*/ */
public static func fixMyValet() public static func fixMyValet(completed: @escaping () -> Void)
{ {
brew("services restart dnsmasq", sudo: true) InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
brew("services restart dnsmasq", sudo: true)
PhpEnv.shared.detectPhpVersions().forEach { (version) in brew("services restart php", sudo: true)
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)" brew("services restart nginx", sudo: true)
brew("unlink php@\(version)") completed()
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 import Cocoa
class Constants { struct Constants {
/** /**
* The latest PHP version that is considered to be stable at the time of release. * The latest PHP version that is considered to be stable at the time of release.
@ -34,7 +34,7 @@ class Constants {
// STABLE RELEASES // STABLE RELEASES
// ==================== // ====================
// Versions of PHP that are stable and are supported. // Versions of PHP that are stable and are supported.
"5.6", "5.6", // only supported when Valet 2.x is active
"7.0", "7.0",
"7.1", "7.1",
"7.2", "7.2",
@ -50,10 +50,19 @@ class Constants {
// dev release. In this case, that means that the version below is detected. // dev release. In this case, that means that the version below is detected.
"8.2" "8.2"
] ]
/** struct Urls {
The URL that people can visit if they wish to help support the project.
*/ static let DonationPayment = URL(
static let DonationUrl = URL(string: "https://nicoverbruggen.be/sponsor#pay-now")! string: "https://nicoverbruggen.be/sponsor#pay-now"
)!
static let DonationPage = URL(
string: "https://nicoverbruggen.be/sponsor"
)!
static let FrequentlyAskedQuestions = URL(
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
)!
}
} }

View File

@ -9,11 +9,11 @@
// MARK: Common Shell Commands // MARK: Common Shell Commands
/** /**
Runs a `valet` command. Runs a `valet` command. Defaults to running as superuser.
*/ */
func valet(_ command: String) -> String func valet(_ command: String, sudo: Bool = true) -> String
{ {
return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true) return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
} }
/** /**
@ -35,7 +35,7 @@ func sed(file: String, original: String, replacement: String)
// Check if gsed exists; it is able to follow symlinks, // Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension // which we want to do to toggle the extension
if Shell.fileExists("\(Paths.binPath)/gsed") { if Filesystem.fileExists("\(Paths.binPath)/gsed") {
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)") Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else { } else {
Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)") Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")

View File

@ -27,25 +27,31 @@ class Log {
static func err(_ item: Any) { static func err(_ item: Any) {
if Verbosity.error.isApplicable() { if Verbosity.error.isApplicable() {
print("[ERR] \(item)") print("[E] \(item)")
} }
} }
static func warn(_ item: Any) { static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() { if Verbosity.warning.isApplicable() {
print("[WRN] \(item)") print("[W] \(item)")
} }
} }
static func info(_ item: Any) { static func info(_ item: Any) {
if Verbosity.info.isApplicable() { if Verbosity.info.isApplicable() {
print(item) print("\(item)")
} }
} }
static func perf(_ item: Any) { static func perf(_ item: Any) {
if Verbosity.performance.isApplicable() { if Verbosity.performance.isApplicable() {
print(item) print("[P] \(item)")
}
}
static func separator(as verbosity: Verbosity = .info) {
if verbosity.isApplicable() {
print("==================================")
} }
} }

View File

@ -15,15 +15,19 @@ public class Paths {
public static let shared = Paths() public static let shared = Paths()
private var baseDir : Paths.HomebrewDir internal var baseDir: Paths.HomebrewDir
private var userName : String private var userName: String
init() { init() {
baseDir = FileManager.default.fileExists(atPath: "\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr baseDir = App.architecture != "x86_64" ? .opt : .usr
userName = String(Shell.pipe("whoami").split(separator: "\n")[0]) userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
} }
public func detectBinaryPaths() {
detectComposerBinary()
}
// - MARK: Binaries // - MARK: Binaries
public static var valet: String { public static var valet: String {
@ -42,6 +46,11 @@ public class Paths {
return "\(binPath)/php-config" return "\(binPath)/php-config"
} }
// - MARK: Detected Binaries
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
public static var composer: String? = nil
// - MARK: Paths // - MARK: Paths
public static var whoami: String { public static var whoami: String {
@ -64,6 +73,21 @@ public class Paths {
return "\(shared.baseDir.rawValue)/etc" return "\(shared.baseDir.rawValue)/etc"
} }
// MARK: - Flexible Binaries
// (these can be in multiple locations, so we scan common places because)
// (PHP Monitor will not use the user's own PATH)
private func detectComposerBinary() {
if Filesystem.fileExists("/usr/local/bin/composer") {
Paths.composer = "/usr/local/bin/composer"
} else if Filesystem.fileExists("/opt/homebrew/bin/composer") {
Paths.composer = "/opt/homebrew/bin/composer"
} else {
Paths.composer = nil
Log.warn("Composer was not found.")
}
}
// MARK: - Enum // MARK: - Enum
public enum HomebrewDir: String { public enum HomebrewDir: String {

View File

@ -0,0 +1,59 @@
//
// Process.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Process {
/**
When a process is running in the background, it can send content to standard
output or standard error, just like it would in a terminal. Using `listen`
allows us to react whenever data is received by running a particular closure,
depending on which type of data is received.
*/
public func listen(
didReceiveStandardOutputData: @escaping (String) -> Void,
didReceiveStandardErrorData: @escaping (String) -> Void
) {
let outputPipe = Pipe()
let errorPipe = Pipe()
self.standardOutput = outputPipe
self.standardError = errorPipe
[
(outputPipe, didReceiveStandardOutputData),
(errorPipe, didReceiveStandardErrorData)
].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()
}
}
}
/**
After the process is done running, you'll want to stop listening.
*/
public func haltListening() {
if let pipe = self.standardOutput as? Pipe {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
if let pipe = self.standardError as? Pipe {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
}
}

View File

@ -104,15 +104,6 @@ public class Shell {
) )
} }
/**
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. Creates a new process with the correct PATH and shell.
*/ */
@ -128,42 +119,6 @@ public class Shell {
return task 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 class Output {
public let standardOutput: String public let standardOutput: String
public let errorOutput: String public let errorOutput: String

View File

@ -0,0 +1,38 @@
//
// NSWindowExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 17/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
extension NSWindow {
/**
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
*/
func shake(){
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
let frame: CGRect = self.frame
let shakeAnimation :CAKeyframeAnimation = CAKeyframeAnimation()
let shakePath = CGMutablePath()
shakePath.move( to: CGPoint(x:NSMinX(frame), y:NSMinY(frame)))
for _ in 0...numberOfShakes-1 {
shakePath.addLine(to: CGPoint(x:NSMinX(frame) - frame.size.width * vigourOfShake, y:NSMinY(frame)))
shakePath.addLine(to: CGPoint(x:NSMinX(frame) + frame.size.width * vigourOfShake, y:NSMinY(frame)))
}
shakePath.closeSubpath()
shakeAnimation.path = shakePath
shakeAnimation.duration = durationOfShake
self.animations = ["frameOrigin":shakeAnimation]
self.animator().setFrameOrigin(self.frame.origin)
}
}

View File

@ -9,24 +9,6 @@ import Cocoa
class Alert { class Alert {
public static func present(
messageText: String,
informativeText: String,
buttonTitle: String = "OK",
secondButtonTitle: String = "",
style: NSAlert.Style = .informational
) -> Bool {
let alert = NSAlert.init()
alert.alertStyle = style
alert.messageText = messageText
alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) {
alert.addButton(withTitle: secondButtonTitle)
}
return alert.runModal() == .alertFirstButtonReturn
}
public static func confirm( public static func confirm(
onWindow window: NSWindow, onWindow window: NSWindow,
messageText: String, messageText: String,
@ -36,6 +18,10 @@ class Alert {
style: NSAlert.Style = .warning, style: NSAlert.Style = .warning,
onFirstButtonPressed: @escaping (() -> Void) onFirstButtonPressed: @escaping (() -> Void)
) { ) {
if !Thread.isMainThread {
fatalError("You should always present alerts on the main thread!")
}
let alert = NSAlert.init() let alert = NSAlert.init()
alert.alertStyle = style alert.alertStyle = style
alert.messageText = messageText alert.messageText = messageText
@ -51,31 +37,4 @@ 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,
informativeText: info,
buttonTitle: "OK",
secondButtonTitle: "",
style: style
)
}
/**
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

@ -1,29 +0,0 @@
//
// 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

@ -11,7 +11,7 @@ import Foundation
class VersionExtractor { class VersionExtractor {
/** /**
This attempts to extract the version number from the command line output of Valet. This attempts to extract the version number from any given string.
*/ */
public static func from(_ string: String) -> String? { public static func from(_ string: String) -> String? {
do { do {

View File

@ -137,7 +137,7 @@ class ActivePhpInstallation {
} }
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :) // Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
return Shell.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf") return Filesystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
} }
// MARK: - Structs // MARK: - Structs

View File

@ -18,4 +18,21 @@ struct HomebrewService: Decodable, Equatable {
let status: String? let status: String?
let log_path: String? let log_path: String?
let error_log_path: String? let error_log_path: String?
public static func loadAll(
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
completion: @escaping ([HomebrewService]) -> Void
) {
DispatchQueue.global(qos: .background).async {
let data = Shell
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
.data(using: .utf8)!
let services = try! JSONDecoder()
.decode([HomebrewService].self, from: data)
.filter({ return filter.contains($0.name) })
completion(services)
}
}
} }

View File

@ -95,7 +95,7 @@ class PhpEnv {
let phpAlias = homebrewPackage.version let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate // Avoid inserting a duplicate
if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) { if (!versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php")) {
versionsOnly.append(phpAlias) versionsOnly.append(phpAlias)
} }
@ -117,13 +117,23 @@ class PhpEnv {
/** /**
Extracts valid PHP versions from an array of strings. Extracts valid PHP versions from an array of strings.
This array of strings is usually retrieved from `grep`. This array of strings is usually retrieved from `grep`.
If `generateHelpers` is set to true, after detecting
all versions, helper scripts are generated as well.
*/ */
public func extractPhpVersions( public func extractPhpVersions(
from versions: [String], from versions: [String],
checkBinaries: Bool = true checkBinaries: Bool = true,
generateHelpers: Bool = true
) -> [String] { ) -> [String] {
var output : [String] = [] var output : [String] = []
var supported = Constants.SupportedPhpVersions
if !Valet.enabled(feature: .supportForPhp56) {
supported.removeAll { $0 == "5.6" }
}
versions.filter { (version) -> Bool in versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@ // Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected) // (e.g. something-php@8.0 won't be detected)
@ -133,13 +143,17 @@ class PhpEnv {
// Only append the version if it doesn't already exist (avoid dupes), // Only append the version if it doesn't already exist (avoid dupes),
// is supported and where the binary exists (avoids broken installs) // is supported and where the binary exists (avoids broken installs)
if !output.contains(version) if !output.contains(version)
&& Constants.SupportedPhpVersions.contains(version) && supported.contains(version)
&& (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) && (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
{ {
output.append(version) output.append(version)
} }
} }
if generateHelpers {
output.forEach { PhpHelper.generate(for: $0) }
}
return output return output
} }

View File

@ -0,0 +1,60 @@
//
// PhpHelper.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 17/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpHelper {
static let keyPhrase = "This file was automatically generated by PHP Monitor."
public static func generate(for version: String) {
// Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "")
do {
let destination = "/usr/local/bin/pm\(dotless)"
if FileManager.default.fileExists(atPath: destination) {
let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) {
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor (or is unreadable). Not updating this file.")
return
}
}
// Let's follow the symlink to the PHP binary folder
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path
// The contents of the script!
let script = """
#!/bin/zsh
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
&& echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH
"""
// Write to the destination
try script.write(
to: URL(fileURLWithPath: destination),
atomically: true,
encoding: String.Encoding.utf8
)
// Make sure the file is executable
Shell.run("chmod +x \(destination)")
} catch {
print(error)
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
}
}
}

View File

@ -13,7 +13,7 @@ public struct PhpVersionNumberCollection: Equatable {
public static func make(from versions: [String]) -> Self { public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection( return PhpVersionNumberCollection(
versions: versions.map { PhpVersionNumber.make(from: $0)! } versions: versions.map { try! PhpVersionNumber.parse($0) }
) )
} }
@ -96,6 +96,12 @@ public struct PhpVersionNumber: Equatable {
let minor: Int let minor: Int
let patch: Int? let patch: Int?
public func toString() -> String {
return self.patch == nil
? "\(major).\(minor)"
: "\(major).\(minor).\(patch!)"
}
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int { public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999) return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
} }
@ -111,7 +117,7 @@ public struct PhpVersionNumber: Equatable {
case greaterThanOrEqual = #"^>=(?<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"# case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
// TODO: (5.1) Handle these cases (even though I suspect these are uncommon) // TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
/* /*
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"# case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"# case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#

View File

@ -10,7 +10,7 @@ import Foundation
class PhpInstallation { class PhpInstallation {
var longVersion: PhpVersionNumber var versionNumber: PhpVersionNumber
/** /**
In order to determine details about a PHP installation, well simply run `php-config --version` In order to determine details about a PHP installation, well simply run `php-config --version`
@ -19,9 +19,9 @@ class PhpInstallation {
init(_ version: String) { init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config" let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
self.longVersion = PhpVersionNumber.make(from: version)! self.versionNumber = PhpVersionNumber.make(from: version)!
if Shell.fileExists(phpConfigExecutablePath) { if Filesystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute( let longVersionString = Command.execute(
path: phpConfigExecutablePath, path: phpConfigExecutablePath,
arguments: ["--version"] arguments: ["--version"]
@ -29,8 +29,7 @@ class PhpInstallation {
// The parser should always work, or the string has to be very unusual. // 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. // 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.versionNumber = try! PhpVersionNumber.parse(longVersionString)
self.longVersion = try! PhpVersionNumber.parse(longVersionString)
} }
} }

View File

@ -24,20 +24,26 @@ class InternalSwitcher: PhpSwitcher {
{ {
Log.info("Switching to \(version), unlinking all versions...") Log.info("Switching to \(version), unlinking all versions...")
let isolated = Valet.shared.sites.filter { site in
site.isolatedPhpVersion != nil
}.map { site in
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
}
var versions: Set<String> = [version]
if (Valet.enabled(feature: .isolatedSites)) {
versions = versions.union(isolated)
}
let group = DispatchGroup() let group = DispatchGroup()
PhpEnv.shared.availablePhpVersions.forEach { (available) in PhpEnv.shared.availablePhpVersions.forEach { (available) in
group.enter() group.enter()
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let formula = (available == PhpEnv.brewPhpVersion) self.disableDefaultPhpFpmPool(available)
? "php" : "php@\(available)" self.stopPhpVersion(available)
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
Log.perf("Unlinked and stopped services for \(formula)")
group.leave() group.leave()
} }
} }
@ -46,16 +52,62 @@ class InternalSwitcher: PhpSwitcher {
Log.info("All versions have been unlinked!") Log.info("All versions have been unlinked!")
Log.info("Linking the new version!") Log.info("Linking the new version!")
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)" for formula in versions {
brew("link \(formula) --overwrite --force") self.startPhpVersion(formula, primary: (version == formula))
brew("services start \(formula)", sudo: true) }
Log.info("Restarting nginx, just to be sure!") Log.info("Restarting nginx, just to be sure!")
brew("services restart nginx", sudo: true) brew("services restart nginx", sudo: true)
Log.info("The new version has been linked!") Log.info("The new version(s) have been linked!")
completion() completion()
} }
} }
private func disableDefaultPhpFpmPool(_ version: String) {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileManager.default.fileExists(atPath: pool) {
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
let existing = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf")!
let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
do {
if (FileManager.default.fileExists(atPath: new.path)) {
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), cleaning up so the newer `www.conf` can be moved again.")
try FileManager.default.removeItem(at: new)
}
try FileManager.default.moveItem(at: existing, to: new)
Log.info("Success: A default `www.conf` file was disabled for PHP \(version).")
} catch {
Log.err(error)
}
}
}
private func stopPhpVersion(_ version: String) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)")
}
private func startPhpVersion(_ version: String, primary: Bool) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
if (primary) {
Log.info("\(formula) is the primary formula, linking and starting services...")
brew("link \(formula) --overwrite --force")
} else {
Log.info("\(formula) is an isolated PHP version, starting services only...")
}
brew("services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "")
Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
}
}
} }

View File

@ -10,9 +10,9 @@ import Foundation
protocol PhpSwitcherDelegate: AnyObject { protocol PhpSwitcherDelegate: AnyObject {
func switcherDidStartSwitching(to: String) func switcherDidStartSwitching(to version: String)
func switcherDidCompleteSwitch(to: String) func switcherDidCompleteSwitch(to version: String)
} }

View File

@ -6,7 +6,6 @@
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2021 Nico Verbruggen. All rights reserved.
// //
import HotKey
import Cocoa import Cocoa
extension App { extension App {

View File

@ -6,7 +6,6 @@
// //
import Cocoa import Cocoa
import HotKey
class App { class App {
@ -22,9 +21,18 @@ class App {
return "\(version) (\(build))" return "\(version) (\(build))"
} }
/** Whether the app is busy doing something. Used to determine what UI to display. */ static var architecture: String {
static var busy: Bool { var systeminfo = utsname()
return PhpEnv.shared.isBusy uname(&systeminfo)
let machine = withUnsafeBytes(of: &systeminfo.machine) {bufPtr->String in
let data = Data(bufPtr)
if let lastIndex = data.lastIndex(where: {$0 != 0}) {
return String(data: data[0...lastIndex], encoding: .isoLatin1)!
} else {
return String(data: data, encoding: .isoLatin1)!
}
}
return machine
} }
// MARK: Variables // MARK: Variables

View File

@ -64,10 +64,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
*/ */
override init() { override init() {
logger.verbosity = .info logger.verbosity = .info
Log.info("==================================") #if DEBUG
logger.verbosity = .performance
#endif
Log.separator(as: .info)
Log.info("PHP MONITOR by Nico Verbruggen") Log.info("PHP MONITOR by Nico Verbruggen")
Log.info("Version \(App.version)") Log.info("Version \(App.version)")
Log.info("==================================") Log.separator(as: .info)
self.sharedShell = Shell.user self.sharedShell = Shell.user
self.state = App.shared self.state = App.shared
self.menu = MainMenu.shared self.menu = MainMenu.shared
@ -91,7 +94,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// Make sure notifications will work // Make sure notifications will work
setupNotifications() setupNotifications()
// Make sure the menu performs its initial checks // Make sure the menu performs its initial checks
menu.startup() Task { await menu.startup() }
} }
} }

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<capability name="Image references" minToolsVersion="12.0"/> <capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/> <capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
@ -387,10 +387,10 @@
<window key="window" title="Domains" subtitle="Linked &amp; Parked" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1"> <window key="window" title="Domains" subtitle="Linked &amp; Parked" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/> <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="425" y="461" width="550" height="263"/> <rect key="contentRect" x="425" y="461" width="600" height="263"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/> <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="uVx-Da-x4I"> <view key="contentView" id="uVx-Da-x4I">
<rect key="frame" x="0.0" y="0.0" width="550" height="263"/> <rect key="frame" x="0.0" y="0.0" width="600" height="263"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</view> </view>
<toolbar key="toolbar" implicitIdentifier="594015E3-8428-4926-9341-4B8CE4C7E373" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconOnly" sizeMode="regular" id="OOz-oZ-vlN"> <toolbar key="toolbar" implicitIdentifier="594015E3-8428-4926-9341-4B8CE4C7E373" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconOnly" sizeMode="regular" id="OOz-oZ-vlN">
@ -407,12 +407,12 @@
<action selector="pressedReload:" target="8Ec-9q-82s" id="fLc-bD-oYQ"/> <action selector="pressedReload:" target="8Ec-9q-82s" id="fLc-bD-oYQ"/>
</connections> </connections>
</toolbarItem> </toolbarItem>
<searchToolbarItem implicitItemIdentifier="629F0782-3C5F-4CD0-9396-3A054A422180" label="Search" paletteLabel="Search" visibilityPriority="1001" id="Q7Z-fw-lB9"> <searchToolbarItem implicitItemIdentifier="7C834FBE-7118-4082-A09F-7CBECEC1356A" label="Search" paletteLabel="Search" visibilityPriority="1001" id="G2g-jS-RVc">
<nil key="toolTip"/> <nil key="toolTip"/>
<searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="oWA-TH-Pm7"> <searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/> <rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="3NO-6x-aLc"> <searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="vp9-vH-goQ">
<font key="font" usesAppearanceFont="YES"/> <font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -423,7 +423,7 @@
<defaultToolbarItems> <defaultToolbarItems>
<toolbarItem reference="GsC-ra-40U"/> <toolbarItem reference="GsC-ra-40U"/>
<toolbarItem reference="YtK-vM-5y7"/> <toolbarItem reference="YtK-vM-5y7"/>
<searchToolbarItem reference="Q7Z-fw-lB9"/> <searchToolbarItem reference="G2g-jS-RVc"/>
</defaultToolbarItems> </defaultToolbarItems>
</toolbar> </toolbar>
<connections> <connections>
@ -431,13 +431,13 @@
</connections> </connections>
</window> </window>
<connections> <connections>
<outlet property="searchToolbarItem" destination="Q7Z-fw-lB9" id="J5o-oh-VhO"/> <outlet property="searchToolbarItem" destination="G2g-jS-RVc" id="xlc-qe-k7e"/>
<segue destination="JZI-Vd-9oq" kind="relationship" relationship="window.shadowedContentViewController" id="9Gy-Gw-hPH"/> <segue destination="JZI-Vd-9oq" kind="relationship" relationship="window.shadowedContentViewController" id="9Gy-Gw-hPH"/>
</connections> </connections>
</windowController> </windowController>
<customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-374" y="758"/> <point key="canvasLocation" x="-374" y="746"/>
</scene> </scene>
<!--Window Controller--> <!--Window Controller-->
<scene sceneID="HTI-x5-rOp"> <scene sceneID="HTI-x5-rOp">
@ -462,7 +462,177 @@
</windowController> </windowController>
<customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-339" y="1147"/> <point key="canvasLocation" x="-409" y="1137"/>
</scene>
<!--Window Controller-->
<scene sceneID="BD0-La-ygq">
<objects>
<windowController storyboardIdentifier="noticeWindow" id="nfT-AN-9ZW" sceneMemberID="viewController">
<window key="window" title="Notice" separatorStyle="none" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="alertPanel" frameAutosaveName="" titlebarAppearsTransparent="YES" toolbarStyle="unified" titleVisibility="hidden" id="AoF-SN-xB0">
<windowStyleMask key="styleMask" titled="YES" fullSizeContentView="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="Src-7L-4Z4">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="nfT-AN-9ZW" id="8dd-JR-bQG"/>
</connections>
</window>
<connections>
<segue destination="hkw-9V-NxP" kind="relationship" relationship="window.shadowedContentViewController" id="eob-YS-ACy"/>
</connections>
</windowController>
<customObject id="i3j-z8-nxv" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-575" y="1624"/>
</scene>
<!--Better AlertVC-->
<scene sceneID="y9E-bB-wIG">
<objects>
<viewController storyboardIdentifier="noticeVC" id="hkw-9V-NxP" customClass="BetterAlertVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="UPH-hV-Naz">
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<visualEffectView blendingMode="behindWindow" material="popover" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="JVG-5w-fPd">
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
<subviews>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8zu-cF-KCX">
<rect key="frame" x="383" y="13" width="104" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="4Uf-fh-jWJ"/>
</constraints>
<buttonCell key="cell" type="push" title="Primary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="F26-vf-hFH">
<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="primaryButtonAction:" target="hkw-9V-NxP" id="W7d-3b-pZT"/>
</connections>
</button>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TCp-nS-HN2">
<rect key="frame" x="281" y="13" width="104" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="QWZ-BA-0g9"/>
</constraints>
<buttonCell key="cell" type="push" title="Secondary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="eCk-FC-9Zr">
<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="secondaryButtonAction:" target="hkw-9V-NxP" id="YJs-Hu-lFP"/>
</connections>
</button>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j">
<rect key="frame" x="13" y="13" width="82" height="32"/>
<buttonCell key="cell" type="push" title="Tertiary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="mzA-Uu-gyf">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="tertiaryButtonAction:" target="hkw-9V-NxP" id="o1C-av-ifx"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="8zu-cF-KCX" secondAttribute="trailing" constant="20" symbolic="YES" id="0DC-rd-xbu"/>
<constraint firstItem="8zu-cF-KCX" firstAttribute="leading" secondItem="TCp-nS-HN2" secondAttribute="trailing" constant="12" symbolic="YES" id="8tH-KO-fjY"/>
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="L6V-KQ-nj3"/>
<constraint firstItem="n5T-nn-k3j" firstAttribute="leading" secondItem="JVG-5w-fPd" secondAttribute="leading" constant="20" symbolic="YES" id="QLS-fE-1PM"/>
<constraint firstAttribute="trailing" secondItem="8zu-cF-KCX" secondAttribute="trailing" constant="20" symbolic="YES" id="RRS-WO-6KO"/>
<constraint firstAttribute="bottom" secondItem="n5T-nn-k3j" secondAttribute="bottom" constant="20" symbolic="YES" id="Scj-z1-AdN"/>
<constraint firstAttribute="bottom" secondItem="TCp-nS-HN2" secondAttribute="bottom" constant="20" symbolic="YES" id="fYa-HG-gmL"/>
<constraint firstItem="n5T-nn-k3j" firstAttribute="bottom" secondItem="TCp-nS-HN2" secondAttribute="bottom" id="lOI-ZI-wCd"/>
<constraint firstItem="TCp-nS-HN2" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="n5T-nn-k3j" secondAttribute="trailing" constant="12" symbolic="YES" id="rUC-0t-9H9"/>
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="wIl-uw-y3p"/>
</constraints>
</visualEffectView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
<rect key="frame" x="98" y="153" width="384" height="19"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="380" id="WgB-hj-d4P"/>
</constraints>
<textFieldCell key="cell" selectable="YES" title="This is the title of the notice window." id="bzW-MI-jXb">
<font key="font" metaFont="systemBold" size="15"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
<rect key="frame" x="98" y="127" width="384" height="16"/>
<textFieldCell key="cell" selectable="YES" title="This is a slightly more expanded explanation." id="rY3-Nd-Iit">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0rX-Ss-3Xd">
<rect key="frame" x="12" y="144" width="48" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="Uib-R1-GEx">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QkM-5D-ZEQ">
<rect key="frame" x="20" y="111" width="64" height="64"/>
<constraints>
<constraint firstAttribute="height" constant="64" id="VhJ-fI-IKC"/>
<constraint firstAttribute="width" constant="64" id="a2d-Gn-Oor"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="7eT-Hw-EL9"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
<rect key="frame" x="98" y="70" width="384" height="42"/>
<textFieldCell key="cell" selectable="YES" id="7iW-Lc-DqO">
<font key="font" metaFont="smallSystem"/>
<string key="title">Sometimes you need a really long explanation and in that case you can get a really, really long description here, along with, for example, various steps you can take. This allows for a lot of text to be displayed, yay!</string>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="hml-dl-Cah" firstAttribute="leading" secondItem="yI6-qf-htf" secondAttribute="leading" id="1Lh-ve-rwK"/>
<constraint firstItem="U1c-qS-cIm" firstAttribute="leading" secondItem="QkM-5D-ZEQ" secondAttribute="trailing" constant="16" id="2xX-Ma-iQZ"/>
<constraint firstAttribute="trailing" secondItem="U1c-qS-cIm" secondAttribute="trailing" constant="20" symbolic="YES" id="39u-Uk-TDI"/>
<constraint firstItem="8zu-cF-KCX" firstAttribute="top" secondItem="hml-dl-Cah" secondAttribute="bottom" constant="30" id="7fT-EZ-cAO"/>
<constraint firstItem="JVG-5w-fPd" firstAttribute="top" secondItem="UPH-hV-Naz" secondAttribute="top" id="CE7-54-G09"/>
<constraint firstItem="hml-dl-Cah" firstAttribute="trailing" secondItem="yI6-qf-htf" secondAttribute="trailing" id="DBa-7d-sS3"/>
<constraint firstItem="yI6-qf-htf" firstAttribute="trailing" secondItem="U1c-qS-cIm" secondAttribute="trailing" id="NvJ-vf-wEl"/>
<constraint firstItem="hml-dl-Cah" firstAttribute="top" secondItem="yI6-qf-htf" secondAttribute="bottom" constant="15" id="Pmz-I9-2up"/>
<constraint firstItem="U1c-qS-cIm" firstAttribute="top" secondItem="UPH-hV-Naz" secondAttribute="top" constant="40" id="Uqt-sc-vxn"/>
<constraint firstItem="QkM-5D-ZEQ" firstAttribute="top" secondItem="U1c-qS-cIm" secondAttribute="top" constant="-3" id="WAj-rw-srg"/>
<constraint firstItem="yI6-qf-htf" firstAttribute="leading" secondItem="U1c-qS-cIm" secondAttribute="leading" id="bng-pH-jSG"/>
<constraint firstAttribute="trailing" secondItem="JVG-5w-fPd" secondAttribute="trailing" id="dRt-Pq-6n0"/>
<constraint firstItem="JVG-5w-fPd" firstAttribute="leading" secondItem="UPH-hV-Naz" secondAttribute="leading" id="ejC-of-zjN"/>
<constraint firstAttribute="bottom" secondItem="JVG-5w-fPd" secondAttribute="bottom" id="hGp-DD-cKr"/>
<constraint firstItem="QkM-5D-ZEQ" firstAttribute="leading" secondItem="UPH-hV-Naz" secondAttribute="leading" constant="20" symbolic="YES" id="jG8-dt-l4x"/>
<constraint firstItem="yI6-qf-htf" firstAttribute="top" secondItem="U1c-qS-cIm" secondAttribute="bottom" constant="10" id="kqR-yg-zdG"/>
</constraints>
</view>
<connections>
<outlet property="buttonPrimary" destination="8zu-cF-KCX" id="MT5-Px-K97"/>
<outlet property="buttonSecondary" destination="TCp-nS-HN2" id="nPn-OX-Z4m"/>
<outlet property="buttonTertiary" destination="n5T-nn-k3j" id="UnB-8x-s3D"/>
<outlet property="imageView" destination="QkM-5D-ZEQ" id="zsW-l7-eH0"/>
<outlet property="labelDescription" destination="hml-dl-Cah" id="ehw-zs-EPc"/>
<outlet property="labelSubtitle" destination="yI6-qf-htf" id="m7A-bX-HE8"/>
<outlet property="labelTitle" destination="U1c-qS-cIm" id="oM3-kl-PL8"/>
<outlet property="primaryButtonTopMargin" destination="7fT-EZ-cAO" id="r6u-1l-dbl"/>
</connections>
</viewController>
<customObject id="5Ts-EZ-bJh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="38" y="1624"/>
</scene> </scene>
<!--Add SiteVC--> <!--Add SiteVC-->
<scene sceneID="6JC-H6-u4K"> <scene sceneID="6JC-H6-u4K">
@ -472,8 +642,16 @@
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/> <rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="js9-OW-xzC">
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
<view key="contentView" id="HRC-RT-LxR">
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
<rect key="frame" x="13" y="13" width="103" height="32"/> <rect key="frame" x="363" y="13" width="104" height="32"/>
<buttonCell key="cell" type="push" title="Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s"> <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"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -486,7 +664,10 @@ DQ
</connections> </connections>
</button> </button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
<rect key="frame" x="391" y="13" width="76" height="32"/> <rect key="frame" x="13" y="13" width="94" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
</constraints>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp"> <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"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -539,7 +720,7 @@ Gw
<rect key="frame" x="20" y="185" width="440" height="22"/> <rect key="frame" x="20" y="185" width="440" height="22"/>
<pathCell key="cell" selectable="YES" refusesFirstResponder="YES" alignment="left" id="m8d-XF-kh9"> <pathCell key="cell" selectable="YES" refusesFirstResponder="YES" alignment="left" id="m8d-XF-kh9">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
<url key="url" string="file:///Users/nicoverbruggen/Code/nicoverbruggen.be/"/> <url key="url" string="file:///Users/"/>
</pathCell> </pathCell>
</pathControl> </pathControl>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
@ -551,7 +732,7 @@ Gw
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID"> <textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
<rect key="frame" x="115" y="23" width="128" height="14"/> <rect key="frame" x="229" y="23" width="128" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That link already exists." id="jOt-n6-TQf"> <textFieldCell key="cell" lineBreakMode="clipping" title="That link already exists." id="jOt-n6-TQf">
<font key="font" metaFont="smallSystem"/> <font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
@ -562,27 +743,32 @@ Gw
<constraints> <constraints>
<constraint firstItem="VzR-5a-cmT" firstAttribute="trailing" secondItem="ZX9-s1-23i" secondAttribute="trailing" id="06B-dj-IBU"/> <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 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 firstAttribute="bottom" secondItem="SwS-o8-pbl" secondAttribute="bottom" constant="20" symbolic="YES" id="2pB-nW-NVx"/>
<constraint firstItem="900-Z2-tID" firstAttribute="centerY" secondItem="PVw-cM-qAB" secondAttribute="centerY" id="578-2f-4x8"/> <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="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="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="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="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="900-Z2-tID" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="SwS-o8-pbl" secondAttribute="trailing" constant="8" symbolic="YES" id="IMv-ZD-VXf"/>
<constraint firstItem="js9-OW-xzC" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" id="IpM-ot-dBG"/>
<constraint firstItem="VzR-5a-cmT" firstAttribute="leading" secondItem="ZX9-s1-23i" secondAttribute="leading" id="UPN-Ad-j3X"/> <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 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 firstAttribute="bottom" secondItem="PVw-cM-qAB" secondAttribute="bottom" constant="20" symbolic="YES" id="VsP-Q0-zRW"/>
<constraint firstAttribute="trailing" secondItem="PVw-cM-qAB" secondAttribute="trailing" constant="20" symbolic="YES" id="X5z-G4-CBv"/>
<constraint firstItem="ZX9-s1-23i" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="bJ4-Yr-4ah"/> <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 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 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="PVw-cM-qAB" firstAttribute="leading" secondItem="900-Z2-tID" secondAttribute="trailing" constant="15" id="cx5-Gi-XS7"/>
<constraint firstItem="mmQ-7e-dlb" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="fKH-1r-MIf"/> <constraint firstItem="mmQ-7e-dlb" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="fKH-1r-MIf"/>
<constraint firstItem="js9-OW-xzC" firstAttribute="top" secondItem="JJJ-T9-Yuv" secondAttribute="top" id="ffu-hT-qSK"/>
<constraint firstAttribute="bottom" secondItem="js9-OW-xzC" secondAttribute="bottom" id="hLd-Kd-y6k"/>
<constraint firstAttribute="trailing" secondItem="mmQ-7e-dlb" secondAttribute="trailing" constant="20" symbolic="YES" id="hjv-Xq-cxV"/> <constraint firstAttribute="trailing" secondItem="mmQ-7e-dlb" secondAttribute="trailing" constant="20" symbolic="YES" id="hjv-Xq-cxV"/>
<constraint firstItem="SwS-o8-pbl" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="jkg-UC-GPr"/>
<constraint firstItem="6JT-Vt-3q0" firstAttribute="leading" secondItem="P0B-Ht-R8n" secondAttribute="leading" id="jxP-vM-eA9"/> <constraint firstItem="6JT-Vt-3q0" firstAttribute="leading" secondItem="P0B-Ht-R8n" secondAttribute="leading" id="jxP-vM-eA9"/>
<constraint firstAttribute="bottom" secondItem="PVw-cM-qAB" secondAttribute="bottom" constant="20" symbolic="YES" id="kGp-mI-1Ic"/>
<constraint firstItem="P0B-Ht-R8n" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="msC-eG-Fop"/> <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 firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="P0B-Ht-R8n" secondAttribute="trailing" constant="20" symbolic="YES" id="nvj-Ij-dcd"/>
<constraint firstAttribute="trailing" secondItem="js9-OW-xzC" secondAttribute="trailing" id="rc3-XI-7CY"/>
<constraint firstItem="VzR-5a-cmT" firstAttribute="top" secondItem="ZX9-s1-23i" secondAttribute="bottom" constant="8" symbolic="YES" id="sVP-EV-07F"/> <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" 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"/> <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="KZf-b0-9cm" secondAttribute="trailing" constant="20" symbolic="YES" id="zq0-Ce-sCs"/>
@ -602,35 +788,68 @@ Gw
</viewController> </viewController>
<customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="277" y="1137.5"/> <point key="canvasLocation" x="191" y="1098.5"/>
</scene> </scene>
<!--Site ListVC--> <!--Site ListVC-->
<scene sceneID="aZt-6w-TFl"> <scene sceneID="aZt-6w-TFl">
<objects> <objects>
<viewController identifier="siteList" 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"> <view key="view" id="rIZ-4U-bhj">
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/> <rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<customView id="j65-Lf-0lG"> <scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
<rect key="frame" x="9" y="0.0" width="581" height="203"/> <rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w">
</customView> <rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<scrollView autohidesScrollers="YES" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/>
<clipView key="contentView" id="6IL-DW-37w">
<rect key="frame" x="1" y="1" width="598" height="307"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" rowHeight="54" rowSizeStyle="automatic" viewBased="YES" id="cp3-34-pQj"> <tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj">
<rect key="frame" x="0.0" y="0.0" width="598" height="307"/> <rect key="frame" x="0.0" y="0.0" width="662" height="281"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/> <size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/> <tableViewGridLines key="gridStyleMask" horizontal="YES"/>
<color key="gridColor" name="quaternaryLabelColor" catalog="System" colorSpace="catalog"/>
<tableColumns> <tableColumns>
<tableColumn width="586" minWidth="40" maxWidth="10000" id="oeH-B2-0rA"> <tableColumn identifier="TLS" width="36" minWidth="36" maxWidth="36" id="z6X-Ni-Eev">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border"> <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="center" title="TLS">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="OsS-YW-O4s">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Secure"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListTLSCell" id="hft-M4-nWb" customClass="SiteListTLSCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="18" y="0.0" width="34" height="55"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Wel-Ho-Kpp">
<rect key="frame" x="7" y="18" width="20" height="20"/>
<constraints>
<constraint firstAttribute="width" constant="20" id="7mC-me-Nse"/>
<constraint firstAttribute="height" constant="20" id="AjD-xX-suI"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Lock" id="gK0-Mh-K9Y"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="Wel-Ho-Kpp" firstAttribute="centerY" secondItem="hft-M4-nWb" secondAttribute="centerY" id="L6B-iA-BCQ"/>
<constraint firstItem="Wel-Ho-Kpp" firstAttribute="centerX" secondItem="hft-M4-nWb" secondAttribute="centerX" id="jAF-AV-EeX"/>
</constraints>
<connections>
<outlet property="imageViewLock" destination="Wel-Ho-Kpp" id="iji-uw-8we"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="DOMAIN" width="290" minWidth="250" maxWidth="10000" id="oeH-B2-0rA">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Domain">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell> </tableHeaderCell>
@ -639,14 +858,15 @@ Gw
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Domain"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews> <prototypeCellViews>
<tableCellView identifier="siteItem" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListCell" customModule="PHP_Monitor" customModuleProvider="target"> <tableCellView identifier="siteListNameCell" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="581" height="54"/> <rect key="frame" x="69" y="0.0" width="290" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
<rect key="frame" x="38" y="26" width="145" height="16"/> <rect key="frame" x="3" y="26" width="145" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd"> <textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
<font key="font" metaFont="systemSemibold" size="13"/> <font key="font" metaFont="systemSemibold" size="13"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -654,106 +874,166 @@ Gw
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
<rect key="frame" x="38" y="12" width="75" height="14"/> <rect key="frame" x="3" y="12" width="75" height="14"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9"> <textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
<font key="font" metaFont="smallSystem"/> <font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QPX-eu-eV8"> </subviews>
<rect key="frame" x="10" y="22" width="20" height="20"/> <constraints>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="20" symbolic="YES" id="62d-gz-2lX"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="CXK-Q9-CpO" secondAttribute="trailing" constant="20" symbolic="YES" id="Agn-Ag-QYd"/>
<constraint firstItem="CXK-Q9-CpO" firstAttribute="leading" secondItem="XJL-Uw-frD" secondAttribute="leading" id="Ojw-VZ-3EG"/>
<constraint firstItem="XJL-Uw-frD" firstAttribute="top" secondItem="5GY-nN-BWd" secondAttribute="top" constant="12" id="QeE-c7-I9U"/>
<constraint firstItem="CXK-Q9-CpO" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="bottom" id="VKg-Vq-sYa"/>
<constraint firstItem="XJL-Uw-frD" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" constant="5" id="u1q-b0-iKq"/>
</constraints>
<connections>
<outlet property="labelPathName" destination="CXK-Q9-CpO" id="iVZ-cL-azB"/>
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="ENVIRONMENT" width="100" minWidth="100" maxWidth="150" id="hzb-XI-Out">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="center" title="Active">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="ryK-6j-qWW">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="PHP"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListPhpCell" wantsLayer="YES" id="T49-0U-d58" customClass="SiteListPhpCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="376" y="0.0" width="100" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
<rect key="frame" x="27" y="18" width="70" height="18"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="20" id="Bmk-CN-Yyn"/> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="70" id="MBa-bB-DTB"/>
<constraint firstAttribute="height" constant="20" id="d4z-lb-Ww0"/>
</constraints> </constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Lock" id="aJ0-ia-YrZ"/> <buttonCell key="cell" type="inline" title="PHP X.X" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="Tfk-YR-L4B">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystemBold"/>
</buttonCell>
<connections>
<action selector="pressedPhpVersion:" target="T49-0U-d58" id="jVO-TS-F6d"/>
</connections>
</button>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Yq0-qk-bFt">
<rect key="frame" x="1" y="18" width="18" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="18" id="5fd-EQ-BgV"/>
<constraint firstAttribute="height" constant="18" id="nP7-13-SSn"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="S66-ow-Jo1">
<imageReference key="image" image="Checkmark" symbolScale="default"/>
</imageCell>
<color key="contentTintColor" name="IconColorGreen"/>
</imageView> </imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jKi-Ls-7FZ"> </subviews>
<rect key="frame" x="474" y="28" width="64" height="11"/> <constraints>
<textFieldCell key="cell" lineBreakMode="clipping" title="DRIVER TYPE" id="fjd-eb-itv"> <constraint firstItem="Yq0-qk-bFt" firstAttribute="centerY" secondItem="T49-0U-d58" secondAttribute="centerY" id="4Kz-lX-kOP"/>
<font key="font" metaFont="miniSystem"/> <constraint firstItem="ZXQ-bg-Xba" firstAttribute="leading" secondItem="Yq0-qk-bFt" secondAttribute="trailing" constant="8" symbolic="YES" id="6jM-Qf-SG3"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/> <constraint firstItem="ZXQ-bg-Xba" firstAttribute="centerY" secondItem="T49-0U-d58" secondAttribute="centerY" id="FFK-4B-qIb"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <constraint firstItem="ZXQ-bg-Xba" firstAttribute="centerX" secondItem="T49-0U-d58" secondAttribute="centerX" constant="12" id="zjr-UQ-Dd7"/>
</textFieldCell> </constraints>
</textField> <connections>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TbX-e2-3QL"> <outlet property="buttonPhpVersion" destination="ZXQ-bg-Xba" id="vxL-if-CCC"/>
<rect key="frame" x="474" y="15" width="36" height="14"/> <outlet property="imageViewPhpVersionOK" destination="Yq0-qk-bFt" id="0hT-cZ-9NI"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Driver" id="GMt-SG-vFl"> </connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="KIND" width="36" minWidth="36" maxWidth="36" id="7EV-ZL-92u">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Kind">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="kAe-u7-lN6">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Kind"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="SiteListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="493" y="0.0" width="36" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
<rect key="frame" x="9" y="18" width="18" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="18" id="XcB-uw-szU"/>
<constraint firstAttribute="height" constant="18" id="bGN-Vh-Sh0"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="IconLinked" id="T6e-IU-aZy"/>
<color key="contentTintColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="sYR-vb-OW1" firstAttribute="centerY" secondItem="AhT-xR-16a" secondAttribute="centerY" id="4mB-oS-T6I"/>
<constraint firstItem="sYR-vb-OW1" firstAttribute="centerX" secondItem="AhT-xR-16a" secondAttribute="centerX" id="LyQ-XZ-J3u"/>
</constraints>
<connections>
<outlet property="imageViewType" destination="sYR-vb-OW1" id="txH-es-roq"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="TYPE" width="100" minWidth="100" maxWidth="100" id="ncU-Ge-cyW">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="center" title="Project Type">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="qHO-1M-TT2">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Type"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="SiteListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="546" y="0.0" width="97" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
<rect key="frame" x="6" y="26" width="93" height="14"/>
<textFieldCell key="cell" alignment="left" title="Laravel" id="0lu-L6-oKr">
<font key="font" metaFont="smallSystem"/> <font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="syz-LF-l6P"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
<rect key="frame" x="0.0" y="-2" width="581" height="5"/> <rect key="frame" x="6" y="15" width="93" height="11"/>
</box> <textFieldCell key="cell" alignment="left" title="PHP 8.0" id="puf-Jh-ham">
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0NQ-ZD-CqD"> <font key="font" metaFont="miniSystem"/>
<rect key="frame" x="450" y="18" width="18" height="18"/> <color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<constraints> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<constraint firstAttribute="width" constant="18" id="Suw-gm-AEi"/> </textFieldCell>
<constraint firstAttribute="height" constant="18" id="qO6-vg-5nC"/> </textField>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="IconLinked" id="2ng-pK-kvv"/>
<color key="contentTintColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/>
</imageView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="3xt-wC-hUJ">
<rect key="frame" x="363" y="18" width="75" height="18"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="75" id="VI8-MP-7Hv"/>
</constraints>
<buttonCell key="cell" type="inline" title=" PHP X.X" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="anZ-hP-G0R">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystemBold"/>
</buttonCell>
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<connections>
<action selector="pressedPhpVersion:" target="5GY-nN-BWd" id="mB5-WD-aZy"/>
</connections>
</button>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5aN-ZI-D7U">
<rect key="frame" x="341" y="20" width="14" height="14"/>
<constraints>
<constraint firstAttribute="height" constant="14" id="NKD-Pc-okU"/>
<constraint firstAttribute="width" constant="14" id="wrl-lJ-3eN"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Checkmark" id="R5o-Cd-a91"/>
<color key="contentTintColor" name="IconColorGreen"/>
</imageView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" secondItem="3xt-wC-hUJ" secondAttribute="trailing" constant="12" id="2G8-Ow-FTu"/> <constraint firstItem="aPK-Xc-J4B" firstAttribute="top" secondItem="Ljl-8B-key" secondAttribute="bottom" id="0Ta-4x-l8E"/>
<constraint firstItem="3xt-wC-hUJ" firstAttribute="leading" secondItem="5aN-ZI-D7U" secondAttribute="trailing" constant="8" symbolic="YES" id="39Z-nB-kXx"/> <constraint firstItem="Ljl-8B-key" firstAttribute="centerY" secondItem="ntU-Rl-ciP" secondAttribute="centerY" constant="-6" id="9Lh-QK-qnR"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="TbX-e2-3QL" secondAttribute="trailing" constant="20" symbolic="YES" id="3vE-LR-S7N"/> <constraint firstItem="aPK-Xc-J4B" firstAttribute="leading" secondItem="Ljl-8B-key" secondAttribute="leading" id="PNb-yh-XOn"/>
<constraint firstItem="TbX-e2-3QL" firstAttribute="leading" secondItem="0NQ-ZD-CqD" secondAttribute="trailing" constant="8" symbolic="YES" id="4cb-D9-8d1"/> <constraint firstItem="aPK-Xc-J4B" firstAttribute="trailing" secondItem="Ljl-8B-key" secondAttribute="trailing" id="T28-JT-Zkq"/>
<constraint firstItem="XJL-Uw-frD" firstAttribute="leading" secondItem="QPX-eu-eV8" secondAttribute="trailing" constant="10" id="55y-3V-RYt"/> <constraint firstAttribute="trailing" secondItem="Ljl-8B-key" secondAttribute="trailing" id="Y7O-lc-fqb"/>
<constraint firstItem="syz-LF-l6P" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" id="8QK-nf-Fiw"/> <constraint firstItem="Ljl-8B-key" firstAttribute="leading" secondItem="ntU-Rl-ciP" secondAttribute="leading" constant="8" id="idV-Vu-YeP"/>
<constraint firstItem="QPX-eu-eV8" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="top" id="9QB-jZ-k1V"/>
<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 firstItem="0NQ-ZD-CqD" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="Utr-aa-tqX"/>
<constraint firstItem="CXK-Q9-CpO" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="bottom" id="VKg-Vq-sYa"/>
<constraint firstItem="5aN-ZI-D7U" firstAttribute="centerY" secondItem="3xt-wC-hUJ" secondAttribute="centerY" id="a6n-E2-i2x"/>
<constraint firstItem="TbX-e2-3QL" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" constant="5" id="cN8-zO-fnc"/>
<constraint firstAttribute="bottom" secondItem="syz-LF-l6P" secondAttribute="bottom" id="gj7-cJ-Lle"/>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="CXK-Q9-CpO" secondAttribute="trailing" constant="8" symbolic="YES" id="iEd-Y3-zhp"/>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="8" symbolic="YES" id="lLA-Jx-Q4W"/>
<constraint firstItem="3xt-wC-hUJ" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="vhb-WC-3NC"/>
<constraint firstAttribute="trailing" secondItem="jKi-Ls-7FZ" secondAttribute="trailing" constant="45" id="vwD-Sg-Lzc"/>
<constraint firstItem="jKi-Ls-7FZ" firstAttribute="leading" secondItem="TbX-e2-3QL" secondAttribute="leading" id="zjN-s3-2Ww"/>
</constraints> </constraints>
<connections> <connections>
<outlet property="buttonPhpVersion" destination="3xt-wC-hUJ" id="LpB-7n-qUr"/> <outlet property="labelDriver" destination="Ljl-8B-key" id="82M-LT-pHT"/>
<outlet property="imageViewLock" destination="QPX-eu-eV8" id="Nnh-kB-adG"/> <outlet property="labelPhpVersion" destination="aPK-Xc-J4B" id="TdR-xE-xhX"/>
<outlet property="imageViewPhpVersionOK" destination="5aN-ZI-D7U" id="ePz-Cb-dWk"/>
<outlet property="imageViewType" destination="0NQ-ZD-CqD" id="Cph-FN-LaY"/>
<outlet property="labelDriver" destination="TbX-e2-3QL" id="qJh-Ak-Dge"/>
<outlet property="labelDriverType" destination="jKi-Ls-7FZ" id="ZTq-pP-qUC"/>
<outlet property="labelPathName" destination="CXK-Q9-CpO" id="iVZ-cL-azB"/>
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
</connections> </connections>
</tableCellView> </tableCellView>
</prototypeCellViews> </prototypeCellViews>
@ -765,22 +1045,27 @@ Gw
</connections> </connections>
</tableView> </tableView>
</subviews> </subviews>
<nil key="backgroundColor"/>
</clipView> </clipView>
<constraints> <constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="300" id="R3Z-g3-tYQ"/> <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="300" id="R3Z-g3-tYQ"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="600" id="iRQ-sz-oyv"/> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/>
</constraints> </constraints>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT"> <scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
<rect key="frame" x="1" y="292" width="598" height="16"/> <rect key="frame" x="0.0" y="293" width="626" height="16"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</scroller> </scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10"> <scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
<rect key="frame" x="558" y="29" width="15" height="225"/> <rect key="frame" x="610" y="28" width="16" height="281"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</scroller> </scroller>
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
<rect key="frame" x="0.0" y="0.0" width="662" height="28"/>
<autoresizingMask key="autoresizingMask"/>
</tableHeaderView>
</scrollView> </scrollView>
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ"> <progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
<rect key="frame" x="285" y="150" width="30" height="30"/> <rect key="frame" x="298" y="150" width="30" height="30"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/> <constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/> <constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
@ -803,7 +1088,7 @@ Gw
</viewController> </viewController>
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="251" y="741.5"/> <point key="canvasLocation" x="388" y="715.5"/>
</scene> </scene>
</scenes> </scenes>
<resources> <resources>

View File

@ -56,7 +56,10 @@ class InterApp {
if PhpEnv.shared.availablePhpVersions.contains(version) { if PhpEnv.shared.availablePhpVersions.contains(version) {
MainMenu.shared.switchToPhpVersion(version) MainMenu.shared.switchToPhpVersion(version)
} else { } else {
Alert.notify(message: "Unsupported version", info: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available.") BetterAlert().withInformation(
title: "Unsupported version",
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
).withPrimary(text: "OK").show()
} }
}), }),
]} ]}

View File

@ -10,88 +10,70 @@ import AppKit
class Startup { class Startup {
public var failed : Bool = false
public var failureCallback = {}
/** /**
Checks the user's environment and checks if PHP Monitor can be used properly. Checks the user's environment and checks if PHP Monitor can be used properly.
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more. This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
- Parameter success: Callback that is fired if the application can proceed with launch If this method returns false, there was a failed check and an alert was displayed.
- Parameter failure: Callback that is fired if the application must retry launch If this method returns true, then all checks succeeded and the app can continue.
*/ */
func checkEnvironment(success: () -> Void, failure: @escaping () -> Void) func checkEnvironment() async -> Bool
{ {
failureCallback = failure // Do the important system setup checks
Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)")
performEnvironmentCheck( for check in self.checks {
!Shell.fileExists("\(Paths.binPath)/php"), if await check.succeeds() {
messageText: "startup.errors.php_binary.title".localized, Log.info("[OK] \(check.name)")
informativeText: "startup.errors.php_binary.desc".localized, continue
breaking: true }
)
// If we get here, something's gone wrong and the check has failed...
Log.info("[FAIL] \(check.name)")
showAlert(for: check)
return false
}
performEnvironmentCheck( // If we get here, nothing has gone wrong. That's what we want!
!Shell.pipe("ls \(Paths.optPath) | grep php").contains("php"), initializeSwitcher()
messageText: "startup.errors.php_opt.title".localized, Log.separator(as: .info)
informativeText: "startup.errors.php_opt.desc".localized, Log.info("PHP Monitor has determined the application has successfully passed all checks.")
breaking: true return true
) }
performEnvironmentCheck( /**
// Check for Valet; it can be symlinked or in .composer/vendor/bin Displays an alert for a particular check. There are two types of alerts:
!(Shell.fileExists("/usr/local/bin/valet") - ones that require an app restart, which prompt the user to exit the app
|| Shell.fileExists("/opt/homebrew/bin/valet") - ones that allow the app to continue, which allow the user to retry
|| Shell.fileExists("~/.composer/vendor/bin/valet") */
), private func showAlert(for check: EnvironmentCheck) {
messageText: "startup.errors.valet_executable.title".localized, DispatchQueue.main.async {
informativeText: "startup.errors.valet_executable.desc".localized, if check.requiresAppRestart {
breaking: true BetterAlert()
) .withInformation(
title: check.titleText,
performEnvironmentCheck( subtitle: check.subtitleText,
HomebrewDiagnostics.cannotLoadService(), description: check.descriptionText
messageText: "startup.errors.services_json_error.title".localized, )
informativeText: "startup.errors.services_json_error.desc".localized, .withPrimary(text: check.buttonText, action: { _ in
breaking: true exit(1)
) }).show()
}
performEnvironmentCheck(
!Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"), BetterAlert()
messageText: "startup.errors.sudoers_brew.title".localized, .withInformation(
informativeText: "startup.errors.sudoers_brew.desc".localized, title: check.titleText,
breaking: true subtitle: check.subtitleText,
) description: check.descriptionText
)
performEnvironmentCheck( .withPrimary(text: "OK")
// Check for Valet; it MUST be symlinked thanks to sudoers .show()
!(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")
),
messageText: "startup.errors.sudoers_valet.title".localized,
informativeText: "startup.errors.sudoers_valet.desc".localized,
breaking: true
)
// Determine the Valet version only AFTER confirming the correct permission is in place
Valet.shared.version = VersionExtractor.from(valet("--version"))
performEnvironmentCheck(
Valet.shared.version == nil,
messageText: "startup.errors.valet_version_unknown.title".localized,
informativeText: "startup.errors.valet_version_unknown.desc".localized,
breaking: true
)
if (!failed) {
initializeSwitcher()
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
success()
} }
} }
/** /**
Because the Switcher requires various environment guarantees, the switcher is only Because the Switcher requires various environment guarantees, the switcher is only
initialized when it is done working. initialized when it is done working. The switcher must be initialized on the main thread.
*/ */
private func initializeSwitcher() { private func initializeSwitcher() {
DispatchQueue.main.async { DispatchQueue.main.async {
@ -99,35 +81,157 @@ class Startup {
appDelegate.initializeSwitcher() appDelegate.initializeSwitcher()
} }
} }
/** // MARK: - Check (List)
Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
public var checks: [EnvironmentCheck] = [
- Parameter condition: Fail condition to check for; if this returns `true`, the alert will be shown // =================================================================================
- Parameter messageText: Short description of what is wrong // The Homebrew binary must exist.
- Parameter informativeText: Expanded description of the environment check that failed // =================================================================================
- Parameter breaking: If the application should terminate afterwards EnvironmentCheck(
*/ command: { return !FileManager.default.fileExists(atPath: Paths.brew) },
private func performEnvironmentCheck( name: "`\(Paths.brew)` exists",
_ condition: Bool, titleText: "alert.homebrew_missing.title".localized,
messageText: String, subtitleText: "alert.homebrew_missing.subtitle".localized,
informativeText: String, descriptionText: "alert.homebrew_missing.info".localized(
breaking: Bool App.architecture
) { .replacingOccurrences(of: "x86_64", with: "Intel")
if (!condition) { return } .replacingOccurrences(of: "arm64", with: "Apple Silicon"),
Paths.brew
failed = breaking ),
buttonText: "alert.homebrew_missing.quit".localized,
DispatchQueue.main.async { [self] in requiresAppRestart: true
// Present the information to the user ),
Alert.notify( // =================================================================================
message: messageText, // The PHP binary must exist.
info: informativeText, // =================================================================================
style: breaking ? .critical : .warning EnvironmentCheck(
command: { return !Filesystem.fileExists(Paths.php) },
name: "`\(Paths.php)` exists",
titleText: "startup.errors.php_binary.title".localized,
subtitleText: "startup.errors.php_binary.subtitle".localized,
descriptionText: "startup.errors.php_binary.desc".localized(Paths.php)
),
// =================================================================================
// Make sure we can detect one or more PHP installations.
// =================================================================================
EnvironmentCheck(
command: { return !Shell.pipe("ls \(Paths.optPath) | grep php").contains("php") },
name: "`ls \(Paths.optPath) | grep php` returned php result",
titleText: "startup.errors.php_opt.title".localized,
subtitleText: "startup.errors.php_opt.subtitle".localized(
Paths.optPath
),
descriptionText: "startup.errors.php_opt.desc".localized
),
// =================================================================================
// The Valet binary must exist.
// =================================================================================
EnvironmentCheck(
command: {
return !(Filesystem.fileExists(Paths.valet) || Filesystem.fileExists("~/.composer/vendor/bin/valet"))
},
name: "`valet` binary exists",
titleText: "startup.errors.valet_executable.title".localized,
subtitleText: "startup.errors.valet_executable.subtitle".localized,
descriptionText: "startup.errors.valet_executable.desc".localized(
Paths.valet
) )
// Only breaking issues will throw the extra retry modal ),
breaking ? failureCallback() : () // =================================================================================
// Check if Valet and Homebrew need manual password intervention. If they do, then
// PHP Monitor will be unable to run these commands, which prevents PHP Monitor from
// functioning correctly. Let the user know that they need to run `valet trust`.
// =================================================================================
EnvironmentCheck(
command: { return !Shell.pipe("cat /private/etc/sudoers.d/brew").contains(Paths.brew) },
name: "`/private/etc/sudoers.d/brew` contains brew",
titleText: "startup.errors.sudoers_brew.title".localized,
subtitleText: "startup.errors.sudoers_brew.subtitle".localized
),
EnvironmentCheck(
command: { return !Shell.pipe("cat /private/etc/sudoers.d/valet").contains(Paths.valet) },
name: "`/private/etc/sudoers.d/valet` contains valet",
titleText: "startup.errors.sudoers_valet.title".localized,
subtitleText: "startup.errors.sudoers_valet.subtitle".localized
),
// =================================================================================
// Verify if the Homebrew services are running (as root).
// =================================================================================
EnvironmentCheck(
command: { return HomebrewDiagnostics.cannotLoadService() },
name: "`sudo \(Paths.brew) services info` JSON loaded",
titleText: "startup.errors.services_json_error.title".localized,
subtitleText: "startup.errors.services_json_error.subtitle".localized,
descriptionText: "startup.errors.services_json_error.desc".localized
),
// =================================================================================
// Determine that the Valet configuration JSON file is valid.
// =================================================================================
EnvironmentCheck(
command: {
// Detect additional binaries (e.g. Composer)
Paths.shared.detectBinaryPaths()
// Load the configuration file (config.json)
Valet.shared.loadConfiguration()
// This check fails when the config is nil
return Valet.shared.config == nil
},
name: "`config.json` was valid",
titleText: "startup.errors.valet_json_invalid.title".localized,
subtitleText: "startup.errors.valet_json_invalid.subtitle".localized,
descriptionText: "startup.errors.valet_json_invalid.desc".localized
),
// =================================================================================
// Determine the Valet version and ensure it isn't unknown.
// =================================================================================
EnvironmentCheck(
command: {
Valet.shared.version = VersionExtractor.from(valet("--version", sudo: false))
return Valet.shared.version == nil
},
name: "`valet --version` was loaded",
titleText: "startup.errors.valet_version_unknown.title".localized,
subtitleText: "startup.errors.valet_version_unknown.subtitle".localized,
descriptionText: "startup.errors.valet_version_unknown.desc".localized
)
]
// MARK: - EnvironmentCheck struct
/**
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary.
Checks that require an app restart will always lead to an alert and app termination shortly after.
*/
struct EnvironmentCheck {
let command: () async -> Bool
let name: String
let titleText: String
let subtitleText: String
let descriptionText: String
let buttonText: String
let requiresAppRestart: Bool
init(
command: @escaping () async -> Bool,
name: String,
titleText: String,
subtitleText: String,
descriptionText: String = "",
buttonText: String = "OK",
requiresAppRestart: Bool = false
) {
self.command = command
self.name = name
self.titleText = titleText
self.subtitleText = subtitleText
self.descriptionText = descriptionText
self.buttonText = buttonText
self.requiresAppRestart = requiresAppRestart
}
public func succeeds() async -> Bool {
return await !self.command()
} }
} }
} }

View File

@ -29,6 +29,7 @@ struct ComposerJson: Decodable {
struct Config: Decodable { struct Config: Decodable {
let platform: Platform? let platform: Platform?
} }
struct Platform: Decodable { struct Platform: Decodable {
let php: String? let php: String?
} }
@ -39,19 +40,20 @@ struct ComposerJson: Decodable {
Checks what the PHP version constraint is. Checks what the PHP version constraint is.
Returns a tuple (constraint, location of constraint). Returns a tuple (constraint, location of constraint).
*/ */
public func getPhpVersion() -> (String, String) { public func getPhpVersion() -> (String, ValetSite.VersionSource)
{
// Check if in platform // Check if in platform
if configuration?.platform?.php != nil { if configuration?.platform?.php != nil {
return (configuration!.platform!.php!, "platform") return (configuration!.platform!.php!, .platform)
} }
// Check if in dependencies // Check if in dependencies
if dependencies?["php"] != nil { if dependencies?["php"] != nil {
return (dependencies!["php"]!, "require") return (dependencies!["php"]!, .require)
} }
// Unknown! // Unknown!
return ("???", "unknown") return ("???", .unknown)
} }
/** /**

View File

@ -0,0 +1,128 @@
//
// MainMenu+Composer.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class ComposerWindow {
private var menu: MainMenu? = nil
private var shouldNotify: Bool! = nil
private var completion: ((Bool) -> Void)! = nil
private var window: ProgressWindowController? = nil
/**
Updates the global dependencies and runs the completion callback when done.
*/
func updateGlobalDependencies(notify: Bool, completion: @escaping (Bool) -> Void) {
self.menu = MainMenu.shared
self.shouldNotify = notify
self.completion = completion
Paths.shared.detectBinaryPaths()
if Paths.composer == nil {
presentMissingAlert()
return
}
PhpEnv.shared.isBusy = true
menu?.setBusyImage()
menu?.rebuild()
window = ProgressWindowController.display(
title: "alert.composer_progress.title".localized,
description: "alert.composer_progress.info".localized
)
window?.setType(info: true)
DispatchQueue.global(qos: .userInitiated).async { [self] in
let task = Shell.user.createTask(
for: "\(Paths.composer!) global update", requiresPath: true
)
DispatchQueue.main.async {
self.window?.addToConsole("\(Paths.composer!) global update\n")
}
task.listen(
didReceiveStandardOutputData: { string in
DispatchQueue.main.async {
self.window?.addToConsole(string)
}
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
},
didReceiveStandardErrorData: { string in
DispatchQueue.main.async {
self.window?.addToConsole(string)
}
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
}
)
task.launch()
task.waitUntilExit()
task.haltListening()
if task.terminationStatus <= 0 {
composerUpdateSucceeded()
} else {
composerUpdateFailed()
}
}
}
private func composerUpdateSucceeded() {
// Closing the window should happen after a slight delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in
window?.close()
if (shouldNotify) {
LocalNotification.send(
title: "alert.composer_success.title".localized,
subtitle: "alert.composer_success.info".localized
)
}
window = nil
removeBusyStatus()
completion(true)
}
}
private func composerUpdateFailed() {
// Showing that something failed should be shown immediately
DispatchQueue.main.async { [self] in
window?.setType(info: false)
window?.progressView?.labelTitle.stringValue = "alert.composer_failure.title".localized
window?.progressView?.labelDescription.stringValue = "alert.composer_failure.info".localized
window = nil
removeBusyStatus()
completion(false)
}
}
// MARK: Main Menu Update
private func removeBusyStatus() {
PhpEnv.shared.isBusy = false
DispatchQueue.main.async { [self] in
menu?.updatePhpVersionInStatusBar()
}
}
// MARK: Alert
private func presentMissingAlert() {
BetterAlert()
.withInformation(
title: "alert.composer_missing.title".localized,
subtitle: "alert.composer_missing.subtitle".localized,
description: "alert.composer_missing.desc".localized
)
.withPrimary(text: "OK")
.show()
}
}

View File

@ -36,7 +36,8 @@ struct PhpFrameworks {
"statamic/cms": "Statamic", "statamic/cms": "Statamic",
"johnpbloch/wordpress-core": "WordPress", "johnpbloch/wordpress-core": "WordPress",
"zendframework/zendframework": "Zend", "zendframework/zendframework": "Zend",
"zendframework/zend-mvc": "Zend" "zendframework/zend-mvc": "Zend",
"typo3/cms-core": "Typo3",
// TODO (5.1): Handle these in v5.1 // TODO (5.1): Handle these in v5.1
// "magento/*": "Magento", // "magento/*": "Magento",
@ -58,6 +59,10 @@ struct PhpFrameworks {
"/wp-config.php", "/wp-config.php",
"/wp-config-sample.php" "/wp-config-sample.php"
], ],
"Typo3": [
"/typo3",
"/public/typo3",
]
] ]
/** /**

View File

@ -0,0 +1,39 @@
//
// NginxConfigParser.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class NginxConfigParser {
var contents: String!
init(filePath: String) {
self.contents = try! String(contentsOfFile: filePath
.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
)
}
lazy var isolatedVersion: String? = {
let regex = try! NSRegularExpression(
// PHP versions have (so far) never needed multiple digits for version numbers
pattern: #"(ISOLATED_PHP_VERSION=(php)?(@)?)((?<major>\d)(.)?(?<minor>\d))"#,
options: []
)
let match = regex.firstMatch(in: contents, range: NSMakeRange(0, contents.count))
if match == nil {
return nil
}
let major: String = contents[Range(match!.range(withName: "major"), in: contents)!]
let minor: String = contents[Range(match!.range(withName: "minor"), in: contents)!]
return "\(major).\(minor)"
}()
}

View File

@ -0,0 +1,134 @@
//
// ValetSiteScanner.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 19/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol SiteScanner
{
func resolveSiteCount(paths: [String]) -> Int
func resolveSitesFrom(paths: [String]) -> [ValetSite]
func resolveSite(path: String) -> ValetSite?
}
class FakeSiteScanner: SiteScanner
{
let fakes = [
ValetSite(fakeWithName: "laravel", tld: "test", secure: true, path: "~/Code/laravel/framework", linked: true),
ValetSite(fakeWithName: "tailwind", tld: "test", secure: true, path: "~/Code/tailwind/site", linked: true, constraint: "8.0"),
ValetSite(fakeWithName: "forge", tld: "test", secure: true, path: "~/Code/laravel/forge", linked: true),
ValetSite(fakeWithName: "concord", tld: "test", secure: false,
path: "~/Code/concord", linked: true, driver: "Laravel (^8.0)", constraint: "^7.4", isolated: "7.4"),
ValetSite(fakeWithName: "drupal", tld: "test", secure: false,
path: "~/Sites/drupal", linked: false, driver: "Drupal", constraint: "^7.4", isolated: "7.4"),
ValetSite(fakeWithName: "wordpress", tld: "test", secure: false,
path: "~/Sites/wordpress", linked: false, driver: "WordPress", constraint: "^7.4", isolated: "7.4")
]
func resolveSiteCount(paths: [String]) -> Int {
return fakes.count
}
func resolveSitesFrom(paths: [String]) -> [ValetSite] {
return fakes
}
func resolveSite(path: String) -> ValetSite? {
return nil
}
}
class ValetSiteScanner: SiteScanner
{
func resolveSiteCount(paths: [String]) -> Int {
return paths.map { path in
let entries = try! FileManager.default
.contentsOfDirectory(atPath: path)
return entries
.map { self.isSite($0, forPath: path) }
.filter{ $0 == true}
.count
}.reduce(0, +)
}
func resolveSitesFrom(paths: [String]) -> [ValetSite] {
var sites: [ValetSite] = []
paths.forEach { path in
let entries = try! FileManager.default
.contentsOfDirectory(atPath: path)
return entries.forEach {
if let site = self.resolveSite(path: "\(path)/\($0)") {
sites.append(site)
}
}
}
return sites
}
/**
Determines whether the site can be resolved as a symbolic link or as a directory.
Regular files are ignored, and the site is added to Valet's list of sites.
*/
func resolveSite(path: String) -> ValetSite? {
// Get the TLD from the global Valet object
let tld = Valet.shared.config.tld
// See if the file is a symlink, if so, resolve it
guard let attrs = try? FileManager.default.attributesOfItem(atPath: path) else {
Log.warn("Could not parse the site: \(path), skipping!")
return nil
}
// We can also determine whether the thing at the path is a directory, too
let type = attrs[FileAttributeKey.type] as! FileAttributeType
// We should also check that we can interpret the path correctly
if URL(fileURLWithPath: path).lastPathComponent == "" {
Log.warn("Could not parse the site: \(path), skipping!")
return nil
}
if type == FileAttributeType.typeSymbolicLink {
return ValetSite(aliasPath: path, tld: tld)
} else if type == FileAttributeType.typeDirectory {
return ValetSite(absolutePath: path, tld: tld)
}
return nil
}
/**
Determines whether the site can be resolved as a symbolic link or as a directory.
Regular files are ignored. Returns true if the path can be parsed.
*/
private func isSite(_ entry: String, forPath path: String) -> Bool {
let siteDir = path + "/" + entry
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
let type = attrs[FileAttributeKey.type] as! FileAttributeType
if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory {
return true
}
return false
}
}

View File

@ -10,6 +10,11 @@ import Foundation
class Valet { class Valet {
enum FeatureFlag {
case isolatedSites,
supportForPhp56
}
static let shared = Valet() static let shared = Valet()
/// The version of Valet that was detected. /// The version of Valet that was detected.
@ -19,33 +24,61 @@ class Valet {
var config: Valet.Configuration! var config: Valet.Configuration!
/// A cached list of sites that were detected after analyzing the paths set up for Valet. /// A cached list of sites that were detected after analyzing the paths set up for Valet.
var sites: [Site] = [] var sites: [ValetSite] = []
/// Whether we're busy with some blocking operation. /// Whether we're busy with some blocking operation.
var isBusy: Bool = false var isBusy: Bool = false
/// When initialising the Valet singleton, extract the Valet version and assume no sites loaded. /// Various feature flags. Enabled based on the installed Valet version.
var features: [FeatureFlag] = []
/// When initialising the Valet singleton assume no sites loaded. We will load the version later.
init() { init() {
self.version = nil self.version = nil
self.sites = [] self.sites = []
} }
/**
If marketing mode is enabled, show a list of sites that are used for promotional screenshots.
This can be done by swapping out the real Valet scanner with one that always returns a fixed
list of fake sites. You should not interact with these sites!
*/
static var siteScanner: SiteScanner {
if ProcessInfo.processInfo.environment["PHPMON_MARKETING_MODE"] != nil {
return FakeSiteScanner()
}
return ValetSiteScanner()
}
/**
Check if a particular feature is enabled.
*/
public static func enabled(feature: FeatureFlag) -> Bool {
return self.shared.features.contains(feature)
}
/** /**
We don't want to load the initial config.json file as soon as the class is initialised. 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 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 have passed: otherwise the file might not exist, leading to a crash.
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. Since version 5.2, it is no longer possible for an invalid file to crash the app.
If the JSON is invalid when the app launches, an alert will be presented, however.
*/ */
public func loadConfiguration() { public func loadConfiguration() {
let file = FileManager.default.homeDirectoryForCurrentUser let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet/config.json") .appendingPathComponent(".config/valet/config.json")
// TODO: (5.1) Fix loading of invalid JSON: do not crash the app do {
config = try! JSONDecoder().decode( config = try JSONDecoder().decode(
Valet.Configuration.self, Valet.Configuration.self,
from: try! String(contentsOf: file, encoding: .utf8).data(using: .utf8)! from: try String(contentsOf: file, encoding: .utf8).data(using: .utf8)!
) )
} catch {
Log.err(error)
}
} }
/** /**
@ -54,7 +87,7 @@ class Valet {
(This is done to keep the startup speed as fast as possible.) (This is done to keep the startup speed as fast as possible.)
*/ */
public func startPreloadingSites() { public func startPreloadingSites() {
let maximumPreload = 30 let maximumPreload = 50
let foundSites = self.countPaths() let foundSites = self.countPaths()
if foundSites <= maximumPreload { if foundSites <= maximumPreload {
// Preload the sites and their drivers // Preload the sites and their drivers
@ -67,14 +100,34 @@ class Valet {
/** /**
Reloads the list of sites, assuming that the list isn't being reloaded at the time. 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! (We don't want to do duplicate or parallel work!)
*/ */
public func reloadSites() { public func reloadSites() {
loadConfiguration()
if (isBusy) { if (isBusy) {
return return
} }
resolvePaths(tld: config.tld) resolvePaths()
}
/**
Depending on the version of Valet that is active, the feature set of PHP Monitor will change.
In version 6.0, support for Valet 2.x will be dropped, but until then features are evaluated by using the helper
`enabled(feature)`, which contains information about the feature set of the version of Valet that is currently
in use. This allows PHP Monitor to do different things when Valet 3.0 is enabled.
*/
public func evaluateFeatureSupport() -> Void {
let isOlderThanVersionThree = version.versionCompare("3.0") == .orderedAscending
if isOlderThanVersionThree {
self.features.append(.supportForPhp56)
} else {
Log.info("This version of Valet supports isolation.")
self.features.append(.isolatedSites)
}
} }
/** /**
@ -83,11 +136,21 @@ class Valet {
installed is not recent enough. installed is not recent enough.
*/ */
public func validateVersion() -> Void { public func validateVersion() -> Void {
// 1. Evaluate feature support
Valet.shared.evaluateFeatureSupport()
// 2. Notify user if the version is too old
if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending { if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending {
let version = version let version = version
Log.warn("Valet version \(version!) is too old! (recommended: \(Constants.MinimumRecommendedValetVersion))") Log.warn("Valet version \(version!) is too old! (recommended: \(Constants.MinimumRecommendedValetVersion))")
DispatchQueue.main.async { DispatchQueue.main.async {
Alert.notify(message: "alert.min_valet_version.title".localized, info: "alert.min_valet_version.info".localized(version!, Constants.MinimumRecommendedValetVersion)) BetterAlert()
.withInformation(
title: "alert.min_valet_version.title".localized,
subtitle:"alert.min_valet_version.info".localized(version!, Constants.MinimumRecommendedValetVersion)
)
.withPrimary(text: "OK")
.show()
} }
} else { } else {
Log.info("Valet version \(version!) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))") Log.info("Valet version \(version!) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
@ -98,235 +161,28 @@ class Valet {
Returns a count of how many sites are linked and parked. Returns a count of how many sites are linked and parked.
*/ */
private func countPaths() -> Int { private func countPaths() -> Int {
var count = 0 return Self.siteScanner
for path in config.paths { .resolveSiteCount(paths: config.paths)
let entries = try! FileManager.default.contentsOfDirectory(atPath: path)
for entry in entries {
if resolveSite(entry, forPath: path) {
count += 1
}
}
}
return count
} }
/** /**
Resolves all paths and creates linked or parked site instances that can be referenced later. Resolves all paths and creates linked or parked site instances that can be referenced later.
*/ */
private func resolvePaths(tld: String) { private func resolvePaths() {
isBusy = true isBusy = true
sites = [] sites = Self.siteScanner
.resolveSitesFrom(paths: config.paths)
.sorted { $0.absolutePath < $1.absolutePath }
for path in config.paths { if let defaultPath = Valet.shared.config.defaultSite,
let entries = try! FileManager.default.contentsOfDirectory(atPath: path) let site = ValetSiteScanner().resolveSite(path: defaultPath) {
for entry in entries { sites.insert(site, at: 0)
resolvePath(entry, forPath: path, tld: tld)
}
} }
sites = sites.sorted { $0.absolutePath < $1.absolutePath }
isBusy = false isBusy = false
} }
/**
Determines whether the site can be resolved as a symbolic link or as a directory.
Regular files are ignored. Returns true if the path can be parsed.
*/
private func resolveSite(_ entry: String, forPath path: String) -> Bool {
let siteDir = path + "/" + entry
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
let type = attrs[FileAttributeKey.type] as! FileAttributeType
if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory {
return true
}
return false
}
/**
Determines whether the site can be resolved as a symbolic link or as a directory.
Regular files are ignored, and the site is added to Valet's list of sites.
*/
private func resolvePath(_ entry: String, forPath path: String, tld: String) {
let siteDir = path + "/" + entry
// See if the file is a symlink, if so, resolve it
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
// We can also determine whether the thing at the path is a directory, too
let type = attrs[FileAttributeKey.type] as! FileAttributeType
// We should also check that we can interpret the path correctly
if URL(fileURLWithPath: siteDir).lastPathComponent == "" {
Log.warn("Could not parse the site: \(siteDir), skipping!")
return
}
if type == FileAttributeType.typeSymbolicLink {
sites.append(Site(aliasPath: siteDir, tld: tld))
} else if type == FileAttributeType.typeDirectory {
sites.append(Site(absolutePath: siteDir, tld: tld))
}
}
// MARK: - Structs
class Site {
/// Name of the site. Does not include the TLD.
var name: String!
/// 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?
/// Whether the site has been secured.
var secured: Bool!
/// 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) {
self.init()
self.absolutePath = absolutePath
self.name = URL(fileURLWithPath: absolutePath).lastPathComponent
self.aliasPath = nil
determineSecured(tld)
determineComposerPhpVersion()
determineDriver()
}
convenience init(aliasPath: String, tld: String) {
self.init()
self.absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
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
.replacingOccurrences(of: "This site is served by [", with: "")
.replacingOccurrences(of: "ValetDriver].\n", with: "")
} else {
self.driver = nil
}
}
}
struct Configuration: Decodable { struct Configuration: Decodable {
/// Top level domain suffix. Usually "test" but can be set to something else. /// Top level domain suffix. Usually "test" but can be set to something else.
/// - Important: Does not include the actual dot. ("test", not ".test"!) /// - Important: Does not include the actual dot. ("test", not ".test"!)

View File

@ -0,0 +1,44 @@
//
// ValetSite+Fake.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 19/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension ValetSite {
convenience init(
fakeWithName name: String,
tld: String,
secure: Bool,
path: String,
linked: Bool,
driver: String = "Laravel (^9.0)",
constraint: String = "^8.1",
isolated: String? = nil
) {
self.init(name: name, tld: tld, absolutePath: path, aliasPath: nil, makeDeterminations: false)
self.secured = secure
self.composerPhp = constraint
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)
self.driver = driver
self.driverDeterminedByComposer = true
if linked {
self.aliasPath = self.absolutePath
}
if let isolated = isolated {
self.isolatedPhpVersion = PhpInstallation(isolated)
}
}
}

View File

@ -0,0 +1,234 @@
//
// Valet+Subclasses.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 22/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class ValetSite {
/// Name of the site. Does not include the TLD.
var name: String
/// 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: "~")
}()
/// The TLD used to locate this site.
var tld: String = "test"
/// The PHP version that is being used to serve this site specifically (if not global).
var isolatedPhpVersion: PhpInstallation?
/// Location of the alias. If set, this is a linked domain.
var aliasPath: String?
/// Whether the site has been secured.
var secured: Bool!
/// 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` or in .valetphprc.
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: VersionSource = .unknown
/// Which version of PHP is actually used to serve this site.
var servingPhpVersion: String {
return self.isolatedPhpVersion?.versionNumber.homebrewVersion
?? PhpEnv.phpInstall.version.short
}
enum VersionSource: String {
case unknown = "unknown"
case require = "require"
case platform = "platform"
case valetphprc = "valetphprc"
}
init(
name: String,
tld: String,
absolutePath: String,
aliasPath: String? = nil,
makeDeterminations: Bool = true
) {
self.name = name
self.tld = tld
self.absolutePath = absolutePath
self.aliasPath = aliasPath
self.secured = false
if makeDeterminations {
determineSecured()
determineComposerPhpVersion()
determineDriver()
determineIsolated()
}
}
convenience init(absolutePath: String, tld: String) {
let name = URL(fileURLWithPath: absolutePath).lastPathComponent
self.init(name: name, tld: tld, absolutePath: absolutePath)
}
convenience init(aliasPath: String, tld: String) {
let name = URL(fileURLWithPath: aliasPath).lastPathComponent
let absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
self.init(name: name, tld: tld, absolutePath: absolutePath, aliasPath: aliasPath)
}
/**
Determine whether a site is isolated.
*/
public func determineIsolated() {
if let version = ValetSite.isolatedVersion("~/.config/valet/Nginx/\(self.name).\(self.tld)") {
if (!PhpEnv.shared.cachedPhpInstallations.keys.contains(version)) {
Log.err("The PHP version \(version) is isolated for the site \(self.name) but that PHP version is unavailable.")
return
}
self.isolatedPhpVersion = PhpEnv.shared.cachedPhpInstallations[version]
} else {
self.isolatedPhpVersion = nil
}
}
/**
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() {
secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.name).\(self.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` or via .valetphprc)
- 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() {
self.determineComposerInformation()
self.determineValetPhpFileInfo()
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
}
}
}
/**
Checks the contents of the composer.json file and determine the notable dependencies,
as well as the requested PHP version. If no composer.json file is found, nothing happens.
*/
private func determineComposerInformation() {
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.")
}
}
/**
Checks the contents of the .valetphprc file and determine the version, if possible.
*/
private func determineValetPhpFileInfo() {
let path = "\(absolutePath)/.valetphprc"
do {
if Filesystem.fileExists(path) {
let contents = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
if let version = VersionExtractor.from(contents) {
self.composerPhp = version
self.composerPhpSource = .valetphprc
}
}
} catch {
Log.err("Something went wrong parsing the .valetphprc file")
}
}
// MARK: File Parsing
public static func isolatedVersion(_ filePath: String) -> String? {
if Filesystem.fileExists(filePath) {
return NginxConfigParser
.init(filePath: filePath)
.isolatedVersion
}
return nil
}
}

View File

@ -1,98 +0,0 @@
//
// MainMenu+Composer.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension MainMenu {
/**
Updates the global dependencies and runs the completion callback when done.
This method should probably be broken up into several smaller methods at some point.
*/
func updateGlobalDependencies(notify: Bool, completion: @escaping (Bool) -> Void) {
if !Shell.fileExists("/usr/local/bin/composer") {
Alert.notify(
message: "alert.composer_missing.title".localized,
info: "alert.composer_missing.info".localized
)
return
}
PhpEnv.shared.isBusy = true
setBusyImage()
self.rebuild()
let noLongerBusy = {
PhpEnv.shared.isBusy = false
DispatchQueue.main.async { [self] in
self.updatePhpVersionInStatusBar()
self.rebuild()
}
}
var window: ProgressWindowController? = ProgressWindowController.display(
title: "alert.composer_progress.title".localized,
description: "alert.composer_progress.info".localized
)
window?.setType(info: true)
DispatchQueue.global(qos: .userInitiated).async {
let task = Shell.user.createTask(
for: "/usr/local/bin/composer global update", requiresPath: true
)
DispatchQueue.main.async {
window?.addToConsole("/usr/local/bin/composer global update\n")
}
Shell.captureOutput(
task,
didReceiveStdOutData: { string in
DispatchQueue.main.async {
window?.addToConsole(string)
}
Log.perf("\(string.trimmingCharacters(in: .newlines))")
},
didReceiveStdErrData: { string in
DispatchQueue.main.async {
window?.addToConsole(string)
}
Log.perf("\(string.trimmingCharacters(in: .newlines))")
}
)
task.launch()
task.waitUntilExit()
Shell.haltCapturingOutput(task)
DispatchQueue.main.async {
if task.terminationStatus <= 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
window?.close()
if (notify) {
LocalNotification.send(
title: "alert.composer_success.title".localized,
subtitle: "alert.composer_success.info".localized
)
}
window = nil
noLongerBusy()
completion(true)
}
} else {
window?.setType(info: false)
window?.progressView?.labelTitle.stringValue = "alert.composer_failure.title".localized
window?.progressView?.labelDescription.stringValue = "alert.composer_failure.info".localized
window = nil
noLongerBusy()
completion(false)
}
}
}
}
}

View File

@ -0,0 +1,85 @@
//
// MainMenu+FixMyValet.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 20/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
extension MainMenu {
@objc func fixMyValet() {
let previousVersion = PhpEnv.phpInstall.version.short
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
presentAlertForMissingFormula()
return
}
if !BetterAlert()
.withInformation(
title: "alert.fix_my_valet.title".localized,
subtitle: "alert.fix_my_valet.info".localized(PhpEnv.brewPhpVersion)
)
.withPrimary(text: "alert.fix_my_valet.ok".localized)
.withSecondary(text: "alert.fix_my_valet.cancel".localized)
.didSelectPrimary()
{
Log.info("The user has chosen to abort Fix My Valet")
return
}
Actions.fixMyValet {
DispatchQueue.main.async {
if previousVersion == PhpEnv.brewPhpVersion {
self.presentAlertForSameVersion()
} else {
self.presentAlertForDifferentVersion(version: previousVersion)
}
}
}
}
private func presentAlertForMissingFormula() {
BetterAlert()
.withInformation(
title: "alert.php_formula_missing.title".localized,
subtitle: "alert.php_formula_missing.info".localized
)
.withPrimary(text: "OK")
.show()
}
private func presentAlertForSameVersion() {
BetterAlert()
.withInformation(
title: "alert.fix_my_valet_done.title".localized,
subtitle: "alert.fix_my_valet_done.subtitle".localized,
description: "alert.fix_my_valet_done.desc".localized
)
.withPrimary(text: "OK")
.show()
}
private func presentAlertForDifferentVersion(version: String) {
BetterAlert()
.withInformation(
title: "alert.fix_my_valet_done.title".localized,
subtitle: "alert.fix_my_valet_done.subtitle".localized,
description: "alert.fix_my_valet_done.desc".localized
)
.withPrimary(text: "alert.fix_my_valet_done.switch_back".localized(version), action: { alert in
alert.close(with: .alertSecondButtonReturn)
MainMenu.shared.switchToPhpVersion(version)
})
.withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnv.brewPhpVersion))
.withTertiary(text: "", action: { alert in
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
})
.show()
}
}

View File

@ -12,15 +12,16 @@ extension MainMenu {
/** /**
Kick off the startup of the rendering of the main menu. Kick off the startup of the rendering of the main menu.
*/ */
func startup() { func startup() async {
// Start with the icon // Start with the icon
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) DispatchQueue.main.async {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
}
// Perform environment boot checks if await Startup().checkEnvironment() {
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in self.onEnvironmentPass()
Startup().checkEnvironment(success: { onEnvironmentPass() }, } else {
failure: { onEnvironmentFail() } self.onEnvironmentFail()
)
} }
} }
@ -28,15 +29,27 @@ extension MainMenu {
When the environment is all clear and the app can run, let's go. When the environment is all clear and the app can run, let's go.
*/ */
private func onEnvironmentPass() { private func onEnvironmentPass() {
// Attempt to find out more info about Valet
if Valet.shared.version != nil {
Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)")
}
// Validate the version (this will enforce which versions of PHP are supported)
Valet.shared.validateVersion()
// Actually detect the PHP versions
PhpEnv.detectPhpVersions() PhpEnv.detectPhpVersions()
// Check for an alias conflict
if HomebrewDiagnostics.hasAliasConflict() { if HomebrewDiagnostics.hasAliasConflict() {
DispatchQueue.main.async { DispatchQueue.main.async {
Alert.notify( BetterAlert()
message: "alert.php_alias_conflict.title".localized, .withInformation(
info: "alert.php_alias_conflict.info".localized, title: "alert.php_alias_conflict.title".localized,
style: .critical subtitle: "alert.php_alias_conflict.info".localized
) )
.withPrimary(text: "OK")
.show()
} }
} }
@ -68,15 +81,19 @@ extension MainMenu {
// Load the global hotkey // Load the global hotkey
App.shared.loadGlobalHotkey() App.shared.loadGlobalHotkey()
// Attempt to find out more info about Valet // Preload sites
if Valet.shared.version != nil {
Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)")
}
Valet.shared.loadConfiguration()
Valet.shared.validateVersion()
Valet.shared.startPreloadingSites() Valet.shared.startPreloadingSites()
if (Valet.shared.config.tld != "test") {
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "alert.warnings.tld_issue.title".localized,
subtitle: "alert.warnings.tld_issue.subtitle".localized,
description: "alert.warnings.tld_issue.description".localized
).withPrimary(text: "OK").show()
}
}
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil) NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
Log.info("PHP Monitor is ready to serve!") Log.info("PHP Monitor is ready to serve!")
@ -101,18 +118,21 @@ extension MainMenu {
*/ */
private func onEnvironmentFail() { private func onEnvironmentFail() {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
let close = Alert.present(
messageText: "alert.cannot_start.title".localized,
informativeText: "alert.cannot_start.info".localized,
buttonTitle: "alert.cannot_start.close".localized,
secondButtonTitle: "alert.cannot_start.retry".localized
)
if (close) { BetterAlert()
exit(1) .withInformation(
} title: "alert.cannot_start.title".localized,
subtitle: "alert.cannot_start.subtitle".localized,
description: "alert.cannot_start.description".localized
)
.withPrimary(text: "alert.cannot_start.retry".localized)
.withSecondary(text: "alert.cannot_start.close".localized, action: { vc in
vc.close(with: .alertSecondButtonReturn)
exit(1)
})
.show()
startup() Task { await startup() }
} }
} }
} }

View File

@ -39,9 +39,13 @@ extension MainMenu {
// Run composer updates // Run composer updates
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) { if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
self.updateGlobalDependencies(notify: false, completion: { _ in ComposerWindow().updateGlobalDependencies(
self.notifyAboutVersionChange(to: version) notify: false,
}) completion: { _ in
self.notifyAboutVersionChange(to: version)
}
)
} else { } else {
self.notifyAboutVersionChange(to: version) self.notifyAboutVersionChange(to: version)
} }
@ -52,12 +56,15 @@ extension MainMenu {
} }
} }
private func suggestFixMyValet(failed version: String) { @MainActor private func suggestFixMyValet(failed version: String) {
let outcome = Alert.present( let outcome = BetterAlert()
messageText: "alert.php_switch_failed.title".localized(version), .withInformation(
informativeText: "alert.php_switch_failed.info".localized(version), title: "alert.php_switch_failed.title".localized(version),
buttonTitle: "alert.php_switch_failed.confirm".localized, subtitle: "alert.php_switch_failed.info".localized(version)
secondButtonTitle: "alert.php_switch_failed.cancel".localized, style: .informational) )
.withPrimary(text: "alert.php_switch_failed.confirm".localized)
.withSecondary(text: "alert.php_switch_failed.cancel".localized)
.didSelectPrimary()
if outcome { if outcome {
MainMenu.shared.fixMyValet() MainMenu.shared.fixMyValet()
} }

View File

@ -145,7 +145,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
/** Refreshes the icon with the PHP version. */ /** Refreshes the icon with the PHP version. */
@objc func refreshIcon() { @objc func refreshIcon() {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
if (App.busy) { if (PhpEnv.shared.isBusy) {
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
} else { } else {
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false { if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
@ -170,26 +170,31 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
// MARK: - Actions // MARK: - Actions
@objc func fixHomebrewPermissions() { @objc func fixHomebrewPermissions() {
if !Alert.present( if !BetterAlert()
messageText: "alert.fix_homebrew_permissions.title".localized, .withInformation(
informativeText: "alert.fix_homebrew_permissions.info".localized, title: "alert.fix_homebrew_permissions.title".localized,
buttonTitle: "alert.fix_homebrew_permissions.ok".localized, subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
secondButtonTitle: "alert.fix_homebrew_permissions.cancel".localized, description: "alert.fix_homebrew_permissions.desc".localized
style: .warning )
) { .withPrimary(text: "alert.fix_homebrew_permissions.ok".localized)
.withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized)
.didSelectPrimary() {
return return
} }
asyncExecution { asyncExecution {
try Actions.fixHomebrewPermissions() try Actions.fixHomebrewPermissions()
} success: { } success: {
Alert.notify( BetterAlert()
message: "alert.fix_homebrew_permissions_done.title".localized, .withInformation(
info: "alert.fix_homebrew_permissions_done.info".localized, title: "alert.fix_homebrew_permissions_done.title".localized,
style: .warning subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
) description: "alert.fix_homebrew_permissions_done.desc".localized
)
.withPrimary(text: "OK")
.show()
} failure: { error in } failure: { error in
Alert.notify(about: error as! HomebrewPermissionError) BetterAlert.show(for: error as! HomebrewPermissionError)
} }
} }
@ -259,30 +264,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
} }
} }
@objc func fixMyValet() {
if !Alert.present(
messageText: "alert.fix_my_valet.title".localized,
informativeText: "alert.fix_my_valet.info".localized(PhpEnv.brewPhpVersion),
buttonTitle: "alert.fix_my_valet.ok".localized,
secondButtonTitle: "alert.fix_my_valet.cancel".localized,
style: .warning
) {
Log.info("The user has chosen to abort Fix My Valet")
return
}
asyncExecution {
Actions.fixMyValet()
} success: {
Alert.notify(
message: "alert.fix_my_valet_done.title".localized,
info: "alert.fix_my_valet_done.info".localized
)
}
}
@objc func updateGlobalComposerDependencies() { @objc func updateGlobalComposerDependencies() {
self.updateGlobalDependencies(notify: true, completion: { _ in }) ComposerWindow().updateGlobalDependencies(
notify: true,
completion: { _ in }
)
} }
@objc func openActiveConfigFolder() { @objc func openActiveConfigFolder() {
@ -342,7 +328,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
} }
@objc func openDonate() { @objc func openDonate() {
NSWorkspace.shared.open(Constants.DonationUrl) NSWorkspace.shared.open(Constants.Urls.DonationPage)
} }
@objc func terminateApp() { @objc func terminateApp() {
@ -354,6 +340,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
func menuWillOpen(_ menu: NSMenu) { func menuWillOpen(_ menu: NSMenu) {
// Make sure the shortcut key does not trigger this when the menu is open // Make sure the shortcut key does not trigger this when the menu is open
App.shared.shortcutHotkey?.isPaused = true App.shared.shortcutHotkey?.isPaused = true
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
} }
func menuDidClose(_ menu: NSMenu) { func menuDidClose(_ menu: NSMenu) {

View File

@ -44,36 +44,15 @@ class ServicesView: NSView, XibLoadable {
) )
return item return item
} }
override func viewWillDraw() {
super.viewWillDraw()
self.loadData()
}
@objc func updateInformation() { @objc func updateInformation() {
self.loadData() self.loadData()
} }
// TODO: (5.1) Move data fetching, caching & retrieval somewhere else
func loadData() { func loadData() {
// Use stale data
self.applyAllInfoFieldsFromCachedValue() self.applyAllInfoFieldsFromCachedValue()
HomebrewService.loadAll { services in
// Re-fetch services ServicesView.services = Dictionary(uniqueKeysWithValues: services.map{ ($0.name, $0) })
runAsync {
let servicesList = try! JSONDecoder().decode(
[HomebrewService].self,
from: Shell.pipe(
"sudo \(Paths.brew) services info --all --json",
requiresPath: true
).data(using: .utf8)!
).filter({ service in
return [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"].contains(service.name)
})
ServicesView.services = Dictionary(uniqueKeysWithValues: servicesList.map{ ($0.name, $0) })
} completion: {
// Use fresh data
self.applyAllInfoFieldsFromCachedValue() self.applyAllInfoFieldsFromCachedValue()
} }
} }

View File

@ -22,7 +22,7 @@ class StatusMenu : NSMenu {
} }
func addPhpActionMenuItems() { func addPhpActionMenuItems() {
if App.busy { if PhpEnv.shared.isBusy {
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: "")) addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
return return
} }
@ -97,26 +97,20 @@ class StatusMenu : NSMenu {
func addFirstAidAndServicesMenuItems() { func addFirstAidAndServicesMenuItems() {
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "") let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
let servicesMenu = NSMenu() let servicesMenu = NSMenu()
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_first_aid".localized))
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) { let fixMyValetMenuItem = NSMenuItem(
servicesMenu.addItem(NSMenuItem( title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
title: "mi_fix_my_valet_unavailable".localized(PhpEnv.brewPhpVersion), action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
action: nil, keyEquivalent: "f")
)
} else {
servicesMenu.addItem(NSMenuItem(
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
action: #selector(MainMenu.fixMyValet), keyEquivalent: "")
)
}
/* (disabled until v5.1 and further tweaking)
servicesMenu.addItem(NSMenuItem(
title: "mi_fix_brew_permissions".localized(),
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: "")
) )
*/ fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized
servicesMenu.addItem(fixMyValetMenuItem)
let fixHomebrewMenuItem = NSMenuItem(
title: "mi_fix_brew_permissions".localized(),
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: ""
)
fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized
servicesMenu.addItem(fixHomebrewMenuItem)
servicesMenu.addItem(NSMenuItem.separator()) servicesMenu.addItem(NSMenuItem.separator())
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized)) servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
@ -150,10 +144,10 @@ class StatusMenu : NSMenu {
// Get the short and long version // Get the short and long version
let shortVersion = PhpEnv.shared.availablePhpVersions[index] let shortVersion = PhpEnv.shared.availablePhpVersions[index]
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.longVersion let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
let versionString = long ? longVersion.homebrewVersion : shortVersion let versionString = long ? longVersion.toString() : shortVersion
let action = #selector(MainMenu.switchToPhpVersion(sender:)) let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)" let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"

View File

@ -0,0 +1,119 @@
//
// Notice.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class BetterAlert {
var windowController: NSWindowController!
var noticeVC: BetterAlertVC {
return self.windowController.contentViewController as! BetterAlertVC
}
init() {
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
self.windowController = storyboard.instantiateController(
withIdentifier: "noticeWindow"
) as? NSWindowController
}
public static func make() -> BetterAlert {
return BetterAlert()
}
public func withPrimary(
text: String,
action: @escaping (BetterAlertVC) -> Void = {
vc in vc.close(with: .alertFirstButtonReturn)
}
) -> Self {
self.noticeVC.buttonPrimary.title = text
self.noticeVC.actionPrimary = action
return self
}
public func withSecondary(
text: String,
action: ((BetterAlertVC) -> Void)? = {
vc in vc.close(with: .alertSecondButtonReturn)
}
) -> Self {
self.noticeVC.buttonSecondary.title = text
self.noticeVC.actionSecondary = action
return self
}
public func withTertiary(
text: String = "",
action: ((BetterAlertVC) -> Void)? = nil
) -> Self {
if text == "" {
self.noticeVC.buttonTertiary.bezelStyle = .helpButton
}
self.noticeVC.buttonTertiary.title = text
self.noticeVC.actionTertiary = action
return self
}
public func withInformation(
title: String,
subtitle: String,
description: String = ""
) -> Self {
self.noticeVC.labelTitle.stringValue = title
self.noticeVC.labelSubtitle.stringValue = subtitle
self.noticeVC.labelDescription.stringValue = description
// If the description is missing, handle the excess space and change the top margin
if (description == "") {
self.noticeVC.labelDescription.isHidden = true
self.noticeVC.primaryButtonTopMargin.constant = 0
}
return self
}
/**
Shows the modal and returns a ModalResponse.
If you wish to simply show the alert and disregard the outcome, use `show`.
*/
public func runModal() -> NSApplication.ModalResponse {
if !Thread.isMainThread {
fatalError("You should always present alerts on the main thread!")
}
NSApp.activate(ignoringOtherApps: true)
windowController.window?.makeKeyAndOrderFront(nil)
return NSApplication.shared.runModal(for: windowController.window!)
}
/** Shows the modal and returns true if the user pressed the primary button. */
public func didSelectPrimary() -> Bool {
return self.runModal() == .alertFirstButtonReturn
}
/**
Shows the modal and does not return anything.
*/
public func show() {
_ = self.runModal()
}
/**
Shows the modal for a particular error.
*/
public static func show(for error: Error & AlertableError) {
let key = error.getErrorMessageKey()
return BetterAlert().withInformation(
title: "\(key).title".localized,
subtitle: "\(key).description".localized
).withPrimary(text: "OK").show()
}
}

View File

@ -0,0 +1,79 @@
//
// NoticeVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class BetterAlertVC: NSViewController {
// MARK: - Outlets
@IBOutlet weak var labelTitle: NSTextField!
@IBOutlet weak var labelSubtitle: NSTextField!
@IBOutlet weak var labelDescription: NSTextField!
@IBOutlet weak var buttonPrimary: NSButton!
@IBOutlet weak var buttonSecondary: NSButton!
@IBOutlet weak var buttonTertiary: NSButton!
var actionPrimary: (BetterAlertVC) -> Void = { _ in }
var actionSecondary: ((BetterAlertVC) -> Void)?
var actionTertiary: ((BetterAlertVC) -> Void)?
@IBOutlet weak var imageView: NSImageView!
@IBOutlet weak var primaryButtonTopMargin: NSLayoutConstraint!
// MARK: - Lifecycle
override func viewWillAppear() {
imageView.image = NSApp.applicationIconImage
if actionSecondary == nil {
buttonSecondary.isHidden = true
}
if actionTertiary == nil {
buttonTertiary.isHidden = true
}
}
override func viewDidAppear() {
view.window?.makeFirstResponder(buttonPrimary)
}
deinit {
Log.perf("A BetterAlert has been deinitialized.")
}
// MARK: Outlet Actions
@IBAction func primaryButtonAction(_ sender: Any) {
self.actionPrimary(self)
}
@IBAction func secondaryButtonAction(_ sender: Any) {
if self.actionSecondary != nil {
self.actionSecondary!(self)
} else {
self.close(with: .alertSecondButtonReturn)
}
}
@IBAction func tertiaryButtonAction(_ sender: Any) {
if self.actionSecondary != nil {
self.actionTertiary!(self)
}
}
public func close(with code: NSApplication.ModalResponse) {
self.view.window?.close()
NSApplication.shared.stopModal(withCode: code)
}
}

View File

@ -21,11 +21,14 @@ extension ActivePhpInstallation {
public func notifyAboutBrokenPhpFpm() { public func notifyAboutBrokenPhpFpm() {
if !self.checkPhpFpmStatus() { if !self.checkPhpFpmStatus() {
DispatchQueue.main.async { DispatchQueue.main.async {
Alert.notify( BetterAlert()
message: "alert.php_fpm_broken.title".localized, .withInformation(
info: "alert.php_fpm_broken.info".localized, title: "alert.php_fpm_broken.title".localized,
style: .critical subtitle: "alert.php_fpm_broken.info".localized,
) description: "alert.php_fpm_broken.description".localized
)
.withPrimary(text: "OK")
.show()
} }
} }
} }

View File

@ -7,7 +7,6 @@
// //
import Cocoa import Cocoa
import HotKey
import Carbon import Carbon
class PrefsVC: NSViewController { class PrefsVC: NSViewController {

View File

@ -86,6 +86,7 @@ class Stats {
(see `didSeeSponsorEncouragement`) (see `didSeeSponsorEncouragement`)
*/ */
public static func evaluateSponsorMessageShouldBeDisplayed() { public static func evaluateSponsorMessageShouldBeDisplayed() {
if Bundle.main.bundleIdentifier?.contains("beta") ?? false { if Bundle.main.bundleIdentifier?.contains("beta") ?? false {
return Log.info("Sponsor messages never apply to beta builds.") return Log.info("Sponsor messages never apply to beta builds.")
} }
@ -99,16 +100,24 @@ class Stats {
} }
DispatchQueue.main.async { DispatchQueue.main.async {
let donate = Alert.present( let donate = BetterAlert()
messageText: "startup.sponsor_encouragement.title".localized, .withInformation(
informativeText: "startup.sponsor_encouragement.desc".localized, title: "startup.sponsor_encouragement.title".localized,
buttonTitle: "startup.sponsor_encouragement.accept".localized, subtitle: "startup.sponsor_encouragement.subtitle".localized,
secondButtonTitle: "startup.sponsor_encouragement.skip".localized, description: "startup.sponsor_encouragement.desc".localized
style: .informational) )
.withPrimary(text: "startup.sponsor_encouragement.accept".localized)
.withSecondary(text: "startup.sponsor_encouragement.skip".localized)
.withTertiary(text: "", action: { vc in
vc.close(with: .alertThirdButtonReturn)
NSWorkspace.shared.open(Constants.Urls.DonationPage)
}).didSelectPrimary()
if donate { if donate {
Log.info("The user is an absolute badass for choosing this option. Thank you.") Log.info("The user is an absolute badass for choosing this option. Thank you.")
NSWorkspace.shared.open(Constants.DonationUrl) NSWorkspace.shared.open(Constants.Urls.DonationPayment)
} }
UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue) UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue)
} }
} }

View File

@ -9,7 +9,6 @@
import Foundation import Foundation
import Foundation import Foundation
import HotKey
import Cocoa import Cocoa
class HotkeyPreferenceView: NSView, XibLoadable { class HotkeyPreferenceView: NSView, XibLoadable {

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