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

Compare commits

..

235 Commits

Author SHA1 Message Date
e29ca9e1ad 🚀 Version 5.6.5 2022-11-27 14:37:15 +01:00
40e05d9445 🔧 Bump build 2022-11-27 14:04:15 +01:00
3ebf51b319 👌 Fix threading issue with Composer update (#212) 2022-11-27 14:03:46 +01:00
4b8ad911f1 Add support for nginx-full (#211) 2022-11-21 18:10:33 +01:00
efd902b4f3 🚀 Version 5.6.4 2022-11-18 18:39:00 +01:00
918e272da7 🔧 Bump build 2022-11-18 18:37:31 +01:00
272a9182d3 🔥 Remove reference to LatestStablePhpVersion 2022-11-18 18:37:20 +01:00
63f85aff91 🐛 Fix issue when using services quick toggle 2022-11-18 18:32:38 +01:00
581c2d1974 📝 Update supported PHP versions in README 2022-11-07 20:45:58 +01:00
4deef64537 📝 Update SECURITY.md 2022-11-02 21:08:57 +01:00
bedabaa3bb 👌 PHP 8.2 release, PHP 8.3-dev support 2022-11-02 21:06:36 +01:00
9da3772212 🚀 Version 5.6.3 2022-10-22 17:34:53 +02:00
e62b03d070 👌 Style fix 2022-10-22 16:43:05 +02:00
9a11d2efed 🔧 Bump build 2022-10-22 16:42:26 +02:00
b134e62328 🐛 Handle empty output for brew info 2022-10-22 16:41:54 +02:00
5c69133c42 👌 Add brew tap homebrew/services instruction
This now recommends the appropriate solution for #208.
2022-10-20 20:44:44 +02:00
f4448e0640 🔧 Bump version number 2022-10-10 21:49:51 +02:00
7fd30d7c54 Add preference to disable TLD alert (#206) 2022-10-10 21:49:43 +02:00
2c57dea97f 🔀 Merge branch 'main' into dev/5.6 2022-10-09 22:00:36 +02:00
a77fa5557a 📝 Update README for Login Items on Ventura 2022-10-09 21:56:13 +02:00
45704fc736 🚀 Version 5.6.2 2022-10-02 13:28:58 +02:00
f28354e634 🐛 Use valet secure sitename (#197) 2022-10-02 13:28:01 +02:00
8055a32bde 🐛 Fix ComposerWindow deinit not firing 2022-09-29 18:50:40 +02:00
5b3054326e 🔧 Bump version number 2022-09-28 18:24:26 +02:00
e7f3c7e59c 🐛 Fix an issue with missing separator item 2022-09-28 18:24:01 +02:00
a407515534 🚀 Version 5.6.1 2022-09-24 12:56:14 +02:00
b827ffb869 🔧 Bump version number 2022-09-23 16:46:23 +02:00
bdb718598e 🐛 Various bugfixes
- Fixes issue with `scanApps` being non-optional in custom configuration
- Fixes issue with position of separator if Xdebug is not detected
- Ensure that `isRunningSwiftUIPreview` modifier always return false for debug builds
2022-09-23 16:46:13 +02:00
ddfc73e033 🐛 Resolve issue with determining PATH (#194)
In previous builds, PHP Monitor would use interactive mode when opening
a /bin/zsh shell, in order to be able to load the full PATH.

This is a problem because when launching PHP Monitor via the command
line or when you have some interactivity in an actual interactive
shell, it is possible to get stuck waiting for a particular input
which will effectively 'freeze' PHP Monitor's shell.

There are two options to fix this:
1) work with a timeout, which may or may not return a PATH
2) use a non-interactive shell and source .zshrc

I chose option 2, which is the more robust choice. If no .zshrc file
exists, it is also not sourced to avoid warnings or errors from ending
up in the PATH.
2022-09-23 16:44:06 +02:00
cfae520984 👌 Correctly resolve tagged version (#195) 2022-09-22 18:43:09 +02:00
0c176493e5 🔀 Merge branch 'main' into dev/5.6 2022-09-18 14:08:48 +02:00
71da62f954 📝 Update contribution guidelines 2022-09-18 14:08:10 +02:00
d6781568a3 📝 Update README 2022-09-18 13:39:40 +02:00
c9c7e14416 👌 Add support for <= and < version constraints
I am entirely unsure why one would need these, but I figured I'd get
these in the app before I start the work on PHP Monitor 6.0.

This ensures all common version constraints can now be parsed correctly.
2022-09-18 00:05:37 +02:00
0947dc5ecc 🚀 Version 5.6 2022-09-16 19:33:42 +02:00
286cdd00e9 🔧 Prepare for release 2022-09-16 19:31:32 +02:00
42b79d3cb3 🔧 Upgrade to Xcode 14 2022-09-10 21:44:17 +02:00
36aa41568c 🐛 Fix issue with minimum width w/ hidden UI 2022-09-10 21:43:26 +02:00
273c51f702 🔧 Update credits URLs 2022-09-09 21:41:20 +02:00
186f80c90e Allow more UI tweaking 2022-09-08 18:14:22 +02:00
d93af814c9 👌 Fix extension visibility of NSMenu 2022-09-08 17:21:41 +02:00
f4885f7dbc 👌 Update pre-release PHP version notice 2022-09-08 17:20:30 +02:00
805c9f5e6a Add new Preferences panel for UI tweaking 2022-09-07 21:40:43 +02:00
183d0bbc30 Allow hiding of global version switcher 2022-09-07 21:14:56 +02:00
4855c14d28 ♻️ Refactor Preferences & PreferenceName
Also added a few new preferences related to toggling specific menu items
based on your personal choices.

(These new settings still need to be added to the UI.)
2022-09-07 20:33:23 +02:00
588398ea76 🍱 New screenshots with updated UI 2022-09-06 18:02:27 +02:00
d2502cfba2 Improved site isolation switch (#191) 2022-09-05 21:09:31 +02:00
8d46fa8a4e 👌 Right-clicking a row selects it in domain list 2022-09-05 20:05:16 +02:00
e59b89ea49 🔧 Tweak URLs to reference phpmon.app domain 2022-09-05 19:55:41 +02:00
18b103bf9c 🚀 Version 5.5.1
This release fixes an issue that could occur if your username and your
home directory have different names. (#189)
2022-09-01 17:30:55 +02:00
9b8de47f5d Fix file membership so all tests pass again 2022-09-01 17:27:17 +02:00
da2934c2e5 Fix file membership so all tests pass again 2022-08-31 17:08:51 +02:00
9de4fc6712 🐛 Don't use whoami and use NSHomeDirectory()
- This commit replaces the usage of `whoami` with `id -un`.
- This also changes all `~` replacements with the result of calling
  the `NSHomeDirectory()` which may differ from `id -un` (#189)
2022-08-29 18:15:13 +02:00
4f4c950349 🐛 Don't use whoami and use NSHomeDirectory()
- This commit replaces the usage of `whoami` with `id -un`.
- This also changes all `~` replacements with the result of calling
  the `NSHomeDirectory()` which may differ from `id -un` (#189)
2022-08-29 18:06:11 +02:00
331ca8a9a4 🚀 Version 5.5 2022-08-28 15:24:28 +02:00
87e08e4607 🔧 Use icon for stable build 2022-08-24 20:33:35 +02:00
845235a276 🔧 Bump build number for new build 2022-08-24 20:28:12 +02:00
7587126a42 🐛 Potential fix for parsing Valet version (#188) 2022-08-24 20:27:40 +02:00
f4b1e0745a 🐛 Potential fix for parsing Valet version (#188) 2022-08-24 20:27:19 +02:00
c7bb4c1d37 ♻️ Refactor submenu creation 2022-08-22 19:10:36 +02:00
a17512bfad 📝 Update README 2022-08-22 18:48:43 +02:00
a85e016b4a 📝 Update README 2022-08-22 18:47:47 +02:00
1292e91b33 ♻️ Cleanup 2022-08-18 17:38:46 +02:00
8c55fee18d ♻️ Refactor NSMenu structuring 2022-08-18 02:12:24 +02:00
663082d725 ♻️ Refactor NSMenu structuring
* Added a different way to create menus
* Added NoDomainResultsView (WIP)
2022-08-18 01:59:06 +02:00
fcdb4a8993 Allow opening of PHP Doctor via First Aid menu 2022-08-16 19:35:18 +02:00
9134f08ec9 👌 Fix no warnings view 2022-08-16 19:29:15 +02:00
b281df3bbd ♻️ Correctly refresh warnings 2022-08-15 01:57:22 +02:00
a9f9c38e0d ♻️ Rework how the user's PATH is loaded 2022-08-15 01:47:55 +02:00
bbbdce6b44 ♻️ Reworked helper scripts
- Add 'Welcome Tour' to First Aid menu
- Updated 'Welcome Tour'
- Helpers are now always written to ~/.config/phpmon/bin
- Updated helpers (now symlinked)
- Updated checks for when to symlink helpers
2022-08-14 23:57:46 +02:00
237185995d 🔥 Removed assets from tour 2022-08-14 22:23:39 +02:00
48f950fc3a 🐛 Fix translation string (#185)
This ensures that the "failed to check for update" modal now displays
the correct localized string.
2022-08-14 22:22:06 +02:00
2ee0934080 🔧 Make PHP Doctor optional (preference) 2022-08-13 23:16:10 +02:00
cfbb83976d 👌 Tweak preview 2022-08-12 21:07:38 +02:00
4e5b178e36 🏗 WIP: Tweaks to PHP Doctor 2022-08-12 20:50:27 +02:00
0c59d14da5 🐛 Fix isWriteableFile:atPath 2022-08-11 22:41:47 +02:00
2a9c8e6830 Check if /usr/local/bin helper is writable 2022-08-10 21:20:11 +02:00
78e750d764 Check if running the app with Rosetta 2022-08-10 21:04:23 +02:00
c1c7561361 🏗 WIP: Warning manager 2022-08-09 21:55:59 +02:00
f90925ee17 🏗 WIP: Warnings window & views 2022-08-09 20:31:17 +02:00
ccfb15c83c ♻️ Use WindowController instead of WC
- Renamed `ProgressWC` to `TerminalProgressWindowController` so it
  better reflects the functionality of the class.

- Renamed all `-WC` suffixed classes to `-WindowController`. This is
  cleaner because it avoids referencing water closets. 🚽

- Fixed some instances where the copyright notice used the wrong
  filename. All window controller classes are now accurate.
2022-08-09 18:04:18 +02:00
bcc80b5210 👌 More SwiftUI tuning (automatic height) 2022-08-05 20:05:29 +02:00
023043a81d 🍱 Further tweaks to onboarding view 2022-08-03 19:10:13 +02:00
a2c93833df 🍱 Tweaked onboarding view 2022-08-01 22:39:21 +02:00
8b6267f411 👌 Fix WarningView styling 2022-08-01 21:18:20 +02:00
1bff75311b Add WarningView 2022-07-31 21:27:52 +02:00
7a580eef0c 📝 Update README 2022-07-29 18:14:24 +02:00
2306529936 ♻️ Differentiate between folder and file existence
This also fixes #182, because it introduces a check for Valet's config
directory, which does not exist if Valet has not been installed yet.
2022-07-28 21:26:41 +02:00
08fdcdbc6c 👌 Simple cleanup 2022-07-27 20:38:39 +02:00
8cb8e5e409 🍱 Visual changes to onboarding screen 2022-07-26 20:30:19 +02:00
6094f83e98 👌 Do not require window from storyboard 2022-07-26 19:36:27 +02:00
cdb4b60487 👌 Add warning message for WIP launch screen 2022-07-26 19:34:30 +02:00
ae5bed2532 🔀 Merge branch 'feature/onboarding' into dev/5.5 2022-07-26 19:32:45 +02:00
f098ffbf3d ♻️ Consistency in naming of window controllers 2022-07-26 19:30:35 +02:00
0a55b45c60 🔧 Prepare 5.5 branch for dev builds 2022-07-25 21:40:43 +02:00
418d1e2479 Add support for custom environment vars (#183)
This commit adds support for custom environment variables, which can be
set in PHP Monitor's custom configuration file.

Let's say you wish to customize the `COMPOSER_HOME` env variable, like
in #183. You can fix this like so:

```json
{
    "scan_apps": [],
    "services": [],
    "presets": []
    "export": {
        "COMPOSER_HOME": "/absolute/path/to/composer/folder"
    }
}
```

Please note that while it is possible to set the `PATH` this way, you
WILL MOST CERTAINLY break PHP Monitor in the process. Setting other
environment variables should generally not pose an issue, unless said
environment variable affects the output of the shell that PHP Monitor
uses internally.
2022-07-25 21:40:02 +02:00
fa3ec2aaa3 🏗 WIP: Present OnboardingWC if launch count >= 1 2022-07-25 21:11:24 +02:00
a1df2deec5 👌 Cleanup 2022-07-25 20:48:26 +02:00
8fb43f7a16 🍱 Fix indentation 2022-07-13 22:22:30 +02:00
8a8d32cb5d 🍱 Tweaked onboarding screen 2022-07-13 11:27:32 +02:00
6e3bb1d322 Added onboarding sample 2022-07-08 21:13:46 +02:00
cac7048d92 👌 Use MainActor for methods 2022-07-08 20:44:40 +02:00
1aa051e12c 🔀 Merge branch 'dev/5.4' into dev/5.5 2022-07-08 20:42:28 +02:00
58fd045bdf 🚀 Version 5.4.1 2022-07-08 14:07:37 +02:00
2fc71303df 👌 Update README, improve modals 2022-07-08 14:04:35 +02:00
2c61d991d6 👌 Improve global composer check 2022-07-07 23:22:32 +02:00
beca09b76f 📝 Updated README 2022-06-29 15:32:52 +02:00
5c3e856a7b 🐛 Check for platform issue after switch (#178) 2022-06-29 15:09:36 +02:00
6fbbd499f8 🐛 Gracefully detect #178 during startup 2022-06-29 13:28:12 +02:00
1066bdc653 🚀 Version 5.4 2022-06-28 12:58:36 +02:00
4ef5918b7a 🔀 Merge branch 'dev/5.4' into dev/5.5 2022-06-23 17:36:22 +02:00
e45db50f48 🔧 Final dev build 2022-06-23 17:30:54 +02:00
8a7f045bb2 📝 New screenshots (including dark mode) 2022-06-22 19:21:33 +02:00
10272f3d7e 📝 Update README 2022-06-21 23:18:15 +02:00
96ce462021 📝 Update README and SECURITY 2022-06-20 19:10:43 +02:00
8b635f2a14 🔧 Bump version number 2022-06-20 18:53:54 +02:00
f8f3fd5c9b 👌 Improve macOS Ventura appearance (#176) 2022-06-20 18:49:43 +02:00
6801283597 🐛 Fix typo when PHP switch failed 2022-06-19 13:39:25 +02:00
cf03727dc0 🏗 Use @MainActor on MainMenu 2022-06-19 13:08:54 +02:00
25a7dded73 🐛 Fix typo when PHP switch failed 2022-06-19 13:08:19 +02:00
c5fd43312f 🏗 Use @MainActor for BetterAlert 2022-06-18 17:59:43 +02:00
ea7572ffbb 👌 Fix log message 2022-06-15 18:15:17 +02:00
4d5b5de84b 👌 Copy file, do not move 2022-06-15 17:54:08 +02:00
721bb32087 👌 Updated menu 2022-06-14 23:13:41 +02:00
1fe086d53e 👌 Move legacy .phpmon.conf.json to new path 2022-06-14 22:48:07 +02:00
347a9b7345 📝 Updated README about presets 2022-06-14 22:35:03 +02:00
e0c087dbcb 🚀 Version 5.3.2 2022-06-14 18:23:09 +02:00
fd34deb7bd 👌 Toggle service via button 2022-06-12 22:26:35 +02:00
ccb0d96453 ️ Use EmptyView for empty state 2022-06-12 13:37:27 +02:00
d821968a49 👌 Allow per-row customization 2022-06-12 13:26:24 +02:00
7d7a38546a 🐛 Apply fixes from v5.3.2 2022-06-12 12:07:24 +02:00
30059353fe New Features
* Differentiate between services running as root and current user
* Support for custom services (via config.json)
* Renamed "Restart All Services" to "Restart Valet Services"
* Use SwiftUI for Stats, Services and Header view
* Added Color extension for debugging (PAINT_PHPMON_SWIFTUI_VIEWS)
2022-06-11 23:18:36 +02:00
090440abc8 ♻️ Rename manager property 2022-06-11 20:19:45 +02:00
ceeba611d0 🏗 WIP 2022-06-11 20:11:11 +02:00
f1feb11baa 🏗 WIP 2022-06-11 13:27:17 +02:00
90a02f364a 🏗 WIP 2022-06-10 19:44:31 +02:00
6b8c68b058 👌 Tweak phrasing of isolation info 2022-06-09 18:22:29 +02:00
fb34ea7c89 👌 New StatsView which uses SwiftUI 2022-06-09 18:08:14 +02:00
507d4785aa 🚀 Version 5.3.1 2022-06-08 18:36:24 +02:00
0c0045aead 👌 Re-check compatibility after (un)isolate 2022-06-08 18:35:27 +02:00
1fdf687c15 👌 Handle additional states 2022-06-08 18:26:06 +02:00
1040bcd037 👌 Cleanup 2022-06-08 16:01:31 +02:00
655cd52ac4 👌 Popover with SwiftUI view 2022-06-08 15:38:18 +02:00
2229f01eb0 🔧 Learn more about presets 2022-06-07 20:10:49 +02:00
f95eb7023f 🔧 Global accent color based on icon color 2022-06-05 22:18:14 +02:00
320e1ad3c5 🔧 New dev build 2022-06-05 14:22:59 +02:00
e1880d9dfc 👌 Granular notification control 2022-06-05 14:21:08 +02:00
998bbd231a ♻️ Refactor preferences window 2022-06-05 12:52:59 +02:00
fbf2158488 👌 Improved alerts, localization 2022-06-05 03:03:00 +02:00
94df551b4b 👌 Show alert with what will be rolled back 2022-06-03 23:52:40 +02:00
01288154a7 👌 Better alerts (WIP) 2022-06-03 22:57:22 +02:00
29b4fe2962 Load persisted revert & allow revert
This also moves the location of the .phpmon.conf.json file to a new
location: ~/.config/phpmon/config.json.
2022-06-02 20:31:01 +02:00
5907d9f689 Build revertable preset (#175)
For any given preset, we need to be able to determine what we'd need to
do in order to revert the preset. That means figuring out what the diff
is between the current PHP setup and what the preset would change.

We'll persist that to its own preset, which can be reapplied if needed.
The "revertable" preset is persisted to the following file:

    ~/.config/phpmon/preset_revert.json

If that file is present and valid, the app should enable the 'revert'
option. (That still needs to be implemented.)
2022-06-01 21:01:18 +02:00
86d74619b1 📝 Use non-standard way to add dark images 2022-06-01 12:47:54 +02:00
0c09e808bd 📝 Test dark mode image 2022-06-01 12:44:22 +02:00
da8659adba Enable version switching in presets
* Moved Preset to dedicated file
* Added async friendly PHP version switch
* Added conditional PHP switch based on Preset
2022-05-31 21:43:24 +02:00
bbebe78997 Updated UI for presets 2022-05-30 19:34:10 +02:00
19aa804cbb 🐛 Alert user about issue #174 2022-05-29 22:30:11 +02:00
64491c6fe1 Allow application of presets 2022-05-29 12:32:48 +02:00
382cb177be Correctly load presets from config file 2022-05-21 15:24:40 +02:00
e9f0d19d9a 🔧 Use dev icon 2022-05-19 20:12:45 +02:00
7709cd9f6c 👌 Cleanup 2022-05-19 20:12:30 +02:00
83eac7bf04 👌 Save multiple Xdebug modes 2022-05-19 20:01:28 +02:00
db8197df3d 👌 Handle multiple modes w/ Xdebug menu item
This commit also fixes the width of the header items.
2022-05-19 19:06:03 +02:00
40e404fe24 👌 Prototype (non-functional) for presets 2022-05-19 01:50:15 +02:00
e7f80ebce8 Switch Xdebug mode 2022-05-19 01:05:06 +02:00
990152d77d Allow replacing of config values 2022-05-19 00:08:26 +02:00
2e61479c75 Allow reading of configuration files 2022-05-18 19:45:16 +02:00
0579ebb1c1 ️ More efficient extension parsing 2022-05-18 19:23:56 +02:00
f9df86851c Tweak description about sudoers files 2022-05-15 15:42:01 +02:00
b0c62e226a 👌 Code cleanup 2022-05-15 15:15:49 +02:00
1392b6e4a0 🔧 New version under development 2022-05-13 17:04:45 +02:00
12163bc87b 🔀 Merge branch 'main' into dev/5.4 2022-05-13 17:04:07 +02:00
c645bb7610 🚀 Version 5.3
Merge branch 'dev/5.3'
2022-05-13 16:33:48 +02:00
e6574966da 📝 Clarify network requests 2022-05-13 00:38:42 +02:00
1e9cfff05e 📝 Updated README 2022-05-13 00:34:57 +02:00
bd34c2b255 👌 Improved nginx file parsing 2022-05-13 00:24:54 +02:00
c040ac3200 🔧 Prepare for release build 2022-05-10 19:20:47 +02:00
6c6888c9cb 👌 Bump version number for new beta build 2022-05-10 19:02:37 +02:00
78cb6922b3 🐛 Fix issue with version parser 2022-05-10 19:01:37 +02:00
c16377c688 👌 Improved updater 2022-05-10 18:52:48 +02:00
540ea5c310 👌 Changes to updater 2022-05-10 18:34:34 +02:00
4ba2b25f18 👌 App version parsing 2022-05-10 18:09:22 +02:00
81b75dcaa8 👌 Async unlink and unproxy to prevent main thread hang 2022-05-10 10:44:24 +02:00
884784d024 🐛 Handle trailing semicolon (#170) 2022-05-10 10:26:48 +02:00
e81ff2870d Add test and prepare for new prerelease 2022-05-10 01:00:18 +02:00
7c631099b2 👌 Fix regular expression 2022-05-10 00:43:17 +02:00
f7a98b88a7 👌 Improve proxy subject validation 2022-05-10 00:38:13 +02:00
3fc21fff2a ♻️ Cleanup 2022-05-10 00:18:45 +02:00
0306c2b726 ♻️ Clarify parameter name 2022-05-10 00:16:47 +02:00
9d822df54e Check for updates 2022-05-10 00:14:48 +02:00
f413b84a45 🏗 WIP: Check for updates 2022-05-09 23:41:52 +02:00
b82811e6bf 🐛 Fix issue with tertiary action 2022-05-09 23:01:36 +02:00
af922664ab 🏗 WIP: Check for updates 2022-05-09 17:28:35 +02:00
8b73e69495 🐛 Fix issue with listing extensions 2022-05-09 15:27:55 +02:00
29a9e14741 🐛 Fix crash issue with .DS_Store 2022-05-09 15:27:43 +02:00
997fb27596 👌 Update copyright, verbose logging tweak 2022-05-07 18:43:36 +02:00
c171df0a93 Add additional verbosity option (#169) 2022-05-07 13:32:45 +02:00
1c15a4e07f Add (un)secure option for proxies 2022-05-06 18:27:03 +02:00
5067c7b87f 🏗 WIP: Add secure/unsecure option for proxy 2022-05-05 21:29:03 +02:00
f679231ade ♻️ Cleanup 2022-05-05 20:09:40 +02:00
f725e09f55 ♻️ WIP: Parsing logic for configuration file 2022-05-05 20:05:52 +02:00
99881bf4cd ♻️ WIP: Refactor determining PHP configuration
The way .ini files are loaded is changing with this commit. Instead of
directly saving which extensions were found, the extensions loaded are
now determined by reading the .ini file.

However, there are some performance concerns here. Perhaps it is worth
*not* reloading the contents of these files unless absolutely necessary.
2022-05-04 20:25:59 +02:00
2987464da8 📝 Added information about linter 2022-05-03 18:20:11 +02:00
4d04275c57 Added linting 2022-05-03 18:16:26 +02:00
790f63e8c9 🔧 Disable Xdebug item for 5.3 2022-05-02 18:26:02 +02:00
86b49812c3 ♻️ Cleanup menu item generation (#168) 2022-05-02 18:24:44 +02:00
ef9e0fd916 Begin work on Xdebug mode switcher (#168) 2022-05-01 22:07:18 +02:00
af8807f799 🔀 Merge bugfixes from 5.2 into 5.3 2022-04-23 12:25:18 +02:00
ba93ed93e4 Allow opening of proxies in browser 2022-04-21 19:18:05 +02:00
932a0fe176 Add 'Remove Proxy' to right-click menu 2022-04-21 19:08:40 +02:00
eb80214785 ✏️ Updated copy 2022-04-19 20:39:51 +02:00
80a4e361a4 🔧 Prepare for new DEV build 2022-04-19 20:33:19 +02:00
2af88b2bee ✏️ Update copy about non-standard TLDs 2022-04-19 20:26:44 +02:00
5048ccab8c 👌 Update various TODO items 2022-04-18 12:00:54 +02:00
66d13c92d5 Run the valet proxy command (#105) 2022-04-18 11:59:14 +02:00
836b076da9 👌 Cleanup, ensure dynamic form works correctly 2022-04-17 14:33:59 +02:00
1a75838a3b Set up proxy view strings and outlets 2022-04-17 14:02:44 +02:00
a18b7962a7 ♻️ Fix link 2022-04-16 23:21:29 +02:00
84548634ec Add proxy view 2022-04-16 23:19:13 +02:00
419ebe61f7 Add selection view 2022-04-14 14:56:51 +02:00
c45817b127 Added new UI for proxies to storyboard 2022-04-13 19:14:08 +02:00
2c0c0c5a11 Correctly detect secured proxies 2022-04-12 20:43:57 +02:00
1b8d6311ba ♻️ Refactor displaying domains 2022-04-12 17:36:18 +02:00
f0f7a3f7d6 👌 Scan proxies (#105) 2022-04-11 22:56:40 +02:00
8304d774c3 ♻️ Refactoring of files and tests 2022-04-02 15:48:21 +02:00
faeea4e866 Ensure normal nginx file does not have proxy 2022-03-31 18:28:47 +02:00
6470daf7d3 Added test to parse the proxy address 2022-03-31 18:27:26 +02:00
94139a3669 🚚 Moved test files into separate directories 2022-03-31 18:09:50 +02:00
8057019898 🐛 Fix isolation command (#158) 2022-03-31 13:35:23 +02:00
9b59fc5dae 👌 Cleanup proxies 2022-03-31 13:34:56 +02:00
75f4377de8 Added tooltips next to PHP version 2022-03-31 13:29:04 +02:00
d3657716c4 ♻️ Added ValetProxy struct 2022-03-30 21:26:53 +02:00
a13990b96f ♻️ Rename SiteList to DomainList 2022-03-30 19:19:36 +02:00
4c7aa7fead Add UI for displaying proxies (#105) 2022-03-29 21:40:10 +02:00
200 changed files with 9096 additions and 3604 deletions

23
.github/contributing.md vendored Normal file
View File

@ -0,0 +1,23 @@
# Contribution Guidelines
Thank you for your interest in contributing to PHP Monitor.
I consider this project a bit of a nice side-project to my daily gig, so it is very much a personal affair where I love to tinker around.
**While the code of the latest PHP Monitor release is public, many things are constantly in flux that may not be pushed to this repository yet.**
I don't mean to be rude, but I don't want other people involved with the project beyond simply contributing a few small things here and there, as has been the case in the past.
The extra mental overhead of having additional contributors to report to, whose code will need to be reviewed... it's a lot and it makes working on PHP Monitor less enjoyable for me.
Plus, at this point, the majority of PHP Monitor's main functionality is also done.
As a result, I may refer you to this file at some point. Again, I don't wish to be rude, but this general rule stands:
**Making any changes in a fork and opening a pull request without opening an issue first will most likely result in your PR being closed without mercy.**
To repeat, I am **not opposed** to small contributions and fixes, if they are **meaningful or insightful**.
To learn more, please check out the [pull request template](/.github/pull_request_template.md) which contains more information about my contribution requirements. (This will also show up when you open a new PR.)
Thank you for respecting this!

View File

@ -16,7 +16,7 @@ In short: It is usually best to *get in touch first* if you are making substanti
## About destination branches ## About destination branches
Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Merge requests that target `main` will be closed without mercy.** Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Pull requests that target `main` will be closed without mercy.**
Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released. Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released.

15
.swiftlint.yml Normal file
View File

@ -0,0 +1,15 @@
disabled_rules:
- todo
- identifier_name
- force_try
- force_cast
opt_in_rules:
- empty_count
included:
- phpmon
- phpmon-tests
excluded:
- phpmon/Vendor

View File

@ -1,5 +1,19 @@
# DEVELOPER README # DEVELOPER README
## ✅ Linting
This project uses the [SwiftLint](https://github.com/realm/SwiftLint) linter. You must install it and can run it like so:
```
swiftlint
```
It also automatically runs when you try to build the project. You'll get a warning if `swiftlint` is not installed, though. You can attempt to automatically fix issues:
```
swiftlint --fix
```
## 🔧 Build instructions ## 🔧 Build instructions
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/> <img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
@ -27,6 +41,12 @@ If you'd like to create a production build, choose "Any Mac" as the target and s
10. Update Cask with new version + hash 10. Update Cask with new version + hash
11. Check new version can be installed via Cask 11. Check new version can be installed via Cask
## 🍱 Marketing Mode
You can enable marketing mode by setting the `PHPMON_MARKETING_MODE` environment variable. It preloads a list of (fake) domains in the domain window list for screenshot & marketing purposes.
launchctl setenv PHPMON_MARKETING_MODE true
## 🐛 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.

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1320" LastUpgradeVersion = "1400"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@ -61,10 +61,21 @@
ReferencedContainer = "container:PHP Monitor.xcodeproj"> ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "--v"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables> <EnvironmentVariables>
<EnvironmentVariable <EnvironmentVariable
key = "PHPMON_MARKETING_MODE" key = "EXTREME_DOCTOR_MODE"
value = "YES" value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
value = ""
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>

193
README.md
View File

@ -1,16 +1,19 @@
> 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!** ⭐️ > **Note**
> 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 [sponsoring](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️
<p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p> <p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></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 app</u> (consult the FAQ below with info about how to set up your environment). **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="./docs/screenshot.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/> <img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" 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.</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)!
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/> <img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
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).
@ -21,10 +24,10 @@ You can also add new domains as links, isolate sites, manage various services, a
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) * 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 later
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew` * Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* Homebrew `php` formula is installed * Homebrew `php` formula is installed
* Laravel Valet 2.16 or newer (supports Valet 3) * Laravel Valet 3 recommended (but compatible with Valet 2)
_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._ _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._
@ -85,7 +88,8 @@ If you're still having issues, here's a few common questions & answers, as well
<li>PHP 7.4</li> <li>PHP 7.4</li>
<li>PHP 8.0</li> <li>PHP 8.0</li>
<li>PHP 8.1</li> <li>PHP 8.1</li>
<li>PHP 8.2 (experimental)</li> <li>PHP 8.2</li>
<li>PHP 8.3 (experimental)</li>
</ul> </ul>
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. 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.
@ -95,7 +99,9 @@ For more details, consult the [constants file](https://github.com/nicoverbruggen
<details> <details>
<summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary> <summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary>
You can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account. On macOS Ventura, you can accomplish this by going to **System Settings > General > Login Items** and adding PHP Monitor.app to the list **Open at Login**. You can do this with any application, by the way.
On older versions of macOS, you can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account.
Super convenient! Super convenient!
</details> </details>
@ -103,14 +109,14 @@ Super convenient!
<details> <details>
<summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary> <summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary>
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it: If you want to set up your computer for the very first time with PHP Monitor, here's how I do it.
Install [Homebrew](https://brew.sh) first. **I have also created [a video tutorial](https://www.youtube.com/watch?v=fO3hVhkvm3w) which may be easier to follow. If you just want the terminal commands, keep reading.**
Install PHP, composer, add to path: Install [Homebrew](https://brew.sh) first. Follow the instructions there first!
Then, you'll need to set up your PATH.
brew install php
brew install composer
nano .zshrc nano .zshrc
Make sure the following line is not in the comments: Make sure the following line is not in the comments:
@ -123,30 +129,70 @@ If you're on an Apple Silicon-based Mac, you'll need to add:
# on an M1 Mac # on an M1 Mac
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
and add the following to your .zshrc, but add this BEFORE the homebrew PATH additions: and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
If you're adding composer and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this: If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
export PATH=$HOME/bin:/usr/local/bin:$PATH export PATH=$HOME/bin:/usr/local/bin:$PATH
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
If you are *not* on Apple Silicon, you should remove the third line.
Install the `php` and `composer` formulae:
brew install php composer
Make sure PHP is linked correctly: Make sure PHP is linked correctly:
which php which php
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php`) should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon)
composer global require laravel/valet composer global require laravel/valet
For optimal results, you should lock your PHP platform for global dependencies to the oldest version of PHP you intend to run. If that version is PHP 7.0, your `~/.composer/composer.json` file could look like this (please adjust the version accordingly!):
```
{
"require": {
"laravel/valet": "^3.0",
},
"config": {
"platform": {
"php": "7.0"
}
}
}
```
Run `composer global update` again. This ensures that when you switch to a different global PHP version, [Valet won't break](https://github.com/nicoverbruggen/phpmon/issues/178). If it does, PHP Monitor will let you know what you can do about this.
Then, install Valet:
valet install valet install
This should install `dnsmasq` and set up Valet. Great, almost there! This should install `dnsmasq` and set up Valet. Great, almost there!
valet trust valet trust
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You can now install PHP Monitor, if you haven't already:
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
</details>
<details>
<summary><strong>How frequently does PHP Monitor check for updates?</strong></summary>
PHP Monitor will check if an update is available every time you start the app.
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". You can always check for updates manually.
</details> </details>
<details> <details>
@ -278,9 +324,102 @@ PHP Monitor is a universal app and supports both architectures, so [find out her
<details> <details>
<summary><strong>Why is the app doing network requests?</strong></summary> <summary><strong>Why is the app doing network requests?</strong></summary>
It's Homebrew. I can't prevent `brew` from doing things via the network when I invoke it. The app will automatically check for updates, which is the most likely culprit.
PHP Monitor itself doesn't do any network requests. Feel free to check the source code or intercept the traffic, if you don't believe me. This happens at launch (unless disabled), and the app directly checks the Caskfile hosted on GitHub. This data is not, and will not be used for analytics (and, as far as I can tell, cannot).
I also can't prevent `brew` from doing things via the network when PHP Monitor uses the binary.
The app includes an Internet Access Policy file, so if you're using something like Little Snitch there should be a description why these calls occur.
</details>
<details>
<summary><strong>How do I various presets to show up?</strong></summary>
You must set these presets up in a JSON file, located in `~/.config/phpmon/config.json`.
You must have set up at least one valid preset for this presets to work in PHP Monitor.
Here's an example of a working preset:
<pre>
{
"scan_apps": [],
"services": [],
"presets": [
{
"name": "Legacy Project",
"php": "8.0",
"extensions": {
"xdebug": false
},
"configuration": {
"memory_limit": "128M",
"upload_max_filesize": "128M",
"post_max_size": "128M"
}
}
],
"export": {}
}
</pre>
You can omit the `php` key in the preset if you do not wish for the preset to switch to a given PHP version.
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
<details>
<summary><strong>How do I ensure additional Homebrew services are shown in the app?</strong></summary>
You must set these services up in a JSON file, located in `~/.config/phpmon/config.json`.
You can specify custom services in the configuration file for Homebrew services that run as your own user (not root).
> **Info**
> If your service must run as root, it cannot currently be added to PHP Monitor.
You can find out which services are available by running `brew services list`.
Here's an example where we add the `mailhog` and `mysql` services to PHP Monitor:
<pre>
{
"scan_apps": [],
"services": ["mailhog", "mysql"],
"presets": [],
"export": {}
}
</pre>
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
<details>
<summary><strong>How do I set custom environment variables?</strong></summary>
You must configure these custom environment variables up in a JSON file, located in `~/.config/phpmon/config.json`.
PHP Monitor uses a default Shell environment, with no custom environment variables. You need to set custom environment variables manually. These are then used for e.g. Composer.
Here's an example of a working `COMPOSER_HOME` environment variable which is respected:
<pre>
{
"scan_apps": [],
"services": [],
"presets": [],
"export": {
"COMPOSER_HOME": "/absolute/path/to/composer/folder"
}
}
</pre>
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details> </details>
@ -295,7 +434,7 @@ All of these apps should just be detected correctly, no matter their location on
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift). To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
You can add your own apps by creating and editing a `~/.phpmon.conf.json` file, with the following entry: You can add your own apps by creating and editing a `~/.config/phpmon/config.json` file, and make sure the `scan_apps` key is set:
<pre> <pre>
{ {
@ -304,6 +443,9 @@ You can add your own apps by creating and editing a `~/.phpmon.conf.json` file,
</pre> </pre>
You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary. You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary.
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details> </details>
<details> <details>
@ -393,14 +535,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, 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: Special thanks go out to:
* My colleagues at [DIVE](https://dive.be) * Everyone supporting me via [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen)
* Everyone who has donated via [my sponsor page](https://nicoverbruggen.be/sponsor)
* 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
* Everyone who has left feedback and reported bugs (appreciate it!) * Everyone in the Laravel community who shared the app, especially on Twitter
* Everyone in the Laravel community who shared the app (thanks!)
Thank you very much for your contributions, kind words and support. Thank you very much for your contributions, kind words and support.
@ -434,7 +576,8 @@ If an extension or other process writes to a single file a bunch of times in a s
1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name. 1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name.
1. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all. 1. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all.
*Note*: If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly. > **Note**
> If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
### Want to know more? ### Want to know more?

View File

@ -6,9 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version | | Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- | ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 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) | | 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 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
@ -16,9 +14,9 @@ These versions of PHP Monitor are no longer supported, but if youre using an
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version | | Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- | ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 | | 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 | | 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 | | 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 | | 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 |
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 | 2.13 | | 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 | 2.13 |
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable | | 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |

Binary file not shown.

BIN
docs/notification-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/screenshot-dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 519 KiB

View File

@ -3,7 +3,7 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 13/02/2021. // Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
@ -15,11 +15,11 @@ class CommandTest: XCTestCase {
path: Paths.php, path: Paths.php,
arguments: ["-v"] arguments: ["-v"]
) )
XCTAssert(version.contains("(cli)")) XCTAssert(version.contains("(cli)"))
XCTAssert(version.contains("NTS")) XCTAssert(version.contains("NTS"))
XCTAssert(version.contains("built")) XCTAssert(version.contains("built"))
XCTAssert(version.contains("Zend")) XCTAssert(version.contains("Zend"))
} }
} }

View File

@ -3,18 +3,18 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 14/02/2021. // Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
class BrewJsonParserTest: XCTestCase { class HomebrewPackageTest: XCTestCase {
// - MARK: SYNTHETIC TESTS // - MARK: SYNTHETIC TESTS
static var jsonBrewFile: URL { static var jsonBrewFile: URL {
return Bundle(for: Self.self) return Bundle(for: Self.self)
.url(forResource: "brew", withExtension: "json")! .url(forResource: "brew-formula", withExtension: "json")!
} }
func testCanLoadExtensionJson() throws { func testCanLoadExtensionJson() throws {
@ -22,7 +22,7 @@ class BrewJsonParserTest: XCTestCase {
let package = try! JSONDecoder().decode( let package = try! JSONDecoder().decode(
[HomebrewPackage].self, from: json.data(using: .utf8)! [HomebrewPackage].self, from: json.data(using: .utf8)!
).first! ).first!
XCTAssertEqual(package.name, "php") XCTAssertEqual(package.name, "php")
XCTAssertEqual(package.full_name, "php") XCTAssertEqual(package.full_name, "php")
XCTAssertEqual(package.aliases.first!, "php@8.0") XCTAssertEqual(package.aliases.first!, "php@8.0")
@ -30,23 +30,23 @@ class BrewJsonParserTest: XCTestCase {
installed.version.starts(with: "8.0") installed.version.starts(with: "8.0")
}), true) }), true)
} }
static var jsonBrewServicesFile: URL { static var jsonBrewServicesFile: URL {
return Bundle(for: Self.self) return Bundle(for: Self.self)
.url(forResource: "brew-services", withExtension: "json")! .url(forResource: "brew-services", withExtension: "json")!
} }
func testCanParseServicesJson() throws { func testCanParseServicesJson() throws {
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8) let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
let services = try! JSONDecoder().decode( let services = try! JSONDecoder().decode(
[HomebrewService].self, from: json.data(using: .utf8)! [HomebrewService].self, from: json.data(using: .utf8)!
) )
XCTAssertGreaterThan(services.count, 0) XCTAssertGreaterThan(services.count, 0)
XCTAssertEqual(services.first?.name, "dnsmasq") XCTAssertEqual(services.first?.name, "dnsmasq")
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq") XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
} }
// - MARK: LIVE TESTS // - MARK: LIVE TESTS
/// This test requires that you have a valid Homebrew installation set up, /// This test requires that you have a valid Homebrew installation set up,
@ -63,13 +63,13 @@ class BrewJsonParserTest: XCTestCase {
).filter({ service in ).filter({ service in
return ["php", "nginx", "dnsmasq"].contains(service.name) return ["php", "nginx", "dnsmasq"].contains(service.name)
}) })
XCTAssertTrue(services.contains(where: {$0.name == "php"} )) XCTAssertTrue(services.contains(where: {$0.name == "php"}))
XCTAssertTrue(services.contains(where: {$0.name == "nginx"} )) XCTAssertTrue(services.contains(where: {$0.name == "nginx"}))
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} )) XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"}))
XCTAssertEqual(services.count, 3) XCTAssertEqual(services.count, 3)
} }
/// This test requires that you have a valid Homebrew installation set up, /// This test requires that you have a valid Homebrew installation set up,
/// and requires the `php` formula to be installed. /// and requires the `php` formula to be installed.
/// If this test fails, there is an issue with your Homebrew installation /// If this test fails, there is an issue with your Homebrew installation
@ -79,7 +79,7 @@ class BrewJsonParserTest: XCTestCase {
[HomebrewPackage].self, [HomebrewPackage].self,
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)! from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
).first! ).first!
XCTAssertTrue(package.name == "php") XCTAssertTrue(package.name == "php")
} }
} }

View File

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

@ -0,0 +1,81 @@
//
// NginxConfigurationTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class NginxConfigurationTest: XCTestCase {
// MARK: - Test Files
static var regularUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")!
}
static var isolatedUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")!
}
static var proxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")!
}
static var secureProxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")!
}
static var customTldProxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy-custom-tld", withExtension: "test")!
}
// MARK: - Tests
func testCanDetermineSiteNameAndTld() throws {
XCTAssertEqual(
"nginx-site",
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
)
XCTAssertEqual(
"test",
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld
)
}
func testCanDetermineIsolation() throws {
XCTAssertNil(
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
)
XCTAssertEqual(
"8.1",
NginxConfigurationFile.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion
)
}
func testCanDetermineProxy() throws {
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.proxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
let normal = NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)!
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual(nil, normal.proxy)
}
func testCanDetermineSecuredProxy() throws {
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.secureProxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
}
func testCanDetermineProxyWithCustomTld() throws {
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://localhost:8080", proxied.proxy)
}
}

View File

@ -0,0 +1,84 @@
//
// PhpConfigurationTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class PhpConfigurationTest: XCTestCase {
static var phpIniFileUrl: URL {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
}
func testCanLoadExtension() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertNotNil(iniFile)
XCTAssertGreaterThan(iniFile.extensions.count, 0)
}
func testCanCheckKeyExistence() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertTrue(iniFile.has(key: "error_reporting"))
XCTAssertTrue(iniFile.has(key: "display_errors"))
XCTAssertFalse(iniFile.has(key: "my_unknown_key"))
}
func testCanCheckKeyValue() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertNotNil(iniFile.get(for: "error_reporting"))
XCTAssert(iniFile.get(for: "error_reporting") == "E_ALL")
XCTAssertNotNil(iniFile.get(for: "display_errors"))
XCTAssert(iniFile.get(for: "display_errors") == "On")
}
func testCanCustomizeConfigurationValue() throws {
let destination = Utility
.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let configurationFile = PhpConfigurationFile
.from(filePath: destination.path)!
// 0. Verify the original value
XCTAssertEqual(configurationFile.get(for: "error_reporting"), "E_ALL")
// 1. Change the value
try! configurationFile.replace(
key: "error_reporting",
value: "E_ALL & ~E_DEPRECATED & ~E_STRICT"
)
XCTAssertEqual(
configurationFile.get(for: "error_reporting"),
"E_ALL & ~E_DEPRECATED & ~E_STRICT"
)
// 2. Ensure that same key and value doesn't break subsequent saves
try! configurationFile.replace(
key: "error_reporting",
value: "error_reporting"
)
XCTAssertEqual(
configurationFile.get(for: "error_reporting"),
"error_reporting"
)
// 3. Verify subsequent saves weren't broken
try! configurationFile.replace(
key: "error_reporting",
value: "E_ALL"
)
XCTAssertEqual(
configurationFile.get(for: "error_reporting"),
"E_ALL"
)
}
}

View File

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

View File

@ -3,20 +3,20 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 29/11/2021. // Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
class ValetConfigParserTest: XCTestCase { class ValetConfigurationTest: XCTestCase {
static var jsonConfigFileUrl: URL { static var jsonConfigFileUrl: URL {
return Bundle(for: Self.self).url( return Bundle(for: Self.self).url(
forResource: "valet-config", forResource: "valet-config",
withExtension: "json" withExtension: "json"
)! )!
} }
func testCanLoadConfigFile() throws { func testCanLoadConfigFile() throws {
let json = try? String( let json = try? String(
contentsOf: Self.jsonConfigFileUrl, contentsOf: Self.jsonConfigFileUrl,
@ -26,7 +26,7 @@ class ValetConfigParserTest: XCTestCase {
Valet.Configuration.self, Valet.Configuration.self,
from: json!.data(using: .utf8)! from: json!.data(using: .utf8)!
) )
XCTAssertEqual(config.tld, "test") XCTAssertEqual(config.tld, "test")
XCTAssertEqual(config.paths, [ XCTAssertEqual(config.paths, [
"/Users/username/.config/valet/Sites", "/Users/username/.config/valet/Sites",
@ -35,5 +35,5 @@ class ValetConfigParserTest: XCTestCase {
XCTAssertEqual(config.defaultSite, "/Users/username/default-site") 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,81 @@
# valet stub: proxy.valet.conf
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
root /;
charset utf-8;
client_max_body_size 128M;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://127.0.0.1:90;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}
server {
listen 127.0.0.1:60;
#listen 127.0.0.1:60; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.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/;
}
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://127.0.0.1:90;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -0,0 +1,57 @@
# valet stub: secure.proxy.valet.conf
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com;
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 live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com;
root /;
charset utf-8;
client_max_body_size 128M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.crt";
ssl_certificate_key "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.key";
access_log off;
error_log "/Users/phpmon/.config/valet/Log/live.whatagraph.dev.com-error.log";
error_page 404 "/Users/phpmon/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -0,0 +1,57 @@
# valet stub: secure.proxy.valet.conf
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.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 my-proxy.test www.my-proxy.test *.my-proxy.test;
root /;
charset utf-8;
client_max_body_size 128M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.crt";
ssl_certificate_key "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.key";
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://127.0.0.1:90;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -0,0 +1,34 @@
{
"scan_apps": [],
"presets": [
{
"name": "Default PHP",
"extensions": {
"xdebug": false
},
"configuration": {
"memory_limit": "128M"
}
},
{
"name": "Personal Site",
"extensions": {
"xdebug": true
},
"configuration": {
"xdebug.mode": "coverage",
"memory_limit": "512M"
}
},
{
"name": "PHP Monitor",
"extensions": {
"xdebug": true
},
"configuration": {
"xdebug.mode": "coverage",
"memory_limit": "512M"
}
}
]
}

View File

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

View File

@ -0,0 +1,47 @@
//
// AppUpdaterCheckTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 10/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class AppUpdaterCheckTest: XCTestCase {
func testCanRetrieveVersionFromCask() {
let caskVersion = AppUpdateChecker.retrieveVersionFromCask()
let version = VersionExtractor.from(caskVersion)
XCTAssertNotNil(version)
}
func testTaggedReleaseOmitsZeroPatch() {
let version = AppVersion.from("3.5.0_333")!
XCTAssertEqual(version.tagged, "3.5")
XCTAssertEqual(version.version, "3.5.0")
}
func testTaggedReleaseDoesntOmitNonZeroPatch() {
let version = AppVersion.from("3.5.1_333")!
XCTAssertEqual(version.tagged, "3.5.1")
XCTAssertEqual(version.version, "3.5.1")
}
func testTagTruncationDoesntAffectMajorVersions() {
var version = AppVersion.from("5.0_333")!
XCTAssertEqual(version.tagged, "5.0")
XCTAssertEqual(version.version, "5.0")
version = AppVersion.from("5.0.0_333")!
XCTAssertEqual(version.tagged, "5.0")
XCTAssertEqual(version.version, "5.0.0")
}
}

View File

@ -0,0 +1,62 @@
//
// AppVersionTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class AppVersionTest: XCTestCase {
func testCanRetrieveInternalAppVersion() {
XCTAssertNotNil(AppVersion.fromCurrentVersion())
}
func testCanParseNormalVersionString() {
let version = AppVersion.from("1.0.0")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual(nil, version?.build)
XCTAssertEqual(nil, version?.suffix)
}
func testCanParseCaskVersionString() {
let version = AppVersion.from("1.0.0_600")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual("600", version?.build)
XCTAssertEqual(nil, version?.suffix)
}
func testCanParseDevVersionStringWithoutBuildNumber() {
let version = AppVersion.from("1.0.0-dev")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual(nil, version?.build)
XCTAssertEqual("dev", version?.suffix)
}
func testCanParseDevVersionStringWithBuildNumber() {
let version = AppVersion.from("1.0.0-dev,870")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual("870", version?.build)
XCTAssertEqual("dev", version?.suffix)
}
func testCanParseUnderscoresAsBuildSeparatorToo() {
let version = AppVersion.from("1.0.0-dev_870")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual("870", version?.build)
XCTAssertEqual("dev", version?.suffix)
}
}

View File

@ -3,7 +3,7 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 01/04/2021. // Created by Nico Verbruggen on 01/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
@ -23,7 +23,7 @@ class PhpVersionDetectionTest: XCTestCase {
"php@5.6", "php@5.6",
"php@5.4" // should be omitted, not supported "php@5.4" // should be omitted, not supported
], checkBinaries: false, generateHelpers: false) ], checkBinaries: false, generateHelpers: false)
XCTAssertEqual(outcome, ["8.0", "7.0"]) XCTAssertEqual(outcome, ["8.0", "7.0"])
} }
} }

View File

@ -8,6 +8,7 @@
import XCTest import XCTest
// swiftlint:disable type_body_length
class PhpVersionNumberTest: XCTestCase { class PhpVersionNumberTest: XCTestCase {
func testCanDeconstructPhpVersion() throws { func testCanDeconstructPhpVersion() throws {
@ -36,13 +37,13 @@ class PhpVersionNumberTest: XCTestCase {
nil nil
) )
} }
func testPhpVersionNumberParse() throws { func testPhpVersionNumberParse() throws {
XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
XCTAssertTrue(error is VersionParseError) XCTAssertTrue(error is VersionParseError)
} }
} }
func testCanCheckFixedConstraints() throws { func testCanCheckFixedConstraints() throws {
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@ -51,7 +52,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0"]).all .make(from: ["7.0"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"]) .make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"])
@ -59,7 +60,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0.3"]).all .make(from: ["7.0.3"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -67,7 +68,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0"]).all .make(from: ["7.0"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -76,7 +77,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: []).all .make(from: []).all
) )
} }
func testCanCheckCaretConstraints() throws { func testCanCheckCaretConstraints() throws {
// 1. Imprecise checks // 1. Imprecise checks
XCTAssertEqual( XCTAssertEqual(
@ -86,7 +87,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// 2. Imprecise check with precise constraint (lenient AKA not strict) // 2. Imprecise check with precise constraint (lenient AKA not strict)
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc. // These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual( XCTAssertEqual(
@ -96,7 +97,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// 3. Imprecise check with precise constraint (strict mode) // 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc. // These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual( XCTAssertEqual(
@ -106,7 +107,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all .make(from: ["7.4", "7.3", "7.2", "7.1"]).all
) )
// 4. Precise members and constraint all around // 4. Precise members and constraint all around
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@ -115,7 +116,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
// 5. Precise members but imprecise constraint (strict mode) // 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0 // In strict mode the constraint's patch version is assumed to be 0
XCTAssertEqual( XCTAssertEqual(
@ -125,7 +126,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
// 6. Precise members but imprecise constraint (lenient mode) // 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal // In lenient mode the constraint's patch version is assumed to be equal
XCTAssertEqual( XCTAssertEqual(
@ -136,7 +137,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
} }
func testCanCheckTildeConstraints() throws { func testCanCheckTildeConstraints() throws {
// 1. Imprecise checks // 1. Imprecise checks
XCTAssertEqual( XCTAssertEqual(
@ -146,7 +147,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// 2. Imprecise check with precise constraint (lenient AKA not strict) // 2. Imprecise check with precise constraint (lenient AKA not strict)
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc. // These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual( XCTAssertEqual(
@ -159,7 +160,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0"]).all .make(from: ["7.0"]).all
) )
// 3. Imprecise check with precise constraint (strict mode) // 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc. // These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual( XCTAssertEqual(
@ -172,7 +173,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: []).all .make(from: []).all
) )
// 4. Precise members and constraint all around // 4. Precise members and constraint all around
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@ -183,7 +184,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0.10"]).all .make(from: ["7.0.10"]).all
) )
// 5. Precise members but imprecise constraint (strict mode) // 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0. // In strict mode the constraint's patch version is assumed to be 0.
XCTAssertEqual( XCTAssertEqual(
@ -193,7 +194,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
// 6. Precise members but imprecise constraint (lenient mode) // 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal. // In lenient mode the constraint's patch version is assumed to be equal.
// (Strictness does not make any difference here, but both should be tested.) // (Strictness does not make any difference here, but both should be tested.)
@ -205,7 +206,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
} }
func testCanCheckGreaterThanOrEqualConstraints() throws { func testCanCheckGreaterThanOrEqualConstraints() throws {
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@ -214,7 +215,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -222,7 +223,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0) // Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@ -231,7 +232,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all .make(from: ["7.4", "7.3"]).all
) )
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999) // Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@ -241,7 +242,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4", "7.3", "7.2"]).all .make(from: ["7.4", "7.3", "7.2"]).all
) )
} }
func testCanCheckGreaterThanConstraints() throws { func testCanCheckGreaterThanConstraints() throws {
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@ -250,7 +251,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all .make(from: ["7.4", "7.3", "7.2", "7.1"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -259,7 +260,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2"]).all .make(from: ["7.4", "7.3", "7.2"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -268,7 +269,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all .make(from: ["7.4", "7.3"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"]) .make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
@ -277,7 +278,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2"]).all .make(from: ["7.3.1", "7.2.9", "7.2"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"]) .make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
@ -287,4 +288,76 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.3.1", "7.2.9"]).all .make(from: ["7.3.1", "7.2.9"]).all
) )
} }
func testCanCheckLessThanOrEqualConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<=7.2", strict: true),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<=7.2.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<=7.2.5", strict: true),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
// Non-strict check (ignoring patch has no effect)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<=7.2.5", strict: false),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
}
func testCanCheckLessThanConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<7.2", strict: true),
PhpVersionNumberCollection
.make(from: ["7.1", "7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<7.2.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.1", "7.0"]).all
)
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<7.2.5", strict: true),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
// Non-strict check (patch resolves to 7.2.999, which is bigger than 7.2.5)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<7.2.5", strict: false),
PhpVersionNumberCollection
.make(from: ["7.1", "7.0"]).all
)
}
} }

View File

@ -3,7 +3,7 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 29/11/2021. // Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
@ -14,5 +14,5 @@ class ValetVersionExtractorTest: XCTestCase {
let version = valet("--version", sudo: false) let version = valet("--version", sudo: false)
XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3")) XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3"))
} }
} }

View File

@ -3,7 +3,7 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 16/12/2021. // Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
@ -14,12 +14,12 @@ class VersionExtractorTest: XCTestCase {
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1") XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0") XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
} }
func testVersionComparison() { func testVersionComparison() {
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending) XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending) XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame) XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)
XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending) XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending)
} }
} }

View File

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

View File

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

View File

@ -1,12 +1,12 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "ServiceOn.png", "filename" : "Proxy.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "ServiceOn@2x.png", "filename" : "Proxy@2x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -2,116 +2,133 @@
// Services.swift // Services.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
import AppKit import AppKit
class Actions { class Actions {
// MARK: - Services // MARK: - Services
public static func restartPhpFpm() public static func restartPhpFpm() {
{ brew("services restart \(Homebrew.Formulae.php)", sudo: true)
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
} }
public static func restartNginx() public static func restartNginx() {
{ brew("services restart \(Homebrew.Formulae.nginx)", sudo: true)
brew("services restart nginx", sudo: true)
} }
public static func restartDnsMasq() public static func restartDnsMasq() {
{ brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: true)
brew("services restart dnsmasq", sudo: true)
} }
public static func stopAllServices() public static func stopValetServices() {
{ brew("services stop \(Homebrew.Formulae.php)", sudo: true)
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true) brew("services stop \(Homebrew.Formulae.nginx)", sudo: true)
brew("services stop nginx", sudo: true) brew("services stop \(Homebrew.Formulae.dnsmasq)", sudo: true)
brew("services stop dnsmasq", sudo: true)
} }
public static func fixHomebrewPermissions() throws public static func fixHomebrewPermissions() throws {
{
var servicesCommands = [ var servicesCommands = [
"\(Paths.brew) services stop nginx", "\(Paths.brew) services stop \(Homebrew.Formulae.nginx)",
"\(Paths.brew) services stop dnsmasq", "\(Paths.brew) services stop \(Homebrew.Formulae.dnsmasq)"
] ]
var cellarCommands = [ var cellarCommands = [
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx", "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.nginx)",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq" "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.dnsmasq)"
] ]
PhpEnv.shared.availablePhpVersions.forEach { version in PhpEnv.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnv.brewPhpVersion let formula = version == PhpEnv.brewPhpVersion ? "php" : "php@\(version)"
? "php"
: "php@\(version)"
servicesCommands.append("\(Paths.brew) services stop \(formula)") servicesCommands.append("\(Paths.brew) services stop \(formula)")
cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)") cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
} }
let script = let script =
servicesCommands.joined(separator: " && ") servicesCommands.joined(separator: " && ")
+ " && " + " && "
+ cellarCommands.joined(separator: " && ") + cellarCommands.joined(separator: " && ")
let appleScript = NSAppleScript( let appleScript = NSAppleScript(
source: "do shell script \"\(script)\" with administrator privileges" source: "do shell script \"\(script)\" with administrator privileges"
) )
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil) let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
if (eventResult == nil) { if eventResult == nil {
throw HomebrewPermissionError(kind: .applescriptNilError) throw HomebrewPermissionError(kind: .applescriptNilError)
} }
} }
// MARK: - Third Party Services
public static func stopService(name: String, completion: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
brew("services stop \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
ServicesManager.loadHomebrewServices(completed: {
DispatchQueue.main.async {
completion()
}
})
}
}
public static func startService(name: String, completion: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
brew("services start \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
ServicesManager.loadHomebrewServices(completed: {
DispatchQueue.main.async {
completion()
}
})
}
}
// MARK: - Finding Config Files // MARK: - Finding Config Files
public static func openGenericPhpConfigFolder() public static func openGenericPhpConfigFolder() {
{ let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
} }
public static func openGlobalComposerFolder() public static func openGlobalComposerFolder() {
{
let file = FileManager.default.homeDirectoryForCurrentUser let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json") .appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
} }
public static func openPhpConfigFolder(version: String) public static func openPhpConfigFolder(version: String) {
{ let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
} }
public static func openValetConfigFolder() public static func openValetConfigFolder() {
{
let file = FileManager.default.homeDirectoryForCurrentUser let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet") .appendingPathComponent(".config/valet")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
} }
public static func openPhpMonitorConfigFile() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/phpmon")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Other Actions // MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL public static func createTempPhpInfoFile() -> URL {
{
// Write a file called `phpmon_phpinfo.php` to /tmp // Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8) try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
// Tell php-cgi to run the PHP and output as an .html file // Tell php-cgi to run the PHP and output as an .html file
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html") Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")! return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
} }
// MARK: - Fix My Valet // MARK: - Fix My Valet
/** /**
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.
@ -124,12 +141,11 @@ class Actions {
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(completed: @escaping () -> Void) public static func fixMyValet(completed: @escaping () -> Void) {
{
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: { InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
brew("services restart dnsmasq", sudo: true) brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: true)
brew("services restart php", sudo: true) brew("services restart \(Homebrew.Formulae.php)", sudo: true)
brew("services restart nginx", sudo: true) brew("services restart \(Homebrew.Formulae.nginx)", sudo: true)
completed() completed()
}) })
} }

View File

@ -2,13 +2,13 @@
// Command.swift // Command.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
public class Command { public class Command {
/** /**
Immediately executes a command. Immediately executes a command.
@ -20,21 +20,21 @@ public class Command {
let task = Process() let task = Process()
task.launchPath = path task.launchPath = path
task.arguments = arguments task.arguments = arguments
let pipe = Pipe() let pipe = Pipe()
task.standardOutput = pipe task.standardOutput = pipe
task.launch() task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile() let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String.init(data: data, encoding: String.Encoding.utf8)! let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
if (trimNewlines) { if trimNewlines {
return output.components(separatedBy: .newlines) return output.components(separatedBy: .newlines)
.filter({ !$0.isEmpty }) .filter({ !$0.isEmpty })
.joined(separator: "\n") .joined(separator: "\n")
} }
return output return output
} }
} }

View File

@ -2,19 +2,13 @@
// Constants.swift // Constants.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
struct Constants { struct Constants {
/**
* The latest PHP version that is considered to be stable at the time of release.
* This version number is currently not used (only as a default fallback).
*/
static let LatestStablePhpVersion = "8.1"
/** /**
The minimum version of Valet that is recommended. The minimum version of Valet that is recommended.
If the installed version is older, a notification will be shown If the installed version is older, a notification will be shown
@ -24,7 +18,7 @@ struct Constants {
See also: https://github.com/laravel/valet/releases/tag/v2.16.2 See also: https://github.com/laravel/valet/releases/tag/v2.16.2
*/ */
static let MinimumRecommendedValetVersion = "2.16.2" static let MinimumRecommendedValetVersion = "2.16.2"
/** /**
* The PHP versions supported by this application. * The PHP versions supported by this application.
* Versions that do not appear in this array are omitted from the list. * Versions that do not appear in this array are omitted from the list.
@ -42,27 +36,46 @@ struct Constants {
"7.4", "7.4",
"8.0", "8.0",
"8.1", "8.1",
"8.2",
// ==================== // ====================
// EXPERIMENTAL SUPPORT // EXPERIMENTAL SUPPORT
// ==================== // ====================
// Every release that supports the next release will always support the next // Every release that supports the next release will always support the next
// dev release. In this case, that means that the version below is detected. // dev release. In this case, that means that the version below is detected.
"8.2" "8.3"
] ]
struct Urls { struct Urls {
static let DonationPayment = URL( // phpmon.app URLs (these are aliased to redirect correctly)
string: "https://nicoverbruggen.be/sponsor#pay-now"
)!
static let DonationPage = URL( static let DonationPage = URL(
string: "https://nicoverbruggen.be/sponsor" string: "https://phpmon.app/sponsor"
)! )!
static let FrequentlyAskedQuestions = URL( static let FrequentlyAskedQuestions = URL(
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting" string: "https://phpmon.app/faq"
)! )!
static let DonationPayment = URL(
string: "https://phpmon.app/sponsor/now"
)!
// GitHub URLs (do not alias these)
static let GitHubReleases = URL(
string: "https://github.com/nicoverbruggen/phpmon/releases"
)!
static let StableBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb"
)!
static let DevBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
)!
} }
} }

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
class Events { class Events {
static let ServicesUpdated = Notification.Name("ServicesUpdated") static let ServicesUpdated = Notification.Name("ServicesUpdated")
} }

View File

@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 24/12/2021. // Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
// MARK: Common Shell Commands // MARK: Common Shell Commands
@ -11,28 +11,25 @@
/** /**
Runs a `valet` command. Defaults to running as superuser. Runs a `valet` command. Defaults to running as superuser.
*/ */
func valet(_ command: String, sudo: Bool = true) -> String func valet(_ command: String, sudo: Bool = true) -> String {
{
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true) return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
} }
/** /**
Runs a `brew` command. Can run as superuser. Runs a `brew` command. Can run as superuser.
*/ */
func brew(_ command: String, sudo: Bool = false) func brew(_ command: String, sudo: Bool = false) {
{
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)") Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
} }
/** /**
Runs `sed` in order to replace all occurrences of a string in a specific file with another. Runs `sed` in order to replace all occurrences of a string in a specific file with another.
*/ */
func sed(file: String, original: String, replacement: String) func sed(file: String, original: String, replacement: String) {
{
// Escape slashes (or `sed` won't work) // Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/") let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/") let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
// 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 Filesystem.fileExists("\(Paths.binPath)/gsed") { if Filesystem.fileExists("\(Paths.binPath)/gsed") {
@ -45,8 +42,7 @@ func sed(file: String, original: String, replacement: String)
/** /**
Uses `grep` to determine whether a particular query string can be found in a particular file. Uses `grep` to determine whether a particular query string can be found in a particular file.
*/ */
func grepContains(file: String, query: String) -> Bool func grepContains(file: String, query: String) -> Bool {
{
return Shell.pipe(""" return Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO" grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""") """)

View File

@ -0,0 +1,25 @@
//
// Homebrew.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/11/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class Homebrew {
struct Formulae {
static var php: String {
return PhpEnv.phpInstall.formula
}
static var nginx: String {
return HomebrewDiagnostics.usesNginxFullFormula ? "nginx-full" : "nginx"
}
static var dnsmasq: String {
return "dnsmasq"
}
}
}

View File

@ -3,56 +3,56 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 21/12/2021. // Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class Log { class Log {
static var shared = Log() static var shared = Log()
enum Verbosity: Int { enum Verbosity: Int {
case error = 1, case error = 1,
warning = 2, warning = 2,
info = 3, info = 3,
performance = 4 performance = 4
public func isApplicable() -> Bool { public func isApplicable() -> Bool {
return Log.shared.verbosity.rawValue >= self.rawValue return Log.shared.verbosity.rawValue >= self.rawValue
} }
} }
var verbosity: Verbosity = .warning var verbosity: Verbosity = .warning
static func err(_ item: Any) { static func err(_ item: Any) {
if Verbosity.error.isApplicable() { if Verbosity.error.isApplicable() {
print("[E] \(item)") print("[E] \(item)")
} }
} }
static func warn(_ item: Any) { static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() { if Verbosity.warning.isApplicable() {
print("[W] \(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("[P] \(item)") print("[P] \(item)")
} }
} }
static func separator(as verbosity: Verbosity = .info) { static func separator(as verbosity: Verbosity = .info) {
if verbosity.isApplicable() { if verbosity.isApplicable() {
print("==================================") print("==================================")
} }
} }
} }

View File

@ -2,7 +2,7 @@
// Paths.swift // Paths.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@ -12,71 +12,75 @@ import Foundation
The path to the Homebrew directory and the user's name are fetched only once, at boot. The path to the Homebrew directory and the user's name are fetched only once, at boot.
*/ */
public class Paths { public class Paths {
public static let shared = Paths() public static let shared = Paths()
internal var baseDir: Paths.HomebrewDir internal var baseDir: Paths.HomebrewDir
private var userName: String private var userName: String
init() { init() {
baseDir = App.architecture != "x86_64" ? .opt : .usr baseDir = App.architecture != "x86_64" ? .opt : .usr
userName = String(Shell.pipe("whoami").split(separator: "\n")[0]) userName = String(Shell.pipe("id -un").split(separator: "\n")[0])
} }
public func detectBinaryPaths() { public func detectBinaryPaths() {
detectComposerBinary() detectComposerBinary()
} }
// - MARK: Binaries // - MARK: Binaries
public static var valet: String { public static var valet: String {
return "\(binPath)/valet" return "\(binPath)/valet"
} }
public static var brew: String { public static var brew: String {
return "\(binPath)/brew" return "\(binPath)/brew"
} }
public static var php: String { public static var php: String {
return "\(binPath)/php" return "\(binPath)/php"
} }
public static var phpConfig: String { public static var phpConfig: String {
return "\(binPath)/php-config" return "\(binPath)/php-config"
} }
// - MARK: Detected Binaries // - MARK: Detected Binaries
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */ /** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
public static var composer: String? = nil public static var composer: String?
// - MARK: Paths // - MARK: Paths
public static var whoami: String { public static var whoami: String {
return shared.userName return shared.userName
} }
public static var homePath: String {
return NSHomeDirectory()
}
public static var cellarPath: String { public static var cellarPath: String {
return "\(shared.baseDir.rawValue)/Cellar" return "\(shared.baseDir.rawValue)/Cellar"
} }
public static var binPath: String { public static var binPath: String {
return "\(shared.baseDir.rawValue)/bin" return "\(shared.baseDir.rawValue)/bin"
} }
public static var optPath: String { public static var optPath: String {
return "\(shared.baseDir.rawValue)/opt" return "\(shared.baseDir.rawValue)/opt"
} }
public static var etcPath: String { public static var etcPath: String {
return "\(shared.baseDir.rawValue)/etc" return "\(shared.baseDir.rawValue)/etc"
} }
// MARK: - Flexible Binaries // MARK: - Flexible Binaries
// (these can be in multiple locations, so we scan common places because) // (these can be in multiple locations, so we scan common places because)
// (PHP Monitor will not use the user's own PATH) // (PHP Monitor will not use the user's own PATH)
private func detectComposerBinary() { private func detectComposerBinary() {
if Filesystem.fileExists("/usr/local/bin/composer") { if Filesystem.fileExists("/usr/local/bin/composer") {
Paths.composer = "/usr/local/bin/composer" Paths.composer = "/usr/local/bin/composer"
@ -87,12 +91,12 @@ public class Paths {
Log.warn("Composer was not found.") Log.warn("Composer was not found.")
} }
} }
// MARK: - Enum // MARK: - Enum
public enum HomebrewDir: String { public enum HomebrewDir: String {
case opt = "/opt/homebrew" case opt = "/opt/homebrew"
case usr = "/usr/local" case usr = "/usr/local"
} }
} }

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
extension Process { extension Process {
/** /**
When a process is running in the background, it can send content to standard 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` output or standard error, just like it would in a terminal. Using `listen`
@ -22,10 +22,10 @@ extension Process {
) { ) {
let outputPipe = Pipe() let outputPipe = Pipe()
let errorPipe = Pipe() let errorPipe = Pipe()
self.standardOutput = outputPipe self.standardOutput = outputPipe
self.standardError = errorPipe self.standardError = errorPipe
[ [
(outputPipe, didReceiveStandardOutputData), (outputPipe, didReceiveStandardOutputData),
(errorPipe, didReceiveStandardErrorData) (errorPipe, didReceiveStandardErrorData)
@ -35,15 +35,18 @@ extension Process {
forName: NSNotification.Name.NSFileHandleDataAvailable, forName: NSNotification.Name.NSFileHandleDataAvailable,
object: pipe.fileHandleForReading, object: pipe.fileHandleForReading,
queue: nil queue: nil
) { notification in ) { _ in
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) { if let outputString = String(
data: pipe.fileHandleForReading.availableData,
encoding: String.Encoding.utf8
) {
callback(outputString) callback(outputString)
} }
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify() pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
} }
} }
} }
/** /**
After the process is done running, you'll want to stop listening. After the process is done running, you'll want to stop listening.
*/ */
@ -55,5 +58,5 @@ extension Process {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading) NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
} }
} }
} }

View File

@ -0,0 +1,33 @@
//
// Shell+PATH.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Shell {
var PATH: String {
let task = Process()
task.launchPath = "/bin/zsh"
let command = Filesystem.fileExists("~/.zshrc")
// source the user's .zshrc file if it exists to complete $PATH
? ". ~/.zshrc && echo $PATH"
// otherwise, non-interactive mode is sufficient
: "echo $PATH"
task.arguments = ["--login", "-lc", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: String.Encoding.utf8) ?? ""
}
}

View File

@ -2,41 +2,44 @@
// Shell.swift // Shell.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
public class Shell { public class Shell {
// MARK: - Invoke static functions // MARK: - Invoke static functions
public static func run( public static func run(
_ command: String, _ command: String,
requiresPath: Bool = false requiresPath: Bool = false
) { ) {
Shell.user.run(command, requiresPath: requiresPath) Shell.user.run(command, requiresPath: requiresPath)
} }
public static func pipe( public static func pipe(
_ command: String, _ command: String,
requiresPath: Bool = false requiresPath: Bool = false
) -> String { ) -> String {
return Shell.user.pipe(command, requiresPath: requiresPath) return Shell.user.pipe(command, requiresPath: requiresPath)
} }
// MARK: - Singleton // MARK: - Singleton
/** /**
We now require macOS 11, so no need to detect which terminal to use. We now require macOS 11, so no need to detect which terminal to use.
*/ */
public var shell: String = "/bin/sh" public var shell: String = "/bin/sh"
/** Additional exports that are sent if `requiresPath` is set to true. */
public var exports: String = ""
/** /**
Singleton to access a user shell (with --login) Singleton to access a user shell (with --login)
*/ */
public static let user = Shell() public static let user = Shell()
/** /**
Runs a shell command without using the output. Runs a shell command without using the output.
Uses the default shell. Uses the default shell.
@ -51,7 +54,7 @@ public class Shell {
// Equivalent of piping to /dev/null; don't do anything with the string // Equivalent of piping to /dev/null; don't do anything with the string
_ = Shell.pipe(command, requiresPath: requiresPath) _ = Shell.pipe(command, requiresPath: requiresPath)
} }
/** /**
Runs a shell command and returns the output. Runs a shell command and returns the output.
@ -69,7 +72,7 @@ public class Shell {
) )
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
} }
/** /**
Runs the command and returns a `ShellOutput` object, which contains info about the process. Runs the command and returns a `ShellOutput` object, which contains info about the process.
@ -81,17 +84,17 @@ public class Shell {
_ command: String, _ command: String,
requiresPath: Bool = false requiresPath: Bool = false
) -> Shell.Output { ) -> Shell.Output {
let outputPipe = Pipe() let outputPipe = Pipe()
let errorPipe = Pipe() let errorPipe = Pipe()
let task = self.createTask(for: command, requiresPath: requiresPath) let task = self.createTask(for: command, requiresPath: requiresPath)
task.standardOutput = outputPipe task.standardOutput = outputPipe
task.standardError = errorPipe task.standardError = errorPipe
task.launch() task.launch()
task.waitUntilExit() task.waitUntilExit()
return Shell.Output( let output = Shell.Output(
standardOutput: String( standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(), data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8 encoding: .utf8
@ -102,28 +105,61 @@ public class Shell {
)!, )!,
task: task task: task
) )
if CommandLine.arguments.contains("--v") {
log(task: task, output: output)
}
return output
} }
/** /**
Creates a new process with the correct PATH and shell. Creates a new process with the correct PATH and shell.
*/ */
public func createTask(for command: String, requiresPath: Bool) -> Process { public func createTask(for command: String, requiresPath: Bool) -> Process {
let tailoredCommand = requiresPath var completeCommand = ""
? "export PATH=\(Paths.binPath):$PATH && \(command)"
: command if requiresPath {
// Basic export (PATH)
completeCommand += "export PATH=\(Paths.binPath):$PATH && "
// Put additional exports in between
if !self.exports.isEmpty {
completeCommand += "\(self.exports) && "
}
}
completeCommand += command
let task = Process() let task = Process()
task.launchPath = self.shell task.launchPath = self.shell
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand] task.arguments = ["--noprofile", "-norc", "--login", "-c", completeCommand]
return task return task
} }
/**
Verbose logging for PHP Monitor's synchronous shell output.
*/
private func log(task: Process, output: Output) {
Log.info("")
Log.info("==== COMMAND ====")
Log.info("")
Log.info("\(self.shell) \(task.arguments?.joined(separator: " ") ?? "")")
Log.info("")
Log.info("==== OUTPUT ====")
Log.info("")
dump(output)
Log.info("")
Log.info("==== END OUTPUT ====")
Log.info("")
}
public class Output { public class Output {
public let standardOutput: String public let standardOutput: String
public let errorOutput: String public let errorOutput: String
public let task: Process public let task: Process
init(standardOutput: String, init(standardOutput: String,
errorOutput: String, errorOutput: String,
task: Process) { task: Process) {

View File

@ -15,9 +15,9 @@ struct HomebrewPermissionError: Error, AlertableError {
enum Kind: String { enum Kind: String {
case applescriptNilError = "homebrew_permissions.applescript_returned_nil" case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
} }
let kind: Kind let kind: Kind
func getErrorMessageKey() -> String { func getErrorMessageKey() -> String {
return "alert.errors.\(self.kind.rawValue)" return "alert.errors.\(self.kind.rawValue)"
} }

View File

@ -0,0 +1,24 @@
//
// ArrayExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Array {
/**
Sourced from Stack Overflow
https://stackoverflow.com/a/33540708
*/
func chunked(by distance: Int) -> [[Element]] {
let indicesSequence = stride(from: startIndex, to: endIndex, by: distance)
let array: [[Element]] = indicesSequence.map {
let newIndex = $0.advanced(by: distance) > endIndex ? endIndex : $0.advanced(by: distance)
return Array(self[$0 ..< newIndex])
}
return array
}
}

View File

@ -2,17 +2,17 @@
// Date.swift // Date.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
extension Date { extension Date {
func toString() -> String { func toString() -> String {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.string(from: self) return dateFormatter.string(from: self)
} }
} }

View File

@ -3,27 +3,23 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 14/04/2021. // Created by Nico Verbruggen on 14/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
extension NSMenu { extension NSMenu {
convenience init(items: [NSMenuItem], target: NSObject? = nil) {
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) { self.init()
newItem.keyEquivalentModifierMask = modifier self.addItems(items, target: target)
self.addItem(newItem)
} }
}
@IBDesignable class LocalizedMenuItem: NSMenuItem { public func addItems(_ items: [NSMenuItem], target: NSObject? = nil) {
for item in items {
@IBInspectable self.addItem(item)
var localizationKey: String? { if target != nil {
didSet { item.target = target
self.title = localizationKey?.localized ?? self.title }
} }
} }
} }

View File

@ -0,0 +1,87 @@
//
// NSMenuItem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 18/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension NSMenuItem {
convenience init(
title: String,
action: Selector? = nil,
keyEquivalent: String = "",
keyModifier: NSEvent.ModifierFlags = [],
toolTip: String? = nil
) {
self.init(title: title, action: action, keyEquivalent: keyEquivalent)
self.keyEquivalentModifierMask = keyModifier
self.toolTip = toolTip
}
convenience init(
title: String,
keyEquivalent: String = "",
keyModifier: NSEvent.ModifierFlags = [],
toolTip: String? = nil,
submenu: [NSMenuItem],
target: NSObject? = nil
) {
self.init(title: title, action: nil, keyEquivalent: keyEquivalent)
self.keyEquivalentModifierMask = keyModifier
self.toolTip = toolTip
self.submenu = NSMenu(items: submenu, target: target)
}
}
// MARK: - NSMenuItem subclasses
@IBDesignable class LocalizedMenuItem: NSMenuItem {
@IBInspectable var localizationKey: String? {
didSet {
self.title = localizationKey?.localized ?? self.title
}
}
}
class PhpMenuItem: NSMenuItem {
var version: String = ""
}
class XdebugMenuItem: NSMenuItem {
var mode: String = ""
}
class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension?
}
class EditorMenuItem: NSMenuItem {
var editor: Application?
}
class PresetMenuItem: NSMenuItem {
var preset: Preset?
static func getAll() -> [NSMenuItem] {
return Preferences.custom.presets!.map { preset in
let presetMenuItem = PresetMenuItem(
title: preset.getMenuItemText(),
action: #selector(MainMenu.togglePreset(sender:))
)
if let attributedString = try? NSMutableAttributedString(
data: preset.getMenuItemText().data(using: .utf8)!,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil
) {
presetMenuItem.attributedTitle = attributedString
}
presetMenuItem.preset = preset
return presetMenuItem
}
}
}

View File

@ -10,29 +10,43 @@ import Foundation
import Cocoa import Cocoa
extension NSWindow { extension NSWindow {
/**
Centers a window. Taken from: https://stackoverflow.com/a/66140320
*/
public func setCenterPosition(offsetY: CGFloat = 0) {
if let screenSize = screen?.visibleFrame.size {
self.setFrameOrigin(
NSPoint(
x: (screenSize.width - frame.size.width) / 2,
y: (screenSize.height - frame.size.height) / 2 + offsetY
)
)
}
}
/** /**
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/ Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
*/ */
func shake(){ func shake() {
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03 let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
let frame: CGRect = self.frame let frame: CGRect = self.frame
let shakeAnimation :CAKeyframeAnimation = CAKeyframeAnimation() let shakeAnimation: CAKeyframeAnimation = CAKeyframeAnimation()
let shakePath = CGMutablePath() let shakePath = CGMutablePath()
shakePath.move( to: CGPoint(x:NSMinX(frame), y:NSMinY(frame))) shakePath.move( to: CGPoint(x: frame.minX, y: frame.minY))
for _ in 0...numberOfShakes-1 { for _ in 0...numberOfShakes-1 {
shakePath.addLine(to: CGPoint(x:NSMinX(frame) - frame.size.width * vigourOfShake, y:NSMinY(frame))) shakePath.addLine(to: CGPoint(x: frame.minX - frame.size.width * vigourOfShake, y: frame.minY))
shakePath.addLine(to: CGPoint(x:NSMinX(frame) + frame.size.width * vigourOfShake, y:NSMinY(frame))) shakePath.addLine(to: CGPoint(x: frame.minX + frame.size.width * vigourOfShake, y: frame.minY))
} }
shakePath.closeSubpath() shakePath.closeSubpath()
shakeAnimation.path = shakePath shakeAnimation.path = shakePath
shakeAnimation.duration = durationOfShake shakeAnimation.duration = durationOfShake
self.animations = ["frameOrigin":shakeAnimation] self.animations = ["frameOrigin": shakeAnimation]
self.animator().setFrameOrigin(self.frame.origin) self.animator().setFrameOrigin(self.frame.origin)
} }
} }

View File

@ -2,42 +2,52 @@
// StringExtension.swift // StringExtension.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
import SwiftUI
extension String { extension String {
var localized: String { var localized: String {
if #available(macOS 13, *) {
return NSLocalizedString(
self, tableName: nil, bundle: Bundle.main, value: "", comment: ""
).replacingOccurrences(of: "Preferences", with: "Settings")
}
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "") return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
} }
var localizedForSwiftUI: LocalizedStringKey {
return LocalizedStringKey(self.localized)
}
func localized(_ args: CVarArg...) -> String { func localized(_ args: CVarArg...) -> String {
String(format: self.localized, arguments: args) String(format: self.localized, arguments: args)
} }
func countInstances(of stringToFind: String) -> Int { func countInstances(of stringToFind: String) -> Int {
if (stringToFind.isEmpty) { if stringToFind.isEmpty {
return 0 return 0
} }
var count = 0 var count = 0
var searchRange: Range<String.Index>? var searchRange: Range<String.Index>?
while let foundRange = range(of: stringToFind, options: [], range: searchRange) { while let foundRange = range(of: stringToFind, options: [], range: searchRange) {
count += 1 count += 1
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex)) searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
} }
return count return count
} }
subscript (r: Range<String.Index>) -> String { subscript(r: Range<String.Index>) -> String {
let start = r.lowerBound let start = r.lowerBound
let end = r.upperBound let end = r.upperBound
return String(self[start ..< end]) return String(self[start ..< end])
} }
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/ // Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
/* /*
<1> We split the version by period (.). <1> We split the version by period (.).
@ -50,12 +60,12 @@ extension String {
*/ */
func versionCompare(_ otherVersion: String) -> ComparisonResult { func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "." let versionDelimiter = "."
var versionComponents = self.components(separatedBy: versionDelimiter) // <1> var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter) var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2> let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
if zeroDiff == 0 { // <3> if zeroDiff == 0 { // <3>
// Same format, compare normally // Same format, compare normally
return self.compare(otherVersion, options: .numeric) return self.compare(otherVersion, options: .numeric)
@ -70,5 +80,23 @@ extension String {
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6> .compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
} }
} }
var stripped: String {
do {
guard let data = self.data(using: .unicode) else {
return ""
}
let attributed = try NSAttributedString(
data: data,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue],
documentAttributes: nil
)
return attributed.string
} catch {
return ""
}
}
} }

View File

@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 04/02/2021. // Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@ -12,25 +12,25 @@ import Cocoa
// Adapted from: https://stackoverflow.com/a/46268778 // Adapted from: https://stackoverflow.com/a/46268778
protocol XibLoadable { protocol XibLoadable {
static var xibName: String? { get } static var xibName: String? { get }
static func createFromXib(in bundle: Bundle) -> Self? static func createFromXib(in bundle: Bundle) -> Self?
} }
extension XibLoadable where Self: NSView { extension XibLoadable where Self: NSView {
static var xibName: String? { static var xibName: String? {
return String(describing: Self.self) return String(describing: Self.self)
} }
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? { static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
guard let xibName = xibName else { return nil } guard let xibName = xibName else { return nil }
var topLevelArray: NSArray? = nil var topLevelArray: NSArray?
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray) bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
guard let results = topLevelArray else { return nil } guard let results = topLevelArray else { return nil }
let views = Array<Any>(results).filter { $0 is Self } let views = [Any](results).filter { $0 is Self }
return views.last as? Self return views.last as? Self
} }
} }

View File

@ -2,13 +2,13 @@
// Alert.swift // Alert.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
class Alert { class Alert {
public static func confirm( public static func confirm(
onWindow window: NSWindow, onWindow window: NSWindow,
messageText: String, messageText: String,
@ -21,13 +21,13 @@ class Alert {
if !Thread.isMainThread { if !Thread.isMainThread {
fatalError("You should always present alerts on the main thread!") 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
alert.informativeText = informativeText alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle) alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) { if !secondButtonTitle.isEmpty {
alert.addButton(withTitle: secondButtonTitle) alert.addButton(withTitle: secondButtonTitle)
} }
alert.beginSheetModal(for: window) { response in alert.beginSheetModal(for: window) { response in
@ -36,5 +36,5 @@ class Alert {
} }
} }
} }
} }

View File

@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 07/12/2021. // Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@ -12,23 +12,23 @@ import Foundation
/// In most cases this is going to be a code editor, but it could also be another application /// In most cases this is going to be a code editor, but it could also be another application
/// that supports opening those directories, like a visual Git client or a terminal app. /// that supports opening those directories, like a visual Git client or a terminal app.
class Application { class Application {
enum AppType { enum AppType {
case editor, browser, git_gui, terminal, user_supplied case editor, browser, git_gui, terminal, user_supplied
} }
/// Name of the app. Used for display purposes and to determine `name.app` exists. /// Name of the app. Used for display purposes and to determine `name.app` exists.
let name: String let name: String
/// Application type. Depending on the type, a different action might occur. /// Application type. Depending on the type, a different action might occur.
let type: AppType let type: AppType
/// Initializer. Used to detect a specific app of a specific type. /// Initializer. Used to detect a specific app of a specific type.
init(_ name: String, _ type: AppType) { init(_ name: String, _ type: AppType) {
self.name = name self.name = name
self.type = type self.type = type
} }
/** /**
Attempt to open a specific directory in the app of choice. Attempt to open a specific directory in the app of choice.
(This will open the app if it isn't open yet.) (This will open the app if it isn't open yet.)
@ -36,7 +36,7 @@ class Application {
@objc public func openDirectory(file: String) { @objc public func openDirectory(file: String) {
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"") return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
} }
/** Checks if the app is installed. */ /** Checks if the app is installed. */
func isInstalled() -> Bool { func isInstalled() -> Bool {
// If this script does not complain, the app exists! // If this script does not complain, the app exists!
@ -45,7 +45,7 @@ class Application {
requiresPath: false requiresPath: false
).task.terminationStatus == 0 ).task.terminationStatus == 0
} }
/** /**
Detect which apps are available to open a specific directory. Detect which apps are available to open a specific directory.
*/ */

View File

@ -1,23 +1,61 @@
// //
// FileSystem.swift // Filesystem.swift
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 07/12/2021. // Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
import Foundation
class Filesystem { class Filesystem {
/** /**
Checks if a file exists at the provided path. Checks if a file or directory exists at the provided path.
Uses `FileManager`.
*/ */
public static func fileExists(_ path: String) -> Bool { public static func exists(_ path: String) -> Bool {
return FileManager.default.fileExists( return FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)") atPath: path.replacingOccurrences(of: "~", with: Paths.homePath)
) )
} }
/**
Checks if a file exists at the provided path.
*/
public static func fileExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
isDirectory: &isDirectory
)
return exists && !isDirectory.boolValue
}
/**
Checks if a directory exists at the provided path.
*/
public static func directoryExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
isDirectory: &isDirectory
)
return exists && isDirectory.boolValue
}
/**
Checks if a given file is a symbolic link.
*/
public static func fileIsSymlink(_ path: String) -> Bool {
do {
let attribs = try FileManager.default.attributesOfItem(atPath: path)
return attribs[.type] as! FileAttributeType == FileAttributeType.typeSymbolicLink
} catch {
return false
}
}
} }

View File

@ -2,26 +2,30 @@
// LocalNotification.swift // LocalNotification.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
import UserNotifications import UserNotifications
class LocalNotification { class LocalNotification {
public static func send(title: String, subtitle: String) { @MainActor public static func send(title: String, subtitle: String, preference: PreferenceName) {
if !Preferences.isEnabled(preference) {
return
}
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = title content.title = title
content.body = subtitle content.body = subtitle
let uuidString = UUID().uuidString let uuidString = UUID().uuidString
let request = UNNotificationRequest( let request = UNNotificationRequest(
identifier: uuidString, identifier: uuidString,
content: content, content: content,
trigger: nil trigger: nil
) )
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) { (error) in notificationCenter.add(request) { (error) in
if error != nil { if error != nil {
@ -29,5 +33,5 @@ class LocalNotification {
} }
} }
} }
} }

View File

@ -2,46 +2,46 @@
// ImageGenerator.swift // ImageGenerator.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
class MenuBarImageGenerator { class MenuBarImageGenerator {
/** /**
Takes a string and converts it to an image that can be displayed in the menu bar. Takes a string and converts it to an image that can be displayed in the menu bar.
The width of the NSImage depends on the length of the text. The width of the NSImage depends on the length of the text.
*/ */
public static func textToImage(text: String) -> NSImage { public static func textToImage(text: String) -> NSImage {
let font = NSFont.systemFont(ofSize: 14, weight: .medium) let font = NSFont.systemFont(ofSize: 14, weight: .medium)
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [ let textFontAttributes = [
NSAttributedString.Key.font: font, NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: NSColor.black, NSAttributedString.Key.foregroundColor: NSColor.black,
NSAttributedString.Key.paragraphStyle: textStyle NSAttributedString.Key.paragraphStyle: textStyle
] ]
let padding : CGFloat = 2.0; let padding: CGFloat = 2.0
// Create an attributed string so we'll know how wide the item will need to be // Create an attributed string so we'll know how wide the item will need to be
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes) let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
let textSize = attributedString.size() let textSize = attributedString.size()
// Add padding to the width of the menu bar item // Add padding to the width of the menu bar item
let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height) let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height)
let image = NSImage(size: size) let image = NSImage(size: size)
// Set the image rect with the appropriate dimensions // Set the image rect with the appropriate dimensions
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
// Position the text inside the image rect // Position the text inside the image rect
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height) let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
let targetImage: NSImage = NSImage(size: image.size) let targetImage: NSImage = NSImage(size: image.size)
let representation: NSBitmapImageRep = NSBitmapImageRep( let representation: NSBitmapImageRep = NSBitmapImageRep(
bitmapDataPlanes: nil, bitmapDataPlanes: nil,
pixelsWide: Int(image.size.width), pixelsWide: Int(image.size.width),
@ -54,40 +54,40 @@ class MenuBarImageGenerator {
bytesPerRow: 0, bytesPerRow: 0,
bitsPerPixel: 0 bitsPerPixel: 0
)! )!
targetImage.addRepresentation(representation) targetImage.addRepresentation(representation)
targetImage.lockFocus() targetImage.lockFocus()
image.draw(in: imageRect) image.draw(in: imageRect)
text.draw(in: textRect, withAttributes: textFontAttributes) text.draw(in: textRect, withAttributes: textFontAttributes)
targetImage.unlockFocus() targetImage.unlockFocus()
return targetImage return targetImage
} }
/** /**
The same as before, but also attempts to add an icon to the left. The same as before, but also attempts to add an icon to the left.
*/ */
public static func textToImageWithIcon(text: String) -> NSImage { public static func textToImageWithIcon(text: String) -> NSImage {
// We'll start out with the image containing the text // We'll start out with the image containing the text
let textImage = self.textToImage(text: text) let textImage = self.textToImage(text: text)
// Then we'll fetch the image we want on the left // Then we'll fetch the image we want on the left
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
if iconType == nil { if iconType == nil {
Log.warn("Invalid icon type found, using the default") Log.warn("Invalid icon type found, using the default")
iconType = MenuBarIcon.iconPhp.rawValue iconType = MenuBarIcon.iconPhp.rawValue
} }
let iconImage = NSImage(named: "MenuBar_\(iconType!)")! let iconImage = NSImage(named: "MenuBar_\(iconType!)")!
// We'll need to reference the width of the icon a bunch of times // We'll need to reference the width of the icon a bunch of times
let iconWidthSize = iconImage.size.width let iconWidthSize = iconImage.size.width
// There will also be an additional divider between the image and the text (image) // There will also be an additional divider between the image and the text (image)
let divider: CGFloat = 3 let divider: CGFloat = 3
// Use a fixed size for the height of the menu bar (18pt) // Use a fixed size for the height of the menu bar (18pt)
let imageRect = CGRect( let imageRect = CGRect(
x: 0, x: 0,
@ -95,14 +95,14 @@ class MenuBarImageGenerator {
width: textImage.size.width + iconWidthSize + divider, width: textImage.size.width + iconWidthSize + divider,
height: 18 height: 18
) )
// Create a new image, we'll draw the text and our icon in there // Create a new image, we'll draw the text and our icon in there
let image: NSImage = NSImage(size: imageRect.size) let image: NSImage = NSImage(size: imageRect.size)
image.lockFocus() image.lockFocus()
// Calculate the offset between the image and the text // Calculate the offset between the image and the text
let offset = imageRect.size.width - textImage.size.width let offset = imageRect.size.width - textImage.size.width
// Draw the text with a negative x offset (so there is room on the left for the icon) // Draw the text with a negative x offset (so there is room on the left for the icon)
textImage.draw( textImage.draw(
in: imageRect, in: imageRect,
@ -115,7 +115,7 @@ class MenuBarImageGenerator {
operation: .overlay, operation: .overlay,
fraction: 1 fraction: 1
) )
// Draw the icon directly in the left of the imageRect (where we left space) // Draw the icon directly in the left of the imageRect (where we left space)
iconImage.draw( iconImage.draw(
in: imageRect, in: imageRect,
@ -128,11 +128,11 @@ class MenuBarImageGenerator {
operation: .overlay, operation: .overlay,
fraction: 1 fraction: 1
) )
// We're done with this image // We're done with this image
image.unlockFocus() image.unlockFocus()
return image return image
} }
} }

View File

@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 05/12/2021. // Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
@ -15,32 +15,32 @@ import Cocoa
- Note: This class does make a simple assumption: each window controller corresponds to a single view. - Note: This class does make a simple assumption: each window controller corresponds to a single view.
*/ */
class PMWindowController: NSWindowController, NSWindowDelegate { class PMWindowController: NSWindowController, NSWindowDelegate {
public var windowName: String { public var windowName: String {
fatalError("Please specify a window name") fatalError("Please specify a window name")
} }
override func showWindow(_ sender: Any?) { override func showWindow(_ sender: Any?) {
super.showWindow(sender) super.showWindow(sender)
App.shared.register(window: windowName) App.shared.register(window: windowName)
} }
func windowWillClose(_ notification: Notification) { func windowWillClose(_ notification: Notification) {
App.shared.remove(window: windowName) App.shared.remove(window: windowName)
} }
deinit { deinit {
Log.perf("Window controller '\(windowName)' was deinitialized") Log.perf("deinit: \(String(describing: self)).\(#function)")
} }
} }
extension NSWindowController { extension NSWindowController {
public func positionWindowInTopLeftCorner() { public func positionWindowInTopLeftCorner() {
guard let frame = NSScreen.main?.frame else { return } guard let frame = NSScreen.main?.frame else { return }
guard let window = self.window else { return } guard let window = self.window else { return }
window.setFrame(NSRect( window.setFrame(NSRect(
x: frame.size.width - window.frame.size.width - 20, x: frame.size.width - window.frame.size.width - 20,
y: frame.size.height - window.frame.size.height - 40, y: frame.size.height - window.frame.size.height - 40,
@ -48,5 +48,5 @@ extension NSWindowController {
height: window.frame.height height: window.frame.height
), display: true) ), display: true)
} }
} }

View File

@ -3,13 +3,13 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 16/12/2021. // Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class VersionExtractor { class VersionExtractor {
/** /**
This attempts to extract the version number from any given string. This attempts to extract the version number from any given string.
*/ */
@ -19,26 +19,26 @@ class VersionExtractor {
pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#, pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
options: [] options: []
) )
let match = regex.matches( let match = regex.matches(
in: string, in: string,
options: [], options: [],
range: NSMakeRange(0, string.count) range: NSRange(location: 0, length: string.count)
).first ).first
guard let match = match else { guard let match = match else {
return nil return nil
} }
let range = Range( let range = Range(
match.range(withName: "version"), match.range(withName: "version"),
in: string in: string
)! )!
return String(string[range]) return String(string[range])
} catch { } catch {
return nil return nil
} }
} }
} }

View File

@ -2,7 +2,7 @@
// ActivePhpInstallation.swift // ActivePhpInstallation.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@ -20,79 +20,89 @@ class ActivePhpInstallation {
var version: Version! var version: Version!
var limits: Limits! var limits: Limits!
var extensions: [PhpExtension]! var iniFiles: [PhpConfigurationFile] = []
var extensions: [PhpExtension] {
return iniFiles.flatMap { initFile in
return initFile.extensions
}
}
// MARK: - Computed // MARK: - Computed
var formula: String { var formula: String {
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)" return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
} }
// MARK: - Initializer // MARK: - Initializer
init() { init() {
// Show information about the current version // Show information about the current version
getVersion() getVersion()
// Initialize the list of ini files that are loaded
iniFiles = []
// If an error occurred, exit early // If an error occurred, exit early
if (version.error) { if version.error {
limits = Limits() limits = Limits()
extensions = []
return return
} }
// Load extension information // Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path)
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
iniFiles.append(file)
}
// Get configuration values // Get configuration values
limits = Limits( limits = Limits(
memory_limit: getByteCount(key: "memory_limit"), memory_limit: getByteCount(key: "memory_limit"),
upload_max_filesize: getByteCount(key: "upload_max_filesize"), upload_max_filesize: getByteCount(key: "upload_max_filesize"),
post_max_size: getByteCount(key: "post_max_size") post_max_size: getByteCount(key: "post_max_size")
) )
// Return a list of .ini files parsed after php.ini // Return a list of .ini files parsed after php.ini
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"]) let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
.replacingOccurrences(of: "\n", with: "") .replacingOccurrences(of: "\n", with: "")
.split(separator: ",") .split(separator: ",")
.map { String($0) } .map { String($0) }
// See if any extensions are present in said .ini files // See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in paths.forEach { (iniFilePath) in
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath)) if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
if exts.count > 0 { iniFiles.append(file)
extensions.append(contentsOf: exts)
} }
} }
} }
/** /**
When the app tries to retrieve the version, the installation is considered broken if the output is nothing, When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case. _or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
*/ */
private func getVersion() -> Void { private func getVersion() {
self.version = Version() self.version = Version()
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true) let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
if (version == "" || version.contains("Warning") || version.contains("Error")) { if version == "" || version.contains("Warning") || version.contains("Error") {
self.version.short = "💩 BROKEN" self.version.short = "💩 BROKEN"
self.version.long = "" self.version.long = ""
self.version.error = true self.version.error = true
return return
} }
// That's the long version // That's the long version
self.version.long = version self.version.long = version
// Next up, let's strip away the minor version number // Next up, let's strip away the minor version number
let segments = self.version.long.components(separatedBy: ".") let segments = self.version.long.components(separatedBy: ".")
// Get the first two elements // Get the first two elements
self.version.short = segments[0...1].joined(separator: ".") self.version.short = segments[0...1].joined(separator: ".")
} }
/** /**
Retrieves the display value for a specific key in the `.ini` file. Retrieves the display value for a specific key in the `.ini` file.
@ -110,18 +120,18 @@ class ActivePhpInstallation {
*/ */
private func getByteCount(key: String) -> String { private func getByteCount(key: String) -> String {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"]) let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
// Check if the value is unlimited // Check if the value is unlimited
if (value == "-1") { if value == "-1" {
return "" return ""
} }
// Check if the syntax is valid otherwise // Check if the syntax is valid otherwise
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: []) let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first let match = regex.matches(in: value, options: [], range: NSRange(location: 0, length: value.count)).first
return (match == nil) ? "⚠️" : "\(value)B" return (match == nil) ? "⚠️" : "\(value)B"
} }
/** /**
Determine if PHP-FPM is configured correctly. Determine if PHP-FPM is configured correctly.
@ -135,11 +145,11 @@ class ActivePhpInstallation {
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf" let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
return Shell.pipe("cat \(fileName)").contains("valet.sock") return Shell.pipe("cat \(fileName)").contains("valet.sock")
} }
// 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 Filesystem.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
/** /**
@ -153,7 +163,7 @@ class ActivePhpInstallation {
var long = "???" var long = "???"
var error = false var error = false
} }
/** /**
Struct containing information about the limits of the current PHP installation. Struct containing information about the limits of the current PHP installation.
Includes: memory limit, max upload size and max post size. Includes: memory limit, max upload size and max post size.
@ -163,5 +173,5 @@ class ActivePhpInstallation {
var upload_max_filesize = "???" var upload_max_filesize = "???"
var post_max_size = "???" var post_max_size = "???"
} }
} }

View File

@ -0,0 +1,61 @@
//
// Xdebug.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class Xdebug {
public static var enabled: Bool {
return PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") != nil
}
public static var activeModes: [String] {
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
return []
}
guard let value = file.get(for: "xdebug.mode") else {
return []
}
return value.components(separatedBy: ",").filter { self.modes.contains($0) }
}
public static func asMenuItems() -> [NSMenuItem] {
var items: [NSMenuItem] = []
let activeModes = Self.activeModes
for mode in Self.modes {
let item = XdebugMenuItem(
title: mode,
action: #selector(MainMenu.toggleXdebugMode(sender:)),
keyEquivalent: ""
)
item.state = activeModes.contains(mode) ? .on : .off
item.mode = mode
items.append(item)
}
return items
}
public static var modes: [String] {
return [
"develop",
"coverage",
"debug",
"gcstats",
"profile",
"trace"
]
}
}

View File

@ -2,24 +2,24 @@
// HomebrewPackage.swift // HomebrewPackage.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
struct HomebrewPackage: Decodable { struct HomebrewPackage: Decodable {
let name: String let name: String
let full_name: String let full_name: String
let aliases: [String] let aliases: [String]
let installed: [HomebrewInstalled] let installed: [HomebrewInstalled]
let linked_keg: String? let linked_keg: String?
public var version: String { public var version: String {
return aliases.first! return aliases.first!
.replacingOccurrences(of: "php@", with: "") .replacingOccurrences(of: "php@", with: "")
} }
} }
struct HomebrewInstalled: Decodable { struct HomebrewInstalled: Decodable {

View File

@ -18,21 +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"], Dummy data for preview purposes.
completion: @escaping ([HomebrewService]) -> Void */
) { public static func dummy(named service: String, enabled: Bool) -> Self {
DispatchQueue.global(qos: .background).async { return HomebrewService(
let data = Shell name: service,
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true) service_name: service,
.data(using: .utf8)! running: enabled,
loaded: enabled,
let services = try! JSONDecoder() pid: nil,
.decode([HomebrewService].self, from: data) user: nil,
.filter({ return filter.contains($0.name) }) status: nil,
log_path: nil,
completion(services) error_log_path: nil
} )
} }
} }

View File

@ -3,48 +3,48 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 21/12/2021. // Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class PhpEnv { class PhpEnv {
// MARK: - Initializer // MARK: - Initializer
init() { init() {
self.currentInstall = ActivePhpInstallation() self.currentInstall = ActivePhpInstallation()
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json"); let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json")
self.homebrewPackage = try! JSONDecoder().decode( self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self, [HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)! from: brewPhpAlias.data(using: .utf8)!
).first! ).first!
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!") Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
} }
// MARK: - Properties // MARK: - Properties
/** The delegate that is informed of updates. */ /** The delegate that is informed of updates. */
weak var delegate: PhpSwitcherDelegate? weak var delegate: PhpSwitcherDelegate?
/** The static app instance. Accessible at any time. */ /** The static app instance. Accessible at any time. */
static let shared = PhpEnv() static let shared = PhpEnv()
/** Whether the switcher is busy performing any actions. */ /** Whether the switcher is busy performing any actions. */
var isBusy: Bool = false var isBusy: Bool = false
/** All available versions of PHP. */ /** All available versions of PHP. */
var availablePhpVersions: [String] = [] var availablePhpVersions: [String] = []
/** Cached information about the PHP installations. */ /** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:] var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */ /** Information about the currently linked PHP installation. */
var currentInstall: ActivePhpInstallation var currentInstall: ActivePhpInstallation
/** /**
The version that the `php` formula via Brew is aliased to on the current system. The version that the `php` formula via Brew is aliased to on the current system.
@ -57,63 +57,62 @@ class PhpEnv {
static var brewPhpVersion: String { static var brewPhpVersion: String {
return Self.shared.homebrewPackage.version return Self.shared.homebrewPackage.version
} }
/** /**
The currently linked and active PHP installation. The currently linked and active PHP installation.
*/ */
static var phpInstall: ActivePhpInstallation { static var phpInstall: ActivePhpInstallation {
return Self.shared.currentInstall return Self.shared.currentInstall
} }
/** /**
Information we were able to discern from the Homebrew info command. Information we were able to discern from the Homebrew info command.
*/ */
var homebrewPackage: HomebrewPackage! = nil var homebrewPackage: HomebrewPackage! = nil
// MARK: - Methods // MARK: - Methods
public static var switcher: PhpSwitcher { public static var switcher: PhpSwitcher {
return InternalSwitcher() return InternalSwitcher()
} }
public static func detectPhpVersions() -> Void { public static func detectPhpVersions() {
_ = Self.shared.detectPhpVersions() _ = Self.shared.detectPhpVersions()
} }
/** /**
Detects which versions of PHP are installed. Detects which versions of PHP are installed.
*/ */
public func detectPhpVersions() -> [String] public func detectPhpVersions() -> [String] {
{
let files = Shell.pipe("ls \(Paths.optPath) | grep php@") let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n")) var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
// Make sure the aliased version is detected // Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0` // The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed // We should also detect that as a version that is installed
let phpAlias = homebrewPackage.version let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate // Avoid inserting a duplicate
if (!versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php")) { if !versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php") {
versionsOnly.append(phpAlias) versionsOnly.append(phpAlias)
} }
Log.info("The PHP versions that were detected are: \(versionsOnly)") Log.info("The PHP versions that were detected are: \(versionsOnly)")
availablePhpVersions = versionsOnly availablePhpVersions = versionsOnly
var mappedVersions: [String: PhpInstallation] = [:] var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version) mappedVersions[version] = PhpInstallation(version)
} }
cachedPhpInstallations = mappedVersions cachedPhpInstallations = mappedVersions
return versionsOnly return versionsOnly
} }
/** /**
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`.
@ -126,14 +125,14 @@ class PhpEnv {
checkBinaries: Bool = true, checkBinaries: Bool = true,
generateHelpers: Bool = true generateHelpers: Bool = true
) -> [String] { ) -> [String] {
var output : [String] = [] var output: [String] = []
var supported = Constants.SupportedPhpVersions var supported = Constants.SupportedPhpVersions
if !Valet.enabled(feature: .supportForPhp56) { if !Valet.enabled(feature: .supportForPhp56) {
supported.removeAll { $0 == "5.6" } 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)
@ -144,19 +143,18 @@ class PhpEnv {
// 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)
&& supported.contains(version) && supported.contains(version)
&& (checkBinaries ? Filesystem.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 { if generateHelpers {
output.forEach { PhpHelper.generate(for: $0) } output.forEach { PhpHelper.generate(for: $0) }
} }
return output return output
} }
public func validVersions(for constraint: String) -> [PhpVersionNumber] { public func validVersions(for constraint: String) -> [PhpVersionNumber] {
constraint.split(separator: "|").flatMap { constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection return PhpVersionNumberCollection
@ -164,7 +162,7 @@ class PhpEnv {
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines)) .matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
} }
} }
/** /**
Validates whether the currently running version matches the provided version. Validates whether the currently running version matches the provided version.
*/ */
@ -173,7 +171,17 @@ class PhpEnv {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.") Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
return true return true
} }
return false return false
} }
/**
Returns the configuration file instance that is used for a specific config value.
You can then use the configuration file instance to change values.
*/
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
return PhpEnv.phpInstall.iniFiles
.reversed()
.first(where: { $0.has(key: key) })
}
} }

View File

@ -9,27 +9,38 @@
import Foundation import Foundation
class PhpHelper { class PhpHelper {
static let keyPhrase = "This file was automatically generated by PHP Monitor." static let keyPhrase = "This file was automatically generated by PHP Monitor."
public static func generate(for version: String) { public static func generate(for version: String) {
// Take the PHP version (e.g. "7.2") and generate a dotless version // Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "") let dotless = version.replacingOccurrences(of: ".", with: "")
// Determine the dotless name for this PHP version
let destination = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
// Check if the ~/.config/phpmon/bin directory is in the PATH
let inPath = Shell.user.PATH.contains("\(Paths.homePath)/.config/phpmon/bin")
// Check if we can create symlinks (`/usr/local/bin` must be writable)
let canWriteSymlinks = FileManager.default.isWritableFile(atPath: "/usr/local/bin/")
do { do {
let destination = "/usr/local/bin/pm\(dotless)" Shell.run("mkdir -p ~/.config/phpmon/bin")
if FileManager.default.fileExists(atPath: destination) { if FileManager.default.fileExists(atPath: destination) {
let contents = try String(contentsOfFile: destination) let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) { 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.") Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
+ "(or is unreadable). Not updating this file.")
return return
} }
} }
// Let's follow the symlink to the PHP binary folder // Let's follow the symlink to the PHP binary folder
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin") let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path .resolvingSymlinksInPath().path
// The contents of the script! // The contents of the script!
let script = """ let script = """
#!/bin/zsh #!/bin/zsh
@ -41,20 +52,50 @@ class PhpHelper {
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!"; || echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH export PATH=\(path):$PATH
""" """
// Write to the destination // Write to the destination
try script.write( try script.write(
to: URL(fileURLWithPath: destination), to: URL(fileURLWithPath: destination),
atomically: true, atomically: true,
encoding: String.Encoding.utf8 encoding: String.Encoding.utf8
) )
// Make sure the file is executable // Make sure the file is executable
Shell.run("chmod +x \(destination)") Shell.run("chmod +x \(destination)")
// Create a symlink if the folder is not in the PATH
if !inPath {
// First, check if we can create symlinks at all
if !canWriteSymlinks {
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
return
}
// Write the symlink
self.createSymlink(dotless)
}
} catch { } catch {
print(error) print(error)
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)") Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
} }
} }
private static func createSymlink(_ dotless: String) {
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
let destination = "/usr/local/bin/pm\(dotless)"
if !Filesystem.fileExists(destination) {
Log.info("Creating new symlink: \(destination)")
Shell.run("ln -s \(source) \(destination)")
return
}
if !Filesystem.fileIsSymlink(destination) {
Log.info("Overwriting existing file with new symlink: \(destination)")
Shell.run("ln -fs \(source) \(destination)")
return
}
Log.info("Symlink in \(destination) already exists, OK.")
}
} }

View File

@ -10,21 +10,21 @@ import Foundation
public struct PhpVersionNumberCollection: Equatable { public struct PhpVersionNumberCollection: Equatable {
let versions: [PhpVersionNumber] let versions: [PhpVersionNumber]
public static func make(from versions: [String]) -> Self { public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection( return PhpVersionNumberCollection(
versions: versions.map { try! PhpVersionNumber.parse($0) } versions: versions.map { try! PhpVersionNumber.parse($0) }
) )
} }
public var first: PhpVersionNumber? { public var first: PhpVersionNumber? {
return self.versions.first return self.versions.first
} }
public var all: [PhpVersionNumber] { public var all: [PhpVersionNumber] {
return self.versions return self.versions
} }
/** /**
Checks if any versions of PHP are valid for the constraint provided. Checks if any versions of PHP are valid for the constraint provided.
Due to the complexity of evaluating these, a important test is maintained. Due to the complexity of evaluating these, a important test is maintained.
@ -61,13 +61,13 @@ public struct PhpVersionNumberCollection: Equatable {
// Strict constraint (e.g. "7.0") -> returns specific version // Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter { $0.isSameAs(version, strict) } return self.versions.filter { $0.isSameAs(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) { if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
// Caret range means that the major version is never higher but minor version can be higher // Caret range means that the major version is never higher but minor version can be higher
// ^7.2 will be compatible with all versions between 7.2 and 8.0 // ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) } return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) { if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis. // Tilde range means that most specific digit is used as the basis.
return self.versions.filter { return self.versions.filter {
@ -78,64 +78,73 @@ public struct PhpVersionNumberCollection: Equatable {
: $0.hasSameMajorButNewerOrSameMinor(version, strict) : $0.hasSameMajorButNewerOrSameMinor(version, strict)
} }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) { if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) } return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) { if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) } return self.versions.filter { $0.isNewerThan(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isOlderThan(version, strict)}
}
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThan) {
return self.versions.filter { $0.isOlderThan(version, strict)}
}
return [] return []
} }
} }
public struct PhpVersionNumber: Equatable { public struct PhpVersionNumber: Equatable, Hashable {
let major: Int let major: Int
let minor: Int let minor: Int
let patch: Int? let patch: Int?
public func toString() -> String { public func toString() -> String {
return self.patch == nil return self.patch == nil
? "\(major).\(minor)" ? "\(major).\(minor)"
: "\(major).\(minor).\(patch!)" : "\(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)
} }
public var homebrewVersion: String { public var homebrewVersion: String {
return "\(major).\(minor)" return "\(major).\(minor)"
} }
public enum MatchType: String { public enum MatchType: String {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"# case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"# case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"# case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
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: (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"#
*/
} }
public static func parse(_ text: String) throws -> Self { public static func parse(_ text: String) throws -> Self {
guard let versionText = VersionExtractor.from(text) else { guard let versionText = VersionExtractor.from(text) else {
throw VersionParseError() throw VersionParseError()
} }
return Self.make(from: versionText)! return Self.make(from: versionText)!
} }
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? { public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: []) let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
let match = regex.matches(
in: versionString,
options: [],
range: NSRange(location: 0, length: versionString.count)
).first
if match != nil { if match != nil {
let major = Int( let major = Int(
versionString[Range(match!.range(withName: "major"), in: versionString)!] versionString[Range(match!.range(withName: "major"), in: versionString)!]
@ -143,24 +152,24 @@ public struct PhpVersionNumber: Equatable {
let minor = Int( let minor = Int(
versionString[Range(match!.range(withName: "minor"), in: versionString)!] versionString[Range(match!.range(withName: "minor"), in: versionString)!]
)! )!
var patch: Int? = nil var patch: Int?
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) { if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
patch = Int(versionString[minorRange]) patch = Int(versionString[minorRange])
} }
return Self(major: major, minor: minor, patch: patch) return Self(major: major, minor: minor, patch: patch)
} }
return nil return nil
} }
// MARK: Comparison Logic // MARK: Comparison Logic
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major return self.major == version.major
&& self.minor == version.minor && self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true) && (strict ? self.patch(strict, version) == version.patch(strict) : true)
} }
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return ( return (
self.major > version.major || self.major > version.major ||
@ -169,7 +178,16 @@ public struct PhpVersionNumber: Equatable {
&& self.patch(strict) > version.patch(strict) && self.patch(strict) > version.patch(strict)
) )
} }
internal func isOlderThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return (
self.major < version.major ||
self.major == version.major && self.minor < version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) < version.patch(strict)
)
}
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && return self.major == version.major &&
( (
@ -177,12 +195,12 @@ public struct PhpVersionNumber: Equatable {
|| self.minor > version.minor || self.minor > version.minor
) )
} }
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict) && self.patch(strict, version) >= version.patch(strict)
} }
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major return self.major == version.major
&& self.minor >= version.minor && self.minor >= version.minor

View File

@ -0,0 +1,216 @@
//
// PhpConfigurationFile.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpConfigurationFile: CreatedFromFile {
struct ConfigValue {
let lineIndex: Int
let value: String
}
typealias Section = [String: ConfigValue]
typealias Config = [String: Section]
/// The file where this configuration file was located.
let filePath: String
/// The extensions found in this .ini file.
var extensions: [PhpExtension]
/// The actual, structured content of the configuration file.
var content: Config
/// The original lines of the file.
var lines: [String]
/** Resolves a PHP configuration file (.ini) */
static func from(filePath: String) -> Self? {
let path = filePath.replacingOccurrences(of: "~", with: Paths.homePath)
do {
let fileContents = try String(contentsOfFile: path)
return Self.init(path: path, contents: fileContents)
} catch {
Log.warn("Could not read the PHP configuration file at: `\(filePath)`")
return nil
}
}
required init(path: String, contents: String) {
self.filePath = path
self.lines = contents.components(separatedBy: "\n")
self.extensions = PhpExtension.from(lines, filePath: path)
self.content = Self.parseConfig(lines: lines)
}
// MARK: API
public func has(key: String) -> Bool {
return self.content.contains { (_: String, section: Section) in
return section.keys.contains(key)
}
}
public func get(for key: String) -> String? {
return getConfig(for: key)?.value
}
public func getConfig(for key: String) -> ConfigValue? {
for (_, section) in self.content where section.keys.contains(key) {
return section[key]!
}
return nil
}
enum ReplacementErrors: Error {
case missingKey
}
/**
Replaces the value for a specific (existing) key with a new value.
The key must exist for this to work.
*/
public func replace(key: String, value: String) throws {
// Ensure that the key exists
guard let item = getConfig(for: key) else {
throw ReplacementErrors.missingKey
}
// Figure out what comes after the assignment
var components = self
.lines[item.lineIndex]
.components(separatedBy: "=")
// Replace the value with the new one
components[1] = components[1]
.replacingOccurrences(of: item.value, with: value)
// Replace the specific line
self.lines[item.lineIndex] = components.joined(separator: "=")
// Finally, join the string and save the file atomatically again
try self.lines.joined(separator: "\n")
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
// Reload the original file
self.reload()
}
public func reload() {
self.lines = try! String(contentsOfFile: self.filePath)
.components(separatedBy: "\n")
self.extensions = PhpExtension.from(lines, filePath: self.filePath)
self.content = Self.parseConfig(lines: lines)
}
// MARK: Parsing Logic
// Slightly modified from: https://gist.github.com/jetmind/f776c0d223e4ac6aec1ff9389e874553
/**
Attempts to parse the configuration file, based on an array of strings.
Each string is a line from the configuration file.
*/
private static func parseConfig(lines: [String]) -> Config {
var config = Config()
var currentSectionName = "main"
for (index, line) in lines.enumerated() {
let line = trim(line)
if line.hasPrefix("[") && line.hasSuffix("]") {
currentSectionName = parseSectionHeader(line)
} else if let (key, value) = parseLine(line) {
var section = config[currentSectionName] ?? [:]
section[key] = ConfigValue(
lineIndex: index,
value: value
)
config[currentSectionName] = section
}
}
return config
}
/**
Remove all whitespace and additional characters from individual lines.
*/
private static func trim(_ string: String) -> String {
let whitespaces = CharacterSet(charactersIn: " \n\r\t")
return string.trimmingCharacters(in: whitespaces)
}
/**
It may prove beneficial to strip all comments, which can start with # or ;.
In this case, strip both.
*/
private static func stripComment(_ line: String) -> String {
var line = line
let characters: [String.Element] = ["#", ";"]
for character in characters {
// Only keep checking for comments as long as the line isn't empty
if line.isEmpty {
return line
}
// Check for the next comment character
line = strip(character: character, line)
}
return line
}
/**
Empties a line if it happens to be commented out, causing it to be ignored.
*/
private static func strip(character: String.Element, _ line: String) -> String {
let parts = line.split(
separator: character,
maxSplits: 1,
omittingEmptySubsequences: false
)
if !parts.isEmpty {
return String(parts[0])
}
return ""
}
/**
Attempts to parse a section header. Requires the line to start with [ and end with ].
*/
private static func parseSectionHeader(_ line: String) -> String {
let from = line.index(after: line.startIndex)
let to = line.index(before: line.endIndex)
return line[from..<to]
}
/**
Attempts to parse a regular line, which may contain a configuration value that is being set.
*/
private static func parseLine(_ line: String) -> (String, String)? {
let parts = stripComment(line)
.split(separator: "=", maxSplits: 1)
if parts.count == 2 {
let k = trim(String(parts[0]))
let v = trim(String(parts[1]))
return (k, v)
}
return nil
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 31/01/2021. // Created by Nico Verbruggen on 31/01/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@ -16,24 +16,26 @@ import Foundation
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/ instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
*/ */
class PhpExtension { class PhpExtension {
/// The file where this extension was located. /// The file where this extension was located.
var file: String var file: String
/// The original string that was used to determine this extension is active. /// The original string that was used to determine this extension is active.
var line: String var line: String
/// The name of the extension. This is always identical to the name found in the original string. If you want to display this name, capitalize this. /// The name of the extension. This is always identical to the name found in the original string.
/// If you want to display this name, capitalize this.
var name: String var name: String
/// Whether the extension has been enabled. /// Whether the extension has been enabled.
var enabled: Bool var enabled: Bool
/// The file where this extension was located, but only the filename, not the full path to the .ini file. /// The file where this extension was located, but only the filename, not the full path to the .ini file.
var fileNameOnly: String { var fileNameOnly: String {
return String(file.split(separator: "/").last ?? "php.ini") return String(file.split(separator: "/").last ?? "php.ini")
} }
// swiftlint:disable line_length
/** /**
This regular expression will allow us to identify lines which activate an extension. This regular expression will allow us to identify lines which activate an extension.
@ -47,29 +49,31 @@ class PhpExtension {
- Note: Extensions that are disabled in a different way will not be detected. This is intentional. - Note: Extensions that are disabled in a different way will not be detected. This is intentional.
*/ */
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"# static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
// swiftlint:enable line_length
/** /**
When registering an extension, we do that based on the line found inside the .ini file. When registering an extension, we do that based on the line found inside the .ini file.
*/ */
init(_ line: String, file: String) { init(_ line: String, file: String) {
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: []) let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
let match = regex.matches(in: line, options: [], range: NSMakeRange(0, line.count)).first let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
let range = Range(match!.range(withName: "name"), in: line)! let range = Range(match!.range(withName: "name"), in: line)!
self.line = line self.line = line
let fullPath = String(line[range]) let fullPath = String(line[range])
.replacingOccurrences(of: "\"", with: "") // replace excess " .replacingOccurrences(of: "\"", with: "") // replace excess "
.replacingOccurrences(of: ".so", with: "") // replace excess .so .replacingOccurrences(of: ".so", with: "") // replace excess .so
self.name = String(fullPath.split(separator: "/").last!) // take last segment self.name = String(fullPath.split(separator: "/").last!) // take last segment
self.enabled = !line.contains(";") self.enabled = !line.contains(";")
self.file = file self.file = file
} }
/** /**
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply. This simply toggles the extension in the .ini file.
You may need to restart the other services in order for this change to apply.
*/ */
func toggle() { func toggle() {
let newLine = enabled let newLine = enabled
@ -77,32 +81,34 @@ class PhpExtension {
? "; \(line)" ? "; \(line)"
// ENABLED: Line where the comment delimiter (;) is removed // ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "") : line.replacingOccurrences(of: "; ", with: "")
sed(file: file, original: line, replacement: newLine) sed(file: file, original: line, replacement: newLine)
enabled.toggle() enabled.toggle()
} }
// MARK: - Static Methods // MARK: - Static Methods
/** static func from(_ lines: [String], filePath: String) -> [PhpExtension] {
This method will attempt to identify all extensions in the .ini file at a certain URL. return lines.filter {
*/ return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
static func load(from path: URL) -> [PhpExtension] { }.map {
let file = try? String(contentsOf: path, encoding: .utf8) return PhpExtension($0, file: filePath)
}
if (file == nil) { }
static func from(filePath: String) -> [PhpExtension] {
let file = try? String(contentsOfFile: filePath)
if file == nil {
Log.err("There was an issue reading the file. Assuming no extensions were found.") Log.err("There was an issue reading the file. Assuming no extensions were found.")
return [] return []
} }
return file!.components(separatedBy: "\n") return Self.from(
.filter { file!.components(separatedBy: "\n"),
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil filePath: filePath
} )
.map {
return PhpExtension($0, file: path.path)
}
} }
} }

View File

@ -3,34 +3,34 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 28/11/2021. // Created by Nico Verbruggen on 28/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class PhpInstallation { class PhpInstallation {
var versionNumber: 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`
in the relevant directory. in the relevant directory.
*/ */
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.versionNumber = PhpVersionNumber.make(from: version)! self.versionNumber = PhpVersionNumber.make(from: version)!
if Filesystem.fileExists(phpConfigExecutablePath) { if Filesystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute( let longVersionString = Command.execute(
path: phpConfigExecutablePath, path: phpConfigExecutablePath,
arguments: ["--version"] arguments: ["--version"]
).trimmingCharacters(in: .whitespacesAndNewlines) ).trimmingCharacters(in: .whitespacesAndNewlines)
// 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.
self.versionNumber = try! PhpVersionNumber.parse(longVersionString) self.versionNumber = try! PhpVersionNumber.parse(longVersionString)
} }
} }
} }

View File

@ -3,13 +3,13 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 24/12/2021. // Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class InternalSwitcher: PhpSwitcher { class InternalSwitcher: PhpSwitcher {
/** /**
Switching to a new PHP version involves: Switching to a new PHP version involves:
- unlinking the current version - unlinking the current version
@ -20,59 +20,70 @@ class InternalSwitcher: PhpSwitcher {
the version that is switched to may or may not be identical to `php` the version that is switched to may or may not be identical to `php`
(without @version). (without @version).
*/ */
func performSwitch(to version: String, completion: @escaping () -> Void) func performSwitch(to version: String, completion: @escaping () -> Void) {
{
Log.info("Switching to \(version), unlinking all versions...") Log.info("Switching to \(version), unlinking all versions...")
let isolated = Valet.shared.sites.filter { site in let versions = getVersionsToBeHandled(version)
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 {
self.disableDefaultPhpFpmPool(available) self.disableDefaultPhpFpmPool(available)
self.stopPhpVersion(available) self.stopPhpVersion(available)
group.leave() group.leave()
} }
} }
group.notify(queue: .global(qos: .userInitiated)) { group.notify(queue: .global(qos: .userInitiated)) {
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!")
for formula in versions { for formula in versions {
self.startPhpVersion(formula, primary: (version == formula)) self.startPhpVersion(formula, primary: (version == formula))
} }
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 \(Homebrew.Formulae.nginx)", sudo: true)
Log.info("The new version(s) have been linked!") Log.info("The new version(s) have been linked!")
completion() completion()
} }
} }
private func disableDefaultPhpFpmPool(_ version: String) { func getVersionsToBeHandled(_ primary: String) -> Set<String> {
let isolated = Valet.shared.sites.filter { site in
site.isolatedPhpVersion != nil
}.map { site in
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
}
var versions: Set<String> = [primary]
if Valet.enabled(feature: .isolatedSites) {
versions = versions.union(isolated)
}
return versions
}
func requiresDisablingOfDefaultPhpFpmPool(_ version: String) -> Bool {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
return FileManager.default.fileExists(atPath: pool)
}
func disableDefaultPhpFpmPool(_ version: String) {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf" let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileManager.default.fileExists(atPath: pool) { if FileManager.default.fileExists(atPath: pool) {
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).") 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 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")! let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
do { do {
if (FileManager.default.fileExists(atPath: new.path)) { 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.") 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.removeItem(at: new)
} }
try FileManager.default.moveItem(at: existing, to: new) try FileManager.default.moveItem(at: existing, to: new)
@ -82,26 +93,26 @@ class InternalSwitcher: PhpSwitcher {
} }
} }
} }
private func stopPhpVersion(_ version: String) { func stopPhpVersion(_ version: String) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)" let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink \(formula)") brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true) brew("services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)") Log.info("Unlinked and stopped services for \(formula)")
} }
private func startPhpVersion(_ version: String, primary: Bool) { func startPhpVersion(_ version: String, primary: Bool) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)" let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
if (primary) { if primary {
Log.info("\(formula) is the primary formula, linking and starting services...") Log.info("\(formula) is the primary formula, linking and starting services...")
brew("link \(formula) --overwrite --force") brew("link \(formula) --overwrite --force")
} else { } else {
Log.info("\(formula) is an isolated PHP version, starting services only...") Log.info("\(formula) is an isolated PHP version, starting services only...")
} }
brew("services start \(formula)", sudo: true) brew("services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary { if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "") let socketVersion = version.replacingOccurrences(of: ".", with: "")
Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock") Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
@ -109,5 +120,5 @@ class InternalSwitcher: PhpSwitcher {
} }
} }
} }

View File

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

View File

@ -0,0 +1,15 @@
//
// CreatedFromFile.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol CreatedFromFile {
static func from(filePath: String) -> Self?
}

View File

@ -13,9 +13,9 @@
</head> </head>
<body> <body>
<br> <br>
<p><b>Want to spread the love?</b> Leave a <a href="https://github.com/nicoverbruggen/phpmon">star on GitHub</a>!</p> <p><b>Do you enjoy using the app?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
<p><b>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p> <p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p>
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p> <p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p> <p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
<br> <br>
</body> </body>

View File

@ -3,16 +3,16 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 05/12/2021. // Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
import Foundation import Foundation
extension App { extension App {
// MARK: - Application State // MARK: - Application State
/** /**
Registers a window as currently open. Registers a window as currently open.
*/ */
@ -22,7 +22,7 @@ extension App {
} }
updateActivationPolicy() updateActivationPolicy()
} }
/** /**
Removes a window, assuming it was closed. Removes a window, assuming it was closed.
*/ */
@ -32,13 +32,13 @@ extension App {
} }
updateActivationPolicy() updateActivationPolicy()
} }
/** /**
If there are any open windows, the app will be a regular app. If there are any open windows, the app will be a regular app.
If there are no windows open, the app will be an accessory (toolbar) app. If there are no windows open, the app will be an accessory (toolbar) app.
*/ */
public func updateActivationPolicy() { public func updateActivationPolicy() {
NSApp.setActivationPolicy(openWindows.count > 0 ? .regular : .accessory) NSApp.setActivationPolicy(!openWindows.isEmpty ? .regular : .accessory)
} }
} }

View File

@ -3,15 +3,15 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 05/12/2021. // Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
extension App { extension App {
// MARK: - Methods // MARK: - Methods
/** /**
On startup, the preferences should be loaded from the .plist, On startup, the preferences should be loaded from the .plist,
and we'll enable the shortcut if it is set. and we'll enable the shortcut if it is set.
@ -22,20 +22,20 @@ extension App {
Log.info("No global hotkey was saved in preferences. None set.") Log.info("No global hotkey was saved in preferences. None set.")
return return
} }
// Make sure we can parse the JSON into the desired format // Make sure we can parse the JSON into the desired format
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else { guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
Log.err("No global hotkey loaded, could not be parsed!") Log.err("No global hotkey loaded, could not be parsed!")
shortcutHotkey = nil shortcutHotkey = nil
return return
} }
shortcutHotkey = HotKey(keyCombo: KeyCombo( shortcutHotkey = HotKey(keyCombo: KeyCombo(
carbonKeyCode: keybindPref.keyCode, carbonKeyCode: keybindPref.keyCode,
carbonModifiers: keybindPref.carbonFlags carbonModifiers: keybindPref.carbonFlags
)) ))
} }
/** /**
Sets up the action that needs to occur when the shortcut key is pressed Sets up the action that needs to occur when the shortcut key is pressed
(opens the menu). (opens the menu).
@ -44,11 +44,11 @@ extension App {
guard let hotkey = shortcutHotkey else { guard let hotkey = shortcutHotkey else {
return return
} }
hotkey.keyDownHandler = { hotkey.keyDownHandler = {
MainMenu.shared.statusItem.button?.performClick(nil) MainMenu.shared.statusItem.button?.performClick(nil)
NSApplication.shared.activate(ignoringOtherApps: true) NSApplication.shared.activate(ignoringOtherApps: true)
} }
} }
} }

View File

@ -2,25 +2,35 @@
// StateManager.swift // StateManager.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
class App { class App {
// MARK: Static Vars // MARK: Static Vars
/** The static app instance. Accessible at any time. */ /** The static app instance. Accessible at any time. */
static let shared = App() static let shared = App()
/** Retrieve the version number from the main info dictionary, Info.plist. */ /** Retrieve the version number from the main info dictionary, Info.plist. */
static var version: String { static var version: String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
return "\(version) (\(build))" return "\(version) (\(build))"
} }
/** Just the bundle version (build). */
static var bundleVersion: String {
return Bundle.main.infoDictionary?["CFBundleVersion"] as! String
}
/** Just the version number. */
static var shortVersion: String {
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
}
static var architecture: String { static var architecture: String {
var systeminfo = utsname() var systeminfo = utsname()
uname(&systeminfo) uname(&systeminfo)
@ -34,37 +44,49 @@ class App {
} }
return machine return machine
} }
// MARK: Variables // MARK: Variables
/** The list of preferences that are currently active. */ /** The list of preferences that are currently active. */
var preferences: [PreferenceName: Bool]! var preferences: [PreferenceName: Bool]!
/** The window controller of the currently active preferences window. */ /** The window controller of the currently active preferences window. */
var preferencesWindowController: PrefsWC? = nil var preferencesWindowController: PreferencesWindowController?
/** The window controller of the currently active site list window. */ /** The window controller of the currently active site list window. */
var siteListWindowController: SiteListWC? = nil var domainListWindowController: DomainListWindowController?
/** The window controller of the onboarding window. */
var onboardingWindowController: OnboardingWindowController?
/** The window controller of the warnings window. */
var warningsWindowController: WarningsWindowController?
/** List of detected (installed) applications that PHP Monitor can work with. */ /** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = [] var detectedApplications: [Application] = []
/** The services manager, responsible for figuring out what services are active/inactive. */
var services = ServicesManager.shared
/** The warning manager, responsible for keeping track of warnings. */
var warnings = WarningManager.shared
/** Timer that will periodically reload info about the user's PHP installation. */ /** Timer that will periodically reload info about the user's PHP installation. */
var timer: Timer? var timer: Timer?
// MARK: - Global Hotkey // MARK: - Global Hotkey
/** /**
The shortcut the user has requested. The shortcut the user has requested.
*/ */
var shortcutHotkey: HotKey? = nil { var shortcutHotkey: HotKey? {
didSet { didSet {
setupGlobalHotkeyListener() setupGlobalHotkeyListener()
} }
} }
// MARK: - Activation Policy // MARK: - Activation Policy
/** /**
Variable that keeps track of which windows are currently open. Variable that keeps track of which windows are currently open.
(Please note that window controllers remain open in memory once opened.) (Please note that window controllers remain open in memory once opened.)
@ -74,9 +96,9 @@ class App {
(as a normal app or as a toolbar app). (as a normal app or as a toolbar app).
*/ */
var openWindows: [String] = [] var openWindows: [String] = []
// MARK: - App Watchers // MARK: - App Watchers
/** /**
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder. The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
*/ */

View File

@ -3,14 +3,14 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 20/12/2021. // Created by Nico Verbruggen on 20/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
import Foundation import Foundation
extension AppDelegate { extension AppDelegate {
/** /**
This is an entry point for future development for integrating with the PHP Monitor This is an entry point for future development for integrating with the PHP Monitor
application URL. You can use the `phpmon://` protocol to communicate with the app. application URL. You can use the `phpmon://` protocol to communicate with the app.
@ -21,20 +21,20 @@ extension AppDelegate {
Please note that PHP Monitor needs to be running in the background for this to work. Please note that PHP Monitor needs to be running in the background for this to work.
*/ */
func application(_ application: NSApplication, open urls: [URL]) { func application(_ application: NSApplication, open urls: [URL]) {
if !Preferences.isEnabled(.allowProtocolForIntegrations) { if !Preferences.isEnabled(.allowProtocolForIntegrations) {
Log.info("Acting on commands via phpmon:// has been disabled.") Log.info("Acting on commands via phpmon:// has been disabled.")
return return
} }
guard let url = urls.first else { return } guard let url = urls.first else { return }
self.interpretCommand( self.interpretCommand(
url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""), url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""),
commands: InterApp.getCommands() commands: InterApp.getCommands()
) )
} }
private func interpretCommand(_ command: String, commands: [InterApp.Action]) { private func interpretCommand(_ command: String, commands: [InterApp.Action]) {
commands.forEach { action in commands.forEach { action in
if command.starts(with: action.command) { if command.starts(with: action.command) {
@ -44,4 +44,3 @@ extension AppDelegate {
} }
} }
} }

View File

@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 05/12/2021. // Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@ -22,34 +22,34 @@ import AppKit
For more information about this, please see the ActivationPolicy-related extension. For more information about this, please see the ActivationPolicy-related extension.
*/ */
extension AppDelegate { extension AppDelegate {
// MARK: - Menu Interactions // MARK: - Menu Interactions
@IBAction func addSiteLinkPressed(_ sender: Any) { @IBAction func addSiteLinkPressed(_ sender: Any) {
SiteListVC.show() DomainListVC.show()
guard let windowController = App.shared.siteListWindowController else { return } guard let windowController = App.shared.domainListWindowController else { return }
windowController.pressedAddLink(nil) windowController.pressedAddLink(nil)
} }
@IBAction func reloadSiteListPressed(_ sender: Any) { @IBAction func reloadDomainListPressed(_ sender: Any) {
let vc = App.shared.siteListWindowController? let vc = App.shared.domainListWindowController?
.window?.contentViewController as? SiteListVC .window?.contentViewController as? DomainListVC
if vc != nil { if vc != nil {
// If the view exists, directly reload the list of sites // If the view exists, directly reload the list of sites
vc!.reloadSites() vc!.reloadDomains()
} else { } else {
// If the view does not exist, reload the cached data that was populated when the app initially launched. // If the view does not exist, reload the cached data that was populated when the app initially launched.
Valet.shared.reloadSites() Valet.shared.reloadSites()
} }
} }
@IBAction func focusSearchField(_ sender: Any) { @IBAction func focusSearchField(_ sender: Any) {
SiteListVC.show() DomainListVC.show()
guard let windowController = App.shared.siteListWindowController else { return } guard let windowController = App.shared.domainListWindowController else { return }
windowController.searchToolbarItem.searchField.becomeFirstResponder() windowController.searchToolbarItem.searchField.becomeFirstResponder()
} }
} }

View File

@ -3,16 +3,16 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 06/12/2021. // Created by Nico Verbruggen on 06/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
import UserNotifications import UserNotifications
extension AppDelegate { extension AppDelegate {
// MARK: - Notifications // MARK: - Notifications
/** /**
Sets up notifications. That does mean we need to ask for permission first. Sets up notifications. That does mean we need to ask for permission first.
If we cannot get permission, we should log this. If we cannot get permission, we should log this.
@ -30,7 +30,7 @@ extension AppDelegate {
} }
}) })
} }
/** /**
Ensure that the application displays notifications even when the app is active. Ensure that the application displays notifications even when the app is active.
*/ */
@ -42,5 +42,5 @@ extension AppDelegate {
) { ) {
completionHandler([.banner]) completionHandler([.banner])
} }
} }

View File

@ -2,7 +2,7 @@
// AppDelegate.swift // AppDelegate.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
@ -10,55 +10,55 @@ import UserNotifications
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
// MARK: - Variables // MARK: - Variables
/** /**
The Shell singleton that keeps track of the history of all The Shell singleton that keeps track of the history of all
(invoked by PHP Monitor) shell commands. It is used to (invoked by PHP Monitor) shell commands. It is used to
invoke all commands in this application. invoke all commands in this application.
*/ */
let sharedShell: Shell let sharedShell: Shell
/** /**
The App singleton contains information about the state of The App singleton contains information about the state of
the application and global variables. the application and global variables.
*/ */
let state: App let state: App
/** /**
The MainMenu singleton is responsible for rendering the The MainMenu singleton is responsible for rendering the
menu bar item and its menu, as well as its actions. menu bar item and its menu, as well as its actions.
*/ */
let menu: MainMenu let menu: MainMenu
/** /**
The paths singleton that determines where Homebrew is installed, The paths singleton that determines where Homebrew is installed,
and where to look for binaries. and where to look for binaries.
*/ */
let paths: Paths let paths: Paths
/** /**
The Valet singleton that determines all information The Valet singleton that determines all information
about Valet and its current configuration. about Valet and its current configuration.
*/ */
let valet: Valet let valet: Valet
/** /**
The PhpEnv singleton that handles PHP version The PhpEnv singleton that handles PHP version
detection, as well as switching. It is initialized detection, as well as switching. It is initialized
when the app is ready and passed all checks. when the app is ready and passed all checks.
*/ */
var phpEnvironment: PhpEnv! = nil var phpEnvironment: PhpEnv! = nil
/** /**
The logger is responsible for different levels of logging. The logger is responsible for different levels of logging.
You can tweak the verbosity in the `init` method here. You can tweak the verbosity in the `init` method here.
*/ */
var logger = Log.shared var logger = Log.shared
// MARK: - Initializer // MARK: - Initializer
/** /**
When the application initializes, create all singletons. When the application initializes, create all singletons.
*/ */
@ -67,6 +67,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
#if DEBUG #if DEBUG
logger.verbosity = .performance logger.verbosity = .performance
#endif #endif
if CommandLine.arguments.contains("--v") {
logger.verbosity = .performance
Log.info("Extra verbose mode has been activated.")
}
Log.separator(as: .info) 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)")
@ -78,13 +82,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
self.valet = Valet.shared self.valet = Valet.shared
super.init() super.init()
} }
func initializeSwitcher() { func initializeSwitcher() {
self.phpEnvironment = PhpEnv.shared self.phpEnvironment = PhpEnv.shared
} }
// MARK: - Lifecycle // MARK: - Lifecycle
/** /**
When the application has finished launching, we'll want to set up When the application has finished launching, we'll want to set up
the user notification center permissions, and kickoff the menu the user notification center permissions, and kickoff the menu
@ -96,5 +100,5 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// Make sure the menu performs its initial checks // Make sure the menu performs its initial checks
Task { await menu.startup() } Task { await menu.startup() }
} }
} }

View File

@ -0,0 +1,182 @@
//
// Updater.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 09/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class AppUpdateChecker {
public static var enabled: Bool = {
return Preferences.isEnabled(.automaticBackgroundUpdateCheck)
}()
public static var isDev: Bool = {
return App.version.contains("-dev")
}()
public static func retrieveVersionFromCask(
_ initiatedFromBackground: Bool = true
) -> String {
let caskFile = App.version.contains("-dev")
? Constants.Urls.DevBuildCaskFile.absoluteString
: Constants.Urls.StableBuildCaskFile.absoluteString
var command = "curl -s"
if initiatedFromBackground {
command = "curl -s --max-time 5"
}
return Shell.pipe(
"\(command) '\(caskFile)' | grep version"
)
}
public static func checkIfNewerVersionIsAvailable(
initiatedFromBackground: Bool = true
) {
if initiatedFromBackground {
if !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
Log.info("Automatic updates are disabled. No check will be performed.")
return
}
Log.info("Automatic updates are enabled, a check will be performed.")
}
let versionString = retrieveVersionFromCask(initiatedFromBackground)
guard let onlineVersion = AppVersion.from(versionString) else {
Log.err("We couldn't check for updates!")
// Only notify about connection issues if the request to check for updates was explicit
if !initiatedFromBackground {
notifyAboutConnectionIssue()
}
return
}
let currentVersion = AppVersion.fromCurrentVersion()
handleVersionComparison(
currentVersion,
onlineVersion,
initiatedFromBackground
)
}
private static func handleVersionComparison(
_ currentVersion: AppVersion,
_ onlineVersion: AppVersion,
_ background: Bool
) {
switch onlineVersion.version.versionCompare(currentVersion.version) {
case .orderedAscending:
Log.info("You are running a newer version of PHP Monitor "
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
if !background { notifyVersionDoesNotNeedUpgrade() }
case .orderedDescending:
Log.info("There is a newer version (\(onlineVersion)) available! "
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
notifyAboutNewerVersion(version: onlineVersion)
case .orderedSame:
if currentVersion.build != nil
&& onlineVersion.build != nil
&& buildDiffers(currentVersion, onlineVersion, background) {
return
}
Log.info("The installed version (\(currentVersion.computerReadable)) matches the latest release "
+ "(\(onlineVersion.computerReadable)).")
if !background { notifyVersionDoesNotNeedUpgrade() }
}
}
private static func buildDiffers(
_ currentVersion: AppVersion,
_ onlineVersion: AppVersion,
_ background: Bool
) -> Bool {
if Int(onlineVersion.build!)! > Int(currentVersion.build!)! {
Log.info("There is a newer build of PHP Monitor available! "
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
notifyAboutNewerVersion(version: onlineVersion)
return true
} else if Int(onlineVersion.build!)! < Int(currentVersion.build!)! {
Log.info("You are running a newer build of PHP Monitor "
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
if !background { notifyVersionDoesNotNeedUpgrade() }
return true
}
return false
}
private static func notifyVersionDoesNotNeedUpgrade() {
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.alerts.is_latest_version.title".localized,
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
description: ""
)
.withPrimary(text: "OK")
.show()
}
}
private static func notifyAboutNewerVersion(version: AppVersion) {
let devSuffix = isDev ? "-dev" : ""
let command = isDev ? "brew upgrade phpmon-dev" : "brew upgrade phpmon"
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.alerts.newer_version_available.title".localized(version.humanReadable),
subtitle: "updater.alerts.newer_version_available.subtitle".localized,
description: HomebrewDiagnostics.customCaskInstalled
? "updater.installation_source.brew".localized(command)
: "updater.installation_source.direct".localized
)
.withPrimary(
text: "updater.alerts.buttons.release_notes".localized,
action: { vc in
vc.close(with: .OK)
NSWorkspace.shared.open(
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version.tagged)\(devSuffix)")
)
}
)
.withTertiary(text: "Dismiss", action: { vc in
vc.close(with: .OK)
})
.show()
}
}
private static func notifyAboutConnectionIssue() {
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.alerts.cannot_check_for_update.title".localized,
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
description: "updater.alerts.cannot_check_for_update.description".localized(
App.version
)
)
.withTertiary(
text: "updater.alerts.buttons.releases_on_github".localized,
action: { _ in
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
}
)
.withPrimary(text: "OK")
.show()
}
}
}

View File

@ -0,0 +1,85 @@
//
// AppVersion.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class AppVersion {
var version: String
var build: String?
var suffix: String?
init(version: String, build: String?, suffix: String? = nil) {
self.version = version
self.build = build
self.suffix = suffix
}
public static func from(_ string: String) -> AppVersion? {
do {
let regex = try NSRegularExpression(
pattern: #"(?<version>(\d+)[.](\d+)([.](\d+))?)(-(?<suffix>[a-z]+)){0,1}((,|_)(?<build>\d+)){0,1}"#,
options: []
)
let match = regex.matches(
in: string,
options: [],
range: NSRange(location: 0, length: string.count)
).first
guard let match = match else {
return nil
}
var version: String = ""
var build: String?
var suffix: String?
if let versionRange = Range(match.range(withName: "version"), in: string) {
version = String(string[versionRange])
}
if let buildRange = Range(match.range(withName: "build"), in: string) {
build = String(string[buildRange])
}
if let suffixRange = Range(match.range(withName: "suffix"), in: string) {
suffix = String(string[suffixRange])
}
return AppVersion(
version: version,
build: build,
suffix: suffix
)
} catch {
return nil
}
}
public static func fromCurrentVersion() -> AppVersion {
return AppVersion.from("\(App.shortVersion)_\(App.bundleVersion)")!
}
var tagged: String {
if version.suffix(2) == ".0" && version.count > 3 {
return String(version.dropLast(2))
}
return version
}
var computerReadable: String {
return "\(version)_\(build ?? "0")"
}
var humanReadable: String {
return "\(version) (\(build ?? "???"))"
}
}

View File

@ -60,16 +60,16 @@
</menuItem> </menuItem>
<menuItem title="reload-list" keyEquivalent="r" id="Ema-AU-Nbr" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target"> <menuItem title="reload-list" keyEquivalent="r" id="Ema-AU-Nbr" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes> <userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_site_list"/> <userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_domain_list"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
<connections> <connections>
<action selector="reloadSiteListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/> <action selector="reloadDomainListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="2ux-8Q-UjK"/> <menuItem isSeparatorItem="YES" id="2ux-8Q-UjK"/>
<menuItem title="focus-find" keyEquivalent="f" id="I95-fb-EL7" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target"> <menuItem title="focus-find" keyEquivalent="f" id="I95-fb-EL7" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes> <userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_site_list"/> <userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_domain_list"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
<connections> <connections>
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/> <action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
@ -319,41 +319,37 @@
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/> <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
</objects> </objects>
<point key="canvasLocation" x="-495" y="-44"/> <point key="canvasLocation" x="-360" y="-94"/>
</scene> </scene>
<!--Window Controller--> <!--Window Controller-->
<scene sceneID="PQa-AT-b2a"> <scene sceneID="PQa-AT-b2a">
<objects> <objects>
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PrefsWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController"> <customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PreferencesWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="h4c-3b-nko"> <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="h4c-3b-nko">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/> <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="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="372" y="403" width="480" height="270"/> <rect key="contentRect" x="372" y="403" width="550" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/> <rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/>
<view key="contentView" id="2yL-50-11x"> <view key="contentView" id="2yL-50-11x">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/> <rect key="frame" x="0.0" y="0.0" width="550" height="270"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</view> </view>
<toolbar key="toolbar" implicitIdentifier="611E3485-DC7F-46A0-8528-11CF9366370C" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconAndLabel" sizeMode="regular" id="fcq-wR-7iv">
<allowedToolbarItems/>
<defaultToolbarItems/>
</toolbar>
<connections> <connections>
<outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/> <outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/>
</connections> </connections>
</window> </window>
<connections> <connections>
<segue destination="AW2-rV-rbS" kind="relationship" relationship="window.shadowedContentViewController" id="3dX-9V-eA0"/> <segue destination="PCI-2c-55Y" kind="relationship" relationship="window.shadowedContentViewController" id="egC-A4-am8"/>
</connections> </connections>
</windowController> </windowController>
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-374" y="327"/> <point key="canvasLocation" x="-374" y="238"/>
</scene> </scene>
<!--Preferences--> <!--Preferences-->
<scene sceneID="iyi-IS-7Ps"> <scene sceneID="iyi-IS-7Ps">
<objects> <objects>
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController"> <viewController title="Preferences" identifier="preferencesTemplateVC" storyboardIdentifier="preferencesTemplateVC" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="GenericPreferenceVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" wantsLayer="YES" id="Pf1-A5-3Xz"> <view key="view" wantsLayer="YES" id="Pf1-A5-3Xz">
<rect key="frame" x="0.0" y="0.0" width="550" height="498"/> <rect key="frame" x="0.0" y="0.0" width="550" height="498"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
@ -378,13 +374,33 @@
</viewController> </viewController>
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="251" y="205"/> <point key="canvasLocation" x="844" y="-153"/>
</scene>
<!--Tab View Controller-->
<scene sceneID="B5x-d3-c7D">
<objects>
<customObject id="pNW-tM-SQu" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<tabViewController tabStyle="toolbar" canPropagateSelectedChildViewControllerTitle="NO" id="PCI-2c-55Y" sceneMemberID="viewController">
<tabView key="tabView" type="noTabsNoBorder" id="l0U-9a-nM6">
<rect key="frame" x="0.0" y="0.0" width="508" height="300"/>
<autoresizingMask key="autoresizingMask"/>
<font key="font" metaFont="message"/>
<connections>
<outlet property="delegate" destination="PCI-2c-55Y" id="6gR-GR-cwq"/>
</connections>
</tabView>
<connections>
<outlet property="tabView" destination="l0U-9a-nM6" id="tfn-UN-1Aa"/>
</connections>
</tabViewController>
</objects>
<point key="canvasLocation" x="283" y="-252"/>
</scene> </scene>
<!--Window Controller--> <!--Window Controller-->
<scene sceneID="4XS-kY-YIS"> <scene sceneID="4XS-kY-YIS">
<objects> <objects>
<windowController storyboardIdentifier="siteListWindow" id="8Ec-9q-82s" customClass="SiteListWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController"> <windowController storyboardIdentifier="domainListWindow" id="8Ec-9q-82s" customClass="DomainListWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<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" separatorStyle="line" 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="600" height="263"/> <rect key="contentRect" x="425" y="461" width="600" height="263"/>
@ -437,7 +453,7 @@
</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="746"/> <point key="canvasLocation" x="-374" y="745.5"/>
</scene> </scene>
<!--Window Controller--> <!--Window Controller-->
<scene sceneID="HTI-x5-rOp"> <scene sceneID="HTI-x5-rOp">
@ -462,7 +478,7 @@
</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="-409" y="1137"/> <point key="canvasLocation" x="-374" y="1137"/>
</scene> </scene>
<!--Window Controller--> <!--Window Controller-->
<scene sceneID="BD0-La-ygq"> <scene sceneID="BD0-La-ygq">
@ -486,7 +502,7 @@
</windowController> </windowController>
<customObject id="i3j-z8-nxv" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="i3j-z8-nxv" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-575" y="1624"/> <point key="canvasLocation" x="-374" y="2267"/>
</scene> </scene>
<!--Better AlertVC--> <!--Better AlertVC-->
<scene sceneID="y9E-bB-wIG"> <scene sceneID="y9E-bB-wIG">
@ -532,7 +548,7 @@ Gw
</connections> </connections>
</button> </button>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j"> <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j">
<rect key="frame" x="13" y="13" width="82" height="32"/> <rect key="frame" x="13" y="13" width="81" height="32"/>
<buttonCell key="cell" type="push" title="Tertiary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="mzA-Uu-gyf"> <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"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -632,27 +648,27 @@ Gw
</viewController> </viewController>
<customObject id="5Ts-EZ-bJh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="5Ts-EZ-bJh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="38" y="1624"/> <point key="canvasLocation" x="230" y="2267"/>
</scene> </scene>
<!--Add SiteVC--> <!--Add SiteVC-->
<scene sceneID="6JC-H6-u4K"> <scene sceneID="6JC-H6-u4K">
<objects> <objects>
<viewController storyboardIdentifier="newSiteLink" id="glS-wF-sEU" customClass="AddSiteVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController"> <viewController storyboardIdentifier="newSiteLink" id="glS-wF-sEU" customClass="AddSiteVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="JJJ-T9-Yuv"> <view key="view" id="JJJ-T9-Yuv">
<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="245"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="js9-OW-xzC"> <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"/> <rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
<view key="contentView" id="HRC-RT-LxR"> <view key="contentView" id="HRC-RT-LxR">
<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="245"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view> </view>
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box> </box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
<rect key="frame" x="363" y="13" width="104" height="32"/> <rect key="frame" x="326" y="13" width="141" 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="[i18n] 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"/>
<string key="keyEquivalent" base64-UTF8="YES"> <string key="keyEquivalent" base64-UTF8="YES">
@ -664,11 +680,11 @@ 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="13" y="13" width="94" height="32"/> <rect key="frame" x="13" y="13" width="114" height="32"/>
<constraints> <constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
</constraints> </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="[i18n] 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"/>
<string key="keyEquivalent" base64-UTF8="YES"> <string key="keyEquivalent" base64-UTF8="YES">
@ -680,8 +696,8 @@ Gw
</connections> </connections>
</button> </button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i"> <textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
<rect key="frame" x="20" y="156" width="440" height="21"/> <rect key="frame" x="20" y="150" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a potential domain name here." drawsBackground="YES" id="NFa-1D-Bi4"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
<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"/>
@ -691,16 +707,16 @@ Gw
</connections> </connections>
</textField> </textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
<rect key="frame" x="18" y="134" width="444" height="14"/> <rect key="frame" x="18" y="128" width="444" height="14"/>
<textFieldCell key="cell" title="FOLDER_AVAILABLE" id="bJr-s6-tdP"> <textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
<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>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KZf-b0-9cm"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KZf-b0-9cm">
<rect key="frame" x="18" y="101" width="227" height="18"/> <rect key="frame" x="18" y="95" width="266" height="18"/>
<buttonCell key="cell" type="check" title="Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ"> <buttonCell key="cell" type="check" title="[i18n] Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
</buttonCell> </buttonCell>
@ -709,31 +725,31 @@ Gw
</connections> </connections>
</button> </button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
<rect key="frame" x="18" y="66" width="444" height="28"/> <rect key="frame" x="18" y="60" width="444" height="28"/>
<textFieldCell key="cell" title="Securing a site requires administrative privileges. You will be prompted for your password or Touch ID." id="4gd-KM-5Fu"> <textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges. You may be prompted for your password or Touch ID." id="4gd-KM-5Fu">
<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>
<pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6JT-Vt-3q0"> <pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6JT-Vt-3q0">
<rect key="frame" x="20" y="185" width="440" height="22"/> <rect key="frame" x="20" y="179" 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/"/> <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">
<rect key="frame" x="18" y="215" width="87" height="16"/> <rect key="frame" x="18" y="209" width="128" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Link a Folder" id="S4j-ZC-ddT"> <textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
<font key="font" textStyle="headline" name=".SFNS-Bold"/> <font key="font" textStyle="headline" name=".SFNS-Bold"/>
<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>
<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="229" y="23" width="128" height="14"/> <rect key="frame" x="140" y="23" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That link already exists." id="jOt-n6-TQf"> <textFieldCell key="cell" lineBreakMode="clipping" title="That domain name 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"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -752,6 +768,7 @@ Gw
<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="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="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="SwS-o8-pbl" firstAttribute="top" secondItem="mmQ-7e-dlb" secondAttribute="bottom" constant="20" id="VNW-fB-2Xj"/>
<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 firstAttribute="trailing" secondItem="PVw-cM-qAB" secondAttribute="trailing" constant="20" symbolic="YES" id="X5z-G4-CBv"/>
@ -778,7 +795,7 @@ Gw
<outlet property="buttonCancel" destination="SwS-o8-pbl" id="N1v-uy-2Mi"/> <outlet property="buttonCancel" destination="SwS-o8-pbl" id="N1v-uy-2Mi"/>
<outlet property="buttonCreateLink" destination="PVw-cM-qAB" id="0Oo-xW-He7"/> <outlet property="buttonCreateLink" destination="PVw-cM-qAB" id="0Oo-xW-He7"/>
<outlet property="buttonSecure" destination="KZf-b0-9cm" id="5A7-Bn-NB7"/> <outlet property="buttonSecure" destination="KZf-b0-9cm" id="5A7-Bn-NB7"/>
<outlet property="linkName" destination="ZX9-s1-23i" id="yT6-80-Zr1"/> <outlet property="inputDomainName" destination="ZX9-s1-23i" id="yT6-80-Zr1"/>
<outlet property="pathControl" destination="6JT-Vt-3q0" id="f5K-8h-VOd"/> <outlet property="pathControl" destination="6JT-Vt-3q0" id="f5K-8h-VOd"/>
<outlet property="previewText" destination="VzR-5a-cmT" id="qwd-wX-645"/> <outlet property="previewText" destination="VzR-5a-cmT" id="qwd-wX-645"/>
<outlet property="textFieldError" destination="900-Z2-tID" id="qUk-FE-IKW"/> <outlet property="textFieldError" destination="900-Z2-tID" id="qUk-FE-IKW"/>
@ -788,12 +805,12 @@ 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="191" y="1098.5"/> <point key="canvasLocation" x="210" y="1128"/>
</scene> </scene>
<!--Site ListVC--> <!--Domain 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="domainList" storyboardIdentifier="domainList" id="JZI-Vd-9oq" customClass="DomainListVC" 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="626" height="309"/> <rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
@ -804,8 +821,8 @@ Gw
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/> <rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<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"> <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" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="662" height="281"/> <rect key="frame" x="0.0" y="0.0" width="626" 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"/>
@ -825,7 +842,7 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Secure"/> <sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Secure"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews> <prototypeCellViews>
<tableCellView identifier="siteListTLSCell" id="hft-M4-nWb" customClass="SiteListTLSCell" customModule="PHP_Monitor" customModuleProvider="target"> <tableCellView identifier="domainListTLSCell" id="hft-M4-nWb" customClass="DomainListTLSCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="18" y="0.0" width="34" height="55"/> <rect key="frame" x="18" y="0.0" width="34" height="55"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
@ -848,7 +865,7 @@ Gw
</tableCellView> </tableCellView>
</prototypeCellViews> </prototypeCellViews>
</tableColumn> </tableColumn>
<tableColumn identifier="DOMAIN" width="290" minWidth="250" maxWidth="10000" id="oeH-B2-0rA"> <tableColumn identifier="DOMAIN" width="200" minWidth="200" maxWidth="10000" id="oeH-B2-0rA">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Domain"> <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"/>
@ -861,8 +878,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Domain"/> <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="siteListNameCell" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListNameCell" customModule="PHP_Monitor" customModuleProvider="target"> <tableCellView identifier="domainListNameCell" wantsLayer="YES" id="5GY-nN-BWd" customClass="DomainListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="69" y="0.0" width="290" height="54"/> <rect key="frame" x="69" y="0.0" width="200" 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">
@ -910,8 +927,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="PHP"/> <sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="PHP"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews> <prototypeCellViews>
<tableCellView identifier="siteListPhpCell" wantsLayer="YES" id="T49-0U-d58" customClass="SiteListPhpCell" customModule="PHP_Monitor" customModuleProvider="target"> <tableCellView identifier="domainListPhpCell" wantsLayer="YES" id="T49-0U-d58" customClass="DomainListPhpCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="376" y="0.0" width="100" height="54"/> <rect key="frame" x="286" y="0.0" width="100" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
@ -965,8 +982,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Kind"/> <sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Kind"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews> <prototypeCellViews>
<tableCellView identifier="siteListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="SiteListKindCell" customModule="PHP_Monitor" customModuleProvider="target"> <tableCellView identifier="domainListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="DomainListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="493" y="0.0" width="36" height="54"/> <rect key="frame" x="403" y="0.0" width="36" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1"> <imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
@ -1002,8 +1019,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Type"/> <sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Type"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews> <prototypeCellViews>
<tableCellView identifier="siteListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="SiteListTypeCell" customModule="PHP_Monitor" customModuleProvider="target"> <tableCellView identifier="domainListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="DomainListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="546" y="0.0" width="97" height="54"/> <rect key="frame" x="456" y="0.0" width="97" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
@ -1060,7 +1077,7 @@ Gw
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</scroller> </scroller>
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh"> <tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
<rect key="frame" x="0.0" y="0.0" width="662" height="28"/> <rect key="frame" x="0.0" y="0.0" width="626" height="28"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</tableHeaderView> </tableHeaderView>
</scrollView> </scrollView>
@ -1088,12 +1105,377 @@ 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="388" y="715.5"/> <point key="canvasLocation" x="323" y="723"/>
</scene>
<!--Add ProxyVC-->
<scene sceneID="g8z-pE-RL9">
<objects>
<viewController storyboardIdentifier="newProxyLink" id="dwh-CF-6iv" customClass="AddProxyVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="U5U-QR-YXS">
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="kkd-UV-SnA">
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
<view key="contentView" id="IXW-35-8NJ">
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
<rect key="frame" x="20" y="196" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
<rect key="frame" x="18" y="221" width="325" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Proxy subject (usually: protocol, IP address and port)" id="G1Z-3f-BhL">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
<rect key="frame" x="18" y="172" width="112" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
<rect key="frame" x="20" y="147" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="dwh-CF-6iv" id="e9n-PM-7s8"/>
</connections>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="SNw-oQ-bnb" secondAttribute="trailing" constant="20" id="2ui-Jg-BUV"/>
<constraint firstItem="mlA-Zt-Hu8" firstAttribute="top" secondItem="QCK-Z9-w7g" secondAttribute="bottom" constant="10" id="8sn-dT-SW6"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Uib-vA-HRc" secondAttribute="trailing" constant="20" symbolic="YES" id="Cue-3e-doM"/>
<constraint firstItem="QCK-Z9-w7g" firstAttribute="leading" secondItem="SNw-oQ-bnb" secondAttribute="leading" id="N1K-69-wLz"/>
<constraint firstItem="mlA-Zt-Hu8" firstAttribute="leading" secondItem="QCK-Z9-w7g" secondAttribute="leading" id="R74-k0-96U"/>
<constraint firstItem="SNw-oQ-bnb" firstAttribute="leading" secondItem="IXW-35-8NJ" secondAttribute="leading" constant="20" id="WZR-f8-mgf"/>
<constraint firstItem="SNw-oQ-bnb" firstAttribute="top" secondItem="mlA-Zt-Hu8" secondAttribute="bottom" constant="4" id="XDn-h9-dgp"/>
<constraint firstItem="QCK-Z9-w7g" firstAttribute="top" secondItem="Uib-vA-HRc" secondAttribute="bottom" constant="4" id="fGU-al-B0w"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="mlA-Zt-Hu8" secondAttribute="trailing" constant="20" symbolic="YES" id="uFE-cU-KOg"/>
<constraint firstItem="QCK-Z9-w7g" firstAttribute="trailing" secondItem="SNw-oQ-bnb" secondAttribute="trailing" id="xQE-yY-gPd"/>
</constraints>
</view>
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Vi-cN-ude">
<rect key="frame" x="317" y="13" width="150" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create Proxy" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="H2Z-c5-5Vk">
<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="pressedCreateProxy:" target="dwh-CF-6iv" id="wFW-Aw-FOR"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
<rect key="frame" x="13" y="13" width="114" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="uCc-fF-wS2"/>
</constraints>
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="D8g-GE-7TU">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
<rect key="frame" x="18" y="128" width="444" height="14"/>
<textFieldCell key="cell" title="[i18n] Preview text here" id="ISE-9R-ncQ">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rJa-yg-nCn">
<rect key="frame" x="18" y="95" width="170" height="18"/>
<buttonCell key="cell" type="check" title="[i18n] Secure this proxy" bezelStyle="regularSquare" imagePosition="left" inset="2" id="5LI-lt-Asl">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="pressedSecure:" target="dwh-CF-6iv" id="b74-8T-AzO"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
<rect key="frame" x="18" y="60" width="444" height="28"/>
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges. You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
<rect key="frame" x="18" y="250" width="123" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
<rect key="frame" x="131" y="23" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="nC0-dk-QaF" secondAttribute="bottom" constant="20" symbolic="YES" id="3Kk-fY-SB7"/>
<constraint firstItem="JSZ-x8-Pqi" firstAttribute="trailing" secondItem="SNw-oQ-bnb" secondAttribute="trailing" id="3So-Wu-1cz"/>
<constraint firstItem="DAh-br-Dfx" firstAttribute="top" secondItem="U5U-QR-YXS" secondAttribute="top" constant="20" symbolic="YES" id="3im-Qd-loW"/>
<constraint firstItem="kkd-UV-SnA" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" id="6iw-dd-hTX"/>
<constraint firstItem="Uib-vA-HRc" firstAttribute="leading" secondItem="DAh-br-Dfx" secondAttribute="leading" id="6jA-Kj-Q7l"/>
<constraint firstAttribute="trailing" secondItem="kkd-UV-SnA" secondAttribute="trailing" id="8YX-CO-sY2"/>
<constraint firstAttribute="trailing" secondItem="5x7-ll-2f7" secondAttribute="trailing" constant="20" symbolic="YES" id="8jr-cl-x78"/>
<constraint firstItem="kkd-UV-SnA" firstAttribute="top" secondItem="U5U-QR-YXS" secondAttribute="top" id="Afh-Ur-QgJ"/>
<constraint firstItem="4Vi-cN-ude" firstAttribute="leading" secondItem="w0k-CK-0u4" secondAttribute="trailing" constant="15" id="D3C-co-B10"/>
<constraint firstItem="w0k-CK-0u4" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="nC0-dk-QaF" secondAttribute="trailing" constant="8" symbolic="YES" id="FGk-wm-1Mu"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="rJa-yg-nCn" secondAttribute="trailing" constant="20" symbolic="YES" id="Fa7-Rc-1lj"/>
<constraint firstAttribute="trailing" secondItem="4Vi-cN-ude" secondAttribute="trailing" constant="20" symbolic="YES" id="Fbg-C8-v6E"/>
<constraint firstItem="5x7-ll-2f7" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="Fd0-zd-od8"/>
<constraint firstAttribute="bottom" secondItem="4Vi-cN-ude" secondAttribute="bottom" constant="20" symbolic="YES" id="GyL-uL-sjW"/>
<constraint firstItem="w0k-CK-0u4" firstAttribute="centerY" secondItem="4Vi-cN-ude" secondAttribute="centerY" id="HcL-wb-0s6"/>
<constraint firstItem="rJa-yg-nCn" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="IEg-SN-bHB"/>
<constraint firstItem="rJa-yg-nCn" firstAttribute="top" secondItem="JSZ-x8-Pqi" secondAttribute="bottom" constant="16" id="IW3-MX-3Kh"/>
<constraint firstItem="DAh-br-Dfx" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="LY1-r0-viF"/>
<constraint firstItem="nC0-dk-QaF" firstAttribute="top" secondItem="5x7-ll-2f7" secondAttribute="bottom" constant="20" id="OjY-dM-dOG"/>
<constraint firstItem="nC0-dk-QaF" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="V6L-YR-ufX"/>
<constraint firstItem="JSZ-x8-Pqi" firstAttribute="leading" secondItem="SNw-oQ-bnb" secondAttribute="leading" id="dpc-5M-0Cq"/>
<constraint firstItem="5x7-ll-2f7" firstAttribute="top" secondItem="rJa-yg-nCn" secondAttribute="bottom" constant="8" symbolic="YES" id="dzE-Ob-SVG"/>
<constraint firstAttribute="bottom" secondItem="4Vi-cN-ude" secondAttribute="bottom" constant="20" symbolic="YES" id="ny2-RO-bEI"/>
<constraint firstAttribute="bottom" secondItem="kkd-UV-SnA" secondAttribute="bottom" id="oCP-dn-6dx"/>
<constraint firstItem="JSZ-x8-Pqi" firstAttribute="top" secondItem="SNw-oQ-bnb" secondAttribute="bottom" constant="5" id="sX3-MK-14k"/>
<constraint firstItem="Uib-vA-HRc" firstAttribute="top" secondItem="DAh-br-Dfx" secondAttribute="bottom" constant="15" id="tWI-S8-17J"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="DAh-br-Dfx" secondAttribute="trailing" constant="20" symbolic="YES" id="vDR-5D-1eN"/>
</constraints>
</view>
<connections>
<outlet property="buttonCancel" destination="nC0-dk-QaF" id="n5Q-jg-UCe"/>
<outlet property="buttonCreateProxy" destination="4Vi-cN-ude" id="rdK-xc-N7F"/>
<outlet property="buttonSecure" destination="rJa-yg-nCn" id="WIs-zt-f3a"/>
<outlet property="inputDomainName" destination="SNw-oQ-bnb" id="ELH-63-cAe"/>
<outlet property="inputProxySubject" destination="QCK-Z9-w7g" id="76U-te-Jzt"/>
<outlet property="previewText" destination="JSZ-x8-Pqi" id="Mve-6W-Owd"/>
<outlet property="textFieldDomainName" destination="mlA-Zt-Hu8" id="cHL-Yu-Yvx"/>
<outlet property="textFieldError" destination="w0k-CK-0u4" id="28h-bn-igB"/>
<outlet property="textFieldProxySubject" destination="Uib-vA-HRc" id="5tV-3l-Wbw"/>
<outlet property="textFieldSecure" destination="5x7-ll-2f7" id="NlV-g8-rYP"/>
<outlet property="textFieldTitle" destination="DAh-br-Dfx" id="8SA-EW-wcq"/>
</connections>
</viewController>
<customObject id="VaP-ZM-OcY" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="210" y="1524"/>
</scene>
<!--Window Controller-->
<scene sceneID="5Gf-7O-tdA">
<objects>
<windowController storyboardIdentifier="addProxyWindow" id="ogq-ok-UVi" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="SMz-Va-x2z">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="HsN-qQ-BhO">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="ogq-ok-UVi" id="9CA-sB-ZTD"/>
</connections>
</window>
<connections>
<segue destination="dwh-CF-6iv" kind="relationship" relationship="window.shadowedContentViewController" id="My6-qb-eRg"/>
</connections>
</windowController>
<customObject id="5qP-qX-rbc" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="1530"/>
</scene>
<!--SelectionVC-->
<scene sceneID="UXm-Ci-yEB">
<objects>
<viewController storyboardIdentifier="addDomainChoice" id="gOD-Gu-zDG" customClass="SelectionVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="ysc-sm-sli">
<rect key="frame" x="0.0" y="0.0" width="540" height="177"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<visualEffectView blendingMode="behindWindow" material="toolTip" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="F37-zt-gM3">
<rect key="frame" x="0.0" y="0.0" width="540" height="177"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
<rect key="frame" x="13" y="13" width="114" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="Zhu-D8-cLK"/>
</constraints>
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="LxP-t4-H2W">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="pressedCancel:" target="gOD-Gu-zDG" id="wMp-sM-0A4"/>
</connections>
</button>
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pYe-Qu-qnK">
<rect key="frame" x="187" y="20" width="333" height="20"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="L5n-Gw-J27">
<rect key="frame" x="-7" y="-7" width="172" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="8UP-Sw-TP6">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">l</string>
</buttonCell>
<connections>
<action selector="pressedCreateLink:" target="gOD-Gu-zDG" id="77M-Ip-GMi"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="01Z-IV-hv1">
<rect key="frame" x="159" y="-7" width="181" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="bJ4-q8-1Ej">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">p</string>
</buttonCell>
<connections>
<action selector="pressedCreateProxy:" target="gOD-Gu-zDG" id="UDf-lD-KCS"/>
</connections>
</button>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
<rect key="frame" x="18" y="138" width="504" height="19"/>
<textFieldCell key="cell" selectable="YES" alignment="left" title="[i18n] What kind of domain would you like to set up?" id="agk-Nj-FLd">
<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 wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
<rect key="frame" x="18" y="60" width="504" height="70"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="tbl-AV-4qB"/>
</constraints>
<textFieldCell key="cell" selectable="YES" alignment="left" id="3i9-RG-Ift">
<font key="font" metaFont="smallSystem"/>
<string key="title">[i18n] Links are used to directly serve projects. If you have a Laravel, Symfony, WordPress, etc. folder with code, you'll want to create a link and choose the folder where your code lives.If you are in need of a proxy, you can proxy e.g. a container to a particular domain name. This can be useful in combination with Docker, for example.</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="FhN-AM-SkI" firstAttribute="leading" secondItem="F37-zt-gM3" secondAttribute="leading" constant="20" symbolic="YES" id="3dg-JM-MDr"/>
<constraint firstItem="fJK-Ke-IK3" firstAttribute="top" secondItem="F37-zt-gM3" secondAttribute="top" constant="20" symbolic="YES" id="FbX-Le-O7Q"/>
<constraint firstAttribute="trailing" secondItem="pYe-Qu-qnK" secondAttribute="trailing" constant="20" symbolic="YES" id="IJA-vN-Rbv"/>
<constraint firstItem="urj-Xq-TrJ" firstAttribute="leading" secondItem="fJK-Ke-IK3" secondAttribute="leading" id="JcY-ae-6ZH"/>
<constraint firstItem="urj-Xq-TrJ" firstAttribute="trailing" secondItem="fJK-Ke-IK3" secondAttribute="trailing" id="ZBI-pN-kOz"/>
<constraint firstItem="fJK-Ke-IK3" firstAttribute="leading" secondItem="F37-zt-gM3" secondAttribute="leading" constant="20" symbolic="YES" id="d4o-6b-Dho"/>
<constraint firstItem="urj-Xq-TrJ" firstAttribute="top" secondItem="fJK-Ke-IK3" secondAttribute="bottom" constant="8" symbolic="YES" id="hOk-eL-Eg0"/>
<constraint firstItem="FhN-AM-SkI" firstAttribute="top" secondItem="urj-Xq-TrJ" secondAttribute="bottom" constant="20" id="kCc-Vp-Gvq"/>
<constraint firstAttribute="bottom" secondItem="pYe-Qu-qnK" secondAttribute="bottom" constant="20" id="lPX-ZF-XZN"/>
<constraint firstAttribute="trailing" secondItem="fJK-Ke-IK3" secondAttribute="trailing" constant="20" symbolic="YES" id="spl-Bn-xtw"/>
<constraint firstAttribute="bottom" secondItem="FhN-AM-SkI" secondAttribute="bottom" constant="20" symbolic="YES" id="t5w-aL-tOa"/>
<constraint firstItem="pYe-Qu-qnK" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="FhN-AM-SkI" secondAttribute="trailing" constant="8" symbolic="YES" id="y7k-sl-xqe"/>
</constraints>
</visualEffectView>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cNh-Wc-ADk">
<rect key="frame" x="200" y="109" width="0.0" 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="OQ5-hX-qai">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="F37-zt-gM3" secondAttribute="trailing" id="ZRD-3j-s4x"/>
<constraint firstAttribute="bottom" secondItem="F37-zt-gM3" secondAttribute="bottom" id="et1-At-Rgj"/>
<constraint firstItem="F37-zt-gM3" firstAttribute="top" secondItem="ysc-sm-sli" secondAttribute="top" id="jp3-eE-mOy"/>
<constraint firstItem="F37-zt-gM3" firstAttribute="leading" secondItem="ysc-sm-sli" secondAttribute="leading" id="wIo-zP-KId"/>
</constraints>
</view>
<connections>
<outlet property="buttonCancel" destination="FhN-AM-SkI" id="iqV-2E-q7e"/>
<outlet property="buttonCreateLink" destination="L5n-Gw-J27" id="SHV-4l-Red"/>
<outlet property="buttonCreateProxy" destination="01Z-IV-hv1" id="J1v-7J-4fx"/>
<outlet property="textFieldDescription" destination="urj-Xq-TrJ" id="u1w-O0-kI3"/>
<outlet property="textFieldTitle" destination="fJK-Ke-IK3" id="x8p-qx-HX4"/>
</connections>
</viewController>
<customObject id="bZa-dD-d4J" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="250" y="1900"/>
</scene>
<!--Window Controller-->
<scene sceneID="HW6-nV-trE">
<objects>
<windowController storyboardIdentifier="showSelectionWindow" id="t4x-Mh-iya" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="IeW-fo-4yK">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="Oe0-yv-Jcy">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="t4x-Mh-iya" id="4oO-gI-bd2"/>
</connections>
</window>
<connections>
<segue destination="gOD-Gu-zDG" kind="relationship" relationship="window.shadowedContentViewController" id="KRt-OH-8uc"/>
</connections>
</windowController>
<customObject id="hBK-Bw-dwa" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="1909"/>
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="Checkmark" width="512" height="512"/> <image name="Checkmark" width="512" height="512"/>
<image name="IconLinked" width="25" height="25"/> <image name="IconLinked" width="25" height="25"/>
<image name="IconProxy" width="25" height="25"/>
<image name="Lock" width="30" height="30"/> <image name="Lock" width="30" height="30"/>
<image name="arrow.clockwise" catalog="system" width="14" height="16"/> <image name="arrow.clockwise" catalog="system" width="14" height="16"/>
<image name="plus" catalog="system" width="14" height="13"/> <image name="plus" catalog="system" width="14" height="13"/>

View File

@ -0,0 +1,45 @@
//
// EnvironmentCheck.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
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()
}
}

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