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

Compare commits

...

497 Commits

Author SHA1 Message Date
4568f03a65 🚀 Version 5.7.2 2023-01-30 19:54:09 +01:00
2eda8d6382 📝 Update about verbose logging 2023-01-30 19:24:46 +01:00
dd330fecce 🔧 Bump build 2023-01-30 19:12:03 +01:00
aaa7c636db 👌 Remove unneeded print() statement 2023-01-30 19:11:47 +01:00
ff75fb7be3 🐛 Fix version parsing (#227) 2023-01-30 19:11:08 +01:00
4d7b01831b Extra verbose logging
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose`

Once this file exists, you can find the latest log in: `~/.config/phpmon/last_session.log`.
2023-01-30 19:10:52 +01:00
0fceb852bb Add test to validate Valet version number with deprecations 2023-01-30 13:08:23 +01:00
9fb5f33770 🔧 Extra CLI mode (--cli) 2023-01-30 13:07:37 +01:00
2f658ee569 🚀 Version 5.7.1 2023-01-29 15:00:09 +01:00
ad179a325a 🔧 Bump build 2023-01-29 14:59:52 +01:00
4baeaea85f 🐛 Fix services not shutting down (#225) 2023-01-29 14:58:09 +01:00
9e25254ec8 🚀 Version 5.7 2023-01-26 20:49:47 +01:00
e623207844 🔧 New build 2023-01-26 20:32:39 +01:00
f60ecb877c ♻️ Cleanup 2023-01-26 20:30:00 +01:00
66393094b0 Correctly parse .valetrc files 2023-01-24 19:47:28 +01:00
b5d2fef184 🏗 WIP: Add test for feature to be implemented 2023-01-20 16:42:50 +01:00
44bc07c9dc 🏗 WIP: Parsing .valetrc file 2023-01-20 16:36:56 +01:00
93790f3951 👌 Update copyright message 2023-01-19 18:09:42 +01:00
d3b1afe9fd 🐛 Fix bug related to "?" not showing up 2023-01-19 18:03:00 +01:00
2fa50a7dc4 🔧 Bump build for new pre-release build 2023-01-19 17:33:55 +01:00
18b62ecc3f 👌 Adjust SECURITY and support matrix for Valet 4
This commit ensures that PHP Monitor knows about which versions of
PHP are supported by the upcoming Valet 4.0: PHP 7.1 and higher.

Ensures compatibility with https://github.com/laravel/valet/pull/1318
2023-01-19 17:30:21 +01:00
2d59b8c6e8 👌 Improve onboarding 2023-01-18 20:42:24 +01:00
450d7ec001 👌 Fixed onboarding for initial launch 2023-01-18 20:27:02 +01:00
b6b1174ca3 👌 More compact ServicesView 2023-01-18 19:56:54 +01:00
e509f6b59d 📝 TL QC Pass 1 2023-01-18 19:22:45 +01:00
6014320441 📝 Update information about supported PHP versions 2023-01-18 19:18:25 +01:00
a0d80423e9 📝 Update documentation
* Updated README
* Updated SECURITY
* Revert the minimum Valet version to v2.16
2023-01-18 19:10:43 +01:00
35b19efc3e 👌 Use specific theme colors for services status 2023-01-17 18:05:22 +01:00
9a175e7291 🔧 Bump build for new pre-release build 2023-01-16 21:10:58 +01:00
d53e92ce94 👌 Sort PHP versions, amend message 2023-01-16 21:09:25 +01:00
3344fdb1dc 👌 Update support matrix 2023-01-13 20:01:09 +01:00
020a0260f1 Tell users about older unsupported PHP versions 2023-01-13 19:48:02 +01:00
3ce7e8f48b 🔧 Keep track of unsupported (but installed) PHP versions 2023-01-13 19:13:43 +01:00
ae7e13de9b 🔧 Bump build for new pre-release build 2023-01-12 17:20:58 +01:00
c82ea1fac1 🐛 Fix concurrency crashes with @objc methods 2023-01-12 17:20:15 +01:00
d6258f54a9 🐛 Fix crash when Valet.shared is nil 2023-01-11 22:23:39 +01:00
e6d2c873a5 👌 Add modal to inform about helper scripts 2023-01-11 18:03:10 +01:00
300b10c5d8 🐛 Fix crash with check for updates 2023-01-11 17:50:55 +01:00
6eea08cd4f 👌 Finalize PHP Guard functionality 2023-01-10 17:53:55 +01:00
3c946a53e8 👌 Keep track of last used global PHP version 2023-01-09 19:19:18 +01:00
d8738b685f 👌 Get rid of warnings 2023-01-09 18:10:51 +01:00
55fc90bcf5 👌 Better alerts for error state 2023-01-08 13:14:18 +01:00
6a2f4d248c 👌 Improved error handling 2023-01-08 12:48:47 +01:00
a50eb04f3c 👌 Services now report error status 2023-01-08 11:50:01 +01:00
4cbfbeb4e5 👌 Add copy about inactive services 2023-01-07 23:54:01 +01:00
18dd597d38 👌 SwiftUI fixes 2023-01-07 19:01:31 +01:00
e5c80ab52f 👌 Resolve height issues 2023-01-07 18:54:07 +01:00
0b3a83c1e4 👌 Cleanup 2023-01-07 18:10:31 +01:00
6e7c0d827c 👌 Initial loading state for services manager 2023-01-07 17:16:51 +01:00
d05f39efe7 👌 Check if Valet version is supported 2023-01-07 13:24:52 +01:00
27894e4884 👌 Handle TODOs 2023-01-07 13:03:27 +01:00
f153fee05c Fix broken tests after test config using 8.2 2023-01-07 12:57:45 +01:00
71e1ed1b93 👌 Async switcher (Swift concurrency) 2023-01-07 12:53:27 +01:00
61ecefb6e7 👌 Use filesystem abstraction 2023-01-07 12:53:07 +01:00
422a7738bd 👌 Add fake services system 2023-01-06 21:50:34 +01:00
0beda388eb 👌 Cleanup 2023-01-06 21:33:51 +01:00
684a53fc4a 🏗 WIP: Functional service toggling 2023-01-06 20:30:25 +01:00
456948ffd9 🏗 WIP: Broken services toggling
You can break it by opening the menu twice.
2023-01-06 19:14:17 +01:00
a090cbc20b 👌 Revert loading 2023-01-06 18:34:39 +01:00
7b07520440 🏗 WIP: Service toggling 2023-01-06 18:33:55 +01:00
6d52992c9d 👌 Add comments about concurrency 2023-01-06 13:07:02 +01:00
291d20b2b3 🏗 WIP: Functional services list 2023-01-06 12:59:15 +01:00
3d505aebde 🚛 VersionNumber, PhpVersionNumberCollection 2023-01-06 12:41:37 +01:00
d854dee2a1 Fix tests related to version number parsing 2023-01-05 17:58:55 +01:00
67e8589834 ♻️ Alter how PHP version support is handled 2023-01-05 17:45:23 +01:00
a36c9b1563 📝 Update documentation 2023-01-05 12:17:31 +01:00
df1b1c5856 🏗️ Investigate event propagation 2023-01-03 20:17:29 +01:00
5af7214320 🏗️ Layout changes 2023-01-03 20:15:44 +01:00
e20d3ffd22 🏗️ WIP: Various fixes and improvements
- Fixed `brewPhpAlias` (must be configurable later)
- Added TODOs for where the filesystem abstraction is required
- Set `Homebrew.fake` early on when applying testable configuration
- Evaluate `FakeValetSite` compatibility again
- Never display sponsor alert when running tests
- Upgrade TestableConfiguration.working to use PHP 8.2
2023-01-03 19:29:44 +01:00
be70559d4c 🏗 WIP: Adjust dimensions 2023-01-03 16:58:48 +01:00
61988a141b 🏗 WIP: Center buttons and checkmarks 2023-01-03 16:14:45 +01:00
3bcf52bf0a 🏗 WIP: Use objectWillChange.send() 2023-01-03 16:11:55 +01:00
296bc486c4 🏗 WIP: Fake services manager 2022-12-24 15:17:47 +01:00
59f60b5013 🏗 WIP: Services manager main status
Please note that these are computed properties.
They should be computed when a service status
is modified, and should be `Published`.
2022-12-24 15:04:25 +01:00
de2c1aca5d 🏗 More SwiftUI experiments 2022-12-24 14:51:23 +01:00
44c1ea7de4 🏗 SwiftUI experiments 2022-12-23 19:20:04 +01:00
923f0237e8 🔥 WIP: Removed broken view 2022-12-20 19:42:27 +01:00
ff7c68ddfb 🏗 WIP: Services rewrite 2022-12-19 18:09:10 +01:00
bf2c0c259f 👌 Mastodon > Twitter 2022-12-19 17:35:32 +01:00
0fdd1264ed 👌 Await the outcome of cash conflict check 2022-12-19 17:30:56 +01:00
c85a8e3818 Add test for tapping on "add domain" 2022-12-18 14:50:03 +01:00
15fe5e4716 Add tests for domain list interaction 2022-12-16 20:50:53 +01:00
de6dea066e Use ValetInteractor to add links and proxies 2022-12-16 20:20:31 +01:00
ee230f3086 👌 Unify scanners 2022-12-15 22:28:15 +01:00
bc96b50630 Add fake isolation interaction 2022-12-14 20:28:49 +01:00
d6554ceea9 Add site isolation to ValetInteractor 2022-12-14 20:18:06 +01:00
4b04f70638 👌 Fix phrasing ("site is unsecured")
A site can be either insecure or unsecured. Insecure has a rather
negative connotation and is also a human trait. Unsecured is a better 
term in this particular case.
2022-12-13 23:49:55 +01:00
d49e74fab1 🚛 Move FakeValetInteractor to separate file 2022-12-13 23:47:59 +01:00
e34dadcb9b 👌 Reinstate UI busy for secure, add fake delay 2022-12-13 23:45:19 +01:00
a696a9a386 👌 Style fix 2022-12-13 23:32:50 +01:00
837392d606 Add unlinking sites for fake links 2022-12-13 23:32:19 +01:00
912d9e7423 Add FakeValetInteractor 2022-12-13 23:21:15 +01:00
cb98d40bef 👌 Fix crash with new secure/unsecure mechanism 2022-12-13 23:02:30 +01:00
1f165058b2 ♻️ Rework interaction with Valet commands 2022-12-13 22:56:36 +01:00
04c78eba35 👌 Generate a new JSON file for current dev env 2022-12-13 20:24:48 +01:00
4bfde7b062 👌 Add warning about https proxy subjects 2022-12-13 20:21:34 +01:00
00cc6711a1 🚀 Version 5.6.6 2022-12-10 13:04:16 +01:00
209244e162 🔧 Bump build 2022-12-09 18:27:19 +01:00
e48d8ed98b 👌 Add warning about https proxy subjects 2022-12-09 18:26:56 +01:00
834f76e5a1 📝 Updated README about PHP compatibility 2022-12-09 18:08:05 +01:00
5f39cd757a 📝 Update README 2022-12-09 18:05:31 +01:00
7e48893247 📝 Update README 2022-12-09 18:04:05 +01:00
073b7cf943 🏗 WIP: Interactions 2022-12-06 20:53:42 +01:00
31a0bb986f ♻️ Some odd refactoring 2022-12-06 19:43:45 +01:00
81183acea8 ♻️ Cleanup 2022-12-06 19:28:20 +01:00
e29ca9e1ad 🚀 Version 5.6.5 2022-11-27 14:37:15 +01:00
40e05d9445 🔧 Bump build 2022-11-27 14:04:15 +01:00
3ebf51b319 👌 Fix threading issue with Composer update (#212) 2022-11-27 14:03:46 +01:00
3f8739dc30 👌 Fix timing issue 2022-11-22 00:14:46 +01:00
c3e55df9fb WIP: Add support for nginx-full, formulae tweaks 2022-11-22 00:01:31 +01:00
4b8ad911f1 Add support for nginx-full (#211) 2022-11-21 18:10:33 +01:00
c6aa06842c 🔥 Remove reference to LatestStablePhpVersion 2022-11-18 19:18:36 +01:00
efd902b4f3 🚀 Version 5.6.4 2022-11-18 18:39:00 +01:00
918e272da7 🔧 Bump build 2022-11-18 18:37:31 +01:00
272a9182d3 🔥 Remove reference to LatestStablePhpVersion 2022-11-18 18:37:20 +01:00
63f85aff91 🐛 Fix issue when using services quick toggle 2022-11-18 18:32:38 +01:00
a801174f0a Fully cover RealFileSystem 2022-11-09 20:05:15 +01:00
fa5c843619 Add tests for RealFileSystem class 2022-11-08 20:14:10 +01:00
581c2d1974 📝 Update supported PHP versions in README 2022-11-07 20:45:58 +01:00
bc739e1982 👌 Add feature test for InternalSwitcher 2022-11-07 20:42:52 +01:00
a21d928a6c 📝 Update SECURITY.md 2022-11-02 21:22:49 +01:00
4deef64537 📝 Update SECURITY.md 2022-11-02 21:08:57 +01:00
bedabaa3bb 👌 PHP 8.2 release, PHP 8.3-dev support 2022-11-02 21:06:36 +01:00
76d96b3507 Fix issue with UI tests and .localizable 2022-11-02 21:05:12 +01:00
4de7179d1c 👌 Include brew (un)link commands for tests 2022-11-02 20:08:15 +01:00
f2d5b94831 Fix tests 2022-11-02 19:44:36 +01:00
ff5fdd82b1 👌 Add default system "www.conf" file 2022-11-01 17:11:55 +01:00
4f6bae87d4 Add Composer to testable configuration 2022-11-01 17:08:01 +01:00
ce44166b48 Added more tests, added to fake & real FS 2022-11-01 17:02:26 +01:00
5caca85d7a 👌 Fake FS: ~ and intermediate directories 2022-11-01 16:47:45 +01:00
fa2d2105c5 👌 Removed remaining FileManager.default usage 2022-11-01 14:11:34 +01:00
8417d637fe 👌 FileSystem changes, rework and testing 2022-11-01 13:47:16 +01:00
e8c85f93f9 👌 Change where scanners are initialized 2022-11-01 12:10:47 +01:00
24659d4385 👌 Bump recommended Valet version 2022-10-25 23:30:49 +02:00
786b59aa92 🐛 Handle empty output for brew info 2022-10-22 17:39:43 +02:00
9da3772212 🚀 Version 5.6.3 2022-10-22 17:34:53 +02:00
e62b03d070 👌 Style fix 2022-10-22 16:43:05 +02:00
9a11d2efed 🔧 Bump build 2022-10-22 16:42:26 +02:00
b134e62328 🐛 Handle empty output for brew info 2022-10-22 16:41:54 +02:00
771f8dc757 🏗 WIP for fake site enhancements 2022-10-21 19:45:42 +02:00
e18db4eadd 👌 Mark determineVersion as throws 2022-10-21 19:31:05 +02:00
1ece5c34bf Correctly parse pre-release PHP versions 2022-10-21 19:27:31 +02:00
04ed29bc9f 👌 Fix warnings 2022-10-21 18:19:31 +02:00
ac2184ba97 👌 Add brew tap homebrew/services instruction
This now recommends the appropriate solution for #208.
2022-10-20 20:44:54 +02:00
5c69133c42 👌 Add brew tap homebrew/services instruction
This now recommends the appropriate solution for #208.
2022-10-20 20:44:44 +02:00
8310cf2729 👌 Touching up correctness of process handling 2022-10-19 14:57:28 +02:00
696f9bf351 👌 Various async improvements 2022-10-19 14:27:39 +02:00
17b1329d71 👌 Cleanup 2022-10-19 13:52:11 +02:00
4bfa98fc20 ♻️ Refactor DispatchQueue to new Task API 2022-10-19 13:44:53 +02:00
658cec27c1 👌 Cleanup 2022-10-18 23:44:18 +02:00
1c0d9f6826 👌 Swift 6 compatibility 2022-10-18 23:41:46 +02:00
507d7d5b23 Fix PHP version detection requirement 2022-10-18 16:43:17 +02:00
4b8b46a822 Fix test 2022-10-18 14:12:57 +02:00
ea5dd3bc46 👌 Snake case for tests 2022-10-18 14:11:55 +02:00
83657fee6f Tests are final 2022-10-16 15:37:17 +02:00
4c752b6a15 Localization support for in test files 2022-10-16 15:13:13 +02:00
5e3e0c087b 👌 Read configuration from JSON file
This allows us to alter the configuration prior to launching the app,
which allows for additional flexibility during testing.
2022-10-16 14:35:19 +02:00
273070ef27 ️ Sped up and improved UI test 2022-10-15 16:37:33 +02:00
cb3208c008 Real UI test 2022-10-15 15:36:03 +02:00
d401fe997d Make UI test actually functional 2022-10-15 15:14:49 +02:00
eaf6ef658f 🚛 Moved tests around, added Feature, UI tests 2022-10-14 18:03:14 +02:00
d91e16d674 Add test plan, fix unit tests 2022-10-14 17:10:58 +02:00
008603b8c3 👌 Fix project structure 2022-10-14 16:55:37 +02:00
728274aaca 👌 Fix warnings 2022-10-14 16:54:11 +02:00
daaece3cfa Added fake commands 2022-10-12 23:25:06 +02:00
eaa74b7141 👌 Annotate configurations 2022-10-12 22:50:06 +02:00
8a6656d3e2 👌 Improve logging 2022-10-12 22:40:48 +02:00
ad46f51d73 👌 Use fake filesystem 2022-10-12 22:38:03 +02:00
12a4efc775 👌 Improvements to BetterAlert, apply() configs
- MainActor fixes for BetterAlert
- Added `apply` for TestableConfiguration
2022-10-12 22:19:36 +02:00
ec4c4df5fd Add preference to disable TLD alert (#206) 2022-10-12 18:52:44 +02:00
f4448e0640 🔧 Bump version number 2022-10-10 21:49:51 +02:00
7fd30d7c54 Add preference to disable TLD alert (#206) 2022-10-10 21:49:43 +02:00
2c57dea97f 🔀 Merge branch 'main' into dev/5.6 2022-10-09 22:00:36 +02:00
a77fa5557a 📝 Update README for Login Items on Ventura 2022-10-09 21:56:13 +02:00
b0f27dcfa5 ♻️ Filesystem to FileSystem / ActiveFileSystem 2022-10-08 01:04:51 +02:00
f5d2ec2b7e 👌 Add delay global function 2022-10-08 00:27:29 +02:00
6db5cdec25 🐛 Introduce SLOW SHELL and fix a few issues 2022-10-07 22:55:48 +02:00
03cf4ef3e4 👌 Added various TODO items 2022-10-06 23:35:14 +02:00
6feb8118d9 🚛 Move files around 2022-10-06 23:34:31 +02:00
45a82b2c9e 🏗 Checked and fixed various Task { } blocks 2022-10-06 23:29:13 +02:00
ed3622cc4e Fix tests 2022-10-06 23:00:21 +02:00
e2ab7f08ed 🏗 Remove LegacyShell entirely 2022-10-06 22:55:05 +02:00
ad41e3891e 🏗 Mark old Shell as deprecated from now on 2022-10-05 22:39:36 +02:00
108ae05c1d 🏗 Use new shell when parsing apps 2022-10-05 22:38:54 +02:00
fb1ca60240 Fix tests, new shell in various places 2022-10-05 01:07:09 +02:00
3267dc8add ♻️ Use new Shell.pipe to replace legacy shell 2022-10-05 00:38:17 +02:00
1fd7db15a7 🏗 Testable terminal output 2022-10-04 23:42:43 +02:00
0b33116eb0 🏗 Fake shell in use 2022-10-04 19:39:34 +02:00
2c25bcbdb5 🏗 Ensure Shellable has PATH 2022-10-04 19:00:33 +02:00
8a6139d5e7 🏗 Remove synchronous terminal commands 2022-10-04 18:40:41 +02:00
953ccb3792 🏗 WIP 2022-10-04 17:57:05 +02:00
c26c491340 🏗 WIP 2022-10-03 22:27:50 +02:00
c9a5cd3a9f 🏗 WIP 2022-10-03 19:27:14 +02:00
45704fc736 🚀 Version 5.6.2 2022-10-02 13:28:58 +02:00
f28354e634 🐛 Use valet secure sitename (#197) 2022-10-02 13:28:01 +02:00
86eb295489 👌 Add runComposerUpdateShellCommand method 2022-09-30 23:45:32 +02:00
572330eaa1 👌 Remove reference to singleton 2022-09-30 23:44:16 +02:00
5ebafdb4e3 👌 Shell tweaks, fix ComposerWindow async issue 2022-09-29 19:08:40 +02:00
ffffcad84b 🐛 Fix ComposerWindow deinit not firing 2022-09-29 19:00:59 +02:00
8055a32bde 🐛 Fix ComposerWindow deinit not firing 2022-09-29 18:50:40 +02:00
4c11fae541 🏗 WIP: Run shell commands in parallel 2022-09-28 22:18:16 +02:00
99da328921 🏗 WIP: Even better Shell functionality 2022-09-28 21:43:18 +02:00
bbac2632a2 🏗 WIP: Much improved Shell protocol 2022-09-28 21:28:51 +02:00
513a86ec39 🐛 Fix an issue with missing separator item 2022-09-28 18:24:34 +02:00
5b3054326e 🔧 Bump version number 2022-09-28 18:24:26 +02:00
e7f3c7e59c 🐛 Fix an issue with missing separator item 2022-09-28 18:24:01 +02:00
a59efb7fce 🏗 WIP: Shell rework 2022-09-27 22:27:33 +02:00
3483569410 🏗 WIP: Shell rework 2022-09-27 20:26:11 +02:00
5399bddfeb 🏗 WIP: Shell rework 2022-09-26 20:37:24 +02:00
a407515534 🚀 Version 5.6.1 2022-09-24 12:56:14 +02:00
a682d0cfb0 🐛 Merge fixes from 'dev/5.6' into dev/6.0 2022-09-23 16:48:47 +02:00
b827ffb869 🔧 Bump version number 2022-09-23 16:46:23 +02:00
bdb718598e 🐛 Various bugfixes
- Fixes issue with `scanApps` being non-optional in custom configuration
- Fixes issue with position of separator if Xdebug is not detected
- Ensure that `isRunningSwiftUIPreview` modifier always return false for debug builds
2022-09-23 16:46:13 +02:00
ddfc73e033 🐛 Resolve issue with determining PATH (#194)
In previous builds, PHP Monitor would use interactive mode when opening
a /bin/zsh shell, in order to be able to load the full PATH.

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

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

I chose option 2, which is the more robust choice. If no .zshrc file
exists, it is also not sourced to avoid warnings or errors from ending
up in the PATH.
2022-09-23 16:44:06 +02:00
cfae520984 👌 Correctly resolve tagged version (#195) 2022-09-22 18:43:09 +02:00
e871a00490 👌 Add additional commentary to new shell classes 2022-09-21 21:42:22 +02:00
1d396202db 👌 Add PATH to SystemShell 2022-09-21 21:31:00 +02:00
39769d815f 🚛 Move around files 2022-09-21 21:06:11 +02:00
90a69338f7 🏗 Add additional test 2022-09-20 20:53:15 +02:00
3f25759d4f 🏗 Fix SwiftLint, WIP shell rework 2022-09-20 20:49:29 +02:00
4494a0555f 🚧 WIP: Shell rework 2022-09-20 00:33:58 +02:00
0d86f3ded6 📝 Update README, contribution guidelines 2022-09-18 14:09:36 +02:00
0c176493e5 🔀 Merge branch 'main' into dev/5.6 2022-09-18 14:08:48 +02:00
71da62f954 📝 Update contribution guidelines 2022-09-18 14:08:10 +02:00
d6781568a3 📝 Update README 2022-09-18 13:39:40 +02:00
fc27131cca 👌 Add support for <= and < version constraints
I am entirely unsure why one would need these, but I figured I'd get
these in the app before I start the work on PHP Monitor 6.0.

This ensures all common version constraints can now be parsed correctly.
2022-09-18 00:06:37 +02:00
c9c7e14416 👌 Add support for <= and < version constraints
I am entirely unsure why one would need these, but I figured I'd get
these in the app before I start the work on PHP Monitor 6.0.

This ensures all common version constraints can now be parsed correctly.
2022-09-18 00:05:37 +02:00
bb124bd0ee 👌 Cleanup and removal of unneeded dump 2022-09-17 23:16:49 +02:00
c35e7781f4 🔥 Remove unneeded plist file 2022-09-17 23:14:39 +02:00
0947dc5ecc 🚀 Version 5.6 2022-09-16 19:33:42 +02:00
286cdd00e9 🔧 Prepare for release 2022-09-16 19:31:32 +02:00
61528cea46 🏗 WIP 2022-09-14 19:05:14 +02:00
883f5a1a5d 🔀 Merge branch 'dev/5.6' into dev/6.0 2022-09-10 21:46:17 +02:00
42b79d3cb3 🔧 Upgrade to Xcode 14 2022-09-10 21:44:17 +02:00
36aa41568c 🐛 Fix issue with minimum width w/ hidden UI 2022-09-10 21:43:26 +02:00
95729c5315 ♻️ Single target, multiple configurations 2022-09-10 21:21:58 +02:00
f02e45486e ♻️ Refactor ActivePhpInstallation 2022-09-09 22:18:52 +02:00
6ddddc744a 🔧 Add target for PHP Monitor SE 2022-09-09 21:46:20 +02:00
273c51f702 🔧 Update credits URLs 2022-09-09 21:41:20 +02:00
186f80c90e Allow more UI tweaking 2022-09-08 18:14:22 +02:00
d93af814c9 👌 Fix extension visibility of NSMenu 2022-09-08 17:21:41 +02:00
f4885f7dbc 👌 Update pre-release PHP version notice 2022-09-08 17:20:30 +02:00
805c9f5e6a Add new Preferences panel for UI tweaking 2022-09-07 21:40:43 +02:00
183d0bbc30 Allow hiding of global version switcher 2022-09-07 21:14:56 +02:00
4855c14d28 ♻️ Refactor Preferences & PreferenceName
Also added a few new preferences related to toggling specific menu items
based on your personal choices.

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

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

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

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

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

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

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

    ~/.config/phpmon/preset_revert.json

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

However, there are some performance concerns here. Perhaps it is worth
*not* reloading the contents of these files unless absolutely necessary.
2022-05-04 20:25:59 +02:00
2987464da8 📝 Added information about linter 2022-05-03 18:20:11 +02:00
4d04275c57 Added linting 2022-05-03 18:16:26 +02:00
790f63e8c9 🔧 Disable Xdebug item for 5.3 2022-05-02 18:26:02 +02:00
86b49812c3 ♻️ Cleanup menu item generation (#168) 2022-05-02 18:24:44 +02:00
ef9e0fd916 Begin work on Xdebug mode switcher (#168) 2022-05-01 22:07:18 +02:00
af8807f799 🔀 Merge bugfixes from 5.2 into 5.3 2022-04-23 12:25:18 +02:00
4eea13f059 🚀 Version 5.2.2
Merge branch 'dev/5.2'
2022-04-22 19:50:57 +02:00
ba93ed93e4 Allow opening of proxies in browser 2022-04-21 19:18:05 +02:00
932a0fe176 Add 'Remove Proxy' to right-click menu 2022-04-21 19:08:40 +02:00
3cff2d6469 🐛 Fix issue w/ flag evaluation order (#165) 2022-04-20 22:31:20 +02:00
df506e4128 🐛 Fix various minor issues (discovered via #162)
- Renaming the configuration files from `www.conf` to the backup
  (`disabled-by-phpmon`) will now succeed if the `disabled-by-phpmon`
  file already exists. This would fail if the `disabled-by-phpmon` file
  already existed in previous builds.

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

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

- Updated tests to reflect these changes to `PhpVersionNumber`.
2022-04-20 12:19:02 +02:00
eb80214785 ✏️ Updated copy 2022-04-19 20:39:51 +02:00
80a4e361a4 🔧 Prepare for new DEV build 2022-04-19 20:33:19 +02:00
2af88b2bee ✏️ Update copy about non-standard TLDs 2022-04-19 20:26:44 +02:00
5048ccab8c 👌 Update various TODO items 2022-04-18 12:00:54 +02:00
66d13c92d5 Run the valet proxy command (#105) 2022-04-18 11:59:14 +02:00
836b076da9 👌 Cleanup, ensure dynamic form works correctly 2022-04-17 14:33:59 +02:00
1a75838a3b Set up proxy view strings and outlets 2022-04-17 14:02:44 +02:00
a18b7962a7 ♻️ Fix link 2022-04-16 23:21:29 +02:00
84548634ec Add proxy view 2022-04-16 23:19:13 +02:00
419ebe61f7 Add selection view 2022-04-14 14:56:51 +02:00
c45817b127 Added new UI for proxies to storyboard 2022-04-13 19:14:08 +02:00
2c0c0c5a11 Correctly detect secured proxies 2022-04-12 20:43:57 +02:00
1b8d6311ba ♻️ Refactor displaying domains 2022-04-12 17:36:18 +02:00
f0f7a3f7d6 👌 Scan proxies (#105) 2022-04-11 22:56:40 +02:00
8304d774c3 ♻️ Refactoring of files and tests 2022-04-02 15:48:21 +02:00
faeea4e866 Ensure normal nginx file does not have proxy 2022-03-31 18:28:47 +02:00
6470daf7d3 Added test to parse the proxy address 2022-03-31 18:27:26 +02:00
94139a3669 🚚 Moved test files into separate directories 2022-03-31 18:09:50 +02:00
f3b1172d0e 🚀 Version 5.2.1
This is a bugfix release that fixes the isolation command (#158).
2022-03-31 13:51:46 +02:00
8057019898 🐛 Fix isolation command (#158) 2022-03-31 13:35:23 +02:00
9b59fc5dae 👌 Cleanup proxies 2022-03-31 13:34:56 +02:00
75f4377de8 Added tooltips next to PHP version 2022-03-31 13:29:04 +02:00
d3657716c4 ♻️ Added ValetProxy struct 2022-03-30 21:26:53 +02:00
a13990b96f ♻️ Rename SiteList to DomainList 2022-03-30 19:19:36 +02:00
4c7aa7fead Add UI for displaying proxies (#105) 2022-03-29 21:40:10 +02:00
32278533bf 🚀 Version 5.2
Merge branch 'dev/5.x'
2022-03-29 17:32:26 +02:00
39908f7fc5 🔧 Bump build 2022-03-29 17:32:09 +02:00
347d79c88d 👌 Cleanup 2022-03-29 17:25:47 +02:00
9bc117e9f5 🔥 Remove icon 2022-03-29 17:18:44 +02:00
ba5fbed9be 📝 Add logo 2022-03-29 17:18:26 +02:00
211556d5ce 📝 Include system administrator requirement 2022-03-29 14:16:28 +02:00
066d7bc217 🐛 Restoring Homebrew permissions uses admin group now 2022-03-29 14:14:39 +02:00
f072ceae37 📝 Updated FAQ 2022-03-29 09:55:38 +02:00
273dac1ca7 📝 Updated README with PHP upgrade instructions 2022-03-29 09:51:56 +02:00
f3b170ba14 🔧 Prepare for release 2022-03-29 09:23:57 +02:00
78d8030ed6 Fix tests 2022-03-25 23:55:41 +01:00
a300d2f4cf 👌 Show isolation is unavailable 2022-03-25 23:54:32 +01:00
96a658073e 📝 Update README and SECURITY 2022-03-25 17:26:58 +01:00
8395ba407d 👌 Use new, cleaner layout for screenshot 2022-03-25 17:11:40 +01:00
f80e3fed2b 👌 Disable border on table 2022-03-25 16:40:54 +01:00
b48edf7409 📝 New screenshot in README 2022-03-23 19:50:58 +01:00
e0a0eb089d 🔧 Prepare for a new dev build 2022-03-23 18:52:54 +01:00
60b126333d 👌 Extra startup check for invalid config.json
- Up to 50 sites are now preloaded (up from 30)
- No longer crash when invalid config.json is found (only at launch)
- Added `evaluateFeatureSupport` to Valet.swift
- Load configuration during launch checks instead
2022-03-23 18:46:35 +01:00
649a3f4fb5 👌 Handle correct version number 2022-03-23 18:15:17 +01:00
9a326928f3 🐛 Ensure unknown driver is at bottom 2022-03-21 18:45:41 +01:00
52f87ca18a 👌 Add tooltips to First Aid menu items 2022-03-21 18:33:25 +01:00
ad2d2cb57f Allow sorting of site list 2022-03-21 18:33:12 +01:00
22c0021ada 🐛 Workaround broken JSON file (AddSiteVC.swift:66) 2022-03-20 15:56:07 +01:00
2cfc5731fb 👌 Re-enable debugger, site loading 2022-03-20 15:07:34 +01:00
1d74e1536c Add default site to site list (#125) 2022-03-20 14:38:58 +01:00
e6b2ddf2ad 👌 Fix "WordPress" name in fake site window 2022-03-19 15:54:17 +01:00
62fda6224e 👌 Updated fake sites 2022-03-19 15:21:50 +01:00
083f8ebec8 Add marketing mode 2022-03-19 15:15:03 +01:00
aec8fb1168 🔧 Prepare for a new dev build 2022-03-18 18:38:11 +01:00
d5d9b38ed6 ♻️ Reworked "Fix My Valet" to use built-in switcher 2022-03-18 18:36:49 +01:00
9a6975e3d9 ♻️ Include HotKey source code 2022-03-18 18:21:55 +01:00
d1c40f2eb5 🐛 Fix issue with generator 2022-03-18 15:54:53 +01:00
ad58661449 🔥 Cleanup 2022-03-17 23:32:40 +01:00
1d6cfd419e Fix tests 2022-03-17 23:32:13 +01:00
15182ea15a 👌 Cleanup writing helper files 2022-03-17 23:28:03 +01:00
c43e00c88d 👌 Add source_php{version} helpers 2022-03-17 21:21:05 +01:00
25c7004368 🔥 Remove impossible functionality 2022-03-17 20:20:23 +01:00
02ba57cd64 🏗 TODO: Automatic isolated PHP handling 2022-03-17 19:54:13 +01:00
c2dc6302c0 ️ Fix performance issue with async 2022-03-17 19:18:48 +01:00
af9f30a123 ️ Fix performance bottleneck related to view drawing 2022-03-17 18:38:26 +01:00
28c5754800 🐛 Disable www.conf file when switching versions (#152) 2022-03-17 18:03:00 +01:00
48c1d48573 👌 Add support for Typo3 (#153) 2022-03-17 17:49:10 +01:00
582bf0e12c 🐛 Fix storyboard 2022-03-17 17:48:50 +01:00
46b30bbff4 🐛 Ensure isolation is available 2022-03-16 23:36:53 +01:00
372011ca08 Add site isolation option to context menu 2022-03-16 23:30:26 +01:00
7255792910 ♻️ WIP: Various changes 2022-03-16 22:17:17 +01:00
0c96b11b05 ♻️ WIP: Modified layout 2022-03-16 19:40:01 +01:00
ea4da12d3b ♻️ WIP: Cell rendering in site list 2022-03-16 19:15:57 +01:00
8419ebad10 👌 Adjust internal switcher 2022-03-16 18:04:45 +01:00
09a5cf836a 📝 Added TODO comments 2022-03-15 22:40:40 +01:00
1a1a53b472 ♻️ Preliminary refactor for Valet 3.0 (#148) 2022-03-15 22:39:46 +01:00
a8bad8447a 👌 isolatedVersion as a lazy property 2022-03-15 18:27:38 +01:00
ca8f5a8fbe Speed up nginx parsing 2022-03-15 18:16:28 +01:00
a0e7aec228 Parse nginx files + tests 2022-03-14 23:35:08 +01:00
26badc759e 🏗 Conditional PHP 5.6 support 2022-03-14 21:31:30 +01:00
e21c2168ea 🏗 Add isolation icon 2022-03-14 19:39:04 +01:00
589ab3664e 🏗 Add feature flag based on Valet version 2022-03-14 19:19:14 +01:00
48b4f9b160 🏗 Added initial TODO items for #148 2022-03-14 19:13:38 +01:00
279 changed files with 18255 additions and 4987 deletions

View File

@ -29,5 +29,10 @@ If applicable, add screenshots to help explain your problem.
- OS: [e.g. macOS Monterey]
- PHP Monitor version [e.g. v5.0.1]
**Additional log**
You can help me figure out even more information by sending me your verbose log for your latest session of PHP Monitor. Logging is disabled by default.
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor. You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it here!
**Additional context**
Add any other context about the problem here.

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

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

View File

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

15
.swiftlint.yml Normal file
View File

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

View File

@ -1,5 +1,29 @@
# DEVELOPER README
## ✅ Linting
This project uses the [SwiftLint](https://github.com/realm/SwiftLint) linter. You must install it and can run it like so:
```
swiftlint
```
It also automatically runs when you try to build the project. You'll get a warning if `swiftlint` is not installed, though. You can attempt to automatically fix issues:
```
swiftlint --fix
```
## ⚙️ Preferences
You can find the persisted configuration file in `~/Library/Preferences/com.nicoverbruggen.phpmon.plist`
These values are cached by the OS. You can clear this cache by running:
```
defaults delete com.nicoverbruggen.phpmon && killall cfprefsd
```
## 🔧 Build instructions
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
@ -27,6 +51,12 @@ If you'd like to create a production build, choose "Any Mac" as the target and s
10. Update Cask with new version + hash
11. Check new version can be installed via Cask
## 🍱 Marketing Mode
You can enable marketing mode by setting the `PHPMON_MARKETING_MODE` environment variable. It preloads a list of (fake) domains in the domain window list for screenshot & marketing purposes.
launchctl setenv PHPMON_MARKETING_MODE true
## 🐛 Symbolication of crashes
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Nico Verbruggen
Copyright (c) 2019-2023 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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug.Dev"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
BuildableName = "Unit Tests.xctest"
BlueprintName = "Unit Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7BB28F9B90F0021E251"
BuildableName = "UI Tests.xctest"
BlueprintName = "UI Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7AC28F9B4940021E251"
BuildableName = "Feature Tests.xctest"
BlueprintName = "Feature Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug.Dev"
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>
<CommandLineArguments>
<CommandLineArgument
argument = "--v"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--cli"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_working.json"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_broken.json"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "EXTREME_DOCTOR_MODE"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release.Dev"
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.Dev">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release.Dev"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
LastUpgradeVersion = "1400"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@ -27,14 +27,42 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:PHP Monitor.xcodeproj/PHP Monitor.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
BuildableName = "phpmon-tests.xctest"
BlueprintName = "phpmon-tests"
BuildableName = "Unit Tests.xctest"
BlueprintName = "Unit Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7AC28F9B4940021E251"
BuildableName = "Feature Tests.xctest"
BlueprintName = "Feature Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7BB28F9B90F0021E251"
BuildableName = "UI Tests.xctest"
BlueprintName = "UI Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
@ -61,6 +89,29 @@
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "--v"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "EXTREME_DOCTOR_MODE"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SLOW_SHELL_MODE"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</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 = "Unit Tests.xctest"
BlueprintName = "Unit 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"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

309
README.md
View File

@ -1,47 +1,62 @@
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project.
> You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy.<br>**Thank you!** ⭐️
> **Note**
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider [sponsoring](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️
<h1 align="center"><b>PHP Monitor</b> (phpmon)</h1>
<p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p>
<p align="center">
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
</p>
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this</u>.
<img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot50.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: Showing the key functionality of PHP Monitor. You can also add new domains as links, manage various services, and perform First Aid to fix all kinds of common PHP link issues.</i></small>
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
You can also add new domains as links, isolate sites, manage various services, and perform First Aid to fix all kinds of common PHP link issues.
## 🖥 System requirements
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
* macOS 11 Big Sur or later
* 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.16.2 or higher (older versions might be compatible but are not supported)
* Homebrew `php` formula is installed
* Laravel Valet (works with Valet v2, v3 and v4)
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`._
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
For more information, please see [SECURITY.md](./SECURITY.md) to find out which version of the app is currently supported.
## 🚀 How to install
Again, make sure you have **Laravel Valet** installed first. Once that's done, you can install via Homebrew (recommended), or may download the latest release on GitHub.
Again, make sure you have **[Laravel Valet](https://laravel.com/docs/master/valet)** installed first:
```sh
composer global require laravel/valet
valet install
valet trust
```
Once that's done, you can install PHP Monitor via Homebrew (recommended), or (alternatively) you may download the latest release on GitHub.
To install via Homebrew, run:
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
```sh
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
```
To upgrade your existing installation, run:
brew upgrade phpmon
```sh
brew upgrade phpmon
```
(You may need to run `brew update` or `brew update-reset` first in order to update the cask file if you ran a Homebrew operation recently.)
@ -78,20 +93,43 @@ If you're still having issues, here's a few common questions & answers, as well
<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>
All stable and supported PHP versions are also supported by PHP Monitor. However, depending on which version of Valet you have installed, which versions of PHP that are made available for switching purposes may differ.
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.
> **Note**
> If you have versions of PHP installed that can be detected by PHP Monitor but is *not* supported by the currently active version of Valet, you will be alerted by an item in the menu with an exclamation mark emoji. (⚠️)
Backports are available via [this tap](https://github.com/shivammathur/homebrew-php). For more information about those backports, please see the next FAQ entry.
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
</details>
<details>
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
You can install other supported versions of PHP out of the box, so `php@8.0` and `php@8.1` at the time of writing.
If you wish to install older (officially unsupported) versions of PHP for local use, you can do so by using [Shivam Mathur's tap](https://github.com/shivammathur/homebrew-php):
```sh
brew tap shivammathur/php
```
You may find that this tap is already in use: if you've used Valet before, it automatically uses this tap for legacy versions of PHP.
```sh
brew install shivammathur/php/php@7.4
brew install shivammathur/php/php@7.3
brew install shivammathur/php/php@7.2
brew install shivammathur/php/php@7.1
brew install shivammathur/php/php@7.0
```
**Always make sure to restart PHP Monitor after installing or upgrading PHP versions!**
> *Note*: Using this tap may cause [temporary alias conflicts](https://github.com/nicoverbruggen/phpmon/issues/54#issuecomment-979789724) while the core tap alias and the tap's alias refer to a different version of PHP, but this is generally speaking a minor inconvenience, since this normally only applies when a new PHP version releases.
</details>
<details>
@ -105,14 +143,14 @@ Super convenient!
<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:
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it.
Install [Homebrew](https://brew.sh) first.
**I have also created [a video tutorial](https://www.youtube.com/watch?v=fO3hVhkvm3w) which may be easier to follow. If you just want the terminal commands, keep reading.**
Install PHP, composer, add to path:
Install [Homebrew](https://brew.sh) first. Follow the instructions there first!
Then, you'll need to set up your PATH.
brew install php
brew install composer
nano .zshrc
Make sure the following line is not in the comments:
@ -125,30 +163,93 @@ 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, but add this BEFORE the homebrew PATH additions:
and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
If you're adding composer and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
export PATH=$HOME/bin:/usr/local/bin:$PATH
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
If you are *not* on Apple Silicon, you should remove the third line.
Install the `php` and `composer` formulae:
brew install php composer
Make sure PHP is linked correctly:
which php
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php`)
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon)
composer global require laravel/valet
For optimal results, you should lock your PHP platform for global dependencies to the oldest version of PHP you intend to run. If that version is PHP 7.0, your `~/.composer/composer.json` file could look like this (please adjust the version accordingly!):
```
{
"require": {
"laravel/valet": "^3.0",
},
"config": {
"platform": {
"php": "7.0"
}
}
}
```
Run `composer global update` again. This ensures that when you switch to a different global PHP version, [Valet won't break](https://github.com/nicoverbruggen/phpmon/issues/178). If it does, PHP Monitor will let you know what you can do about this.
Then, install Valet:
valet install
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.
You can now install PHP Monitor, if you haven't already:
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
</details>
<details>
<summary><strong>How frequently does PHP Monitor check for updates?</strong></summary>
PHP Monitor will check if an update is available every time you start the app.
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". You can always check for updates manually.
</details>
<details>
<summary><strong>I have PHP Monitor installed, and it works. I want to upgrade my PHP installations to the latest version, what's the best way to do this?</strong></summary>
It's easy to make a mistake here, and end up with an unlinked version of PHP or have versions missing from PHP Monitor.
Here's what I usually do:
* Open PHP Monitor and select **First Aid & Services** > **Restore Homebrew Permissions**.
* Close PHP Monitor after the pop-up tells you the permissions were restored.
* Run `brew update-reset`
* Run `brew upgrade`
If after this, any PHP versions are missing in PHP Monitor, please run the following for the versions that are missing:
* Run `brew uninstall php@x.x` (where `x.x` is the version)
* Run `brew cleanup` (if you get any permission issues you may need to manually clean up the folder)
* Run `brew install php@x.x` (where `x.x` is the version)
You may still need to run `brew link php` after upgrading, too.
That's it. Now start up PHP Monitor again and you should be golden!
</details>
<details>
@ -203,6 +304,12 @@ You should see an error or a warning here in the output.
Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling).
</details>
<details>
<summary><strong>The option to isolate a site is disabled! What's going on?</strong></summary>
Make sure you have at least **Valet 3.0** installed, since support for isolation was added in this version of Valet. (Please note that this version of Valet drops support for PHP 5.6.)
</details>
<details>
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
@ -251,9 +358,102 @@ PHP Monitor is a universal app and supports both architectures, so [find out her
<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.
The app will automatically check for updates, which is the most likely culprit.
PHP Monitor itself doesn't do any network requests. Feel free to check the source code or intercept the traffic, if you don't believe me.
This happens at launch (unless disabled), and the app directly checks the Caskfile hosted on GitHub. This data is not, and will not be used for analytics (and, as far as I can tell, cannot).
I also can't prevent `brew` from doing things via the network when PHP Monitor uses the binary.
The app includes an Internet Access Policy file, so if you're using something like Little Snitch there should be a description why these calls occur.
</details>
<details>
<summary><strong>How do I various presets to show up?</strong></summary>
You must set these presets up in a JSON file, located in `~/.config/phpmon/config.json`.
You must have set up at least one valid preset for this presets to work in PHP Monitor.
Here's an example of a working preset:
<pre>
{
"scan_apps": [],
"services": [],
"presets": [
{
"name": "Legacy Project",
"php": "8.0",
"extensions": {
"xdebug": false
},
"configuration": {
"memory_limit": "128M",
"upload_max_filesize": "128M",
"post_max_size": "128M"
}
}
],
"export": {}
}
</pre>
You can omit the `php` key in the preset if you do not wish for the preset to switch to a given PHP version.
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
<details>
<summary><strong>How do I ensure additional Homebrew services are shown in the app?</strong></summary>
You must set these services up in a JSON file, located in `~/.config/phpmon/config.json`.
You can specify custom services in the configuration file for Homebrew services that run as your own user (not root).
> **Info**
> If your service must run as root, it cannot currently be added to PHP Monitor.
You can find out which services are available by running `brew services list`.
Here's an example where we add the `mailhog` and `mysql` services to PHP Monitor:
<pre>
{
"scan_apps": [],
"services": ["mailhog", "mysql"],
"presets": [],
"export": {}
}
</pre>
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
<details>
<summary><strong>How do I set custom environment variables?</strong></summary>
You must configure these custom environment variables up in a JSON file, located in `~/.config/phpmon/config.json`.
PHP Monitor uses a default Shell environment, with no custom environment variables. You need to set custom environment variables manually. These are then used for e.g. Composer.
Here's an example of a working `COMPOSER_HOME` environment variable which is respected:
<pre>
{
"scan_apps": [],
"services": [],
"presets": [],
"export": {
"COMPOSER_HOME": "/absolute/path/to/composer/folder"
}
}
</pre>
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
@ -268,7 +468,7 @@ All of these apps should just be detected correctly, no matter their location on
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
You can add your own apps by creating and editing a `~/.phpmon.conf.json` file, with the following entry:
You can add your own apps by creating and editing a `~/.config/phpmon/config.json` file, and make sure the `scan_apps` key is set:
<pre>
{
@ -277,6 +477,9 @@ You can add your own apps by creating and editing a `~/.phpmon.conf.json` file,
</pre>
You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary.
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
<details>
@ -350,6 +553,10 @@ If you would like to report a crash, please include the associated **log files**
To find the logs, take a look in `~/Library/Logs/DiagnosticReports` (in Finder) and see if there's any (log) files that start with "PHP Monitor".
Additionally, you can help me figure out even more information by sending me your verbose log for your latest session of PHP Monitor. Logging is disabled by default.
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor. You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it to the relevant bug report.
</details>
## 📝 Having another issue?
@ -366,13 +573,14 @@ Donations really help with the Apple Developer Program cost, and keep me motivat
## 😎 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:
Special thanks go out to:
* My colleagues at [DIVE](https://dive.be)
* Everyone supporting me via [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen)
* Everyone who has donated via [my sponsor page](https://nicoverbruggen.be/sponsor)
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors)
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so
* Everyone in the Laravel community who shared the app (thanks!)
* Everyone who left feedback via issues & who donated to keep the project up and running
* Everyone who has left feedback and reported bugs
* Everyone in the Laravel community who shared the app, especially on Twitter
Thank you very much for your contributions, kind words and support.
@ -388,7 +596,9 @@ In order to save power, this only happens once every 60 seconds.
This utility will detect which PHP versions you have installed via Homebrew, and then allows you to switch between them.
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be much faster than Valets switcher.
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be a bit faster than Valets switcher.
If you're using Valet 3, versions of PHP-FPM required to keep isolated sites up and running will also be started or stopped as needed.
### Config change detection
@ -404,7 +614,8 @@ If an extension or other process writes to a single file a bunch of times in a s
1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name.
1. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all.
*Note*: If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
> **Note**
> If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
### Want to know more?

View File

@ -4,9 +4,11 @@
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 5 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 5.7 | ✅ Universal binary | ✅ Yes | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x*) | 3.0 or higher recommended<br/> 2.16.2 minimum |
(*) Preliminary listing. Valet 4 hasn't been released yet and the versions of PHP Valet can work with might still change.
## Legacy versions
@ -14,9 +16,10 @@ These versions of PHP Monitor are no longer supported, but if youre using an
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0)* | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 |
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 | 2.13 |
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |

Binary file not shown.

Binary file not shown.

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docs/notification-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/screenshot-dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

BIN
docs/screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

View File

@ -1,22 +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>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

@ -1,332 +0,0 @@
[
{
"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
}
]

View File

@ -1,29 +0,0 @@
//
// 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 = PhpEnv.shared.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

@ -1,18 +0,0 @@
//
// ValetTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class ValetVersionExtractorTest: XCTestCase {
func testDetermineValetVersion() {
let version = valet("--version", sudo: false)
XCTAssert(version.contains("Laravel Valet 2."))
}
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
//
// ActiveCommand.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
var Command: CommandProtocol {
return ActiveCommand.shared
}
class ActiveCommand {
static var shared: CommandProtocol = RealCommand()
public static func useTestable(_ output: [String: String]) {
Self.shared = TestableCommand(commands: output)
}
public static func useSystem() {
Self.shared = RealCommand()
}
}

View File

@ -0,0 +1,22 @@
//
// CommandProtocol.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol CommandProtocol {
/**
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.
*/
func execute(path: String, arguments: [String], trimNewlines: Bool) -> String
}

View File

@ -2,39 +2,32 @@
// Command.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
public 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 {
public class RealCommand: CommandProtocol {
public 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) {
if trimNewlines {
return output.components(separatedBy: .newlines)
.filter({ !$0.isEmpty })
.joined(separator: "\n")
}
return output
}
}

View File

@ -2,116 +2,109 @@
// Services.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class Actions {
// MARK: - Services
public static func restartPhpFpm()
{
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
public static func restartPhpFpm() async {
await brew("services restart \(Homebrew.Formulae.php.name)", sudo: Homebrew.Formulae.php.elevated)
}
public static func restartNginx()
{
brew("services restart nginx", sudo: true)
public static func restartNginx() async {
await brew("services restart \(Homebrew.Formulae.nginx.name)", sudo: Homebrew.Formulae.nginx.elevated)
}
public static func restartDnsMasq()
{
brew("services restart dnsmasq", sudo: true)
public static func restartDnsMasq() async {
await brew("services restart \(Homebrew.Formulae.dnsmasq.name)", sudo: Homebrew.Formulae.dnsmasq.elevated)
}
public static func stopAllServices()
{
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true)
public static func stopValetServices() async {
await brew("services stop \(Homebrew.Formulae.php.name)", sudo: Homebrew.Formulae.php.elevated)
await brew("services stop \(Homebrew.Formulae.nginx.name)", sudo: Homebrew.Formulae.nginx.elevated)
await brew("services stop \(Homebrew.Formulae.dnsmasq.name)", sudo: Homebrew.Formulae.dnsmasq.elevated)
}
public static func fixHomebrewPermissions() throws
{
public static func fixHomebrewPermissions() throws {
var servicesCommands = [
"\(Paths.brew) services stop nginx",
"\(Paths.brew) services stop dnsmasq",
"\(Paths.brew) services stop \(Homebrew.Formulae.nginx)",
"\(Paths.brew) services stop \(Homebrew.Formulae.dnsmasq)"
]
var cellarCommands = [
"chown -R \(Paths.whoami):staff \(Paths.cellarPath)/nginx",
"chown -R \(Paths.whoami):staff \(Paths.cellarPath)/dnsmasq"
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.nginx)",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.dnsmasq)"
]
PhpEnv.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnv.brewPhpVersion
let formula = version == PhpEnv.brewPhpAlias
? "php"
: "php@\(version)"
servicesCommands.append("\(Paths.brew) services stop \(formula)")
cellarCommands.append("chown -R \(Paths.whoami):staff \(Paths.cellarPath)/\(formula)")
cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
}
let script =
servicesCommands.joined(separator: " && ")
+ " && "
+ cellarCommands.joined(separator: " && ")
let appleScript = NSAppleScript(
source: "do shell script \"\(script)\" with administrator privileges"
)
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
if (eventResult == nil) {
if eventResult == nil {
throw HomebrewPermissionError(kind: .applescriptNilError)
}
}
// MARK: - Finding Config Files
public static func openGenericPhpConfigFolder()
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
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")
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 openGlobalComposerFolder() {
let file = URL(string: "file://~/.composer/composer.json".replacingTildeWithHomeDirectory)!
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 = URL(string: "file://~/.config/valet".replacingTildeWithHomeDirectory)!
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openValetConfigFolder()
{
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet")
public static func openPhpMonitorConfigFile() {
let file = URL(string: "file://~/.config/phpmon".replacingTildeWithHomeDirectory)!
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL
{
// Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
public static func createTempPhpInfoFile() async -> URL {
try! FileSystem.writeAtomicallyToFile("/tmp/phpmon_phpinfo.php", content: "<?php phpinfo();")
// 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")
await Shell.quiet("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
}
// MARK: - Fix My Valet
/**
Detects all currently available PHP versions,
and unlinks each and every one of them.
@ -124,25 +117,10 @@ class Actions {
If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`.
*/
public static func fixMyValet()
{
brew("services restart dnsmasq", sudo: true)
PhpEnv.shared.detectPhpVersions().forEach { (version) in
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink php@\(version)")
brew("services stop \(formula)")
brew("services stop \(formula)", sudo: true)
}
brew("services stop dnsmasq")
brew("services stop php")
brew("services stop nginx")
brew("link php --overwrite --force")
brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true)
brew("services restart nginx", sudo: true)
public static func fixMyValet() async {
await InternalSwitcher().performSwitch(to: PhpEnv.brewPhpAlias)
await brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated)
await brew("services restart \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated)
await brew("services restart \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated)
}
}

View File

@ -2,67 +2,86 @@
// Constants.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
struct Constants {
/**
* The latest PHP version that is considered to be stable at the time of release.
* This version number is currently not used (only as a default fallback).
*/
static let LatestStablePhpVersion = "8.1"
/**
The minimum version of Valet that is recommended.
If the installed version is older, a notification will be shown
every time the app launches (with a recommendation to upgrade).
The minimum requirement is currently synced to PHP 8.1 compatibility.
See also: https://github.com/laravel/valet/releases/tag/v2.16.2
*/
static let MinimumRecommendedValetVersion = "2.16.2"
/**
* The PHP versions supported by this application.
* Versions that do not appear in this array are omitted from the list.
* Any other PHP versions are considered invalid.
*/
static let SupportedPhpVersions = [
// ====================
// STABLE RELEASES
// ====================
// Versions of PHP that are stable and are supported.
static let DetectedPhpVersions: Set = [
"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"
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.3"
]
/**
The PHP versions supported by each version of Valet.
*/
static let ValetSupportedPhpVersionMatrix: [Int: Set] = [
2: // Valet v2 has the broadest legacy support
[
"5.6",
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2"
],
3: // Valet v3 dropped support for v5.6
[
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2",
"8.3" // dev
],
4: // Valet v4 dropped support for v7.0
[
"7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2",
"8.3" // dev
]
]
struct Urls {
static let DonationPayment = URL(
string: "https://nicoverbruggen.be/sponsor#pay-now"
)!
// phpmon.app URLs (these are aliased to redirect correctly)
static let DonationPage = URL(
string: "https://nicoverbruggen.be/sponsor"
string: "https://phpmon.app/sponsor"
)!
static let FrequentlyAskedQuestions = URL(
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
string: "https://phpmon.app/faq"
)!
static let DonationPayment = URL(
string: "https://phpmon.app/sponsor/now"
)!
// GitHub URLs (do not alias these)
static let GitHubReleases = URL(
string: "https://github.com/nicoverbruggen/phpmon/releases"
)!
static let StableBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb"
)!
static let DevBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
)!
}
}

View File

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

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
// MARK: Common Shell Commands
@ -11,45 +11,49 @@
/**
Runs a `valet` command. Defaults to running as superuser.
*/
func valet(_ command: String, sudo: Bool = true) -> String
{
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
func valet(_ command: String, sudo: Bool = true) async -> String {
return await Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)").out
}
/**
Runs a `brew` command. Can run as superuser.
*/
func brew(_ command: String, sudo: Bool = false)
{
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
func brew(_ command: String, sudo: Bool = false) async {
await Shell.quiet("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
}
/**
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
*/
func sed(file: String, original: String, replacement: String)
{
func sed(file: String, original: String, replacement: String) async {
// 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 Filesystem.fileExists("\(Paths.binPath)/gsed") {
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
if FileSystem.fileExists("\(Paths.binPath)/gsed") {
await Shell.quiet("\(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)")
await Shell.quiet("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
}
}
/**
Uses `grep` to determine whether a particular query string can be found in a particular file.
*/
func grepContains(file: String, query: String) -> Bool
{
return Shell.pipe("""
func grepContains(file: String, query: String) async -> Bool {
return await Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""")
""").out
.trimmingCharacters(in: .whitespacesAndNewlines)
.contains("YES")
}
/**
Attempts to introduce sleep for a particular duration. Use with caution.
Only intended for testing purposes.
*/
func delay(seconds: Double) async {
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}

View File

@ -0,0 +1,56 @@
//
// Homebrew.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class Homebrew {
static var fake: Bool = false
struct Formulae {
static var php: HomebrewFormula {
if Homebrew.fake {
return HomebrewFormula("php", elevated: true)
}
if PhpEnv.shared.homebrewPackage == nil {
fatalError("You must either load the HomebrewPackage object or call `fake` on the Homebrew class.")
}
return HomebrewFormula(PhpEnv.phpInstall.formula, elevated: true)
}
static var nginx: HomebrewFormula {
return HomebrewDiagnostics.usesNginxFullFormula
? HomebrewFormula("nginx-full", elevated: true)
: HomebrewFormula("nginx", elevated: true)
}
static var dnsmasq: HomebrewFormula {
return HomebrewFormula("dnsmasq", elevated: true)
}
}
}
class HomebrewFormula: Equatable, Hashable {
let name: String
let elevated: Bool
init(_ name: String, elevated: Bool = true) {
self.name = name
self.elevated = elevated
}
static func == (lhs: HomebrewFormula, rhs: HomebrewFormula) -> Bool {
return lhs.elevated == rhs.elevated && lhs.name == rhs.name
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(elevated)
}
}

View File

@ -3,56 +3,85 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class Log {
static var shared = Log()
var logFilePath = "~/.config/phpmon/last_session.log"
var logExists = false
enum Verbosity: Int {
case error = 1,
warning = 2,
info = 3,
performance = 4
performance = 4,
cli = 5
public func isApplicable() -> Bool {
return Log.shared.verbosity.rawValue >= self.rawValue
}
}
var verbosity: Verbosity = .warning
public func prepareLogFile() {
if !isRunningTests && Verbosity.cli.isApplicable() {
_ = system("mkdir -p ~/.config/phpmon 2> /dev/null")
_ = system("rm ~/.config/phpmon/last_session.log 2> /dev/null")
_ = system("touch ~/.config/phpmon/last_session.log 2> /dev/null")
self.logExists = FileSystem.fileExists(self.logFilePath)
}
}
var verbosity: Verbosity = .warning {
didSet {
self.prepareLogFile()
}
}
static func err(_ item: Any) {
if Verbosity.error.isApplicable() {
print("[E] \(item)")
Log.shared.log("[E] \(item)")
}
}
static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() {
print("[W] \(item)")
Log.shared.log("[W] \(item)")
}
}
static func info(_ item: Any) {
if Verbosity.info.isApplicable() {
print("\(item)")
Log.shared.log("\(item)")
}
}
static func perf(_ item: Any) {
if Verbosity.performance.isApplicable() {
print("[P] \(item)")
Log.shared.log("[P] \(item)")
}
}
static func separator(as verbosity: Verbosity = .info) {
if verbosity.isApplicable() {
print("==================================")
Log.shared.log("==================================")
}
}
private func log(_ text: String) {
print(text)
if logExists && Verbosity.cli.isApplicable() {
let logFile = URL(string: self.logFilePath.replacingTildeWithHomeDirectory)!
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
fileHandle.write(text.appending("\n").data(using: .utf8).unsafelyUnwrapped)
fileHandle.closeFile()
}
}
}
}

View File

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

View File

@ -3,13 +3,13 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 23/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Process {
/**
When a process is running in the background, it can send content to standard
output or standard error, just like it would in a terminal. Using `listen`
@ -22,10 +22,10 @@ extension Process {
) {
let outputPipe = Pipe()
let errorPipe = Pipe()
self.standardOutput = outputPipe
self.standardError = errorPipe
[
(outputPipe, didReceiveStandardOutputData),
(errorPipe, didReceiveStandardErrorData)
@ -35,15 +35,18 @@ extension Process {
forName: NSNotification.Name.NSFileHandleDataAvailable,
object: pipe.fileHandleForReading,
queue: nil
) { notification in
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
) { _ in
if let outputString = String(
data: pipe.fileHandleForReading.availableData,
encoding: String.Encoding.utf8
) {
callback(outputString)
}
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
}
}
}
/**
After the process is done running, you'll want to stop listening.
*/
@ -55,5 +58,5 @@ extension Process {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
}
}

View File

@ -1,135 +0,0 @@
//
// Shell.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
public class Shell {
// MARK: - Invoke static functions
public static func run(
_ command: String,
requiresPath: Bool = false
) {
Shell.user.run(command, requiresPath: requiresPath)
}
public static func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
return Shell.user.pipe(command, requiresPath: requiresPath)
}
// MARK: - Singleton
/**
We now require macOS 11, so no need to detect which terminal to use.
*/
public var shell: String = "/bin/sh"
/**
Singleton to access a user shell (with --login)
*/
public static let user = Shell()
/**
Runs a shell command without using the output.
Uses the default shell.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
private func run(
_ command: String,
requiresPath: Bool = false
) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = Shell.pipe(command, requiresPath: requiresPath)
}
/**
Runs a shell command and returns the output.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
private func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
let shellOutput = self.executeSynchronously(command, requiresPath: requiresPath)
let hasError = (
shellOutput.standardOutput == ""
&& shellOutput.errorOutput.lengthOfBytes(using: .utf8) > 0
)
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
}
/**
Runs the command and returns a `ShellOutput` object, which contains info about the process.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
- Parameter waitUntilExit: Waits for the command to complete before returning the `ShellOutput`
*/
public func executeSynchronously(
_ command: String,
requiresPath: Bool = false
) -> Shell.Output {
let outputPipe = Pipe()
let errorPipe = Pipe()
let task = self.createTask(for: command, requiresPath: requiresPath)
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
return Shell.Output(
standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
errorOutput: String(
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
task: task
)
}
/**
Creates a new process with the correct PATH and shell.
*/
public func createTask(for command: String, requiresPath: Bool) -> Process {
let tailoredCommand = requiresPath
? "export PATH=\(Paths.binPath):$PATH && \(command)"
: command
let task = Process()
task.launchPath = self.shell
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
return task
}
public class Output {
public let standardOutput: String
public let errorOutput: String
public let task: Process
init(standardOutput: String,
errorOutput: String,
task: Process) {
self.standardOutput = standardOutput
self.errorOutput = errorOutput
self.task = task
}
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 06/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 08/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -15,9 +15,9 @@ struct HomebrewPermissionError: Error, AlertableError {
enum Kind: String {
case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
}
let kind: Kind
func getErrorMessageKey() -> String {
return "alert.errors.\(self.kind.rawValue)"
}

View File

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

View File

@ -0,0 +1,21 @@
//
// DataExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Data {
var prettyPrintedJSONString: NSString? {
guard let object = try? JSONSerialization.jsonObject(with: self, options: []),
let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]),
let prettyPrintedString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else {
return nil
}
return prettyPrintedString
}
}

View File

@ -2,17 +2,17 @@
// Date.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 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,17 @@
//
// DictionaryExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Dictionary {
mutating func renameKey(fromKey: Key, toKey: Key) {
if let entry = removeValue(forKey: fromKey) {
self[toKey] = entry
}
}
}

View File

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

View File

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

View File

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

View File

@ -2,42 +2,85 @@
// StringExtension.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import SwiftUI
struct Localization {
static var bundle: Bundle = {
if !isRunningTests {
return Bundle.main
}
let foundBundle = Bundle(identifier: "com.nicoverbruggen.phpmon.dev")
?? Bundle(identifier: "com.nicoverbruggen.phpmon")
?? Bundle(identifier: "com.nicoverbruggen.phpmon.ui-tests")
if foundBundle == nil {
let bundles = Bundle.allBundles
.map { $0.bundleIdentifier }
.filter { $0 != nil }
.map { $0! }
fatalError("The following bundles were found: \(bundles)")
}
return foundBundle!
}()
}
extension String {
var localized: String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
if #available(macOS 13, *) {
return NSLocalizedString(
self, tableName: nil, bundle: Localization.bundle, value: "", comment: ""
).replacingOccurrences(of: "Preferences", with: "Settings")
}
return NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
}
var localizedForSwiftUI: LocalizedStringKey {
return LocalizedStringKey(self.localized)
}
func localized(_ args: CVarArg...) -> String {
String(format: self.localized, arguments: args)
}
func countInstances(of stringToFind: String) -> Int {
if (stringToFind.isEmpty) {
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 {
func matches(pattern: String) -> Bool {
let pred = NSPredicate(format: "self LIKE %@", pattern)
return !NSArray(object: self).filtered(using: pred).isEmpty
}
static func random(_ length: Int) -> String {
let characters = "0123456789abcdefghijklmnopqrstuvwxyz"
return String((0..<length).map { _ in characters.randomElement()! })
}
subscript(r: Range<String.Index>) -> String {
let start = r.lowerBound
let end = r.upperBound
return String(self[start ..< end])
}
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
/*
<1> We split the version by period (.).
@ -50,12 +93,12 @@ extension String {
*/
func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "."
var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
if zeroDiff == 0 { // <3>
// Same format, compare normally
return self.compare(otherVersion, options: .numeric)
@ -70,5 +113,22 @@ extension String {
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
}
}
var stripped: String {
do {
guard let data = self.data(using: .unicode) else {
return ""
}
let attributed = try NSAttributedString(
data: data,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue],
documentAttributes: nil
)
return attributed.string
} catch {
return ""
}
}
}

View File

@ -0,0 +1,15 @@
//
// TimeExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/09/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension TimeInterval {
public static func minutes(_ amount: Int) -> TimeInterval {
return Double(amount * 60)
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -12,25 +12,25 @@ 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
var topLevelArray: NSArray?
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 }
let views = [Any](results).filter { $0 is Self }
return views.last as? Self
}
}

View File

@ -0,0 +1,26 @@
//
// FS.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
var FileSystem: FileSystemProtocol {
return ActiveFileSystem.shared
}
class ActiveFileSystem {
static var shared: FileSystemProtocol = RealFileSystem()
/** Note: Intermediate directories are not automatically inferred and have to be manually declared. */
public static func useTestable(_ files: [String: FakeFile]) {
Self.shared = TestableFileSystem(files: files)
}
public static func useSystem() {
Self.shared = RealFileSystem()
}
}

View File

@ -0,0 +1,50 @@
//
// FileSystemProtocol.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol FileSystemProtocol {
// MARK: - Basics
func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws
func writeAtomicallyToFile(_ path: String, content: String) throws
func getStringFromFile(_ path: String) throws -> String
func getShallowContentsOfDirectory(_ path: String) throws -> [String]
func getDestinationOfSymlink(_ path: String) throws -> String
// MARK: - Move & Delete Files
func move(from path: String, to newPath: String) throws
func remove(_ path: String) throws
// MARK: Attributes
func makeExecutable(_ path: String) throws
// MARK: - Checks
func isExecutableFile(_ path: String) -> Bool
func isWriteableFile(_ path: String) -> Bool
func anyExists(_ path: String) -> Bool
func fileExists(_ path: String) -> Bool
func directoryExists(_ path: String) -> Bool
func isSymlink(_ path: String) -> Bool
func isDirectory(_ path: String) -> Bool
}

View File

@ -0,0 +1,126 @@
//
// RealFileSystem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension String {
var replacingTildeWithHomeDirectory: String {
return self.replacingOccurrences(of: "~", with: Paths.homePath)
}
}
class RealFileSystem: FileSystemProtocol {
// MARK: - Basics
func createDirectory(_ path: String, withIntermediateDirectories: Bool) {
try! FileManager.default.createDirectory(
atPath: path.replacingTildeWithHomeDirectory,
withIntermediateDirectories: withIntermediateDirectories
)
}
func writeAtomicallyToFile(_ path: String, content: String) throws {
try content.write(
to: URL(fileURLWithPath: path.replacingTildeWithHomeDirectory),
atomically: true,
encoding: String.Encoding.utf8
)
}
func getStringFromFile(_ path: String) throws -> String {
return try String(
contentsOf: URL(fileURLWithPath: path.replacingTildeWithHomeDirectory),
encoding: .utf8
)
}
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
return try FileManager.default.contentsOfDirectory(atPath: path)
}
func getDestinationOfSymlink(_ path: String) throws -> String {
return try FileManager.default.destinationOfSymbolicLink(atPath: path)
}
// MARK: - Move & Delete Files
func move(from path: String, to newPath: String) throws {
try FileManager.default.moveItem(atPath: path, toPath: newPath)
}
func remove(_ path: String) throws {
try FileManager.default.removeItem(atPath: path)
}
// MARK: FS Attributes
func makeExecutable(_ path: String) throws {
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
}
// MARK: - Checks
func isExecutableFile(_ path: String) -> Bool {
return FileManager.default.isExecutableFile(
atPath: path.replacingTildeWithHomeDirectory
) && FileManager.default.isReadableFile(
atPath: path.replacingTildeWithHomeDirectory
)
}
func isWriteableFile(_ path: String) -> Bool {
return FileManager.default.isWritableFile(
atPath: path.replacingTildeWithHomeDirectory
)
}
func anyExists(_ path: String) -> Bool {
return FileManager.default.fileExists(
atPath: path.replacingTildeWithHomeDirectory
)
}
func fileExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingTildeWithHomeDirectory,
isDirectory: &isDirectory
)
return exists && !isDirectory.boolValue
}
func directoryExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingTildeWithHomeDirectory,
isDirectory: &isDirectory
)
return exists && isDirectory.boolValue
}
func isSymlink(_ path: String) -> Bool {
do {
let attribs = try FileManager.default.attributesOfItem(atPath: path)
return attribs[.type] as! FileAttributeType == FileAttributeType.typeSymbolicLink
} catch {
return false
}
}
func isDirectory(_ path: String) -> Bool {
do {
let attribs = try FileManager.default.attributesOfItem(atPath: path)
return attribs[.type] as! FileAttributeType == FileAttributeType.typeDirectory
} catch {
return false
}
}
}

View File

@ -2,32 +2,32 @@
// Alert.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Alert {
public static func confirm(
onWindow window: NSWindow,
messageText: String,
informativeText: String,
buttonTitle: String = "OK",
secondButtonTitle: String = "Cancel",
buttonTitle: String = "generic.ok".localized,
secondButtonTitle: String = "generic.cancel".localized,
style: NSAlert.Style = .warning,
onFirstButtonPressed: @escaping (() -> Void)
) {
if !Thread.isMainThread {
fatalError("You should always present alerts on the main thread!")
}
let alert = NSAlert.init()
alert.alertStyle = style
alert.messageText = messageText
alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) {
if !secondButtonTitle.isEmpty {
alert.addButton(withTitle: secondButtonTitle)
}
alert.beginSheetModal(for: window) { response in
@ -36,5 +36,5 @@ class Alert {
}
}
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -12,52 +12,67 @@ import Foundation
/// In most cases this is going to be a code editor, but it could also be another application
/// that supports opening those directories, like a visual Git client or a terminal app.
class Application {
enum AppType {
case editor, browser, git_gui, terminal, user_supplied
}
/// Name of the app. Used for display purposes and to determine `name.app` exists.
let name: String
/// Application type. Depending on the type, a different action might occur.
let type: AppType
/// Initializer. Used to detect a specific app of a specific type.
init(_ name: String, _ type: AppType) {
self.name = name
self.type = type
}
/**
Attempt to open a specific directory in the app of choice.
(This will open the app if it isn't open yet.)
*/
@objc public func openDirectory(file: String) {
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
Task { await Shell.quiet("/usr/bin/open -a \"\(name)\" \"\(file)\"") }
}
/** Checks if the app is installed. */
func isInstalled() -> Bool {
// If this script does not complain, the app exists!
return Shell.user.executeSynchronously(
func isInstalled() async -> Bool {
let (process, output) = try! await Shell.attach(
"/usr/bin/open -Ra \"\(name)\"",
requiresPath: false
).task.terminationStatus == 0
didReceiveOutput: { _, _ in },
withTimeout: 2.0
)
if Shell is TestableShell {
// When testing, check the error output (must not be empty)
return !output.hasError
} else {
// If this script does not complain, the app exists!
return process.terminationStatus == 0
}
}
/**
Detect which apps are available to open a specific directory.
*/
static public func detectPresetApplications() -> [Application] {
return [
static public func detectPresetApplications() async -> [Application] {
var detected: [Application] = []
let detectable = [
Application("PhpStorm", .editor),
Application("Visual Studio Code", .editor),
Application("Sublime Text", .editor),
Application("Sublime Merge", .git_gui),
Application("iTerm", .terminal)
].filter {
return $0.isInstalled()
]
for app in detectable where await app.isInstalled() {
detected.append(app)
}
return detected
}
}

View File

@ -1,23 +0,0 @@
//
// FileSystem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Filesystem {
/**
Checks if a file exists at the provided path.
Uses `FileManager`.
*/
public static func fileExists(_ path: String) -> Bool {
return FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
)
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
//
// System.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
Run a simple blocking Shell command on the user's own system.
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
*/
public func system(_ command: String) -> String {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-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

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

View File

@ -0,0 +1,17 @@
//
// WIP.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
func todo(_ context: String = "") {
if !context.isEmpty {
fatalError("To be implemented: \(context)")
}
fatalError("To be implemented")
}

View File

@ -2,7 +2,7 @@
// ActivePhpInstallation.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -17,82 +17,88 @@ import Foundation
Using `version.short` is advisable if you want to interact with Homebrew.
*/
class ActivePhpInstallation {
var version: Version!
var version: VersionNumber!
var limits: Limits!
var extensions: [PhpExtension]!
// MARK: - Computed
var formula: String {
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
var iniFiles: [PhpConfigurationFile] = []
var hasErrorState: Bool = false
var extensions: [PhpExtension] {
return iniFiles.flatMap { initFile in
return initFile.extensions
}
}
// MARK: - Computed
var formula: String {
return (version.short == PhpEnv.brewPhpAlias) ? "php" : "php@\(version.short)"
}
// MARK: - Initializer
init() {
// Show information about the current version
getVersion()
do {
try determineVersion()
} catch {
// TODO: In future versions of PHP Monitor, this should not crash
fatalError("Could not determine or parse PHP version; aborting")
}
// Initialize the list of ini files that are loaded
iniFiles = []
// If an error occurred, exit early
if (version.error) {
if self.hasErrorState {
limits = Limits()
extensions = []
return
}
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path)
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
iniFiles.append(file)
}
// Get configuration values
limits = Limits(
memory_limit: getByteCount(key: "memory_limit"),
upload_max_filesize: getByteCount(key: "upload_max_filesize"),
post_max_size: 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) }
let paths = Command.execute(
path: Paths.php,
arguments: ["-r", "echo php_ini_scanned_files();"],
trimNewlines: false
)
.replacingOccurrences(of: "\n", with: "")
.split(separator: ",")
.map { String($0) }
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if exts.count > 0 {
extensions.append(contentsOf: exts)
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
iniFiles.append(file)
}
}
}
/**
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: ".")
private func determineVersion() throws {
let output = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
self.hasErrorState = (output == "" || output.contains("Warning") || output.contains("Error"))
self.version = try? VersionNumber.parse(output)
}
/**
Retrieves the display value for a specific key in the `.ini` file.
@ -109,19 +115,19 @@ class ActivePhpInstallation {
- 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)');"])
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"], trimNewlines: false)
// Check if the value is unlimited
if (value == "-1") {
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
let match = regex.matches(in: value, options: [], range: NSRange(location: 0, length: value.count)).first
return (match == nil) ? "⚠️" : "\(value)B"
}
/**
Determine if PHP-FPM is configured correctly.
@ -129,31 +135,20 @@ class ActivePhpInstallation {
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
that means that Valet won't work properly.
*/
func checkPhpFpmStatus() -> Bool {
func checkPhpFpmStatus() async -> 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")
return await Shell.pipe("cat \(fileName)").out
.contains("valet.sock")
}
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
return Filesystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
return FileSystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
}
// MARK: - Structs
/**
Struct containing information about the version number of the current PHP installation.
Also includes information about whether the install is considered "broken" or not.
If an error was found in the terminal output, `error` is set to `true` and the installation
can be considered broken. (The app will display this as well.)
*/
struct Version {
var short = "???"
var long = "???"
var error = false
}
/**
Struct containing information about the limits of the current PHP installation.
Includes: memory limit, max upload size and max post size.
@ -163,5 +158,5 @@ class ActivePhpInstallation {
var upload_max_filesize = "???"
var post_max_size = "???"
}
}

View File

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

View File

@ -2,24 +2,24 @@
// HomebrewPackage.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 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 {

View File

@ -3,12 +3,12 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 11/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
struct HomebrewService: Decodable, Equatable {
final class HomebrewService: Sendable, Decodable {
let name: String
let service_name: String
let running: Bool
@ -18,18 +18,43 @@ struct HomebrewService: Decodable, Equatable {
let status: String?
let log_path: String?
let error_log_path: String?
public static func loadAll(
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"]
) async -> [HomebrewService] {
return try! JSONDecoder().decode(
[HomebrewService].self,
from: Shell.pipe(
"sudo \(Paths.brew) services info --all --json",
requiresPath: true
).data(using: .utf8)!
).filter({ service in
return filter.contains(service.name)
})
init(
name: String,
service_name: String,
running: Bool,
loaded: Bool,
pid: Int? = nil,
user: String? = nil,
status: String? = nil,
log_path: String? = nil,
error_log_path: String? = nil
) {
self.name = name
self.service_name = service_name
self.running = running
self.loaded = loaded
self.pid = pid
self.user = user
self.status = status
self.log_path = log_path
self.error_log_path = error_log_path
}
/**
Dummy data for preview purposes.
*/
public static func dummy(named service: String, enabled: Bool, status: String? = nil) -> HomebrewService {
return HomebrewService(
name: service,
service_name: service,
running: enabled,
loaded: enabled,
pid: nil,
user: nil,
status: status,
log_path: nil,
error_log_path: nil
)
}
}

View File

@ -3,48 +3,53 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpEnv {
// MARK: - Initializer
init() {
self.currentInstall = ActivePhpInstallation()
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
}
func determinePhpAlias() async {
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!")
}
// MARK: - Properties
/** The delegate that is informed of updates. */
weak var delegate: PhpSwitcherDelegate?
/** The static app instance. Accessible at any time. */
static let shared = PhpEnv()
/** Whether the switcher is busy performing any actions. */
var isBusy: Bool = false
/** All available versions of PHP. */
/** All versions of PHP that are currently supported. */
var availablePhpVersions: [String] = []
/** All versions of PHP that are currently installed but not compatible. */
var incompatiblePhpVersions: [String] = []
/** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */
var currentInstall: ActivePhpInstallation
var currentInstall: ActivePhpInstallation!
/**
The version that the `php` formula via Brew is aliased to on the current system.
@ -54,76 +59,90 @@ class PhpEnv {
As such, we take that information from Homebrew.
*/
static var brewPhpVersion: String {
static var brewPhpAlias: String {
if Homebrew.fake { return "8.2" }
return Self.shared.homebrewPackage.version
}
/**
The currently linked and active PHP installation.
*/
static var phpInstall: ActivePhpInstallation {
return Self.shared.currentInstall
}
/**
Information we were able to discern from the Homebrew info command.
*/
var homebrewPackage: HomebrewPackage! = nil
// MARK: - Methods
public static var switcher: PhpSwitcher {
return InternalSwitcher()
}
public static func detectPhpVersions() -> Void {
_ = Self.shared.detectPhpVersions()
public static func detectPhpVersions() async {
_ = await Self.shared.detectPhpVersions()
}
/**
Detects which versions of PHP are installed.
*/
public func detectPhpVersions() -> [String]
{
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
public func detectPhpVersions() async -> Set<String> {
let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out
let versions = await extractPhpVersions(from: files.components(separatedBy: "\n"))
let supportedByValet = Constants.ValetSupportedPhpVersionMatrix[Valet.shared.version.major] ?? []
var supportedVersions = versions.intersection(supportedByValet)
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed
let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate
if (!versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php")) {
versionsOnly.append(phpAlias)
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
supportedVersions.insert(phpAlias)
}
Log.info("The PHP versions that were detected are: \(versionsOnly)")
availablePhpVersions = versionsOnly
availablePhpVersions = Array(supportedVersions)
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
incompatiblePhpVersions = Array(versions.subtracting(supportedByValet))
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
Log.info("The PHP versions that were detected are: \(availablePhpVersions)")
Log.info("The PHP versions that were unsupported are: \(incompatiblePhpVersions)")
var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version)
}
cachedPhpInstallations = mappedVersions
return versionsOnly
return supportedVersions
}
/**
Extracts valid PHP versions from an array of strings.
This array of strings is usually retrieved from `grep`.
If `generateHelpers` is set to true, after detecting
all versions, helper scripts are generated as well.
*/
public func extractPhpVersions(
from versions: [String],
checkBinaries: Bool = true
) -> [String] {
var output : [String] = []
checkBinaries: Bool = true,
generateHelpers: Bool = true
) async -> Set<String> {
let supported = Constants.DetectedPhpVersions
var output: Set<String> = []
versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected)
@ -133,33 +152,51 @@ class PhpEnv {
// 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 ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
{
output.append(version)
&& supported.contains(version)
&& (checkBinaries ? FileSystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
output.insert(version)
}
}
if generateHelpers {
for item in output {
await PhpHelper.generate(for: item)
}
}
return output
}
public func validVersions(for constraint: String) -> [PhpVersionNumber] {
public func validVersions(for constraint: String) -> [VersionNumber] {
constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection
.make(from: self.availablePhpVersions)
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
/**
Validates whether the currently running version matches the provided version.
*/
public func validate(_ version: String) -> Bool {
if self.currentInstall.version.short == version {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
Log.info("Keeping track that this is the new version!")
Stats.persistCurrentGlobalPhpVersion(version: version)
return true
}
return false
}
/**
Returns the configuration file instance that is used for a specific config value.
You can then use the configuration file instance to change values.
*/
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
return PhpEnv.phpInstall.iniFiles
.reversed()
.first(where: { $0.has(key: key) })
}
}

View File

@ -0,0 +1,104 @@
//
// PhpHelper.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 17/03/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpHelper {
static let keyPhrase = "This file was automatically generated by PHP Monitor."
public static func generate(for version: String) async {
// Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "")
// Determine the dotless name for this PHP version
let destination = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
// Check if the ~/.config/phpmon/bin directory is in the PATH
let inPath = Shell.PATH.contains("\(Paths.homePath)/.config/phpmon/bin")
// Check if we can create symlinks (`/usr/local/bin` must be writable)
let canWriteSymlinks = FileSystem.isWriteableFile("/usr/local/bin/")
Task { // Create the appropriate folders and check if the files exist
do {
if !FileSystem.directoryExists("~/.config/phpmon/bin") {
try FileSystem.createDirectory(
"~/.config/phpmon/bin",
withIntermediateDirectories: true
)
}
if FileSystem.fileExists(destination) {
let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) {
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
+ "(or is unreadable). Not updating this file.")
return
}
}
// Let's follow the symlink to the PHP binary folder
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path
// The contents of the script!
let script = """
#!/bin/zsh
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
&& echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH
"""
try FileSystem.writeAtomicallyToFile(destination, content: script)
if !FileSystem.isExecutableFile(destination) {
try FileSystem.makeExecutable(destination)
}
// Create a symlink if the folder is not in the PATH
if !inPath {
// First, check if we can create symlinks at all
if !canWriteSymlinks {
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
return
}
// Write the symlink
await self.createSymlink(dotless)
}
} catch {
Log.err(error)
Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
}
}
}
private static func createSymlink(_ dotless: String) async {
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
let destination = "/usr/local/bin/pm\(dotless)"
if !FileSystem.fileExists(destination) {
Log.info("Creating new symlink: \(destination)")
await Shell.quiet("ln -s \(source) \(destination)")
return
}
if !FileSystem.isSymlink(destination) {
Log.info("Overwriting existing file with new symlink: \(destination)")
await Shell.quiet("ln -fs \(source) \(destination)")
return
}
Log.info("Symlink in \(destination) already exists, OK.")
}
}

View File

@ -1,190 +0,0 @@
//
// PhpVersionNumber.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
public struct PhpVersionNumberCollection: Equatable {
let versions: [PhpVersionNumber]
public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection(
versions: versions.map { PhpVersionNumber.make(from: $0)! }
)
}
public var first: PhpVersionNumber? {
return self.versions.first
}
public var all: [PhpVersionNumber] {
return self.versions
}
/**
Checks if any versions of PHP are valid for the constraint provided.
Due to the complexity of evaluating these, a important test is maintained.
More information on these constraints can be found here:
https://getcomposer.org/doc/articles/versions.md#writing-version-constraints
- Parameter constraint: The full constraint as a string (e.g. "^7.0")
- Parameter strict: Whether the patch version check is strict. See more below.
The strict mode does not matter if a patch version is provided for all versions in the collection.
Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred
from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise,
assumes that the patch version is .999, which means that in all cases the patch version check is
always going to pass.
**STRICT MODE (= patch precision on)**
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will
be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK).
When checking against actual PHP versions installed by the user (with patch precision), use
strict mode.
**NON-STRICT MODE (= patch precision off)**
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0
is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version.
In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde).
If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since
the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.)
*/
public func matching(constraint: String, strict: Bool = false) -> [PhpVersionNumber] {
if let version = PhpVersionNumber.make(from: constraint, type: .versionOnly) {
// Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter { $0.isSameAs(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
// Caret range means that the major version is never higher but minor version can be higher
// ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis.
return self.versions.filter {
version.patch != nil
// If a patch is provided then the minor version cannot be bumped.
? $0.hasSameMajorAndMinorButNewerOrSamePatch(version, strict)
// If a patch is not provided then the major version cannot be bumped.
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
}
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) }
}
return []
}
}
public struct PhpVersionNumber: Equatable {
let major: Int
let minor: Int
let patch: Int?
public func toString() -> String {
return self.patch == nil
? "\(major).\(minor)"
: "\(major).\(minor).\(patch!)"
}
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
}
public var homebrewVersion: String {
return "\(major).\(minor)"
}
public enum MatchType: String {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
// TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
/*
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
*/
}
public static func parse(_ text: String) throws -> Self {
guard let versionText = VersionExtractor.from(text) else {
throw VersionParseError()
}
return Self.make(from: versionText)!
}
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
if match != nil {
let major = Int(
versionString[Range(match!.range(withName: "major"), in: versionString)!]
)!
let minor = Int(
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
)!
var patch: Int? = nil
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
patch = Int(versionString[minorRange])
}
return Self(major: major, minor: minor, patch: patch)
}
return nil
}
// MARK: Comparison Logic
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
}
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return (
self.major > version.major ||
self.major == version.major && self.minor > version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) > version.patch(strict)
)
}
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major &&
(
(self.minor == version.minor && self.patch(strict) >= version.patch(strict, self))
|| self.minor > version.minor
)
}
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict)
}
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor >= version.minor
}
}

View File

@ -0,0 +1,100 @@
//
// PhpVersionNumberCollection.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 06/01/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
public struct PhpVersionNumberCollection: Equatable {
let versions: [VersionNumber]
public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection(
versions: versions.map { try! VersionNumber.parse($0) }
)
}
public var first: VersionNumber? {
return self.versions.first
}
public var all: [VersionNumber] {
return self.versions
}
/**
Checks if any versions of PHP are valid for the constraint provided.
Due to the complexity of evaluating these, a important test is maintained.
More information on these constraints can be found here:
https://getcomposer.org/doc/articles/versions.md#writing-version-constraints
- Parameter constraint: The full constraint as a string (e.g. "^7.0")
- Parameter strict: Whether the patch version check is strict. See more below.
The strict mode does not matter if a patch version is provided for all versions in the collection.
Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred
from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise,
assumes that the patch version is .999, which means that in all cases the patch version check is
always going to pass.
**STRICT MODE (= patch precision on)**
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will
be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK).
When checking against actual PHP versions installed by the user (with patch precision), use
strict mode.
**NON-STRICT MODE (= patch precision off)**
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0
is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version.
In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde).
If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since
the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.)
*/
public func matching(constraint: String, strict: Bool = false) -> [VersionNumber] {
if let version = VersionNumber.make(from: constraint, type: .versionOnly) {
// Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter { $0.isSameAs(version, strict) }
}
if let version = VersionNumber.make(from: constraint, type: .caretVersionRange) {
// Caret range means that the major version is never higher but minor version can be higher
// ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
}
if let version = VersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis.
return self.versions.filter {
version.patch != nil
// If a patch is provided then the minor version cannot be bumped.
? $0.hasSameMajorAndMinorButNewerOrSamePatch(version, strict)
// If a patch is not provided then the major version cannot be bumped.
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
}
}
if let version = VersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
}
if let version = VersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) }
}
if let version = VersionNumber.make(from: constraint, type: .smallerThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isOlderThan(version, strict)}
}
if let version = VersionNumber.make(from: constraint, type: .smallerThan) {
return self.versions.filter { $0.isOlderThan(version, strict)}
}
return []
}
}

View File

@ -0,0 +1,131 @@
//
// PhpVersionNumber.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
A version number that is (mostly) compatible with the semantic versioning standard.
For more information about semantic versioning, see: https://semver.org/
- Note: If you want to check version constraints for PHP versions, please see `PhpVersionNumberCollection`.
*/
public struct VersionNumber: Equatable, Hashable {
let major: Int
let minor: Int
let patch: Int?
var text: String {
return self.patch == nil
? "\(major).\(minor)"
: "\(major).\(minor).\(patch!)"
}
public func patch(_ strictFallback: Bool = true, _ constraint: VersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
}
public var long: String {
return "\(major).\(minor).\(patch ?? 0)"
}
public var short: String {
return "\(major).\(minor)"
}
public enum MatchType: String {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
}
public static func parse(_ text: String) throws -> Self {
guard let versionText = VersionExtractor.from(text) else {
throw VersionParseError()
}
return Self.make(from: versionText)!
}
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(
in: versionString,
options: [],
range: NSRange(location: 0, length: versionString.count)
).first
if match != nil {
let major = Int(
versionString[Range(match!.range(withName: "major"), in: versionString)!]
)!
let minor = Int(
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
)!
var patch: Int?
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
patch = Int(versionString[minorRange])
}
return Self(major: major, minor: minor, patch: patch)
}
return nil
}
// MARK: Comparison Logic
internal func isSameMajorVersionAs(_ version: VersionNumber) -> Bool {
return self.major == version.major
}
internal func isSameAs(_ version: VersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
}
internal func isNewerThan(_ version: VersionNumber, _ strict: Bool) -> Bool {
return (
self.major > version.major ||
self.major == version.major && self.minor > version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) > version.patch(strict)
)
}
internal func isOlderThan(_ version: VersionNumber, _ strict: Bool) -> Bool {
return (
self.major < version.major ||
self.major == version.major && self.minor < version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) < version.patch(strict)
)
}
internal func hasNewerMinorVersionOrPatch(_ version: VersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major &&
(
(self.minor == version.minor && self.patch(strict) >= version.patch(strict, self))
|| self.minor > version.minor
)
}
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: VersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict)
}
internal func hasSameMajorButNewerOrSameMinor(_ version: VersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor >= version.minor
}
}

View File

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

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