Compare commits
600 Commits
Author | SHA1 | Date | |
---|---|---|---|
8b0aeef2e6 | |||
aa406434d0 | |||
d320c49092 | |||
966033e052 | |||
7c192730e1 | |||
13ee618d5c | |||
e149a2500e | |||
aef7d4021c | |||
050c489158 | |||
130bf38dd9 | |||
a6c3f0ceba | |||
f274e9e004 | |||
a07881a987 | |||
715b674929 | |||
cb504cc3f0 | |||
7c08a2fdfe | |||
3f14754177 | |||
2f47610c85 | |||
cc6324b692 | |||
15d75a7f98 | |||
c7c5311ff9 | |||
c93f047909 | |||
f82a2120f7 | |||
857cba9f45 | |||
b0de0c04c6 | |||
ec49257bcc | |||
9c6a21008a | |||
f27e07fc78 | |||
9fceab3e2e | |||
5dffbf57d1 | |||
21a1d6576e | |||
b08912ce11 | |||
7285d24ef3 | |||
ac60c66bb9 | |||
03fdf23f0a | |||
412b7bad5c | |||
9a7575790a | |||
cd5cbccb04 | |||
20291bf034 | |||
2c40f433d3 | |||
5ac4817048 | |||
1216fe4974 | |||
147407666d | |||
4568f03a65 | |||
2eda8d6382 | |||
dd330fecce | |||
aaa7c636db | |||
ff75fb7be3 | |||
4d7b01831b | |||
0fceb852bb | |||
9fb5f33770 | |||
2f658ee569 | |||
ad179a325a | |||
4baeaea85f | |||
9e25254ec8 | |||
e623207844 | |||
f60ecb877c | |||
66393094b0 | |||
b5d2fef184 | |||
44bc07c9dc | |||
93790f3951 | |||
d3b1afe9fd | |||
2fa50a7dc4 | |||
18b62ecc3f | |||
2d59b8c6e8 | |||
450d7ec001 | |||
b6b1174ca3 | |||
e509f6b59d | |||
6014320441 | |||
a0d80423e9 | |||
35b19efc3e | |||
9a175e7291 | |||
d53e92ce94 | |||
3344fdb1dc | |||
020a0260f1 | |||
3ce7e8f48b | |||
ae7e13de9b | |||
c82ea1fac1 | |||
d6258f54a9 | |||
e6d2c873a5 | |||
300b10c5d8 | |||
6eea08cd4f | |||
3c946a53e8 | |||
d8738b685f | |||
55fc90bcf5 | |||
6a2f4d248c | |||
a50eb04f3c | |||
4cbfbeb4e5 | |||
18dd597d38 | |||
e5c80ab52f | |||
0b3a83c1e4 | |||
6e7c0d827c | |||
d05f39efe7 | |||
27894e4884 | |||
f153fee05c | |||
71e1ed1b93 | |||
61ecefb6e7 | |||
422a7738bd | |||
0beda388eb | |||
684a53fc4a | |||
456948ffd9 | |||
a090cbc20b | |||
7b07520440 | |||
6d52992c9d | |||
291d20b2b3 | |||
3d505aebde | |||
d854dee2a1 | |||
67e8589834 | |||
a36c9b1563 | |||
df1b1c5856 | |||
5af7214320 | |||
e20d3ffd22 | |||
be70559d4c | |||
61988a141b | |||
3bcf52bf0a | |||
296bc486c4 | |||
59f60b5013 | |||
de2c1aca5d | |||
44c1ea7de4 | |||
923f0237e8 | |||
ff7c68ddfb | |||
bf2c0c259f | |||
0fdd1264ed | |||
c85a8e3818 | |||
15fe5e4716 | |||
de6dea066e | |||
ee230f3086 | |||
bc96b50630 | |||
d6554ceea9 | |||
4b04f70638 | |||
d49e74fab1 | |||
e34dadcb9b | |||
a696a9a386 | |||
837392d606 | |||
912d9e7423 | |||
cb98d40bef | |||
1f165058b2 | |||
04c78eba35 | |||
4bfde7b062 | |||
00cc6711a1 | |||
209244e162 | |||
e48d8ed98b | |||
834f76e5a1 | |||
5f39cd757a | |||
7e48893247 | |||
073b7cf943 | |||
31a0bb986f | |||
81183acea8 | |||
e29ca9e1ad | |||
40e05d9445 | |||
3ebf51b319 | |||
3f8739dc30 | |||
c3e55df9fb | |||
4b8ad911f1 | |||
c6aa06842c | |||
efd902b4f3 | |||
918e272da7 | |||
272a9182d3 | |||
63f85aff91 | |||
a801174f0a | |||
fa5c843619 | |||
581c2d1974 | |||
bc739e1982 | |||
a21d928a6c | |||
4deef64537 | |||
bedabaa3bb | |||
76d96b3507 | |||
4de7179d1c | |||
f2d5b94831 | |||
ff5fdd82b1 | |||
4f6bae87d4 | |||
ce44166b48 | |||
5caca85d7a | |||
fa2d2105c5 | |||
8417d637fe | |||
e8c85f93f9 | |||
24659d4385 | |||
786b59aa92 | |||
9da3772212 | |||
e62b03d070 | |||
9a11d2efed | |||
b134e62328 | |||
771f8dc757 | |||
e18db4eadd | |||
1ece5c34bf | |||
04ed29bc9f | |||
ac2184ba97 | |||
5c69133c42 | |||
8310cf2729 | |||
696f9bf351 | |||
17b1329d71 | |||
4bfa98fc20 | |||
658cec27c1 | |||
1c0d9f6826 | |||
507d7d5b23 | |||
4b8b46a822 | |||
ea5dd3bc46 | |||
83657fee6f | |||
4c752b6a15 | |||
5e3e0c087b | |||
273070ef27 | |||
cb3208c008 | |||
d401fe997d | |||
eaf6ef658f | |||
d91e16d674 | |||
008603b8c3 | |||
728274aaca | |||
daaece3cfa | |||
eaa74b7141 | |||
8a6656d3e2 | |||
ad46f51d73 | |||
12a4efc775 | |||
ec4c4df5fd | |||
f4448e0640 | |||
7fd30d7c54 | |||
2c57dea97f | |||
a77fa5557a | |||
b0f27dcfa5 | |||
f5d2ec2b7e | |||
6db5cdec25 | |||
03cf4ef3e4 | |||
6feb8118d9 | |||
45a82b2c9e | |||
ed3622cc4e | |||
e2ab7f08ed | |||
ad41e3891e | |||
108ae05c1d | |||
fb1ca60240 | |||
3267dc8add | |||
1fd7db15a7 | |||
0b33116eb0 | |||
2c25bcbdb5 | |||
8a6139d5e7 | |||
953ccb3792 | |||
c26c491340 | |||
c9a5cd3a9f | |||
45704fc736 | |||
f28354e634 | |||
86eb295489 | |||
572330eaa1 | |||
5ebafdb4e3 | |||
ffffcad84b | |||
8055a32bde | |||
4c11fae541 | |||
99da328921 | |||
bbac2632a2 | |||
513a86ec39 | |||
5b3054326e | |||
e7f3c7e59c | |||
a59efb7fce | |||
3483569410 | |||
5399bddfeb | |||
a407515534 | |||
a682d0cfb0 | |||
b827ffb869 | |||
bdb718598e | |||
ddfc73e033 | |||
cfae520984 | |||
e871a00490 | |||
1d396202db | |||
39769d815f | |||
90a69338f7 | |||
3f25759d4f | |||
4494a0555f | |||
0d86f3ded6 | |||
0c176493e5 | |||
71da62f954 | |||
d6781568a3 | |||
fc27131cca | |||
c9c7e14416 | |||
bb124bd0ee | |||
c35e7781f4 | |||
0947dc5ecc | |||
286cdd00e9 | |||
61528cea46 | |||
883f5a1a5d | |||
42b79d3cb3 | |||
36aa41568c | |||
95729c5315 | |||
f02e45486e | |||
6ddddc744a | |||
273c51f702 | |||
186f80c90e | |||
d93af814c9 | |||
f4885f7dbc | |||
805c9f5e6a | |||
183d0bbc30 | |||
4855c14d28 | |||
588398ea76 | |||
d2502cfba2 | |||
8d46fa8a4e | |||
e59b89ea49 | |||
18b103bf9c | |||
9b8de47f5d | |||
da2934c2e5 | |||
9de4fc6712 | |||
4f4c950349 | |||
331ca8a9a4 | |||
87e08e4607 | |||
845235a276 | |||
7587126a42 | |||
f4b1e0745a | |||
c7bb4c1d37 | |||
a17512bfad | |||
a85e016b4a | |||
1292e91b33 | |||
8c55fee18d | |||
663082d725 | |||
fcdb4a8993 | |||
9134f08ec9 | |||
b281df3bbd | |||
a9f9c38e0d | |||
bbbdce6b44 | |||
237185995d | |||
48f950fc3a | |||
2ee0934080 | |||
cfbb83976d | |||
4e5b178e36 | |||
0c59d14da5 | |||
2a9c8e6830 | |||
78e750d764 | |||
c1c7561361 | |||
f90925ee17 | |||
ccfb15c83c | |||
bcc80b5210 | |||
023043a81d | |||
a2c93833df | |||
8b6267f411 | |||
1bff75311b | |||
7a580eef0c | |||
2306529936 | |||
08fdcdbc6c | |||
8cb8e5e409 | |||
6094f83e98 | |||
cdb4b60487 | |||
ae5bed2532 | |||
f098ffbf3d | |||
0a55b45c60 | |||
418d1e2479 | |||
fa3ec2aaa3 | |||
a1df2deec5 | |||
8fb43f7a16 | |||
8a8d32cb5d | |||
6e3bb1d322 | |||
cac7048d92 | |||
1aa051e12c | |||
58fd045bdf | |||
2fc71303df | |||
2c61d991d6 | |||
beca09b76f | |||
5c3e856a7b | |||
6fbbd499f8 | |||
1066bdc653 | |||
4ef5918b7a | |||
e45db50f48 | |||
8a7f045bb2 | |||
10272f3d7e | |||
96ce462021 | |||
8b635f2a14 | |||
f8f3fd5c9b | |||
6801283597 | |||
cf03727dc0 | |||
25a7dded73 | |||
c5fd43312f | |||
ea7572ffbb | |||
4d5b5de84b | |||
721bb32087 | |||
1fe086d53e | |||
347a9b7345 | |||
e0c087dbcb | |||
fd34deb7bd | |||
ccb0d96453 | |||
d821968a49 | |||
7d7a38546a | |||
30059353fe | |||
090440abc8 | |||
ceeba611d0 | |||
f1feb11baa | |||
90a02f364a | |||
6b8c68b058 | |||
fb34ea7c89 | |||
507d4785aa | |||
0c0045aead | |||
1fdf687c15 | |||
1040bcd037 | |||
655cd52ac4 | |||
2229f01eb0 | |||
f95eb7023f | |||
320e1ad3c5 | |||
e1880d9dfc | |||
998bbd231a | |||
fbf2158488 | |||
94df551b4b | |||
01288154a7 | |||
29b4fe2962 | |||
5907d9f689 | |||
86d74619b1 | |||
0c09e808bd | |||
da8659adba | |||
bbebe78997 | |||
19aa804cbb | |||
64491c6fe1 | |||
382cb177be | |||
e9f0d19d9a | |||
7709cd9f6c | |||
83eac7bf04 | |||
db8197df3d | |||
40e404fe24 | |||
e7f80ebce8 | |||
990152d77d | |||
2e61479c75 | |||
0579ebb1c1 | |||
f9df86851c | |||
b0c62e226a | |||
1392b6e4a0 | |||
12163bc87b | |||
c645bb7610 | |||
e6574966da | |||
1e9cfff05e | |||
bd34c2b255 | |||
c040ac3200 | |||
6c6888c9cb | |||
78cb6922b3 | |||
c16377c688 | |||
540ea5c310 | |||
4ba2b25f18 | |||
81b75dcaa8 | |||
884784d024 | |||
e81ff2870d | |||
7c631099b2 | |||
f7a98b88a7 | |||
3fc21fff2a | |||
0306c2b726 | |||
9d822df54e | |||
f413b84a45 | |||
b82811e6bf | |||
af922664ab | |||
8b73e69495 | |||
29a9e14741 | |||
997fb27596 | |||
c171df0a93 | |||
1c15a4e07f | |||
5067c7b87f | |||
f679231ade | |||
f725e09f55 | |||
99881bf4cd | |||
2987464da8 | |||
4d04275c57 | |||
790f63e8c9 | |||
86b49812c3 | |||
ef9e0fd916 | |||
af8807f799 | |||
4eea13f059 | |||
ba93ed93e4 | |||
932a0fe176 | |||
3cff2d6469 | |||
df506e4128 | |||
eb80214785 | |||
80a4e361a4 | |||
2af88b2bee | |||
5048ccab8c | |||
66d13c92d5 | |||
836b076da9 | |||
1a75838a3b | |||
a18b7962a7 | |||
84548634ec | |||
419ebe61f7 | |||
c45817b127 | |||
2c0c0c5a11 | |||
1b8d6311ba | |||
f0f7a3f7d6 | |||
8304d774c3 | |||
faeea4e866 | |||
6470daf7d3 | |||
94139a3669 | |||
f3b1172d0e | |||
8057019898 | |||
9b59fc5dae | |||
75f4377de8 | |||
d3657716c4 | |||
a13990b96f | |||
4c7aa7fead | |||
32278533bf | |||
39908f7fc5 | |||
347d79c88d | |||
9bc117e9f5 | |||
ba5fbed9be | |||
211556d5ce | |||
066d7bc217 | |||
f072ceae37 | |||
273dac1ca7 | |||
f3b170ba14 | |||
78d8030ed6 | |||
a300d2f4cf | |||
96a658073e | |||
8395ba407d | |||
f80e3fed2b | |||
b48edf7409 | |||
e0a0eb089d | |||
60b126333d | |||
649a3f4fb5 | |||
9a326928f3 | |||
52f87ca18a | |||
ad2d2cb57f | |||
22c0021ada | |||
2cfc5731fb | |||
1d74e1536c | |||
e6b2ddf2ad | |||
62fda6224e | |||
083f8ebec8 | |||
aec8fb1168 | |||
d5d9b38ed6 | |||
9a6975e3d9 | |||
d1c40f2eb5 | |||
ad58661449 | |||
1d6cfd419e | |||
15182ea15a | |||
c43e00c88d | |||
25c7004368 | |||
02ba57cd64 | |||
c2dc6302c0 | |||
af9f30a123 | |||
28c5754800 | |||
48c1d48573 | |||
582bf0e12c | |||
46b30bbff4 | |||
372011ca08 | |||
7255792910 | |||
0c96b11b05 | |||
ea4da12d3b | |||
8419ebad10 | |||
09a5cf836a | |||
1a1a53b472 | |||
a8bad8447a | |||
ca8f5a8fbe | |||
a0e7aec228 | |||
26badc759e | |||
e21c2168ea | |||
589ab3664e | |||
48b4f9b160 | |||
139e416c3b | |||
ba4ed3b365 | |||
06a8022265 | |||
3b297e07dc | |||
68fa8e523e | |||
768bf06a9d | |||
6a8d66758a | |||
078e6e6f23 | |||
3f80bfb641 | |||
a34389c3a9 | |||
692d3c143f | |||
bc86a45925 | |||
2a412b794a | |||
bf673263d8 | |||
ce498d3019 | |||
e398f089af | |||
e8ba24e48b | |||
e0f40be188 | |||
42848764cf | |||
46ac0c339c | |||
a0fe68f3ab | |||
c952c3d031 | |||
c05cdeda72 | |||
ae7b285eb0 | |||
6b3c562af2 | |||
e3ae878cae | |||
dd43c94e6e | |||
0e8fe1fcfb | |||
293b7f564e | |||
634ffb4c57 | |||
9468a2e9f8 | |||
921ecd99d6 | |||
d06f92051d | |||
97d68f89f1 | |||
115863f1ee | |||
bc27a4d25a | |||
5a0b2f319b | |||
50f003a2bd | |||
bc0b50f5bf | |||
dd20b25900 | |||
d0469467ac | |||
61427ec505 | |||
6cd1d78572 | |||
0ad5049785 | |||
b5c1960260 | |||
e6fbe7c4a4 | |||
537536d443 | |||
f702d14749 | |||
74bb544f3c | |||
dae47e3779 | |||
dc91d0e00c | |||
6187eb3e4e | |||
0fa6d337f2 | |||
a10e1cad11 | |||
7c252deede | |||
224c5c4fe2 | |||
e945b5fe94 | |||
23e534c5c9 | |||
dcddf74830 | |||
9baf69394e |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something going wrong? File a bug report!
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: nicoverbruggen
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Required information**
|
||||
- Did you consult the FAQ in the README? [yes/no]
|
||||
- Did you try "Fix My Valet"? [yes/no]
|
||||
- OS: [e.g. macOS Monterey]
|
||||
- PHP Monitor version [e.g. v5.0.1]
|
||||
|
||||
**Additional 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.
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an enhancement.
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: nicoverbruggen
|
||||
|
||||
---
|
||||
|
||||
_Enhancement requests that are not immediately approved will be moved to a discussion instead._
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
23
.github/contributing.md
vendored
Normal 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!
|
2
.github/pull_request_template.md
vendored
@ -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.
|
||||
|
||||
|
1
.gitignore
vendored
@ -2,4 +2,5 @@ phpmon.xcodeproj/project.xcworkspace
|
||||
phpmon.xcodeproj/xcuserdata
|
||||
PHP Monitor.xcodeproj/project.xcworkspace
|
||||
PHP Monitor.xcodeproj/xcuserdata
|
||||
phpmon-updater/PHP Monitor Self-Updater.app
|
||||
.DS_Store
|
15
.swiftlint.yml
Normal 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
|
44
DEVELOPER.md
@ -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"/>
|
||||
@ -13,6 +37,26 @@ Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you
|
||||
|
||||
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
||||
|
||||
## 🚀 Release procedure
|
||||
|
||||
1. Merge into `main`
|
||||
2. Create tag
|
||||
3. Add changes to changelog + update security document
|
||||
4. Archive
|
||||
5. Notarize and prepare for own distribution
|
||||
6. After notarization, export .app
|
||||
7. Create zipped version
|
||||
8. Calculate SHA256: `openssl dgst -sha256 phpmon.zip`
|
||||
9. Upload to GitHub and add to tagged release
|
||||
10. Update Cask with new version + hash
|
||||
11. Check new version can be installed via Cask
|
||||
|
||||
## 🍱 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.
|
||||
|
2
LICENSE
@ -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
|
||||
|
@ -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 = "NO">
|
||||
</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>
|
@ -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"
|
||||
|
@ -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>
|
342
README.md
@ -1,17 +1,13 @@
|
||||
> 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" 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)!
|
||||
|
||||
@ -19,31 +15,60 @@ It's super convenient to switch between different versions of PHP. You'll even g
|
||||
|
||||
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
|
||||
|
||||
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 12.4 or later (Monterey and Ventura are supported)
|
||||
* 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:
|
||||
|
||||
To install via Homebrew, run:
|
||||
```sh
|
||||
composer global require laravel/valet
|
||||
valet install
|
||||
valet trust
|
||||
```
|
||||
|
||||
brew tap nicoverbruggen/homebrew-cask
|
||||
brew install --cask phpmon
|
||||
#### Manual installation (recommended, first time only)
|
||||
|
||||
To upgrade your existing installation, run:
|
||||
Once that's done, you can [download the latest release](https://github.com/nicoverbruggen/phpmon/releases/latest), unzip it and place it in `/Applications`.
|
||||
|
||||
brew upgrade phpmon
|
||||
#### Installation via Homebrew
|
||||
|
||||
(You may need to run `brew update` first in order to update the cask file if you ran a Homebrew operation recently.)
|
||||
*Prior to version 5.8, this was the recommended way of installing PHP Monitor.*
|
||||
|
||||
If you prefer to install the app via Homebrew, you can also run the following:
|
||||
|
||||
```sh
|
||||
brew tap nicoverbruggen/homebrew-cask
|
||||
brew install --cask phpmon
|
||||
```
|
||||
|
||||
## ⬆️ How to update
|
||||
|
||||
The recommended method of updating the app to the latest version is to use **the built-in updater**.
|
||||
|
||||
If you have a very slow internet connection, the updater may report that the download has timed out. In that case, you may wish to manually update by [downloading the latest release](https://github.com/nicoverbruggen/phpmon/releases/latest) and placing the app in `/Applications`.
|
||||
|
||||
(You may also use Homebrew to update PHP Monitor, but this will require you to approve the app every time an update is installed. If you use the built-in updater, this won't be necessary.)
|
||||
|
||||
## ⚡️ Launchers (Alfred, Raycast)
|
||||
|
||||
If you would like to integrate with your launcher of choice, you can also download an [Alfred workflow](https://github.com/nicoverbruggen/phpmon/raw/main/integrations/phpmon.alfredworkflow) or [Raycast extension](https://www.raycast.com/nicoverbruggen/php-monitor) that works with PHP Monitor.
|
||||
|
||||
The app must be running in the background for these to work, and the _Allow third-party integrations_ checkbox must be enabled in Preferences (it is by default).
|
||||
|
||||
## 🔑 Is the app signed & notarized?
|
||||
|
||||
@ -72,20 +97,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>
|
||||
@ -99,14 +147,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:
|
||||
@ -119,30 +167,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>
|
||||
@ -166,6 +277,8 @@ This problem is usually resolved by upgrading Valet and running `valet install`
|
||||
|
||||
composer global update
|
||||
valet install
|
||||
|
||||
If you are seeing a 502 (Bad Gateway) error after about 30 seconds or so, your request is likely timing out. You may need to solve a performance issue with your own code.
|
||||
|
||||
</details>
|
||||
|
||||
@ -197,6 +310,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>
|
||||
@ -231,16 +350,116 @@ Since v3.4 all of the loaded .ini files are sourced to determine which extension
|
||||
<details>
|
||||
<summary><strong>I've got two Homebrew installations on my Apple Silicon Mac, can I choose which installation to use with PHP Monitor?</strong></summary>
|
||||
|
||||
Not at this time, no. PHP Monitor will prefer the `/opt/homebrew` installation over the classic installation directory.
|
||||
If you are using PHP Monitor on an Intel machine or on an Apple Silicon machine with Rosetta enabled, PHP Monitor expects the main Homebrew binary in `/usr/local/bin/brew`.
|
||||
|
||||
If you are using PHP Monitor on Apple Silicon without Rosetta, PHP Monitor expects the main Homebrew binary in `/opt/homebrew/bin/brew`.
|
||||
|
||||
If there's an issue here, you'll get an alert at launch.
|
||||
|
||||
Make sure that the version of Homebrew that you are running normally is the same as the one that PHP Monitor expects. If you are on M1 hardware for example, but still using Rosetta for Homebrew, you'll need to run PHP Monitor under Rosetta as well.
|
||||
|
||||
PHP Monitor is a universal app and supports both architectures, so [find out here](https://support.apple.com/en-us/HT211861) how to enable Rosetta with PHP Monitor.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<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>
|
||||
|
||||
@ -255,7 +474,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>
|
||||
{
|
||||
@ -264,12 +483,17 @@ 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>
|
||||
<summary><strong>How can the app integrate with third party tools, like Alfred?</strong></summary>
|
||||
<summary><strong>How can the app integrate with third party tools, like Alfred or Raycast?</strong></summary>
|
||||
|
||||
There's an Alfred workflow usually included in the release list, you can grab it by going to releases and downloading the asset `phpmon.alfredworkflow`.
|
||||
PHP Monitor supports third party app integrations by default, and this feature is enabled in Preferences unless you disable it.
|
||||
|
||||
You can grab the official [Alfred workflow](https://github.com/nicoverbruggen/phpmon/raw/main/integrations/phpmon.alfredworkflow) or [Raycast extension](https://www.raycast.com/nicoverbruggen/php-monitor).
|
||||
|
||||
If you'd like to integrate something yourself, all you need to to is use the `phpmon://` protocol and ensure that third party app integrations are enabled in Preferences (in PHP Monitor).
|
||||
|
||||
@ -335,6 +559,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?
|
||||
@ -351,13 +579,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.
|
||||
|
||||
@ -373,7 +602,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 Valet’s 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 Valet’s 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
|
||||
|
||||
@ -389,7 +620,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?
|
||||
|
||||
@ -399,4 +631,4 @@ I have done my best to annotate as much as humanly possible, and have avoided us
|
||||
|
||||
I also have a few tests for key parts of the application that I found needed to be tested. In the future, I would like to add even more tests for some of the UI stuff, but for now the tests are more unit tests than feature tests.
|
||||
|
||||
For more detailed information for developers, please see [the documentation file for developers](./DEVS.md).
|
||||
For more detailed information for developers, please see [the documentation file for developers](./DEVELOPER.md).
|
||||
|
13
RELEASE.md
@ -1,13 +0,0 @@
|
||||
# Release Procedure
|
||||
|
||||
1. Merge into `main`
|
||||
2. Create tag
|
||||
3. Add changes to changelog + update security document
|
||||
4. Archive
|
||||
5. Notarize and prepare for own distribution
|
||||
6. After notarization, export .app
|
||||
7. Create zipped version
|
||||
8. Calculate SHA256: `openssl dgst -sha256 phpmon.zip`
|
||||
9. Upload to GitHub and add to tagged release
|
||||
10. Update Cask with new version + hash
|
||||
11. Check new version can be installed via Cask
|
12
SECURITY.md
@ -4,9 +4,9 @@
|
||||
|
||||
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
|
||||
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 5.0 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||
| 5.8 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | 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 |
|
||||
|
||||
## Legacy versions
|
||||
|
||||
@ -14,9 +14,11 @@ These versions of PHP Monitor are no longer supported, but if you’re 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.7 | ✅ 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 |
|
||||
| 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 |
|
||||
|
BIN
assets/affinity/icon.afdesign
Normal file
BIN
assets/affinity/icon_se.afdesign
Normal file
BIN
assets/affinity/icons.afdesign
Normal file
BIN
docs/logo.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
docs/screenshot.jpg
Normal file
After Width: | Height: | Size: 627 KiB |
Before Width: | Height: | Size: 370 KiB |
@ -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>
|
@ -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"])
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
@ -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 ValetTest: XCTestCase {
|
||||
|
||||
func testDetermineValetVersion() {
|
||||
let version = valet("--version")
|
||||
XCTAssert(version.contains("Laravel Valet 2."))
|
||||
}
|
||||
|
||||
}
|
38
phpmon/Assets.xcassets/AppColor.colorset/Contents.json
Normal 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
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 585 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 134 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_128x128.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_128x128@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_16x16.png
Normal file
After Width: | Height: | Size: 632 B |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_16x16@2x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_256x256.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_256x256@2x.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_32x32.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_32x32@2x.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_512x512.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_512x512@2x.png
Normal file
After Width: | Height: | Size: 163 KiB |
38
phpmon/Assets.xcassets/AppSecondary.colorset/Contents.json
Normal 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
|
||||
}
|
||||
}
|
@ -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"
|
||||
},
|
BIN
phpmon/Assets.xcassets/IconDefault.imageset/Default.png
vendored
Normal file
After Width: | Height: | Size: 861 B |
BIN
phpmon/Assets.xcassets/IconDefault.imageset/Default@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
@ -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"
|
||||
},
|
BIN
phpmon/Assets.xcassets/IconProxy.imageset/Proxy.png
vendored
Normal file
After Width: | Height: | Size: 935 B |
BIN
phpmon/Assets.xcassets/IconProxy.imageset/Proxy@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
@ -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"
|
||||
},
|
BIN
phpmon/Assets.xcassets/Isolated.imageset/Isolated.png
vendored
Normal file
After Width: | Height: | Size: 690 B |
BIN
phpmon/Assets.xcassets/Isolated.imageset/Isolated@2x.png
vendored
Normal file
After Width: | Height: | Size: 827 B |
Before Width: | Height: | Size: 854 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 826 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 819 B |
Before Width: | Height: | Size: 1.2 KiB |
@ -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
|
||||
}
|
||||
}
|
38
phpmon/Assets.xcassets/StatusColorRed.colorset/Contents.json
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
25
phpmon/Common/Command/ActiveCommand.swift
Normal 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()
|
||||
}
|
||||
}
|
22
phpmon/Common/Command/CommandProtocol.swift
Normal 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
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -2,145 +2,126 @@
|
||||
// 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)", 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)", 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)", 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)", sudo: Homebrew.Formulae.php.elevated)
|
||||
await brew("services stop \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated)
|
||||
await brew("services stop \(Homebrew.Formulae.dnsmasq)", 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 source = "do shell script \"\(script)\" with administrator privileges"
|
||||
|
||||
Log.perf(source)
|
||||
let appleScript = NSAppleScript(source: source)
|
||||
|
||||
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.
|
||||
|
||||
This all happens in sequence, nothing runs in parallel.
|
||||
|
||||
After this, the brew services are also stopped,
|
||||
the latest PHP version is linked, and php + nginx are restarted.
|
||||
|
||||
If this does not solve the issue, the user may need to install additional
|
||||
extensions and/or run `composer global update`.
|
||||
*/
|
||||
public static func fixMyValet()
|
||||
{
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
|
||||
PhpEnv.shared.detectPhpVersions().forEach { (version) in
|
||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
brew("unlink php@\(version)")
|
||||
brew("services stop \(formula)")
|
||||
brew("services stop \(formula)", sudo: true)
|
||||
}
|
||||
|
||||
brew("services stop dnsmasq")
|
||||
brew("services stop php")
|
||||
brew("services stop nginx")
|
||||
|
||||
brew("link php --overwrite --force")
|
||||
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
brew("services restart php", sudo: true)
|
||||
brew("services restart nginx", sudo: true)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -2,58 +2,86 @@
|
||||
// Constants.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Constants {
|
||||
|
||||
/**
|
||||
* The latest PHP version that is considered to be stable at the time of release.
|
||||
* This version number is currently not used (only as a default fallback).
|
||||
*/
|
||||
static let LatestStablePhpVersion = "8.1"
|
||||
|
||||
struct Constants {
|
||||
|
||||
/**
|
||||
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 URL that people can visit if they wish to help support the project.
|
||||
The PHP versions supported by each version of Valet.
|
||||
*/
|
||||
static let DonationUrl = URL(string: "https://nicoverbruggen.be/sponsor#pay-now")!
|
||||
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 {
|
||||
|
||||
// phpmon.app URLs (these are aliased to redirect correctly)
|
||||
|
||||
static let DonationPage = URL(
|
||||
string: "https://phpmon.app/sponsor"
|
||||
)!
|
||||
|
||||
static let FrequentlyAskedQuestions = URL(
|
||||
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"
|
||||
)!
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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")
|
||||
|
||||
|
||||
}
|
||||
|
@ -3,53 +3,57 @@
|
||||
// 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
|
||||
|
||||
/**
|
||||
Runs a `valet` command.
|
||||
Runs a `valet` command. Defaults to running as superuser.
|
||||
*/
|
||||
func valet(_ command: String) -> String
|
||||
{
|
||||
return Shell.pipe("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 Shell.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))
|
||||
}
|
||||
|
60
phpmon/Common/Core/Homebrew.swift
Normal file
@ -0,0 +1,60 @@
|
||||
//
|
||||
// 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, CustomStringConvertible {
|
||||
let name: String
|
||||
let elevated: Bool
|
||||
|
||||
var description: String {
|
||||
return name
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -3,50 +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("[ERR] \(item)")
|
||||
Log.shared.log("[E] \(item)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func warn(_ item: Any) {
|
||||
if Verbosity.warning.isApplicable() {
|
||||
print("[WRN] \(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(item)
|
||||
Log.shared.log("[P] \(item)")
|
||||
}
|
||||
}
|
||||
|
||||
static func separator(as verbosity: Verbosity = .info) {
|
||||
if verbosity.isApplicable() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,63 +12,105 @@ 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()
|
||||
|
||||
private var baseDir : Paths.HomebrewDir
|
||||
|
||||
private var userName : String
|
||||
|
||||
|
||||
internal var baseDir: Paths.HomebrewDir
|
||||
private var userName: String
|
||||
|
||||
init() {
|
||||
baseDir = FileManager.default.fileExists(atPath: "\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr
|
||||
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
|
||||
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
||||
userName = identity()
|
||||
Log.info("[ID] The current username is `\(userName)`.")
|
||||
}
|
||||
|
||||
|
||||
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?
|
||||
|
||||
// - 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"
|
||||
}
|
||||
|
||||
|
||||
public static var caskroomPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||
}
|
||||
|
||||
// MARK: - Flexible Binaries
|
||||
// (these can be in multiple locations, so we scan common places because)
|
||||
// (PHP Monitor will not use the user's own PATH)
|
||||
|
||||
private func detectComposerBinary() {
|
||||
if FileSystem.fileExists("/usr/local/bin/composer") {
|
||||
Paths.composer = "/usr/local/bin/composer"
|
||||
} else if FileSystem.fileExists("/opt/homebrew/bin/composer") {
|
||||
Paths.composer = "/opt/homebrew/bin/composer"
|
||||
} else {
|
||||
Paths.composer = nil
|
||||
Log.warn("Composer was not found.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Enum
|
||||
|
||||
|
||||
public enum HomebrewDir: String {
|
||||
case opt = "/opt/homebrew"
|
||||
case usr = "/usr/local"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
62
phpmon/Common/Core/Process.swift
Normal file
@ -0,0 +1,62 @@
|
||||
//
|
||||
// Process.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 23/02/2022.
|
||||
// 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`
|
||||
allows us to react whenever data is received by running a particular closure,
|
||||
depending on which type of data is received.
|
||||
*/
|
||||
public func listen(
|
||||
didReceiveStandardOutputData: @escaping (String) -> Void,
|
||||
didReceiveStandardErrorData: @escaping (String) -> Void
|
||||
) {
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
self.standardOutput = outputPipe
|
||||
self.standardError = errorPipe
|
||||
|
||||
[
|
||||
(outputPipe, didReceiveStandardOutputData),
|
||||
(errorPipe, didReceiveStandardErrorData)
|
||||
].forEach { (pipe: Pipe, callback: @escaping (String) -> Void) in
|
||||
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSNotification.Name.NSFileHandleDataAvailable,
|
||||
object: pipe.fileHandleForReading,
|
||||
queue: nil
|
||||
) { _ in
|
||||
if let outputString = String(
|
||||
data: pipe.fileHandleForReading.availableData,
|
||||
encoding: String.Encoding.utf8
|
||||
) {
|
||||
callback(outputString)
|
||||
}
|
||||
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
After the process is done running, you'll want to stop listening.
|
||||
*/
|
||||
public func haltListening() {
|
||||
if let pipe = self.standardOutput as? Pipe {
|
||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||
}
|
||||
if let pipe = self.standardError as? Pipe {
|
||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,180 +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
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if a file exists at a certain path.
|
||||
Used to be done with a shell command, now uses the native FileManager class instead.
|
||||
*/
|
||||
public static func fileExists(_ path: String) -> Bool {
|
||||
let fullPath = path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
|
||||
return FileManager.default.fileExists(atPath: fullPath)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new process with the correct PATH and shell.
|
||||
*/
|
||||
public func createTask(for command: String, requiresPath: Bool) -> Process {
|
||||
let tailoredCommand = requiresPath
|
||||
? "export PATH=\(Paths.binPath):$PATH && \(command)"
|
||||
: command
|
||||
|
||||
let task = Process()
|
||||
task.launchPath = self.shell
|
||||
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
public static func captureOutput(
|
||||
_ task: Process,
|
||||
didReceiveStdOutData: @escaping (String) -> Void,
|
||||
didReceiveStdErrData: @escaping (String) -> Void
|
||||
) {
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
|
||||
[(outputPipe, didReceiveStdOutData), (errorPipe, didReceiveStdErrData)].forEach {
|
||||
(pipe: Pipe, callback: @escaping (String) -> Void) in
|
||||
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSNotification.Name.NSFileHandleDataAvailable,
|
||||
object: pipe.fileHandleForReading,
|
||||
queue: nil
|
||||
) { notification in
|
||||
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
|
||||
callback(outputString)
|
||||
}
|
||||
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func haltCapturingOutput(_ task: Process) {
|
||||
if let pipe = task.standardOutput as? Pipe {
|
||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||
}
|
||||
if let pipe = task.standardError as? Pipe {
|
||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||
}
|
||||
}
|
||||
|
||||
public class Output {
|
||||
public let standardOutput: String
|
||||
public let errorOutput: String
|
||||
public let task: Process
|
||||
|
||||
init(standardOutput: String,
|
||||
errorOutput: String,
|
||||
task: Process) {
|
||||
self.standardOutput = standardOutput
|
||||
self.errorOutput = errorOutput
|
||||
self.task = task
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)"
|
||||
}
|
||||
|
24
phpmon/Common/Extensions/ArrayExtension.swift
Normal 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
|
||||
}
|
||||
}
|
21
phpmon/Common/Extensions/DataExtension.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
17
phpmon/Common/Extensions/DictionaryExtension.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
87
phpmon/Common/Extensions/NSMenuItemExtension.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
52
phpmon/Common/Extensions/NSWindowExtension.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// NSWindowExtension.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 17/02/2022.
|
||||
// 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() {
|
||||
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
|
||||
|
||||
let frame: CGRect = self.frame
|
||||
let shakeAnimation: CAKeyframeAnimation = CAKeyframeAnimation()
|
||||
|
||||
let shakePath = CGMutablePath()
|
||||
shakePath.move( to: CGPoint(x: frame.minX, y: frame.minY))
|
||||
|
||||
for _ in 0...numberOfShakes-1 {
|
||||
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.animator().setFrameOrigin(self.frame.origin)
|
||||
}
|
||||
}
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
phpmon/Common/Extensions/TimeIntervalExtension.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
26
phpmon/Common/Filesystem/ActiveFileSystem.swift
Normal 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()
|
||||
}
|
||||
}
|
50
phpmon/Common/Filesystem/FileSystemProtocol.swift
Normal 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
|
||||
}
|
129
phpmon/Common/Filesystem/RealFileSystem.swift
Normal file
@ -0,0 +1,129 @@
|
||||
//
|
||||
// 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.replacingTildeWithHomeDirectory)
|
||||
}
|
||||
|
||||
func getDestinationOfSymlink(_ path: String) throws -> String {
|
||||
return try FileManager.default.destinationOfSymbolicLink(atPath: path.replacingTildeWithHomeDirectory)
|
||||
}
|
||||
|
||||
// MARK: - Move & Delete Files
|
||||
|
||||
func move(from path: String, to newPath: String) throws {
|
||||
try FileManager.default.moveItem(
|
||||
atPath: path.replacingTildeWithHomeDirectory,
|
||||
toPath: newPath.replacingTildeWithHomeDirectory
|
||||
)
|
||||
}
|
||||
|
||||
func remove(_ path: String) throws {
|
||||
try FileManager.default.removeItem(atPath: path.replacingTildeWithHomeDirectory)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
@ -2,46 +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 present(
|
||||
messageText: String,
|
||||
informativeText: String,
|
||||
buttonTitle: String = "OK",
|
||||
secondButtonTitle: String = "",
|
||||
style: NSAlert.Style = .informational
|
||||
) -> Bool {
|
||||
let alert = NSAlert.init()
|
||||
alert.alertStyle = style
|
||||
alert.messageText = messageText
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: buttonTitle)
|
||||
if (!secondButtonTitle.isEmpty) {
|
||||
alert.addButton(withTitle: secondButtonTitle)
|
||||
}
|
||||
return alert.runModal() == .alertFirstButtonReturn
|
||||
}
|
||||
|
||||
|
||||
public static func confirm(
|
||||
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
|
||||
@ -50,32 +36,5 @@ class Alert {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Notify the user about something by showing an alert.
|
||||
*/
|
||||
public static func notify(message: String, info: String, style: NSAlert.Style = .informational) {
|
||||
_ = present(
|
||||
messageText: message,
|
||||
informativeText: info,
|
||||
buttonTitle: "OK",
|
||||
secondButtonTitle: "",
|
||||
style: style
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Notify the user about a particular error (which must be `Alertable`)
|
||||
by showing an alert.
|
||||
*/
|
||||
public static func notify(about error: Error & AlertableError) {
|
||||
let key = error.getErrorMessageKey()
|
||||
_ = present(
|
||||
messageText: "\(key).title".localized,
|
||||
informativeText: "\(key).description".localized,
|
||||
buttonTitle: "OK",
|
||||
secondButtonTitle: "",
|
||||
style: .critical
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +0,0 @@
|
||||
//
|
||||
// Async.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 23/01/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This generic async helper is something I'd like to use in more places.
|
||||
|
||||
The `DispatchQueue.global` into `DispatchQueue.main.async` logic is common in the app.
|
||||
|
||||
I could also use try `async` support which was introduced in Swift but that would
|
||||
require too much refactoring at this time to consider. I also need to read up on async
|
||||
in order to properly grasp all the gotchas. Looking into that later at some point.
|
||||
*/
|
||||
public func runAsync(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
|
||||
{
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
execute()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -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 preference != nil && !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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
25
phpmon/Common/Helpers/LoginItemManager.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// LoginItemManager.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 15/02/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import ServiceManagement
|
||||
|
||||
@available(macOS 13.0, *)
|
||||
class LoginItemManager {
|
||||
func loginItemIsEnabled() -> Bool {
|
||||
return SMAppService.mainApp.status == .enabled
|
||||
}
|
||||
|
||||
func disableLoginItem() {
|
||||
try? SMAppService.mainApp.unregister()
|
||||
}
|
||||
|
||||
func enableLoginItem() {
|
||||
try? SMAppService.mainApp.register()
|
||||
}
|
||||
}
|