Compare commits
172 Commits
Author | SHA1 | Date | |
---|---|---|---|
efd902b4f3 | |||
918e272da7 | |||
272a9182d3 | |||
63f85aff91 | |||
581c2d1974 | |||
4deef64537 | |||
bedabaa3bb | |||
9da3772212 | |||
e62b03d070 | |||
9a11d2efed | |||
b134e62328 | |||
5c69133c42 | |||
f4448e0640 | |||
7fd30d7c54 | |||
2c57dea97f | |||
a77fa5557a | |||
45704fc736 | |||
f28354e634 | |||
8055a32bde | |||
5b3054326e | |||
e7f3c7e59c | |||
a407515534 | |||
b827ffb869 | |||
bdb718598e | |||
ddfc73e033 | |||
cfae520984 | |||
0c176493e5 | |||
71da62f954 | |||
d6781568a3 | |||
c9c7e14416 | |||
0947dc5ecc | |||
286cdd00e9 | |||
42b79d3cb3 | |||
36aa41568c | |||
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 | |||
f679231ade | |||
f725e09f55 | |||
99881bf4cd |
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.
|
||||
|
||||
|
@ -41,6 +41,12 @@ If you'd like to create a production build, choose "Any Mac" as the target and s
|
||||
10. Update Cask with new version + hash
|
||||
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.
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@ -69,8 +69,13 @@
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "PHPMON_MARKETING_MODE"
|
||||
value = "YES"
|
||||
key = "EXTREME_DOCTOR_MODE"
|
||||
value = ""
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
|
||||
value = ""
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
|
176
README.md
@ -1,16 +1,19 @@
|
||||
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️
|
||||
> **Note**
|
||||
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider [sponsoring](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️
|
||||
|
||||
<p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p>
|
||||
|
||||
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
|
||||
|
||||
<img src="./docs/screenshot.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
|
||||
<img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
|
||||
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
|
||||
|
||||
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
|
||||
|
||||
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
|
||||
|
||||
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
|
||||
<img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
|
||||
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
|
||||
|
||||
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
|
||||
|
||||
@ -21,10 +24,10 @@ You can also add new domains as links, isolate sites, manage various services, a
|
||||
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
|
||||
|
||||
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
|
||||
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
|
||||
* macOS 11 Big Sur or later
|
||||
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
|
||||
* Homebrew `php` formula is installed
|
||||
* Laravel Valet 2.16 or newer (supports Valet 3)
|
||||
* Laravel Valet 3 recommended (but compatible with Valet 2)
|
||||
|
||||
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
|
||||
|
||||
@ -85,7 +88,8 @@ If you're still having issues, here's a few common questions & answers, as well
|
||||
<li>PHP 7.4</li>
|
||||
<li>PHP 8.0</li>
|
||||
<li>PHP 8.1</li>
|
||||
<li>PHP 8.2 (experimental)</li>
|
||||
<li>PHP 8.2</li>
|
||||
<li>PHP 8.3 (experimental)</li>
|
||||
</ul>
|
||||
|
||||
For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Common/Core/Constants.swift#L16) file to see which versions are supported.
|
||||
@ -95,7 +99,9 @@ For more details, consult the [constants file](https://github.com/nicoverbruggen
|
||||
<details>
|
||||
<summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary>
|
||||
|
||||
You can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account.
|
||||
On macOS Ventura, you can accomplish this by going to **System Settings > General > Login Items** and adding PHP Monitor.app to the list **Open at Login**. You can do this with any application, by the way.
|
||||
|
||||
On older versions of macOS, you can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account.
|
||||
|
||||
Super convenient!
|
||||
</details>
|
||||
@ -103,14 +109,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:
|
||||
@ -123,30 +129,61 @@ 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>
|
||||
@ -297,6 +334,95 @@ The app includes an Internet Access Policy file, so if you're using something li
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I various presets to show up?</strong></summary>
|
||||
|
||||
You must set these presets up in a JSON file, located in `~/.config/phpmon/config.json`.
|
||||
|
||||
You must have set up at least one valid preset for this presets to work in PHP Monitor.
|
||||
|
||||
Here's an example of a working preset:
|
||||
|
||||
<pre>
|
||||
{
|
||||
"scan_apps": [],
|
||||
"services": [],
|
||||
"presets": [
|
||||
{
|
||||
"name": "Legacy Project",
|
||||
"php": "8.0",
|
||||
"extensions": {
|
||||
"xdebug": false
|
||||
},
|
||||
"configuration": {
|
||||
"memory_limit": "128M",
|
||||
"upload_max_filesize": "128M",
|
||||
"post_max_size": "128M"
|
||||
}
|
||||
}
|
||||
],
|
||||
"export": {}
|
||||
}
|
||||
</pre>
|
||||
|
||||
You can omit the `php` key in the preset if you do not wish for the preset to switch to a given PHP version.
|
||||
|
||||
> **Warning**
|
||||
> You must restart PHP Monitor for these changes to be detected.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I ensure additional Homebrew services are shown in the app?</strong></summary>
|
||||
|
||||
You must set these services up in a JSON file, located in `~/.config/phpmon/config.json`.
|
||||
|
||||
You can specify custom services in the configuration file for Homebrew services that run as your own user (not root).
|
||||
|
||||
> **Info**
|
||||
> If your service must run as root, it cannot currently be added to PHP Monitor.
|
||||
|
||||
You can find out which services are available by running `brew services list`.
|
||||
|
||||
Here's an example where we add the `mailhog` and `mysql` services to PHP Monitor:
|
||||
|
||||
<pre>
|
||||
{
|
||||
"scan_apps": [],
|
||||
"services": ["mailhog", "mysql"],
|
||||
"presets": [],
|
||||
"export": {}
|
||||
}
|
||||
</pre>
|
||||
|
||||
> **Warning**
|
||||
> You must restart PHP Monitor for these changes to be detected.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I set custom environment variables?</strong></summary>
|
||||
|
||||
You must configure these custom environment variables up in a JSON file, located in `~/.config/phpmon/config.json`.
|
||||
|
||||
PHP Monitor uses a default Shell environment, with no custom environment variables. You need to set custom environment variables manually. These are then used for e.g. Composer.
|
||||
|
||||
Here's an example of a working `COMPOSER_HOME` environment variable which is respected:
|
||||
|
||||
<pre>
|
||||
{
|
||||
"scan_apps": [],
|
||||
"services": [],
|
||||
"presets": [],
|
||||
"export": {
|
||||
"COMPOSER_HOME": "/absolute/path/to/composer/folder"
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
> **Warning**
|
||||
> You must restart PHP Monitor for these changes to be detected.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I get various applications to show up in the domain list's right-click menu?</strong></summary>
|
||||
|
||||
@ -308,7 +434,7 @@ All of these apps should just be detected correctly, no matter their location on
|
||||
|
||||
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
|
||||
|
||||
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>
|
||||
{
|
||||
@ -317,6 +443,9 @@ You can add your own apps by creating and editing a `~/.phpmon.conf.json` file,
|
||||
</pre>
|
||||
|
||||
You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary.
|
||||
|
||||
> **Warning**
|
||||
> You must restart PHP Monitor for these changes to be detected.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -406,14 +535,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, PHP Monitor started out from various learning experiments during work hours at my employer, DIVE. I'd also like to shout out the following folks:
|
||||
Special thanks go out to:
|
||||
|
||||
* My colleagues at [DIVE](https://dive.be)
|
||||
* Everyone supporting me via [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen)
|
||||
* Everyone who has donated via [my sponsor page](https://nicoverbruggen.be/sponsor)
|
||||
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors)
|
||||
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so
|
||||
* My [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen) and those who have donated
|
||||
* Everyone who has left feedback and reported bugs (appreciate it!)
|
||||
* Everyone in the Laravel community who shared the app (thanks!)
|
||||
* Everyone 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.
|
||||
|
||||
@ -447,7 +576,8 @@ If an extension or other process writes to a single file a bunch of times in a s
|
||||
1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name.
|
||||
1. **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?
|
||||
|
||||
|
10
SECURITY.md
@ -6,9 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
|
||||
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 (*) | 3.0 (2.16.2 minimum) |
|
||||
|
||||
_(*) Support for PHP 5.6 is only included if you are using Valet 2.x, since support for PHP 5.6 was dropped in Valet 3.0._
|
||||
| 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
||||
|
||||
## Legacy versions
|
||||
|
||||
@ -16,9 +14,9 @@ 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 |
|
||||
| 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
docs/notification-dark.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
docs/screenshot-dark.jpg
Normal file
After Width: | Height: | Size: 524 KiB |
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 519 KiB |
@ -37,43 +37,43 @@ class NginxConfigurationTest: XCTestCase {
|
||||
func testCanDetermineSiteNameAndTld() throws {
|
||||
XCTAssertEqual(
|
||||
"nginx-site",
|
||||
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
|
||||
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
|
||||
)
|
||||
XCTAssertEqual(
|
||||
"test",
|
||||
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld
|
||||
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld
|
||||
)
|
||||
}
|
||||
|
||||
func testCanDetermineIsolation() throws {
|
||||
XCTAssertNil(
|
||||
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
|
||||
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
"8.1",
|
||||
NginxConfiguration.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion
|
||||
NginxConfigurationFile.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion
|
||||
)
|
||||
}
|
||||
|
||||
func testCanDetermineProxy() throws {
|
||||
let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.proxyUrl.path)!
|
||||
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.proxyUrl.path)!
|
||||
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
|
||||
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
||||
|
||||
let normal = NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)!
|
||||
let normal = NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)!
|
||||
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
|
||||
XCTAssertEqual(nil, normal.proxy)
|
||||
}
|
||||
|
||||
func testCanDetermineSecuredProxy() throws {
|
||||
let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.secureProxyUrl.path)!
|
||||
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.secureProxyUrl.path)!
|
||||
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
|
||||
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
||||
}
|
||||
|
||||
func testCanDetermineProxyWithCustomTld() throws {
|
||||
let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)!
|
||||
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)!
|
||||
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
|
||||
XCTAssertEqual("http://localhost:8080", proxied.proxy)
|
||||
}
|
||||
|
84
phpmon-tests/Parsers/PhpConfigurationTest.swift
Normal file
@ -0,0 +1,84 @@
|
||||
//
|
||||
// PhpConfigurationTest.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 04/05/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class PhpConfigurationTest: XCTestCase {
|
||||
|
||||
static var phpIniFileUrl: URL {
|
||||
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
|
||||
}
|
||||
|
||||
func testCanLoadExtension() throws {
|
||||
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
|
||||
|
||||
XCTAssertNotNil(iniFile)
|
||||
|
||||
XCTAssertGreaterThan(iniFile.extensions.count, 0)
|
||||
}
|
||||
|
||||
func testCanCheckKeyExistence() throws {
|
||||
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
|
||||
|
||||
XCTAssertTrue(iniFile.has(key: "error_reporting"))
|
||||
XCTAssertTrue(iniFile.has(key: "display_errors"))
|
||||
XCTAssertFalse(iniFile.has(key: "my_unknown_key"))
|
||||
}
|
||||
|
||||
func testCanCheckKeyValue() throws {
|
||||
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
|
||||
|
||||
XCTAssertNotNil(iniFile.get(for: "error_reporting"))
|
||||
XCTAssert(iniFile.get(for: "error_reporting") == "E_ALL")
|
||||
|
||||
XCTAssertNotNil(iniFile.get(for: "display_errors"))
|
||||
XCTAssert(iniFile.get(for: "display_errors") == "On")
|
||||
}
|
||||
|
||||
func testCanCustomizeConfigurationValue() throws {
|
||||
let destination = Utility
|
||||
.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
||||
|
||||
let configurationFile = PhpConfigurationFile
|
||||
.from(filePath: destination.path)!
|
||||
|
||||
// 0. Verify the original value
|
||||
XCTAssertEqual(configurationFile.get(for: "error_reporting"), "E_ALL")
|
||||
|
||||
// 1. Change the value
|
||||
try! configurationFile.replace(
|
||||
key: "error_reporting",
|
||||
value: "E_ALL & ~E_DEPRECATED & ~E_STRICT"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
configurationFile.get(for: "error_reporting"),
|
||||
"E_ALL & ~E_DEPRECATED & ~E_STRICT"
|
||||
)
|
||||
|
||||
// 2. Ensure that same key and value doesn't break subsequent saves
|
||||
try! configurationFile.replace(
|
||||
key: "error_reporting",
|
||||
value: "error_reporting"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
configurationFile.get(for: "error_reporting"),
|
||||
"error_reporting"
|
||||
)
|
||||
|
||||
// 3. Verify subsequent saves weren't broken
|
||||
try! configurationFile.replace(
|
||||
key: "error_reporting",
|
||||
value: "E_ALL"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
configurationFile.get(for: "error_reporting"),
|
||||
"E_ALL"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -15,13 +15,13 @@ class PhpExtensionTest: XCTestCase {
|
||||
}
|
||||
|
||||
func testCanLoadExtension() throws {
|
||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
|
||||
|
||||
XCTAssertGreaterThan(extensions.count, 0)
|
||||
}
|
||||
|
||||
func testExtensionNameIsCorrect() throws {
|
||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
|
||||
|
||||
let extensionNames = extensions.map { (ext) -> String in
|
||||
return ext.name
|
||||
@ -40,7 +40,7 @@ class PhpExtensionTest: XCTestCase {
|
||||
}
|
||||
|
||||
func testExtensionStatusIsCorrect() throws {
|
||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
|
||||
|
||||
// xdebug should be enabled
|
||||
XCTAssertEqual(extensions[0].enabled, true)
|
||||
@ -51,7 +51,7 @@ class PhpExtensionTest: XCTestCase {
|
||||
|
||||
func testToggleWorksAsExpected() throws {
|
||||
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
||||
let extensions = PhpExtension.load(from: destination)
|
||||
let extensions = PhpExtension.from(filePath: destination.path)
|
||||
XCTAssertEqual(extensions.count, 6)
|
||||
|
||||
// Try to disable xdebug (should be detected first)!
|
||||
@ -66,12 +66,7 @@ class PhpExtensionTest: XCTestCase {
|
||||
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
|
||||
|
||||
// Make sure if we load the data again, it's disabled
|
||||
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
|
||||
}
|
||||
|
||||
func testCanRetrieveXdebugMode() throws {
|
||||
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
|
||||
XCTAssertEqual(value, "coverage")
|
||||
XCTAssertEqual(PhpExtension.from(filePath: destination.path).first!.enabled, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
34
phpmon-tests/Test Files/phpmon/phpmon-config.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"scan_apps": [],
|
||||
"presets": [
|
||||
{
|
||||
"name": "Default PHP",
|
||||
"extensions": {
|
||||
"xdebug": false
|
||||
},
|
||||
"configuration": {
|
||||
"memory_limit": "128M"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Personal Site",
|
||||
"extensions": {
|
||||
"xdebug": true
|
||||
},
|
||||
"configuration": {
|
||||
"xdebug.mode": "coverage",
|
||||
"memory_limit": "512M"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PHP Monitor",
|
||||
"extensions": {
|
||||
"xdebug": true
|
||||
},
|
||||
"configuration": {
|
||||
"xdebug.mode": "coverage",
|
||||
"memory_limit": "512M"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -18,4 +18,30 @@ class AppUpdaterCheckTest: XCTestCase {
|
||||
XCTAssertNotNil(version)
|
||||
}
|
||||
|
||||
func testTaggedReleaseOmitsZeroPatch() {
|
||||
let version = AppVersion.from("3.5.0_333")!
|
||||
|
||||
XCTAssertEqual(version.tagged, "3.5")
|
||||
XCTAssertEqual(version.version, "3.5.0")
|
||||
}
|
||||
|
||||
func testTaggedReleaseDoesntOmitNonZeroPatch() {
|
||||
let version = AppVersion.from("3.5.1_333")!
|
||||
|
||||
XCTAssertEqual(version.tagged, "3.5.1")
|
||||
XCTAssertEqual(version.version, "3.5.1")
|
||||
}
|
||||
|
||||
func testTagTruncationDoesntAffectMajorVersions() {
|
||||
var version = AppVersion.from("5.0_333")!
|
||||
|
||||
XCTAssertEqual(version.tagged, "5.0")
|
||||
XCTAssertEqual(version.version, "5.0")
|
||||
|
||||
version = AppVersion.from("5.0.0_333")!
|
||||
|
||||
XCTAssertEqual(version.tagged, "5.0")
|
||||
XCTAssertEqual(version.version, "5.0.0")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
class PhpVersionNumberTest: XCTestCase {
|
||||
|
||||
func testCanDeconstructPhpVersion() throws {
|
||||
@ -287,4 +288,76 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
.make(from: ["7.3.1", "7.2.9"]).all
|
||||
)
|
||||
}
|
||||
|
||||
func testCanCheckLessThanOrEqualConstraints() throws {
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "<=7.2", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "<=7.2.0", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "<=7.2.5", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
// Non-strict check (ignoring patch has no effect)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "<=7.2.5", strict: false),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
}
|
||||
|
||||
func testCanCheckLessThanConstraints() throws {
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "<7.2", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "<7.2.0", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "<7.2.5", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
// Non-strict check (patch resolves to 7.2.999, which is bigger than 7.2.5)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "<7.2.5", strict: false),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.1", "7.0"]).all
|
||||
)
|
||||
}
|
||||
}
|
||||
|
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.277"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.988",
|
||||
"green" : "0.723",
|
||||
"red" : "0.277"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
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,25 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ServiceLoading.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "ServiceLoading@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 854 B |
Before Width: | Height: | Size: 1.3 KiB |
@ -1,25 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ServiceOff.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "ServiceOff@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 826 B |
Before Width: | Height: | Size: 1.2 KiB |
@ -1,25 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ServiceOn.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "ServiceOn@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 819 B |
Before Width: | Height: | Size: 1.2 KiB |
@ -24,7 +24,7 @@ class Actions {
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
}
|
||||
|
||||
public static func stopAllServices() {
|
||||
public static func stopValetServices() {
|
||||
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
|
||||
brew("services stop nginx", sudo: true)
|
||||
brew("services stop dnsmasq", sudo: true)
|
||||
@ -64,6 +64,29 @@ class Actions {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Third Party Services
|
||||
public static func stopService(name: String, completion: @escaping () -> Void) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
brew("services stop \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
|
||||
ServicesManager.loadHomebrewServices(completed: {
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public static func startService(name: String, completion: @escaping () -> Void) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
brew("services start \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
|
||||
ServicesManager.loadHomebrewServices(completed: {
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Finding Config Files
|
||||
|
||||
public static func openGenericPhpConfigFolder() {
|
||||
@ -88,6 +111,12 @@ class Actions {
|
||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||
}
|
||||
|
||||
public static func openPhpMonitorConfigFile() {
|
||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".config/phpmon")
|
||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||
}
|
||||
|
||||
// MARK: - Other Actions
|
||||
|
||||
public static func createTempPhpInfoFile() -> URL {
|
||||
|
@ -9,12 +9,6 @@ import Cocoa
|
||||
|
||||
struct Constants {
|
||||
|
||||
/**
|
||||
* The latest PHP version that is considered to be stable at the time of release.
|
||||
* This version number is currently not used (only as a default fallback).
|
||||
*/
|
||||
static let LatestStablePhpVersion = "8.1"
|
||||
|
||||
/**
|
||||
The minimum version of Valet that is recommended.
|
||||
If the installed version is older, a notification will be shown
|
||||
@ -42,29 +36,34 @@ struct Constants {
|
||||
"7.4",
|
||||
"8.0",
|
||||
"8.1",
|
||||
"8.2",
|
||||
|
||||
// ====================
|
||||
// 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"
|
||||
"8.3"
|
||||
]
|
||||
|
||||
struct Urls {
|
||||
|
||||
static let DonationPayment = URL(
|
||||
string: "https://nicoverbruggen.be/sponsor#pay-now"
|
||||
)!
|
||||
// phpmon.app URLs (these are aliased to redirect correctly)
|
||||
|
||||
static let DonationPage = URL(
|
||||
string: "https://nicoverbruggen.be/sponsor"
|
||||
string: "https://phpmon.app/sponsor"
|
||||
)!
|
||||
|
||||
static let FrequentlyAskedQuestions = URL(
|
||||
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
|
||||
string: "https://phpmon.app/faq"
|
||||
)!
|
||||
|
||||
static let DonationPayment = URL(
|
||||
string: "https://phpmon.app/sponsor/now"
|
||||
)!
|
||||
|
||||
// GitHub URLs (do not alias these)
|
||||
|
||||
static let GitHubReleases = URL(
|
||||
string: "https://github.com/nicoverbruggen/phpmon/releases"
|
||||
)!
|
||||
|
@ -21,7 +21,7 @@ public class Paths {
|
||||
|
||||
init() {
|
||||
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
||||
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
|
||||
userName = String(Shell.pipe("id -un").split(separator: "\n")[0])
|
||||
}
|
||||
|
||||
public func detectBinaryPaths() {
|
||||
@ -57,6 +57,10 @@ public class Paths {
|
||||
return shared.userName
|
||||
}
|
||||
|
||||
public static var homePath: String {
|
||||
return NSHomeDirectory()
|
||||
}
|
||||
|
||||
public static var cellarPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Cellar"
|
||||
}
|
||||
|
33
phpmon/Common/Core/Shell+PATH.swift
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// Shell+PATH.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 15/08/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Shell {
|
||||
|
||||
var PATH: String {
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/zsh"
|
||||
|
||||
let command = Filesystem.fileExists("~/.zshrc")
|
||||
// source the user's .zshrc file if it exists to complete $PATH
|
||||
? ". ~/.zshrc && echo $PATH"
|
||||
// otherwise, non-interactive mode is sufficient
|
||||
: "echo $PATH"
|
||||
|
||||
task.arguments = ["--login", "-lc", command]
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.launch()
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
|
||||
return String(data: data, encoding: String.Encoding.utf8) ?? ""
|
||||
}
|
||||
}
|
@ -32,6 +32,9 @@ public class Shell {
|
||||
*/
|
||||
public var shell: String = "/bin/sh"
|
||||
|
||||
/** Additional exports that are sent if `requiresPath` is set to true. */
|
||||
public var exports: String = ""
|
||||
|
||||
/**
|
||||
Singleton to access a user shell (with --login)
|
||||
*/
|
||||
@ -114,13 +117,23 @@ public class Shell {
|
||||
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
|
||||
var completeCommand = ""
|
||||
|
||||
if requiresPath {
|
||||
// Basic export (PATH)
|
||||
completeCommand += "export PATH=\(Paths.binPath):$PATH && "
|
||||
|
||||
// Put additional exports in between
|
||||
if !self.exports.isEmpty {
|
||||
completeCommand += "\(self.exports) && "
|
||||
}
|
||||
}
|
||||
|
||||
completeCommand += command
|
||||
|
||||
let task = Process()
|
||||
task.launchPath = self.shell
|
||||
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
|
||||
task.arguments = ["--noprofile", "-norc", "--login", "-c", completeCommand]
|
||||
|
||||
return task
|
||||
}
|
||||
|
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 © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Array {
|
||||
/**
|
||||
Sourced from Stack Overflow
|
||||
https://stackoverflow.com/a/33540708
|
||||
*/
|
||||
func chunked(by distance: Int) -> [[Element]] {
|
||||
let indicesSequence = stride(from: startIndex, to: endIndex, by: distance)
|
||||
let array: [[Element]] = indicesSequence.map {
|
||||
let newIndex = $0.advanced(by: distance) > endIndex ? endIndex : $0.advanced(by: distance)
|
||||
return Array(self[$0 ..< newIndex])
|
||||
}
|
||||
return array
|
||||
}
|
||||
}
|
@ -9,21 +9,17 @@
|
||||
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 © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension NSMenuItem {
|
||||
convenience init(
|
||||
title: String,
|
||||
action: Selector? = nil,
|
||||
keyEquivalent: String = "",
|
||||
keyModifier: NSEvent.ModifierFlags = [],
|
||||
toolTip: String? = nil
|
||||
) {
|
||||
self.init(title: title, action: action, keyEquivalent: keyEquivalent)
|
||||
self.keyEquivalentModifierMask = keyModifier
|
||||
self.toolTip = toolTip
|
||||
}
|
||||
|
||||
convenience init(
|
||||
title: String,
|
||||
keyEquivalent: String = "",
|
||||
keyModifier: NSEvent.ModifierFlags = [],
|
||||
toolTip: String? = nil,
|
||||
submenu: [NSMenuItem],
|
||||
target: NSObject? = nil
|
||||
) {
|
||||
self.init(title: title, action: nil, keyEquivalent: keyEquivalent)
|
||||
self.keyEquivalentModifierMask = keyModifier
|
||||
self.toolTip = toolTip
|
||||
self.submenu = NSMenu(items: submenu, target: target)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSMenuItem subclasses
|
||||
|
||||
@IBDesignable class LocalizedMenuItem: NSMenuItem {
|
||||
@IBInspectable var localizationKey: String? {
|
||||
didSet {
|
||||
self.title = localizationKey?.localized ?? self.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PhpMenuItem: NSMenuItem {
|
||||
var version: String = ""
|
||||
}
|
||||
|
||||
class XdebugMenuItem: NSMenuItem {
|
||||
var mode: String = ""
|
||||
}
|
||||
|
||||
class ExtensionMenuItem: NSMenuItem {
|
||||
var phpExtension: PhpExtension?
|
||||
}
|
||||
|
||||
class EditorMenuItem: NSMenuItem {
|
||||
var editor: Application?
|
||||
}
|
||||
|
||||
class PresetMenuItem: NSMenuItem {
|
||||
var preset: Preset?
|
||||
|
||||
static func getAll() -> [NSMenuItem] {
|
||||
return Preferences.custom.presets!.map { preset in
|
||||
let presetMenuItem = PresetMenuItem(
|
||||
title: preset.getMenuItemText(),
|
||||
action: #selector(MainMenu.togglePreset(sender:))
|
||||
)
|
||||
|
||||
if let attributedString = try? NSMutableAttributedString(
|
||||
data: preset.getMenuItemText().data(using: .utf8)!,
|
||||
options: [.documentType: NSAttributedString.DocumentType.html],
|
||||
documentAttributes: nil
|
||||
) {
|
||||
presetMenuItem.attributedTitle = attributedString
|
||||
}
|
||||
|
||||
presetMenuItem.preset = preset
|
||||
return presetMenuItem
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,20 @@ 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/
|
||||
*/
|
||||
|
@ -5,13 +5,23 @@
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension String {
|
||||
|
||||
var localized: String {
|
||||
if #available(macOS 13, *) {
|
||||
return NSLocalizedString(
|
||||
self, tableName: nil, bundle: Bundle.main, value: "", comment: ""
|
||||
).replacingOccurrences(of: "Preferences", with: "Settings")
|
||||
}
|
||||
|
||||
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
|
||||
}
|
||||
|
||||
var localizedForSwiftUI: LocalizedStringKey {
|
||||
return LocalizedStringKey(self.localized)
|
||||
}
|
||||
|
||||
func localized(_ args: CVarArg...) -> String {
|
||||
String(format: self.localized, arguments: args)
|
||||
}
|
||||
@ -32,7 +42,7 @@ extension String {
|
||||
return count
|
||||
}
|
||||
|
||||
subscript (r: Range<String.Index>) -> String {
|
||||
subscript(r: Range<String.Index>) -> String {
|
||||
let start = r.lowerBound
|
||||
let end = r.upperBound
|
||||
return String(self[start ..< end])
|
||||
@ -71,4 +81,22 @@ extension String {
|
||||
}
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// FileSystem.swift
|
||||
// Filesystem.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 07/12/2021.
|
||||
@ -7,17 +7,55 @@
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class Filesystem {
|
||||
|
||||
/**
|
||||
Checks if a file exists at the provided path.
|
||||
Uses `FileManager`.
|
||||
Checks if a file or directory exists at the provided path.
|
||||
*/
|
||||
public static func fileExists(_ path: String) -> Bool {
|
||||
public static func exists(_ path: String) -> Bool {
|
||||
return FileManager.default.fileExists(
|
||||
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
|
||||
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if a file exists at the provided path.
|
||||
*/
|
||||
public static func fileExists(_ path: String) -> Bool {
|
||||
var isDirectory: ObjCBool = true
|
||||
let exists = FileManager.default.fileExists(
|
||||
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
|
||||
isDirectory: &isDirectory
|
||||
)
|
||||
|
||||
return exists && !isDirectory.boolValue
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if a directory exists at the provided path.
|
||||
*/
|
||||
public static func directoryExists(_ path: String) -> Bool {
|
||||
var isDirectory: ObjCBool = true
|
||||
let exists = FileManager.default.fileExists(
|
||||
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
|
||||
isDirectory: &isDirectory
|
||||
)
|
||||
|
||||
return exists && isDirectory.boolValue
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if a given file is a symbolic link.
|
||||
*/
|
||||
public static func fileIsSymlink(_ path: String) -> Bool {
|
||||
do {
|
||||
let attribs = try FileManager.default.attributesOfItem(atPath: path)
|
||||
return attribs[.type] as! FileAttributeType == FileAttributeType.typeSymbolicLink
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,11 @@ import UserNotifications
|
||||
|
||||
class LocalNotification {
|
||||
|
||||
public static func send(title: String, subtitle: String) {
|
||||
@MainActor public static func send(title: String, subtitle: String, preference: PreferenceName) {
|
||||
if !Preferences.isEnabled(preference) {
|
||||
return
|
||||
}
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.body = subtitle
|
||||
|
@ -30,7 +30,7 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
||||
}
|
||||
|
||||
deinit {
|
||||
Log.perf("Window controller '\(windowName)' was deinitialized")
|
||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,7 +20,13 @@ class ActivePhpInstallation {
|
||||
|
||||
var version: Version!
|
||||
var limits: Limits!
|
||||
var extensions: [PhpExtension]!
|
||||
var iniFiles: [PhpConfigurationFile] = []
|
||||
|
||||
var extensions: [PhpExtension] {
|
||||
return iniFiles.flatMap { initFile in
|
||||
return initFile.extensions
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Computed
|
||||
|
||||
@ -34,16 +40,21 @@ class ActivePhpInstallation {
|
||||
// Show information about the current version
|
||||
getVersion()
|
||||
|
||||
// Initialize the list of ini files that are loaded
|
||||
iniFiles = []
|
||||
|
||||
// If an error occurred, exit early
|
||||
if version.error {
|
||||
limits = Limits()
|
||||
extensions = []
|
||||
return
|
||||
}
|
||||
|
||||
// Load extension information
|
||||
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||
extensions = PhpExtension.load(from: path)
|
||||
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||
|
||||
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
||||
iniFiles.append(file)
|
||||
}
|
||||
|
||||
// Get configuration values
|
||||
limits = Limits(
|
||||
@ -60,9 +71,8 @@ class ActivePhpInstallation {
|
||||
|
||||
// See if any extensions are present in said .ini files
|
||||
paths.forEach { (iniFilePath) in
|
||||
let loadedExtensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
||||
if !loadedExtensions.isEmpty {
|
||||
extensions.append(contentsOf: loadedExtensions)
|
||||
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
|
||||
iniFiles.append(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,20 +7,48 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class Xdebug {
|
||||
|
||||
public static var enabled: Bool {
|
||||
return !self.mode.isEmpty
|
||||
return PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") != nil
|
||||
}
|
||||
|
||||
public static var mode: String {
|
||||
return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
|
||||
public static var activeModes: [String] {
|
||||
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
|
||||
return []
|
||||
}
|
||||
|
||||
guard let value = file.get(for: "xdebug.mode") else {
|
||||
return []
|
||||
}
|
||||
|
||||
return value.components(separatedBy: ",").filter { self.modes.contains($0) }
|
||||
}
|
||||
|
||||
public static func asMenuItems() -> [NSMenuItem] {
|
||||
var items: [NSMenuItem] = []
|
||||
|
||||
let activeModes = Self.activeModes
|
||||
|
||||
for mode in Self.modes {
|
||||
let item = XdebugMenuItem(
|
||||
title: mode,
|
||||
action: #selector(MainMenu.toggleXdebugMode(sender:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
|
||||
item.state = activeModes.contains(mode) ? .on : .off
|
||||
item.mode = mode
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
public static var modes: [String] {
|
||||
return [
|
||||
"off",
|
||||
"develop",
|
||||
"coverage",
|
||||
"debug",
|
||||
|
@ -19,20 +19,20 @@ struct HomebrewService: Decodable, Equatable {
|
||||
let log_path: String?
|
||||
let error_log_path: String?
|
||||
|
||||
public static func loadAll(
|
||||
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
|
||||
completion: @escaping ([HomebrewService]) -> Void
|
||||
) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let data = Shell
|
||||
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
|
||||
.data(using: .utf8)!
|
||||
|
||||
let services = try! JSONDecoder()
|
||||
.decode([HomebrewService].self, from: data)
|
||||
.filter({ return filter.contains($0.name) })
|
||||
|
||||
completion(services)
|
||||
}
|
||||
/**
|
||||
Dummy data for preview purposes.
|
||||
*/
|
||||
public static func dummy(named service: String, enabled: Bool) -> Self {
|
||||
return HomebrewService(
|
||||
name: service,
|
||||
service_name: service,
|
||||
running: enabled,
|
||||
loaded: enabled,
|
||||
pid: nil,
|
||||
user: nil,
|
||||
status: nil,
|
||||
log_path: nil,
|
||||
error_log_path: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -174,4 +174,14 @@ class PhpEnv {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the configuration file instance that is used for a specific config value.
|
||||
You can then use the configuration file instance to change values.
|
||||
*/
|
||||
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
|
||||
return PhpEnv.phpInstall.iniFiles
|
||||
.reversed()
|
||||
.first(where: { $0.has(key: key) })
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,18 @@ class PhpHelper {
|
||||
// Take the PHP version (e.g. "7.2") and generate a dotless version
|
||||
let dotless = version.replacingOccurrences(of: ".", with: "")
|
||||
|
||||
// Determine the dotless name for this PHP version
|
||||
let destination = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
||||
|
||||
// Check if the ~/.config/phpmon/bin directory is in the PATH
|
||||
let inPath = Shell.user.PATH.contains("\(Paths.homePath)/.config/phpmon/bin")
|
||||
|
||||
// Check if we can create symlinks (`/usr/local/bin` must be writable)
|
||||
let canWriteSymlinks = FileManager.default.isWritableFile(atPath: "/usr/local/bin/")
|
||||
|
||||
do {
|
||||
let destination = "/usr/local/bin/pm\(dotless)"
|
||||
Shell.run("mkdir -p ~/.config/phpmon/bin")
|
||||
|
||||
if FileManager.default.fileExists(atPath: destination) {
|
||||
let contents = try String(contentsOfFile: destination)
|
||||
if !contents.contains(keyPhrase) {
|
||||
@ -52,10 +62,40 @@ class PhpHelper {
|
||||
|
||||
// Make sure the file is executable
|
||||
Shell.run("chmod +x \(destination)")
|
||||
|
||||
// Create a symlink if the folder is not in the PATH
|
||||
if !inPath {
|
||||
// First, check if we can create symlinks at all
|
||||
if !canWriteSymlinks {
|
||||
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
|
||||
return
|
||||
}
|
||||
|
||||
// Write the symlink
|
||||
self.createSymlink(dotless)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
|
||||
Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
|
||||
}
|
||||
}
|
||||
|
||||
private static func createSymlink(_ dotless: String) {
|
||||
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
||||
let destination = "/usr/local/bin/pm\(dotless)"
|
||||
|
||||
if !Filesystem.fileExists(destination) {
|
||||
Log.info("Creating new symlink: \(destination)")
|
||||
Shell.run("ln -s \(source) \(destination)")
|
||||
return
|
||||
}
|
||||
|
||||
if !Filesystem.fileIsSymlink(destination) {
|
||||
Log.info("Overwriting existing file with new symlink: \(destination)")
|
||||
Shell.run("ln -fs \(source) \(destination)")
|
||||
return
|
||||
}
|
||||
|
||||
Log.info("Symlink in \(destination) already exists, OK.")
|
||||
}
|
||||
}
|
||||
|
@ -87,11 +87,19 @@ public struct PhpVersionNumberCollection: Equatable {
|
||||
return self.versions.filter { $0.isNewerThan(version, strict) }
|
||||
}
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThanOrEqual) {
|
||||
return self.versions.filter { $0.isSameAs(version, strict) || $0.isOlderThan(version, strict)}
|
||||
}
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThan) {
|
||||
return self.versions.filter { $0.isOlderThan(version, strict)}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public struct PhpVersionNumber: Equatable {
|
||||
public struct PhpVersionNumber: Equatable, Hashable {
|
||||
let major: Int
|
||||
let minor: Int
|
||||
let patch: Int?
|
||||
@ -116,12 +124,8 @@ public struct PhpVersionNumber: Equatable {
|
||||
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
|
||||
// TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
|
||||
/*
|
||||
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
*/
|
||||
}
|
||||
|
||||
public static func parse(_ text: String) throws -> Self {
|
||||
@ -175,6 +179,15 @@ public struct PhpVersionNumber: Equatable {
|
||||
)
|
||||
}
|
||||
|
||||
internal func isOlderThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return (
|
||||
self.major < version.major ||
|
||||
self.major == version.major && self.minor < version.minor ||
|
||||
self.major == version.major && self.minor == version.minor
|
||||
&& self.patch(strict) < version.patch(strict)
|
||||
)
|
||||
}
|
||||
|
||||
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return self.major == version.major &&
|
||||
(
|
||||
|
216
phpmon/Common/PHP/PhpConfigurationFile.swift
Normal file
@ -0,0 +1,216 @@
|
||||
//
|
||||
// PhpConfigurationFile.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 04/05/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpConfigurationFile: CreatedFromFile {
|
||||
|
||||
struct ConfigValue {
|
||||
let lineIndex: Int
|
||||
let value: String
|
||||
}
|
||||
|
||||
typealias Section = [String: ConfigValue]
|
||||
typealias Config = [String: Section]
|
||||
|
||||
/// The file where this configuration file was located.
|
||||
let filePath: String
|
||||
|
||||
/// The extensions found in this .ini file.
|
||||
var extensions: [PhpExtension]
|
||||
|
||||
/// The actual, structured content of the configuration file.
|
||||
var content: Config
|
||||
|
||||
/// The original lines of the file.
|
||||
var lines: [String]
|
||||
|
||||
/** Resolves a PHP configuration file (.ini) */
|
||||
static func from(filePath: String) -> Self? {
|
||||
let path = filePath.replacingOccurrences(of: "~", with: Paths.homePath)
|
||||
|
||||
do {
|
||||
let fileContents = try String(contentsOfFile: path)
|
||||
return Self.init(path: path, contents: fileContents)
|
||||
} catch {
|
||||
Log.warn("Could not read the PHP configuration file at: `\(filePath)`")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
required init(path: String, contents: String) {
|
||||
self.filePath = path
|
||||
self.lines = contents.components(separatedBy: "\n")
|
||||
self.extensions = PhpExtension.from(lines, filePath: path)
|
||||
self.content = Self.parseConfig(lines: lines)
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
public func has(key: String) -> Bool {
|
||||
return self.content.contains { (_: String, section: Section) in
|
||||
return section.keys.contains(key)
|
||||
}
|
||||
}
|
||||
|
||||
public func get(for key: String) -> String? {
|
||||
return getConfig(for: key)?.value
|
||||
}
|
||||
|
||||
public func getConfig(for key: String) -> ConfigValue? {
|
||||
for (_, section) in self.content where section.keys.contains(key) {
|
||||
return section[key]!
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
enum ReplacementErrors: Error {
|
||||
case missingKey
|
||||
}
|
||||
|
||||
/**
|
||||
Replaces the value for a specific (existing) key with a new value.
|
||||
The key must exist for this to work.
|
||||
*/
|
||||
public func replace(key: String, value: String) throws {
|
||||
// Ensure that the key exists
|
||||
guard let item = getConfig(for: key) else {
|
||||
throw ReplacementErrors.missingKey
|
||||
}
|
||||
|
||||
// Figure out what comes after the assignment
|
||||
var components = self
|
||||
.lines[item.lineIndex]
|
||||
.components(separatedBy: "=")
|
||||
|
||||
// Replace the value with the new one
|
||||
components[1] = components[1]
|
||||
.replacingOccurrences(of: item.value, with: value)
|
||||
|
||||
// Replace the specific line
|
||||
self.lines[item.lineIndex] = components.joined(separator: "=")
|
||||
|
||||
// Finally, join the string and save the file atomatically again
|
||||
try self.lines.joined(separator: "\n")
|
||||
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
|
||||
|
||||
// Reload the original file
|
||||
self.reload()
|
||||
}
|
||||
|
||||
public func reload() {
|
||||
self.lines = try! String(contentsOfFile: self.filePath)
|
||||
.components(separatedBy: "\n")
|
||||
self.extensions = PhpExtension.from(lines, filePath: self.filePath)
|
||||
self.content = Self.parseConfig(lines: lines)
|
||||
}
|
||||
|
||||
// MARK: Parsing Logic
|
||||
// Slightly modified from: https://gist.github.com/jetmind/f776c0d223e4ac6aec1ff9389e874553
|
||||
|
||||
/**
|
||||
Attempts to parse the configuration file, based on an array of strings.
|
||||
Each string is a line from the configuration file.
|
||||
*/
|
||||
private static func parseConfig(lines: [String]) -> Config {
|
||||
var config = Config()
|
||||
|
||||
var currentSectionName = "main"
|
||||
|
||||
for (index, line) in lines.enumerated() {
|
||||
let line = trim(line)
|
||||
|
||||
if line.hasPrefix("[") && line.hasSuffix("]") {
|
||||
currentSectionName = parseSectionHeader(line)
|
||||
} else if let (key, value) = parseLine(line) {
|
||||
var section = config[currentSectionName] ?? [:]
|
||||
section[key] = ConfigValue(
|
||||
lineIndex: index,
|
||||
value: value
|
||||
)
|
||||
config[currentSectionName] = section
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all whitespace and additional characters from individual lines.
|
||||
*/
|
||||
private static func trim(_ string: String) -> String {
|
||||
let whitespaces = CharacterSet(charactersIn: " \n\r\t")
|
||||
return string.trimmingCharacters(in: whitespaces)
|
||||
}
|
||||
|
||||
/**
|
||||
It may prove beneficial to strip all comments, which can start with # or ;.
|
||||
In this case, strip both.
|
||||
*/
|
||||
private static func stripComment(_ line: String) -> String {
|
||||
var line = line
|
||||
|
||||
let characters: [String.Element] = ["#", ";"]
|
||||
|
||||
for character in characters {
|
||||
// Only keep checking for comments as long as the line isn't empty
|
||||
if line.isEmpty {
|
||||
return line
|
||||
}
|
||||
|
||||
// Check for the next comment character
|
||||
line = strip(character: character, line)
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
/**
|
||||
Empties a line if it happens to be commented out, causing it to be ignored.
|
||||
*/
|
||||
private static func strip(character: String.Element, _ line: String) -> String {
|
||||
let parts = line.split(
|
||||
separator: character,
|
||||
maxSplits: 1,
|
||||
omittingEmptySubsequences: false
|
||||
)
|
||||
|
||||
if !parts.isEmpty {
|
||||
return String(parts[0])
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to parse a section header. Requires the line to start with [ and end with ].
|
||||
*/
|
||||
private static func parseSectionHeader(_ line: String) -> String {
|
||||
let from = line.index(after: line.startIndex)
|
||||
let to = line.index(before: line.endIndex)
|
||||
|
||||
return line[from..<to]
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to parse a regular line, which may contain a configuration value that is being set.
|
||||
*/
|
||||
private static func parseLine(_ line: String) -> (String, String)? {
|
||||
let parts = stripComment(line)
|
||||
.split(separator: "=", maxSplits: 1)
|
||||
|
||||
if parts.count == 2 {
|
||||
let k = trim(String(parts[0]))
|
||||
let v = trim(String(parts[1]))
|
||||
return (k, v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
@ -89,24 +89,26 @@ class PhpExtension {
|
||||
|
||||
// MARK: - Static Methods
|
||||
|
||||
/**
|
||||
This method will attempt to identify all extensions in the .ini file at a certain URL.
|
||||
*/
|
||||
static func load(from path: URL) -> [PhpExtension] {
|
||||
let file = try? String(contentsOf: path, encoding: .utf8)
|
||||
static func from(_ lines: [String], filePath: String) -> [PhpExtension] {
|
||||
return lines.filter {
|
||||
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||
}.map {
|
||||
return PhpExtension($0, file: filePath)
|
||||
}
|
||||
}
|
||||
|
||||
static func from(filePath: String) -> [PhpExtension] {
|
||||
let file = try? String(contentsOfFile: filePath)
|
||||
|
||||
if file == nil {
|
||||
Log.err("There was an issue reading the file. Assuming no extensions were found.")
|
||||
return []
|
||||
}
|
||||
|
||||
return file!.components(separatedBy: "\n")
|
||||
.filter {
|
||||
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||
}
|
||||
.map {
|
||||
return PhpExtension($0, file: path.path)
|
||||
}
|
||||
return Self.from(
|
||||
file!.components(separatedBy: "\n"),
|
||||
filePath: filePath
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,17 +23,7 @@ class InternalSwitcher: PhpSwitcher {
|
||||
func performSwitch(to version: String, completion: @escaping () -> Void) {
|
||||
Log.info("Switching to \(version), unlinking all versions...")
|
||||
|
||||
let isolated = Valet.shared.sites.filter { site in
|
||||
site.isolatedPhpVersion != nil
|
||||
}.map { site in
|
||||
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
|
||||
}
|
||||
|
||||
var versions: Set<String> = [version]
|
||||
|
||||
if Valet.enabled(feature: .isolatedSites) {
|
||||
versions = versions.union(isolated)
|
||||
}
|
||||
let versions = getVersionsToBeHandled(version)
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
@ -63,7 +53,28 @@ class InternalSwitcher: PhpSwitcher {
|
||||
}
|
||||
}
|
||||
|
||||
private func disableDefaultPhpFpmPool(_ version: String) {
|
||||
func getVersionsToBeHandled(_ primary: String) -> Set<String> {
|
||||
let isolated = Valet.shared.sites.filter { site in
|
||||
site.isolatedPhpVersion != nil
|
||||
}.map { site in
|
||||
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
|
||||
}
|
||||
|
||||
var versions: Set<String> = [primary]
|
||||
|
||||
if Valet.enabled(feature: .isolatedSites) {
|
||||
versions = versions.union(isolated)
|
||||
}
|
||||
|
||||
return versions
|
||||
}
|
||||
|
||||
func requiresDisablingOfDefaultPhpFpmPool(_ version: String) -> Bool {
|
||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||
return FileManager.default.fileExists(atPath: pool)
|
||||
}
|
||||
|
||||
func disableDefaultPhpFpmPool(_ version: String) {
|
||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||
if FileManager.default.fileExists(atPath: pool) {
|
||||
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
|
||||
@ -83,14 +94,14 @@ class InternalSwitcher: PhpSwitcher {
|
||||
}
|
||||
}
|
||||
|
||||
private func stopPhpVersion(_ version: String) {
|
||||
func stopPhpVersion(_ version: String) {
|
||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
brew("unlink \(formula)")
|
||||
brew("services stop \(formula)", sudo: true)
|
||||
Log.info("Unlinked and stopped services for \(formula)")
|
||||
}
|
||||
|
||||
private func startPhpVersion(_ version: String, primary: Bool) {
|
||||
func startPhpVersion(_ version: String, primary: Bool) {
|
||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
|
||||
if primary {
|
||||
|
15
phpmon/Common/Protocols/CreatedFromFile.swift
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// CreatedFromFile.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 15/05/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol CreatedFromFile {
|
||||
|
||||
static func from(filePath: String) -> Self?
|
||||
|
||||
}
|
@ -13,9 +13,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<p><b>Want to spread the love?</b> Leave a <a href="https://github.com/nicoverbruggen/phpmon">star on GitHub</a>!</p>
|
||||
<p><b>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p>
|
||||
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p>
|
||||
<p><b>Do you enjoy using the app?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
|
||||
<p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p>
|
||||
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
||||
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
|
||||
<br>
|
||||
</body>
|
||||
|
@ -51,14 +51,26 @@ class App {
|
||||
var preferences: [PreferenceName: Bool]!
|
||||
|
||||
/** The window controller of the currently active preferences window. */
|
||||
var preferencesWindowController: PrefsWC?
|
||||
var preferencesWindowController: PreferencesWindowController?
|
||||
|
||||
/** The window controller of the currently active site list window. */
|
||||
var domainListWindowController: DomainListWC?
|
||||
var domainListWindowController: DomainListWindowController?
|
||||
|
||||
/** The window controller of the onboarding window. */
|
||||
var onboardingWindowController: OnboardingWindowController?
|
||||
|
||||
/** The window controller of the warnings window. */
|
||||
var warningsWindowController: WarningsWindowController?
|
||||
|
||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||
var detectedApplications: [Application] = []
|
||||
|
||||
/** The services manager, responsible for figuring out what services are active/inactive. */
|
||||
var services = ServicesManager.shared
|
||||
|
||||
/** The warning manager, responsible for keeping track of warnings. */
|
||||
var warnings = WarningManager.shared
|
||||
|
||||
/** Timer that will periodically reload info about the user's PHP installation. */
|
||||
var timer: Timer?
|
||||
|
||||
|
@ -146,8 +146,9 @@ class AppUpdateChecker {
|
||||
text: "updater.alerts.buttons.release_notes".localized,
|
||||
action: { vc in
|
||||
vc.close(with: .OK)
|
||||
|
||||
NSWorkspace.shared.open(
|
||||
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version.version)\(devSuffix)")
|
||||
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version.tagged)\(devSuffix)")
|
||||
)
|
||||
}
|
||||
)
|
||||
@ -161,14 +162,14 @@ class AppUpdateChecker {
|
||||
private static func notifyAboutConnectionIssue() {
|
||||
DispatchQueue.main.async {
|
||||
BetterAlert().withInformation(
|
||||
title: "updater.errors.cannot_check_for_update.title".localized,
|
||||
subtitle: "updater.errors.cannot_check_for_update.subtitle".localized,
|
||||
description: "updater.errors.cannot_check_for_update.description".localized(
|
||||
title: "updater.alerts.cannot_check_for_update.title".localized,
|
||||
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
|
||||
description: "updater.alerts.cannot_check_for_update.description".localized(
|
||||
App.version
|
||||
)
|
||||
)
|
||||
.withTertiary(
|
||||
text: "updater.errors.buttons.releases_on_github".localized,
|
||||
text: "updater.alerts.buttons.releases_on_github".localized,
|
||||
action: { _ in
|
||||
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
|
||||
}
|
||||
|
@ -66,6 +66,14 @@ class AppVersion {
|
||||
return AppVersion.from("\(App.shortVersion)_\(App.bundleVersion)")!
|
||||
}
|
||||
|
||||
var tagged: String {
|
||||
if version.suffix(2) == ".0" && version.count > 3 {
|
||||
return String(version.dropLast(2))
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
var computerReadable: String {
|
||||
return "\(version)_\(build ?? "0")"
|
||||
}
|
||||
|
@ -324,36 +324,32 @@
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="PQa-AT-b2a">
|
||||
<objects>
|
||||
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PrefsWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PreferencesWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="h4c-3b-nko">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="372" y="403" width="480" height="270"/>
|
||||
<rect key="contentRect" x="372" y="403" width="550" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/>
|
||||
<view key="contentView" id="2yL-50-11x">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="550" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
<toolbar key="toolbar" implicitIdentifier="611E3485-DC7F-46A0-8528-11CF9366370C" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconAndLabel" sizeMode="regular" id="fcq-wR-7iv">
|
||||
<allowedToolbarItems/>
|
||||
<defaultToolbarItems/>
|
||||
</toolbar>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="AW2-rV-rbS" kind="relationship" relationship="window.shadowedContentViewController" id="3dX-9V-eA0"/>
|
||||
<segue destination="PCI-2c-55Y" kind="relationship" relationship="window.shadowedContentViewController" id="egC-A4-am8"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-374" y="238"/>
|
||||
</scene>
|
||||
<!--Preferences-->
|
||||
<scene sceneID="iyi-IS-7Ps">
|
||||
<objects>
|
||||
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController title="Preferences" identifier="preferencesTemplateVC" storyboardIdentifier="preferencesTemplateVC" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="GenericPreferenceVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" wantsLayer="YES" id="Pf1-A5-3Xz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="550" height="498"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
@ -378,12 +374,32 @@
|
||||
</viewController>
|
||||
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="260" y="217"/>
|
||||
<point key="canvasLocation" x="844" y="-153"/>
|
||||
</scene>
|
||||
<!--Tab View Controller-->
|
||||
<scene sceneID="B5x-d3-c7D">
|
||||
<objects>
|
||||
<customObject id="pNW-tM-SQu" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<tabViewController tabStyle="toolbar" canPropagateSelectedChildViewControllerTitle="NO" id="PCI-2c-55Y" sceneMemberID="viewController">
|
||||
<tabView key="tabView" type="noTabsNoBorder" id="l0U-9a-nM6">
|
||||
<rect key="frame" x="0.0" y="0.0" width="508" height="300"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<font key="font" metaFont="message"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="PCI-2c-55Y" id="6gR-GR-cwq"/>
|
||||
</connections>
|
||||
</tabView>
|
||||
<connections>
|
||||
<outlet property="tabView" destination="l0U-9a-nM6" id="tfn-UN-1Aa"/>
|
||||
</connections>
|
||||
</tabViewController>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="283" y="-252"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="4XS-kY-YIS">
|
||||
<objects>
|
||||
<windowController storyboardIdentifier="domainListWindow" id="8Ec-9q-82s" customClass="DomainListWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<windowController storyboardIdentifier="domainListWindow" id="8Ec-9q-82s" customClass="DomainListWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<window key="window" separatorStyle="line" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
@ -532,7 +548,7 @@ Gw
|
||||
</connections>
|
||||
</button>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j">
|
||||
<rect key="frame" x="13" y="13" width="82" height="32"/>
|
||||
<rect key="frame" x="13" y="13" width="81" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Tertiary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="mzA-Uu-gyf">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -651,7 +667,7 @@ Gw
|
||||
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</box>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
|
||||
<rect key="frame" x="325" y="13" width="142" height="32"/>
|
||||
<rect key="frame" x="326" y="13" width="141" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="[i18n] Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -732,7 +748,7 @@ Gw
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
||||
<rect key="frame" x="139" y="23" width="180" height="14"/>
|
||||
<rect key="frame" x="140" y="23" width="180" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
||||
@ -805,7 +821,7 @@ Gw
|
||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj">
|
||||
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="626" height="281"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<size key="intercellSpacing" width="17" height="0.0"/>
|
||||
@ -1381,7 +1397,7 @@ Gw
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" alignment="left" id="3i9-RG-Ift">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<mutableString key="title">[i18n] Links are used to directly serve projects. If you have a Laravel, Symfony, WordPress, etc. folder with code, you'll want to create a link and choose the folder where your code lives.
If you are in need of a proxy, you can proxy e.g. a container to a particular domain name. This can be useful in combination with Docker, for example.</mutableString>
|
||||
<string key="title">[i18n] Links are used to directly serve projects. If you have a Laravel, Symfony, WordPress, etc. folder with code, you'll want to create a link and choose the folder where your code lives.
If you are in need of a proxy, you can proxy e.g. a container to a particular domain name. This can be useful in combination with Docker, for example.</string>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
@ -1399,6 +1415,7 @@ Gw
|
||||
<constraint firstAttribute="bottom" secondItem="pYe-Qu-qnK" secondAttribute="bottom" constant="20" id="lPX-ZF-XZN"/>
|
||||
<constraint firstAttribute="trailing" secondItem="fJK-Ke-IK3" secondAttribute="trailing" constant="20" symbolic="YES" id="spl-Bn-xtw"/>
|
||||
<constraint firstAttribute="bottom" secondItem="FhN-AM-SkI" secondAttribute="bottom" constant="20" symbolic="YES" id="t5w-aL-tOa"/>
|
||||
<constraint firstItem="pYe-Qu-qnK" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="FhN-AM-SkI" secondAttribute="trailing" constant="8" symbolic="YES" id="y7k-sl-xqe"/>
|
||||
</constraints>
|
||||
</visualEffectView>
|
||||
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cNh-Wc-ADk">
|
||||
|
45
phpmon/Domain/App/EnvironmentCheck.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// EnvironmentCheck.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 10/08/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary.
|
||||
Checks that require an app restart will always lead to an alert and app termination shortly after.
|
||||
*/
|
||||
struct EnvironmentCheck {
|
||||
let command: () async -> Bool
|
||||
let name: String
|
||||
let titleText: String
|
||||
let subtitleText: String
|
||||
let descriptionText: String
|
||||
let buttonText: String
|
||||
let requiresAppRestart: Bool
|
||||
|
||||
init(
|
||||
command: @escaping () async -> Bool,
|
||||
name: String,
|
||||
titleText: String,
|
||||
subtitleText: String,
|
||||
descriptionText: String = "",
|
||||
buttonText: String = "OK",
|
||||
requiresAppRestart: Bool = false
|
||||
) {
|
||||
self.command = command
|
||||
self.name = name
|
||||
self.titleText = titleText
|
||||
self.subtitleText = subtitleText
|
||||
self.descriptionText = descriptionText
|
||||
self.buttonText = buttonText
|
||||
self.requiresAppRestart = requiresAppRestart
|
||||
}
|
||||
|
||||
public func succeeds() async -> Bool {
|
||||
return await !self.command()
|
||||
}
|
||||
}
|
@ -26,10 +26,10 @@ class InterApp {
|
||||
DomainListVC.show()
|
||||
}),
|
||||
InterApp.Action(command: "services/stop", action: { _ in
|
||||
MainMenu.shared.stopAllServices()
|
||||
MainMenu.shared.stopValetServices()
|
||||
}),
|
||||
InterApp.Action(command: "services/restart/all", action: { _ in
|
||||
MainMenu.shared.restartAllServices()
|
||||
MainMenu.shared.restartValetServices()
|
||||
}),
|
||||
InterApp.Action(command: "services/restart/nginx", action: { _ in
|
||||
MainMenu.shared.restartNginx()
|
||||
@ -56,10 +56,14 @@ class InterApp {
|
||||
if PhpEnv.shared.availablePhpVersions.contains(version) {
|
||||
MainMenu.shared.switchToPhpVersion(version)
|
||||
} else {
|
||||
BetterAlert().withInformation(
|
||||
title: "Unsupported version",
|
||||
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
|
||||
).withPrimary(text: "OK").show()
|
||||
DispatchQueue.main.async {
|
||||
BetterAlert().withInformation(
|
||||
title: "alert.php_switch_unavailable.title".localized,
|
||||
subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
|
||||
).withPrimary(
|
||||
text: "alert.php_switch_unavailable.ok".localized
|
||||
).show()
|
||||
}
|
||||
}
|
||||
})
|
||||
]}
|
||||
|
78
phpmon/Domain/App/ServicesManager.swift
Normal file
@ -0,0 +1,78 @@
|
||||
//
|
||||
// ServicesManager.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 11/06/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class ServicesManager: ObservableObject {
|
||||
|
||||
static var shared = ServicesManager()
|
||||
|
||||
@Published var rootServices: [String: HomebrewService] = [:]
|
||||
@Published var userServices: [String: HomebrewService] = [:]
|
||||
|
||||
public static func loadHomebrewServices(completed: (() -> Void)? = nil) {
|
||||
let rootServiceNames = [
|
||||
PhpEnv.phpInstall.formula,
|
||||
"nginx",
|
||||
"dnsmasq"
|
||||
]
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let data = Shell
|
||||
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
|
||||
.data(using: .utf8)!
|
||||
|
||||
let services = try! JSONDecoder()
|
||||
.decode([HomebrewService].self, from: data)
|
||||
.filter({ return rootServiceNames.contains($0.name) })
|
||||
|
||||
DispatchQueue.main.async {
|
||||
ServicesManager.shared.rootServices = Dictionary(
|
||||
uniqueKeysWithValues: services.map { ($0.name, $0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let userServiceNames = Preferences.custom.services ?? []
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let data = Shell
|
||||
.pipe("\(Paths.brew) services info --all --json", requiresPath: true)
|
||||
.data(using: .utf8)!
|
||||
|
||||
let services = try! JSONDecoder()
|
||||
.decode([HomebrewService].self, from: data)
|
||||
.filter({ return userServiceNames.contains($0.name) })
|
||||
|
||||
DispatchQueue.main.async {
|
||||
ServicesManager.shared.userServices = Dictionary(
|
||||
uniqueKeysWithValues: services.map { ($0.name, $0) }
|
||||
)
|
||||
completed?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadData() {
|
||||
Self.loadHomebrewServices()
|
||||
}
|
||||
|
||||
/**
|
||||
Dummy data for preview purposes.
|
||||
*/
|
||||
func withDummyServices(_ services: [String: Bool]) -> Self {
|
||||
for (service, enabled) in services {
|
||||
let item = HomebrewService.dummy(named: service, enabled: enabled)
|
||||
self.rootServices[service] = item
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
@ -146,13 +146,15 @@ class Startup {
|
||||
command: { return !Shell.pipe("cat /private/etc/sudoers.d/brew").contains(Paths.brew) },
|
||||
name: "`/private/etc/sudoers.d/brew` contains brew",
|
||||
titleText: "startup.errors.sudoers_brew.title".localized,
|
||||
subtitleText: "startup.errors.sudoers_brew.subtitle".localized
|
||||
subtitleText: "startup.errors.sudoers_brew.subtitle".localized,
|
||||
descriptionText: "startup.errors.sudoers_brew.desc".localized
|
||||
),
|
||||
EnvironmentCheck(
|
||||
command: { return !Shell.pipe("cat /private/etc/sudoers.d/valet").contains(Paths.valet) },
|
||||
name: "`/private/etc/sudoers.d/valet` contains valet",
|
||||
titleText: "startup.errors.sudoers_valet.title".localized,
|
||||
subtitleText: "startup.errors.sudoers_valet.subtitle".localized
|
||||
subtitleText: "startup.errors.sudoers_valet.subtitle".localized,
|
||||
descriptionText: "startup.errors.sudoers_valet.desc".localized
|
||||
),
|
||||
// =================================================================================
|
||||
// Verify if the Homebrew services are running (as root).
|
||||
@ -165,6 +167,18 @@ class Startup {
|
||||
descriptionText: "startup.errors.services_json_error.desc".localized
|
||||
),
|
||||
// =================================================================================
|
||||
// Determine that Valet is installed
|
||||
// =================================================================================
|
||||
EnvironmentCheck(
|
||||
command: {
|
||||
return !Filesystem.directoryExists("~/.config/valet")
|
||||
},
|
||||
name: "`.config/valet` not empty (Valet installed)",
|
||||
titleText: "startup.errors.valet_not_installed.title".localized,
|
||||
subtitleText: "startup.errors.valet_not_installed.subtitle".localized,
|
||||
descriptionText: "startup.errors.valet_not_installed.desc".localized
|
||||
),
|
||||
// =================================================================================
|
||||
// Determine that the Valet configuration JSON file is valid.
|
||||
// =================================================================================
|
||||
EnvironmentCheck(
|
||||
@ -182,11 +196,51 @@ class Startup {
|
||||
descriptionText: "startup.errors.valet_json_invalid.desc".localized
|
||||
),
|
||||
// =================================================================================
|
||||
// Check for `which` alias issue
|
||||
// =================================================================================
|
||||
EnvironmentCheck(
|
||||
command: {
|
||||
return App.architecture == "x86_64"
|
||||
&& FileManager.default.fileExists(atPath: "/usr/local/bin/which")
|
||||
&& Shell.pipe("which node", requiresPath: false)
|
||||
.contains("env: node: No such file or directory")
|
||||
},
|
||||
name: "`env: node` issue does not apply",
|
||||
titleText: "startup.errors.which_alias_issue.title".localized,
|
||||
subtitleText: "startup.errors.which_alias_issue.subtitle".localized,
|
||||
descriptionText: "startup.errors.which_alias_issue.desc".localized
|
||||
),
|
||||
// =================================================================================
|
||||
// Determine that Valet works correctly (no issues in platform detected)
|
||||
// =================================================================================
|
||||
EnvironmentCheck(
|
||||
command: {
|
||||
return valet("--version", sudo: false)
|
||||
.contains("Composer detected issues in your platform")
|
||||
},
|
||||
name: "`no global composer issues",
|
||||
titleText: "startup.errors.global_composer_platform_issues.title".localized,
|
||||
subtitleText: "startup.errors.global_composer_platform_issues.subtitle".localized,
|
||||
descriptionText: "startup.errors.global_composer_platform_issues.desc".localized
|
||||
),
|
||||
// =================================================================================
|
||||
// Determine the Valet version and ensure it isn't unknown.
|
||||
// =================================================================================
|
||||
EnvironmentCheck(
|
||||
command: {
|
||||
Valet.shared.version = VersionExtractor.from(valet("--version", sudo: false))
|
||||
let output = valet("--version", sudo: false)
|
||||
// Failure condition #1: does not contain Laravel Valet
|
||||
if !output.contains("Laravel Valet") {
|
||||
return true
|
||||
}
|
||||
// Failure condition #2: version cannot be parsed
|
||||
let versionString = output
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.components(separatedBy: "Laravel Valet")[1]
|
||||
.trimmingCharacters(in: .whitespaces)
|
||||
// Extract the version number
|
||||
Valet.shared.version = VersionExtractor.from(output)
|
||||
// Get the actual version
|
||||
return Valet.shared.version == nil
|
||||
},
|
||||
name: "`valet --version` was loaded",
|
||||
@ -195,42 +249,4 @@ class Startup {
|
||||
descriptionText: "startup.errors.valet_version_unknown.desc".localized
|
||||
)
|
||||
]
|
||||
|
||||
// MARK: - EnvironmentCheck struct
|
||||
|
||||
/**
|
||||
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary.
|
||||
Checks that require an app restart will always lead to an alert and app termination shortly after.
|
||||
*/
|
||||
struct EnvironmentCheck {
|
||||
let command: () async -> Bool
|
||||
let name: String
|
||||
let titleText: String
|
||||
let subtitleText: String
|
||||
let descriptionText: String
|
||||
let buttonText: String
|
||||
let requiresAppRestart: Bool
|
||||
|
||||
init(
|
||||
command: @escaping () async -> Bool,
|
||||
name: String,
|
||||
titleText: String,
|
||||
subtitleText: String,
|
||||
descriptionText: String = "",
|
||||
buttonText: String = "OK",
|
||||
requiresAppRestart: Bool = false
|
||||
) {
|
||||
self.command = command
|
||||
self.name = name
|
||||
self.titleText = titleText
|
||||
self.subtitleText = subtitleText
|
||||
self.descriptionText = descriptionText
|
||||
self.buttonText = buttonText
|
||||
self.requiresAppRestart = requiresAppRestart
|
||||
}
|
||||
|
||||
public func succeeds() async -> Bool {
|
||||
return await !self.command()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
|
||||
let path = pathControl.url!.path
|
||||
let name = inputDomainName.stringValue
|
||||
|
||||
if !FileManager.default.fileExists(atPath: path) {
|
||||
if !Filesystem.exists(path) {
|
||||
Alert.confirm(
|
||||
onWindow: view.window!,
|
||||
messageText: "domain_list.alert.folder_missing.title".localized,
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
import Cocoa
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||
static let reusableName = "domainListPhpCell"
|
||||
@ -20,10 +21,17 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||
func populateCell(with site: ValetSite) {
|
||||
self.site = site
|
||||
|
||||
buttonPhpVersion.isHidden = false
|
||||
imageViewPhpVersionOK.isHidden = false
|
||||
|
||||
buttonPhpVersion.title = " PHP \(site.servingPhpVersion)"
|
||||
|
||||
imageViewPhpVersionOK.toolTip = nil
|
||||
|
||||
imageViewPhpVersionOK.contentTintColor = site.composerPhpCompatibleWithLinked
|
||||
? NSColor(named: "IconColorGreen")
|
||||
: NSColor(named: "IconColorRed")
|
||||
|
||||
if site.isolatedPhpVersion != nil {
|
||||
imageViewPhpVersionOK.isHidden = false
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
|
||||
@ -32,10 +40,8 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||
imageViewPhpVersionOK.isHidden = (site.composerPhp == "???" || !site.composerPhpCompatibleWithLinked)
|
||||
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
|
||||
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp)
|
||||
}
|
||||
|
||||
buttonPhpVersion.isHidden = false
|
||||
imageViewPhpVersionOK.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func populateCell(with proxy: ValetProxy) {
|
||||
@ -47,56 +53,25 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
|
||||
@IBAction func pressedPhpVersion(_ sender: Any) {
|
||||
guard let site = self.site else { return }
|
||||
|
||||
let alert = NSAlert.init()
|
||||
alert.alertStyle = .informational
|
||||
var validPhpSuggestions: [PhpVersionNumber] {
|
||||
if site.isolatedPhpVersion != nil {
|
||||
return []
|
||||
}
|
||||
|
||||
var information = ""
|
||||
|
||||
if self.site?.isolatedPhpVersion != nil {
|
||||
information += "alert.composer_php_isolated.desc".localized(
|
||||
self.site!.isolatedPhpVersion!.versionNumber.homebrewVersion,
|
||||
PhpEnv.phpInstall.version.short
|
||||
)
|
||||
information += "\n\n"
|
||||
}
|
||||
|
||||
information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)"
|
||||
.localized
|
||||
|
||||
alert.messageText = "alert.composer_php_requirement.title"
|
||||
.localized("\(site.name).\(Valet.shared.config.tld)", site.composerPhp)
|
||||
alert.informativeText = information
|
||||
|
||||
alert.addButton(withTitle: "site_link.close".localized)
|
||||
|
||||
var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue
|
||||
var map: [Int: String] = [:]
|
||||
|
||||
if site.isolatedPhpVersion == nil {
|
||||
// Determine which installed versions would be ideal to switch to,
|
||||
// but make sure to exclude the currently linked version
|
||||
PhpEnv.shared.validVersions(for: site.composerPhp).filter({ version in
|
||||
return PhpEnv.shared.validVersions(for: site.composerPhp).filter({ version in
|
||||
version.homebrewVersion != PhpEnv.phpInstall.version.short
|
||||
}).forEach { version in
|
||||
alert.addButton(withTitle: "site_link.switch_to_php".localized(version.homebrewVersion))
|
||||
map[mapIndex] = version.homebrewVersion
|
||||
mapIndex += 1
|
||||
}
|
||||
|
||||
// Site is not isolated, show options to switch global PHP version
|
||||
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) { response in
|
||||
if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue {
|
||||
if map.keys.contains(response.rawValue) {
|
||||
let version = map[response.rawValue]!
|
||||
Log.info("Pressed button to switch to \(version)")
|
||||
MainMenu.shared.switchToPhpVersion(version)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Site is isolated, do not show any options to switch
|
||||
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!)
|
||||
})
|
||||
}
|
||||
|
||||
let button = self.buttonPhpVersion!
|
||||
let popover = NSPopover()
|
||||
|
||||
let view = VersionPopoverView(site: site, validPhpVersions: validPhpSuggestions, parent: popover)
|
||||
|
||||
popover.contentViewController = NSHostingController(rootView: view)
|
||||
popover.behavior = .transient
|
||||
popover.animates = true
|
||||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ extension DomainListVC {
|
||||
let originalSecureStatus = selectedSite!.secured
|
||||
let action = selectedSite!.secured ? "unsecure" : "secure"
|
||||
let selectedSite = selectedSite!
|
||||
let command = "cd '\(selectedSite.absolutePath)' && sudo \(Paths.valet) \(action) && exit;"
|
||||
let command = "sudo \(Paths.valet) \(action) '\(selectedSite.name)' && exit;"
|
||||
|
||||
waitAndExecute {
|
||||
Shell.run(command, requiresPath: true)
|
||||
@ -69,7 +69,8 @@ extension DomainListVC {
|
||||
.localized(
|
||||
"\(selectedSite.name).\(Valet.shared.config.tld)",
|
||||
newState
|
||||
)
|
||||
),
|
||||
preference: .notifyAboutSecureToggle
|
||||
)
|
||||
}
|
||||
|
||||
@ -116,6 +117,7 @@ extension DomainListVC {
|
||||
|
||||
self.performAction(command: command) {
|
||||
self.selectedSite!.determineIsolated()
|
||||
self.selectedSite!.determineComposerPhpVersion()
|
||||
|
||||
if self.selectedSite!.isolatedPhpVersion == nil {
|
||||
BetterAlert()
|
||||
@ -133,6 +135,7 @@ extension DomainListVC {
|
||||
@objc func removeIsolatedSite() {
|
||||
self.performAction(command: "sudo \(Paths.valet) unisolate --site '\(self.selectedSite!.name)' && exit;") {
|
||||
self.selectedSite!.isolatedPhpVersion = nil
|
||||
self.selectedSite!.determineComposerPhpVersion()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,14 +42,15 @@ extension DomainListVC {
|
||||
addDisabledIsolation(to: menu)
|
||||
}
|
||||
|
||||
addUnlink(to: menu, with: site)
|
||||
menu.addItem(HeaderView.asMenuItem(text: "domain_list.actions".localized))
|
||||
addToggleSecure(to: menu, secured: site.secured)
|
||||
addUnlink(to: menu, with: site)
|
||||
|
||||
tableView.menu = menu
|
||||
}
|
||||
|
||||
private func addSystemApps(to menu: NSMenu) {
|
||||
menu.addItem(withTitle: "domain_list.system_apps".localized, action: nil, keyEquivalent: "")
|
||||
menu.addItem(HeaderView.asMenuItem(text: "domain_list.system_apps".localized))
|
||||
menu.addItem(
|
||||
withTitle: "domain_list.open_in_finder".localized,
|
||||
action: #selector(self.openInFinder),
|
||||
@ -70,7 +71,7 @@ extension DomainListVC {
|
||||
private func addDetectedApps(to menu: NSMenu) {
|
||||
if !applications.isEmpty {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
menu.addItem(withTitle: "domain_list.detected_apps".localized, action: nil, keyEquivalent: "")
|
||||
menu.addItem(HeaderView.asMenuItem(text: "domain_list.detected_apps".localized))
|
||||
|
||||
for editor in applications {
|
||||
let editorMenuItem = EditorMenuItem(
|
||||
@ -96,38 +97,40 @@ extension DomainListVC {
|
||||
}
|
||||
|
||||
private func addDisabledIsolation(to menu: NSMenu) {
|
||||
menu.addItem(HeaderView.asMenuItem(text: "domain_list.site_isolation".localized))
|
||||
menu.addItem(withTitle: "domain_list.isolation_unavailable".localized, action: nil, keyEquivalent: "")
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
private func addIsolate(to menu: NSMenu, with site: ValetSite) {
|
||||
if site.isolatedPhpVersion == nil {
|
||||
// ISOLATION POSSIBLE
|
||||
let isolationMenuItem = NSMenuItem(title: "domain_list.isolate".localized, action: nil, keyEquivalent: "")
|
||||
let submenu = NSMenu()
|
||||
submenu.addItem(withTitle: "Choose a PHP version", action: nil, keyEquivalent: "")
|
||||
for version in PhpEnv.shared.availablePhpVersions.reversed() {
|
||||
let item = PhpMenuItem(
|
||||
title: "Always use PHP \(version)",
|
||||
action: #selector(self.isolateSite),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
item.version = version
|
||||
submenu.addItem(item)
|
||||
}
|
||||
menu.setSubmenu(submenu, for: isolationMenuItem)
|
||||
var items: [NSMenuItem] = []
|
||||
|
||||
menu.addItem(isolationMenuItem)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
} else {
|
||||
// REMOVE ISOLATION POSSIBLE
|
||||
menu.addItem(
|
||||
withTitle: "domain_list.remove_isolation".localized,
|
||||
action: #selector(self.removeIsolatedSite),
|
||||
for version in PhpEnv.shared.availablePhpVersions.reversed() {
|
||||
let item = PhpMenuItem(
|
||||
title: "domain_list.always_use_php".localized(version),
|
||||
action: #selector(self.isolateSite),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
if site.servingPhpVersion == version && site.isolatedPhpVersion != nil {
|
||||
item.state = .on
|
||||
item.action = nil
|
||||
}
|
||||
item.version = version
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
// Add the option to remove site isolation
|
||||
if site.isolatedPhpVersion != nil {
|
||||
items.append(NSMenuItem.separator())
|
||||
items.append(NSMenuItem(
|
||||
title: "domain_list.remove_isolation".localized,
|
||||
action: #selector(self.removeIsolatedSite)
|
||||
))
|
||||
}
|
||||
|
||||
menu.addItem(HeaderView.asMenuItem(text: "domain_list.site_isolation".localized))
|
||||
menu.addItem(NSMenuItem(title: "domain_list.isolate".localized, submenu: items))
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
private func addToggleSecure(to menu: NSMenu, secured: Bool) {
|
||||
|
@ -13,7 +13,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
@IBOutlet weak var tableView: NSTableView!
|
||||
@IBOutlet weak var tableView: PMTableView!
|
||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||
|
||||
// MARK: - Variables
|
||||
@ -64,17 +64,16 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
|
||||
let windowController = storyboard.instantiateController(
|
||||
withIdentifier: "domainListWindow"
|
||||
) as! DomainListWC
|
||||
) as! DomainListWindowController
|
||||
|
||||
windowController.window!.title = "domain_list.title".localized
|
||||
windowController.window!.subtitle = "domain_list.subtitle".localized
|
||||
windowController.window!.delegate = delegate
|
||||
windowController.window!.styleMask = [
|
||||
.titled, .closable, .resizable, .miniaturizable
|
||||
]
|
||||
windowController.window!.minSize = NSSize(width: 550, height: 200)
|
||||
windowController.window!.delegate = windowController
|
||||
windowController.window!.setFrameAutosaveName("domainListWindow")
|
||||
guard let window = windowController.window else { return }
|
||||
|
||||
window.title = "domain_list.title".localized
|
||||
window.subtitle = "domain_list.subtitle".localized
|
||||
window.delegate = delegate ?? windowController
|
||||
window.styleMask = [.titled, .closable, .resizable, .miniaturizable]
|
||||
window.minSize = NSSize(width: 550, height: 200)
|
||||
window.setFrameAutosaveName("domainListWindow")
|
||||
|
||||
App.shared.domainListWindowController = windowController
|
||||
}
|
||||
@ -293,6 +292,6 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
|
||||
// MARK: - Deinitialization
|
||||
|
||||
deinit {
|
||||
Log.perf("DomainListVC deallocated")
|
||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// DomainListWC.swift
|
||||
// DomainListWindowController.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 03/12/2021.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
|
||||
class DomainListWindowController: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
|
||||
|
||||
// MARK: - Window Identifier
|
||||
|
||||
@ -124,8 +124,6 @@ class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate
|
||||
withIdentifier: "addProxyWindow"
|
||||
) as! NSWindowController
|
||||
|
||||
// let viewController = windowController.window!.contentViewController as! AddSiteVC
|
||||
|
||||
self.window?.beginSheet(windowController.window!)
|
||||
}
|
||||
}
|
27
phpmon/Domain/DomainList/PMTableView.swift
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// PMTableView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 05/09/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
/**
|
||||
This subclassed version of NSTableView selects a row upon right-clicking,
|
||||
thus making the domain list behave more like you'd expect.
|
||||
*/
|
||||
public class PMTableView: NSTableView {
|
||||
|
||||
override open func menu(for event: NSEvent) -> NSMenu? {
|
||||
let row = self.row(at: self.convert(event.locationInWindow, from: nil))
|
||||
|
||||
if row >= 0 {
|
||||
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
||||
}
|
||||
|
||||
return super.menu(for: event)
|
||||
}
|
||||
|
||||
}
|
@ -11,7 +11,7 @@ import Cocoa
|
||||
|
||||
class SelectionVC: NSViewController {
|
||||
|
||||
weak var domainListWC: DomainListWC?
|
||||
weak var domainListWC: DomainListWindowController?
|
||||
|
||||
@IBOutlet weak var textFieldTitle: NSTextField!
|
||||
@IBOutlet weak var textFieldDescription: NSTextField!
|
||||
|
@ -13,7 +13,7 @@ class ComposerWindow {
|
||||
private var menu: MainMenu?
|
||||
private var shouldNotify: Bool! = nil
|
||||
private var completion: ((Bool) -> Void)! = nil
|
||||
private var window: ProgressWindowController?
|
||||
private var window: TerminalProgressWindowController?
|
||||
|
||||
/**
|
||||
Updates the global dependencies and runs the completion callback when done.
|
||||
@ -25,7 +25,9 @@ class ComposerWindow {
|
||||
|
||||
Paths.shared.detectBinaryPaths()
|
||||
if Paths.composer == nil {
|
||||
presentMissingAlert()
|
||||
DispatchQueue.main.async {
|
||||
self.presentMissingAlert()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -33,7 +35,7 @@ class ComposerWindow {
|
||||
menu?.setBusyImage()
|
||||
menu?.rebuild()
|
||||
|
||||
window = ProgressWindowController.display(
|
||||
window = TerminalProgressWindowController.display(
|
||||
title: "alert.composer_progress.title".localized,
|
||||
description: "alert.composer_progress.info".localized
|
||||
)
|
||||
@ -50,15 +52,15 @@ class ComposerWindow {
|
||||
}
|
||||
|
||||
task.listen(
|
||||
didReceiveStandardOutputData: { string in
|
||||
didReceiveStandardOutputData: { [weak self] string in
|
||||
DispatchQueue.main.async {
|
||||
self.window?.addToConsole(string)
|
||||
self?.window?.addToConsole(string)
|
||||
}
|
||||
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
|
||||
},
|
||||
didReceiveStandardErrorData: { string in
|
||||
didReceiveStandardErrorData: { [weak self] string in
|
||||
DispatchQueue.main.async {
|
||||
self.window?.addToConsole(string)
|
||||
self?.window?.addToConsole(string)
|
||||
}
|
||||
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
|
||||
}
|
||||
@ -83,11 +85,13 @@ class ComposerWindow {
|
||||
if shouldNotify {
|
||||
LocalNotification.send(
|
||||
title: "alert.composer_success.title".localized,
|
||||
subtitle: "alert.composer_success.info".localized
|
||||
subtitle: "alert.composer_success.info".localized,
|
||||
preference: .notifyAboutGlobalComposerStatus
|
||||
)
|
||||
}
|
||||
window = nil
|
||||
removeBusyStatus()
|
||||
menu = nil
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
@ -100,6 +104,7 @@ class ComposerWindow {
|
||||
window?.progressView?.labelDescription.stringValue = "alert.composer_failure.info".localized
|
||||
window = nil
|
||||
removeBusyStatus()
|
||||
menu = nil
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
@ -115,7 +120,7 @@ class ComposerWindow {
|
||||
|
||||
// MARK: Alert
|
||||
|
||||
private func presentMissingAlert() {
|
||||
@MainActor private func presentMissingAlert() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.composer_missing.title".localized,
|
||||
@ -125,4 +130,8 @@ class ComposerWindow {
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
|
||||
deinit {
|
||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||
}
|
||||
}
|
||||
|
@ -38,8 +38,6 @@ struct PhpFrameworks {
|
||||
"zendframework/zendframework": "Zend",
|
||||
"zendframework/zend-mvc": "Zend",
|
||||
"typo3/cms-core": "Typo3"
|
||||
|
||||
// TODO (6.0): Handle these in v6.0
|
||||
// "magento/*": "Magento",
|
||||
// "concrete5/*": "Concrete5",
|
||||
// "contao/*": "Contao",
|
||||
@ -73,7 +71,7 @@ struct PhpFrameworks {
|
||||
public static func detectFallbackDependency(_ basePath: String) -> String? {
|
||||
for entry in Self.FileMapping {
|
||||
let found = entry.value
|
||||
.map { path in return Filesystem.fileExists(basePath + path) }
|
||||
.map { path in return Filesystem.exists(basePath + path) }
|
||||
.contains(true)
|
||||
|
||||
if found {
|
||||
|
@ -42,13 +42,42 @@ class HomebrewDiagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
It is possible to upgrade PHP, but forget running `valet install`.
|
||||
This results in a scenario where a rogue www.conf file exists.
|
||||
*/
|
||||
public static func checkForPhpFpmPoolConflicts() {
|
||||
Log.info("Checking for PHP-FPM pool conflicts...")
|
||||
|
||||
// We'll need to know what the primary PHP version is
|
||||
let primary = PhpEnv.shared.currentInstall.version.short
|
||||
|
||||
// Versions to be handled
|
||||
let switcher = InternalSwitcher()
|
||||
var versions = switcher.getVersionsToBeHandled(primary)
|
||||
|
||||
versions = versions.filter { version in
|
||||
return switcher.requiresDisablingOfDefaultPhpFpmPool(version)
|
||||
}
|
||||
|
||||
if versions.isEmpty {
|
||||
Log.info("No PHP-FPM pools need to be fixed. All OK.")
|
||||
}
|
||||
|
||||
versions.forEach { version in
|
||||
switcher.disableDefaultPhpFpmPool(version)
|
||||
switcher.stopPhpVersion(version)
|
||||
switcher.startPhpVersion(version, primary: version == primary)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Check if the alias conflict as documented in `checkForCaskConflict` actually occurred.
|
||||
*/
|
||||
private static func hasAliasConflict() -> Bool {
|
||||
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
|
||||
|
||||
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
|
||||
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") || tapAlias.isEmpty {
|
||||
Log.info("The user does not appear to have tapped: shivammathur/php")
|
||||
return false
|
||||
} else {
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// NginxConfiguration.swift
|
||||
// NginxConfigurationFile.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 15/03/2022.
|
||||
@ -8,36 +8,32 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class NginxConfiguration {
|
||||
class NginxConfigurationFile: CreatedFromFile {
|
||||
|
||||
/** Contents of the Nginx file in question, as a string. */
|
||||
/// Contents of the Nginx file in question, as a string.
|
||||
var contents: String!
|
||||
|
||||
/** The name of the domain, usually derived from the name of the file. */
|
||||
/// The name of the domain, usually derived from the name of the file.
|
||||
var domain: String
|
||||
|
||||
/** The TLD of the domain, usually derived from the name of the file. */
|
||||
/// The TLD of the domain, usually derived from the name of the file.
|
||||
var tld: String
|
||||
|
||||
static func from(filePath: String) -> NginxConfiguration? {
|
||||
let path = filePath.replacingOccurrences(
|
||||
of: "~",
|
||||
with: "/Users/\(Paths.whoami)"
|
||||
)
|
||||
/** Resolves an nginx configuration file (.conf) */
|
||||
static func from(filePath: String) -> Self? {
|
||||
let path = filePath.replacingOccurrences(of: "~", with: Paths.homePath)
|
||||
|
||||
do {
|
||||
let fileContents = try String(contentsOfFile: path)
|
||||
return NginxConfiguration.init(
|
||||
path: path,
|
||||
contents: fileContents
|
||||
)
|
||||
|
||||
return Self.init(path: path, contents: fileContents)
|
||||
} catch {
|
||||
Log.warn("Could not read the nginx configuration file at: `\(filePath)`")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
init(path: String, contents: String) {
|
||||
required init(path: String, contents: String) {
|
||||
let domain = String(path.split(separator: "/").last!)
|
||||
let tld = String(domain.split(separator: ".").last!)
|
||||
|
||||
@ -46,9 +42,7 @@ class NginxConfiguration {
|
||||
self.tld = tld
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves what address this domain is proxying.
|
||||
*/
|
||||
/** Retrieves what address this domain is proxying. */
|
||||
lazy var proxy: String? = {
|
||||
let regex = try! NSRegularExpression(
|
||||
pattern: #"proxy_pass (?<proxy>.*:\d*)(\/*);"#,
|
||||
@ -61,9 +55,7 @@ class NginxConfiguration {
|
||||
return contents[Range(match.range(withName: "proxy"), in: contents)!]
|
||||
}()
|
||||
|
||||
/**
|
||||
Retrieves which isolated version is active for this domain (if applicable).
|
||||
*/
|
||||
/** Retrieves which isolated version is active for this domain (if applicable). */
|
||||
lazy var isolatedVersion: String? = {
|
||||
let regex = try! NSRegularExpression(
|
||||
// PHP versions have (so far) never needed multiple digits for version numbers
|
@ -13,8 +13,11 @@ class ValetProxyScanner: ProxyScanner {
|
||||
return try! FileManager
|
||||
.default
|
||||
.contentsOfDirectory(atPath: directoryPath)
|
||||
.filter {
|
||||
return !$0.starts(with: ".")
|
||||
}
|
||||
.compactMap {
|
||||
return NginxConfiguration.from(filePath: "\(directoryPath)/\($0)")
|
||||
return NginxConfigurationFile.from(filePath: "\(directoryPath)/\($0)")
|
||||
}
|
||||
.filter {
|
||||
return $0.proxy != nil
|
||||
|
@ -14,7 +14,7 @@ class ValetProxy: DomainListable {
|
||||
var target: String
|
||||
var secured: Bool = false
|
||||
|
||||
init(_ configuration: NginxConfiguration) {
|
||||
init(_ configuration: NginxConfigurationFile) {
|
||||
self.domain = configuration.domain
|
||||
self.tld = configuration.tld
|
||||
self.target = configuration.proxy!
|
||||
|
@ -23,22 +23,26 @@ extension ValetSite {
|
||||
self.init(name: name, tld: tld, absolutePath: path, aliasPath: nil, makeDeterminations: false)
|
||||
self.secured = secure
|
||||
self.composerPhp = constraint
|
||||
|
||||
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
|
||||
.map { string in
|
||||
return !PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
||||
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
.isEmpty
|
||||
}.contains(true)
|
||||
self.composerPhpSource = constraint != "" ? .require : .unknown
|
||||
|
||||
self.driver = driver
|
||||
self.driverDeterminedByComposer = true
|
||||
|
||||
if linked {
|
||||
self.aliasPath = self.absolutePath
|
||||
}
|
||||
|
||||
if let isolated = isolated {
|
||||
self.isolatedPhpVersion = PhpInstallation(isolated)
|
||||
}
|
||||
|
||||
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
|
||||
.map { string in
|
||||
let origin = self.isolatedPhpVersion?.versionNumber.homebrewVersion ?? PhpEnv.phpInstall.version.long
|
||||
return !PhpVersionNumberCollection.make(from: [origin])
|
||||
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
.isEmpty
|
||||
}.contains(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ class ValetSite: DomainListable {
|
||||
/// replacing the user's home folder with ~.
|
||||
lazy var absolutePathRelative: String = {
|
||||
return self.absolutePath
|
||||
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
|
||||
.replacingOccurrences(of: Paths.homePath, with: "~")
|
||||
}()
|
||||
|
||||
/// The TLD used to locate this site.
|
||||
@ -81,9 +81,9 @@ class ValetSite: DomainListable {
|
||||
|
||||
if makeDeterminations {
|
||||
determineSecured()
|
||||
determineIsolated()
|
||||
determineComposerPhpVersion()
|
||||
determineDriver()
|
||||
determineIsolated()
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +133,6 @@ class ValetSite: DomainListable {
|
||||
with the currently linked version of PHP (see `composerPhpMatchesSystem`).
|
||||
*/
|
||||
public func determineComposerPhpVersion() {
|
||||
|
||||
self.determineComposerInformation()
|
||||
self.determineValetPhpFileInfo()
|
||||
|
||||
@ -145,7 +144,8 @@ class ValetSite: DomainListable {
|
||||
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
|
||||
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
|
||||
.map { string in
|
||||
return !PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
|
||||
let origin = self.isolatedPhpVersion?.versionNumber.homebrewVersion ?? PhpEnv.phpInstall.version.long
|
||||
return !PhpVersionNumberCollection.make(from: [origin])
|
||||
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
.isEmpty
|
||||
}.contains(true)
|
||||
@ -225,7 +225,7 @@ class ValetSite: DomainListable {
|
||||
|
||||
public static func isolatedVersion(_ filePath: String) -> String? {
|
||||
if Filesystem.fileExists(filePath) {
|
||||
return NginxConfiguration
|
||||
return NginxConfigurationFile
|
||||
.from(filePath: filePath)?
|
||||
.isolatedVersion ?? nil
|
||||
}
|
||||
|
@ -78,13 +78,19 @@ class Valet {
|
||||
Notify the user about a non-default TLD being set.
|
||||
*/
|
||||
public static func notifyAboutUnsupportedTLD() {
|
||||
if Valet.shared.config.tld != "test" {
|
||||
if Valet.shared.config.tld != "test" && Preferences.isEnabled(.warnAboutNonStandardTLD) {
|
||||
DispatchQueue.main.async {
|
||||
BetterAlert().withInformation(
|
||||
title: "alert.warnings.tld_issue.title".localized,
|
||||
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
||||
description: "alert.warnings.tld_issue.description".localized
|
||||
).withPrimary(text: "OK").show()
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.withTertiary(text: "alert.do_not_tell_again".localized, action: { alert in
|
||||
Preferences.update(.warnAboutNonStandardTLD, value: false)
|
||||
alert.close(with: .alertThirdButtonReturn)
|
||||
})
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -113,20 +119,11 @@ class Valet {
|
||||
}
|
||||
|
||||
/**
|
||||
Starts the preload of sites, but only if the maximum amount of sites is 30.
|
||||
For users with more sites, the site list is loaded when they bring up the site list window.
|
||||
(This is done to keep the startup speed as fast as possible.)
|
||||
Starts the preload of sites. In order to make sure PHP Monitor can correctly
|
||||
handle all PHP versions including isolation, it needs to know about all sites.
|
||||
*/
|
||||
public func startPreloadingSites() {
|
||||
let maximumPreload = 50
|
||||
let foundSites = self.countPaths()
|
||||
if foundSites <= maximumPreload {
|
||||
// Preload the sites and their drivers
|
||||
Log.info("Fewer than or \(maximumPreload) sites found, preloading list of sites...")
|
||||
self.reloadSites()
|
||||
} else {
|
||||
Log.info("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!")
|
||||
}
|
||||
self.reloadSites()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -192,6 +189,11 @@ class Valet {
|
||||
}
|
||||
}
|
||||
|
||||
public func hasPlatformIssues() -> Bool {
|
||||
return valet("--version", sudo: false)
|
||||
.contains("Composer detected issues in your platform")
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a count of how many sites are linked and parked.
|
||||
*/
|
||||
@ -225,6 +227,8 @@ class Valet {
|
||||
sites.insert(site, at: 0)
|
||||
}
|
||||
|
||||
Log.info("\(sites.count) sites & \(proxies.count) proxies have been scanned.")
|
||||
|
||||
isBusy = false
|
||||
}
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
//
|
||||
// HeaderView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 04/02/2021.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class HeaderView: NSView, XibLoadable {
|
||||
|
||||
@IBOutlet weak var textField: NSTextField!
|
||||
|
||||
static func asMenuItem(text: String) -> NSMenuItem {
|
||||
let view = Self.createFromXib()
|
||||
view!.textField.stringValue = text.uppercased()
|
||||
let item = NSMenuItem()
|
||||
item.view = view
|
||||
item.target = self
|
||||
return item
|
||||
}
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner"/>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customView id="c22-O7-iKe" customClass="HeaderView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="270" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ddg-VQ-cOT">
|
||||
<rect key="frame" x="12" y="5" width="113" height="15"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="left" title="ACTIVE SERVICES" id="NHz-MZ-8FK">
|
||||
<font key="font" metaFont="systemBold" size="12"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ddg-VQ-cOT" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="n4Z-WN-RIh"/>
|
||||
<constraint firstItem="ddg-VQ-cOT" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="14" id="yuW-pb-GQJ"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="textField" destination="ddg-VQ-cOT" id="aaQ-Xb-o2X"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="177" y="105"/>
|
||||
</customView>
|
||||
</objects>
|
||||
</document>
|
291
phpmon/Domain/Menu/MainMenu+Actions.swift
Normal file
@ -0,0 +1,291 @@
|
||||
//
|
||||
// MainMenu+Actions.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 19/05/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension MainMenu {
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc func fixHomebrewPermissions() {
|
||||
if !BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_homebrew_permissions.title".localized,
|
||||
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
|
||||
description: "alert.fix_homebrew_permissions.desc".localized
|
||||
)
|
||||
.withPrimary(text: "alert.fix_homebrew_permissions.ok".localized)
|
||||
.withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized)
|
||||
.didSelectPrimary() {
|
||||
return
|
||||
}
|
||||
|
||||
asyncExecution {
|
||||
try Actions.fixHomebrewPermissions()
|
||||
} success: {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_homebrew_permissions_done.title".localized,
|
||||
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
|
||||
description: "alert.fix_homebrew_permissions_done.desc".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
} failure: { error in
|
||||
BetterAlert.show(for: error as! HomebrewPermissionError)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restartPhpFpm() {
|
||||
asyncExecution {
|
||||
Actions.restartPhpFpm()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restartValetServices() {
|
||||
asyncExecution {
|
||||
Actions.restartDnsMasq()
|
||||
Actions.restartPhpFpm()
|
||||
Actions.restartNginx()
|
||||
} success: {
|
||||
LocalNotification.send(
|
||||
title: "notification.services_restarted".localized,
|
||||
subtitle: "notification.services_restarted_desc".localized,
|
||||
preference: .notifyAboutServices
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func stopValetServices() {
|
||||
asyncExecution {
|
||||
Actions.stopValetServices()
|
||||
} success: {
|
||||
LocalNotification.send(
|
||||
title: "notification.services_stopped".localized,
|
||||
subtitle: "notification.services_stopped_desc".localized,
|
||||
preference: .notifyAboutServices
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restartNginx() {
|
||||
asyncExecution {
|
||||
Actions.restartNginx()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restartDnsMasq() {
|
||||
asyncExecution {
|
||||
Actions.restartDnsMasq()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func disableAllXdebugModes() {
|
||||
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
|
||||
Log.info("xdebug.mode could not be found in any .ini file, aborting.")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try file.replace(key: "xdebug.mode", value: "off")
|
||||
|
||||
Log.perf("Refreshing menu...")
|
||||
MainMenu.shared.rebuild()
|
||||
restartPhpFpm()
|
||||
} catch {
|
||||
Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleXdebugMode(sender: XdebugMenuItem) {
|
||||
Log.info("Switching Xdebug to mode: \(sender.mode)")
|
||||
|
||||
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
|
||||
return Log.info("xdebug.mode could not be found in any .ini file, aborting.")
|
||||
}
|
||||
|
||||
do {
|
||||
var modes = Xdebug.activeModes
|
||||
|
||||
if let index = modes.firstIndex(of: sender.mode) {
|
||||
modes.remove(at: index)
|
||||
} else {
|
||||
modes.append(sender.mode)
|
||||
}
|
||||
|
||||
var newValue = modes.joined(separator: ",")
|
||||
if newValue.isEmpty {
|
||||
newValue = "off"
|
||||
}
|
||||
|
||||
try file.replace(key: "xdebug.mode", value: newValue)
|
||||
|
||||
Log.perf("Refreshing menu...")
|
||||
MainMenu.shared.rebuild()
|
||||
restartPhpFpm()
|
||||
} catch {
|
||||
Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
||||
asyncExecution {
|
||||
sender.phpExtension?.toggle()
|
||||
|
||||
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
|
||||
Actions.restartPhpFpm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performRollback() {
|
||||
asyncExecution {
|
||||
PresetHelper.rollbackPreset?.apply()
|
||||
PresetHelper.rollbackPreset = nil
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor @objc func rollbackPreset() {
|
||||
guard let preset = PresetHelper.rollbackPreset else {
|
||||
return
|
||||
}
|
||||
|
||||
BetterAlert().withInformation(
|
||||
title: "alert.revert_description.title".localized,
|
||||
subtitle: "alert.revert_description.subtitle".localized(
|
||||
preset.textDescription
|
||||
)
|
||||
)
|
||||
.withPrimary(text: "alert.revert_description.ok".localized, action: { alert in
|
||||
alert.close(with: .OK)
|
||||
self.performRollback()
|
||||
})
|
||||
.withSecondary(text: "alert.revert_description.cancel".localized)
|
||||
.show()
|
||||
}
|
||||
|
||||
@objc func togglePreset(sender: PresetMenuItem) {
|
||||
asyncExecution {
|
||||
sender.preset?.apply()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor @objc func showPresetHelp() {
|
||||
BetterAlert().withInformation(
|
||||
title: "preset_help_title".localized,
|
||||
subtitle: "preset_help_info".localized,
|
||||
description: "preset_help_desc".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.withTertiary(text: "", action: { alert in
|
||||
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
|
||||
alert.close(with: .OK)
|
||||
})
|
||||
.show()
|
||||
}
|
||||
|
||||
@objc func openPhpInfo() {
|
||||
var url: URL?
|
||||
|
||||
asyncWithBusyUI {
|
||||
url = Actions.createTempPhpInfoFile()
|
||||
} completion: {
|
||||
if url != nil { NSWorkspace.shared.open(url!) }
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateGlobalComposerDependencies() {
|
||||
ComposerWindow().updateGlobalDependencies(
|
||||
notify: true,
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
@objc func openActiveConfigFolder() {
|
||||
if PhpEnv.phpInstall.version.error {
|
||||
Actions.openGenericPhpConfigFolder()
|
||||
return
|
||||
}
|
||||
|
||||
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
|
||||
}
|
||||
|
||||
@objc func openPhpMonitorConfigurationFile() {
|
||||
Actions.openPhpMonitorConfigFile()
|
||||
}
|
||||
|
||||
@objc func openGlobalComposerFolder() {
|
||||
Actions.openGlobalComposerFolder()
|
||||
}
|
||||
|
||||
@objc func openValetConfigFolder() {
|
||||
Actions.openValetConfigFolder()
|
||||
}
|
||||
|
||||
@objc func switchToPhpVersion(sender: PhpMenuItem) {
|
||||
self.switchToPhpVersion(sender.version)
|
||||
}
|
||||
|
||||
@objc func switchToPhpVersion(_ version: String) {
|
||||
setBusyImage()
|
||||
PhpEnv.shared.isBusy = true
|
||||
PhpEnv.shared.delegate = self
|
||||
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
rebuild()
|
||||
PhpEnv.switcher.performSwitch(
|
||||
to: version,
|
||||
completion: {
|
||||
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Async
|
||||
|
||||
/**
|
||||
This async-friendly version of the switcher can be invoked elsewhere in the app:
|
||||
```
|
||||
Task {
|
||||
await MainMenu.shared.switchToPhp("8.1")
|
||||
// thing to do after the switch
|
||||
}
|
||||
```
|
||||
Since this async function uses `withCheckedContinuation`
|
||||
any code after will run only after the switcher is done.
|
||||
*/
|
||||
func switchToPhp(_ version: String) async {
|
||||
DispatchQueue.main.async { [self] in
|
||||
setBusyImage()
|
||||
PhpEnv.shared.isBusy = true
|
||||
PhpEnv.shared.delegate = self
|
||||
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
}
|
||||
|
||||
return await withCheckedContinuation({ continuation in
|
||||
updatePhpVersionInStatusBar()
|
||||
rebuild()
|
||||
PhpEnv.switcher.performSwitch(
|
||||
to: version,
|
||||
completion: {
|
||||
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version)
|
||||
continuation.resume()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -36,8 +36,8 @@ extension MainMenu {
|
||||
*/
|
||||
func asyncExecution(
|
||||
_ execute: @escaping () throws -> Void,
|
||||
success: @escaping () -> Void = {},
|
||||
failure: @escaping (Error) -> Void = { _ in },
|
||||
success: @MainActor @escaping () -> Void = {},
|
||||
failure: @MainActor @escaping (Error) -> Void = { _ in },
|
||||
behaviours: [AsyncBehaviour] = [
|
||||
.setsBusyUI,
|
||||
.reloadsPhpInstallation,
|
||||
@ -74,10 +74,14 @@ extension MainMenu {
|
||||
}
|
||||
|
||||
if behaviours.contains(.broadcastServicesUpdate) {
|
||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||
ServicesManager.shared.loadData()
|
||||
}
|
||||
|
||||
error == nil ? success() : failure(error!)
|
||||
if error != nil {
|
||||
return failure(error!)
|
||||
}
|
||||
|
||||
success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import AppKit
|
||||
|
||||
extension MainMenu {
|
||||
|
||||
@objc func fixMyValet() {
|
||||
@MainActor @objc func fixMyValet() {
|
||||
let previousVersion = PhpEnv.phpInstall.version.short
|
||||
|
||||
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
|
||||
@ -42,7 +42,7 @@ extension MainMenu {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentAlertForMissingFormula() {
|
||||
@MainActor private func presentAlertForMissingFormula() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.php_formula_missing.title".localized,
|
||||
@ -52,7 +52,7 @@ extension MainMenu {
|
||||
.show()
|
||||
}
|
||||
|
||||
private func presentAlertForSameVersion() {
|
||||
@MainActor private func presentAlertForSameVersion() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_my_valet_done.title".localized,
|
||||
@ -63,7 +63,7 @@ extension MainMenu {
|
||||
.show()
|
||||
}
|
||||
|
||||
private func presentAlertForDifferentVersion(version: String) {
|
||||
@MainActor private func presentAlertForDifferentVersion(version: String) {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_my_valet_done.title".localized,
|
||||
|
@ -49,35 +49,26 @@ extension MainMenu {
|
||||
// Check for an alias conflict
|
||||
HomebrewDiagnostics.checkForCaskConflict()
|
||||
|
||||
// Update the icon
|
||||
updatePhpVersionInStatusBar()
|
||||
|
||||
Log.info("Determining broken PHP-FPM...")
|
||||
// Attempt to find out if PHP-FPM is broken
|
||||
Log.info("Determining broken PHP-FPM...")
|
||||
let installation = PhpEnv.phpInstall
|
||||
installation.notifyAboutBrokenPhpFpm()
|
||||
|
||||
// Set up the config watchers on launch
|
||||
// (these are automatically updated via delegate methods if the user switches)
|
||||
// Check for other problems
|
||||
WarningManager.shared.evaluateWarnings()
|
||||
|
||||
// Set up the config watchers on launch (updated automatically when switching)
|
||||
Log.info("Setting up watchers...")
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
|
||||
// Detect applications (preset + custom)
|
||||
Log.info("Detecting applications...")
|
||||
App.shared.detectedApplications = Application.detectPresetApplications()
|
||||
// Detect built-in and custom applications
|
||||
detectApplications()
|
||||
|
||||
let customApps = Preferences.custom.scanApps.map { appName in
|
||||
return Application(appName, .user_supplied)
|
||||
}.filter { app in
|
||||
return app.isInstalled()
|
||||
}
|
||||
|
||||
App.shared.detectedApplications.append(contentsOf: customApps)
|
||||
|
||||
let appNames = App.shared.detectedApplications.map { app in
|
||||
return app.name
|
||||
}
|
||||
|
||||
Log.info("Detected applications: \(appNames)")
|
||||
// Load the rollback preset
|
||||
PresetHelper.loadRollbackPresetFromFile()
|
||||
|
||||
// Load the global hotkey
|
||||
App.shared.loadGlobalHotkey()
|
||||
@ -85,29 +76,36 @@ extension MainMenu {
|
||||
// Preload sites
|
||||
Valet.shared.startPreloadingSites()
|
||||
|
||||
// After preloading sites, check for PHP-FPM pool conflicts
|
||||
HomebrewDiagnostics.checkForPhpFpmPoolConflicts()
|
||||
|
||||
// A non-default TLD is not officially supported since Valet 3.2.x
|
||||
Valet.notifyAboutUnsupportedTLD()
|
||||
|
||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||
// Find out which services are active
|
||||
ServicesManager.shared.loadData()
|
||||
|
||||
// Schedule a request to fetch the PHP version every 60 seconds
|
||||
DispatchQueue.main.async { [self] in
|
||||
App.shared.timer = Timer.scheduledTimer(
|
||||
timeInterval: 60,
|
||||
target: self,
|
||||
selector: #selector(refreshActiveInstallation),
|
||||
userInfo: nil,
|
||||
repeats: true
|
||||
)
|
||||
}
|
||||
// Start the background refresh timer
|
||||
startSharedTimer()
|
||||
|
||||
// Update the stats
|
||||
Stats.incrementSuccessfulLaunchCount()
|
||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||
|
||||
// Present first launch screen if needed
|
||||
if Stats.successfulLaunchCount == 0 && !isRunningSwiftUIPreview {
|
||||
Log.info("Should present the first launch screen!")
|
||||
DispatchQueue.main.async {
|
||||
OnboardingWindowController.show()
|
||||
}
|
||||
}
|
||||
|
||||
// Check for updates
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
AppUpdateChecker.checkIfNewerVersionIsAvailable()
|
||||
}
|
||||
|
||||
// We are ready!
|
||||
Log.info("PHP Monitor is ready to serve!")
|
||||
}
|
||||
|
||||
@ -133,4 +131,42 @@ extension MainMenu {
|
||||
Task { await startup() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Schedule a request to fetch the PHP version every 60 seconds.
|
||||
*/
|
||||
private func startSharedTimer() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
App.shared.timer = Timer.scheduledTimer(
|
||||
timeInterval: 60,
|
||||
target: self,
|
||||
selector: #selector(refreshActiveInstallation),
|
||||
userInfo: nil,
|
||||
repeats: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Detect which applications are installed that can be used to open a domain's source directory.
|
||||
*/
|
||||
private func detectApplications() {
|
||||
Log.info("Detecting applications...")
|
||||
|
||||
App.shared.detectedApplications = Application.detectPresetApplications()
|
||||
|
||||
let customApps = Preferences.custom.scanApps?.map { appName in
|
||||
return Application(appName, .user_supplied)
|
||||
}.filter { app in
|
||||
return app.isInstalled()
|
||||
} ?? []
|
||||
|
||||
App.shared.detectedApplications.append(contentsOf: customApps)
|
||||
|
||||
let appNames = App.shared.detectedApplications.map { app in
|
||||
return app.name
|
||||
}
|
||||
|
||||
Log.info("Detected applications: \(appNames)")
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,6 @@ extension MainMenu {
|
||||
func switcherDidStartSwitching(to version: String) {}
|
||||
|
||||
func switcherDidCompleteSwitch(to version: String) {
|
||||
// Update the PHP version
|
||||
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
||||
|
||||
// Ensure the config watcher gets reloaded
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
|
||||
// Mark as no longer busy
|
||||
PhpEnv.shared.isBusy = false
|
||||
|
||||
@ -45,22 +39,32 @@ extension MainMenu {
|
||||
self.notifyAboutVersionChange(to: version)
|
||||
}
|
||||
)
|
||||
|
||||
} else {
|
||||
self.notifyAboutVersionChange(to: version)
|
||||
}
|
||||
|
||||
// Check if Valet still works correctly
|
||||
self.checkForPlatformIssues()
|
||||
|
||||
// Update stats
|
||||
Stats.incrementSuccessfulSwitchCount()
|
||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor private func checkForPlatformIssues() {
|
||||
if Valet.shared.hasPlatformIssues() {
|
||||
Log.info("Composer platform issue(s) detected.")
|
||||
self.suggestFixMyComposer()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor private func suggestFixMyValet(failed version: String) {
|
||||
let outcome = BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.php_switch_failed.title".localized(version),
|
||||
subtitle: "alert.php_switch_failed.info".localized(version)
|
||||
subtitle: "alert.php_switch_failed.info".localized(version),
|
||||
description: "alert.php_switch_failed.desc".localized()
|
||||
)
|
||||
.withPrimary(text: "alert.php_switch_failed.confirm".localized)
|
||||
.withSecondary(text: "alert.php_switch_failed.cancel".localized)
|
||||
@ -70,6 +74,32 @@ extension MainMenu {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor private func suggestFixMyComposer() {
|
||||
BetterAlert().withInformation(
|
||||
title: "alert.global_composer_platform_issues.title".localized,
|
||||
subtitle: "alert.global_composer_platform_issues.subtitle".localized,
|
||||
description: "alert.global_composer_platform_issues.desc".localized
|
||||
)
|
||||
.withPrimary(text: "alert.global_composer_platform_issues.buttons.update".localized, action: { alert in
|
||||
alert.close(with: .OK)
|
||||
Log.info("The user has chosen to update global dependencies.")
|
||||
ComposerWindow().updateGlobalDependencies(
|
||||
notify: true,
|
||||
completion: { success in
|
||||
Log.info("Dependencies updated successfully: \(success)")
|
||||
Log.info("Re-checking for platform issue(s)...")
|
||||
self.checkForPlatformIssues()
|
||||
}
|
||||
)
|
||||
})
|
||||
.withSecondary(text: "", action: nil)
|
||||
.withTertiary(text: "alert.global_composer_platform_issues.buttons.quit".localized, action: { alert in
|
||||
alert.close(with: .OK)
|
||||
self.terminateApp()
|
||||
})
|
||||
.show()
|
||||
}
|
||||
|
||||
private func reloadDomainListData() {
|
||||
if let window = App.shared.domainListWindowController {
|
||||
DispatchQueue.main.async {
|
||||
@ -81,11 +111,14 @@ extension MainMenu {
|
||||
}
|
||||
|
||||
private func notifyAboutVersionChange(to version: String) {
|
||||
LocalNotification.send(
|
||||
title: String(format: "notification.version_changed_title".localized, version),
|
||||
subtitle: String(format: "notification.version_changed_desc".localized, version)
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
LocalNotification.send(
|
||||
title: String(format: "notification.version_changed_title".localized, version),
|
||||
subtitle: String(format: "notification.version_changed_desc".localized, version),
|
||||
preference: .notifyAboutVersionChange
|
||||
)
|
||||
|
||||
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
|
||||
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,12 +11,17 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
|
||||
static let shared = MainMenu()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
statusItem.isVisible = !isRunningSwiftUIPreview
|
||||
}
|
||||
|
||||
weak var menuDelegate: NSMenuDelegate?
|
||||
|
||||
/**
|
||||
The status bar item with variable length.
|
||||
*/
|
||||
let statusItem = NSStatusBar.system.statusItem(
|
||||
@MainActor let statusItem = NSStatusBar.system.statusItem(
|
||||
withLength: NSStatusItem.variableLength
|
||||
)
|
||||
|
||||
@ -45,33 +50,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
Use `rebuild(async:)` to ensure the rebuilding happens in the background.
|
||||
*/
|
||||
private func rebuildMenu() {
|
||||
// Create a new menu
|
||||
let menu = StatusMenu()
|
||||
|
||||
// Add the PHP versions (or error messages)
|
||||
menu.addPhpVersionMenuItems()
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
// Add the possible actions
|
||||
menu.addPhpActionMenuItems()
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
// Add Valet interactions
|
||||
menu.addValetMenuItems()
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
// Add services
|
||||
menu.addRemainingMenuItems()
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
// Add about & quit menu items
|
||||
menu.addCoreMenuItems()
|
||||
|
||||
// Make sure every item can be interacted with
|
||||
menu.addMenuItems()
|
||||
menu.items.forEach({ (item) in
|
||||
item.target = self
|
||||
})
|
||||
|
||||
statusItem.menu = menu
|
||||
statusItem.menu?.delegate = self
|
||||
}
|
||||
@ -79,7 +62,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
/**
|
||||
Sets the status bar image based on a version string.
|
||||
*/
|
||||
func setStatusBarImage(version: String) {
|
||||
@MainActor func setStatusBarImage(version: String) {
|
||||
setStatusBar(
|
||||
image: (Preferences.preferences[.iconTypeToDisplay] as! String != MenuBarIcon.noIcon.rawValue)
|
||||
? MenuBarImageGenerator.textToImageWithIcon(text: version)
|
||||
@ -91,7 +74,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
Sets the status bar image, based on the provided NSImage.
|
||||
The image will be used as a template image.
|
||||
*/
|
||||
func setStatusBar(image: NSImage) {
|
||||
@MainActor func setStatusBar(image: NSImage) {
|
||||
if let button = statusItem.button {
|
||||
image.isTemplate = true
|
||||
button.image = image
|
||||
@ -124,7 +107,17 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
refreshActiveInstallation()
|
||||
refreshIcon()
|
||||
rebuild(async: false)
|
||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||
ServicesManager.shared.loadData()
|
||||
}
|
||||
|
||||
/**
|
||||
Shows the Welcome Tour screen, again.
|
||||
Did this need a comment? No, probably not.
|
||||
*/
|
||||
@objc func showWelcomeTour() {
|
||||
DispatchQueue.main.async {
|
||||
OnboardingWindowController.show()
|
||||
}
|
||||
}
|
||||
|
||||
/** Reloads the menu in the background, using `asyncExecution`. */
|
||||
@ -165,155 +158,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc func fixHomebrewPermissions() {
|
||||
if !BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_homebrew_permissions.title".localized,
|
||||
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
|
||||
description: "alert.fix_homebrew_permissions.desc".localized
|
||||
)
|
||||
.withPrimary(text: "alert.fix_homebrew_permissions.ok".localized)
|
||||
.withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized)
|
||||
.didSelectPrimary() {
|
||||
return
|
||||
}
|
||||
|
||||
asyncExecution {
|
||||
try Actions.fixHomebrewPermissions()
|
||||
} success: {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_homebrew_permissions_done.title".localized,
|
||||
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
|
||||
description: "alert.fix_homebrew_permissions_done.desc".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
} failure: { error in
|
||||
BetterAlert.show(for: error as! HomebrewPermissionError)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restartPhpFpm() {
|
||||
asyncExecution {
|
||||
Actions.restartPhpFpm()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restartAllServices() {
|
||||
asyncExecution {
|
||||
Actions.restartDnsMasq()
|
||||
Actions.restartPhpFpm()
|
||||
Actions.restartNginx()
|
||||
} success: {
|
||||
DispatchQueue.main.async {
|
||||
LocalNotification.send(
|
||||
title: "notification.services_restarted".localized,
|
||||
subtitle: "notification.services_restarted_desc".localized
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func stopAllServices() {
|
||||
asyncExecution {
|
||||
Actions.stopAllServices()
|
||||
} success: {
|
||||
DispatchQueue.main.async {
|
||||
LocalNotification.send(
|
||||
title: "notification.services_stopped".localized,
|
||||
subtitle: "notification.services_stopped_desc".localized
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restartNginx() {
|
||||
asyncExecution {
|
||||
Actions.restartNginx()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restartDnsMasq() {
|
||||
asyncExecution {
|
||||
Actions.restartDnsMasq()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleXdebugMode(sender: XdebugMenuItem) {
|
||||
Log.info("Switching Xdebug to mode: \(sender.mode)")
|
||||
}
|
||||
|
||||
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
||||
asyncExecution {
|
||||
sender.phpExtension?.toggle()
|
||||
|
||||
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
|
||||
Actions.restartPhpFpm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func openPhpInfo() {
|
||||
var url: URL?
|
||||
|
||||
asyncWithBusyUI {
|
||||
url = Actions.createTempPhpInfoFile()
|
||||
} completion: {
|
||||
if url != nil { NSWorkspace.shared.open(url!) }
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateGlobalComposerDependencies() {
|
||||
ComposerWindow().updateGlobalDependencies(
|
||||
notify: true,
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
@objc func openActiveConfigFolder() {
|
||||
if PhpEnv.phpInstall.version.error {
|
||||
// php version was not identified
|
||||
Actions.openGenericPhpConfigFolder()
|
||||
return
|
||||
}
|
||||
|
||||
// php version was identified
|
||||
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
|
||||
}
|
||||
|
||||
@objc func openGlobalComposerFolder() {
|
||||
Actions.openGlobalComposerFolder()
|
||||
}
|
||||
|
||||
@objc func openValetConfigFolder() {
|
||||
Actions.openValetConfigFolder()
|
||||
}
|
||||
|
||||
@objc func switchToPhpVersion(sender: PhpMenuItem) {
|
||||
self.switchToPhpVersion(sender.version)
|
||||
}
|
||||
|
||||
@objc func switchToPhpVersion(_ version: String) {
|
||||
setBusyImage()
|
||||
PhpEnv.shared.isBusy = true
|
||||
PhpEnv.shared.delegate = self
|
||||
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||
updatePhpVersionInStatusBar()
|
||||
rebuild()
|
||||
PhpEnv.switcher.performSwitch(
|
||||
to: version,
|
||||
completion: {
|
||||
PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Menu Item Functionality
|
||||
|
||||
@objc func openAbout() {
|
||||
@ -322,7 +166,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
|
||||
@objc func openPrefs() {
|
||||
PrefsVC.show()
|
||||
PreferencesWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openWarnings() {
|
||||
WarningsWindowController.show()
|
||||
}
|
||||
|
||||
@objc func openDomainList() {
|
||||
@ -348,7 +196,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
func menuWillOpen(_ menu: NSMenu) {
|
||||
// Make sure the shortcut key does not trigger this when the menu is open
|
||||
App.shared.shortcutHotkey?.isPaused = true
|
||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||
ServicesManager.shared.loadData()
|
||||
}
|
||||
|
||||
func menuDidClose(_ menu: NSMenu) {
|
||||
|
@ -1,93 +0,0 @@
|
||||
//
|
||||
// StatsView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 04/02/2021.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
/**
|
||||
The ServicesView is an example of a view that I consider to be "poorly" set up.
|
||||
Why ship it like this, then? Well, it works — that's reason number one, really.
|
||||
|
||||
However, I do believe this should be refactored at some point. Here's why:
|
||||
this view is responsible for retaining the information about the services status.
|
||||
|
||||
The status of the services should live somewhere else, and the fetching of said
|
||||
service information should also not happen in a view. Yet here we are.
|
||||
*/
|
||||
class ServicesView: NSView, XibLoadable {
|
||||
|
||||
@IBOutlet weak var imageViewPhp: NSImageView!
|
||||
@IBOutlet weak var imageViewNginx: NSImageView!
|
||||
@IBOutlet weak var imageViewDnsmasq: NSImageView!
|
||||
|
||||
@IBOutlet weak var textFieldPhp: NSTextField!
|
||||
|
||||
static var services: [String: HomebrewService] = [:]
|
||||
|
||||
static func asMenuItem() -> NSMenuItem {
|
||||
let view = Self.createFromXib()!
|
||||
[view.imageViewPhp, view.imageViewNginx, view.imageViewDnsmasq].forEach { imageView in
|
||||
imageView?.contentTintColor = NSColor(named: "IconColorNormal")
|
||||
}
|
||||
let item = NSMenuItem()
|
||||
item.view = view
|
||||
item.target = self
|
||||
NotificationCenter.default.addObserver(
|
||||
view, selector: #selector(self.updateInformation),
|
||||
name: Events.ServicesUpdated,
|
||||
object: nil
|
||||
)
|
||||
return item
|
||||
}
|
||||
|
||||
@objc func updateInformation() {
|
||||
self.loadData()
|
||||
}
|
||||
|
||||
func loadData() {
|
||||
self.applyAllInfoFieldsFromCachedValue()
|
||||
HomebrewService.loadAll { services in
|
||||
ServicesView.services = Dictionary(uniqueKeysWithValues: services.map { ($0.name, $0) })
|
||||
self.applyAllInfoFieldsFromCachedValue()
|
||||
}
|
||||
}
|
||||
|
||||
func applyAllInfoFieldsFromCachedValue() {
|
||||
if ServicesView.services.keys.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.textFieldPhp.stringValue = PhpEnv.phpInstall.formula.uppercased()
|
||||
self.applyServiceStyling(PhpEnv.phpInstall.formula, self.imageViewPhp)
|
||||
self.applyServiceStyling("nginx", self.imageViewNginx)
|
||||
self.applyServiceStyling("dnsmasq", self.imageViewDnsmasq)
|
||||
}
|
||||
}
|
||||
|
||||
func applyServiceStyling(_ serviceName: String, _ imageView: NSImageView) {
|
||||
if ServicesView.services[serviceName] == nil {
|
||||
imageView.image = NSImage(named: "ServiceLoading")
|
||||
imageView.contentTintColor = NSColor(named: "IconColorNormal")
|
||||
return
|
||||
}
|
||||
|
||||
if ServicesView.services[serviceName]!.running {
|
||||
imageView.image = NSImage(named: "ServiceOn")
|
||||
imageView.contentTintColor = NSColor(named: "IconColorNormal")
|
||||
return
|
||||
}
|
||||
|
||||
imageView.image = NSImage(named: "ServiceOff")
|
||||
imageView.contentTintColor = NSColor(named: "IconColorRed")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self, name: Events.ServicesUpdated, object: nil)
|
||||
}
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner"/>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customView wantsLayer="YES" id="c22-O7-iKe" customClass="ServicesView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="330" height="46"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
|
||||
<rect key="frame" x="30" y="3" width="270" height="40"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
|
||||
<rect key="frame" x="0.0" y="4" width="77" height="32"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="At1-ch-qv2">
|
||||
<rect key="frame" x="23" y="18" width="31" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="PHP" id="LKe-C4-jxo">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="tko-cP-XSz">
|
||||
<rect key="frame" x="26" y="0.0" width="24" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="16" id="Fxu-6h-A2h"/>
|
||||
<constraint firstAttribute="width" constant="24" id="hOc-Ur-dmA"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="vjB-6Z-3xR"/>
|
||||
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<stackView distribution="fillEqually" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g4d-4N-NkC">
|
||||
<rect key="frame" x="97" y="4" width="76" height="32"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7um-XA-djV">
|
||||
<rect key="frame" x="18" y="18" width="40" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="NGINX" id="Qfq-Bl-yuh">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ZqW-6d-vpe">
|
||||
<rect key="frame" x="30" y="0.0" width="16" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="16" id="EPG-jm-7Xs"/>
|
||||
<constraint firstAttribute="width" constant="16" id="iif-kT-phn"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="JmQ-dU-ip7"/>
|
||||
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
|
||||
<rect key="frame" x="193" y="4" width="77" height="32"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
|
||||
<rect key="frame" x="8" y="18" width="62" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="DNSMASQ" id="lGh-MT-TgI">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="DcG-x3-lvy">
|
||||
<rect key="frame" x="31" y="0.0" width="16" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="16" id="AKl-Gq-RtM"/>
|
||||
<constraint firstAttribute="height" constant="16" id="q2g-Ua-eIJ"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="Ign-Cq-DKf"/>
|
||||
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="40" id="2EU-Fd-hMg"/>
|
||||
<constraint firstItem="nWj-33-m8Q" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="CAY-Pw-B8n"/>
|
||||
<constraint firstAttribute="bottom" secondItem="doH-ww-BDw" secondAttribute="bottom" constant="4" id="Dq4-M6-1Wf"/>
|
||||
<constraint firstItem="g4d-4N-NkC" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="bls-fM-H4b"/>
|
||||
<constraint firstAttribute="bottom" secondItem="nWj-33-m8Q" secondAttribute="bottom" constant="4" id="f6j-eI-wiH"/>
|
||||
<constraint firstAttribute="bottom" secondItem="g4d-4N-NkC" secondAttribute="bottom" constant="4" id="faS-Mo-Qa2"/>
|
||||
<constraint firstItem="doH-ww-BDw" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="gL3-5S-OKo"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="TnH-dX-qaQ" secondAttribute="trailing" constant="30" id="3dD-wf-5pS"/>
|
||||
<constraint firstItem="TnH-dX-qaQ" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="3" id="JmY-D0-uAy"/>
|
||||
<constraint firstItem="TnH-dX-qaQ" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="30" id="S8i-CD-j3h"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TnH-dX-qaQ" secondAttribute="bottom" constant="3" id="fDc-OY-YL0"/>
|
||||
<constraint firstItem="TnH-dX-qaQ" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="fFF-rl-3s4"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="imageViewDnsmasq" destination="DcG-x3-lvy" id="XxJ-kZ-bdO"/>
|
||||
<outlet property="imageViewNginx" destination="ZqW-6d-vpe" id="Wil-Ug-8Kb"/>
|
||||
<outlet property="imageViewPhp" destination="tko-cP-XSz" id="q7L-HK-7Pj"/>
|
||||
<outlet property="textFieldPhp" destination="At1-ch-qv2" id="Guk-hr-f1T"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-64" y="195"/>
|
||||
</customView>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ServiceLoading" width="17" height="16"/>
|
||||
</resources>
|
||||
</document>
|
@ -1,36 +0,0 @@
|
||||
//
|
||||
// StatsView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 04/02/2021.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class StatsView: NSView, XibLoadable {
|
||||
|
||||
@IBOutlet weak var titleMemLimit: NSTextField!
|
||||
@IBOutlet weak var titleMaxPost: NSTextField!
|
||||
@IBOutlet weak var titleMaxUpload: NSTextField!
|
||||
|
||||
@IBOutlet weak var labelMemLimit: NSTextField!
|
||||
@IBOutlet weak var labelMaxPost: NSTextField!
|
||||
@IBOutlet weak var labelMaxUpload: NSTextField!
|
||||
|
||||
static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem {
|
||||
let view = Self.createFromXib()
|
||||
view!.titleMemLimit.stringValue = "mi_memory_limit".localized.uppercased()
|
||||
view!.titleMaxPost.stringValue = "mi_post_max_size".localized.uppercased()
|
||||
view!.titleMaxUpload.stringValue = "mi_upload_max_filesize".localized.uppercased()
|
||||
view!.labelMemLimit.stringValue = memory
|
||||
view!.labelMaxPost.stringValue = post
|
||||
view!.labelMaxUpload.stringValue = upload
|
||||
let item = NSMenuItem()
|
||||
item.view = view
|
||||
item.target = self
|
||||
return item
|
||||
}
|
||||
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner"/>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customView id="c22-O7-iKe" customClass="StatsView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="330" height="55"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
|
||||
<rect key="frame" x="30" y="6" width="270" height="43"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
|
||||
<rect key="frame" x="0.0" y="4" width="87" height="35"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="At1-ch-qv2">
|
||||
<rect key="frame" x="-2" y="21" width="91" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="MEMORY LIMIT" id="LKe-C4-jxo">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Emt-m3-Dt6">
|
||||
<rect key="frame" x="16" y="0.0" width="55" height="19"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="H6T-wY-PIG">
|
||||
<font key="font" metaFont="systemMedium" size="16"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<stackView distribution="fillEqually" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g4d-4N-NkC">
|
||||
<rect key="frame" x="107" y="4" width="68" height="35"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7um-XA-djV">
|
||||
<rect key="frame" x="3" y="21" width="63" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX POST" id="Qfq-Bl-yuh">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Vyu-AO-8SH">
|
||||
<rect key="frame" x="7" y="0.0" width="55" height="19"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="uH4-Zy-43x">
|
||||
<font key="font" metaFont="systemMedium" size="16"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
|
||||
<rect key="frame" x="195" y="4" width="75" height="35"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
|
||||
<rect key="frame" x="-2" y="21" width="79" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX UPLOAD" id="lGh-MT-TgI">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eHT-tr-Kwx">
|
||||
<rect key="frame" x="10" y="0.0" width="55" height="19"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="1iA-Ri-zYY">
|
||||
<font key="font" metaFont="systemMedium" size="16"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="nWj-33-m8Q" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="CAY-Pw-B8n"/>
|
||||
<constraint firstAttribute="bottom" secondItem="doH-ww-BDw" secondAttribute="bottom" constant="4" id="Dq4-M6-1Wf"/>
|
||||
<constraint firstItem="g4d-4N-NkC" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="bls-fM-H4b"/>
|
||||
<constraint firstAttribute="bottom" secondItem="nWj-33-m8Q" secondAttribute="bottom" constant="4" id="f6j-eI-wiH"/>
|
||||
<constraint firstAttribute="bottom" secondItem="g4d-4N-NkC" secondAttribute="bottom" constant="4" id="faS-Mo-Qa2"/>
|
||||
<constraint firstItem="doH-ww-BDw" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="gL3-5S-OKo"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="TnH-dX-qaQ" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="6" id="1mo-iG-Z0D"/>
|
||||
<constraint firstAttribute="trailing" secondItem="TnH-dX-qaQ" secondAttribute="trailing" constant="30" id="3dD-wf-5pS"/>
|
||||
<constraint firstItem="TnH-dX-qaQ" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="30" id="S8i-CD-j3h"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TnH-dX-qaQ" secondAttribute="bottom" constant="6" id="eve-qD-gUH"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="labelMaxPost" destination="Vyu-AO-8SH" id="5Cm-QO-hJQ"/>
|
||||
<outlet property="labelMaxUpload" destination="eHT-tr-Kwx" id="5pK-FD-c4h"/>
|
||||
<outlet property="labelMemLimit" destination="Emt-m3-Dt6" id="6nD-Su-XZ6"/>
|
||||
<outlet property="titleMaxPost" destination="7um-XA-djV" id="5MN-Xb-XwL"/>
|
||||
<outlet property="titleMaxUpload" destination="Oef-6n-9QI" id="Q61-JI-RJq"/>
|
||||
<outlet property="titleMemLimit" destination="At1-ch-qv2" id="SQT-B9-sWS"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="139" y="168"/>
|
||||
</customView>
|
||||
</objects>
|
||||
</document>
|
312
phpmon/Domain/Menu/StatusMenu+Items.swift
Normal file
@ -0,0 +1,312 @@
|
||||
//
|
||||
// StatusMenu+Items.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 18/08/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
// MARK: - PHP Version
|
||||
|
||||
extension StatusMenu {
|
||||
|
||||
func addPhpVersionMenuItems() {
|
||||
if PhpEnv.phpInstall.version.error {
|
||||
let brokenMenuItems = ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"]
|
||||
return addItems(brokenMenuItems.map { NSMenuItem(title: $0.localized) })
|
||||
}
|
||||
|
||||
addItem(HeaderView.asMenuItem(
|
||||
text: "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)",
|
||||
minimumWidth: 280 // this ensures the menu is at least wide enough not to cause clipping
|
||||
))
|
||||
}
|
||||
|
||||
func addPhpActionMenuItems() {
|
||||
if PhpEnv.shared.isBusy {
|
||||
addItem(NSMenuItem(title: "mi_busy".localized))
|
||||
return
|
||||
}
|
||||
|
||||
if PhpEnv.shared.availablePhpVersions.isEmpty { return }
|
||||
|
||||
addSwitchToPhpMenuItems()
|
||||
self.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
func addServicesManagerMenuItem() {
|
||||
if PhpEnv.shared.isBusy {
|
||||
return
|
||||
}
|
||||
|
||||
addItems([
|
||||
ServicesView.asMenuItem(),
|
||||
NSMenuItem.separator()
|
||||
])
|
||||
}
|
||||
|
||||
func addSwitchToPhpMenuItems() {
|
||||
var shortcutKey = 1
|
||||
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
|
||||
|
||||
// Get the short and long version
|
||||
let shortVersion = PhpEnv.shared.availablePhpVersions[index]
|
||||
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber
|
||||
|
||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||
let versionString = long ? longVersion.toString() : shortVersion
|
||||
|
||||
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
||||
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
|
||||
let menuItem = PhpMenuItem(
|
||||
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
|
||||
action: (shortVersion == PhpEnv.phpInstall.version.short)
|
||||
? nil
|
||||
: action, keyEquivalent: "\(shortcutKey)"
|
||||
)
|
||||
|
||||
menuItem.version = shortVersion
|
||||
shortcutKey += 1
|
||||
|
||||
addItem(menuItem)
|
||||
}
|
||||
}
|
||||
|
||||
func addCoreMenuItems() {
|
||||
addItems([
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_preferences".localized,
|
||||
action: #selector(MainMenu.openPrefs), keyEquivalent: ","),
|
||||
NSMenuItem(title: "mi_check_for_updates".localized,
|
||||
action: #selector(MainMenu.checkForUpdates)),
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_about".localized,
|
||||
action: #selector(MainMenu.openAbout)),
|
||||
NSMenuItem(title: "mi_quit".localized,
|
||||
action: #selector(MainMenu.terminateApp), keyEquivalent: "q")
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Valet
|
||||
|
||||
func addValetMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_valet".localized),
|
||||
NSMenuItem(title: "mi_valet_config".localized,
|
||||
action: #selector(MainMenu.openValetConfigFolder),
|
||||
keyEquivalent: "v"),
|
||||
NSMenuItem(title: "mi_domain_list".localized,
|
||||
action: #selector(MainMenu.openDomainList),
|
||||
keyEquivalent: "l"),
|
||||
NSMenuItem.separator()
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - PHP Configuration
|
||||
|
||||
func addConfigurationMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_configuration".localized),
|
||||
NSMenuItem(title: "mi_php_config".localized,
|
||||
action: #selector(MainMenu.openActiveConfigFolder),
|
||||
keyEquivalent: "c"),
|
||||
NSMenuItem(title: "mi_phpmon_config".localized,
|
||||
action: #selector(MainMenu.openPhpMonitorConfigurationFile),
|
||||
keyEquivalent: "y"),
|
||||
NSMenuItem(title: "mi_phpinfo".localized,
|
||||
action: #selector(MainMenu.openPhpInfo),
|
||||
keyEquivalent: "i")
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Composer
|
||||
|
||||
func addComposerMenuItems() {
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_composer".localized),
|
||||
NSMenuItem(
|
||||
title: "mi_global_composer".localized,
|
||||
action: #selector(MainMenu.openGlobalComposerFolder),
|
||||
keyEquivalent: "g"
|
||||
),
|
||||
NSMenuItem(
|
||||
title: "mi_update_global_composer".localized,
|
||||
action: PhpEnv.shared.isBusy
|
||||
? nil
|
||||
: #selector(MainMenu.updateGlobalComposerDependencies),
|
||||
keyEquivalent: "g",
|
||||
keyModifier: [.shift]
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Stats
|
||||
|
||||
func addStatsMenuItem() {
|
||||
guard let stats = PhpEnv.phpInstall.limits else { return }
|
||||
|
||||
addItem(StatsView.asMenuItem(
|
||||
memory: stats.memory_limit,
|
||||
post: stats.post_max_size,
|
||||
upload: stats.upload_max_filesize)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
func addExtensionsMenuItems() {
|
||||
addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
|
||||
|
||||
if PhpEnv.phpInstall.extensions.isEmpty {
|
||||
addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
|
||||
}
|
||||
|
||||
var shortcutKey = 1
|
||||
for phpExtension in PhpEnv.phpInstall.extensions {
|
||||
addExtensionItem(phpExtension, shortcutKey)
|
||||
shortcutKey += 1
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Presets
|
||||
|
||||
func addPresetsMenuItem() {
|
||||
guard let presets = Preferences.custom.presets else {
|
||||
addEmptyPresetHelp()
|
||||
return
|
||||
}
|
||||
|
||||
if presets.isEmpty {
|
||||
addEmptyPresetHelp()
|
||||
return
|
||||
}
|
||||
|
||||
addLoadedPresets()
|
||||
}
|
||||
|
||||
private func addEmptyPresetHelp() {
|
||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||
NSMenuItem(title: "mi_no_presets".localized),
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_set_up_presets".localized,
|
||||
action: #selector(MainMenu.showPresetHelp))
|
||||
], target: MainMenu.shared))
|
||||
}
|
||||
|
||||
private func addLoadedPresets() {
|
||||
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
|
||||
NSMenuItem.separator(),
|
||||
HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)
|
||||
] + PresetMenuItem.getAll() + [
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_revert_to_prev_config".localized,
|
||||
action: PresetHelper.rollbackPreset != nil ? #selector(MainMenu.rollbackPreset) : nil),
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_profiles_loaded".localized(Preferences.custom.presets!.count))
|
||||
], target: MainMenu.shared))
|
||||
}
|
||||
|
||||
// MARK: - Xdebug
|
||||
|
||||
func addXdebugMenuItem() {
|
||||
if !Xdebug.enabled {
|
||||
addItem(NSMenuItem.separator())
|
||||
return
|
||||
}
|
||||
|
||||
addItems([
|
||||
NSMenuItem(title: "mi_xdebug_mode".localized, submenu: [
|
||||
HeaderView.asMenuItem(text: "mi_xdebug_available_modes".localized)
|
||||
] + Xdebug.asMenuItems() + [
|
||||
HeaderView.asMenuItem(text: "mi_xdebug_actions".localized),
|
||||
NSMenuItem(title: "mi_xdebug_disable_all".localized,
|
||||
action: #selector(MainMenu.disableAllXdebugModes))
|
||||
], target: MainMenu.shared),
|
||||
NSMenuItem.separator()
|
||||
], target: MainMenu.shared)
|
||||
}
|
||||
|
||||
// MARK: - PHP Doctor
|
||||
|
||||
func addPhpDoctorMenuItem() {
|
||||
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
|
||||
!WarningManager.shared.hasWarnings() {
|
||||
return
|
||||
}
|
||||
|
||||
addItems([
|
||||
HeaderView.asMenuItem(text: "mi_php_doctor".localized),
|
||||
NSMenuItem(title: "mi_recommendations_count".localized(WarningManager.shared.warnings.count)),
|
||||
NSMenuItem(title: "mi_view_recommendations".localized, action: #selector(MainMenu.openWarnings)),
|
||||
NSMenuItem.separator()
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - First Aid & Services
|
||||
|
||||
func addFirstAidAndServicesMenuItems() {
|
||||
let services = NSMenuItem(title: "mi_other".localized)
|
||||
|
||||
let servicesMenu = NSMenu()
|
||||
servicesMenu.addItems([
|
||||
// FIRST AID
|
||||
HeaderView.asMenuItem(text: "mi_first_aid".localized),
|
||||
NSMenuItem(title: "mi_view_onboarding".localized, action: #selector(MainMenu.showWelcomeTour)),
|
||||
NSMenuItem(title: "mi_fa_php_doctor".localized, action: #selector(MainMenu.openWarnings)),
|
||||
NSMenuItem.separator(),
|
||||
NSMenuItem(title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
|
||||
action: #selector(MainMenu.fixMyValet),
|
||||
toolTip: "mi_fix_my_valet_tooltip".localized),
|
||||
NSMenuItem(title: "mi_fix_brew_permissions".localized(), action: #selector(MainMenu.fixHomebrewPermissions),
|
||||
toolTip: "mi_fix_brew_permissions_tooltip".localized),
|
||||
NSMenuItem.separator(),
|
||||
|
||||
// SERVICES
|
||||
HeaderView.asMenuItem(text: "mi_services".localized),
|
||||
NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq),
|
||||
keyEquivalent: "d"),
|
||||
NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm),
|
||||
keyEquivalent: "p"),
|
||||
NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx),
|
||||
keyEquivalent: "n"),
|
||||
NSMenuItem(title: "mi_restart_valet_services".localized, action: #selector(MainMenu.restartValetServices),
|
||||
keyEquivalent: "s"),
|
||||
NSMenuItem(title: "mi_stop_valet_services".localized, action: #selector(MainMenu.stopValetServices),
|
||||
keyEquivalent: "s",
|
||||
keyModifier: [.command, .shift]),
|
||||
NSMenuItem.separator(),
|
||||
|
||||
// MANUAL ACTIONS
|
||||
HeaderView.asMenuItem(text: "mi_manual_actions".localized),
|
||||
NSMenuItem(title: "mi_php_refresh".localized,
|
||||
action: #selector(MainMenu.reloadPhpMonitorMenuInForeground),
|
||||
keyEquivalent: "r")
|
||||
], target: MainMenu.shared)
|
||||
|
||||
setSubmenu(servicesMenu, for: services)
|
||||
addItem(services)
|
||||
}
|
||||
|
||||
// MARK: - Other helper methods to generate menu items
|
||||
|
||||
func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
||||
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
||||
|
||||
let menuItem = ExtensionMenuItem(
|
||||
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
|
||||
action: #selector(MainMenu.toggleExtension),
|
||||
keyEquivalent: keyEquivalent
|
||||
)
|
||||
|
||||
if menuItem.keyEquivalent != "" {
|
||||
menuItem.keyEquivalentModifierMask = [.option]
|
||||
}
|
||||
|
||||
menuItem.state = phpExtension.enabled ? .on : .off
|
||||
menuItem.phpExtension = phpExtension
|
||||
|
||||
addItem(menuItem)
|
||||
}
|
||||
}
|
@ -8,292 +8,63 @@
|
||||
import Cocoa
|
||||
|
||||
class StatusMenu: NSMenu {
|
||||
func addMenuItems() {
|
||||
addPhpVersionMenuItems()
|
||||
addItem(NSMenuItem.separator())
|
||||
|
||||
func addPhpVersionMenuItems() {
|
||||
if PhpEnv.phpInstall.version.error {
|
||||
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
|
||||
addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
|
||||
}
|
||||
return
|
||||
if Preferences.isEnabled(.displayGlobalVersionSwitcher) {
|
||||
addPhpActionMenuItems()
|
||||
addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
let phpVersionText = "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)"
|
||||
addItem(HeaderView.asMenuItem(text: phpVersionText))
|
||||
}
|
||||
|
||||
func addPhpActionMenuItems() {
|
||||
if PhpEnv.shared.isBusy {
|
||||
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
|
||||
return
|
||||
if Preferences.isEnabled(.displayServicesManager) {
|
||||
addServicesManagerMenuItem()
|
||||
addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
if PhpEnv.shared.availablePhpVersions.isEmpty {
|
||||
return
|
||||
if Preferences.isEnabled(.displayValetIntegration) {
|
||||
addValetMenuItems()
|
||||
addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
self.addSwitchToPhpMenuItems()
|
||||
self.addItem(NSMenuItem.separator())
|
||||
if Preferences.isEnabled(.displayPhpConfigFinder) {
|
||||
addConfigurationMenuItems()
|
||||
addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
self.addItem(ServicesView.asMenuItem())
|
||||
self.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
func addValetMenuItems() {
|
||||
self.addItem(HeaderView.asMenuItem(text: "mi_valet".localized))
|
||||
self.addItem(NSMenuItem(
|
||||
title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
|
||||
self.addItem(NSMenuItem(
|
||||
title: "mi_domain_list".localized, action: #selector(MainMenu.openDomainList), keyEquivalent: "l"))
|
||||
self.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
func addRemainingMenuItems() {
|
||||
self.addConfigurationMenuItems()
|
||||
|
||||
self.addItem(NSMenuItem.separator())
|
||||
|
||||
self.addComposerMenuItems()
|
||||
if Preferences.isEnabled(.displayComposerToolkit) {
|
||||
addComposerMenuItems()
|
||||
addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
if PhpEnv.shared.isBusy {
|
||||
return
|
||||
}
|
||||
|
||||
self.addItem(NSMenuItem.separator())
|
||||
|
||||
self.addStatsMenuItem()
|
||||
|
||||
self.addItem(NSMenuItem.separator())
|
||||
|
||||
self.addExtensionsMenuItems()
|
||||
|
||||
self.addItem(NSMenuItem.separator())
|
||||
|
||||
// self.addXdebugMenuItem()
|
||||
|
||||
self.addFirstAidAndServicesMenuItems()
|
||||
}
|
||||
|
||||
func addCoreMenuItems() {
|
||||
self.addItem(NSMenuItem.separator())
|
||||
self.addItem(NSMenuItem(title: "mi_preferences".localized,
|
||||
action: #selector(MainMenu.openPrefs), keyEquivalent: ","))
|
||||
self.addItem(NSMenuItem(title: "mi_check_for_updates".localized,
|
||||
action: #selector(MainMenu.checkForUpdates), keyEquivalent: ""))
|
||||
self.addItem(NSMenuItem.separator())
|
||||
self.addItem(NSMenuItem(title: "mi_about".localized,
|
||||
action: #selector(MainMenu.openAbout), keyEquivalent: ""))
|
||||
self.addItem(NSMenuItem(title: "mi_quit".localized,
|
||||
action: #selector(MainMenu.terminateApp), keyEquivalent: "q"))
|
||||
}
|
||||
|
||||
// MARK: Remaining Menu Items
|
||||
|
||||
func addConfigurationMenuItems() {
|
||||
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
|
||||
self.addItem(
|
||||
NSMenuItem(title: "mi_php_config".localized,
|
||||
action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c")
|
||||
)
|
||||
self.addItem(
|
||||
NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i")
|
||||
)
|
||||
}
|
||||
|
||||
func addComposerMenuItems() {
|
||||
self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized))
|
||||
self.addItem(
|
||||
NSMenuItem(title: "mi_global_composer".localized,
|
||||
action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g")
|
||||
)
|
||||
|
||||
let composerMenuItem = NSMenuItem(
|
||||
title: "mi_update_global_composer".localized,
|
||||
action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies),
|
||||
keyEquivalent: "g"
|
||||
)
|
||||
composerMenuItem.keyEquivalentModifierMask = .shift
|
||||
|
||||
self.addItem(composerMenuItem)
|
||||
}
|
||||
|
||||
func addStatsMenuItem() {
|
||||
guard let stats = PhpEnv.phpInstall.limits else { return }
|
||||
|
||||
self.addItem(StatsView.asMenuItem(
|
||||
memory: stats.memory_limit,
|
||||
post: stats.post_max_size,
|
||||
upload: stats.upload_max_filesize)
|
||||
)
|
||||
}
|
||||
|
||||
func addExtensionsMenuItems() {
|
||||
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
|
||||
|
||||
if PhpEnv.phpInstall.extensions.isEmpty {
|
||||
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
|
||||
if Preferences.isEnabled(.displayLimitsWidget) {
|
||||
addStatsMenuItem()
|
||||
addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
var shortcutKey = 1
|
||||
for phpExtension in PhpEnv.phpInstall.extensions {
|
||||
self.addExtensionItem(phpExtension, shortcutKey)
|
||||
shortcutKey += 1
|
||||
}
|
||||
}
|
||||
if Preferences.isEnabled(.displayExtensions) {
|
||||
addExtensionsMenuItems()
|
||||
NSMenuItem.separator()
|
||||
|
||||
func addXdebugMenuItem() {
|
||||
if !Xdebug.enabled {
|
||||
return
|
||||
addXdebugMenuItem()
|
||||
}
|
||||
|
||||
let xdebugSwitch = NSMenuItem(
|
||||
title: "mi_xdebug_mode".localized,
|
||||
action: nil,
|
||||
keyEquivalent: ""
|
||||
)
|
||||
let xdebugModesMenu = NSMenu()
|
||||
let xdebugMode = Xdebug.mode
|
||||
addPhpDoctorMenuItem()
|
||||
|
||||
for mode in Xdebug.modes {
|
||||
let item = XdebugMenuItem(
|
||||
title: mode,
|
||||
action: #selector(MainMenu.toggleXdebugMode(sender:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
item.state = xdebugMode == mode ? .on : .off
|
||||
item.mode = mode
|
||||
xdebugModesMenu.addItem(item)
|
||||
if Preferences.isEnabled(.displayPresets) {
|
||||
addPresetsMenuItem()
|
||||
}
|
||||
|
||||
for item in xdebugModesMenu.items {
|
||||
item.target = MainMenu.shared
|
||||
if Preferences.isEnabled(.displayMisc) {
|
||||
addFirstAidAndServicesMenuItems()
|
||||
}
|
||||
|
||||
self.setSubmenu(xdebugModesMenu, for: xdebugSwitch)
|
||||
self.addItem(xdebugSwitch)
|
||||
}
|
||||
addItem(NSMenuItem.separator())
|
||||
|
||||
func addFirstAidAndServicesMenuItems() {
|
||||
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
|
||||
let servicesMenu = NSMenu()
|
||||
|
||||
let fixMyValetMenuItem = NSMenuItem(
|
||||
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
|
||||
action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
|
||||
)
|
||||
fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized
|
||||
servicesMenu.addItem(fixMyValetMenuItem)
|
||||
|
||||
let fixHomebrewMenuItem = NSMenuItem(
|
||||
title: "mi_fix_brew_permissions".localized(),
|
||||
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: ""
|
||||
)
|
||||
fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized
|
||||
servicesMenu.addItem(fixHomebrewMenuItem)
|
||||
|
||||
servicesMenu.addItem(NSMenuItem.separator())
|
||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
|
||||
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_dnsmasq".localized,
|
||||
action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_php_fpm".localized,
|
||||
action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_nginx".localized,
|
||||
action: #selector(MainMenu.restartNginx), keyEquivalent: "n")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_all_services".localized,
|
||||
action: #selector(MainMenu.restartAllServices), keyEquivalent: "s")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_stop_all_services".localized,
|
||||
action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
|
||||
withKeyModifier: [.command, .shift]
|
||||
)
|
||||
|
||||
servicesMenu.addItem(NSMenuItem.separator())
|
||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized))
|
||||
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_php_refresh".localized,
|
||||
action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r")
|
||||
)
|
||||
|
||||
for item in servicesMenu.items {
|
||||
item.target = MainMenu.shared
|
||||
}
|
||||
|
||||
self.setSubmenu(servicesMenu, for: services)
|
||||
self.addItem(services)
|
||||
}
|
||||
|
||||
// MARK: Private Helpers
|
||||
|
||||
private func addSwitchToPhpMenuItems() {
|
||||
var shortcutKey = 1
|
||||
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
|
||||
|
||||
// Get the short and long version
|
||||
let shortVersion = PhpEnv.shared.availablePhpVersions[index]
|
||||
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber
|
||||
|
||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||
let versionString = long ? longVersion.toString() : shortVersion
|
||||
|
||||
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
||||
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
|
||||
let menuItem = PhpMenuItem(
|
||||
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
|
||||
action: (shortVersion == PhpEnv.phpInstall.version.short)
|
||||
? nil
|
||||
: action, keyEquivalent: "\(shortcutKey)"
|
||||
)
|
||||
|
||||
menuItem.version = shortVersion
|
||||
shortcutKey += 1
|
||||
|
||||
self.addItem(menuItem)
|
||||
}
|
||||
}
|
||||
|
||||
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
||||
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
||||
|
||||
let menuItem = ExtensionMenuItem(
|
||||
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
|
||||
action: #selector(MainMenu.toggleExtension),
|
||||
keyEquivalent: keyEquivalent
|
||||
)
|
||||
|
||||
if menuItem.keyEquivalent != "" {
|
||||
menuItem.keyEquivalentModifierMask = [.option]
|
||||
}
|
||||
|
||||
menuItem.state = phpExtension.enabled ? .on : .off
|
||||
menuItem.phpExtension = phpExtension
|
||||
|
||||
self.addItem(menuItem)
|
||||
addCoreMenuItems()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSMenuItem subclasses
|
||||
|
||||
class PhpMenuItem: NSMenuItem {
|
||||
var version: String = ""
|
||||
}
|
||||
|
||||
class XdebugMenuItem: NSMenuItem {
|
||||
var mode: String = ""
|
||||
}
|
||||
|
||||
class ExtensionMenuItem: NSMenuItem {
|
||||
var phpExtension: PhpExtension?
|
||||
}
|
||||
|
||||
class EditorMenuItem: NSMenuItem {
|
||||
var editor: Application?
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ class BetterAlert {
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
windowController.window?.makeKeyAndOrderFront(nil)
|
||||
windowController.window?.setCenterPosition(offsetY: 70)
|
||||
return NSApplication.shared.runModal(for: windowController.window!)
|
||||
}
|
||||
|
||||
@ -102,14 +103,14 @@ class BetterAlert {
|
||||
/**
|
||||
Shows the modal and does not return anything.
|
||||
*/
|
||||
public func show() {
|
||||
@MainActor public func show() {
|
||||
_ = self.runModal()
|
||||
}
|
||||
|
||||
/**
|
||||
Shows the modal for a particular error.
|
||||
*/
|
||||
public static func show(for error: Error & AlertableError) {
|
||||
@MainActor public static func show(for error: Error & AlertableError) {
|
||||
let key = error.getErrorMessageKey()
|
||||
return BetterAlert().withInformation(
|
||||
title: "\(key).title".localized,
|
||||
|
@ -47,7 +47,7 @@ class BetterAlertVC: NSViewController {
|
||||
}
|
||||
|
||||
deinit {
|
||||
Log.perf("A BetterAlert has been deinitialized.")
|
||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||
}
|
||||
|
||||
// MARK: Outlet Actions
|
||||
|
45
phpmon/Domain/Onboarding/OnboardingWindowController.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// OnboardingWindowController.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/06/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
|
||||
class OnboardingWindowController: PMWindowController {
|
||||
|
||||
// MARK: - Window Identifier
|
||||
|
||||
override var windowName: String {
|
||||
return "Onboarding"
|
||||
}
|
||||
|
||||
public static func create(delegate: NSWindowDelegate?) {
|
||||
let windowController = Self()
|
||||
windowController.window = NSWindow()
|
||||
|
||||
guard let window = windowController.window else { return }
|
||||
window.title = ""
|
||||
window.styleMask = [.titled, .closable, .miniaturizable]
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.delegate = delegate ?? windowController
|
||||
window.contentView = NSHostingView(rootView: OnboardingView())
|
||||
window.setContentSize(NSSize(width: 600, height: 600))
|
||||
|
||||
App.shared.onboardingWindowController = windowController
|
||||
}
|
||||
|
||||
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||
if App.shared.onboardingWindowController == nil {
|
||||
Self.create(delegate: delegate)
|
||||
}
|
||||
|
||||
App.shared.onboardingWindowController?.showWindow(self)
|
||||
App.shared.onboardingWindowController?.window?.setCenterPosition(offsetY: 70)
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
@ -9,9 +9,87 @@
|
||||
import Foundation
|
||||
|
||||
struct CustomPrefs: Decodable {
|
||||
let scanApps: [String]
|
||||
let scanApps: [String]?
|
||||
let presets: [Preset]?
|
||||
let services: [String]?
|
||||
let environmentVariables: [String: String]?
|
||||
|
||||
public func hasPresets() -> Bool {
|
||||
return self.presets != nil && !self.presets!.isEmpty
|
||||
}
|
||||
|
||||
public func hasServices() -> Bool {
|
||||
return self.services != nil && !self.services!.isEmpty
|
||||
}
|
||||
|
||||
public func hasEnvironmentVariables() -> Bool {
|
||||
return self.environmentVariables != nil && !self.environmentVariables!.keys.isEmpty
|
||||
}
|
||||
|
||||
public func getEnvironmentVariables() -> String {
|
||||
return self.environmentVariables!.map { (key, value) in
|
||||
return "export \(key)=\(value)"
|
||||
}.joined(separator: "&&")
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case scanApps = "scan_apps"
|
||||
case presets = "presets"
|
||||
case services = "services"
|
||||
case environmentVariables = "export"
|
||||
}
|
||||
}
|
||||
|
||||
extension Preferences {
|
||||
func loadCustomPreferences() {
|
||||
// Ensure the configuration directory is created if missing
|
||||
Shell.run("mkdir -p ~/.config/phpmon")
|
||||
|
||||
// Move the legacy file
|
||||
moveOutdatedConfigurationFile()
|
||||
|
||||
// Attempt to load the file if it exists
|
||||
let url = URL(fileURLWithPath: "\(Paths.homePath)/.config/phpmon/config.json")
|
||||
if Filesystem.fileExists(url.path) {
|
||||
|
||||
Log.info("A custom ~/.config/phpmon/config.json file was found. Attempting to parse...")
|
||||
loadCustomPreferencesFile(url)
|
||||
} else {
|
||||
Log.info("There was no /.config/phpmon/config.json file to be loaded.")
|
||||
}
|
||||
}
|
||||
|
||||
func moveOutdatedConfigurationFile() {
|
||||
if Filesystem.fileExists("~/.phpmon.conf.json") && !Filesystem.fileExists("~/.config/phpmon/config.json") {
|
||||
Log.info("An outdated configuration file was found. Moving it...")
|
||||
Shell.run("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
|
||||
Log.info("The configuration file was copied successfully!")
|
||||
}
|
||||
}
|
||||
|
||||
func loadCustomPreferencesFile(_ url: URL) {
|
||||
do {
|
||||
customPreferences = try JSONDecoder().decode(
|
||||
CustomPrefs.self,
|
||||
from: try! String(contentsOf: url, encoding: .utf8).data(using: .utf8)!
|
||||
)
|
||||
|
||||
Log.info("The ~/.config/phpmon/config.json file was successfully parsed.")
|
||||
|
||||
if customPreferences.hasPresets() {
|
||||
Log.info("There are \(customPreferences.presets!.count) custom presets.")
|
||||
}
|
||||
|
||||
if customPreferences.hasServices() {
|
||||
Log.info("There are custom services: \(customPreferences.services!)")
|
||||
}
|
||||
|
||||
if customPreferences.hasEnvironmentVariables() {
|
||||
Log.info("Configuring the additional exports...")
|
||||
Shell.user.exports = customPreferences.getEnvironmentVariables()
|
||||
}
|
||||
} catch {
|
||||
Log.warn("The ~/.config/phpmon/config.json file seems to be missing or malformed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
phpmon/Domain/Preferences/Keys.swift
Normal file
@ -0,0 +1,14 @@
|
||||
//
|
||||
// Keys.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/07/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Keys {
|
||||
static let Escape = 53
|
||||
static let Space = 49
|
||||
}
|
110
phpmon/Domain/Preferences/PreferenceName.swift
Normal file
@ -0,0 +1,110 @@
|
||||
//
|
||||
// PreferenceName.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 07/09/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
/**
|
||||
These are the keys used for every preference in the app.
|
||||
*/
|
||||
enum PreferenceName: String {
|
||||
// FIRST-TIME LAUNCH
|
||||
case wasLaunchedBefore = "launched_before"
|
||||
|
||||
// GENERAL
|
||||
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
|
||||
case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch"
|
||||
case allowProtocolForIntegrations = "allow_protocol_for_integrations"
|
||||
case globalHotkey = "global_hotkey"
|
||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
|
||||
|
||||
// APPEARANCE
|
||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||
case iconTypeToDisplay = "icon_type_to_display"
|
||||
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
|
||||
|
||||
// WARNINGS
|
||||
case warnAboutNonStandardTLD = "warn_about_non_standard_tld"
|
||||
|
||||
// NOTIFICATIONS
|
||||
case notifyAboutVersionChange = "notify_about_version_change"
|
||||
case notifyAboutPhpFpmRestart = "notify_about_php_fpm_restart"
|
||||
case notifyAboutServices = "notify_about_services_restart"
|
||||
case notifyAboutPresets = "notify_about_presets"
|
||||
case notifyAboutSecureToggle = "notify_about_secure_toggle"
|
||||
case notifyAboutGlobalComposerStatus = "notify_about_composer_status"
|
||||
|
||||
// MENU CUSTOMIZATION
|
||||
case displayGlobalVersionSwitcher = "display_global_version_switcher"
|
||||
case displayServicesManager = "display_services_manager"
|
||||
case displayValetIntegration = "display_valet_integration"
|
||||
case displayPhpConfigFinder = "display_php_config_finder"
|
||||
case displayComposerToolkit = "display_composer_toolkit"
|
||||
case displayLimitsWidget = "display_limits_widget"
|
||||
case displayExtensions = "display_extensions"
|
||||
case displayPresets = "display_presets"
|
||||
case displayMisc = "display_misc"
|
||||
|
||||
/**
|
||||
What type of data each preference contains.
|
||||
*/
|
||||
static var mapping: [PreferenceType: [PreferenceName]] = [
|
||||
.boolean: [
|
||||
// Preferences
|
||||
.shouldDisplayDynamicIcon,
|
||||
.fullPhpVersionDynamicIcon,
|
||||
.autoServiceRestartAfterExtensionToggle,
|
||||
.autoComposerGlobalUpdateAfterSwitch,
|
||||
.allowProtocolForIntegrations,
|
||||
.automaticBackgroundUpdateCheck,
|
||||
.showPhpDoctorSuggestions,
|
||||
|
||||
// Notifications
|
||||
.warnAboutNonStandardTLD,
|
||||
.notifyAboutVersionChange,
|
||||
.notifyAboutPhpFpmRestart,
|
||||
.notifyAboutServices,
|
||||
.notifyAboutPresets,
|
||||
.notifyAboutSecureToggle,
|
||||
.notifyAboutGlobalComposerStatus,
|
||||
|
||||
// UI Preferences
|
||||
.displayGlobalVersionSwitcher,
|
||||
.displayServicesManager,
|
||||
.displayValetIntegration,
|
||||
.displayPhpConfigFinder,
|
||||
.displayComposerToolkit,
|
||||
.displayLimitsWidget,
|
||||
.displayExtensions,
|
||||
.displayPresets,
|
||||
.displayMisc
|
||||
],
|
||||
.string: [
|
||||
.globalHotkey,
|
||||
.iconTypeToDisplay
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
enum PreferenceType {
|
||||
case boolean, string
|
||||
}
|
||||
|
||||
/**
|
||||
These are retired preferences that, if present, should be migrated.
|
||||
*/
|
||||
enum RetiredPreferenceName: String {
|
||||
case shouldDisplayPhpHintInIcon = "add_php_to_icon"
|
||||
}
|
||||
|
||||
/**
|
||||
These are internal stats. They NEVER get shared.
|
||||
*/
|
||||
enum InternalStats: String {
|
||||
case launchCount = "times_launched"
|
||||
case switchCount = "times_switched_versions"
|
||||
case didSeeSponsorEncouragement = "did_see_sponsor_encouragement"
|
||||
}
|
@ -8,37 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
These are the keys used for every preference in the app.
|
||||
*/
|
||||
enum PreferenceName: String {
|
||||
case wasLaunchedBefore = "launched_before"
|
||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||
case iconTypeToDisplay = "icon_type_to_display"
|
||||
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
|
||||
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
|
||||
case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch"
|
||||
case allowProtocolForIntegrations = "allow_protocol_for_integrations"
|
||||
case globalHotkey = "global_hotkey"
|
||||
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
|
||||
}
|
||||
|
||||
/**
|
||||
These are retired preferences that, if present, should be migrated.
|
||||
*/
|
||||
enum RetiredPreferenceName: String {
|
||||
case shouldDisplayPhpHintInIcon = "add_php_to_icon"
|
||||
}
|
||||
|
||||
/**
|
||||
These are internal stats. They NEVER get shared.
|
||||
*/
|
||||
enum InternalStats: String {
|
||||
case launchCount = "times_launched"
|
||||
case switchCount = "times_switched_versions"
|
||||
case didSeeSponsorEncouragement = "did_see_sponsor_encouragement"
|
||||
}
|
||||
|
||||
class Preferences {
|
||||
|
||||
// MARK: - Singleton
|
||||
@ -52,7 +21,12 @@ class Preferences {
|
||||
public init() {
|
||||
Preferences.handleFirstTimeLaunch()
|
||||
cachedPreferences = Self.cache()
|
||||
customPreferences = CustomPrefs(scanApps: [])
|
||||
customPreferences = CustomPrefs(
|
||||
scanApps: [],
|
||||
presets: [],
|
||||
services: [],
|
||||
environmentVariables: [:]
|
||||
)
|
||||
loadCustomPreferences()
|
||||
}
|
||||
|
||||
@ -70,14 +44,38 @@ class Preferences {
|
||||
*/
|
||||
static func handleFirstTimeLaunch() {
|
||||
UserDefaults.standard.register(defaults: [
|
||||
/// Preferences
|
||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||
PreferenceName.iconTypeToDisplay.rawValue: MenuBarIcon.iconPhp.rawValue,
|
||||
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
|
||||
/// Preferences: General
|
||||
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true,
|
||||
PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue: false,
|
||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
|
||||
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
|
||||
|
||||
/// Preferences: Appearance
|
||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||
PreferenceName.iconTypeToDisplay.rawValue: MenuBarIcon.iconPhp.rawValue,
|
||||
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
|
||||
|
||||
/// Preferences: Notifications
|
||||
PreferenceName.warnAboutNonStandardTLD.rawValue: true,
|
||||
PreferenceName.notifyAboutVersionChange.rawValue: true,
|
||||
PreferenceName.notifyAboutPhpFpmRestart.rawValue: true,
|
||||
PreferenceName.notifyAboutServices.rawValue: true,
|
||||
PreferenceName.notifyAboutPresets.rawValue: true,
|
||||
PreferenceName.notifyAboutSecureToggle.rawValue: true,
|
||||
PreferenceName.notifyAboutGlobalComposerStatus.rawValue: true,
|
||||
|
||||
/// Preferences: UI Preferences
|
||||
PreferenceName.displayGlobalVersionSwitcher.rawValue: true,
|
||||
PreferenceName.displayServicesManager.rawValue: true,
|
||||
PreferenceName.displayValetIntegration.rawValue: true,
|
||||
PreferenceName.displayPhpConfigFinder.rawValue: true,
|
||||
PreferenceName.displayComposerToolkit.rawValue: true,
|
||||
PreferenceName.displayLimitsWidget.rawValue: true,
|
||||
PreferenceName.displayExtensions.rawValue: true,
|
||||
PreferenceName.displayPresets.rawValue: true,
|
||||
PreferenceName.displayMisc.rawValue: true,
|
||||
|
||||
/// Stats
|
||||
InternalStats.switchCount.rawValue: 0,
|
||||
InternalStats.launchCount.rawValue: 0,
|
||||
@ -134,28 +132,18 @@ class Preferences {
|
||||
|
||||
// MARK: - Internal Functionality
|
||||
|
||||
private static func cache() -> [PreferenceName: Any] {
|
||||
return [
|
||||
// Part 1: Always Booleans
|
||||
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
|
||||
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
|
||||
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
|
||||
.autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any,
|
||||
.allowProtocolForIntegrations: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.allowProtocolForIntegrations.rawValue) as Any,
|
||||
.automaticBackgroundUpdateCheck: UserDefaults.standard.bool(
|
||||
forKey: PreferenceName.automaticBackgroundUpdateCheck.rawValue) as Any,
|
||||
|
||||
// Part 2: Always Strings
|
||||
.globalHotkey: UserDefaults.standard.string(
|
||||
forKey: PreferenceName.globalHotkey.rawValue) as Any,
|
||||
.iconTypeToDisplay: UserDefaults.standard.string(
|
||||
forKey: PreferenceName.iconTypeToDisplay.rawValue) as Any
|
||||
]
|
||||
private static func cache() -> [PreferenceName: Any?] {
|
||||
return Dictionary(uniqueKeysWithValues: PreferenceName.mapping
|
||||
.flatMap { (key: PreferenceType, value: [PreferenceName]) in
|
||||
value.map { preference -> (PreferenceName, Any?) in
|
||||
return (preference, { () -> Any? in
|
||||
switch key {
|
||||
case .boolean: return UserDefaults.standard.bool(forKey: preference.rawValue)
|
||||
case .string: return UserDefaults.standard.string(forKey: preference.rawValue)
|
||||
}
|
||||
}())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static func update(_ preference: PreferenceName, value: Any?) {
|
||||
@ -169,29 +157,4 @@ class Preferences {
|
||||
// Update the preferences cache in memory!
|
||||
Preferences.shared.cachedPreferences = Preferences.cache()
|
||||
}
|
||||
|
||||
// MARK: - Custom Preferences
|
||||
|
||||
private func loadCustomPreferences() {
|
||||
let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json")
|
||||
if Filesystem.fileExists(url.path) {
|
||||
Log.info("A custom .phpmon.conf.json file was found. Attempting to parse...")
|
||||
loadCustomPreferencesFile(url)
|
||||
} else {
|
||||
Log.info("There was no .phpmon.conf.json file to be loaded.")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadCustomPreferencesFile(_ url: URL) {
|
||||
do {
|
||||
customPreferences = try JSONDecoder().decode(
|
||||
CustomPrefs.self,
|
||||
from: try! String(contentsOf: url, encoding: .utf8).data(using: .utf8)!
|
||||
)
|
||||
Log.info("The .phpmon.conf.json file was successfully parsed.")
|
||||
} catch {
|
||||
Log.warn("The .phpmon.conf.json file seems to be missing or malformed.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
//
|
||||
// PreferencesWindowController+Hotkey.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/07/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension PreferencesWindowController {
|
||||
|
||||
// MARK: - Key Interaction
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
super.keyDown(with: event)
|
||||
|
||||
guard let tabVC = self.contentViewController as? NSTabViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let vc = tabVC.tabViewItems[tabVC.selectedTabViewItemIndex].viewController as? GenericPreferenceVC else {
|
||||
return
|
||||
}
|
||||
|
||||
if vc.listeningForHotkeyView == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
|
||||
Log.info("A blacklisted key was pressed, canceling listen!")
|
||||
vc.listeningForHotkeyView!.unregister(nil)
|
||||
} else {
|
||||
vc.listeningForHotkeyView!.updateShortcut(event)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
107
phpmon/Domain/Preferences/PreferencesWindowController.swift
Normal file
@ -0,0 +1,107 @@
|
||||
//
|
||||
// PreferencesWindowController.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 02/04/2021.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class PreferencesWindowController: PMWindowController {
|
||||
|
||||
// MARK: - Window Identifier
|
||||
|
||||
override var windowName: String {
|
||||
return "Preferences"
|
||||
}
|
||||
|
||||
public static func create(delegate: NSWindowDelegate?) {
|
||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
let windowController = storyboard.instantiateController(
|
||||
withIdentifier: "preferencesWindow"
|
||||
) as! PreferencesWindowController
|
||||
|
||||
guard let window = windowController.window else { return }
|
||||
|
||||
window.title = "prefs.title".localized
|
||||
window.subtitle = "prefs.subtitle".localized
|
||||
window.delegate = delegate ?? windowController
|
||||
window.styleMask = [.titled, .closable, .miniaturizable]
|
||||
|
||||
App.shared.preferencesWindowController = windowController
|
||||
}
|
||||
|
||||
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||
var justCreated = false
|
||||
|
||||
if App.shared.preferencesWindowController == nil {
|
||||
Self.create(delegate: delegate)
|
||||
|
||||
guard let preferencesWC = App.shared.preferencesWindowController else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let tabVC = preferencesWC.contentViewController as? NSTabViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
for vc in preferencesWC.tabVCs {
|
||||
tabVC.addChild(vc.viewController)
|
||||
let item = tabVC.tabViewItem(for: vc.viewController)
|
||||
item?.image = NSImage(systemSymbolName: vc.icon, accessibilityDescription: "\(vc.label) Icon")
|
||||
item?.label = vc.label
|
||||
}
|
||||
|
||||
tabVC.preferredContentSize = NSSize(
|
||||
width: tabVC.view.frame.size.width,
|
||||
height: tabVC.view.frame.size.height
|
||||
)
|
||||
|
||||
justCreated = true
|
||||
}
|
||||
|
||||
App.shared.preferencesWindowController?.showWindow(self)
|
||||
|
||||
if justCreated {
|
||||
App.shared.preferencesWindowController?.positionWindowInTopLeftCorner()
|
||||
}
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
// MARK: - Tabs
|
||||
|
||||
struct PrefTabView {
|
||||
let viewController: GenericPreferenceVC
|
||||
let label: String
|
||||
let icon: String
|
||||
}
|
||||
|
||||
public lazy var tabVCs: [PrefTabView] = {
|
||||
return [
|
||||
PrefTabView(
|
||||
viewController: GeneralPreferencesVC.fromStoryboard(),
|
||||
label: "General",
|
||||
icon: "gearshape"
|
||||
),
|
||||
PrefTabView(
|
||||
viewController: AppearancePreferencesVC.fromStoryboard(),
|
||||
label: "Appearance",
|
||||
icon: "paintbrush"
|
||||
),
|
||||
PrefTabView(
|
||||
viewController: MenuStructurePreferencesVC.fromStoryboard(),
|
||||
label: "Visibility",
|
||||
icon: "eye"
|
||||
),
|
||||
PrefTabView(
|
||||
viewController: NotificationPreferencesVC.fromStoryboard(),
|
||||
label: "Notifications",
|
||||
icon: "bell.badge"
|
||||
)
|
||||
]
|
||||
}()
|
||||
|
||||
}
|
@ -9,56 +9,26 @@
|
||||
import Cocoa
|
||||
import Carbon
|
||||
|
||||
class PrefsVC: NSViewController {
|
||||
class GenericPreferenceVC: NSViewController {
|
||||
|
||||
// MARK: - Window Identifier
|
||||
// MARK: - Content
|
||||
|
||||
@IBOutlet weak var stackView: NSStackView!
|
||||
|
||||
// MARK: - Display
|
||||
|
||||
public static func create(delegate: NSWindowDelegate?) {
|
||||
let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
||||
|
||||
let windowController = storyboard.instantiateController(
|
||||
withIdentifier: "preferencesWindow"
|
||||
) as! PrefsWC
|
||||
|
||||
windowController.window!.title = "prefs.title".localized
|
||||
windowController.window!.subtitle = "prefs.subtitle".localized
|
||||
windowController.window!.delegate = delegate
|
||||
windowController.window!.styleMask = [.titled, .closable, .miniaturizable]
|
||||
windowController.window!.delegate = windowController
|
||||
windowController.positionWindowInTopLeftCorner()
|
||||
|
||||
App.shared.preferencesWindowController = windowController
|
||||
}
|
||||
|
||||
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||
if App.shared.preferencesWindowController == nil {
|
||||
Self.create(delegate: delegate)
|
||||
}
|
||||
|
||||
App.shared.preferencesWindowController!.showWindow(self)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
var views: [NSView] = []
|
||||
|
||||
override func viewDidLoad() {
|
||||
[
|
||||
getDynamicIconPreferenceView(),
|
||||
getIconOptionsPreferenceView(),
|
||||
getIconDensityPreferenceView(),
|
||||
getAutoRestartPreferenceView(),
|
||||
getAutomaticComposerUpdatePreferenceView(),
|
||||
getShortcutPreferenceView(),
|
||||
getIntegrationsPreferenceView(),
|
||||
getAutomaticUpdateCheckPreferenceView()
|
||||
].forEach({ self.stackView.addArrangedSubview($0) })
|
||||
super.viewDidLoad()
|
||||
self.views.forEach({ self.stackView.addArrangedSubview($0) })
|
||||
}
|
||||
|
||||
private func getDynamicIconPreferenceView() -> NSView {
|
||||
// MARK: - Deinitialization
|
||||
|
||||
deinit {
|
||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||
}
|
||||
|
||||
func getDynamicIconPV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.dynamic_icon".localized,
|
||||
descriptionText: "prefs.dynamic_icon_desc".localized,
|
||||
@ -70,7 +40,7 @@ class PrefsVC: NSViewController {
|
||||
)
|
||||
}
|
||||
|
||||
private func getIconOptionsPreferenceView() -> NSView {
|
||||
func getIconOptionsPV() -> NSView {
|
||||
return SelectPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.icon_options_desc".localized,
|
||||
@ -83,7 +53,7 @@ class PrefsVC: NSViewController {
|
||||
)
|
||||
}
|
||||
|
||||
private func getIconDensityPreferenceView() -> NSView {
|
||||
func getIconDensityPV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.info_density".localized,
|
||||
descriptionText: "prefs.display_full_php_version_desc".localized,
|
||||
@ -96,7 +66,7 @@ class PrefsVC: NSViewController {
|
||||
)
|
||||
}
|
||||
|
||||
private func getAutoRestartPreferenceView() -> NSView {
|
||||
func getAutoRestartPV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.services".localized,
|
||||
descriptionText: "prefs.auto_restart_services_desc".localized,
|
||||
@ -106,7 +76,7 @@ class PrefsVC: NSViewController {
|
||||
)
|
||||
}
|
||||
|
||||
private func getAutomaticComposerUpdatePreferenceView() -> NSView {
|
||||
func getAutomaticComposerUpdatePV() -> NSView {
|
||||
CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.switcher".localized,
|
||||
descriptionText: "prefs.auto_composer_update_desc".localized,
|
||||
@ -116,15 +86,15 @@ class PrefsVC: NSViewController {
|
||||
)
|
||||
}
|
||||
|
||||
private func getShortcutPreferenceView() -> NSView {
|
||||
return HotkeyPreferenceView.make(
|
||||
sectionText: "prefs.global_shortcut".localized,
|
||||
descriptionText: "prefs.shortcut_desc".localized,
|
||||
self
|
||||
func getShortcutPV() -> NSView {
|
||||
return HotkeyPreferenceView.make(
|
||||
sectionText: "prefs.global_shortcut".localized,
|
||||
descriptionText: "prefs.shortcut_desc".localized,
|
||||
self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func getIntegrationsPreferenceView() -> NSView {
|
||||
func getIntegrationsPV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.integrations".localized,
|
||||
descriptionText: "prefs.open_protocol_desc".localized,
|
||||
@ -134,7 +104,7 @@ class PrefsVC: NSViewController {
|
||||
)
|
||||
}
|
||||
|
||||
private func getAutomaticUpdateCheckPreferenceView() -> NSView {
|
||||
func getAutomaticUpdateCheckPV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.updates".localized,
|
||||
descriptionText: "prefs.automatic_update_check_desc".localized,
|
||||
@ -144,6 +114,107 @@ class PrefsVC: NSViewController {
|
||||
)
|
||||
}
|
||||
|
||||
func getShowPhpDoctorSuggestionsPV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.php_doctor".localized,
|
||||
descriptionText: "prefs.php_doctor_suggestions_desc".localized,
|
||||
checkboxText: "prefs.php_doctor_suggestions_title".localized,
|
||||
preference: .showPhpDoctorSuggestions,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func getNotifyAboutVersionChangePV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.notifications".localized,
|
||||
descriptionText: "prefs.notify_about_version_change_desc".localized,
|
||||
checkboxText: "prefs.notify_about_version_change".localized,
|
||||
preference: .notifyAboutVersionChange,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
func getNotifyAboutPhpFpmChangePV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.notify_about_php_fpm_change_desc".localized,
|
||||
checkboxText: "prefs.notify_about_php_fpm_change".localized,
|
||||
preference: .notifyAboutPhpFpmRestart,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
func getNotifyAboutServicesPV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.notify_about_services_desc".localized,
|
||||
checkboxText: "prefs.notify_about_services".localized,
|
||||
preference: .notifyAboutServices,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
func getNotifyAboutPresetsPV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.notify_about_presets_desc".localized,
|
||||
checkboxText: "prefs.notify_about_presets".localized,
|
||||
preference: .notifyAboutPresets,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
func getNotifyAboutSecureTogglePV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.notify_about_secure_status_desc".localized,
|
||||
checkboxText: "prefs.notify_about_secure_status".localized,
|
||||
preference: .notifyAboutSecureToggle,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
func getNotifyAboutGlobalComposerStatusPV() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "",
|
||||
descriptionText: "prefs.notify_about_composer_success_desc".localized,
|
||||
checkboxText: "prefs.notify_about_composer_success".localized,
|
||||
preference: .notifyAboutGlobalComposerStatus,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
func getWarnAboutNonStandardTLD() -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: "prefs.warnings".localized,
|
||||
descriptionText: "prefs.warn_about_non_standard_tld_desc".localized,
|
||||
checkboxText: "prefs.warn_about_non_standard_tld".localized,
|
||||
preference: .warnAboutNonStandardTLD,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
|
||||
func getDisplayMenuSectionPV(
|
||||
_ localizationKey: String,
|
||||
_ preference: PreferenceName,
|
||||
_ first: Bool = false
|
||||
) -> NSView {
|
||||
return CheckboxPreferenceView.make(
|
||||
sectionText: first ? "prefs.menu_contents".localized : "",
|
||||
descriptionText: "\(localizationKey)_desc".localized,
|
||||
checkboxText: localizationKey.localized,
|
||||
preference: preference,
|
||||
action: {
|
||||
MainMenu.shared.refreshIcon()
|
||||
MainMenu.shared.rebuild()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Listening for hotkey delegate
|
||||
|
||||
var listeningForHotkeyView: HotkeyPreferenceView?
|
||||
@ -153,10 +224,85 @@ class PrefsVC: NSViewController {
|
||||
listeningForHotkeyView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Deinitialization
|
||||
class GeneralPreferencesVC: GenericPreferenceVC {
|
||||
|
||||
deinit {
|
||||
Log.perf("PrefsVC deallocated")
|
||||
// MARK: - Lifecycle
|
||||
|
||||
public static func fromStoryboard() -> GenericPreferenceVC {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||
|
||||
vc.views = [
|
||||
vc.getShowPhpDoctorSuggestionsPV(),
|
||||
vc.getAutoRestartPV(),
|
||||
vc.getAutomaticComposerUpdatePV(),
|
||||
vc.getShortcutPV(),
|
||||
vc.getIntegrationsPV(),
|
||||
vc.getAutomaticUpdateCheckPV()
|
||||
]
|
||||
|
||||
return vc
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationPreferencesVC: GenericPreferenceVC {
|
||||
|
||||
public static func fromStoryboard() -> GenericPreferenceVC {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||
|
||||
vc.views = [
|
||||
vc.getNotifyAboutVersionChangePV(),
|
||||
vc.getNotifyAboutPresetsPV(),
|
||||
vc.getNotifyAboutSecureTogglePV(),
|
||||
vc.getNotifyAboutGlobalComposerStatusPV(),
|
||||
vc.getNotifyAboutServicesPV(),
|
||||
vc.getNotifyAboutPhpFpmChangePV(),
|
||||
vc.getWarnAboutNonStandardTLD()
|
||||
]
|
||||
|
||||
return vc
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MenuStructurePreferencesVC: GenericPreferenceVC {
|
||||
|
||||
public static func fromStoryboard() -> GenericPreferenceVC {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||
|
||||
vc.views = [
|
||||
vc.getDisplayMenuSectionPV("prefs.display_global_version_switcher", .displayGlobalVersionSwitcher, true),
|
||||
vc.getDisplayMenuSectionPV("prefs.display_services_manager", .displayServicesManager),
|
||||
vc.getDisplayMenuSectionPV("prefs.display_valet_integration", .displayValetIntegration),
|
||||
vc.getDisplayMenuSectionPV("prefs.display_php_config_finder", .displayPhpConfigFinder),
|
||||
vc.getDisplayMenuSectionPV("prefs.display_composer_toolkit", .displayComposerToolkit),
|
||||
vc.getDisplayMenuSectionPV("prefs.display_limits_widget", .displayLimitsWidget),
|
||||
vc.getDisplayMenuSectionPV("prefs.display_extensions", .displayExtensions),
|
||||
vc.getDisplayMenuSectionPV("prefs.display_presets", .displayPresets),
|
||||
vc.getDisplayMenuSectionPV("prefs.display_misc", .displayMisc)
|
||||
|
||||
]
|
||||
|
||||
return vc
|
||||
}
|
||||
}
|
||||
|
||||
class AppearancePreferencesVC: GenericPreferenceVC {
|
||||
|
||||
public static func fromStoryboard() -> GenericPreferenceVC {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||
|
||||
vc.views = [
|
||||
vc.getDynamicIconPV(),
|
||||
vc.getIconOptionsPV(),
|
||||
vc.getIconDensityPV()
|
||||
]
|
||||
|
||||
return vc
|
||||
}
|
||||
}
|
||||
|