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

Compare commits

...

213 Commits
v1.0 ... v4.0

Author SHA1 Message Date
015f406ddf 📝 Update SECURITY.md 2021-11-28 15:38:07 +01:00
e1a97672b5 🔧 Bump build, reorganize files 2021-11-28 15:11:00 +01:00
493b5945f9 👌 Major changes to PHP version detection
* The information extracted from Homebrew's JSON command now also
  includes information about linked keg and installations.

* The mapped versions in the App class now contain information about
  the Homebrew installation as well.

* A HomebrewDiagnostics class has been added, which is currently able
  to detect conflicts between the `php` formulae of core and the
  `shivammathur/php` tap (which is currently an issue, see #54)

* Alerts are now displayed as critical if they are truly problematic.

* PhpInstallation was renamed to ActivePhpInstallation, to make room
  for a generic PhpInstallation object which contains cached info.

* Shell.pipe() now returns the contents of standardError if
  standardOutput was empty and there was some data in standardError.
  This makes it easier to debug the output of commands that output to
  standardError. (For example, failed brew commands might.)
2021-11-28 02:20:56 +01:00
52606aae8b 👌 Calling detectPhpVersions always immediately caches the info 2021-11-25 18:49:14 +01:00
2d6ca0f841 Also show full PHP version in dropdown (#53) 2021-11-25 18:41:21 +01:00
34900f929f Use gsed so we can follow symlinks to .ini files (#39, #47) 2021-11-13 21:18:01 +01:00
5dbd05fdfb Add option to auto-restart services (#32) 2021-11-13 20:50:33 +01:00
fe3cf9adb1 Add option to view long PHP version in menu bar 2021-11-13 19:11:05 +01:00
9bc8460cce 👌 Updated notification for Monterey 2021-10-19 21:42:17 +02:00
4cbd2fd6eb 📝 Updated documentation 2021-10-19 00:03:36 +02:00
6fef3fe37a 📝 Updated SECURITY.md 2021-10-19 00:02:13 +02:00
72a20d1ed9 🍱 New build screenshot of Xcode 13.1 2021-10-18 23:54:32 +02:00
73ed80434a 📝 Update README to reflect Monterey compatibility 2021-10-18 23:49:55 +02:00
a78672927b Support for upcoming releases of PHP 8.1 and 8.2 (dev) 2021-10-18 23:48:57 +02:00
4256eae442 👌 CS 2021-10-18 18:48:40 +02:00
76412b68f3 👌 Tests have OS X 10.14 as deployment target too 2021-08-31 11:08:00 +02:00
9153bb140a 👌 Code style fixes (empty line before class closes) 2021-08-31 11:03:55 +02:00
c9c15d10f9 👌 Improve handling of global hotkey load on startup 2021-08-31 10:52:49 +02:00
e8c2277ef5 🐛 Omit initial space (if uncommented, in #45) 2021-06-07 19:18:13 +02:00
23720c5dc9 🐛 Fix #45: Adjusted regex to support spaces 2021-06-07 19:13:52 +02:00
f881f07cba 👌 Cleanup 2021-05-07 15:29:47 +02:00
b072ee8dec 🚚 Improved project organisation, updated README 2021-05-03 16:52:51 +02:00
acfbc0b66f 👌 Clean up how file checks are done 2021-04-27 17:00:56 +02:00
c738a03934 📝 Update README & screenshot 2021-04-20 17:05:15 +02:00
84d62f3583 Ensure all tests pass 2021-04-20 17:05:05 +02:00
f9faa03b92 #41: Notify about broken PHP-FPM configuration 2021-04-19 20:30:37 +02:00
55f6c3c6cd Add "Locate global composer.json file" 2021-04-19 13:14:34 +02:00
a0c6753761 🐛 Use php_ini_scanned_files for .ini scan
Using `php-config --ini-dir` seems to fail on PHP 7.2 and below, likely
because said option was not available in these earlier versions. Because
all we need are the additional .ini files, calling php_ini_scanned_files
is a better solution since it is supported from PHP 5 and up.

This commit fixes the crash issue that was caused by running the failing
`php-config` command.

More information: https://www.php.net/manual/en/function.php-ini-scanned-files.php
2021-04-19 10:26:14 +02:00
327125608a 👌 Polish preferences screen 2021-04-15 23:16:42 +02:00
6c0045302b 📝 Updated README 2021-04-14 20:13:19 +02:00
9c85bebe72 Add option to turn all services off (#35) 2021-04-14 20:08:50 +02:00
fb56cd551e 👌 Improved parallelization 2021-04-14 19:40:07 +02:00
e83d507e79 Parallelize unlinking PHP versions 2021-04-14 18:46:33 +02:00
0c0e7fc87d Extension loading improvements (#31) and more
* The README has been updated with additional information
* The acknowledgements section has been added to the README
* The php@X.X/opt/bin/php-config binary is now used (#39)
* Extensions are now loaded from all possible .ini files
* PHP Monitor's preferences window can now be triggered via hotkey
* The first nine extensions can be triggered via hotkey
2021-04-07 16:58:05 +02:00
faf49fbe1d 👌 Prevent hotkey multi-fire (#33) 2021-04-02 18:32:07 +02:00
2925b0ff79 🏗 WIP: Global shortcut key (#33) 2021-04-02 17:50:46 +02:00
acb18474c8 👌 Fine-tuning the README 2021-04-01 21:17:35 +02:00
ed61490398 📝 Update README 2021-04-01 21:14:03 +02:00
abb76273c9 📝 New screenshot 2021-04-01 20:56:28 +02:00
2f15af4ff8 Improved test 2021-04-01 20:44:53 +02:00
e29e8416d5 Fix tests, add test for version detection 2021-04-01 20:38:24 +02:00
5d423210dd 👌 Improved PHP version filter 2021-04-01 20:19:55 +02:00
340c36fdf8 🔧 Bump version & build number 2021-04-01 00:18:54 +02:00
3085158b80 🐛 Validate detected PHP version (#30) 2021-04-01 00:18:33 +02:00
c2585f9bf4 👌 Cleanup, add separator before refresh item 2021-03-31 23:57:17 +02:00
d478137742 🔧 Bump version & build number 2021-03-31 19:50:38 +02:00
827bd182b1 🐛 Avoid duplicates (#30) 2021-03-31 19:38:30 +02:00
f7500637fe 🐛 Be more lenient about Valet checks (#27) 2021-03-31 12:21:06 +02:00
7c884610b1 👌 Use snake case for all key names 2021-03-30 16:38:09 +02:00
d3d219751e 🍱 Updated layout constraints 2021-03-30 16:35:13 +02:00
16d2e7d06f Add preferences dialog to enable static icon (#25) 2021-03-30 16:31:28 +02:00
47b86ff9fa 🚩 New prerelease build 2021-03-29 22:29:44 +02:00
6e574b9154 👌 Make sure 'refresh' reopens the menu afterwards (#24) 2021-03-29 22:24:15 +02:00
485001403d Add refresh button (#24)
By pressing Command-R while the menu is open, the information about the
current PHP installation will be refreshed. 

You'll need to open the menu again, but now it will contain up-to-date 
information. You could also just press the menu item, of course.
2021-03-29 21:50:40 +02:00
694c5e7f7d Add credits 2021-03-29 20:49:55 +02:00
d9ff26385a 🍱 Optimized assets 2021-03-29 20:04:08 +02:00
0cfb7c65bb 👌 Avoid self 2021-03-19 16:15:05 +01:00
3cbc2a0367 👌 Avoid self. (and capture using [self] in) 2021-03-19 16:01:03 +01:00
7733c90206 👌 Whitespace, remove print() 2021-03-19 15:50:43 +01:00
0ad6e5cb1c 👌 Multiple trailing closures 2021-03-19 15:44:26 +01:00
8a47cfd5f1 🐛 Remove which valet check 2021-03-17 14:50:26 +01:00
db4c0399fd 📝 Update README 2021-03-12 12:26:55 +01:00
b0f29b72cd 💰 Add funding option to GitHub 2021-03-12 12:24:57 +01:00
98a1622ccc 🔀 Merge branch 'develop'
* develop:
  💰 Add funding option to GitHub
  📝 Update SECURITY
2021-03-12 12:10:12 +01:00
a6ac737590 💰 Add funding option to GitHub 2021-03-12 12:09:49 +01:00
bba719012f 📝 Update SECURITY 2021-03-12 10:01:46 +01:00
cdc0cea01c 📝 Update SECURITY 2021-03-12 10:01:32 +01:00
d3a4f2091c 🚀 Version 3.1
* develop:
  📝 Tweak error message string
  🔧 Bump build version
  🐛 Handle `valet` install in `/opt/homebrew/bin`
  👌 Cleanup
  📝 Updated README
  🔧 Determine default shell based on macOS version
   Added tests
  🚚 Moved files around
  📝 Update copyright information
2021-03-12 09:54:40 +01:00
c7dc17dded 📝 Tweak error message string 2021-03-12 09:54:12 +01:00
d70f8627bb 🔧 Bump build version 2021-03-12 09:50:29 +01:00
7661f4643d 🐛 Handle valet install in /opt/homebrew/bin 2021-03-12 09:46:38 +01:00
11d74277e5 👌 Cleanup 2021-02-19 18:59:39 +01:00
e4096ab662 📝 Updated README 2021-02-19 17:35:48 +01:00
d367f1f353 🔧 Determine default shell based on macOS version
This fixes #21 (fixes the issue on Mojave) and closes #22 since the min
SDK does not need to be bumped.
2021-02-19 17:28:57 +01:00
79fbdef41b Added tests 2021-02-14 22:54:45 +01:00
37ede7e2a3 🚚 Moved files around 2021-02-14 22:54:38 +01:00
80eabe3bf3 📝 Update copyright information 2021-02-12 10:00:25 +01:00
aa07a42d8f 🚀 Version 3.0
* develop: (29 commits)
  🔧 Bump version for release
  👌 All methods are internal by default
  📝 Move all FAQ to README
  📝 Reorganized document
  📝 Updated quick troubleshooting
  📝 New screenshot
  🔧 Bump build version
  🎨 Use Self to refer to current type
   Improved byte value parsing
  📝 Updated SECURITY
  📝 Updated README, new screenshot
  🔧 Bump build version
  🚚 Move files around
  🔥 Crash if the view is unavailable
  🎨 Fix threading issue
   Completed new design for v3.0
   Tweak custom views
   Custom views
  🎨 Updated localization, cleanup
  🔥 Cleanup
  ...
2021-02-12 09:47:28 +01:00
b6b3a9a74c 🔧 Bump version for release 2021-02-12 09:38:09 +01:00
ec4371e6da 👌 All methods are internal by default 2021-02-10 16:35:23 +01:00
c7a92d502f 📝 Move all FAQ to README 2021-02-07 11:52:53 +01:00
172a38e459 📝 Reorganized document 2021-02-05 10:59:55 +01:00
d18e8298b6 📝 Updated quick troubleshooting 2021-02-05 10:54:09 +01:00
df325474c6 📝 New screenshot 2021-02-05 10:27:34 +01:00
60278d7817 🔧 Bump build version 2021-02-05 10:11:15 +01:00
9e8117d31f 🎨 Use Self to refer to current type 2021-02-05 10:07:48 +01:00
3d042b24ed Improved byte value parsing 2021-02-05 09:56:02 +01:00
fcf3483dd0 📝 Updated SECURITY 2021-02-04 22:35:37 +01:00
3db333dd77 📝 Updated README, new screenshot 2021-02-04 22:32:46 +01:00
b0f0fd8c6b 🔧 Bump build version 2021-02-04 22:09:36 +01:00
69755cd8cc 🚚 Move files around 2021-02-04 22:07:09 +01:00
6b2fa0c605 🔥 Crash if the view is unavailable 2021-02-04 22:04:29 +01:00
dd41b5a1e0 🎨 Fix threading issue 2021-02-04 22:00:58 +01:00
a3fe09ed8a Completed new design for v3.0 2021-02-04 21:41:36 +01:00
ad81c4a3db Tweak custom views 2021-02-04 21:27:52 +01:00
572b0f69dd Custom views 2021-02-04 21:23:41 +01:00
c1b80552a0 🎨 Updated localization, cleanup 2021-02-03 22:09:53 +01:00
0493f61dc8 🔥 Cleanup 2021-02-03 19:39:49 +01:00
9824ed2b83 Load certain info from ini files 2021-02-03 18:01:25 +01:00
3e0a16cc73 📝 Document extension detection & toggling 2021-01-31 23:06:57 +01:00
5a50e7fdab Allow toggling of PHP extensions in php.ini 2021-01-31 22:50:39 +01:00
1dc85a4d48 🔧 Bump version and build 2021-01-29 20:48:49 +01:00
85ef3ff067 ♻️ Paths now uses static variables instead of methods 2021-01-29 20:39:55 +01:00
2958378411 ♻️ Add static methods to Shell 2021-01-29 20:36:33 +01:00
3a2cd3bff6 ♻️ Rework file checks & replacement into separate methods 2021-01-29 20:32:12 +01:00
fbe791902c 🎨 Update copyright, minor bracket cleanup 2021-01-29 20:08:27 +01:00
77f4de8ca9 🎨 Cleanup code 2021-01-29 19:57:13 +01:00
0f6e47a594 📝 Added build instructions 2021-01-09 13:46:45 +01:00
7408da2a91 ♻️ Assets now available as SVG 2021-01-09 13:38:32 +01:00
12a66f3467 📝 Updated Homebrew instructions 2021-01-06 18:09:16 +01:00
d3615138dd 🚀 Version 2.6
* develop:
  📝 Updated quick troubleshooting
  📝 Updated documentation
  📝 Updated SECURITY
  📝 Updated README
   New build number, updated shortcut keys
  🌐 Add info about `valet install` when running force reload
   Fix my PHP should also restart dnsmasq
   Add menu item to restart dnsmasq
  🔧 Bump build number
  ♻️ Cleanup, updated README (#20)
  ♻️ Refactoring, version bump for M1 support (#20)
   Add support for Homebrew in /opt/homebrew (#20)
  👌 Clean up switchToPhpVersion() and fixMyPhp()
  👌 Avoid using sender.tag
  👌 Improved pipe() method
  🌐 Localized startup environment checks
  🐛 Perform the phpinfo.html on another thread and show busy
  📝 Updated instructions
2021-01-06 17:51:29 +01:00
dd27b91527 📝 Updated quick troubleshooting 2021-01-06 17:49:26 +01:00
bc093cc945 📝 Updated documentation 2021-01-06 17:42:09 +01:00
fd46ce6b35 📝 Updated SECURITY 2021-01-06 17:29:24 +01:00
e35acadeaf 📝 Updated README 2021-01-06 17:18:59 +01:00
7b8aab85d6 New build number, updated shortcut keys
- Cmd-F now [F]orce reloads
- Cmd-S now restarts all [S]ervices
- Cmd-P now restarts the [P]hp service
- Cmd-D now restarts the [D]nsmasq service
2021-01-01 23:55:37 +01:00
ec59715b3d 🌐 Add info about valet install when running force reload 2021-01-01 23:49:51 +01:00
6f21913ae2 Fix my PHP should also restart dnsmasq 2021-01-01 23:48:12 +01:00
b53fbe471b Add menu item to restart dnsmasq 2021-01-01 23:19:31 +01:00
209f3e889d 🔧 Bump build number 2021-01-01 23:06:24 +01:00
5825e8d0b0 ♻️ Cleanup, updated README (#20) 2021-01-01 23:05:16 +01:00
4ea11c5f59 ♻️ Refactoring, version bump for M1 support (#20) 2021-01-01 22:58:27 +01:00
94f086881a Add support for Homebrew in /opt/homebrew (#20) 2021-01-01 22:54:03 +01:00
e73474e30c 👌 Clean up switchToPhpVersion() and fixMyPhp() 2020-12-19 18:38:00 +01:00
c7a0e25336 👌 Avoid using sender.tag 2020-12-13 19:32:43 +01:00
e353fb7524 👌 Improved pipe() method 2020-12-13 19:16:51 +01:00
458868d051 🌐 Localized startup environment checks 2020-12-08 23:18:52 +01:00
932fafc728 🐛 Perform the phpinfo.html on another thread and show busy 2020-11-27 17:01:30 +01:00
b70c4f690a 📝 Updated instructions 2020-11-27 16:40:37 +01:00
4147cc7b4b 🚀 Version 2.5 2020-11-27 16:32:42 +01:00
72b309a716 📝 Be more explicit in main README 2020-11-27 16:31:54 +01:00
12c2716715 📝 Add information about running valet install 2020-11-27 16:29:02 +01:00
4d4019204b 📝 Updated README with new screenshot 2020-11-27 16:24:06 +01:00
157033a3b3 🚀 Version 2.5 (RC) 2020-11-27 11:32:26 +01:00
717cddacdd 🐛 Fix issue with php install now showing up 2020-11-27 11:29:40 +01:00
f13ed5dd90 Add option to open output of phpinfo() in browser 2020-11-27 01:33:06 +01:00
a8f823cd04 📝 Update README.md, ADDITIONAL.md 2020-11-26 18:59:49 +01:00
51fd22f595 Cleanup storyboard, add Brew package name to switcher 2020-11-26 18:53:15 +01:00
bf6ebff3bf Detect what version of PHP the php package is linked to 2020-11-26 17:58:34 +01:00
1887b19329 🚧 WIP: Get JSON about current PHP version 2020-11-26 17:02:24 +01:00
a53972404c 🚀 Version 2.4
* develop:
  📝 Updated SECURITY to reflect new support status
  🔧 Updated build settings
  🔧 Add PHP 8.0 to list of detected PHP versions
  📝 Universal application
2020-11-14 15:23:19 +01:00
64a605235a 📝 Updated SECURITY to reflect new support status 2020-11-14 02:33:46 +01:00
a194ecdebe 🔧 Updated build settings 2020-11-14 02:20:26 +01:00
03158a568c 🔧 Add PHP 8.0 to list of detected PHP versions 2020-11-14 02:19:42 +01:00
bdc6be7384 📝 Universal application 2020-11-14 02:19:28 +01:00
4908dba57e 📝 Update support information 2020-08-16 16:45:42 +02:00
0f0aa176b6 📝 Update README 2020-08-16 16:43:08 +02:00
485729f9a5 🚀 Version 2.3
Merge branch 'develop' into main

* develop:
  📝 Updated README, SECURITY docs
  🍱 Updated screenshot to Big Sur
  ♻️ Cleanup
  🚧 WIP: Notify the user about PHP switch completion (#15)
2020-08-16 16:27:38 +02:00
5eb36a9bdf 📝 Updated README, SECURITY docs 2020-07-20 17:28:59 +02:00
86113d2067 🍱 Updated screenshot to Big Sur 2020-07-19 13:17:55 +02:00
70c04d4dc7 ♻️ Cleanup
- Bump version number to 2.3
- Disable Metal validation in project configuration
- Added comments to various files to clarify code
- Moved various strings to Localizable.strings
- Removed references to old view controller for command log
- No longer log previously ran commands
2020-07-16 23:16:39 +02:00
5d69b423c1 🚧 WIP: Notify the user about PHP switch completion (#15) 2020-07-16 22:25:51 +02:00
1617b57b1e 🚀 Version 2.2
Merge branch 'develop'

* develop:
  📝 Update README
  📝 Amend modal copy
  🐛 Improved startup procedure
  🚧 Add Internet Access Policy
   Add shared scheme
2020-07-16 19:37:21 +02:00
33825e7b66 📝 Update README 2020-07-16 19:34:38 +02:00
464b7106b2 📝 Amend modal copy 2020-07-16 19:29:01 +02:00
0cf85f9958 🐛 Improved startup procedure
- First issue encountered will notify the user
- User has the option to Retry or Terminate the app
- Retry will go through launch checks again
- Some issues are marked as non-breaking, meaning the user will be
  notified, but the app will just start as usual
2020-07-16 19:26:29 +02:00
00cf2bc360 🚧 Add Internet Access Policy 2020-07-11 11:18:31 +02:00
dbb5329908 Add shared scheme 2020-07-11 10:54:05 +02:00
9fd8e7042a 🚀 Version 2.1
Merge branch 'develop'

* develop:
  📝 Add installation instructions
  📝 Added docs for release procedure
  📝 Updated README
  🔧 Hardened runtime (required for notarization)
  🍱 Updated icon for macOS 11 style, updated alert
2020-07-11 10:41:11 +02:00
cb62a20f2a 📝 Add installation instructions 2020-07-11 10:40:02 +02:00
bb1742a390 📝 Added docs for release procedure 2020-07-11 10:05:47 +02:00
6db43beddf 📝 Updated README 2020-07-11 01:07:51 +02:00
7e2c7cdd59 🔧 Hardened runtime (required for notarization) 2020-07-11 01:00:31 +02:00
e76dd0daeb 🍱 Updated icon for macOS 11 style, updated alert 2020-07-11 00:49:32 +02:00
0c33e8f8cc 🎉 Version 2.0 2020-05-17 11:36:31 +02:00
a6a196518a 🔀 Merge branch 'develop'
* develop:
  📝 Changes to the README, bump version number
  ♻️ Menu refactor
  ♻️ Use Localizable.strings
  ♻️ Change default shell for commands, cleanup files
2020-05-17 00:22:40 +02:00
a287ebf6e4 📝 Changes to the README, bump version number 2020-05-17 00:17:45 +02:00
2200b395f1 Merge branch 'cleanup' into develop
* cleanup:
  ♻️ Menu refactor
  ♻️ Use Localizable.strings
  ♻️ Change default shell for commands, cleanup files
2020-05-16 23:56:33 +02:00
bc8a572072 ♻️ Menu refactor 2020-05-16 23:55:04 +02:00
1c455ea05a ♻️ Use Localizable.strings 2020-05-16 23:32:34 +02:00
f2d01748be ♻️ Change default shell for commands, cleanup files
The shell that is now invoked is /bin/sh as opposed to /bin/bash, which
has proven to be faster in various cases.
2020-05-16 23:06:52 +02:00
d3e59e560f New beta for version 1.9 2020-05-13 17:34:51 +02:00
55e849c21b Various fixes and improvements
- Prevent crashes with incorrectly loading modules
- Prevent crashes when dyld libraries are missing
- Indicate when the PHP installation is broken
- New warning added at boot (multiple services)
- Added "Force load latest PHP version" option

It is known that PHP 5.6, 7.0 and 7.1 are causing issues with the
newer versions of certain libraries ("dyld library" warning).

It is recommended to only use PHP 7.2, PHP 7.3 and PHP 7.4 for a minimal
amount of issues. Otherwise, there are certain fixes that are possible
but they are not supported via PHP Monitor since they require manual
formula changes.

For more information, see:
https://github.com/eXolnet/homebrew-deprecated/issues/23#issuecomment-619976586
2020-05-13 17:26:00 +02:00
50f6afb3c5 🔀 Merge branch 'develop'
* develop:
   Add option to restart nginx, open valet config
   Allow restarting of PHP-FPM
  📝 Update copyright message
2020-02-20 17:24:48 +01:00
67e80aac8d Add option to restart nginx, open valet config 2020-02-20 17:24:21 +01:00
e683b6bc9a Allow restarting of PHP-FPM 2020-02-20 16:12:38 +01:00
e854ebe114 📝 Update copyright message 2020-02-20 11:58:16 +01:00
b792f55e5f 🔀 Merge branch 'develop'
* develop:
  ♻️ Change how the active PHP version is switched
2020-02-20 11:49:09 +01:00
cbf5526881 ♻️ Change how the active PHP version is switched 2020-02-20 11:47:49 +01:00
f62297ee3d 🔀 Merge branch 'develop'
* develop:
  🔥 Remove broken log
  🔧 Version bump
  🚧 WIP: Adds support to PHP 7.4
  ️ Use Command.execute to retrieve PHP version
  🚧 WIP: Add toggle to enable or disable Xdebug
  📝 Update readme
2019-12-20 11:59:04 +01:00
2f017f6732 📢 Version 1.6 for personal use 2019-12-20 11:55:25 +01:00
1c62a100af 🔥 Remove broken log
(Will just log this in a file - the right way)
2019-12-20 11:54:15 +01:00
e176d6fa2d 🔧 Version bump 2019-12-20 11:48:20 +01:00
ea60626c47 🚧 WIP: Adds support to PHP 7.4
(PHP 7.4 isn't out for Homebrew just yet.)
2019-11-28 12:46:14 +01:00
69c3386088 ️ Use Command.execute to retrieve PHP version 2019-11-20 08:43:09 +01:00
3e898d435b 🚧 WIP: Add toggle to enable or disable Xdebug 2019-10-17 19:04:49 +02:00
a4dcd0cd3d 📝 Update readme 2019-10-15 10:39:42 +02:00
bb382c0f74 📝 Update readme 2019-10-14 11:14:28 +02:00
c87c532d1c 📝 Update readme for Catalina compatibility 2019-10-14 10:54:03 +02:00
b26099369f Fix text position 2019-07-15 17:31:22 +02:00
90a8e7c336 Updated margins of shell output window 2019-07-15 17:25:57 +02:00
398dad9d5e Minor cleanup, shell output window title 2019-07-15 17:23:08 +02:00
48d0878dfd 📝 Adds LICENSE 2019-07-15 17:19:51 +02:00
4ba3415bf4 ♻️ phpmon -> PHP Monitor 2019-07-15 17:14:09 +02:00
0fc7f2c905 📝 Minor tweaks, updated README 2019-07-15 08:14:51 +02:00
eebfa823f1 No more fixed width menu bar icon 2019-07-15 07:55:42 +02:00
edc23f97de 🐛 Fixes being unable to quit after setting target 2019-07-11 09:49:04 +02:00
7b79cb7fe8 ♻️ Move logic away from AppDelegate 2019-07-11 09:11:41 +02:00
88d81f343e ♻️ Move window logic to VC 2019-07-11 08:30:46 +02:00
8486997f01 🔥 Additional cleanup 2019-07-11 08:27:44 +02:00
af55e7fabc 🔥 Performance improvements, memory deallocation 2019-07-09 19:15:42 +02:00
2025c951e8 📝 Updated README 2019-07-09 18:39:52 +02:00
a71c6afc0d ♻️ Rework Shell as singleton, logging 2019-07-09 18:33:48 +02:00
1bfcdd546b 🐛 Fixes #3: Bad Gateway caused by valet use
Note: The terminal output work-in-progress is currently disabled in this
particular commit.
2019-07-08 10:43:56 +02:00
78702ae325 🚧 Adds window to view terminal output 2019-07-08 08:40:29 +02:00
22c173c2c6 📝 Adds screenshot to README 2019-07-03 19:22:57 +02:00
593ac60cae 📝 Update README 2019-07-03 19:16:18 +02:00
cea95f4b64 Code cleanup 2019-07-03 19:13:02 +02:00
9427b206b4 Project structure changes, version change 2019-07-03 19:07:04 +02:00
ff6ca2b79d 🚧 WIP: Boot check changes 2019-07-01 13:58:56 +02:00
8de76dc95a 🚨 Cleanup 2019-07-01 13:46:02 +02:00
5b319d2691 🔧 Tweaked order of versions
The latest version is now visible at the top.
2019-07-01 13:31:14 +02:00
76390a687f 📝 Updated README 2019-07-01 13:30:13 +02:00
85 changed files with 7034 additions and 813 deletions

1
.github/FUNDING.yml vendored Normal file
View File

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

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
phpmon.xcodeproj/project.xcworkspace
phpmon.xcodeproj/xcuserdata
PHP Monitor.xcodeproj/project.xcworkspace
PHP Monitor.xcodeproj/xcuserdata
.DS_Store

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Nico Verbruggen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,796 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */; };
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; };
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
C42295DD2358D02000E263B2 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; };
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; };
C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; };
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */; };
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; };
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C8F25CC7FD000CC7490 /* StatsView.xib */; };
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C9925CC888B00CC7490 /* HeaderView.xib */; };
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
C4998F0626175E7200B2526E /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = C4998F0526175E7200B2526E /* HotKey */; };
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */; };
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4F780B425D80B51000DBC97 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
C4F780B725D80B5D000DBC97 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; };
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
C4F7807E25D7F84B000DBC97 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = C41C1B2B22B0097F00E7CF16 /* Project object */;
proxyType = 1;
remoteGlobalIDString = C41C1B3222B0097F00E7CF16;
remoteInfo = "PHP Monitor";
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = "<group>"; };
5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = "<group>"; };
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = "<group>"; };
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; };
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C41C1B3D22B0098000E7CF16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
C41C1B3F22B0098000E7CF16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = "<group>"; };
C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = "<group>"; };
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = "<group>"; };
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = "<group>"; };
C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = "<group>"; };
C43A8A1925D9CD1000591B77 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = "<group>"; };
C43A8A1F25D9D1D700591B77 /* brew.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = brew.json; sourceTree = "<group>"; };
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewJsonParserTest.swift; sourceTree = "<group>"; };
C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = "<group>"; };
C474B00524C0E98C00066A22 /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = "<group>"; };
C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
C4811D2322D70A4700B5F6B3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
C48D0C8F25CC7FD000CC7490 /* StatsView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatsView.xib; sourceTree = "<group>"; };
C48D0C9225CC804200CC7490 /* XibLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XibLoadable.swift; sourceTree = "<group>"; };
C48D0C9525CC80B100CC7490 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
C48D0C9925CC888B00CC7490 /* HeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HeaderView.xib; sourceTree = "<group>"; };
C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = "<group>"; };
C49EAB45259FC305007F6C3B /* Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = "<group>"; };
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = "<group>"; };
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
C4F7807425D7F7E5000DBC97 /* RELEASE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = RELEASE.md; sourceTree = "<group>"; };
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
C4F780A725D80AE8000DBC97 /* php.ini */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = php.ini; sourceTree = "<group>"; };
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionParserTest.swift; sourceTree = "<group>"; };
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
C4F8C0A522D4FA41002EFE61 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionDetectionTest.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
C41C1B3022B0097F00E7CF16 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C4998F0626175E7200B2526E /* HotKey in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C4F7807625D7F84B000DBC97 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5420395726135DB800FB00FA /* Preferences */ = {
isa = PBXGroup;
children = (
C4998F092617633900B2526E /* PrefsWC.swift */,
5420395826135DC100FB00FA /* PrefsVC.swift */,
5420395E2613607600FB00FA /* Preferences.swift */,
C41CD0272628D8E20065BBED /* Keybinds */,
);
path = Preferences;
sourceTree = "<group>";
};
54B20EDF263AA22C00D3250E /* PHP */ = {
isa = PBXGroup;
children = (
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */,
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */,
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
);
path = PHP;
sourceTree = "<group>";
};
C405A4CD24B9B9070062FAFA /* IAP */ = {
isa = PBXGroup;
children = (
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */,
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */,
);
path = IAP;
sourceTree = "<group>";
};
C41C1B2A22B0097F00E7CF16 = {
isa = PBXGroup;
children = (
C4F8C0A522D4FA41002EFE61 /* README.md */,
C4E713562570150F00007428 /* SECURITY.md */,
C4F7807425D7F7E5000DBC97 /* RELEASE.md */,
C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */,
C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
C41C1B3422B0097F00E7CF16 /* Products */,
);
sourceTree = "<group>";
};
C41C1B3422B0097F00E7CF16 /* Products */ = {
isa = PBXGroup;
children = (
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */,
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup;
children = (
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
C4EE188322D3386B00E126E5 /* Constants.swift */,
C41E181722CB61EB0072CF09 /* Domain */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
C4232EE42612526500158FC6 /* Credits.html */,
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
C473319E2470923A009A0597 /* Localizable.strings */,
C405A4CD24B9B9070062FAFA /* IAP */,
);
path = phpmon;
sourceTree = "<group>";
};
C41CD0272628D8E20065BBED /* Keybinds */ = {
isa = PBXGroup;
children = (
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */,
);
path = Keybinds;
sourceTree = "<group>";
};
C41E181722CB61EB0072CF09 /* Domain */ = {
isa = PBXGroup;
children = (
C4B13B1D25C4915000548C3A /* Core */,
54B20EDF263AA22C00D3250E /* PHP */,
C4F7808A25D7F918000DBC97 /* Terminal */,
C47331A0247093AC009A0597 /* Menu */,
5420395726135DB800FB00FA /* Preferences */,
C4811D2822D70D9C00B5F6B3 /* Helpers */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
);
path = Domain;
sourceTree = "<group>";
};
C47331A0247093AC009A0597 /* Menu */ = {
isa = PBXGroup;
children = (
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */,
C47331A1247093B7009A0597 /* StatusMenu.swift */,
C48D0C9525CC80B100CC7490 /* HeaderView.swift */,
C48D0C9925CC888B00CC7490 /* HeaderView.xib */,
C48D0CA225CC992000CC7490 /* StatsView.swift */,
C48D0C8F25CC7FD000CC7490 /* StatsView.xib */,
);
path = Menu;
sourceTree = "<group>";
};
C4811D2822D70D9C00B5F6B3 /* Helpers */ = {
isa = PBXGroup;
children = (
C476FF9722B0DD830098105B /* Alert.swift */,
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */,
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
C4B13B1D25C4915000548C3A /* Core */ = {
isa = PBXGroup;
children = (
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
C4811D2322D70A4700B5F6B3 /* App.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
);
path = Core;
sourceTree = "<group>";
};
C4F7807A25D7F84B000DBC97 /* phpmon-tests */ = {
isa = PBXGroup;
children = (
C43A8A1F25D9D1D700591B77 /* brew.json */,
C4F780A725D80AE8000DBC97 /* php.ini */,
C4F7807D25D7F84B000DBC97 /* Info.plist */,
C4F7809B25D80344000DBC97 /* CommandTest.swift */,
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
C43A8A1925D9CD1000591B77 /* Utility.swift */,
);
path = "phpmon-tests";
sourceTree = "<group>";
};
C4F7808A25D7F918000DBC97 /* Terminal */ = {
isa = PBXGroup;
children = (
C49EAB45259FC305007F6C3B /* Paths.swift */,
C42295DC2358D02000E263B2 /* Command.swift */,
C41C1B4622B009A400E7CF16 /* Shell.swift */,
);
path = Terminal;
sourceTree = "<group>";
};
C4F8C0A222D4F100002EFE61 /* Extensions */ = {
isa = PBXGroup;
children = (
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */,
C46FA23E246C358E00944F05 /* StringExtension.swift */,
C48D0C9225CC804200CC7490 /* XibLoadable.swift */,
C42759662627662800093CAE /* NSMenuExtension.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
C41C1B3222B0097F00E7CF16 /* PHP Monitor */ = {
isa = PBXNativeTarget;
buildConfigurationList = C41C1B4322B0098000E7CF16 /* Build configuration list for PBXNativeTarget "PHP Monitor" */;
buildPhases = (
C41C1B2F22B0097F00E7CF16 /* Sources */,
C41C1B3022B0097F00E7CF16 /* Frameworks */,
C41C1B3122B0097F00E7CF16 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "PHP Monitor";
packageProductDependencies = (
C4998F0526175E7200B2526E /* HotKey */,
);
productName = phpmon;
productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */;
productType = "com.apple.product-type.application";
};
C4F7807825D7F84B000DBC97 /* phpmon-tests */ = {
isa = PBXNativeTarget;
buildConfigurationList = C4F7808025D7F84B000DBC97 /* Build configuration list for PBXNativeTarget "phpmon-tests" */;
buildPhases = (
C4F7807525D7F84B000DBC97 /* Sources */,
C4F7807625D7F84B000DBC97 /* Frameworks */,
C4F7807725D7F84B000DBC97 /* Resources */,
);
buildRules = (
);
dependencies = (
C4F7807F25D7F84B000DBC97 /* PBXTargetDependency */,
);
name = "phpmon-tests";
productName = "phpmon-tests";
productReference = C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
C41C1B2B22B0097F00E7CF16 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1220;
ORGANIZATIONNAME = "Nico Verbruggen";
TargetAttributes = {
C41C1B3222B0097F00E7CF16 = {
CreatedOnToolsVersion = 10.2.1;
};
C4F7807825D7F84B000DBC97 = {
CreatedOnToolsVersion = 12.4;
TestTargetID = C41C1B3222B0097F00E7CF16;
};
};
};
buildConfigurationList = C41C1B2E22B0097F00E7CF16 /* Build configuration list for PBXProject "PHP Monitor" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = C41C1B2A22B0097F00E7CF16;
packageReferences = (
C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */,
);
productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
C41C1B3222B0097F00E7CF16 /* PHP Monitor */,
C4F7807825D7F84B000DBC97 /* phpmon-tests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
C41C1B3122B0097F00E7CF16 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */,
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
C4232EE52612526500158FC6 /* Credits.html in Resources */,
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C4F7807725D7F84B000DBC97 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
C43A8A2025D9D1D700591B77 /* brew.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C41C1B2F22B0097F00E7CF16 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */,
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */,
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */,
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */,
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
C42295DD2358D02000E263B2 /* Command.swift in Sources */,
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
5420395F2613607600FB00FA /* Preferences.swift in Sources */,
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */,
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C4F7807525D7F84B000DBC97 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */,
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */,
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */,
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */,
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */,
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */,
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */,
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */,
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */,
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */,
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */,
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */,
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */,
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */,
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */,
C4F780B425D80B51000DBC97 /* Actions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
C4F7807F25D7F84B000DBC97 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C41C1B3222B0097F00E7CF16 /* PHP Monitor */;
targetProxy = C4F7807E25D7F84B000DBC97 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
C41C1B3C22B0098000E7CF16 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
C41C1B3D22B0098000E7CF16 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C41C1B4122B0098000E7CF16 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "Mac Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
C41C1B4222B0098000E7CF16 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "Mac Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
C41C1B4422B0098000E7CF16 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C41C1B4522B0098000E7CF16 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
C4F7808125D7F84B000DBC97 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
INFOPLIST_FILE = "phpmon-tests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PHP Monitor.app/Contents/MacOS/PHP Monitor";
};
name = Debug;
};
C4F7808225D7F84B000DBC97 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
INFOPLIST_FILE = "phpmon-tests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PHP Monitor.app/Contents/MacOS/PHP Monitor";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
C41C1B2E22B0097F00E7CF16 /* Build configuration list for PBXProject "PHP Monitor" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C41C1B4122B0098000E7CF16 /* Debug */,
C41C1B4222B0098000E7CF16 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C41C1B4322B0098000E7CF16 /* Build configuration list for PBXNativeTarget "PHP Monitor" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C41C1B4422B0098000E7CF16 /* Debug */,
C41C1B4522B0098000E7CF16 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C4F7808025D7F84B000DBC97 /* Build configuration list for PBXNativeTarget "phpmon-tests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C4F7808125D7F84B000DBC97 /* Debug */,
C4F7808225D7F84B000DBC97 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/soffes/HotKey";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.1.3;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
C4998F0526175E7200B2526E /* HotKey */ = {
isa = XCSwiftPackageProductDependency;
package = C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */;
productName = HotKey;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = C41C1B2B22B0097F00E7CF16 /* Project object */;
}

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1220"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
BuildableName = "phpmon-tests.xctest"
BlueprintName = "phpmon-tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

315
README.md
View File

@ -1,3 +1,314 @@
# phpmon
# PHP Monitor
phpmon is a macOS utility that runs on your Mac and displays the active PHP version in your status bar. Handy if you're running multiple versions of PHP with Homebrew and wish to see which version is currently linked.
> If this software has been useful to you, all I ask is that you **please star the repository**, so I know that the software is being used.
> You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy.<br>**Thank you!** ⭐️
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
**PHP Monitor** (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so you need to have it set up before you can use this.
<img src="./docs/screenshot34.png" width="412px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: A menu showing all of the 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)!
<img src="./docs/notification.png" 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).
## 🖥 System requirements
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
* macOS 10.14 Mojave or higher (works on macOS 11 Big Sur and macOS 12 Monterey)
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* The brew formula `php` has to be installed (which version is detected)
* Laravel Valet 2.13 or higher
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released._
## 🚀 How to install
You can install via Homebrew (recommended), or may download the latest release on GitHub.
To install via Homebrew, run:
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
To upgrade your existing installation, run:
brew upgrade phpmon
_The app is signed and notarized, meaning all you have to do is approve its first launch._
## 👨‍💻 Why build this?
I wanted to be able to **see at a glance** which version of PHP was linked, and handle dealing with Laravel Valet in a simple app without having to deal with the terminal every time.
Initially, I had an Alfred workflow for this — but it has now been replaced with this utility, which also does a good job at displaying additional information at a glance, like the current PHP version, memory limits, and more.
## 🤬 The app won't start?!
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
**Follow instructions as specified in the alert in order to resolve any issues.**
## 🙋‍♂️ FAQ & Troubleshooting
> If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. To upgrade Valet, run `composer global update`. Don't forget to run `valet install` after upgrading.
If you're still having issues, here's a few common questions & answers, as well as issues and solutions:
<details>
<summary><strong>Which versions of PHP are supported?</strong></summary>
<ul>
<li>PHP 5.6</li>
<li>PHP 7.0</li>
<li>PHP 7.1</li>
<li>PHP 7.2</li>
<li>PHP 7.3</li>
<li>PHP 7.4</li>
<li>PHP 8.0</li>
<li>PHP 8.1</li>
<li>PHP 8.2 (experimental)</li>
</ul>
For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Constants.swift#L16) file to see which versions are supported.
</details>
<details>
<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.
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>
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.
Install PHP, composer, add to path:
brew install php
brew install composer
nano .zshrc
Make sure the following line is not in the comments:
# on an Intel Mac
export PATH=$HOME/bin:/usr/local/bin:$PATH
If you're on an Apple Silicon-based Mac, you'll need to add:
# on an M1 Mac
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
and add the following to your .zshrc:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
Make sure PHP is linked correctly:
which php
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php`)
composer global require laravel/valet
valet install
This should install `dnsmasq` and set up Valet. Great, almost there!
valet trust
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
</details>
<details>
<summary><strong>PHP Monitor tells me `php` is not installed...</strong></summary>
Try installing again using `brew install php`.
This should resolve the issue! If that does not fix the issue, run `brew link php --force`. (Afterwards, you may need to restart your terminal to make sure the new linked version is detected.)
brew install php
brew link php --force
</details>
<details>
<summary><strong>Valet sites won't load. I'm getting a 502 Bad Gateway error!</strong></summary>
If you're visiting your `.test` domain, and you're getting a 502 (Bad Gateway) after switching to a different PHP version, you're dealing with a common issue.
This problem is usually resolved by upgrading Valet and running `valet install` again.
composer global update
valet install
</details>
<details>
<summary><strong>PHP Monitor tells me my installation is broken, but I don't see why!</strong></summary>
PHP Monitor tells you that a PHP installation is broken, if the configuration is causing warnings or errors when determining the version number.
Since PHP Monitor changes the linked version via Homebrew, both Valet *and* your terminal (CLI) should use the new PHP version.
However, this might not be the case on your system. You _might_ have a specific version of PHP linked if that is not the case. In that case, you may need to change your `.bashrc` or `.zshrc` file where the PATH is set (depending on the terminal you use).
You can find out which version of PHP is being used by running `which php`.
You can find out what exactly is causing the issue by running a command. On Intel, you can run (replace `7.4` with the version that is broken):
```
/usr/local/opt/php@7.4/bin/php -r "print phpversion();"
```
On Apple Silicon, you can run (replace `7.4` with the version that is broken):
```
/opt/homebrew/opt/php@7.4/bin/php -r "print phpversion();"
```
You should see an error or a warning here in the output.
Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling).
</details>
<details>
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
The value you provided in your INI file is invalid. If that is the case, PHP will attempt to parse your value as bytes, which is usually unintended. (`1GB` will resolve to merely a few bytes, and all of your applications will run out of memory!)
You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` is also allowed, or just an integer (which will result in N amount of bytes being the limit).
**Example**: Trying to use `1GB` as the memory limit, for example, will result in this exclamation mark. The correct way to set a 1GB limit is by using `1G` as the value. (Note: The displayed value will append `B` for clarity, so if you set `1G`, the value reported by PHP Monitor will be 1 GB.)
</details>
<details>
<summary><strong>One of my commented out extensions is not being detected...</strong></summary>
The app searches in the relevant `.ini` files for a specific pattern. For regular extensions:
* `extension="*.so"`
* `; extension="*.so"`
For Zend extensions:
* `zend_extension="*.so"`
* `; zend_extension="*.so"`
The `*` is a wildcard and the name of the extension. If you've commented out the extension, make sure you've commented it out with a semicolon (;) and a single space after the semicolon for PHP Monitor to detect it.
Since v3.4 all of the loaded .ini files are sourced to determine which extensions are enabled.
</details>
<details>
<summary><strong>I've got two Homebrew installations on my Apple Silicon Mac, can I choose which installation to use with PHP Monitor?</strong></summary>
Not at this time, no. PHP Monitor will prefer the `/opt/homebrew` installation over the classic installation directory.
</details>
<details>
<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.
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.
</details>
<details>
<summary><strong>After running PHP Monitor, Homebrew sometimes has issues with `brew upgrade`!</strong></summary>
This is a security feature of Brew. When you start a service as an administrator, the root user becomes the owner of relevant binaries.
You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder).
</details>
<details>
<summary><strong>The app has crashed!</strong></summary>
Please get in touch and open an issue. PHP Monitor shouldn't crash :)
</details>
## 📝 Having another issue?
I did not include any tracking or analytics software, so if you encounter issues, let me know [via an issue](https://github.com/nicoverbruggen/phpmon/issues/new).
## 💵 Support me?
PHP Monitor is available entirely **free of charge**, but if you can afford it a donation helps keep the project alive and the app maintained.
You can find a [sponsor](https://nicoverbruggen.be/sponsor) link at the top of this repo or you could click the link here to be taken to my sponsorship page.
Donations really help with the Apple Developer Program cost, and keep me motivated to keep working on PHP Monitor outside of work hours (I do have a day job!).
## 😎 Acknowledgements
While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks:
* My colleagues at [DIVE](https://dive.be)
* The [Homebrew](https://brew.sh/) team who maintain
* The [developers & maintainers of Valet](https://github.com/laravel/valet/graphs/contributors)
* Everyone in the Laravel community who shared the app (thanks!)
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot)
* Everyone who left feedback via issues
Thank you very much for your contributions, kind words and support.
## 🚜 How it works
### Loading info about PHP in the background
This utility runs `php-config --version` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
In order to save power, this only happens once every 60 seconds.
### Switching PHP versions
This utility will detect which PHP versions you have installed via Homebrew, and then allows you to switch between them.
This means:
- You have at least the latest version of PHP installed (`php`)
- You have installed Laravel Valet (`which valet` returns `/usr/local/bin/valet`)
- You ran `valet trust`, which means Valet commands can be run without using sudo
The utility runs the following commands:
- Unlink all detected PHP versions & stop the respective `php@X.X` services
- Link the desired version of PHP, and start the associated service
### Want to know more?
If you want to know more about how this works, I recommend you check out the source code.
This app isn't very complicated after all. In the end, this just (conveniently) executes some shell commands.
## 🔧 Build instructions
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
If you'd like to build PHP Monitor yourself, you need:
* Xcode (usually the latest version)
* The contents of this repository
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to immediately build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.

13
RELEASE.md Normal file
View File

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

20
SECURITY.md Normal file
View File

@ -0,0 +1,20 @@
# Security Policy
## Supported versions
Generally speaking, only the latest version of **PHP Monitor** is supported:
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions |
| ------- | ------------- | ------------------ | ----- | ----- | ----- |
| 4.0 | ✅ Universal binary | ✅ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 |
| 3.5 | ✅ Universal binary | ✅ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 |
| 3.5 | ✅ Universal binary | ✅ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 |
| 3.0—3.4 | ✅ Universal binary | ✅ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 |
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 |
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable |
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable |
| < 2.4 | Intel binary<br/>`/usr/local/homebrew` installations only | ❌ | Catalina (10.15) | macOS 10.14+ | not applicable |
## Reporting a vulnerability
Contact me (Nico Verbruggen) at the email address used for the commits in the repository. Please include "PHP Monitor" in the subject.

1
assets/icon.svg Normal file
View File

@ -0,0 +1 @@
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter x="-4.6%" y="-3.4%" width="109.2%" height="109.2%" filterUnits="objectBoundingBox" id="b"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="5.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.26 0" in="shadowBlurOuter1"/></filter><filter x="-8.6%" y="-11%" width="117.1%" height="121.9%" filterUnits="objectBoundingBox" id="d"><feOffset dx="1" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="6.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.372049825 0" in="shadowBlurOuter1" result="shadowMatrixOuter1"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter><linearGradient x1="50.023%" y1="-15.482%" x2="50.023%" y2="104.493%" id="e"><stop stop-color="#51C2FB" offset=".053%"/><stop stop-color="#1C91FE" offset="100%"/></linearGradient><path d="M167.939 50H344.06c41.01 0 55.881 4.27 70.874 12.288 14.992 8.018 26.759 19.785 34.777 34.777C457.73 112.058 462 126.93 462 167.94V344.06c0 41.01-4.27 55.881-12.288 70.874-8.018 14.992-19.785 26.759-34.777 34.777C399.942 457.73 385.07 462 344.06 462H167.94c-41.01 0-55.881-4.27-70.874-12.288-14.992-8.018-26.759-19.785-34.777-34.777C54.27 399.942 50 385.07 50 344.06V167.94c0-41.01 4.27-55.881 12.288-70.874 8.018-14.992 19.785-26.759 34.777-34.777C112.058 54.27 126.93 50 167.94 50z" id="a"/></defs><g fill="none" fill-rule="evenodd"><mask id="c" fill="#fff"><use xlink:href="#a"/></mask><use filter="url(#b)" xlink:href="#a" fill-rule="nonzero" fill="#000"/><g mask="url(#c)"><path fill="#FFF" d="M50 50h412v412H50z"/></g><g filter="url(#d)" mask="url(#c)"><path fill="#FFF" d="M131.535 155.144l63.377-8.907 33.68 239.645-63.377 8.907zM315.79 263.497l63.377-8.907 15.17 107.94-63.377 8.906z"/><path fill="#FFF" d="M315.79 263.497l63.377-8.907 15.17 107.94-63.377 8.906z"/><path fill="#FFF" d="M316.01 173.224l84.001 68.319-53.919 66.92-84-68.318z"/><path fill="#FFF" d="M93.155 157.813l164.384-23.102 21.433 152.5-164.385 23.104z"/><path d="M133.3 83.75h-12.9V54.437c0-2.303-1.935-4.187-4.3-4.187h-8.6c-2.365 0-4.3 1.884-4.3 4.188v41.874c0 2.304 1.935 4.188 4.3 4.188h25.8c2.365 0 4.3-1.884 4.3-4.188v-8.374c0-2.304-1.935-4.188-4.3-4.188zm202.1 100.5h-8.6v-56.584c0-6.647-2.741-13.033-7.579-17.744L265.525 57.63c-4.837-4.71-11.395-7.38-18.221-7.38H223.6V25.125C223.6 11.254 212.044 0 197.8 0h-172C11.556 0 0 11.254 0 25.125v167.5c0 13.871 11.556 25.125 25.8 25.125h8.6C34.4 245.492 57.513 268 86 268c28.487 0 51.6-22.508 51.6-50.25h68.8c0 27.742 23.112 50.25 51.6 50.25 28.488 0 51.6-22.508 51.6-50.25h25.8c4.73 0 8.6-3.769 8.6-8.375v-16.75c0-4.606-3.87-8.375-8.6-8.375zM86 242.875c-14.244 0-25.8-11.254-25.8-25.125s11.556-25.125 25.8-25.125c14.244 0 25.8 11.254 25.8 25.125s-11.556 25.125-25.8 25.125zm25.8-92.125c-33.271 0-60.2-26.224-60.2-58.625 0-32.4 26.929-58.625 60.2-58.625S172 59.724 172 92.125c0 32.4-26.929 58.625-60.2 58.625zM258 242.875c-14.244 0-25.8-11.254-25.8-25.125s11.556-25.125 25.8-25.125c14.244 0 25.8 11.254 25.8 25.125s-11.556 25.125-25.8 25.125zM301 134h-77.4V75.375h23.704L301 127.666V134z" fill="url(#e)" fill-rule="nonzero" transform="rotate(-8 1129.292 -453.38)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
assets/menubar.svg Normal file
View File

@ -0,0 +1 @@
<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><path d="M8.975 8.375H8.3V6.844a.223.223 0 00-.225-.219h-.45a.223.223 0 00-.225.219V9.03c0 .12.101.219.225.219h1.35a.223.223 0 00.225-.219v-.437a.223.223 0 00-.225-.219zm10.575 5.25h-.45v-2.956c0-.347-.143-.68-.397-.927l-2.81-2.731a1.37 1.37 0 00-.953-.386H13.7V5.312C13.7 4.588 13.095 4 12.35 4h-9C2.605 4 2 4.588 2 5.313v8.75c0 .724.605 1.312 1.35 1.312h.45C3.8 16.825 5.01 18 6.5 18c1.49 0 2.7-1.176 2.7-2.625h3.6c0 1.45 1.21 2.625 2.7 2.625 1.49 0 2.7-1.176 2.7-2.625h1.35c.247 0 .45-.197.45-.438v-.874a.445.445 0 00-.45-.438zM6.5 16.688c-.745 0-1.35-.588-1.35-1.313s.605-1.313 1.35-1.313c.745 0 1.35.588 1.35 1.313s-.605 1.313-1.35 1.313zm1.35-4.813c-1.74 0-3.15-1.37-3.15-3.063C4.7 7.12 6.11 5.75 7.85 5.75S11 7.12 11 8.813c0 1.692-1.41 3.062-3.15 3.062zm7.65 4.813c-.745 0-1.35-.588-1.35-1.313s.605-1.313 1.35-1.313c.745 0 1.35.588 1.35 1.313s-.605 1.313-1.35 1.313zM17.75 11H13.7V7.937h1.24l2.81 2.732V11z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 1017 B

87
assets/source.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/build.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/notification.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/screenshot34.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@ -0,0 +1,31 @@
//
// BrewJsonParserTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class BrewJsonParserTest: XCTestCase {
static var jsonBrewFile: URL {
return Bundle(for: Self.self).url(forResource: "brew", withExtension: "json")!
}
func testCanLoadExtension() throws {
let json = try? String(contentsOf: Self.jsonBrewFile, encoding: .utf8)
let package = try! JSONDecoder().decode(
[HomebrewPackage].self, from: json!.data(using: .utf8)!
).first!
XCTAssertEqual(package.name, "php")
XCTAssertEqual(package.full_name, "php")
XCTAssertEqual(package.aliases.first!, "php@8.0")
XCTAssertEqual(package.installed.contains(where: { installed in
installed.version.starts(with: "8.0")
}), true)
}
}

View File

@ -0,0 +1,25 @@
//
// CommandTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class CommandTest: XCTestCase {
func testDeterminePhpVersion() {
let version = Command.execute(
path: Paths.php,
arguments: ["-v"]
)
XCTAssert(version.contains("(cli)"))
XCTAssert(version.contains("NTS"))
XCTAssert(version.contains("built"))
XCTAssert(version.contains("Zend"))
}
}

View File

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

22
phpmon-tests/Info.plist Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@ -0,0 +1,30 @@
//
// PhpVersionDetectionTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 01/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class PhpVersionDetectionTest: XCTestCase {
func testCanDetectValidPhpVersions() throws {
let outcome = Actions.extractPhpVersions(from: [
"", // empty lines should be omitted
"php@8.0",
"php@8.0", // should only be detected once
"meta-php@8.0", // should be omitted, invalid
"php@8.0-coolio", // should be omitted, invalid
"php@7.0",
"",
"unrelatedphp@1.0", // should be omitted, invalid
"php@5.6",
"php@5.4" // should be omitted, not supported
], checkBinaries: false)
XCTAssertEqual(outcome, ["8.0", "7.0", "5.6"])
}
}

View File

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

332
phpmon-tests/brew.json Normal file
View File

@ -0,0 +1,332 @@
[
{
"name":"php",
"full_name":"php",
"tap":"homebrew/core",
"oldname":null,
"aliases":[
"php@8.0"
],
"versioned_formulae":[
"php@7.4",
"php@7.3",
"php@7.2"
],
"desc":"General-purpose scripting language",
"license":"PHP-3.01",
"homepage":"https://www.php.net/",
"versions":{
"stable":"8.0.2",
"head":"HEAD",
"bottle":true
},
"urls":{
"stable":{
"url":"https://www.php.net/distributions/php-8.0.2.tar.xz",
"tag":null,
"revision":null
}
},
"revision":0,
"version_scheme":0,
"bottle":{
"stable":{
"rebuild":0,
"cellar":"/opt/homebrew/Cellar",
"prefix":"/opt/homebrew",
"root_url":"https://homebrew.bintray.com/bottles",
"files":{
"arm64_big_sur":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.arm64_big_sur.bottle.tar.gz",
"sha256":"cbefa1db73d08b9af4593a44512b8d727e43033ee8517736bae5f16315501b12"
},
"big_sur":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.big_sur.bottle.tar.gz",
"sha256":"6857142e12254b15da4e74c2986dd24faca57dac8d467b04621db349e277dd63"
},
"catalina":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.catalina.bottle.tar.gz",
"sha256":"b651611134c18f93fdf121a4277b51b197a896a19ccb8020289b4e19e0638349"
},
"mojave":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.mojave.bottle.tar.gz",
"sha256":"9583a51fcc6f804aadbb14e18f770d4fb4973deaed6ddc4770342e62974ffbca"
}
}
}
},
"keg_only":false,
"bottle_disabled":false,
"options":[
],
"build_dependencies":[
"httpd",
"pkg-config"
],
"dependencies":[
"apr",
"apr-util",
"argon2",
"aspell",
"autoconf",
"curl",
"freetds",
"gd",
"gettext",
"glib",
"gmp",
"icu4c",
"krb5",
"libffi",
"libpq",
"libsodium",
"libzip",
"oniguruma",
"openldap",
"openssl@1.1",
"pcre2",
"sqlite",
"tidy-html5",
"unixodbc"
],
"recommended_dependencies":[
],
"optional_dependencies":[
],
"uses_from_macos":[
{
"xz":"build"
},
"bzip2",
"libedit",
"libxml2",
"libxslt",
"zlib"
],
"requirements":[
],
"conflicts_with":[
],
"caveats":"To enable PHP in Apache add the following to httpd.conf and restart Apache:\n LoadModule php_module $(brew --prefix)/opt/php/lib/httpd/modules/libphp.so\n\n <FilesMatch \\.php$>\n SetHandler application/x-httpd-php\n </FilesMatch>\n\nFinally, check DirectoryIndex includes index.php\n DirectoryIndex index.php index.html\n\nThe php.ini and php-fpm.ini file can be found in:\n $(brew --prefix)/etc/php/8.0/\n",
"installed":[
{
"version":"8.0.2",
"used_options":[
],
"built_as_bottle":true,
"poured_from_bottle":true,
"runtime_dependencies":[
{
"full_name":"apr",
"version":"1.7.0"
},
{
"full_name":"openssl@1.1",
"version":"1.1.1i"
},
{
"full_name":"apr-util",
"version":"1.6.1"
},
{
"full_name":"argon2",
"version":"20190702"
},
{
"full_name":"aspell",
"version":"0.60.8"
},
{
"full_name":"autoconf",
"version":"2.69"
},
{
"full_name":"brotli",
"version":"1.0.9"
},
{
"full_name":"gettext",
"version":"0.21"
},
{
"full_name":"libunistring",
"version":"0.9.10"
},
{
"full_name":"libidn2",
"version":"2.3.0"
},
{
"full_name":"libmetalink",
"version":"0.1.3"
},
{
"full_name":"libssh2",
"version":"1.9.0"
},
{
"full_name":"c-ares",
"version":"1.17.1"
},
{
"full_name":"jemalloc",
"version":"5.2.1"
},
{
"full_name":"libev",
"version":"4.33"
},
{
"full_name":"nghttp2",
"version":"1.43.0"
},
{
"full_name":"openldap",
"version":"2.4.57"
},
{
"full_name":"rtmpdump",
"version":"2.4+20151223"
},
{
"full_name":"zstd",
"version":"1.4.8"
},
{
"full_name":"curl",
"version":"7.75.0"
},
{
"full_name":"libtool",
"version":"2.4.6"
},
{
"full_name":"unixodbc",
"version":"2.3.9"
},
{
"full_name":"freetds",
"version":"1.2.18"
},
{
"full_name":"libpng",
"version":"1.6.37"
},
{
"full_name":"freetype",
"version":"2.10.4"
},
{
"full_name":"fontconfig",
"version":"2.13.1"
},
{
"full_name":"jpeg",
"version":"9d"
},
{
"full_name":"libtiff",
"version":"4.2.0"
},
{
"full_name":"webp",
"version":"1.2.0"
},
{
"full_name":"gd",
"version":"2.3.1"
},
{
"full_name":"libffi",
"version":"3.3"
},
{
"full_name":"pcre",
"version":"8.44"
},
{
"full_name":"gdbm",
"version":"1.18.1"
},
{
"full_name":"readline",
"version":"8.1"
},
{
"full_name":"sqlite",
"version":"3.34.0"
},
{
"full_name":"tcl-tk",
"version":"8.6.11"
},
{
"full_name":"xz",
"version":"5.2.5"
},
{
"full_name":"python@3.9",
"version":"3.9.1"
},
{
"full_name":"glib",
"version":"2.66.6"
},
{
"full_name":"gmp",
"version":"6.2.1"
},
{
"full_name":"icu4c",
"version":"67.1"
},
{
"full_name":"krb5",
"version":"1.19"
},
{
"full_name":"libpq",
"version":"13.1"
},
{
"full_name":"libsodium",
"version":"1.0.18"
},
{
"full_name":"libzip",
"version":"1.7.3"
},
{
"full_name":"oniguruma",
"version":"6.9.6"
},
{
"full_name":"pcre2",
"version":"10.36"
},
{
"full_name":"tidy-html5",
"version":"5.6.0"
}
],
"installed_as_dependency":false,
"installed_on_request":true
}
],
"linked_keg":"8.0.2",
"pinned":false,
"outdated":false,
"deprecated":false,
"deprecation_date":null,
"deprecation_reason":null,
"disabled":false,
"disable_date":null,
"disable_reason":null
}
]

1957
phpmon-tests/php.ini Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,364 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C41C1B3922B0097F00E7CF16 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3822B0097F00E7CF16 /* ViewController.swift */; };
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
C41C1B4922B00A9800E7CF16 /* ImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* ImageGenerator.swift */; };
C41C1B4B22B019FF00E7CF16 /* PHPVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PHPVersion.swift */; };
C41C1B4D22B0215A00E7CF16 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Services.swift */; };
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
C4D8016622B1584700C6DA1B /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Environment.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
C41C1B3322B0097F00E7CF16 /* phpmon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = phpmon.app; sourceTree = BUILT_PRODUCTS_DIR; };
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C41C1B3822B0097F00E7CF16 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C41C1B3D22B0098000E7CF16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
C41C1B3F22B0098000E7CF16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = "<group>"; };
C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
C41C1B4822B00A9800E7CF16 /* ImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGenerator.swift; sourceTree = "<group>"; };
C41C1B4A22B019FF00E7CF16 /* PHPVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPVersion.swift; sourceTree = "<group>"; };
C41C1B4C22B0215A00E7CF16 /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
C41C1B3022B0097F00E7CF16 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
C41C1B2A22B0097F00E7CF16 = {
isa = PBXGroup;
children = (
C41C1B3522B0097F00E7CF16 /* phpmon */,
C41C1B3422B0097F00E7CF16 /* Products */,
);
sourceTree = "<group>";
};
C41C1B3422B0097F00E7CF16 /* Products */ = {
isa = PBXGroup;
children = (
C41C1B3322B0097F00E7CF16 /* phpmon.app */,
);
name = Products;
sourceTree = "<group>";
};
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup;
children = (
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
C41C1B3822B0097F00E7CF16 /* ViewController.swift */,
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
C41C1B4E22B024F100E7CF16 /* Helpers */,
);
path = phpmon;
sourceTree = "<group>";
};
C41C1B4E22B024F100E7CF16 /* Helpers */ = {
isa = PBXGroup;
children = (
C41C1B4622B009A400E7CF16 /* Shell.swift */,
C41C1B4822B00A9800E7CF16 /* ImageGenerator.swift */,
C41C1B4A22B019FF00E7CF16 /* PHPVersion.swift */,
C41C1B4C22B0215A00E7CF16 /* Services.swift */,
C476FF9722B0DD830098105B /* Alert.swift */,
C4D8016522B1584700C6DA1B /* Environment.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
C41C1B3222B0097F00E7CF16 /* phpmon */ = {
isa = PBXNativeTarget;
buildConfigurationList = C41C1B4322B0098000E7CF16 /* Build configuration list for PBXNativeTarget "phpmon" */;
buildPhases = (
C41C1B2F22B0097F00E7CF16 /* Sources */,
C41C1B3022B0097F00E7CF16 /* Frameworks */,
C41C1B3122B0097F00E7CF16 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = phpmon;
productName = phpmon;
productReference = C41C1B3322B0097F00E7CF16 /* phpmon.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
C41C1B2B22B0097F00E7CF16 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1020;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "Nico Verbruggen";
TargetAttributes = {
C41C1B3222B0097F00E7CF16 = {
CreatedOnToolsVersion = 10.2.1;
};
};
};
buildConfigurationList = C41C1B2E22B0097F00E7CF16 /* Build configuration list for PBXProject "phpmon" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = C41C1B2A22B0097F00E7CF16;
productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
C41C1B3222B0097F00E7CF16 /* phpmon */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
C41C1B3122B0097F00E7CF16 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C41C1B2F22B0097F00E7CF16 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C4D8016622B1584700C6DA1B /* Environment.swift in Sources */,
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
C41C1B4D22B0215A00E7CF16 /* Services.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* ImageGenerator.swift in Sources */,
C41C1B3922B0097F00E7CF16 /* ViewController.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* PHPVersion.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
C41C1B3C22B0098000E7CF16 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
C41C1B3D22B0098000E7CF16 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C41C1B4122B0098000E7CF16 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "Mac Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
C41C1B4222B0098000E7CF16 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "Mac Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
C41C1B4422B0098000E7CF16 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
INFOPLIST_FILE = phpmon/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
C41C1B4522B0098000E7CF16 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
INFOPLIST_FILE = phpmon/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
C41C1B2E22B0097F00E7CF16 /* Build configuration list for PBXProject "phpmon" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C41C1B4122B0098000E7CF16 /* Debug */,
C41C1B4222B0098000E7CF16 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C41C1B4322B0098000E7CF16 /* Build configuration list for PBXNativeTarget "phpmon" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C41C1B4422B0098000E7CF16 /* Debug */,
C41C1B4522B0098000E7CF16 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = C41C1B2B22B0097F00E7CF16 /* Project object */;
}

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:phpmon.xcodeproj">
</FileRef>
</Workspace>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -1,131 +1,81 @@
//
// AppDelegate.swift
// phpmon
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
import UserNotifications
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
let statusItem = NSStatusBar.system.statusItem(
withLength: 32
)
// MARK: - Variables
var timer: Timer?
var version: PHPVersion? = nil
var availablePhpVersions : [String] = []
var busy: Bool = false
/**
The Shell singleton that keeps track of the history of all
(invoked by PHP Monitor) shell commands. It is used to
invoke all commands in this application.
*/
let sharedShell: Shell
/**
The App singleton contains information about the state of
the application and global variables.
*/
let state: App
/**
The MainMenu singleton is responsible for rendering the
menu bar item and its menu, as well as its actions.
*/
let menu: MainMenu
/**
The paths singleton that determines where Homebrew is installed,
and where to look for binaries.
*/
let paths: Paths
// MARK: - Initializer
/**
When the application initializes, create all singletons.
*/
override init() {
self.sharedShell = Shell.user
self.state = App.shared
self.menu = MainMenu.shared
self.paths = Paths.shared
super.init()
}
// MARK: - Lifecycle
/**
When the application has finished launching, we'll want to set up
the user notification center delegate, and kickoff the menu
startup procedure.
*/
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Start with the icon
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
// Perform environment boot checks
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
Environment.performBootChecks()
self.availablePhpVersions = Services.detectPhpVersions()
print("The following PHP versions were detected:")
print(self.availablePhpVersions)
self.updatePhpVersionInStatusBar()
// Schedule a request to fetch the PHP version every 15 seconds
Timer.scheduledTimer(
timeInterval: 15,
target: self,
selector: #selector(self.updatePhpVersionInStatusBar),
userInfo: nil,
repeats: true
)
}
NSUserNotificationCenter.default.delegate = self
self.menu.startup()
}
func setStatusBarImage(version: String) {
self.setStatusBar(image: ImageGenerator.generateImageForStatusBar(width: 32.0, text: version))
// MARK: - NSUserNotificationCenterDelegate
/**
When a notification is sent, the delegate of the notification center
is asked whether the notification should be presented or not. Since
the user can now disable notifications per application since macOS
Catalina, any and all notifications should be displayed.
*/
func userNotificationCenter(
_ center: NSUserNotificationCenter,
shouldPresent notification: NSUserNotification
) -> Bool {
return true
}
func setStatusBar(image: NSImage) {
if let button = statusItem.button {
image.isTemplate = true
button.image = image
}
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
@objc func updatePhpVersionInStatusBar() {
self.version = PHPVersion()
if (self.busy) {
DispatchQueue.main.async {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
}
} else {
DispatchQueue.main.async {
self.setStatusBarImage(version: self.version!.short)
}
}
self.updateMenu()
}
func updateMenu() {
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
let menu = NSMenu()
var string = "We are not sure what version of PHP you are running."
if (self.version != nil) {
string = "You are running PHP \(self.version!.long)"
}
menu.addItem(NSMenuItem(title: string, action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
if (self.availablePhpVersions.count > 0 && !self.busy) {
for index in (0..<self.availablePhpVersions.count) {
let version = self.availablePhpVersions[index]
let action = #selector(self.switchToPhpVersion(sender:))
let menuItem = NSMenuItem(title: "Switch to PHP \(version)", action: (version == self.version?.short) ? nil : action, keyEquivalent: "\(index + 1)")
menuItem.tag = index
menu.addItem(menuItem)
}
menu.addItem(NSMenuItem.separator())
}
if (self.busy) {
menu.addItem(NSMenuItem(title: "Switching PHP versions...", action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
}
menu.addItem(NSMenuItem(title: "About phpmon", action: #selector(self.openAbout), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Quit phpmon", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
DispatchQueue.main.async {
self.statusItem.menu = menu
}
}
}
@objc public func openAbout() {
NSApplication.shared.activate(ignoringOtherApps: true)
NSApplication.shared.orderFrontStandardAboutPanel()
}
@objc public func switchToPhpVersion(sender: AnyObject) {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
let index = sender.tag!
let version = self.availablePhpVersions[index]
self.busy = true
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
// Update the PHP version in the status bar
self.updatePhpVersionInStatusBar()
// Update the menu
self.updateMenu()
// Switch the PHP version
Services.switchToPhpVersion(version: version, availableVersions: self.availablePhpVersions)
// Mark as no longer busy
self.busy = false
// Perform UI updates on main thread
DispatchQueue.main.async {
self.updatePhpVersionInStatusBar()
self.updateMenu()
}
}
}
}

View File

@ -1,68 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "icon_16x16.png",
"scale" : "1x"
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "icon_16x16@2x.png",
"scale" : "2x"
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "icon_32x32.png",
"scale" : "1x"
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "icon_32x32@2x.png",
"scale" : "2x"
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "icon_128x128.png",
"scale" : "1x"
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "icon_128x128@2x.png",
"scale" : "2x"
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "icon_256x256.png",
"scale" : "1x"
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "icon_256x256@2x.png",
"scale" : "2x"
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "icon_512x512.png",
"scale" : "1x"
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "icon_512x512@2x.png",
"scale" : "2x"
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 500 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

View File

@ -1,71 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="phpmon" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="phpmon" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About phpmon" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Quit phpmon" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="phpmon Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="phpmon" customModuleProvider="target"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="0.0"/>
</scene>
<!--View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="phpmon" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" identifier="main" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="425" height="264"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="74.5" y="217"/>
</scene>
</scenes>
</document>

46
phpmon/Constants.swift Normal file
View File

@ -0,0 +1,46 @@
//
// Constants.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class 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 PHP versions supported by this application.
* Versions that do not appear in this array are omitted from the list.
*/
static let SupportedPhpVersions = [
// ====================
// STABLE RELEASES
// ====================
// Versions of PHP that are stable and are supported.
"5.6",
"7.0",
"7.1",
"7.2",
"7.3",
"7.4",
"8.0",
"8.1",
// ====================
// EXPERIMENTAL SUPPORT
// ====================
// 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.
"8.2"
]
}

22
phpmon/Credits.html Normal file
View File

@ -0,0 +1,22 @@
<html>
<head>
<style>
body {
background-color: #FFF;
color: #000;
font-family: -apple-system;
font-size: 11px;
padding: 5px;
margin: 5px;
}
</style>
</head>
<body>
<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>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p>
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p>
<br>
</body>
</html>

View File

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

View File

@ -0,0 +1,130 @@
//
// StateManager.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
import HotKey
class App {
static let shared = App()
init() {
loadGlobalHotkey()
}
/** Information about the currently linked PHP installation. */
static var phpInstall: ActivePhpInstallation? {
return App.shared.currentInstall
}
/** Whether the app is busy doing something. Used to determine what UI to display. */
static var busy: Bool {
return App.shared.busy
}
/** The list of preferences that are currently active. */
var preferences: [PreferenceName: Bool]!
/**
The window controller of the currently active window.
*/
var windowController: NSWindowController? = nil
/**
Whether the application is busy switching versions.
*/
var busy: Bool = false
/**
The currently active installation of PHP.
*/
var currentInstall: ActivePhpInstallation? = nil
/**
All available versions of PHP.
*/
var availablePhpVersions : [String] = []
/**
Cached information about the PHP installations; contains only the full version number at this point.
*/
var cachedPhpInstallations : [String: PhpInstallation] = [:]
/**
The timer that will periodically fetch the PHP version that is currently active.
*/
var timer: Timer?
/**
Information we were able to discern from the Homebrew info command (as JSON).
*/
var brewPhpPackage: HomebrewPackage! = nil {
didSet {
brewPhpVersion = brewPhpPackage!.version
}
}
/**
The version that the `php` formula via Brew is aliased to on the current system.
If you're up to date, `php` will be aliased to the latest version,
but that might not be the case.
We'll technically default to the version in Constants.swift, but the information
should always be loaded from Homebrew itself upon startup.
*/
var brewPhpVersion: String = Constants.LatestStablePhpVersion
/**
The shortcut the user has requested.
*/
var shortcutHotkey: HotKey? = nil {
didSet {
self.setupGlobalHotkeyListener()
}
}
// MARK: - Methods
/**
On startup, the preferences should be loaded from the .plist, and we'll enable the shortcut if it is set.
*/
private func loadGlobalHotkey() {
// Make sure we can retrieve the hotkey from preferences; if we cannot, no hotkey is set
guard let hotkey = Preferences.preferences[.globalHotkey] as? String else {
print("No global hotkey loaded")
return
}
// Make sure we can parse the JSON into the desired format; if we cannot, no hotkey is set
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
print("No global hotkey loaded, could not be parsed!")
self.shortcutHotkey = nil
return
}
self.shortcutHotkey = HotKey(keyCombo: KeyCombo(
carbonKeyCode: keybindPref.keyCode,
carbonModifiers: keybindPref.carbonFlags
))
}
/**
Sets up the action that needs to occur when the shortcut key is pressed (open the menu).
*/
private func setupGlobalHotkeyListener() {
guard let hotkey = self.shortcutHotkey else {
return
}
hotkey.keyDownHandler = {
MainMenu.shared.statusItem.button?.performClick(nil)
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
}

View File

@ -0,0 +1,288 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19455"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="PHP Monitor" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="PHP Monitor" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About PHP Monitor" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Quit PHP Monitor" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="PHP Monitor Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-343" y="-16"/>
</scene>
<!--Window Controller-->
<scene sceneID="PQa-AT-b2a">
<objects>
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PrefsWC" 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">
<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="372" y="403" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/>
<view key="contentView" id="2yL-50-11x">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/>
</connections>
</window>
<connections>
<segue destination="AW2-rV-rbS" kind="relationship" relationship="window.shadowedContentViewController" id="3dX-9V-eA0"/>
</connections>
</windowController>
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="327"/>
</scene>
<!--Preferences-->
<scene sceneID="iyi-IS-7Ps">
<objects>
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" wantsLayer="YES" misplaced="YES" id="Pf1-A5-3Xz">
<rect key="frame" x="0.0" y="0.0" width="574" height="311"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="GSr-K5-3yw">
<rect key="frame" x="485" y="13" width="76" height="32"/>
<buttonCell key="cell" type="push" title="CLOSE" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ocw-Rx-gyh">
<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="pressed:" target="AW2-rV-rbS" id="8dA-y4-voq"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MEf-MN-oXt">
<rect key="frame" x="148" y="274" width="406" height="18"/>
<buttonCell key="cell" type="check" title="DYN_ICON" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="m5s-qp-Iaj">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="toggledDynamicIcon:" target="AW2-rV-rbS" id="cuJ-mt-agf"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JrH-aa-AzL">
<rect key="frame" x="148" y="253" width="408" height="14"/>
<textFieldCell key="cell" title="DYN_ICON_DESC" id="MHA-Xt-xgF">
<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="V7b-jv-oCB">
<rect key="frame" x="143" y="75" width="184" height="32"/>
<constraints>
<constraint firstAttribute="width" constant="170" id="9jD-Bf-T2M"/>
</constraints>
<backgroundFilters>
<ciFilter name="CIDotScreen">
<configuration>
<real key="inputAngle" value="0.0"/>
<ciVector key="inputCenter">
<real value="150"/>
<real value="150"/>
</ciVector>
<null key="inputImage"/>
<real key="inputSharpness" value="0.69999999999999996"/>
<real key="inputWidth" value="6"/>
</configuration>
</ciFilter>
</backgroundFilters>
<buttonCell key="cell" type="push" title="SET_SHORTCUT" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="R63-tN-KVQ">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="register:" target="AW2-rV-rbS" id="4Mj-eM-4eW"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YsQ-AZ-Aei">
<rect key="frame" x="325" y="75" width="138" height="32"/>
<buttonCell key="cell" type="push" title="CLEAR_SHORTCUT" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nvE-5d-VOS">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystem"/>
</buttonCell>
<connections>
<action selector="unregister:" target="AW2-rV-rbS" id="2RI-4w-6Td"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5ZK-BG-o1t">
<rect key="frame" x="42" y="85" width="100" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="PREF_GLOSHO:" id="xiD-8H-p5s">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="31d-gd-auR">
<rect key="frame" x="18" y="275" width="124" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="120" id="8dt-Pg-wFI"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="PREF_DYN_ICON:" id="E10-ss-Cdz">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1TO-9H-z2d">
<rect key="frame" x="148" y="60" width="101" height="14"/>
<textFieldCell key="cell" title="SHORTCUT_DESC" id="nYP-yi-DBf">
<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="vSc-oQ-NC5">
<rect key="frame" x="148" y="220" width="121" height="18"/>
<buttonCell key="cell" type="check" title="FULL_PHP_VER" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="eCd-ja-EwE">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="toggledFullPhpVersion:" target="AW2-rV-rbS" id="RCY-Ah-sLM"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="t24-LR-wKz">
<rect key="frame" x="148" y="199" width="123" height="14"/>
<textFieldCell key="cell" title="FULL_PHP_VER_DESC" id="8gG-qs-mHR">
<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="ogC-wz-ZfO">
<rect key="frame" x="18" y="153" width="124" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="120" id="i9O-6m-Gr9"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="PREF_SERVICES:" id="bm4-rf-kCF">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="47u-9B-eDu">
<rect key="frame" x="148" y="152" width="126" height="18"/>
<buttonCell key="cell" type="check" title="AUTO_RESTART" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="n1d-l4-inL">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="toggledAutoRestartServices:" target="AW2-rV-rbS" id="THn-nu-IiJ"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ObP-GE-ejZ">
<rect key="frame" x="148" y="131" width="126" height="14"/>
<textFieldCell key="cell" title="AUTO_RESTART_DESC" id="F9P-iQ-gBk">
<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>
</subviews>
<constraints>
<constraint firstItem="ogC-wz-ZfO" firstAttribute="trailing" secondItem="31d-gd-auR" secondAttribute="trailing" id="2Lr-Ht-qKI"/>
<constraint firstItem="t24-LR-wKz" firstAttribute="leading" secondItem="vSc-oQ-NC5" secondAttribute="leading" id="3tK-kp-q5R"/>
<constraint firstItem="t24-LR-wKz" firstAttribute="top" secondItem="vSc-oQ-NC5" secondAttribute="bottom" constant="8" symbolic="YES" id="4Ft-lN-vwA"/>
<constraint firstAttribute="trailing" secondItem="JrH-aa-AzL" secondAttribute="trailing" constant="20" symbolic="YES" id="8iM-Xf-ShU"/>
<constraint firstItem="ObP-GE-ejZ" firstAttribute="leading" secondItem="47u-9B-eDu" secondAttribute="leading" id="ASF-WR-A3X"/>
<constraint firstAttribute="trailing" secondItem="GSr-K5-3yw" secondAttribute="trailing" constant="20" symbolic="YES" id="AT9-5F-6g1"/>
<constraint firstItem="YsQ-AZ-Aei" firstAttribute="leading" secondItem="V7b-jv-oCB" secondAttribute="trailing" constant="12" symbolic="YES" id="Bk6-4V-GLk"/>
<constraint firstItem="31d-gd-auR" firstAttribute="top" secondItem="Pf1-A5-3Xz" secondAttribute="top" constant="20" symbolic="YES" id="C3K-NX-BBl"/>
<constraint firstItem="YsQ-AZ-Aei" firstAttribute="top" secondItem="V7b-jv-oCB" secondAttribute="top" id="DY5-za-saX"/>
<constraint firstItem="vSc-oQ-NC5" firstAttribute="leading" secondItem="JrH-aa-AzL" secondAttribute="leading" id="FVa-vu-VGJ"/>
<constraint firstItem="MEf-MN-oXt" firstAttribute="leading" secondItem="31d-gd-auR" secondAttribute="trailing" constant="10" id="G5S-JV-re3"/>
<constraint firstItem="V7b-jv-oCB" firstAttribute="firstBaseline" secondItem="5ZK-BG-o1t" secondAttribute="firstBaseline" id="H5D-2D-DLH"/>
<constraint firstItem="1TO-9H-z2d" firstAttribute="leading" secondItem="V7b-jv-oCB" secondAttribute="leading" id="Imk-o0-2fS"/>
<constraint firstItem="ObP-GE-ejZ" firstAttribute="top" secondItem="47u-9B-eDu" secondAttribute="bottom" constant="8" symbolic="YES" id="JqR-Jd-SoR"/>
<constraint firstItem="JrH-aa-AzL" firstAttribute="leading" secondItem="MEf-MN-oXt" secondAttribute="leading" id="K2H-Af-2qK"/>
<constraint firstItem="5ZK-BG-o1t" firstAttribute="top" secondItem="ObP-GE-ejZ" secondAttribute="bottom" constant="30" id="LO4-8j-ihp"/>
<constraint firstItem="47u-9B-eDu" firstAttribute="top" secondItem="ogC-wz-ZfO" secondAttribute="top" id="T9j-v2-fSW"/>
<constraint firstItem="JrH-aa-AzL" firstAttribute="top" secondItem="MEf-MN-oXt" secondAttribute="bottom" constant="8" symbolic="YES" id="Vf8-fx-H50"/>
<constraint firstItem="MEf-MN-oXt" firstAttribute="firstBaseline" secondItem="31d-gd-auR" secondAttribute="firstBaseline" id="W36-bE-iAT"/>
<constraint firstItem="1TO-9H-z2d" firstAttribute="firstBaseline" secondItem="V7b-jv-oCB" secondAttribute="baseline" constant="25" id="bJG-ed-pch"/>
<constraint firstItem="V7b-jv-oCB" firstAttribute="leading" secondItem="JrH-aa-AzL" secondAttribute="leading" id="bUY-uH-N7A"/>
<constraint firstItem="5ZK-BG-o1t" firstAttribute="trailing" secondItem="31d-gd-auR" secondAttribute="trailing" id="c4g-jO-JUm"/>
<constraint firstAttribute="bottom" secondItem="GSr-K5-3yw" secondAttribute="bottom" constant="20" symbolic="YES" id="dAS-yW-vua"/>
<constraint firstItem="vSc-oQ-NC5" firstAttribute="top" secondItem="JrH-aa-AzL" secondAttribute="bottom" constant="16" id="hQf-4s-iHn"/>
<constraint firstItem="GSr-K5-3yw" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="mTE-WD-54L"/>
<constraint firstItem="47u-9B-eDu" firstAttribute="leading" secondItem="MEf-MN-oXt" secondAttribute="leading" id="n8B-C8-dXs"/>
<constraint firstItem="31d-gd-auR" firstAttribute="leading" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="o0J-yT-TDX"/>
<constraint firstItem="ogC-wz-ZfO" firstAttribute="top" secondItem="t24-LR-wKz" secondAttribute="bottom" constant="30" id="oXh-LE-sRS"/>
<constraint firstAttribute="trailing" secondItem="MEf-MN-oXt" secondAttribute="trailing" constant="20" symbolic="YES" id="pJg-zj-cBs"/>
<constraint firstItem="GSr-K5-3yw" firstAttribute="top" secondItem="1TO-9H-z2d" secondAttribute="bottom" constant="20" id="pMZ-Gx-Jmm"/>
</constraints>
</view>
<connections>
<outlet property="buttonAutoRestartServices" destination="47u-9B-eDu" id="kyg-BX-PQK"/>
<outlet property="buttonClearShortcut" destination="YsQ-AZ-Aei" id="1xo-hk-HgM"/>
<outlet property="buttonClose" destination="GSr-K5-3yw" id="d4I-Cf-gXD"/>
<outlet property="buttonDisplayFullPhpVersion" destination="vSc-oQ-NC5" id="ZLa-Vf-4Dq"/>
<outlet property="buttonDynamicIcon" destination="MEf-MN-oXt" id="qEN-Vg-EZS"/>
<outlet property="buttonSetShortcut" destination="V7b-jv-oCB" id="2aS-S4-cKR"/>
<outlet property="labelAutoRestartServices" destination="ObP-GE-ejZ" id="uwY-D7-Uve"/>
<outlet property="labelDisplayFullPhpVersion" destination="t24-LR-wKz" id="wYj-Z0-a3h"/>
<outlet property="labelDynamicIcon" destination="JrH-aa-AzL" id="CFc-fF-oPq"/>
<outlet property="labelShortcut" destination="1TO-9H-z2d" id="paF-hK-78x"/>
<outlet property="leftLabelDynamicIcon" destination="31d-gd-auR" id="ANZ-Zs-4d7"/>
<outlet property="leftLabelGlobalShortcut" destination="5ZK-BG-o1t" id="73E-9i-cg8"/>
<outlet property="leftLabelServices" destination="ogC-wz-ZfO" id="BYx-Gv-N1p"/>
</connections>
</viewController>
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="264" y="457"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,132 @@
//
// Environment.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class Startup {
public var failed : Bool = false
public var failureCallback = {}
/**
Checks the user's environment and checks if PHP Monitor can be used properly.
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
- Parameter success: Callback that is fired if the application can proceed with launch
- Parameter failure: Callback that is fired if the application must retry launch
*/
func checkEnvironment(success: () -> Void, failure: @escaping () -> Void)
{
failureCallback = failure
performEnvironmentCheck(
!Shell.fileExists("\(Paths.binPath)/php"),
messageText: "startup.errors.php_binary.title".localized,
informativeText: "startup.errors.php_binary_desc".localized,
breaking: true
)
performEnvironmentCheck(
!Shell.pipe("ls \(Paths.optPath) | grep php").contains("php"),
messageText: "startup.errors.php_opt.title".localized,
informativeText: "startup.errors.php_opt.desc".localized,
breaking: true
)
performEnvironmentCheck(
// Check for Valet; it can be symlinked or in .composer/vendor/bin
!(Shell.fileExists("/usr/local/bin/valet")
|| Shell.fileExists("/opt/homebrew/bin/valet")
|| Shell.fileExists("~/.composer/vendor/bin/valet")
),
messageText: "startup.errors.valet_executable.title".localized,
informativeText: "startup.errors.valet_executable.desc".localized,
breaking: true
)
performEnvironmentCheck(
!Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"),
messageText: "startup.errors.sudoers_brew.title".localized,
informativeText: "startup.errors.sudoers_brew.desc".localized,
breaking: true
)
performEnvironmentCheck(
// Check for Valet; it can be symlinked or in .composer/vendor/bin
!(Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet")
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/opt/homebrew/bin/valet")
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains(".composer/vendor/bin/valet")
),
messageText: "startup.errors.sudoers_valet.title".localized,
informativeText: "startup.errors.sudoers_valet.desc".localized,
breaking: true
)
let services = Shell.pipe("\(Paths.brew) services list | grep php")
performEnvironmentCheck(
(services.countInstances(of: "started") > 1),
messageText: "startup.errors.services.title".localized,
informativeText: "startup.errors.services.desc".localized,
breaking: false
)
if (!failed) {
determineBrewAliasVersion()
success()
}
}
/**
* In order to avoid having to hard-code which version of PHP is aliased to what specific subversion,
* PHP Monitor now determines the alias by checking the user's system.
*/
private func determineBrewAliasVersion()
{
print("PHP Monitor has determined the application has successfully passed all checks.")
print("Determining which version of PHP is aliased to `php` via Homebrew...")
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
App.shared.brewPhpPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
print("When on your system, the `php` formula means version \(App.shared.brewPhpVersion)!")
}
/**
* Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
*
* - Parameter condition: Fail condition to check for; if this returns `true`, the alert will be shown
* - Parameter messageText: Short description of what is wrong
* - Parameter informativeText: Expanded description of the environment check that failed
* - Parameter breaking: If the application should terminate afterwards
*/
private func performEnvironmentCheck(
_ condition: Bool,
messageText: String,
informativeText: String,
breaking: Bool
) {
if (!condition) { return }
failed = breaking
DispatchQueue.main.async { [self] in
// Present the information to the user
Alert.notify(
message: messageText,
info: informativeText,
style: breaking ? .critical : .warning
)
// Only breaking issues will throw the extra retry modal
breaking ? failureCallback() : ()
}
}
}

View File

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

View File

@ -0,0 +1,18 @@
//
// NSMenuExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 14/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension NSMenu {
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
newItem.keyEquivalentModifierMask = modifier
self.addItem(newItem)
}
}

View File

@ -0,0 +1,37 @@
//
// StringExtension.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
extension String {
var localized: String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}
func countInstances(of stringToFind: String) -> Int {
if (stringToFind.isEmpty) {
return 0
}
var count = 0
var searchRange: Range<String.Index>?
while let foundRange = range(of: stringToFind, options: [], range: searchRange) {
count += 1
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
}
return count
}
subscript (r: Range<String.Index>) -> String {
let start = r.lowerBound
let end = r.upperBound
return String(self[start ..< end])
}
}

View File

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

View File

@ -0,0 +1,34 @@
//
// Alert.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Alert {
public static func present(
messageText: String,
informativeText: String,
buttonTitle: String = "OK",
secondButtonTitle: String = "",
style: NSAlert.Style = .informational
) -> Bool {
let alert = NSAlert.init()
alert.alertStyle = style
alert.messageText = messageText
alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) {
alert.addButton(withTitle: secondButtonTitle)
}
return alert.runModal() == .alertFirstButtonReturn
}
public static func notify(message: String, info: String, style: NSAlert.Style = .informational) {
_ = self.present(messageText: message, informativeText: info, buttonTitle: "OK", secondButtonTitle: "", style: style)
}
}

View File

@ -0,0 +1,70 @@
//
// AliasConflict.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 28/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class HomebrewDiagnostics {
enum Errors: String {
case aliasConflict = "alias_conflict"
}
static let shared = HomebrewDiagnostics()
var errors: [HomebrewDiagnostics.Errors] = []
init() {
if self.determineAliasConflicts() {
self.errors.append(.aliasConflict)
}
}
/**
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
This will then result in two different aliases claiming to point to the same formula (`php`).
This will break all linking functionality in PHP Monitor, and the user needs to be informed of this.
This check only needs to be performed if the `shivammathur/php` tap is active.
*/
public func determineAliasConflicts() -> Bool
{
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
print("The user does not appear to have tapped: shivammathur/php")
return false
} else {
print("The user DOES have the following tapped: shivammathur/php")
print("Checking for `php` formula conflicts...")
let tapPhp = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: tapAlias.data(using: .utf8)!
).first!
if tapPhp.version != App.shared.brewPhpVersion {
print("The `php` formula alias seems to be the different between the tap and core. This could be a problem!")
print("Determining whether both of these versions are installed...")
let bothInstalled = App.shared.availablePhpVersions.contains(tapPhp.version)
&& App.shared.availablePhpVersions.contains(App.shared.brewPhpVersion)
if bothInstalled {
print("Both conflicting aliases seem to be installed, warning the user!")
} else {
print("Conflicting aliases are not both installed, seems fine!")
}
return bothInstalled
}
print("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
return false
}
}
}

View File

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

View File

@ -0,0 +1,19 @@
//
// LocalNotification.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class LocalNotification {
public static func send(title: String, subtitle: String) {
let notification = NSUserNotification()
notification.title = title
notification.subtitle = subtitle
NSUserNotificationCenter.default.deliver(notification)
}
}

View File

@ -1,21 +1,21 @@
//
// ImageGenerator.swift
// phpmon
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class ImageGenerator {
class MenuBarImageGenerator {
public static func generateImageForStatusBar(width: CGFloat = 30.0, height: CGFloat = 20.0, text: String) -> NSImage {
let image = NSImage(size: NSMakeSize(width, height))
let font = NSFont.systemFont(ofSize: 14)
/**
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.
*/
public static func textToImage(text: String) -> NSImage {
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let textRect = CGRect(x: 5, y: -1, width: image.size.width, height: image.size.height)
let font = NSFont.systemFont(ofSize: 14, weight: .medium)
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [
@ -24,8 +24,23 @@ class ImageGenerator {
NSAttributedString.Key.paragraphStyle: textStyle
]
let targetImage: NSImage = NSImage(size: image.size)
let padding : CGFloat = 2.0;
// Create an attributed string so we'll know how wide the item will need to be
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
let textSize = attributedString.size()
// Add padding to the width of the menu bar item
let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height)
let image = NSImage(size: size)
// Set the image rect with the appropriate dimensions
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
// Position the text inside the image rect
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
let targetImage: NSImage = NSImage(size: image.size)
let rep: NSBitmapImageRep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(image.size.width),

View File

@ -0,0 +1,25 @@
//
// HeaderView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class HeaderView: NSView, XibLoadable {
@IBOutlet weak var textField: NSTextField!
static func asMenuItem(text: String) -> NSMenuItem {
let view = Self.createFromXib()
view!.textField.stringValue = text.uppercased()
let item = NSMenuItem()
item.view = view
item.target = self
return item
}
}

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="HeaderView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="350" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ddg-VQ-cOT">
<rect key="frame" x="12" y="5" width="113" height="15"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="left" title="ACTIVE SERVICES" id="NHz-MZ-8FK">
<font key="font" metaFont="systemBold" size="12"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="ddg-VQ-cOT" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="n4Z-WN-RIh"/>
<constraint firstItem="ddg-VQ-cOT" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="14" id="yuW-pb-GQJ"/>
</constraints>
<connections>
<outlet property="textField" destination="ddg-VQ-cOT" id="aaQ-Xb-o2X"/>
</connections>
<point key="canvasLocation" x="-75" y="38"/>
</customView>
</objects>
</document>

View File

@ -0,0 +1,385 @@
//
// MainMenu.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
static let shared = MainMenu()
weak var menuDelegate: NSMenuDelegate? = nil
/**
The status bar item with variable length.
*/
let statusItem = NSStatusBar.system.statusItem(
withLength: NSStatusItem.variableLength
)
// MARK: - UI related
/**
Kick off the startup of the rendering of the main menu.
*/
func startup() {
// Start with the icon
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
// Perform environment boot checks
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
Startup().checkEnvironment(success: { onEnvironmentPass() },
failure: { onEnvironmentFail() }
)
}
}
/**
When the environment is all clear and the app can run, let's go.
*/
private func onEnvironmentPass() {
_ = Actions.detectPhpVersions()
if HomebrewDiagnostics.shared.errors.contains(.aliasConflict) {
DispatchQueue.main.async {
Alert.notify(
message: "alert.php_alias_conflict.title".localized,
info: "alert.php_alias_conflict.info".localized,
style: .critical
)
}
}
updatePhpVersionInStatusBar()
let installation = App.phpInstall!
installation.notifyAboutBrokenPhpFpm()
// Schedule a request to fetch the PHP version every 60 seconds
DispatchQueue.main.async { [self] in
App.shared.timer = Timer.scheduledTimer(
timeInterval: 60,
target: self,
selector: #selector(updatePhpVersionInStatusBar),
userInfo: nil,
repeats: true
)
}
}
/**
When the environment is not OK, present an alert to inform the user.
*/
private func onEnvironmentFail() {
DispatchQueue.main.async { [self] in
let close = Alert.present(
messageText: "alert.cannot_start.title".localized,
informativeText: "alert.cannot_start.info".localized,
buttonTitle: "alert.cannot_start.close".localized,
secondButtonTitle: "alert.cannot_start.retry".localized
)
if (close) {
exit(1)
}
startup()
}
}
/**
Update the menu's contents, based on what's going on.
*/
func update() {
// Update the menu item on the main thread
DispatchQueue.main.async { [self] in
// Create a new menu
let menu = StatusMenu()
// Add the PHP versions (or error messages)
menu.addPhpVersionMenuItems()
menu.addItem(NSMenuItem.separator())
// Add the possible actions
menu.addPhpActionMenuItems()
menu.addItem(NSMenuItem.separator())
// Add information about services & actions
menu.addPhpConfigurationMenuItems()
menu.addItem(NSMenuItem.separator())
// Add about & quit menu items
menu.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(openPrefs), keyEquivalent: ","))
menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(openAbout), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(terminateApp), keyEquivalent: "q"))
// Make sure every item can be interacted with
menu.items.forEach({ (item) in
item.target = self
})
statusItem.menu = menu
statusItem.menu?.delegate = self
}
}
/**
Sets the status bar image based on a version string.
*/
func setStatusBarImage(version: String) {
setStatusBar(
image: MenuBarImageGenerator.textToImage(text: version)
)
}
/**
Sets the status bar image, based on the provided NSImage.
The image will be used as a template image.
*/
func setStatusBar(image: NSImage) {
if let button = statusItem.button {
image.isTemplate = true
button.image = image
}
}
// MARK: - Nicer callbacks
/**
Executes a specific callback and fires the completion callback,
while updating the UI as required. As long as the completion callback
does not fire, the app is presumed to be busy and the UI reflects this.
- Parameter execute: Callback of the work that needs to happen.
- Parameter completion: Callback that is fired when the work is done.
*/
private func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
{
App.shared.busy = true
setBusyImage()
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
update()
execute()
App.shared.busy = false
DispatchQueue.main.async { [self] in
updatePhpVersionInStatusBar()
update()
completion()
}
}
}
// MARK: - User Interface
@objc func updatePhpVersionInStatusBar() {
App.shared.currentInstall = ActivePhpInstallation()
refreshIcon()
update()
}
func refreshIcon() {
DispatchQueue.main.async { [self] in
if (App.busy) {
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
} else {
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
// Static icon has been requested
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
} else {
// The dynamic icon has been requested
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
setStatusBarImage(version: long ? App.phpInstall!.version.long : App.phpInstall!.version.short)
}
}
}
}
@objc func reloadPhpMonitorMenu() {
waitAndExecute {
// This automatically reloads the menu
print("Reloading information about the PHP installation...")
} completion: {
// Add a slight delay to make sure it loads the new menu
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// Open the menu again
MainMenu.shared.statusItem.button?.performClick(nil)
}
}
}
@objc func setBusyImage() {
DispatchQueue.main.async { [self] in
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
}
}
// MARK: - Actions
@objc func restartPhpFpm() {
waitAndExecute {
Actions.restartPhpFpm()
}
}
@objc func restartAllServices() {
waitAndExecute {
Actions.restartDnsMasq()
Actions.restartPhpFpm()
Actions.restartNginx()
} completion: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_restarted".localized,
subtitle: "notification.services_restarted_desc".localized
)
}
}
}
@objc func stopAllServices() {
waitAndExecute {
Actions.stopAllServices()
} completion: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_stopped".localized,
subtitle: "notification.services_stopped_desc".localized
)
}
}
}
@objc func restartNginx() {
waitAndExecute {
Actions.restartNginx()
}
}
@objc func restartDnsMasq() {
waitAndExecute {
Actions.restartDnsMasq()
}
}
@objc func toggleExtension(sender: ExtensionMenuItem) {
waitAndExecute {
sender.phpExtension?.toggle()
if Preferences.preferences[.autoServiceRestartAfterExtensionToggle] as! Bool == true {
Actions.restartPhpFpm()
}
}
}
@objc func openPhpInfo() {
waitAndExecute {
// Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
// Tell php-cgi to run the PHP and output as an .html file
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
} completion: {
// When this has been completed, open the URL to the file in the browser
NSWorkspace.shared.open(URL(string: "file:///private/tmp/phpmon_phpinfo.html")!)
}
}
@objc func forceRestartLatestPhp() {
// Tell the user the switch is about to occur
Alert.notify(message: "alert.force_reload.title".localized, info: "alert.force_reload.info".localized)
// Start switching
waitAndExecute {
Actions.fixMyPhp()
} completion: {
Alert.notify(message: "alert.force_reload_done.title".localized, info: "alert.force_reload_done.info".localized)
}
}
@objc func openActiveConfigFolder() {
if (App.phpInstall!.version.error) {
// php version was not identified
Actions.openGenericPhpConfigFolder()
return
}
// php version was identified
Actions.openPhpConfigFolder(version: App.phpInstall!.version.short)
}
@objc func openGlobalComposerFolder() {
Actions.openGlobalComposerFolder()
}
@objc func openValetConfigFolder() {
Actions.openValetConfigFolder()
}
@objc func switchToPhpVersion(sender: PhpMenuItem) {
setBusyImage()
App.shared.busy = true
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
// Update the PHP version in the status bar
updatePhpVersionInStatusBar()
// Update the menu
update()
let completion = {
// Mark as no longer busy
App.shared.busy = false
// Perform UI updates on main thread
DispatchQueue.main.async { [self] in
updatePhpVersionInStatusBar()
update()
// Send a notification that the switch has been completed
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, sender.version),
subtitle: String(format: "notification.version_changed_desc".localized, sender.version)
)
App.phpInstall?.notifyAboutBrokenPhpFpm()
}
}
// Switch the PHP version
Actions.switchToPhpVersion(
version: sender.version,
availableVersions: App.shared.availablePhpVersions,
completed: completion
)
}
}
@objc func openAbout() {
NSApplication.shared.activate(ignoringOtherApps: true)
NSApplication.shared.orderFrontStandardAboutPanel()
}
@objc func openPrefs() {
PrefsVC.show()
}
@objc func terminateApp() {
NSApplication.shared.terminate(nil)
}
// MARK: - Menu Delegate
func menuWillOpen(_ menu: NSMenu) {
// Make sure the shortcut key does not trigger this when the menu is open
App.shared.shortcutHotkey?.isPaused = true
}
func menuDidClose(_ menu: NSMenu) {
// When the menu is closed, allow the shortcut to work again
App.shared.shortcutHotkey?.isPaused = false
}
}

View File

@ -0,0 +1,36 @@
//
// StatsView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class StatsView: NSView, XibLoadable {
@IBOutlet weak var titleMemLimit: NSTextField!
@IBOutlet weak var titleMaxPost: NSTextField!
@IBOutlet weak var titleMaxUpload: NSTextField!
@IBOutlet weak var labelMemLimit: NSTextField!
@IBOutlet weak var labelMaxPost: NSTextField!
@IBOutlet weak var labelMaxUpload: NSTextField!
static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem {
let view = Self.createFromXib()
view!.titleMemLimit.stringValue = "mi_memory_limit".localized.uppercased()
view!.titleMaxPost.stringValue = "mi_post_max_size".localized.uppercased()
view!.titleMaxUpload.stringValue = "mi_upload_max_filesize".localized.uppercased()
view!.labelMemLimit.stringValue = memory
view!.labelMaxPost.stringValue = post
view!.labelMaxUpload.stringValue = upload
let item = NSMenuItem()
item.view = view
item.target = self
return item
}
}

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="StatsView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="341" height="55"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
<rect key="frame" x="30" y="6" width="281" height="43"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
<rect key="frame" x="0.0" y="4" width="87" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="At1-ch-qv2">
<rect key="frame" x="-2" y="21" width="91" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="MEMORY LIMIT" id="LKe-C4-jxo">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Emt-m3-Dt6">
<rect key="frame" x="16" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="H6T-wY-PIG">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fillEqually" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g4d-4N-NkC">
<rect key="frame" x="107" y="4" width="77" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7um-XA-djV">
<rect key="frame" x="7" y="21" width="63" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX POST" id="Qfq-Bl-yuh">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Vyu-AO-8SH">
<rect key="frame" x="11" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="uH4-Zy-43x">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
<rect key="frame" x="204" y="4" width="77" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
<rect key="frame" x="-1" y="21" width="79" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX UPLOAD" id="lGh-MT-TgI">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eHT-tr-Kwx">
<rect key="frame" x="11" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="1iA-Ri-zYY">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="nWj-33-m8Q" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="CAY-Pw-B8n"/>
<constraint firstAttribute="bottom" secondItem="doH-ww-BDw" secondAttribute="bottom" constant="4" id="Dq4-M6-1Wf"/>
<constraint firstItem="g4d-4N-NkC" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="bls-fM-H4b"/>
<constraint firstAttribute="bottom" secondItem="nWj-33-m8Q" secondAttribute="bottom" constant="4" id="f6j-eI-wiH"/>
<constraint firstAttribute="bottom" secondItem="g4d-4N-NkC" secondAttribute="bottom" constant="4" id="faS-Mo-Qa2"/>
<constraint firstItem="doH-ww-BDw" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="gL3-5S-OKo"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="6" id="1mo-iG-Z0D"/>
<constraint firstAttribute="trailing" secondItem="TnH-dX-qaQ" secondAttribute="trailing" constant="30" id="3dD-wf-5pS"/>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="30" id="S8i-CD-j3h"/>
<constraint firstAttribute="bottom" secondItem="TnH-dX-qaQ" secondAttribute="bottom" constant="6" id="eve-qD-gUH"/>
</constraints>
<connections>
<outlet property="labelMaxPost" destination="Vyu-AO-8SH" id="5Cm-QO-hJQ"/>
<outlet property="labelMaxUpload" destination="eHT-tr-Kwx" id="5pK-FD-c4h"/>
<outlet property="labelMemLimit" destination="Emt-m3-Dt6" id="6nD-Su-XZ6"/>
<outlet property="titleMaxPost" destination="7um-XA-djV" id="5MN-Xb-XwL"/>
<outlet property="titleMaxUpload" destination="Oef-6n-9QI" id="Q61-JI-RJq"/>
<outlet property="titleMemLimit" destination="At1-ch-qv2" id="SQT-B9-sWS"/>
</connections>
<point key="canvasLocation" x="-84.5" y="44"/>
</customView>
</objects>
</document>

View File

@ -0,0 +1,162 @@
//
// MainMenuBuilder.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class StatusMenu : NSMenu {
func addPhpVersionMenuItems() {
if App.shared.currentInstall == nil {
return
}
if App.phpInstall!.version.error {
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
}
return
}
let phpVersionText = "\("mi_php_version".localized) \(App.phpInstall!.version.long)"
addItem(HeaderView.asMenuItem(text: phpVersionText))
}
func addPhpActionMenuItems() {
if App.busy {
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
return
}
if App.shared.availablePhpVersions.count == 0 {
return
}
self.addSwitchToPhpMenuItems()
self.addItem(NSMenuItem.separator())
self.addServicesMenuItems()
}
private func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
// Get the short and long version
let shortVersion = App.shared.availablePhpVersions[index]
let longVersion = App.shared.cachedPhpInstallations[shortVersion]!.longVersion
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
let versionString = long ? longVersion : shortVersion
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (shortVersion == App.shared.brewPhpVersion) ? "php" : "php@\(shortVersion)"
let menuItem = PhpMenuItem(
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
action: (shortVersion == App.phpInstall?.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
)
menuItem.version = shortVersion
shortcutKey = shortcutKey + 1
self.addItem(menuItem)
}
}
private func addServicesMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_active_services".localized))
let services = NSMenuItem(title: "mi_manage_services".localized, action: nil, keyEquivalent: "")
let servicesMenu = NSMenu()
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
servicesMenu.addItem(
NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
withKeyModifier: [.command, .shift]
)
for item in servicesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(servicesMenu, for: services)
self.addItem(NSMenuItem(title: "mi_force_load_latest".localized, action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: "f"))
self.addItem(services)
self.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
}
func addPhpConfigurationMenuItems() {
if App.shared.currentInstall == nil {
return
}
// Configuration
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
self.addItem(NSMenuItem(title: "mi_global_composer".localized, action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g"))
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
if (App.shared.busy) {
return
}
let stats = App.phpInstall!.configuration
// Stats
self.addItem(NSMenuItem.separator())
self.addItem(StatsView.asMenuItem(
memory: stats!.memory_limit,
post: stats!.post_max_size,
upload: stats!.upload_max_filesize)
)
// Extensions
self.addItem(NSMenuItem.separator())
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
if (App.phpInstall!.extensions.count == 0) {
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
}
var shortcutKey = 1
for phpExtension in App.phpInstall!.extensions {
self.addExtensionItem(phpExtension, shortcutKey)
shortcutKey += 1
}
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r"))
}
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
let menuItem = ExtensionMenuItem(
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
action: #selector(MainMenu.toggleExtension),
keyEquivalent: keyEquivalent
)
if menuItem.keyEquivalent != "" {
menuItem.keyEquivalentModifierMask = [.option]
}
menuItem.state = phpExtension.enabled ? .on : .off
menuItem.phpExtension = phpExtension
self.addItem(menuItem)
}
}
// MARK: - In order to store extra data in each item, NSMenuItem is subclassed
class PhpMenuItem: NSMenuItem {
var version: String = ""
}
class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension? = nil
}

View File

@ -0,0 +1,174 @@
//
// ActivePhpInstallation.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory.
When initialized, that version's .ini files are also scanned (for active or inactive extensions).
Integrity checks can be performed to determine whether PHP-FPM is configured correctly.
- Note: Each installation has a separate version number. Using `version.short` is advisable if you want to interact with Homebrew.
*/
class ActivePhpInstallation {
var version: Version!
var configuration: Configuration!
var extensions: [PhpExtension]!
// MARK: - Computed
var formula: String {
return (version.short == App.shared.brewPhpVersion) ? "php" : "php@\(version.short)"
}
// MARK: - Initializer
init() {
// Show information about the current version
self.getVersion()
// If an error occurred, exit early
if (version.error) {
configuration = Configuration()
extensions = []
return
}
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path)
// Get configuration values
configuration = Configuration(
memory_limit: self.getByteCount(key: "memory_limit"),
upload_max_filesize: self.getByteCount(key: "upload_max_filesize"),
post_max_size: self.getByteCount(key: "post_max_size")
)
// Return a list of .ini files parsed after php.ini
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
.replacingOccurrences(of: "\n", with: "")
.split(separator: ",")
.map { String($0) }
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
let extensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if extensions.count > 0 {
self.extensions.append(contentsOf: extensions)
}
}
}
/**
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.
*/
private func getVersion() -> Void {
self.version = Version()
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
if (version == "" || version.contains("Warning") || version.contains("Error")) {
self.version.short = "💩 BROKEN"
self.version.long = ""
self.version.error = true
return
}
// That's the long version
self.version.long = version
// Next up, let's strip away the minor version number
let segments = self.version.long.components(separatedBy: ".")
// Get the first two elements
self.version.short = segments[0...1].joined(separator: ".")
}
/**
Retrieves the display value for a specific key in the `.ini` file.
The following values are valid:
* -1: unlimited (show the infinity icon)
* 10000: an integer = amount of bytes
* 1K, 1M, 1G = shorthand for kilobytes, megabytes and gigabytes
If none of these notations are used, the _fallback_ value is used. We'll show an emoji to indicate something has gone wrong here.
To clarify, B gets appended to valid values. As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes .
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
*/
private func getByteCount(key: String) -> String {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
// Check if the value is unlimited
if (value == "-1") {
return ""
}
// Check if the syntax is valid otherwise
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
return (match == nil) ? "⚠️" : "\(value)B"
}
/**
It is always possible that the system configuration for PHP-FPM has not been set up for Valet.
This can occur when a user manually installs a new PHP version, but does not run `valet install`.
In that case, we should alert the user!
- Important: The underlying check is `checkPhpFpmStatus`, which can be run multiple times.
This method actively presents a modal if said checks fails, so don't call this method too many times.
*/
public func notifyAboutBrokenPhpFpm() {
if !self.checkPhpFpmStatus() {
DispatchQueue.main.async {
Alert.notify(
message: "alert.php_fpm_broken.title".localized,
info: "alert.php_fpm_broken.info".localized,
style: .critical
)
}
}
}
/**
Determine if PHP-FPM is configured correctly.
For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
that means that Valet won't work properly.
*/
private func checkPhpFpmStatus() -> Bool {
if self.version.short == "5.6" {
// The main PHP config file should contain `valet.sock` and then we're probably fine?
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
return Shell.pipe("cat \(fileName)").contains("valet.sock")
}
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
return Shell.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
}
// MARK: - Structs
struct Version {
var short = "???"
var long = "???"
var error = false
}
struct Configuration {
var memory_limit = "???"
var upload_max_filesize = "???"
var post_max_size = "???"
}
}

View File

@ -0,0 +1,108 @@
//
// PhpExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 31/01/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
A PHP extension that was detected in the php.ini file.
Please note that the extension may be disabled.
- Note: You need to know more about regular expressions to be able to deal with these NSRegularExpression
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
*/
class PhpExtension {
/// The file where this extension was located.
var file: String
/// The original string that was used to determine this extension is active.
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.
var name: String
/// Whether the extension has been enabled.
var enabled: Bool
/// The file where this extension was located, but only the filename, not the full path to the .ini file.
var fileNameOnly: String {
return String(file.split(separator: "/").last ?? "php.ini")
}
/**
This regular expression will allow us to identify lines which activate an extension.
It will match the following items:
* `extension="name.so"`
* `zend_extension="name.so"`
* `; extension="name.so"`
* `; zend_extension="name.so"`
- 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)"?)$"#
/**
When registering an extension, we do that based on the line found inside the .ini file.
*/
init(_ line: String, file: String) {
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
let match = regex.matches(in: line, options: [], range: NSMakeRange(0, line.count)).first
let range = Range(match!.range(withName: "name"), in: line)!
self.line = line
let fullPath = String(line[range])
.replacingOccurrences(of: "\"", with: "") // replace excess "
.replacingOccurrences(of: ".so", with: "") // replace excess .so
self.name = String(fullPath.split(separator: "/").last!) // take last segment
self.enabled = !line.contains(";")
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.
*/
func toggle() {
let newLine = enabled
// DISABLED: Commented out line
? "; \(line)"
// ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "")
Actions.sed(file: file, original: line, replacement: newLine)
enabled.toggle()
}
// MARK: - Static Methods
/**
This method will attempt to identify all extensions in the .ini file at a certain URL.
*/
static func load(from path: URL) -> [PhpExtension] {
let file = try? String(contentsOf: path, encoding: .utf8)
if (file == nil) {
print("There was an issue reading the file. Assuming no extensions were found.")
return []
}
return file!.components(separatedBy: "\n")
.filter {
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
}
.map {
return PhpExtension($0, file: path.path)
}
}
}

View File

@ -0,0 +1,33 @@
//
// BrewPhpInstallation.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 28/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpInstallation {
var longVersion: String
var homebrewInfo: HomebrewPackage
init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
self.longVersion = version
if Shell.fileExists(phpConfigExecutablePath) {
self.longVersion = Command.execute(
path: phpConfigExecutablePath,
arguments: ["--version"]
)
}
let info = Shell.pipe("\(Paths.brew) info php@\(version) --json")
self.homebrewInfo = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: info.data(using: .utf8)!
).first!
}
}

View File

@ -0,0 +1,75 @@
//
// GlobalKeybindPreference.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
struct GlobalKeybindPreference: Codable, CustomStringConvertible {
// MARK: - Internal variables
let function : Bool
let control : Bool
let command : Bool
let shift : Bool
let option : Bool
let capsLock : Bool
let carbonFlags : UInt32
let characters : String?
let keyCode : UInt32
// MARK: - How the keybind is display in Preferences
var description: String {
var stringBuilder = ""
if self.function {
stringBuilder += "Fn"
}
if self.control {
stringBuilder += ""
}
if self.option {
stringBuilder += ""
}
if self.command {
stringBuilder += ""
}
if self.shift {
stringBuilder += ""
}
if self.capsLock {
stringBuilder += ""
}
if let characters = self.characters {
stringBuilder += characters.uppercased()
}
return "\(stringBuilder)"
}
// MARK: - Persisting data to UserDefaults (as JSON)
public func toJson() -> String {
let jsonData = try! JSONEncoder().encode(self)
return String(data: jsonData, encoding: .utf8)!
}
public static func fromJson(_ string: String?) -> GlobalKeybindPreference? {
if string == nil {
return nil
}
if let jsonData = string!.data(using: .utf8) {
let decoder = JSONDecoder()
do {
return try decoder.decode(GlobalKeybindPreference.self, from: jsonData)
} catch {
return nil
}
}
return nil
}
}

View File

@ -0,0 +1,91 @@
//
// Preferences.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
enum PreferenceName: String {
case wasLaunchedBefore = "launched_before"
case shouldDisplayDynamicIcon = "use_dynamic_icon"
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
case globalHotkey = "global_hotkey"
}
class Preferences {
// MARK: - Singleton
static var shared = Preferences()
var cachedPreferences: [PreferenceName: Any?]
public init() {
Preferences.handleFirstTimeLaunch()
self.cachedPreferences = Self.cache()
}
// MARK: - First Time Run
/**
Note: macOS seems to cache plist values in memory as well as in files.
You can find the persisted configuration file in: ~/Library/Preferences/com.nicoverbruggen.phpmon.plist
To clear the cache, and get a first-run experience you may need to run:
```
defaults delete com.nicoverbruggen.phpmon
killall cfprefsd
```
*/
static func handleFirstTimeLaunch() {
UserDefaults.standard.register(defaults: [
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true
])
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
return
}
print("Saving first-time preferences!")
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
UserDefaults.standard.synchronize()
}
// MARK: - API
static var preferences: [PreferenceName: Any?] {
return Self.shared.cachedPreferences
}
// MARK: - Internal Functionality
static func cache() -> [PreferenceName: Any] {
return [
// Part 1: Always Booleans
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
// Part 2: Always Strings
.globalHotkey: UserDefaults.standard.string(forKey: PreferenceName.globalHotkey.rawValue) as Any,
]
}
static func update(_ preference: PreferenceName, value: Any?) {
if (value == nil) {
UserDefaults.standard.removeObject(forKey: preference.rawValue)
} else {
UserDefaults.standard.setValue(value, forKey: preference.rawValue)
}
UserDefaults.standard.synchronize()
// Update the preferences cache in memory!
Preferences.shared.cachedPreferences = Preferences.cache()
}
}

View File

@ -0,0 +1,229 @@
//
// PrefsVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
import HotKey
import Carbon
class PrefsVC: NSViewController {
// Labels on the left
@IBOutlet weak var leftLabelDynamicIcon: NSTextField!
@IBOutlet weak var leftLabelServices: NSTextField!
@IBOutlet weak var leftLabelGlobalShortcut: NSTextField!
// Dynamic icon
@IBOutlet weak var buttonDynamicIcon: NSButton!
@IBOutlet weak var labelDynamicIcon: NSTextField!
// Full PHP version
@IBOutlet weak var buttonDisplayFullPhpVersion: NSButton!
@IBOutlet weak var labelDisplayFullPhpVersion: NSTextField!
// Auto-restart services
@IBOutlet weak var buttonAutoRestartServices: NSButton!
@IBOutlet weak var labelAutoRestartServices: NSTextField!
// Shortcut
@IBOutlet weak var buttonSetShortcut: NSButton!
@IBOutlet weak var buttonClearShortcut: NSButton!
@IBOutlet weak var labelShortcut: NSTextField!
// Close button (bottom right)
@IBOutlet weak var buttonClose: NSButton!
// MARK: - Display
public static func show(delegate: NSWindowDelegate? = nil) {
if (App.shared.windowController == nil) {
let vc = NSStoryboard(name: "Main", bundle: nil)
.instantiateController(withIdentifier: "preferences") as! PrefsVC
let window = NSWindow(contentViewController: vc)
window.title = "prefs.title".localized
window.delegate = delegate
window.styleMask = [.titled, .closable]
App.shared.windowController = PrefsWC(window: window)
}
App.shared.windowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
}
// MARK: - Lifecycle
override func viewWillAppear() {
loadLocalization()
loadDynamicIconFromPreferences()
loadFullPhpVersionFromPreferences()
loadGlobalKeybindFromPreferences()
}
override func viewWillDisappear() {
if self.listeningForGlobalHotkey {
listeningForGlobalHotkey = false
}
}
private func loadLocalization() {
// Dynamic icon
leftLabelDynamicIcon.stringValue = "prefs.dynamic_icon".localized
labelDynamicIcon.stringValue = "prefs.dynamic_icon_desc".localized
buttonDynamicIcon.title = "prefs.dynamic_icon_title".localized
// Full PHP version
buttonDisplayFullPhpVersion.title = "prefs.display_full_php_version".localized
labelDisplayFullPhpVersion.stringValue = "prefs.display_full_php_version_desc".localized
// Services
leftLabelServices.stringValue = "prefs.services".localized
buttonAutoRestartServices.title = "prefs.auto_restart_services_title".localized
labelAutoRestartServices.stringValue = "prefs_auto_restart_services_desc".localized
// Global Shortcut
leftLabelGlobalShortcut.stringValue = "prefs.global_shortcut".localized
labelShortcut.stringValue = "prefs.shortcut_desc".localized
buttonSetShortcut.title = "prefs.shortcut_set".localized
buttonClearShortcut.title = "prefs.shortcut_clear".localized
// Close button
buttonClose.title = "prefs.close".localized
}
// MARK: - Loading Preferences
func loadDynamicIconFromPreferences() {
let shouldDisplay = Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == true
self.buttonDynamicIcon.state = shouldDisplay ? .on : .off
}
func loadFullPhpVersionFromPreferences() {
let shouldDisplay = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool == true
self.buttonDisplayFullPhpVersion.state = shouldDisplay ? .on : .off
}
func loadAutoRestartServicesFromPreferences() {
let shouldDisplay = Preferences.preferences[.autoServiceRestartAfterExtensionToggle] as! Bool == true
self.buttonAutoRestartServices.state = shouldDisplay ? .on : .off
}
// MARK: - Actions
@IBAction func toggledDynamicIcon(_ sender: Any) {
Preferences.update(.shouldDisplayDynamicIcon, value: buttonDynamicIcon.state == .on)
MainMenu.shared.refreshIcon()
}
@IBAction func toggledFullPhpVersion(_ sender: Any) {
Preferences.update(.fullPhpVersionDynamicIcon, value: buttonDisplayFullPhpVersion.state == .on)
MainMenu.shared.refreshIcon()
MainMenu.shared.update()
}
@IBAction func toggledAutoRestartServices(_ sender: Any) {
Preferences.update(.autoServiceRestartAfterExtensionToggle, value: buttonAutoRestartServices.state == .on)
}
// MARK: - Shortcut Preference
// Adapted from: https://dev.to/mitchartemis/creating-a-global-configurable-shortcut-for-macos-apps-in-swift-25e9
var listeningForGlobalHotkey = false {
didSet {
if listeningForGlobalHotkey {
DispatchQueue.main.async { [weak self] in
self?.buttonSetShortcut.highlight(true)
self?.buttonSetShortcut.title = "prefs.shortcut_listening".localized
}
} else {
DispatchQueue.main.async { [weak self] in
self?.buttonSetShortcut.highlight(false)
self?.loadGlobalKeybindFromPreferences()
}
}
}
}
func loadGlobalKeybindFromPreferences() {
let globalKeybind = GlobalKeybindPreference.fromJson(Preferences.preferences[.globalHotkey] as! String?)
if (globalKeybind != nil) {
updateKeybindButton(globalKeybind!)
} else {
buttonSetShortcut.title = "prefs.shortcut_set".localized
}
buttonClearShortcut.isEnabled = globalKeybind != nil
}
func updateGlobalShortcut(_ event : NSEvent) {
self.listeningForGlobalHotkey = false
if let characters = event.charactersIgnoringModifiers {
let newGlobalKeybind = GlobalKeybindPreference.init(
function: event.modifierFlags.contains(.function),
control: event.modifierFlags.contains(.control),
command: event.modifierFlags.contains(.command),
shift: event.modifierFlags.contains(.shift),
option: event.modifierFlags.contains(.option),
capsLock: event.modifierFlags.contains(.capsLock),
carbonFlags: event.modifierFlags.carbonFlags,
characters: characters,
keyCode: UInt32(event.keyCode)
)
Preferences.update(.globalHotkey, value: newGlobalKeybind.toJson())
updateKeybindButton(newGlobalKeybind)
buttonClearShortcut.isEnabled = true
App.shared.shortcutHotkey = HotKey(
keyCombo: KeyCombo(
carbonKeyCode: UInt32(event.keyCode),
carbonModifiers: event.modifierFlags.carbonFlags
)
)
}
}
@IBAction func register(_ sender: Any) {
unregister(nil)
listeningForGlobalHotkey = true
view.window?.makeFirstResponder(nil)
}
@IBAction func unregister(_ sender: Any?) {
listeningForGlobalHotkey = false
App.shared.shortcutHotkey = nil
buttonSetShortcut.title = ""
Preferences.update(.globalHotkey, value: nil)
}
func updateClearButton(_ globalKeybindPreference: GlobalKeybindPreference?) {
if globalKeybindPreference != nil {
buttonClearShortcut.isEnabled = true
} else {
buttonClearShortcut.isEnabled = false
}
}
func updateKeybindButton(_ globalKeybindPreference: GlobalKeybindPreference) {
buttonSetShortcut.title = globalKeybindPreference.description
}
@IBAction func pressed(_ sender: Any) {
self.view.window?.windowController?.close()
}
// MARK: - Deinitialization
deinit {
print("VC deallocated")
}
}

View File

@ -0,0 +1,37 @@
//
// PrefsWC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 02/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
struct Keys {
static let Escape = 53
static let Space = 49
}
class PrefsWC: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
}
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)
if let vc = self.contentViewController as? PrefsVC {
if vc.listeningForGlobalHotkey {
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
print("A blacklisted key was pressed, canceling listen")
vc.listeningForGlobalHotkey = false
} else {
vc.updateGlobalShortcut(event)
}
}
}
}
}

View File

@ -0,0 +1,40 @@
//
// Command.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Command {
/**
Immediately executes a command.
- Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on.
- Parameter trimNewlines: Removes empty new line output.
*/
public static func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String {
let task = Process()
task.launchPath = path
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
if (trimNewlines) {
return output.components(separatedBy: .newlines)
.filter({ !$0.isEmpty })
.joined(separator: "\n")
}
return output
}
}

View File

@ -0,0 +1,68 @@
//
// Paths.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
enum HomebrewDir: String {
case opt = "/opt/homebrew"
case usr = "/usr/local"
}
class Paths {
static let shared = Paths()
var baseDir : HomebrewDir
init() {
let optBrewFound = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew")
let usrBrewFound = Shell.fileExists("\(HomebrewDir.usr.rawValue)/bin/brew")
if (optBrewFound) {
// This is usually the case with Homebrew installed on Apple Silicon
baseDir = .opt
} else if (usrBrewFound) {
// This is usually the case with Homebrew installed on Intel (or Rosetta 2)
baseDir = .usr
} else {
// Falling back to default "legacy" Homebrew location (for Intel)
print("Seems like we couldn't determine the Homebrew directory.")
print("This usually means we're in trouble... (no Homebrew?)")
baseDir = .usr
}
print("Homebrew directory: \(baseDir)")
}
// - MARK: Binaries
public static var brew: String {
return "\(binPath)/brew"
}
public static var php: String {
return "\(binPath)/php"
}
public static var phpConfig: String {
return "\(binPath)/php-config"
}
// - MARK: Paths
public static var binPath: String {
return "\(shared.baseDir.rawValue)/bin"
}
public static var optPath: String {
return "\(shared.baseDir.rawValue)/opt"
}
public static var etcPath: String {
return "\(shared.baseDir.rawValue)/etc"
}
}

View File

@ -0,0 +1,93 @@
//
// Shell.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Shell {
// MARK: - Invoke static functions
public static func run(_ command: String) {
Shell.user.run(command)
}
public static func pipe(_ command: String) -> String {
return Shell.user.pipe(command)
}
// MARK: - Singleton
var shell: String
init() {
// Determine if we're using macOS Catalina or newer (that support /bin/zsh as default shell)
let at_least_10_15 = ProcessInfo.processInfo.isOperatingSystemAtLeast(
.init(majorVersion: 10, minorVersion: 15, patchVersion: 0))
// If macOS Mojave is being used, we'll default to /bin/bash
shell = at_least_10_15
? "/bin/sh"
: "/bin/bash"
print(at_least_10_15
? "Detected recent macOS (> 10.15): defaulting to /bin/sh"
: "Detected older macOS (< 10.15): defaulting to /bin/bash"
)
}
/**
Singleton to access a user shell (with --login)
*/
static let user = Shell()
/**
Runs a shell command without using the output.
Uses the default shell.
- Parameter command: The command to run
*/
func run(_ command: String) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = pipe(command)
}
/**
Runs a shell command and returns the output.
- Parameter command: The command to run
- Parameter shell: Path to the shell to invoke
*/
func pipe(_ command: String) -> String {
let task = Process()
let outputPipe = Pipe()
let errorPipe = Pipe()
task.launchPath = self.shell
task.arguments = ["--login", "-c", command]
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
let error = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
let output = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
if (output == "" && error.lengthOfBytes(using: .utf8) > 0) {
return error
}
return output
}
/**
Checks if a file exists at the provided path.
Uses `/bin/echo` instead of the `builtin` (which does not support `-n`).
*/
public static func fileExists(_ path: String) -> Bool {
return Shell.pipe("if [ -f \(path) ]; then /bin/echo -n \"0\"; fi") == "0"
}
}

View File

@ -1,23 +0,0 @@
//
// Alert.swift
// phpmon
//
// Created by Nico Verbruggen on 12/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Alert {
public static func present(
messageText: String,
informativeText: String,
buttonTitle: String = "OK"
) {
let alert = NSAlert.init()
alert.messageText = messageText
alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle)
alert.runModal()
}
}

View File

@ -1,39 +0,0 @@
//
// Environment.swift
// phpmon
//
// Created by Nico Verbruggen on 12/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Foundation
class Environment {
public static func performBootChecks()
{
if (!Shell.execute(command: "which php").contains("/usr/local/bin/php")) {
DispatchQueue.main.async {
Alert.present(
messageText: "PHP is not correctly installed",
informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue."
)
}
}
if (!Shell.execute(command: "ls /usr/local/opt | grep php@7.3").contains("php@7.3")) {
DispatchQueue.main.async {
Alert.present(
messageText: "PHP 7.3 is not correctly installed",
informativeText: "PHP 7.3 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue."
)
}
}
if (!Shell.execute(command: "which valet").contains("/usr/local/bin/valet")) {
DispatchQueue.main.async {
Alert.present(
messageText: "Laravel Valet is not correctly installed",
informativeText: "You must install Valet via brew. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue."
)
}
}
}
}

View File

@ -1,29 +0,0 @@
//
// PhpVersionExtractor.swift
// phpmon
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Foundation
class PHPVersion {
var short : String = "???"
var long : String = "???"
init() {
// Get the info about the PHP installation
let output = Shell.execute(command: "php -v")
// Get everything before "(cli)" (PHP X.X.X (cli) ...)
var version = output.components(separatedBy: " (cli)")[0]
// Strip away the text before the version number
version = version.components(separatedBy: "PHP ")[1]
self.long = version
// Next up, let's strip away the minor version number
let segments = version.components(separatedBy: ".")
// Get the first two elements
self.short = segments[0...1].joined(separator: ".")
}
}

View File

@ -1,52 +0,0 @@
//
// Services.swift
// phpmon
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Foundation
class Services {
public static func mysqlIsRunning() -> Bool {
let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.mysql")
if (running != "") {
return true
}
return false
}
public static func nginxIsRunning() -> Bool {
let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.nginx")
if (running != "") {
return true
}
return false
}
public static func detectPhpVersions() -> [String] {
let files = Shell.execute(command: "ls /usr/local/opt | grep php@")
var versions = files.components(separatedBy: "\n")
// Remove all empty strings
versions.removeAll { (string) -> Bool in
return (string == "")
}
// Get a list of versions only
var versionsOnly : [String] = []
versions.forEach { (string) in
versionsOnly.append(string.components(separatedBy: "php@")[1])
}
return versionsOnly
}
public static func switchToPhpVersion(version: String, availableVersions: [String]) {
availableVersions.forEach { (version) in
_ = Shell.execute(command: "brew unlink php@\(version)")
}
if (availableVersions.contains("7.3")) {
_ = Shell.execute(command: "brew link php@7.3")
_ = Shell.execute(command: "valet use php@\(version)")
}
}
}

View File

@ -1,27 +0,0 @@
//
// Shell.swift
// phpmon
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Shell {
public static func execute(command: String) -> String
{
let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["--login", "-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
return output
}
}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ApplicationDescription</key>
<string>PHP Monitor is a tool that shows the active PHP version in your menu bar and gives you easy access to certain PHP service actions and config files.</string>
<key>DeveloperName</key>
<string>Nico Verbruggen</string>
<key>Website</key>
<string>https://github.com/nicoverbruggen/phpmon</string>
<key>Connections</key>
<array>
<dict>
<key>IsIncoming</key>
<false/>
<key>Host</key>
<string>registry.npmjs.org</string>
<key>NetworkProtocol</key>
<string>TCP</string>
<key>Port</key>
<string>80, 443</string>
<key>Relevance</key>
<string>Essential</string>
<key>Purpose</key>
<string>PHP Monitor directly invokes Homebrew which contacts the NPM Registry.</string>
<key>DenyConsequences</key>
<string>If you deny these connections, PHP Monitor might not be able to complete its preset set of instructions, causing version switching to fail.</string>
</dict>
<dict>
<key>IsIncoming</key>
<false/>
<key>Host</key>
<string>github.com, api.github.com</string>
<key>NetworkProtocol</key>
<string>TCP</string>
<key>Port</key>
<string>443</string>
<key>Relevance</key>
<string>Essential</string>
<key>Purpose</key>
<string>PHP Monitor directly invokes Homebrew which contacts GitHub. This happens when PHP Monitor asks for more information about the PHP formula to determine which version of PHP you&apos;ve got running.</string>
<key>DenyConsequences</key>
<string>If you deny these connections, PHP Monitor might not be able to complete its preset set of instructions, causing version switching to fail.</string>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,2 @@
// Top-level, general application description:
"ApplicationDescription" = "PHP Monitor is a tool that shows the active PHP version in your menu bar and gives you easy access to certain PHP service actions and config files.";

View File

@ -17,15 +17,17 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>4</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 Nico Verbruggen. All rights reserved.</string>
<string>Copyright © 2021 Nico Verbruggen. All rights reserved.</string>
<key>NSMainStoryboardFile</key>
<string>Main</string>
<key>NSPrincipalClass</key>

133
phpmon/Localizable.strings Normal file
View File

@ -0,0 +1,133 @@
/*
Strings.strings
PHP Monitor
Created by Nico Verbruggen on 16/05/2020.
Copyright © 2020 Nico Verbruggen. All rights reserved.
*/
// MENU ITEMS (MI)
"mi_busy" = "PHP Monitor is busy...";
"mi_unsure" = "We are not sure what version of PHP you are running.";
"mi_php_version" = "You are running PHP";
"mi_php_switch" = "Switch to PHP";
"mi_php_broken_1" = "Oof! It appears your PHP installation is broken...";
"mi_php_broken_2" = "Try running `php -v` in your terminal.";
"mi_php_broken_3" = "You could also try switching to another version.";
"mi_php_broken_4" = "Running `brew reinstall php` (or for the equivalent version) might help.";
"mi_diagnostics" = "Diagnostics";
"mi_active_services" = "Active Services";
"mi_restart_php_fpm" = "Restart service: php";
"mi_restart_nginx" = "Restart service: nginx";
"mi_restart_dnsmasq" = "Restart service: dnsmasq";
"mi_manage_services" = "Manage services";
"mi_restart_all_services" = "Restart all services";
"mi_stop_all_services" = "Stop all services";
"mi_force_load_latest" = "Force load latest PHP version";
"mi_php_refresh" = "Refresh information";
"mi_configuration" = "Configuration";
"mi_limits" = "Limits Configuration";
"mi_memory_limit" = "Memory Limit";
"mi_post_max_size" = "Max POST";
"mi_upload_max_filesize" = "Max Upload";
"mi_valet_config" = "Locate Valet folder (.config/valet)";
"mi_php_config" = "Locate PHP configuration file (php.ini)";
"mi_global_composer" = "Locate global composer.json file (.composer)";
"mi_phpinfo" = "Show current configuration (phpinfo)";
"mi_detected_extensions" = "Detected Extensions";
"mi_no_extensions_detected" = "No additional extensions detected.";
"mi_preferences" = "Preferences...";
"mi_quit" = "Quit PHP Monitor";
"mi_about" = "About PHP Monitor";
// PREFERENCES
"prefs.title" = "PHP Monitor";
"prefs.close" = "Close";
"prefs.global_shortcut" = "Global shortcut:";
"prefs.dynamic_icon" = "Dynamic icon:";
"prefs.services" = "Services:";
"prefs.auto_restart_services_title" = "Auto-restart PHP-FPM";
"prefs_auto_restart_services_desc" = "When checked, will automatically restart PHP-FPM when\nyou check or uncheck an extension. Slightly slower when enabled, \nbut this applies the extension change immediately for all sites \nyou're serving, no need to restart PHP-FPM manually.";
"prefs.dynamic_icon_title" = "Display dynamic icon in menu bar";
"prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible.\nIf checked, it will display the major version number of the\ncurrently linked PHP version.";
"prefs.display_full_php_version" = "Display full PHP version in menu bar";
"prefs.display_full_php_version_desc" = "Display the full version instead of the major version only.\n(This may be undesirable on smaller displays,\nso this is disabled by default.)";
"prefs.shortcut_set" = "Set global shortcut";
"prefs.shortcut_listening" = "<listening for keypress>";
"prefs.shortcut_clear" = "Clear";
"prefs.shortcut_desc" = "If a shortcut combination is set up, you can toggle PHP Monitor\nwherever you are by pressing the key combination you chose.\n(Cancel choosing a shortcut by pressing the spacebar.)";
// NOTIFICATIONS
"notification.version_changed_title" = "PHP %@ now active";
"notification.version_changed_desc" = "PHP Monitor has switched to PHP %@.";
"notification.php_fpm_restarted" = "PHP-FPM automatically restarted";
"notification.php_fpm_restarted_desc" = "You toggled an extension, so PHP-FPM was automatically restarted.";
"notification.services_stopped" = "Valet services stopped";
"notification.services_stopped_desc" = "All services have been successfully stopped.";
"notification.services_restarted" = "Valet services restarted";
"notification.services_restarted_desc" = "All services have been successfully restarted.";
// ALERTS
// Force Reload Started
"alert.force_reload.title" = "PHP Monitor will force reload the latest version of PHP";
"alert.force_reload.info" = "This can take a while. You'll get another alert when the force reload has completed.";
// Force Reload Done
"alert.force_reload_done.title" = "PHP has been force reloaded";
"alert.force_reload_done.info" = "All appropriate services have been restarted, and the latest version of PHP is now active. You can now try switching to another version of PHP. If visiting sites still does not work, you may try running `valet install` again, this can fix a 502 issue (Bad Gateway).";
// PHP FPM Broken
"alert.php_fpm_broken.title" = "PHP-FPM configuration is incorrect";
"alert.php_fpm_broken.info" = "PHP Monitor has determined that there are issues with your PHP-FPM config: it's not pointing to the Valet socket. This will result in 502 Bad Gateway if you visit websites linked via Valet.\n\nYou can usually fix this by running\n`valet install`, which updates your\n PHP-FPM configuration.";
// PHP Monitor Cannot Start
"alert.cannot_start.title" = "PHP Monitor cannot start";
"alert.cannot_start.info" = "The issue you were just notified about is keeping PHP Monitor from functioning correctly. Please fix the issue and restart PHP Monitor. After clicking on OK, PHP Monitor will close.\n\nIf you have fixed the issue (or don't remember what the exact issue is) you can click on Retry, which will have PHP Monitor retry the startup checks.";
"alert.cannot_start.close" = "Close";
"alert.cannot_start.retry" = "Retry";
// PHP alias issue
"alert.php_alias_conflict.title" = "Homebrew `php` formula alias conflict detected";
"alert.php_alias_conflict.info" = "PHP Monitor has detected conflicting `php` aliases in your Homebrew setup, both of which have been detected as installed.\n\nThis will likely result in failed linking when switching PHP versions, and will break PHP Monitor functionality.\n\nFor more information, please visit: https://github.com/nicoverbruggen/phpmon/issues/54";
// STARTUP
/// 1. PHP binary not found
"startup.errors.php_binary.title" = "PHP is not correctly installed";
"startup.errors.php_binary_desc" = "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php` (or `/opt/homebrew/bin/php`). The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)";
/// 2. PHP not found in /usr/local/opt or /opt/homebrew/opt
"startup.errors.php_opt.title" = "PHP is not correctly installed";
"startup.errors.php_opt.desc" = "PHP alias was not found in `/usr/local/opt` or `/opt/homebrew/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php` in order for PHP Monitor to detect this installation.";
/// 3. Valet not installed
"startup.errors.valet_executable.title" = "Laravel Valet is not correctly installed";
"startup.errors.valet_executable.desc" = "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet` or `/opt/homebrew/bin/valet`. The app will not work correctly until you resolve this issue. (PHP Monitor checks for the existence of `valet` in either of these paths.)";
/// 4. Brew & sudoers
"startup.errors.sudoers_brew.title" = "Brew has not been added to sudoers.d";
"startup.errors.sudoers_brew.desc" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
/// 5. Valet & sudoers
"startup.errors.sudoers_valet.title" = "Valet has not been added to sudoers.d";
"startup.errors.sudoers_valet.desc" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
/// 6. Multiple services active
"startup.errors.services.title" = "Multiple PHP services are active";
"startup.errors.services.desc" = "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes.\n\nThe easiest solution is to choose the option 'Force load latest PHP version' in the menu bar.\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies).\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue.\n\nFor more information about this issue, please see the README.md file in the repository on GitHub.";

View File

@ -1,27 +0,0 @@
//
// ViewController.swift
// phpmon
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}