Compare commits
215 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
c645bb7610 | |||
e6574966da | |||
1e9cfff05e | |||
bd34c2b255 | |||
c040ac3200 | |||
6c6888c9cb | |||
78cb6922b3 | |||
c16377c688 | |||
540ea5c310 | |||
4ba2b25f18 | |||
81b75dcaa8 | |||
884784d024 | |||
e81ff2870d | |||
7c631099b2 | |||
f7a98b88a7 | |||
3fc21fff2a | |||
0306c2b726 | |||
9d822df54e | |||
f413b84a45 | |||
b82811e6bf | |||
af922664ab | |||
8b73e69495 | |||
29a9e14741 | |||
997fb27596 | |||
c171df0a93 | |||
1c15a4e07f | |||
5067c7b87f | |||
f679231ade | |||
f725e09f55 | |||
99881bf4cd | |||
2987464da8 | |||
4d04275c57 | |||
790f63e8c9 | |||
86b49812c3 | |||
ef9e0fd916 | |||
af8807f799 | |||
ba93ed93e4 | |||
932a0fe176 | |||
eb80214785 | |||
80a4e361a4 | |||
2af88b2bee | |||
5048ccab8c | |||
66d13c92d5 | |||
836b076da9 | |||
1a75838a3b | |||
a18b7962a7 | |||
84548634ec | |||
419ebe61f7 | |||
c45817b127 | |||
2c0c0c5a11 | |||
1b8d6311ba | |||
f0f7a3f7d6 | |||
8304d774c3 | |||
faeea4e866 | |||
6470daf7d3 | |||
94139a3669 | |||
8057019898 | |||
9b59fc5dae | |||
75f4377de8 | |||
d3657716c4 | |||
a13990b96f | |||
4c7aa7fead |
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
|
## About destination branches
|
||||||
|
|
||||||
Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Merge requests that target `main` will be closed without mercy.**
|
Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Pull requests that target `main` will be closed without mercy.**
|
||||||
|
|
||||||
Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released.
|
Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released.
|
||||||
|
|
||||||
|
15
.swiftlint.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
disabled_rules:
|
||||||
|
- todo
|
||||||
|
- identifier_name
|
||||||
|
- force_try
|
||||||
|
- force_cast
|
||||||
|
|
||||||
|
opt_in_rules:
|
||||||
|
- empty_count
|
||||||
|
|
||||||
|
included:
|
||||||
|
- phpmon
|
||||||
|
- phpmon-tests
|
||||||
|
|
||||||
|
excluded:
|
||||||
|
- phpmon/Vendor
|
20
DEVELOPER.md
@@ -1,5 +1,19 @@
|
|||||||
# DEVELOPER README
|
# DEVELOPER README
|
||||||
|
|
||||||
|
## ✅ Linting
|
||||||
|
|
||||||
|
This project uses the [SwiftLint](https://github.com/realm/SwiftLint) linter. You must install it and can run it like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
swiftlint
|
||||||
|
```
|
||||||
|
|
||||||
|
It also automatically runs when you try to build the project. You'll get a warning if `swiftlint` is not installed, though. You can attempt to automatically fix issues:
|
||||||
|
|
||||||
|
```
|
||||||
|
swiftlint --fix
|
||||||
|
```
|
||||||
|
|
||||||
## 🔧 Build instructions
|
## 🔧 Build instructions
|
||||||
|
|
||||||
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
|
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
|
||||||
@@ -27,6 +41,12 @@ If you'd like to create a production build, choose "Any Mac" as the target and s
|
|||||||
10. Update Cask with new version + hash
|
10. Update Cask with new version + hash
|
||||||
11. Check new version can be installed via Cask
|
11. Check new version can be installed via Cask
|
||||||
|
|
||||||
|
## 🍱 Marketing Mode
|
||||||
|
|
||||||
|
You can enable marketing mode by setting the `PHPMON_MARKETING_MODE` environment variable. It preloads a list of (fake) domains in the domain window list for screenshot & marketing purposes.
|
||||||
|
|
||||||
|
launchctl setenv PHPMON_MARKETING_MODE true
|
||||||
|
|
||||||
## 🐛 Symbolication of crashes
|
## 🐛 Symbolication of crashes
|
||||||
|
|
||||||
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.
|
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1320"
|
LastUpgradeVersion = "1400"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
@@ -61,10 +61,21 @@
|
|||||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "--v"
|
||||||
|
isEnabled = "NO">
|
||||||
|
</CommandLineArgument>
|
||||||
|
</CommandLineArguments>
|
||||||
<EnvironmentVariables>
|
<EnvironmentVariables>
|
||||||
<EnvironmentVariable
|
<EnvironmentVariable
|
||||||
key = "PHPMON_MARKETING_MODE"
|
key = "EXTREME_DOCTOR_MODE"
|
||||||
value = "YES"
|
value = ""
|
||||||
|
isEnabled = "NO">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
|
||||||
|
value = ""
|
||||||
isEnabled = "NO">
|
isEnabled = "NO">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables>
|
||||||
|
186
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>
|
<p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p>
|
||||||
|
|
||||||
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
|
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
|
||||||
|
|
||||||
<img src="./docs/screenshot.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
|
<img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
|
||||||
|
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
|
||||||
|
|
||||||
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
|
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
|
||||||
|
|
||||||
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
|
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
|
||||||
|
|
||||||
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
|
<img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
|
||||||
|
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
|
||||||
|
|
||||||
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
|
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
|
||||||
|
|
||||||
@@ -21,10 +24,10 @@ You can also add new domains as links, isolate sites, manage various services, a
|
|||||||
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
|
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
|
||||||
|
|
||||||
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
|
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
|
||||||
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
|
* macOS 11 Big Sur or later
|
||||||
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
|
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
|
||||||
* Homebrew `php` formula is installed
|
* Homebrew `php` formula is installed
|
||||||
* Laravel Valet 2.16 or newer (supports Valet 3)
|
* Laravel Valet 3 recommended (but compatible with Valet 2)
|
||||||
|
|
||||||
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
|
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
|
||||||
|
|
||||||
@@ -103,14 +106,14 @@ Super convenient!
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary>
|
<summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary>
|
||||||
|
|
||||||
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it:
|
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it.
|
||||||
|
|
||||||
Install [Homebrew](https://brew.sh) first.
|
**I have also created [a video tutorial](https://www.youtube.com/watch?v=fO3hVhkvm3w) which may be easier to follow. If you just want the terminal commands, keep reading.**
|
||||||
|
|
||||||
Install PHP, composer, add to path:
|
Install [Homebrew](https://brew.sh) first. Follow the instructions there first!
|
||||||
|
|
||||||
|
Then, you'll need to set up your PATH.
|
||||||
|
|
||||||
brew install php
|
|
||||||
brew install composer
|
|
||||||
nano .zshrc
|
nano .zshrc
|
||||||
|
|
||||||
Make sure the following line is not in the comments:
|
Make sure the following line is not in the comments:
|
||||||
@@ -123,30 +126,70 @@ If you're on an Apple Silicon-based Mac, you'll need to add:
|
|||||||
# on an M1 Mac
|
# on an M1 Mac
|
||||||
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
|
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
|
||||||
|
|
||||||
and add the following to your .zshrc, but add this BEFORE the homebrew PATH additions:
|
and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions:
|
||||||
|
|
||||||
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
||||||
|
|
||||||
If you're adding composer and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
|
If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
|
||||||
|
|
||||||
export PATH=$HOME/bin:/usr/local/bin:$PATH
|
export PATH=$HOME/bin:/usr/local/bin:$PATH
|
||||||
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
||||||
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
|
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
|
||||||
|
|
||||||
|
If you are *not* on Apple Silicon, you should remove the third line.
|
||||||
|
|
||||||
|
Install the `php` and `composer` formulae:
|
||||||
|
|
||||||
|
brew install php composer
|
||||||
|
|
||||||
Make sure PHP is linked correctly:
|
Make sure PHP is linked correctly:
|
||||||
|
|
||||||
which php
|
which php
|
||||||
|
|
||||||
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php`)
|
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon)
|
||||||
|
|
||||||
composer global require laravel/valet
|
composer global require laravel/valet
|
||||||
|
|
||||||
|
For optimal results, you should lock your PHP platform for global dependencies to the oldest version of PHP you intend to run. If that version is PHP 7.0, your `~/.composer/composer.json` file could look like this (please adjust the version accordingly!):
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"laravel/valet": "^3.0",
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"platform": {
|
||||||
|
"php": "7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `composer global update` again. This ensures that when you switch to a different global PHP version, [Valet won't break](https://github.com/nicoverbruggen/phpmon/issues/178). If it does, PHP Monitor will let you know what you can do about this.
|
||||||
|
|
||||||
|
Then, install Valet:
|
||||||
|
|
||||||
valet install
|
valet install
|
||||||
|
|
||||||
This should install `dnsmasq` and set up Valet. Great, almost there!
|
This should install `dnsmasq` and set up Valet. Great, almost there!
|
||||||
|
|
||||||
valet trust
|
valet trust
|
||||||
|
|
||||||
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
|
You can now install PHP Monitor, if you haven't already:
|
||||||
|
|
||||||
|
brew tap nicoverbruggen/homebrew-cask
|
||||||
|
brew install --cask phpmon
|
||||||
|
|
||||||
|
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>How frequently does PHP Monitor check for updates?</strong></summary>
|
||||||
|
|
||||||
|
PHP Monitor will check if an update is available every time you start the app.
|
||||||
|
|
||||||
|
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". You can always check for updates manually.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -278,9 +321,102 @@ PHP Monitor is a universal app and supports both architectures, so [find out her
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>Why is the app doing network requests?</strong></summary>
|
<summary><strong>Why is the app doing network requests?</strong></summary>
|
||||||
|
|
||||||
It's Homebrew. I can't prevent `brew` from doing things via the network when I invoke it.
|
The app will automatically check for updates, which is the most likely culprit.
|
||||||
|
|
||||||
PHP Monitor itself doesn't do any network requests. Feel free to check the source code or intercept the traffic, if you don't believe me.
|
This happens at launch (unless disabled), and the app directly checks the Caskfile hosted on GitHub. This data is not, and will not be used for analytics (and, as far as I can tell, cannot).
|
||||||
|
|
||||||
|
I also can't prevent `brew` from doing things via the network when PHP Monitor uses the binary.
|
||||||
|
|
||||||
|
The app includes an Internet Access Policy file, so if you're using something like Little Snitch there should be a description why these calls occur.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>How do I various presets to show up?</strong></summary>
|
||||||
|
|
||||||
|
You must set these presets up in a JSON file, located in `~/.config/phpmon/config.json`.
|
||||||
|
|
||||||
|
You must have set up at least one valid preset for this presets to work in PHP Monitor.
|
||||||
|
|
||||||
|
Here's an example of a working preset:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
{
|
||||||
|
"scan_apps": [],
|
||||||
|
"services": [],
|
||||||
|
"presets": [
|
||||||
|
{
|
||||||
|
"name": "Legacy Project",
|
||||||
|
"php": "8.0",
|
||||||
|
"extensions": {
|
||||||
|
"xdebug": false
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"memory_limit": "128M",
|
||||||
|
"upload_max_filesize": "128M",
|
||||||
|
"post_max_size": "128M"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"export": {}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
You can omit the `php` key in the preset if you do not wish for the preset to switch to a given PHP version.
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
> You must restart PHP Monitor for these changes to be detected.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>How do I ensure additional Homebrew services are shown in the app?</strong></summary>
|
||||||
|
|
||||||
|
You must set these services up in a JSON file, located in `~/.config/phpmon/config.json`.
|
||||||
|
|
||||||
|
You can specify custom services in the configuration file for Homebrew services that run as your own user (not root).
|
||||||
|
|
||||||
|
> **Info**
|
||||||
|
> If your service must run as root, it cannot currently be added to PHP Monitor.
|
||||||
|
|
||||||
|
You can find out which services are available by running `brew services list`.
|
||||||
|
|
||||||
|
Here's an example where we add the `mailhog` and `mysql` services to PHP Monitor:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
{
|
||||||
|
"scan_apps": [],
|
||||||
|
"services": ["mailhog", "mysql"],
|
||||||
|
"presets": [],
|
||||||
|
"export": {}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
> You must restart PHP Monitor for these changes to be detected.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>How do I set custom environment variables?</strong></summary>
|
||||||
|
|
||||||
|
You must configure these custom environment variables up in a JSON file, located in `~/.config/phpmon/config.json`.
|
||||||
|
|
||||||
|
PHP Monitor uses a default Shell environment, with no custom environment variables. You need to set custom environment variables manually. These are then used for e.g. Composer.
|
||||||
|
|
||||||
|
Here's an example of a working `COMPOSER_HOME` environment variable which is respected:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
{
|
||||||
|
"scan_apps": [],
|
||||||
|
"services": [],
|
||||||
|
"presets": [],
|
||||||
|
"export": {
|
||||||
|
"COMPOSER_HOME": "/absolute/path/to/composer/folder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
> You must restart PHP Monitor for these changes to be detected.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -295,7 +431,7 @@ All of these apps should just be detected correctly, no matter their location on
|
|||||||
|
|
||||||
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
|
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
|
||||||
|
|
||||||
You can add your own apps by creating and editing a `~/.phpmon.conf.json` file, with the following entry:
|
You can add your own apps by creating and editing a `~/.config/phpmon/config.json` file, and make sure the `scan_apps` key is set:
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
{
|
{
|
||||||
@@ -304,6 +440,9 @@ You can add your own apps by creating and editing a `~/.phpmon.conf.json` file,
|
|||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary.
|
You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary.
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
> You must restart PHP Monitor for these changes to be detected.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -393,14 +532,14 @@ Donations really help with the Apple Developer Program cost, and keep me motivat
|
|||||||
|
|
||||||
## 😎 Acknowledgements
|
## 😎 Acknowledgements
|
||||||
|
|
||||||
While I did make this application during my own free time, PHP Monitor started out from various learning experiments during work hours at my employer, DIVE. I'd also like to shout out the following folks:
|
Special thanks go out to:
|
||||||
|
|
||||||
* My colleagues at [DIVE](https://dive.be)
|
* Everyone supporting me via [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen)
|
||||||
|
* Everyone who has donated via [my sponsor page](https://nicoverbruggen.be/sponsor)
|
||||||
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors)
|
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors)
|
||||||
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so
|
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so
|
||||||
* My [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen) and those who have donated
|
* Everyone who has left feedback and reported bugs
|
||||||
* Everyone who has left feedback and reported bugs (appreciate it!)
|
* Everyone in the Laravel community who shared the app, especially on Twitter
|
||||||
* Everyone in the Laravel community who shared the app (thanks!)
|
|
||||||
|
|
||||||
Thank you very much for your contributions, kind words and support.
|
Thank you very much for your contributions, kind words and support.
|
||||||
|
|
||||||
@@ -434,7 +573,8 @@ If an extension or other process writes to a single file a bunch of times in a s
|
|||||||
1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name.
|
1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name.
|
||||||
1. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all.
|
1. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all.
|
||||||
|
|
||||||
*Note*: If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
|
> **Note**
|
||||||
|
> If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
|
||||||
|
|
||||||
### Want to know more?
|
### Want to know more?
|
||||||
|
|
||||||
|
10
SECURITY.md
@@ -6,9 +6,9 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
|
|||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 (*) | 3.0 (2.16.2 minimum) |
|
| 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0)* | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
||||||
|
|
||||||
_(*) Support for PHP 5.6 is only included if you are using Valet 2.x, since support for PHP 5.6 was dropped in Valet 3.0._
|
_(*) macOS Ventura (13.0) is not officially supported until it officially releases._
|
||||||
|
|
||||||
## Legacy versions
|
## Legacy versions
|
||||||
|
|
||||||
@@ -16,9 +16,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 |
|
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||||
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
|
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
|
||||||
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
|
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
|
||||||
| 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 |
|
| 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 |
|
||||||
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 | 2.13 |
|
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 | 2.13 |
|
||||||
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |
|
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |
|
||||||
|
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 |
@@ -3,7 +3,7 @@
|
|||||||
// phpmon-tests
|
// phpmon-tests
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 13/02/2021.
|
// Created by Nico Verbruggen on 13/02/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
@@ -3,18 +3,18 @@
|
|||||||
// phpmon-tests
|
// phpmon-tests
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 14/02/2021.
|
// Created by Nico Verbruggen on 14/02/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class BrewJsonParserTest: XCTestCase {
|
class HomebrewPackageTest: XCTestCase {
|
||||||
|
|
||||||
// - MARK: SYNTHETIC TESTS
|
// - MARK: SYNTHETIC TESTS
|
||||||
|
|
||||||
static var jsonBrewFile: URL {
|
static var jsonBrewFile: URL {
|
||||||
return Bundle(for: Self.self)
|
return Bundle(for: Self.self)
|
||||||
.url(forResource: "brew", withExtension: "json")!
|
.url(forResource: "brew-formula", withExtension: "json")!
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanLoadExtensionJson() throws {
|
func testCanLoadExtensionJson() throws {
|
||||||
@@ -64,9 +64,9 @@ class BrewJsonParserTest: XCTestCase {
|
|||||||
return ["php", "nginx", "dnsmasq"].contains(service.name)
|
return ["php", "nginx", "dnsmasq"].contains(service.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
XCTAssertTrue(services.contains(where: {$0.name == "php"} ))
|
XCTAssertTrue(services.contains(where: {$0.name == "php"}))
|
||||||
XCTAssertTrue(services.contains(where: {$0.name == "nginx"} ))
|
XCTAssertTrue(services.contains(where: {$0.name == "nginx"}))
|
||||||
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} ))
|
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"}))
|
||||||
XCTAssertEqual(services.count, 3)
|
XCTAssertEqual(services.count, 3)
|
||||||
}
|
}
|
||||||
|
|
@@ -1,32 +0,0 @@
|
|||||||
//
|
|
||||||
// NginxConfigParserTest.swift
|
|
||||||
// phpmon-tests
|
|
||||||
//
|
|
||||||
// Created by Nico Verbruggen on 29/11/2021.
|
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
class NginxConfigParserTest: XCTestCase {
|
|
||||||
|
|
||||||
static var regularUrl: URL {
|
|
||||||
return Bundle(for: Self.self).url(forResource: "nicoverbruggen", withExtension: "test")!
|
|
||||||
}
|
|
||||||
|
|
||||||
static var isolatedUrl: URL {
|
|
||||||
return Bundle(for: Self.self).url(forResource: "nicoverbruggen_isolated", withExtension: "test")!
|
|
||||||
}
|
|
||||||
|
|
||||||
func testCanDetermineIsolation() throws {
|
|
||||||
XCTAssertNil(
|
|
||||||
NginxConfigParser(filePath: NginxConfigParserTest.regularUrl.path).isolatedVersion
|
|
||||||
)
|
|
||||||
|
|
||||||
XCTAssertEqual(
|
|
||||||
"8.1",
|
|
||||||
NginxConfigParser(filePath: NginxConfigParserTest.isolatedUrl.path).isolatedVersion
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
81
phpmon-tests/Parsers/NginxConfigurationTest.swift
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// NginxConfigurationTest.swift
|
||||||
|
// phpmon-tests
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 29/11/2021.
|
||||||
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class NginxConfigurationTest: XCTestCase {
|
||||||
|
|
||||||
|
// MARK: - Test Files
|
||||||
|
|
||||||
|
static var regularUrl: URL {
|
||||||
|
return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")!
|
||||||
|
}
|
||||||
|
|
||||||
|
static var isolatedUrl: URL {
|
||||||
|
return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")!
|
||||||
|
}
|
||||||
|
|
||||||
|
static var proxyUrl: URL {
|
||||||
|
return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")!
|
||||||
|
}
|
||||||
|
|
||||||
|
static var secureProxyUrl: URL {
|
||||||
|
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")!
|
||||||
|
}
|
||||||
|
|
||||||
|
static var customTldProxyUrl: URL {
|
||||||
|
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy-custom-tld", withExtension: "test")!
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tests
|
||||||
|
|
||||||
|
func testCanDetermineSiteNameAndTld() throws {
|
||||||
|
XCTAssertEqual(
|
||||||
|
"nginx-site",
|
||||||
|
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
"test",
|
||||||
|
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanDetermineIsolation() throws {
|
||||||
|
XCTAssertNil(
|
||||||
|
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
"8.1",
|
||||||
|
NginxConfigurationFile.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanDetermineProxy() throws {
|
||||||
|
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.proxyUrl.path)!
|
||||||
|
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
|
||||||
|
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
||||||
|
|
||||||
|
let normal = NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)!
|
||||||
|
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
|
||||||
|
XCTAssertEqual(nil, normal.proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanDetermineSecuredProxy() throws {
|
||||||
|
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.secureProxyUrl.path)!
|
||||||
|
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
|
||||||
|
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanDetermineProxyWithCustomTld() throws {
|
||||||
|
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)!
|
||||||
|
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
|
||||||
|
XCTAssertEqual("http://localhost:8080", proxied.proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -3,25 +3,25 @@
|
|||||||
// phpmon-tests
|
// phpmon-tests
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 13/02/2021.
|
// Created by Nico Verbruggen on 13/02/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class ExtensionParserTest: XCTestCase {
|
class PhpExtensionTest: XCTestCase {
|
||||||
|
|
||||||
static var phpIniFileUrl: URL {
|
static var phpIniFileUrl: URL {
|
||||||
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
|
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanLoadExtension() throws {
|
func testCanLoadExtension() throws {
|
||||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
|
||||||
|
|
||||||
XCTAssertGreaterThan(extensions.count, 0)
|
XCTAssertGreaterThan(extensions.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testExtensionNameIsCorrect() throws {
|
func testExtensionNameIsCorrect() throws {
|
||||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
|
||||||
|
|
||||||
let extensionNames = extensions.map { (ext) -> String in
|
let extensionNames = extensions.map { (ext) -> String in
|
||||||
return ext.name
|
return ext.name
|
||||||
@@ -40,7 +40,7 @@ class ExtensionParserTest: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testExtensionStatusIsCorrect() throws {
|
func testExtensionStatusIsCorrect() throws {
|
||||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
|
||||||
|
|
||||||
// xdebug should be enabled
|
// xdebug should be enabled
|
||||||
XCTAssertEqual(extensions[0].enabled, true)
|
XCTAssertEqual(extensions[0].enabled, true)
|
||||||
@@ -51,7 +51,7 @@ class ExtensionParserTest: XCTestCase {
|
|||||||
|
|
||||||
func testToggleWorksAsExpected() throws {
|
func testToggleWorksAsExpected() throws {
|
||||||
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
||||||
let extensions = PhpExtension.load(from: destination)
|
let extensions = PhpExtension.from(filePath: destination.path)
|
||||||
XCTAssertEqual(extensions.count, 6)
|
XCTAssertEqual(extensions.count, 6)
|
||||||
|
|
||||||
// Try to disable xdebug (should be detected first)!
|
// Try to disable xdebug (should be detected first)!
|
||||||
@@ -66,7 +66,7 @@ class ExtensionParserTest: XCTestCase {
|
|||||||
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
|
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
|
||||||
|
|
||||||
// Make sure if we load the data again, it's disabled
|
// Make sure if we load the data again, it's disabled
|
||||||
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
|
XCTAssertEqual(PhpExtension.from(filePath: destination.path).first!.enabled, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -3,12 +3,12 @@
|
|||||||
// phpmon-tests
|
// phpmon-tests
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 29/11/2021.
|
// Created by Nico Verbruggen on 29/11/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class ValetConfigParserTest: XCTestCase {
|
class ValetConfigurationTest: XCTestCase {
|
||||||
|
|
||||||
static var jsonConfigFileUrl: URL {
|
static var jsonConfigFileUrl: URL {
|
||||||
return Bundle(for: Self.self).url(
|
return Bundle(for: Self.self).url(
|
81
phpmon-tests/Test Files/nginx/nginx-proxy.test
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# valet stub: proxy.valet.conf
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:80;
|
||||||
|
#listen 127.0.0.1:80; # valet loopback
|
||||||
|
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
|
||||||
|
root /;
|
||||||
|
charset utf-8;
|
||||||
|
client_max_body_size 128M;
|
||||||
|
|
||||||
|
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
|
||||||
|
internal;
|
||||||
|
alias /;
|
||||||
|
try_files $uri $uri/;
|
||||||
|
}
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
|
||||||
|
|
||||||
|
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:90;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Client-Verify SUCCESS;
|
||||||
|
proxy_set_header X-Client-DN $ssl_client_s_dn;
|
||||||
|
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
|
||||||
|
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_read_timeout 1800;
|
||||||
|
proxy_connect_timeout 1800;
|
||||||
|
chunked_transfer_encoding on;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:60;
|
||||||
|
#listen 127.0.0.1:60; # valet loopback
|
||||||
|
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
|
||||||
|
root /;
|
||||||
|
charset utf-8;
|
||||||
|
client_max_body_size 128M;
|
||||||
|
|
||||||
|
add_header X-Robots-Tag 'noindex, nofollow, nosnippet, noarchive';
|
||||||
|
|
||||||
|
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
|
||||||
|
internal;
|
||||||
|
alias /;
|
||||||
|
try_files $uri $uri/;
|
||||||
|
}
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
|
||||||
|
|
||||||
|
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:90;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,57 @@
|
|||||||
|
# valet stub: secure.proxy.valet.conf
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:80;
|
||||||
|
#listen 127.0.0.1:80; # valet loopback
|
||||||
|
server_name live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:443 ssl http2;
|
||||||
|
#listen 127.0.0.1:443 ssl http2; # valet loopback
|
||||||
|
server_name live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com;
|
||||||
|
root /;
|
||||||
|
charset utf-8;
|
||||||
|
client_max_body_size 128M;
|
||||||
|
http2_push_preload on;
|
||||||
|
|
||||||
|
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
|
||||||
|
internal;
|
||||||
|
alias /;
|
||||||
|
try_files $uri $uri/;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_certificate "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.crt";
|
||||||
|
ssl_certificate_key "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.key";
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
error_log "/Users/phpmon/.config/valet/Log/live.whatagraph.dev.com-error.log";
|
||||||
|
|
||||||
|
error_page 404 "/Users/phpmon/.composer/vendor/laravel/valet/server.php";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Client-Verify SUCCESS;
|
||||||
|
proxy_set_header X-Client-DN $ssl_client_s_dn;
|
||||||
|
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
|
||||||
|
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_read_timeout 1800;
|
||||||
|
proxy_connect_timeout 1800;
|
||||||
|
chunked_transfer_encoding on;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
57
phpmon-tests/Test Files/nginx/nginx-secure-proxy.test
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# valet stub: secure.proxy.valet.conf
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:80;
|
||||||
|
#listen 127.0.0.1:80; # valet loopback
|
||||||
|
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:443 ssl http2;
|
||||||
|
#listen 127.0.0.1:443 ssl http2; # valet loopback
|
||||||
|
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
|
||||||
|
root /;
|
||||||
|
charset utf-8;
|
||||||
|
client_max_body_size 128M;
|
||||||
|
http2_push_preload on;
|
||||||
|
|
||||||
|
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
|
||||||
|
internal;
|
||||||
|
alias /;
|
||||||
|
try_files $uri $uri/;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_certificate "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.crt";
|
||||||
|
ssl_certificate_key "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.key";
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
|
||||||
|
|
||||||
|
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:90;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Client-Verify SUCCESS;
|
||||||
|
proxy_set_header X-Client-DN $ssl_client_s_dn;
|
||||||
|
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
|
||||||
|
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_read_timeout 1800;
|
||||||
|
proxy_connect_timeout 1800;
|
||||||
|
chunked_transfer_encoding on;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -3,7 +3,7 @@
|
|||||||
// phpmon-tests
|
// phpmon-tests
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 14/02/2021.
|
// Created by Nico Verbruggen on 14/02/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
47
phpmon-tests/Versions/AppUpdaterCheckTest.swift
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// AppUpdaterCheckTest.swift
|
||||||
|
// phpmon-tests
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 10/05/2022.
|
||||||
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class AppUpdaterCheckTest: XCTestCase {
|
||||||
|
|
||||||
|
func testCanRetrieveVersionFromCask() {
|
||||||
|
let caskVersion = AppUpdateChecker.retrieveVersionFromCask()
|
||||||
|
|
||||||
|
let version = VersionExtractor.from(caskVersion)
|
||||||
|
|
||||||
|
XCTAssertNotNil(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTaggedReleaseOmitsZeroPatch() {
|
||||||
|
let version = AppVersion.from("3.5.0_333")!
|
||||||
|
|
||||||
|
XCTAssertEqual(version.tagged, "3.5")
|
||||||
|
XCTAssertEqual(version.version, "3.5.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTaggedReleaseDoesntOmitNonZeroPatch() {
|
||||||
|
let version = AppVersion.from("3.5.1_333")!
|
||||||
|
|
||||||
|
XCTAssertEqual(version.tagged, "3.5.1")
|
||||||
|
XCTAssertEqual(version.version, "3.5.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTagTruncationDoesntAffectMajorVersions() {
|
||||||
|
var version = AppVersion.from("5.0_333")!
|
||||||
|
|
||||||
|
XCTAssertEqual(version.tagged, "5.0")
|
||||||
|
XCTAssertEqual(version.version, "5.0")
|
||||||
|
|
||||||
|
version = AppVersion.from("5.0.0_333")!
|
||||||
|
|
||||||
|
XCTAssertEqual(version.tagged, "5.0")
|
||||||
|
XCTAssertEqual(version.version, "5.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
62
phpmon-tests/Versions/AppVersionTest.swift
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// AppVersionTest.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 10/05/2022.
|
||||||
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class AppVersionTest: XCTestCase {
|
||||||
|
|
||||||
|
func testCanRetrieveInternalAppVersion() {
|
||||||
|
XCTAssertNotNil(AppVersion.fromCurrentVersion())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanParseNormalVersionString() {
|
||||||
|
let version = AppVersion.from("1.0.0")
|
||||||
|
|
||||||
|
XCTAssertNotNil(version)
|
||||||
|
XCTAssertEqual("1.0.0", version?.version)
|
||||||
|
XCTAssertEqual(nil, version?.build)
|
||||||
|
XCTAssertEqual(nil, version?.suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanParseCaskVersionString() {
|
||||||
|
let version = AppVersion.from("1.0.0_600")
|
||||||
|
|
||||||
|
XCTAssertNotNil(version)
|
||||||
|
XCTAssertEqual("1.0.0", version?.version)
|
||||||
|
XCTAssertEqual("600", version?.build)
|
||||||
|
XCTAssertEqual(nil, version?.suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanParseDevVersionStringWithoutBuildNumber() {
|
||||||
|
let version = AppVersion.from("1.0.0-dev")
|
||||||
|
|
||||||
|
XCTAssertNotNil(version)
|
||||||
|
XCTAssertEqual("1.0.0", version?.version)
|
||||||
|
XCTAssertEqual(nil, version?.build)
|
||||||
|
XCTAssertEqual("dev", version?.suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanParseDevVersionStringWithBuildNumber() {
|
||||||
|
let version = AppVersion.from("1.0.0-dev,870")
|
||||||
|
|
||||||
|
XCTAssertNotNil(version)
|
||||||
|
XCTAssertEqual("1.0.0", version?.version)
|
||||||
|
XCTAssertEqual("870", version?.build)
|
||||||
|
XCTAssertEqual("dev", version?.suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanParseUnderscoresAsBuildSeparatorToo() {
|
||||||
|
let version = AppVersion.from("1.0.0-dev_870")
|
||||||
|
|
||||||
|
XCTAssertNotNil(version)
|
||||||
|
XCTAssertEqual("1.0.0", version?.version)
|
||||||
|
XCTAssertEqual("870", version?.build)
|
||||||
|
XCTAssertEqual("dev", version?.suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -3,7 +3,7 @@
|
|||||||
// phpmon-tests
|
// phpmon-tests
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 01/04/2021.
|
// Created by Nico Verbruggen on 01/04/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
|
// swiftlint:disable type_body_length
|
||||||
class PhpVersionNumberTest: XCTestCase {
|
class PhpVersionNumberTest: XCTestCase {
|
||||||
|
|
||||||
func testCanDeconstructPhpVersion() throws {
|
func testCanDeconstructPhpVersion() throws {
|
||||||
@@ -287,4 +288,76 @@ class PhpVersionNumberTest: XCTestCase {
|
|||||||
.make(from: ["7.3.1", "7.2.9"]).all
|
.make(from: ["7.3.1", "7.2.9"]).all
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCanCheckLessThanOrEqualConstraints() throws {
|
||||||
|
XCTAssertEqual(
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
|
.matching(constraint: "<=7.2", strict: true),
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
|
.matching(constraint: "<=7.2.0", strict: true),
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||||
|
)
|
||||||
|
|
||||||
|
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
|
||||||
|
XCTAssertEqual(
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
|
.matching(constraint: "<=7.2.5", strict: true),
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||||
|
)
|
||||||
|
|
||||||
|
// Non-strict check (ignoring patch has no effect)
|
||||||
|
XCTAssertEqual(
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
|
.matching(constraint: "<=7.2.5", strict: false),
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanCheckLessThanConstraints() throws {
|
||||||
|
XCTAssertEqual(
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
|
.matching(constraint: "<7.2", strict: true),
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.1", "7.0"]).all
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
|
.matching(constraint: "<7.2.0", strict: true),
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.1", "7.0"]).all
|
||||||
|
)
|
||||||
|
|
||||||
|
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
|
||||||
|
XCTAssertEqual(
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
|
.matching(constraint: "<7.2.5", strict: true),
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.2", "7.1", "7.0"]).all
|
||||||
|
)
|
||||||
|
|
||||||
|
// Non-strict check (patch resolves to 7.2.999, which is bigger than 7.2.5)
|
||||||
|
XCTAssertEqual(
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||||
|
.matching(constraint: "<7.2.5", strict: false),
|
||||||
|
PhpVersionNumberCollection
|
||||||
|
.make(from: ["7.1", "7.0"]).all
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// phpmon-tests
|
// phpmon-tests
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 29/11/2021.
|
// Created by Nico Verbruggen on 29/11/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// phpmon-tests
|
// phpmon-tests
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 16/12/2021.
|
// Created by Nico Verbruggen on 16/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
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,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "ServiceOn.png",
|
"filename" : "Proxy.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "ServiceOn@2x.png",
|
"filename" : "Proxy@2x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
BIN
phpmon/Assets.xcassets/IconProxy.imageset/Proxy.png
vendored
Normal file
After Width: | Height: | Size: 935 B |
BIN
phpmon/Assets.xcassets/IconProxy.imageset/Proxy@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
@@ -1,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 |
Before Width: | Height: | Size: 819 B |
Before Width: | Height: | Size: 1.2 KiB |
@@ -2,7 +2,7 @@
|
|||||||
// Services.swift
|
// Services.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -12,33 +12,28 @@ class Actions {
|
|||||||
|
|
||||||
// MARK: - Services
|
// MARK: - Services
|
||||||
|
|
||||||
public static func restartPhpFpm()
|
public static func restartPhpFpm() {
|
||||||
{
|
|
||||||
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
|
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func restartNginx()
|
public static func restartNginx() {
|
||||||
{
|
|
||||||
brew("services restart nginx", sudo: true)
|
brew("services restart nginx", sudo: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func restartDnsMasq()
|
public static func restartDnsMasq() {
|
||||||
{
|
|
||||||
brew("services restart dnsmasq", sudo: true)
|
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 \(PhpEnv.phpInstall.formula)", sudo: true)
|
||||||
brew("services stop nginx", sudo: true)
|
brew("services stop nginx", sudo: true)
|
||||||
brew("services stop dnsmasq", sudo: true)
|
brew("services stop dnsmasq", sudo: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func fixHomebrewPermissions() throws
|
public static func fixHomebrewPermissions() throws {
|
||||||
{
|
|
||||||
var servicesCommands = [
|
var servicesCommands = [
|
||||||
"\(Paths.brew) services stop nginx",
|
"\(Paths.brew) services stop nginx",
|
||||||
"\(Paths.brew) services stop dnsmasq",
|
"\(Paths.brew) services stop dnsmasq"
|
||||||
]
|
]
|
||||||
var cellarCommands = [
|
var cellarCommands = [
|
||||||
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
|
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
|
||||||
@@ -64,43 +59,67 @@ class Actions {
|
|||||||
|
|
||||||
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
|
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
|
||||||
|
|
||||||
if (eventResult == nil) {
|
if eventResult == nil {
|
||||||
throw HomebrewPermissionError(kind: .applescriptNilError)
|
throw HomebrewPermissionError(kind: .applescriptNilError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Third Party Services
|
||||||
|
public static func stopService(name: String, completion: @escaping () -> Void) {
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
brew("services stop \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
|
||||||
|
ServicesManager.loadHomebrewServices(completed: {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func startService(name: String, completion: @escaping () -> Void) {
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
brew("services start \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
|
||||||
|
ServicesManager.loadHomebrewServices(completed: {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Finding Config Files
|
// MARK: - Finding Config Files
|
||||||
|
|
||||||
public static func openGenericPhpConfigFolder()
|
public static func openGenericPhpConfigFolder() {
|
||||||
{
|
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]
|
||||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
|
|
||||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func openGlobalComposerFolder()
|
public static func openGlobalComposerFolder() {
|
||||||
{
|
|
||||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".composer/composer.json")
|
.appendingPathComponent(".composer/composer.json")
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func openPhpConfigFolder(version: String)
|
public static func openPhpConfigFolder(version: String) {
|
||||||
{
|
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
|
||||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
|
|
||||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func openValetConfigFolder()
|
public static func openValetConfigFolder() {
|
||||||
{
|
|
||||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".config/valet")
|
.appendingPathComponent(".config/valet")
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func openPhpMonitorConfigFile() {
|
||||||
|
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent(".config/phpmon")
|
||||||
|
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Other Actions
|
// MARK: - Other Actions
|
||||||
|
|
||||||
public static func createTempPhpInfoFile() -> URL
|
public static func createTempPhpInfoFile() -> URL {
|
||||||
{
|
|
||||||
// Write a file called `phpmon_phpinfo.php` to /tmp
|
// Write a file called `phpmon_phpinfo.php` to /tmp
|
||||||
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
@@ -124,8 +143,7 @@ class Actions {
|
|||||||
If this does not solve the issue, the user may need to install additional
|
If this does not solve the issue, the user may need to install additional
|
||||||
extensions and/or run `composer global update`.
|
extensions and/or run `composer global update`.
|
||||||
*/
|
*/
|
||||||
public static func fixMyValet(completed: @escaping () -> Void)
|
public static func fixMyValet(completed: @escaping () -> Void) {
|
||||||
{
|
|
||||||
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
|
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
|
||||||
brew("services restart dnsmasq", sudo: true)
|
brew("services restart dnsmasq", sudo: true)
|
||||||
brew("services restart php", sudo: true)
|
brew("services restart php", sudo: true)
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// Command.swift
|
// Command.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -28,7 +28,7 @@ public class Command {
|
|||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
|
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
|
||||||
|
|
||||||
if (trimNewlines) {
|
if trimNewlines {
|
||||||
return output.components(separatedBy: .newlines)
|
return output.components(separatedBy: .newlines)
|
||||||
.filter({ !$0.isEmpty })
|
.filter({ !$0.isEmpty })
|
||||||
.joined(separator: "\n")
|
.joined(separator: "\n")
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// Constants.swift
|
// Constants.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -53,14 +53,32 @@ struct Constants {
|
|||||||
|
|
||||||
struct Urls {
|
struct Urls {
|
||||||
|
|
||||||
static let DonationPayment = URL(
|
// phpmon.app URLs (these are aliased to redirect correctly)
|
||||||
string: "https://nicoverbruggen.be/sponsor#pay-now"
|
|
||||||
)!
|
|
||||||
static let DonationPage = URL(
|
static let DonationPage = URL(
|
||||||
string: "https://nicoverbruggen.be/sponsor"
|
string: "https://phpmon.app/sponsor"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
static let FrequentlyAskedQuestions = URL(
|
static let FrequentlyAskedQuestions = URL(
|
||||||
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
|
string: "https://phpmon.app/faq"
|
||||||
|
)!
|
||||||
|
|
||||||
|
static let DonationPayment = URL(
|
||||||
|
string: "https://phpmon.app/sponsor/now"
|
||||||
|
)!
|
||||||
|
|
||||||
|
// GitHub URLs (do not alias these)
|
||||||
|
|
||||||
|
static let GitHubReleases = URL(
|
||||||
|
string: "https://github.com/nicoverbruggen/phpmon/releases"
|
||||||
|
)!
|
||||||
|
|
||||||
|
static let StableBuildCaskFile = URL(
|
||||||
|
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb"
|
||||||
|
)!
|
||||||
|
|
||||||
|
static let DevBuildCaskFile = URL(
|
||||||
|
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 24/12/2021.
|
// Created by Nico Verbruggen on 24/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
// MARK: Common Shell Commands
|
// MARK: Common Shell Commands
|
||||||
@@ -11,24 +11,21 @@
|
|||||||
/**
|
/**
|
||||||
Runs a `valet` command. Defaults to running as superuser.
|
Runs a `valet` command. Defaults to running as superuser.
|
||||||
*/
|
*/
|
||||||
func valet(_ command: String, sudo: Bool = true) -> String
|
func valet(_ command: String, sudo: Bool = true) -> String {
|
||||||
{
|
|
||||||
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
|
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Runs a `brew` command. Can run as superuser.
|
Runs a `brew` command. Can run as superuser.
|
||||||
*/
|
*/
|
||||||
func brew(_ command: String, sudo: Bool = false)
|
func brew(_ command: String, sudo: Bool = false) {
|
||||||
{
|
|
||||||
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
|
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
|
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
|
||||||
*/
|
*/
|
||||||
func sed(file: String, original: String, replacement: String)
|
func sed(file: String, original: String, replacement: String) {
|
||||||
{
|
|
||||||
// Escape slashes (or `sed` won't work)
|
// Escape slashes (or `sed` won't work)
|
||||||
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
|
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
|
||||||
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
|
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
|
||||||
@@ -45,8 +42,7 @@ func sed(file: String, original: String, replacement: String)
|
|||||||
/**
|
/**
|
||||||
Uses `grep` to determine whether a particular query string can be found in a particular file.
|
Uses `grep` to determine whether a particular query string can be found in a particular file.
|
||||||
*/
|
*/
|
||||||
func grepContains(file: String, query: String) -> Bool
|
func grepContains(file: String, query: String) -> Bool {
|
||||||
{
|
|
||||||
return Shell.pipe("""
|
return Shell.pipe("""
|
||||||
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
|
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
|
||||||
""")
|
""")
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 21/12/2021.
|
// Created by Nico Verbruggen on 21/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// Paths.swift
|
// Paths.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -21,7 +21,7 @@ public class Paths {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
||||||
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
|
userName = String(Shell.pipe("id -un").split(separator: "\n")[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
public func detectBinaryPaths() {
|
public func detectBinaryPaths() {
|
||||||
@@ -49,7 +49,7 @@ public class Paths {
|
|||||||
// - MARK: Detected Binaries
|
// - MARK: Detected Binaries
|
||||||
|
|
||||||
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
|
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
|
||||||
public static var composer: String? = nil
|
public static var composer: String?
|
||||||
|
|
||||||
// - MARK: Paths
|
// - MARK: Paths
|
||||||
|
|
||||||
@@ -57,6 +57,10 @@ public class Paths {
|
|||||||
return shared.userName
|
return shared.userName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var homePath: String {
|
||||||
|
return NSHomeDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
public static var cellarPath: String {
|
public static var cellarPath: String {
|
||||||
return "\(shared.baseDir.rawValue)/Cellar"
|
return "\(shared.baseDir.rawValue)/Cellar"
|
||||||
}
|
}
|
||||||
|
@@ -35,8 +35,11 @@ extension Process {
|
|||||||
forName: NSNotification.Name.NSFileHandleDataAvailable,
|
forName: NSNotification.Name.NSFileHandleDataAvailable,
|
||||||
object: pipe.fileHandleForReading,
|
object: pipe.fileHandleForReading,
|
||||||
queue: nil
|
queue: nil
|
||||||
) { notification in
|
) { _ in
|
||||||
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
|
if let outputString = String(
|
||||||
|
data: pipe.fileHandleForReading.availableData,
|
||||||
|
encoding: String.Encoding.utf8
|
||||||
|
) {
|
||||||
callback(outputString)
|
callback(outputString)
|
||||||
}
|
}
|
||||||
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||||
|
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) ?? ""
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,7 @@
|
|||||||
// Shell.swift
|
// Shell.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -32,6 +32,9 @@ public class Shell {
|
|||||||
*/
|
*/
|
||||||
public var shell: String = "/bin/sh"
|
public var shell: String = "/bin/sh"
|
||||||
|
|
||||||
|
/** Additional exports that are sent if `requiresPath` is set to true. */
|
||||||
|
public var exports: String = ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Singleton to access a user shell (with --login)
|
Singleton to access a user shell (with --login)
|
||||||
*/
|
*/
|
||||||
@@ -91,7 +94,7 @@ public class Shell {
|
|||||||
task.launch()
|
task.launch()
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
|
|
||||||
return Shell.Output(
|
let output = Shell.Output(
|
||||||
standardOutput: String(
|
standardOutput: String(
|
||||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||||
encoding: .utf8
|
encoding: .utf8
|
||||||
@@ -102,23 +105,56 @@ public class Shell {
|
|||||||
)!,
|
)!,
|
||||||
task: task
|
task: task
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if CommandLine.arguments.contains("--v") {
|
||||||
|
log(task: task, output: output)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates a new process with the correct PATH and shell.
|
Creates a new process with the correct PATH and shell.
|
||||||
*/
|
*/
|
||||||
public func createTask(for command: String, requiresPath: Bool) -> Process {
|
public func createTask(for command: String, requiresPath: Bool) -> Process {
|
||||||
let tailoredCommand = requiresPath
|
var completeCommand = ""
|
||||||
? "export PATH=\(Paths.binPath):$PATH && \(command)"
|
|
||||||
: command
|
if requiresPath {
|
||||||
|
// Basic export (PATH)
|
||||||
|
completeCommand += "export PATH=\(Paths.binPath):$PATH && "
|
||||||
|
|
||||||
|
// Put additional exports in between
|
||||||
|
if !self.exports.isEmpty {
|
||||||
|
completeCommand += "\(self.exports) && "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completeCommand += command
|
||||||
|
|
||||||
let task = Process()
|
let task = Process()
|
||||||
task.launchPath = self.shell
|
task.launchPath = self.shell
|
||||||
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
|
task.arguments = ["--noprofile", "-norc", "--login", "-c", completeCommand]
|
||||||
|
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Verbose logging for PHP Monitor's synchronous shell output.
|
||||||
|
*/
|
||||||
|
private func log(task: Process, output: Output) {
|
||||||
|
Log.info("")
|
||||||
|
Log.info("==== COMMAND ====")
|
||||||
|
Log.info("")
|
||||||
|
Log.info("\(self.shell) \(task.arguments?.joined(separator: " ") ?? "")")
|
||||||
|
Log.info("")
|
||||||
|
Log.info("==== OUTPUT ====")
|
||||||
|
Log.info("")
|
||||||
|
dump(output)
|
||||||
|
Log.info("")
|
||||||
|
Log.info("==== END OUTPUT ====")
|
||||||
|
Log.info("")
|
||||||
|
}
|
||||||
|
|
||||||
public class Output {
|
public class Output {
|
||||||
public let standardOutput: String
|
public let standardOutput: String
|
||||||
public let errorOutput: String
|
public let errorOutput: String
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,7 @@
|
|||||||
// Date.swift
|
// Date.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
@@ -3,27 +3,23 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 14/04/2021.
|
// Created by Nico Verbruggen on 14/04/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
extension NSMenu {
|
extension NSMenu {
|
||||||
|
convenience init(items: [NSMenuItem], target: NSObject? = nil) {
|
||||||
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
|
self.init()
|
||||||
newItem.keyEquivalentModifierMask = modifier
|
self.addItems(items, target: target)
|
||||||
self.addItem(newItem)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
public func addItems(_ items: [NSMenuItem], target: NSObject? = nil) {
|
||||||
|
for item in items {
|
||||||
@IBDesignable class LocalizedMenuItem: NSMenuItem {
|
self.addItem(item)
|
||||||
|
if target != nil {
|
||||||
@IBInspectable
|
item.target = target
|
||||||
var localizationKey: String? {
|
}
|
||||||
didSet {
|
|
||||||
self.title = localizationKey?.localized ?? self.title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
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,28 +11,42 @@ import Cocoa
|
|||||||
|
|
||||||
extension NSWindow {
|
extension NSWindow {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Centers a window. Taken from: https://stackoverflow.com/a/66140320
|
||||||
|
*/
|
||||||
|
public func setCenterPosition(offsetY: CGFloat = 0) {
|
||||||
|
if let screenSize = screen?.visibleFrame.size {
|
||||||
|
self.setFrameOrigin(
|
||||||
|
NSPoint(
|
||||||
|
x: (screenSize.width - frame.size.width) / 2,
|
||||||
|
y: (screenSize.height - frame.size.height) / 2 + offsetY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
|
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
|
||||||
*/
|
*/
|
||||||
func shake(){
|
func shake() {
|
||||||
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
|
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
|
||||||
|
|
||||||
let frame: CGRect = self.frame
|
let frame: CGRect = self.frame
|
||||||
let shakeAnimation :CAKeyframeAnimation = CAKeyframeAnimation()
|
let shakeAnimation: CAKeyframeAnimation = CAKeyframeAnimation()
|
||||||
|
|
||||||
let shakePath = CGMutablePath()
|
let shakePath = CGMutablePath()
|
||||||
shakePath.move( to: CGPoint(x:NSMinX(frame), y:NSMinY(frame)))
|
shakePath.move( to: CGPoint(x: frame.minX, y: frame.minY))
|
||||||
|
|
||||||
for _ in 0...numberOfShakes-1 {
|
for _ in 0...numberOfShakes-1 {
|
||||||
shakePath.addLine(to: CGPoint(x:NSMinX(frame) - frame.size.width * vigourOfShake, y:NSMinY(frame)))
|
shakePath.addLine(to: CGPoint(x: frame.minX - frame.size.width * vigourOfShake, y: frame.minY))
|
||||||
shakePath.addLine(to: CGPoint(x:NSMinX(frame) + frame.size.width * vigourOfShake, y:NSMinY(frame)))
|
shakePath.addLine(to: CGPoint(x: frame.minX + frame.size.width * vigourOfShake, y: frame.minY))
|
||||||
}
|
}
|
||||||
|
|
||||||
shakePath.closeSubpath()
|
shakePath.closeSubpath()
|
||||||
shakeAnimation.path = shakePath
|
shakeAnimation.path = shakePath
|
||||||
shakeAnimation.duration = durationOfShake
|
shakeAnimation.duration = durationOfShake
|
||||||
|
|
||||||
self.animations = ["frameOrigin":shakeAnimation]
|
self.animations = ["frameOrigin": shakeAnimation]
|
||||||
self.animator().setFrameOrigin(self.frame.origin)
|
self.animator().setFrameOrigin(self.frame.origin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,22 +2,32 @@
|
|||||||
// StringExtension.swift
|
// StringExtension.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|
||||||
var localized: String {
|
var localized: String {
|
||||||
|
if #available(macOS 13, *) {
|
||||||
|
return NSLocalizedString(
|
||||||
|
self, tableName: nil, bundle: Bundle.main, value: "", comment: ""
|
||||||
|
).replacingOccurrences(of: "Preferences", with: "Settings")
|
||||||
|
}
|
||||||
|
|
||||||
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
|
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var localizedForSwiftUI: LocalizedStringKey {
|
||||||
|
return LocalizedStringKey(self.localized)
|
||||||
|
}
|
||||||
|
|
||||||
func localized(_ args: CVarArg...) -> String {
|
func localized(_ args: CVarArg...) -> String {
|
||||||
String(format: self.localized, arguments: args)
|
String(format: self.localized, arguments: args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func countInstances(of stringToFind: String) -> Int {
|
func countInstances(of stringToFind: String) -> Int {
|
||||||
if (stringToFind.isEmpty) {
|
if stringToFind.isEmpty {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +42,7 @@ extension String {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript (r: Range<String.Index>) -> String {
|
subscript(r: Range<String.Index>) -> String {
|
||||||
let start = r.lowerBound
|
let start = r.lowerBound
|
||||||
let end = r.upperBound
|
let end = r.upperBound
|
||||||
return String(self[start ..< end])
|
return String(self[start ..< end])
|
||||||
@@ -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 ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 04/02/2021.
|
// Created by Nico Verbruggen on 04/02/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -26,10 +26,10 @@ extension XibLoadable where Self: NSView {
|
|||||||
|
|
||||||
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
|
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
|
||||||
guard let xibName = xibName else { return nil }
|
guard let xibName = xibName else { return nil }
|
||||||
var topLevelArray: NSArray? = nil
|
var topLevelArray: NSArray?
|
||||||
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
|
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
|
||||||
guard let results = topLevelArray else { return nil }
|
guard let results = topLevelArray else { return nil }
|
||||||
let views = Array<Any>(results).filter { $0 is Self }
|
let views = [Any](results).filter { $0 is Self }
|
||||||
return views.last as? Self
|
return views.last as? Self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// Alert.swift
|
// Alert.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -27,7 +27,7 @@ class Alert {
|
|||||||
alert.messageText = messageText
|
alert.messageText = messageText
|
||||||
alert.informativeText = informativeText
|
alert.informativeText = informativeText
|
||||||
alert.addButton(withTitle: buttonTitle)
|
alert.addButton(withTitle: buttonTitle)
|
||||||
if (!secondButtonTitle.isEmpty) {
|
if !secondButtonTitle.isEmpty {
|
||||||
alert.addButton(withTitle: secondButtonTitle)
|
alert.addButton(withTitle: secondButtonTitle)
|
||||||
}
|
}
|
||||||
alert.beginSheetModal(for: window) { response in
|
alert.beginSheetModal(for: window) { response in
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 07/12/2021.
|
// Created by Nico Verbruggen on 07/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
@@ -1,23 +1,61 @@
|
|||||||
//
|
//
|
||||||
// FileSystem.swift
|
// Filesystem.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 07/12/2021.
|
// Created by Nico Verbruggen on 07/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import Foundation
|
||||||
|
|
||||||
class Filesystem {
|
class Filesystem {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks if a file exists at the provided path.
|
Checks if a file or directory exists at the provided path.
|
||||||
Uses `FileManager`.
|
|
||||||
*/
|
*/
|
||||||
public static func fileExists(_ path: String) -> Bool {
|
public static func exists(_ path: String) -> Bool {
|
||||||
return FileManager.default.fileExists(
|
return FileManager.default.fileExists(
|
||||||
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
|
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Checks if a file exists at the provided path.
|
||||||
|
*/
|
||||||
|
public static func fileExists(_ path: String) -> Bool {
|
||||||
|
var isDirectory: ObjCBool = true
|
||||||
|
let exists = FileManager.default.fileExists(
|
||||||
|
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
|
||||||
|
isDirectory: &isDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
return exists && !isDirectory.boolValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Checks if a directory exists at the provided path.
|
||||||
|
*/
|
||||||
|
public static func directoryExists(_ path: String) -> Bool {
|
||||||
|
var isDirectory: ObjCBool = true
|
||||||
|
let exists = FileManager.default.fileExists(
|
||||||
|
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
|
||||||
|
isDirectory: &isDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
return exists && isDirectory.boolValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Checks if a given file is a symbolic link.
|
||||||
|
*/
|
||||||
|
public static func fileIsSymlink(_ path: String) -> Bool {
|
||||||
|
do {
|
||||||
|
let attribs = try FileManager.default.attributesOfItem(atPath: path)
|
||||||
|
return attribs[.type] as! FileAttributeType == FileAttributeType.typeSymbolicLink
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// LocalNotification.swift
|
// LocalNotification.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -10,7 +10,11 @@ import UserNotifications
|
|||||||
|
|
||||||
class LocalNotification {
|
class LocalNotification {
|
||||||
|
|
||||||
public static func send(title: String, subtitle: String) {
|
@MainActor public static func send(title: String, subtitle: String, preference: PreferenceName) {
|
||||||
|
if !Preferences.isEnabled(preference) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = title
|
content.title = title
|
||||||
content.body = subtitle
|
content.body = subtitle
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// ImageGenerator.swift
|
// ImageGenerator.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -24,7 +24,7 @@ class MenuBarImageGenerator {
|
|||||||
NSAttributedString.Key.paragraphStyle: textStyle
|
NSAttributedString.Key.paragraphStyle: textStyle
|
||||||
]
|
]
|
||||||
|
|
||||||
let padding : CGFloat = 2.0;
|
let padding: CGFloat = 2.0
|
||||||
|
|
||||||
// Create an attributed string so we'll know how wide the item will need to be
|
// Create an attributed string so we'll know how wide the item will need to be
|
||||||
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
|
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 05/12/2021.
|
// Created by Nico Verbruggen on 05/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -30,7 +30,7 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Log.perf("Window controller '\(windowName)' was deinitialized")
|
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 16/12/2021.
|
// Created by Nico Verbruggen on 16/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -23,7 +23,7 @@ class VersionExtractor {
|
|||||||
let match = regex.matches(
|
let match = regex.matches(
|
||||||
in: string,
|
in: string,
|
||||||
options: [],
|
options: [],
|
||||||
range: NSMakeRange(0, string.count)
|
range: NSRange(location: 0, length: string.count)
|
||||||
).first
|
).first
|
||||||
|
|
||||||
guard let match = match else {
|
guard let match = match else {
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// ActivePhpInstallation.swift
|
// ActivePhpInstallation.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -20,7 +20,13 @@ class ActivePhpInstallation {
|
|||||||
|
|
||||||
var version: Version!
|
var version: Version!
|
||||||
var limits: Limits!
|
var limits: Limits!
|
||||||
var extensions: [PhpExtension]!
|
var iniFiles: [PhpConfigurationFile] = []
|
||||||
|
|
||||||
|
var extensions: [PhpExtension] {
|
||||||
|
return iniFiles.flatMap { initFile in
|
||||||
|
return initFile.extensions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Computed
|
// MARK: - Computed
|
||||||
|
|
||||||
@@ -34,16 +40,21 @@ class ActivePhpInstallation {
|
|||||||
// Show information about the current version
|
// Show information about the current version
|
||||||
getVersion()
|
getVersion()
|
||||||
|
|
||||||
|
// Initialize the list of ini files that are loaded
|
||||||
|
iniFiles = []
|
||||||
|
|
||||||
// If an error occurred, exit early
|
// If an error occurred, exit early
|
||||||
if (version.error) {
|
if version.error {
|
||||||
limits = Limits()
|
limits = Limits()
|
||||||
extensions = []
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load extension information
|
// Load extension information
|
||||||
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||||
extensions = PhpExtension.load(from: path)
|
|
||||||
|
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
||||||
|
iniFiles.append(file)
|
||||||
|
}
|
||||||
|
|
||||||
// Get configuration values
|
// Get configuration values
|
||||||
limits = Limits(
|
limits = Limits(
|
||||||
@@ -60,9 +71,8 @@ class ActivePhpInstallation {
|
|||||||
|
|
||||||
// See if any extensions are present in said .ini files
|
// See if any extensions are present in said .ini files
|
||||||
paths.forEach { (iniFilePath) in
|
paths.forEach { (iniFilePath) in
|
||||||
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
|
||||||
if exts.count > 0 {
|
iniFiles.append(file)
|
||||||
extensions.append(contentsOf: exts)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,12 +81,12 @@ class ActivePhpInstallation {
|
|||||||
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
|
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
|
||||||
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
|
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
|
||||||
*/
|
*/
|
||||||
private func getVersion() -> Void {
|
private func getVersion() {
|
||||||
self.version = Version()
|
self.version = Version()
|
||||||
|
|
||||||
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
|
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
|
||||||
|
|
||||||
if (version == "" || version.contains("Warning") || version.contains("Error")) {
|
if version == "" || version.contains("Warning") || version.contains("Error") {
|
||||||
self.version.short = "💩 BROKEN"
|
self.version.short = "💩 BROKEN"
|
||||||
self.version.long = ""
|
self.version.long = ""
|
||||||
self.version.error = true
|
self.version.error = true
|
||||||
@@ -112,13 +122,13 @@ class ActivePhpInstallation {
|
|||||||
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
|
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
|
||||||
|
|
||||||
// Check if the value is unlimited
|
// Check if the value is unlimited
|
||||||
if (value == "-1") {
|
if value == "-1" {
|
||||||
return "∞"
|
return "∞"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the syntax is valid otherwise
|
// Check if the syntax is valid otherwise
|
||||||
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
|
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
|
||||||
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
|
let match = regex.matches(in: value, options: [], range: NSRange(location: 0, length: value.count)).first
|
||||||
return (match == nil) ? "⚠️" : "\(value)B"
|
return (match == nil) ? "⚠️" : "\(value)B"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
phpmon/Common/PHP/Extensions/Xdebug.swift
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// Xdebug.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 01/05/2022.
|
||||||
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
class Xdebug {
|
||||||
|
|
||||||
|
public static var enabled: Bool {
|
||||||
|
return PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var activeModes: [String] {
|
||||||
|
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let value = file.get(for: "xdebug.mode") else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.components(separatedBy: ",").filter { self.modes.contains($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func asMenuItems() -> [NSMenuItem] {
|
||||||
|
var items: [NSMenuItem] = []
|
||||||
|
|
||||||
|
let activeModes = Self.activeModes
|
||||||
|
|
||||||
|
for mode in Self.modes {
|
||||||
|
let item = XdebugMenuItem(
|
||||||
|
title: mode,
|
||||||
|
action: #selector(MainMenu.toggleXdebugMode(sender:)),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
item.state = activeModes.contains(mode) ? .on : .off
|
||||||
|
item.mode = mode
|
||||||
|
items.append(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var modes: [String] {
|
||||||
|
return [
|
||||||
|
"develop",
|
||||||
|
"coverage",
|
||||||
|
"debug",
|
||||||
|
"gcstats",
|
||||||
|
"profile",
|
||||||
|
"trace"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -2,7 +2,7 @@
|
|||||||
// HomebrewPackage.swift
|
// HomebrewPackage.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
@@ -19,20 +19,20 @@ struct HomebrewService: Decodable, Equatable {
|
|||||||
let log_path: String?
|
let log_path: String?
|
||||||
let error_log_path: String?
|
let error_log_path: String?
|
||||||
|
|
||||||
public static func loadAll(
|
/**
|
||||||
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
|
Dummy data for preview purposes.
|
||||||
completion: @escaping ([HomebrewService]) -> Void
|
*/
|
||||||
) {
|
public static func dummy(named service: String, enabled: Bool) -> Self {
|
||||||
DispatchQueue.global(qos: .background).async {
|
return HomebrewService(
|
||||||
let data = Shell
|
name: service,
|
||||||
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
|
service_name: service,
|
||||||
.data(using: .utf8)!
|
running: enabled,
|
||||||
|
loaded: enabled,
|
||||||
let services = try! JSONDecoder()
|
pid: nil,
|
||||||
.decode([HomebrewService].self, from: data)
|
user: nil,
|
||||||
.filter({ return filter.contains($0.name) })
|
status: nil,
|
||||||
|
log_path: nil,
|
||||||
completion(services)
|
error_log_path: nil
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 21/12/2021.
|
// Created by Nico Verbruggen on 21/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -15,7 +15,7 @@ class PhpEnv {
|
|||||||
init() {
|
init() {
|
||||||
self.currentInstall = ActivePhpInstallation()
|
self.currentInstall = ActivePhpInstallation()
|
||||||
|
|
||||||
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
|
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json")
|
||||||
|
|
||||||
self.homebrewPackage = try! JSONDecoder().decode(
|
self.homebrewPackage = try! JSONDecoder().decode(
|
||||||
[HomebrewPackage].self,
|
[HomebrewPackage].self,
|
||||||
@@ -76,15 +76,14 @@ class PhpEnv {
|
|||||||
return InternalSwitcher()
|
return InternalSwitcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func detectPhpVersions() -> Void {
|
public static func detectPhpVersions() {
|
||||||
_ = Self.shared.detectPhpVersions()
|
_ = Self.shared.detectPhpVersions()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Detects which versions of PHP are installed.
|
Detects which versions of PHP are installed.
|
||||||
*/
|
*/
|
||||||
public func detectPhpVersions() -> [String]
|
public func detectPhpVersions() -> [String] {
|
||||||
{
|
|
||||||
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
|
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
|
||||||
|
|
||||||
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
|
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
|
||||||
@@ -95,7 +94,7 @@ class PhpEnv {
|
|||||||
let phpAlias = homebrewPackage.version
|
let phpAlias = homebrewPackage.version
|
||||||
|
|
||||||
// Avoid inserting a duplicate
|
// Avoid inserting a duplicate
|
||||||
if (!versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php")) {
|
if !versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php") {
|
||||||
versionsOnly.append(phpAlias)
|
versionsOnly.append(phpAlias)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +125,7 @@ class PhpEnv {
|
|||||||
checkBinaries: Bool = true,
|
checkBinaries: Bool = true,
|
||||||
generateHelpers: Bool = true
|
generateHelpers: Bool = true
|
||||||
) -> [String] {
|
) -> [String] {
|
||||||
var output : [String] = []
|
var output: [String] = []
|
||||||
|
|
||||||
var supported = Constants.SupportedPhpVersions
|
var supported = Constants.SupportedPhpVersions
|
||||||
|
|
||||||
@@ -144,8 +143,7 @@ class PhpEnv {
|
|||||||
// is supported and where the binary exists (avoids broken installs)
|
// is supported and where the binary exists (avoids broken installs)
|
||||||
if !output.contains(version)
|
if !output.contains(version)
|
||||||
&& supported.contains(version)
|
&& supported.contains(version)
|
||||||
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
|
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
|
||||||
{
|
|
||||||
output.append(version)
|
output.append(version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,4 +174,14 @@ class PhpEnv {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns the configuration file instance that is used for a specific config value.
|
||||||
|
You can then use the configuration file instance to change values.
|
||||||
|
*/
|
||||||
|
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
|
||||||
|
return PhpEnv.phpInstall.iniFiles
|
||||||
|
.reversed()
|
||||||
|
.first(where: { $0.has(key: key) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,12 +16,23 @@ class PhpHelper {
|
|||||||
// Take the PHP version (e.g. "7.2") and generate a dotless version
|
// Take the PHP version (e.g. "7.2") and generate a dotless version
|
||||||
let dotless = version.replacingOccurrences(of: ".", with: "")
|
let dotless = version.replacingOccurrences(of: ".", with: "")
|
||||||
|
|
||||||
|
// Determine the dotless name for this PHP version
|
||||||
|
let destination = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
||||||
|
|
||||||
|
// Check if the ~/.config/phpmon/bin directory is in the PATH
|
||||||
|
let inPath = Shell.user.PATH.contains("\(Paths.homePath)/.config/phpmon/bin")
|
||||||
|
|
||||||
|
// Check if we can create symlinks (`/usr/local/bin` must be writable)
|
||||||
|
let canWriteSymlinks = FileManager.default.isWritableFile(atPath: "/usr/local/bin/")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let destination = "/usr/local/bin/pm\(dotless)"
|
Shell.run("mkdir -p ~/.config/phpmon/bin")
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: destination) {
|
if FileManager.default.fileExists(atPath: destination) {
|
||||||
let contents = try String(contentsOfFile: destination)
|
let contents = try String(contentsOfFile: destination)
|
||||||
if !contents.contains(keyPhrase) {
|
if !contents.contains(keyPhrase) {
|
||||||
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor (or is unreadable). Not updating this file.")
|
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
|
||||||
|
+ "(or is unreadable). Not updating this file.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,10 +62,40 @@ class PhpHelper {
|
|||||||
|
|
||||||
// Make sure the file is executable
|
// Make sure the file is executable
|
||||||
Shell.run("chmod +x \(destination)")
|
Shell.run("chmod +x \(destination)")
|
||||||
|
|
||||||
|
// Create a symlink if the folder is not in the PATH
|
||||||
|
if !inPath {
|
||||||
|
// First, check if we can create symlinks at all
|
||||||
|
if !canWriteSymlinks {
|
||||||
|
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the symlink
|
||||||
|
self.createSymlink(dotless)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
print(error)
|
||||||
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
|
Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func createSymlink(_ dotless: String) {
|
||||||
|
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
|
||||||
|
let destination = "/usr/local/bin/pm\(dotless)"
|
||||||
|
|
||||||
|
if !Filesystem.fileExists(destination) {
|
||||||
|
Log.info("Creating new symlink: \(destination)")
|
||||||
|
Shell.run("ln -s \(source) \(destination)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Filesystem.fileIsSymlink(destination) {
|
||||||
|
Log.info("Overwriting existing file with new symlink: \(destination)")
|
||||||
|
Shell.run("ln -fs \(source) \(destination)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info("Symlink in \(destination) already exists, OK.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -87,11 +87,19 @@ public struct PhpVersionNumberCollection: Equatable {
|
|||||||
return self.versions.filter { $0.isNewerThan(version, strict) }
|
return self.versions.filter { $0.isNewerThan(version, strict) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThanOrEqual) {
|
||||||
|
return self.versions.filter { $0.isSameAs(version, strict) || $0.isOlderThan(version, strict)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThan) {
|
||||||
|
return self.versions.filter { $0.isOlderThan(version, strict)}
|
||||||
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PhpVersionNumber: Equatable {
|
public struct PhpVersionNumber: Equatable, Hashable {
|
||||||
let major: Int
|
let major: Int
|
||||||
let minor: Int
|
let minor: Int
|
||||||
let patch: Int?
|
let patch: Int?
|
||||||
@@ -116,12 +124,8 @@ public struct PhpVersionNumber: Equatable {
|
|||||||
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
|
|
||||||
// TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
|
|
||||||
/*
|
|
||||||
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func parse(_ text: String) throws -> Self {
|
public static func parse(_ text: String) throws -> Self {
|
||||||
@@ -134,7 +138,12 @@ public struct PhpVersionNumber: Equatable {
|
|||||||
|
|
||||||
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
|
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
|
||||||
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
|
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
|
||||||
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
|
|
||||||
|
let match = regex.matches(
|
||||||
|
in: versionString,
|
||||||
|
options: [],
|
||||||
|
range: NSRange(location: 0, length: versionString.count)
|
||||||
|
).first
|
||||||
|
|
||||||
if match != nil {
|
if match != nil {
|
||||||
let major = Int(
|
let major = Int(
|
||||||
@@ -143,7 +152,7 @@ public struct PhpVersionNumber: Equatable {
|
|||||||
let minor = Int(
|
let minor = Int(
|
||||||
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
|
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
|
||||||
)!
|
)!
|
||||||
var patch: Int? = nil
|
var patch: Int?
|
||||||
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
|
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
|
||||||
patch = Int(versionString[minorRange])
|
patch = Int(versionString[minorRange])
|
||||||
}
|
}
|
||||||
@@ -170,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 {
|
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||||
return self.major == version.major &&
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 31/01/2021.
|
// Created by Nico Verbruggen on 31/01/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -23,7 +23,8 @@ class PhpExtension {
|
|||||||
/// The original string that was used to determine this extension is active.
|
/// The original string that was used to determine this extension is active.
|
||||||
var line: String
|
var line: String
|
||||||
|
|
||||||
/// The name of the extension. This is always identical to the name found in the original string. If you want to display this name, capitalize this.
|
/// The name of the extension. This is always identical to the name found in the original string.
|
||||||
|
/// If you want to display this name, capitalize this.
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
/// Whether the extension has been enabled.
|
/// Whether the extension has been enabled.
|
||||||
@@ -34,6 +35,7 @@ class PhpExtension {
|
|||||||
return String(file.split(separator: "/").last ?? "php.ini")
|
return String(file.split(separator: "/").last ?? "php.ini")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable line_length
|
||||||
/**
|
/**
|
||||||
This regular expression will allow us to identify lines which activate an extension.
|
This regular expression will allow us to identify lines which activate an extension.
|
||||||
|
|
||||||
@@ -47,13 +49,14 @@ class PhpExtension {
|
|||||||
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
||||||
*/
|
*/
|
||||||
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
||||||
|
// swiftlint:enable line_length
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When registering an extension, we do that based on the line found inside the .ini file.
|
When registering an extension, we do that based on the line found inside the .ini file.
|
||||||
*/
|
*/
|
||||||
init(_ line: String, file: String) {
|
init(_ line: String, file: String) {
|
||||||
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
|
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
|
||||||
let match = regex.matches(in: line, options: [], range: NSMakeRange(0, line.count)).first
|
let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
|
||||||
let range = Range(match!.range(withName: "name"), in: line)!
|
let range = Range(match!.range(withName: "name"), in: line)!
|
||||||
|
|
||||||
self.line = line
|
self.line = line
|
||||||
@@ -69,7 +72,8 @@ class PhpExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply.
|
This simply toggles the extension in the .ini file.
|
||||||
|
You may need to restart the other services in order for this change to apply.
|
||||||
*/
|
*/
|
||||||
func toggle() {
|
func toggle() {
|
||||||
let newLine = enabled
|
let newLine = enabled
|
||||||
@@ -85,24 +89,26 @@ class PhpExtension {
|
|||||||
|
|
||||||
// MARK: - Static Methods
|
// MARK: - Static Methods
|
||||||
|
|
||||||
/**
|
static func from(_ lines: [String], filePath: String) -> [PhpExtension] {
|
||||||
This method will attempt to identify all extensions in the .ini file at a certain URL.
|
return lines.filter {
|
||||||
*/
|
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||||
static func load(from path: URL) -> [PhpExtension] {
|
}.map {
|
||||||
let file = try? String(contentsOf: path, encoding: .utf8)
|
return PhpExtension($0, file: filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (file == nil) {
|
static func from(filePath: String) -> [PhpExtension] {
|
||||||
|
let file = try? String(contentsOfFile: filePath)
|
||||||
|
|
||||||
|
if file == nil {
|
||||||
Log.err("There was an issue reading the file. Assuming no extensions were found.")
|
Log.err("There was an issue reading the file. Assuming no extensions were found.")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return file!.components(separatedBy: "\n")
|
return Self.from(
|
||||||
.filter {
|
file!.components(separatedBy: "\n"),
|
||||||
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
filePath: filePath
|
||||||
}
|
)
|
||||||
.map {
|
|
||||||
return PhpExtension($0, file: path.path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 28/11/2021.
|
// Created by Nico Verbruggen on 28/11/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 24/12/2021.
|
// Created by Nico Verbruggen on 24/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -20,21 +20,10 @@ class InternalSwitcher: PhpSwitcher {
|
|||||||
the version that is switched to may or may not be identical to `php`
|
the version that is switched to may or may not be identical to `php`
|
||||||
(without @version).
|
(without @version).
|
||||||
*/
|
*/
|
||||||
func performSwitch(to version: String, completion: @escaping () -> Void)
|
func performSwitch(to version: String, completion: @escaping () -> Void) {
|
||||||
{
|
|
||||||
Log.info("Switching to \(version), unlinking all versions...")
|
Log.info("Switching to \(version), unlinking all versions...")
|
||||||
|
|
||||||
let isolated = Valet.shared.sites.filter { site in
|
let versions = getVersionsToBeHandled(version)
|
||||||
site.isolatedPhpVersion != nil
|
|
||||||
}.map { site in
|
|
||||||
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
var versions: Set<String> = [version]
|
|
||||||
|
|
||||||
if (Valet.enabled(feature: .isolatedSites)) {
|
|
||||||
versions = versions.union(isolated)
|
|
||||||
}
|
|
||||||
|
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
@@ -64,15 +53,37 @@ 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"
|
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||||
if FileManager.default.fileExists(atPath: pool) {
|
if FileManager.default.fileExists(atPath: pool) {
|
||||||
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
|
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
|
||||||
let existing = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf")!
|
let existing = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf")!
|
||||||
let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
|
let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
|
||||||
do {
|
do {
|
||||||
if (FileManager.default.fileExists(atPath: new.path)) {
|
if FileManager.default.fileExists(atPath: new.path) {
|
||||||
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), cleaning up so the newer `www.conf` can be moved again.")
|
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
|
||||||
|
+ "cleaning up so the newer `www.conf` can be moved again.")
|
||||||
try FileManager.default.removeItem(at: new)
|
try FileManager.default.removeItem(at: new)
|
||||||
}
|
}
|
||||||
try FileManager.default.moveItem(at: existing, to: new)
|
try FileManager.default.moveItem(at: existing, to: new)
|
||||||
@@ -83,17 +94,17 @@ class InternalSwitcher: PhpSwitcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopPhpVersion(_ version: String) {
|
func stopPhpVersion(_ version: String) {
|
||||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||||
brew("unlink \(formula)")
|
brew("unlink \(formula)")
|
||||||
brew("services stop \(formula)", sudo: true)
|
brew("services stop \(formula)", sudo: true)
|
||||||
Log.info("Unlinked and stopped services for \(formula)")
|
Log.info("Unlinked and stopped services for \(formula)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startPhpVersion(_ version: String, primary: Bool) {
|
func startPhpVersion(_ version: String, primary: Bool) {
|
||||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||||
|
|
||||||
if (primary) {
|
if primary {
|
||||||
Log.info("\(formula) is the primary formula, linking and starting services...")
|
Log.info("\(formula) is the primary formula, linking and starting services...")
|
||||||
brew("link \(formula) --overwrite --force")
|
brew("link \(formula) --overwrite --force")
|
||||||
} else {
|
} else {
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 24/12/2021.
|
// Created by Nico Verbruggen on 24/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<br>
|
<br>
|
||||||
<p><b>Want to spread the love?</b> Leave a <a href="https://github.com/nicoverbruggen/phpmon">star on GitHub</a>!</p>
|
<p><b>Do you enjoy using the app?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
|
||||||
<p><b>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p>
|
<p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p>
|
||||||
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p>
|
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
||||||
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
|
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
|
||||||
<br>
|
<br>
|
||||||
</body>
|
</body>
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 05/12/2021.
|
// Created by Nico Verbruggen on 05/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -38,7 +38,7 @@ extension App {
|
|||||||
If there are no windows open, the app will be an accessory (toolbar) app.
|
If there are no windows open, the app will be an accessory (toolbar) app.
|
||||||
*/
|
*/
|
||||||
public func updateActivationPolicy() {
|
public func updateActivationPolicy() {
|
||||||
NSApp.setActivationPolicy(openWindows.count > 0 ? .regular : .accessory)
|
NSApp.setActivationPolicy(!openWindows.isEmpty ? .regular : .accessory)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 05/12/2021.
|
// Created by Nico Verbruggen on 05/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// StateManager.swift
|
// StateManager.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -21,6 +21,16 @@ class App {
|
|||||||
return "\(version) (\(build))"
|
return "\(version) (\(build))"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Just the bundle version (build). */
|
||||||
|
static var bundleVersion: String {
|
||||||
|
return Bundle.main.infoDictionary?["CFBundleVersion"] as! String
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Just the version number. */
|
||||||
|
static var shortVersion: String {
|
||||||
|
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
|
||||||
|
}
|
||||||
|
|
||||||
static var architecture: String {
|
static var architecture: String {
|
||||||
var systeminfo = utsname()
|
var systeminfo = utsname()
|
||||||
uname(&systeminfo)
|
uname(&systeminfo)
|
||||||
@@ -41,14 +51,26 @@ class App {
|
|||||||
var preferences: [PreferenceName: Bool]!
|
var preferences: [PreferenceName: Bool]!
|
||||||
|
|
||||||
/** The window controller of the currently active preferences window. */
|
/** The window controller of the currently active preferences window. */
|
||||||
var preferencesWindowController: PrefsWC? = nil
|
var preferencesWindowController: PreferencesWindowController?
|
||||||
|
|
||||||
/** The window controller of the currently active site list window. */
|
/** The window controller of the currently active site list window. */
|
||||||
var siteListWindowController: SiteListWC? = nil
|
var domainListWindowController: DomainListWindowController?
|
||||||
|
|
||||||
|
/** The window controller of the onboarding window. */
|
||||||
|
var onboardingWindowController: OnboardingWindowController?
|
||||||
|
|
||||||
|
/** The window controller of the warnings window. */
|
||||||
|
var warningsWindowController: WarningsWindowController?
|
||||||
|
|
||||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||||
var detectedApplications: [Application] = []
|
var detectedApplications: [Application] = []
|
||||||
|
|
||||||
|
/** The services manager, responsible for figuring out what services are active/inactive. */
|
||||||
|
var services = ServicesManager.shared
|
||||||
|
|
||||||
|
/** The warning manager, responsible for keeping track of warnings. */
|
||||||
|
var warnings = WarningManager.shared
|
||||||
|
|
||||||
/** Timer that will periodically reload info about the user's PHP installation. */
|
/** Timer that will periodically reload info about the user's PHP installation. */
|
||||||
var timer: Timer?
|
var timer: Timer?
|
||||||
|
|
||||||
@@ -57,7 +79,7 @@ class App {
|
|||||||
/**
|
/**
|
||||||
The shortcut the user has requested.
|
The shortcut the user has requested.
|
||||||
*/
|
*/
|
||||||
var shortcutHotkey: HotKey? = nil {
|
var shortcutHotkey: HotKey? {
|
||||||
didSet {
|
didSet {
|
||||||
setupGlobalHotkeyListener()
|
setupGlobalHotkeyListener()
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 20/12/2021.
|
// Created by Nico Verbruggen on 20/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -44,4 +44,3 @@ extension AppDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 05/12/2021.
|
// Created by Nico Verbruggen on 05/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -26,19 +26,19 @@ extension AppDelegate {
|
|||||||
// MARK: - Menu Interactions
|
// MARK: - Menu Interactions
|
||||||
|
|
||||||
@IBAction func addSiteLinkPressed(_ sender: Any) {
|
@IBAction func addSiteLinkPressed(_ sender: Any) {
|
||||||
SiteListVC.show()
|
DomainListVC.show()
|
||||||
|
|
||||||
guard let windowController = App.shared.siteListWindowController else { return }
|
guard let windowController = App.shared.domainListWindowController else { return }
|
||||||
windowController.pressedAddLink(nil)
|
windowController.pressedAddLink(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func reloadSiteListPressed(_ sender: Any) {
|
@IBAction func reloadDomainListPressed(_ sender: Any) {
|
||||||
let vc = App.shared.siteListWindowController?
|
let vc = App.shared.domainListWindowController?
|
||||||
.window?.contentViewController as? SiteListVC
|
.window?.contentViewController as? DomainListVC
|
||||||
|
|
||||||
if vc != nil {
|
if vc != nil {
|
||||||
// If the view exists, directly reload the list of sites
|
// If the view exists, directly reload the list of sites
|
||||||
vc!.reloadSites()
|
vc!.reloadDomains()
|
||||||
} else {
|
} else {
|
||||||
// If the view does not exist, reload the cached data that was populated when the app initially launched.
|
// If the view does not exist, reload the cached data that was populated when the app initially launched.
|
||||||
Valet.shared.reloadSites()
|
Valet.shared.reloadSites()
|
||||||
@@ -46,9 +46,9 @@ extension AppDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func focusSearchField(_ sender: Any) {
|
@IBAction func focusSearchField(_ sender: Any) {
|
||||||
SiteListVC.show()
|
DomainListVC.show()
|
||||||
|
|
||||||
guard let windowController = App.shared.siteListWindowController else { return }
|
guard let windowController = App.shared.domainListWindowController else { return }
|
||||||
windowController.searchToolbarItem.searchField.becomeFirstResponder()
|
windowController.searchToolbarItem.searchField.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Created by Nico Verbruggen on 06/12/2021.
|
// Created by Nico Verbruggen on 06/12/2021.
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// AppDelegate.swift
|
// AppDelegate.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
@@ -67,6 +67,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
logger.verbosity = .performance
|
logger.verbosity = .performance
|
||||||
#endif
|
#endif
|
||||||
|
if CommandLine.arguments.contains("--v") {
|
||||||
|
logger.verbosity = .performance
|
||||||
|
Log.info("Extra verbose mode has been activated.")
|
||||||
|
}
|
||||||
Log.separator(as: .info)
|
Log.separator(as: .info)
|
||||||
Log.info("PHP MONITOR by Nico Verbruggen")
|
Log.info("PHP MONITOR by Nico Verbruggen")
|
||||||
Log.info("Version \(App.version)")
|
Log.info("Version \(App.version)")
|
||||||
|
182
phpmon/Domain/App/AppUpdateChecker.swift
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
//
|
||||||
|
// Updater.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 09/05/2022.
|
||||||
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
class AppUpdateChecker {
|
||||||
|
|
||||||
|
public static var enabled: Bool = {
|
||||||
|
return Preferences.isEnabled(.automaticBackgroundUpdateCheck)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public static var isDev: Bool = {
|
||||||
|
return App.version.contains("-dev")
|
||||||
|
}()
|
||||||
|
|
||||||
|
public static func retrieveVersionFromCask(
|
||||||
|
_ initiatedFromBackground: Bool = true
|
||||||
|
) -> String {
|
||||||
|
let caskFile = App.version.contains("-dev")
|
||||||
|
? Constants.Urls.DevBuildCaskFile.absoluteString
|
||||||
|
: Constants.Urls.StableBuildCaskFile.absoluteString
|
||||||
|
|
||||||
|
var command = "curl -s"
|
||||||
|
|
||||||
|
if initiatedFromBackground {
|
||||||
|
command = "curl -s --max-time 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
return Shell.pipe(
|
||||||
|
"\(command) '\(caskFile)' | grep version"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func checkIfNewerVersionIsAvailable(
|
||||||
|
initiatedFromBackground: Bool = true
|
||||||
|
) {
|
||||||
|
if initiatedFromBackground {
|
||||||
|
if !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
|
||||||
|
Log.info("Automatic updates are disabled. No check will be performed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info("Automatic updates are enabled, a check will be performed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let versionString = retrieveVersionFromCask(initiatedFromBackground)
|
||||||
|
|
||||||
|
guard let onlineVersion = AppVersion.from(versionString) else {
|
||||||
|
Log.err("We couldn't check for updates!")
|
||||||
|
|
||||||
|
// Only notify about connection issues if the request to check for updates was explicit
|
||||||
|
if !initiatedFromBackground {
|
||||||
|
notifyAboutConnectionIssue()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentVersion = AppVersion.fromCurrentVersion()
|
||||||
|
|
||||||
|
handleVersionComparison(
|
||||||
|
currentVersion,
|
||||||
|
onlineVersion,
|
||||||
|
initiatedFromBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func handleVersionComparison(
|
||||||
|
_ currentVersion: AppVersion,
|
||||||
|
_ onlineVersion: AppVersion,
|
||||||
|
_ background: Bool
|
||||||
|
) {
|
||||||
|
switch onlineVersion.version.versionCompare(currentVersion.version) {
|
||||||
|
case .orderedAscending:
|
||||||
|
Log.info("You are running a newer version of PHP Monitor "
|
||||||
|
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
|
||||||
|
if !background { notifyVersionDoesNotNeedUpgrade() }
|
||||||
|
case .orderedDescending:
|
||||||
|
Log.info("There is a newer version (\(onlineVersion)) available! "
|
||||||
|
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
|
||||||
|
notifyAboutNewerVersion(version: onlineVersion)
|
||||||
|
case .orderedSame:
|
||||||
|
if currentVersion.build != nil
|
||||||
|
&& onlineVersion.build != nil
|
||||||
|
&& buildDiffers(currentVersion, onlineVersion, background) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info("The installed version (\(currentVersion.computerReadable)) matches the latest release "
|
||||||
|
+ "(\(onlineVersion.computerReadable)).")
|
||||||
|
if !background { notifyVersionDoesNotNeedUpgrade() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func buildDiffers(
|
||||||
|
_ currentVersion: AppVersion,
|
||||||
|
_ onlineVersion: AppVersion,
|
||||||
|
_ background: Bool
|
||||||
|
) -> Bool {
|
||||||
|
if Int(onlineVersion.build!)! > Int(currentVersion.build!)! {
|
||||||
|
Log.info("There is a newer build of PHP Monitor available! "
|
||||||
|
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
|
||||||
|
notifyAboutNewerVersion(version: onlineVersion)
|
||||||
|
return true
|
||||||
|
} else if Int(onlineVersion.build!)! < Int(currentVersion.build!)! {
|
||||||
|
Log.info("You are running a newer build of PHP Monitor "
|
||||||
|
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
|
||||||
|
if !background { notifyVersionDoesNotNeedUpgrade() }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func notifyVersionDoesNotNeedUpgrade() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
BetterAlert().withInformation(
|
||||||
|
title: "updater.alerts.is_latest_version.title".localized,
|
||||||
|
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
|
||||||
|
description: ""
|
||||||
|
)
|
||||||
|
.withPrimary(text: "OK")
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func notifyAboutNewerVersion(version: AppVersion) {
|
||||||
|
let devSuffix = isDev ? "-dev" : ""
|
||||||
|
let command = isDev ? "brew upgrade phpmon-dev" : "brew upgrade phpmon"
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
BetterAlert().withInformation(
|
||||||
|
title: "updater.alerts.newer_version_available.title".localized(version.humanReadable),
|
||||||
|
subtitle: "updater.alerts.newer_version_available.subtitle".localized,
|
||||||
|
description: HomebrewDiagnostics.customCaskInstalled
|
||||||
|
? "updater.installation_source.brew".localized(command)
|
||||||
|
: "updater.installation_source.direct".localized
|
||||||
|
)
|
||||||
|
.withPrimary(
|
||||||
|
text: "updater.alerts.buttons.release_notes".localized,
|
||||||
|
action: { vc in
|
||||||
|
vc.close(with: .OK)
|
||||||
|
|
||||||
|
NSWorkspace.shared.open(
|
||||||
|
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version.tagged)\(devSuffix)")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.withTertiary(text: "Dismiss", action: { vc in
|
||||||
|
vc.close(with: .OK)
|
||||||
|
})
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func notifyAboutConnectionIssue() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
BetterAlert().withInformation(
|
||||||
|
title: "updater.alerts.cannot_check_for_update.title".localized,
|
||||||
|
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
|
||||||
|
description: "updater.alerts.cannot_check_for_update.description".localized(
|
||||||
|
App.version
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.withTertiary(
|
||||||
|
text: "updater.alerts.buttons.releases_on_github".localized,
|
||||||
|
action: { _ in
|
||||||
|
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.withPrimary(text: "OK")
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
85
phpmon/Domain/App/AppVersion.swift
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
//
|
||||||
|
// AppVersion.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 10/05/2022.
|
||||||
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class AppVersion {
|
||||||
|
var version: String
|
||||||
|
var build: String?
|
||||||
|
var suffix: String?
|
||||||
|
|
||||||
|
init(version: String, build: String?, suffix: String? = nil) {
|
||||||
|
self.version = version
|
||||||
|
self.build = build
|
||||||
|
self.suffix = suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func from(_ string: String) -> AppVersion? {
|
||||||
|
do {
|
||||||
|
let regex = try NSRegularExpression(
|
||||||
|
pattern: #"(?<version>(\d+)[.](\d+)([.](\d+))?)(-(?<suffix>[a-z]+)){0,1}((,|_)(?<build>\d+)){0,1}"#,
|
||||||
|
options: []
|
||||||
|
)
|
||||||
|
|
||||||
|
let match = regex.matches(
|
||||||
|
in: string,
|
||||||
|
options: [],
|
||||||
|
range: NSRange(location: 0, length: string.count)
|
||||||
|
).first
|
||||||
|
|
||||||
|
guard let match = match else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var version: String = ""
|
||||||
|
var build: String?
|
||||||
|
var suffix: String?
|
||||||
|
|
||||||
|
if let versionRange = Range(match.range(withName: "version"), in: string) {
|
||||||
|
version = String(string[versionRange])
|
||||||
|
}
|
||||||
|
|
||||||
|
if let buildRange = Range(match.range(withName: "build"), in: string) {
|
||||||
|
build = String(string[buildRange])
|
||||||
|
}
|
||||||
|
|
||||||
|
if let suffixRange = Range(match.range(withName: "suffix"), in: string) {
|
||||||
|
suffix = String(string[suffixRange])
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppVersion(
|
||||||
|
version: version,
|
||||||
|
build: build,
|
||||||
|
suffix: suffix
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func fromCurrentVersion() -> AppVersion {
|
||||||
|
return AppVersion.from("\(App.shortVersion)_\(App.bundleVersion)")!
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagged: String {
|
||||||
|
if version.suffix(2) == ".0" && version.count > 3 {
|
||||||
|
return String(version.dropLast(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
var computerReadable: String {
|
||||||
|
return "\(version)_\(build ?? "0")"
|
||||||
|
}
|
||||||
|
|
||||||
|
var humanReadable: String {
|
||||||
|
return "\(version) (\(build ?? "???"))"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -60,16 +60,16 @@
|
|||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem title="reload-list" keyEquivalent="r" id="Ema-AU-Nbr" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
<menuItem title="reload-list" keyEquivalent="r" id="Ema-AU-Nbr" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<userDefinedRuntimeAttributes>
|
<userDefinedRuntimeAttributes>
|
||||||
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_site_list"/>
|
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_domain_list"/>
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="reloadSiteListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
|
<action selector="reloadDomainListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
|
||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem isSeparatorItem="YES" id="2ux-8Q-UjK"/>
|
<menuItem isSeparatorItem="YES" id="2ux-8Q-UjK"/>
|
||||||
<menuItem title="focus-find" keyEquivalent="f" id="I95-fb-EL7" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
<menuItem title="focus-find" keyEquivalent="f" id="I95-fb-EL7" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<userDefinedRuntimeAttributes>
|
<userDefinedRuntimeAttributes>
|
||||||
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_site_list"/>
|
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_domain_list"/>
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
||||||
@@ -319,41 +319,37 @@
|
|||||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
|
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="-495" y="-44"/>
|
<point key="canvasLocation" x="-360" y="-94"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Window Controller-->
|
<!--Window Controller-->
|
||||||
<scene sceneID="PQa-AT-b2a">
|
<scene sceneID="PQa-AT-b2a">
|
||||||
<objects>
|
<objects>
|
||||||
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PrefsWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PreferencesWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="h4c-3b-nko">
|
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="h4c-3b-nko">
|
||||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
<rect key="contentRect" x="372" y="403" width="480" height="270"/>
|
<rect key="contentRect" x="372" y="403" width="550" height="270"/>
|
||||||
<rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/>
|
<rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/>
|
||||||
<view key="contentView" id="2yL-50-11x">
|
<view key="contentView" id="2yL-50-11x">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
<rect key="frame" x="0.0" y="0.0" width="550" height="270"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</view>
|
</view>
|
||||||
<toolbar key="toolbar" implicitIdentifier="611E3485-DC7F-46A0-8528-11CF9366370C" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconAndLabel" sizeMode="regular" id="fcq-wR-7iv">
|
|
||||||
<allowedToolbarItems/>
|
|
||||||
<defaultToolbarItems/>
|
|
||||||
</toolbar>
|
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/>
|
<outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/>
|
||||||
</connections>
|
</connections>
|
||||||
</window>
|
</window>
|
||||||
<connections>
|
<connections>
|
||||||
<segue destination="AW2-rV-rbS" kind="relationship" relationship="window.shadowedContentViewController" id="3dX-9V-eA0"/>
|
<segue destination="PCI-2c-55Y" kind="relationship" relationship="window.shadowedContentViewController" id="egC-A4-am8"/>
|
||||||
</connections>
|
</connections>
|
||||||
</windowController>
|
</windowController>
|
||||||
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="-374" y="327"/>
|
<point key="canvasLocation" x="-374" y="238"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Preferences-->
|
<!--Preferences-->
|
||||||
<scene sceneID="iyi-IS-7Ps">
|
<scene sceneID="iyi-IS-7Ps">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController title="Preferences" identifier="preferencesTemplateVC" storyboardIdentifier="preferencesTemplateVC" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="GenericPreferenceVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" wantsLayer="YES" id="Pf1-A5-3Xz">
|
<view key="view" wantsLayer="YES" id="Pf1-A5-3Xz">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="550" height="498"/>
|
<rect key="frame" x="0.0" y="0.0" width="550" height="498"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
@@ -378,13 +374,33 @@
|
|||||||
</viewController>
|
</viewController>
|
||||||
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="251" y="205"/>
|
<point key="canvasLocation" x="844" y="-153"/>
|
||||||
|
</scene>
|
||||||
|
<!--Tab View Controller-->
|
||||||
|
<scene sceneID="B5x-d3-c7D">
|
||||||
|
<objects>
|
||||||
|
<customObject id="pNW-tM-SQu" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
<tabViewController tabStyle="toolbar" canPropagateSelectedChildViewControllerTitle="NO" id="PCI-2c-55Y" sceneMemberID="viewController">
|
||||||
|
<tabView key="tabView" type="noTabsNoBorder" id="l0U-9a-nM6">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="508" height="300"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<font key="font" metaFont="message"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="PCI-2c-55Y" id="6gR-GR-cwq"/>
|
||||||
|
</connections>
|
||||||
|
</tabView>
|
||||||
|
<connections>
|
||||||
|
<outlet property="tabView" destination="l0U-9a-nM6" id="tfn-UN-1Aa"/>
|
||||||
|
</connections>
|
||||||
|
</tabViewController>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="283" y="-252"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Window Controller-->
|
<!--Window Controller-->
|
||||||
<scene sceneID="4XS-kY-YIS">
|
<scene sceneID="4XS-kY-YIS">
|
||||||
<objects>
|
<objects>
|
||||||
<windowController storyboardIdentifier="siteListWindow" id="8Ec-9q-82s" customClass="SiteListWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
<windowController storyboardIdentifier="domainListWindow" id="8Ec-9q-82s" customClass="DomainListWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<window key="window" title="Domains" subtitle="Linked & Parked" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
|
<window key="window" separatorStyle="line" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
|
||||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
|
||||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
<rect key="contentRect" x="425" y="461" width="600" height="263"/>
|
<rect key="contentRect" x="425" y="461" width="600" height="263"/>
|
||||||
@@ -437,7 +453,7 @@
|
|||||||
</windowController>
|
</windowController>
|
||||||
<customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="-374" y="746"/>
|
<point key="canvasLocation" x="-374" y="745.5"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Window Controller-->
|
<!--Window Controller-->
|
||||||
<scene sceneID="HTI-x5-rOp">
|
<scene sceneID="HTI-x5-rOp">
|
||||||
@@ -462,7 +478,7 @@
|
|||||||
</windowController>
|
</windowController>
|
||||||
<customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="-409" y="1137"/>
|
<point key="canvasLocation" x="-374" y="1137"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Window Controller-->
|
<!--Window Controller-->
|
||||||
<scene sceneID="BD0-La-ygq">
|
<scene sceneID="BD0-La-ygq">
|
||||||
@@ -486,7 +502,7 @@
|
|||||||
</windowController>
|
</windowController>
|
||||||
<customObject id="i3j-z8-nxv" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="i3j-z8-nxv" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="-575" y="1624"/>
|
<point key="canvasLocation" x="-374" y="2267"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Better AlertVC-->
|
<!--Better AlertVC-->
|
||||||
<scene sceneID="y9E-bB-wIG">
|
<scene sceneID="y9E-bB-wIG">
|
||||||
@@ -532,7 +548,7 @@ Gw
|
|||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j">
|
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j">
|
||||||
<rect key="frame" x="13" y="13" width="82" height="32"/>
|
<rect key="frame" x="13" y="13" width="81" height="32"/>
|
||||||
<buttonCell key="cell" type="push" title="Tertiary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="mzA-Uu-gyf">
|
<buttonCell key="cell" type="push" title="Tertiary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="mzA-Uu-gyf">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@@ -632,27 +648,27 @@ Gw
|
|||||||
</viewController>
|
</viewController>
|
||||||
<customObject id="5Ts-EZ-bJh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="5Ts-EZ-bJh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="38" y="1624"/>
|
<point key="canvasLocation" x="230" y="2267"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Add SiteVC-->
|
<!--Add SiteVC-->
|
||||||
<scene sceneID="6JC-H6-u4K">
|
<scene sceneID="6JC-H6-u4K">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="newSiteLink" id="glS-wF-sEU" customClass="AddSiteVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="newSiteLink" id="glS-wF-sEU" customClass="AddSiteVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" id="JJJ-T9-Yuv">
|
<view key="view" id="JJJ-T9-Yuv">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
|
<rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="js9-OW-xzC">
|
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="js9-OW-xzC">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
|
<rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
|
||||||
<view key="contentView" id="HRC-RT-LxR">
|
<view key="contentView" id="HRC-RT-LxR">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
|
<rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
</view>
|
</view>
|
||||||
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</box>
|
</box>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
|
||||||
<rect key="frame" x="363" y="13" width="104" height="32"/>
|
<rect key="frame" x="326" y="13" width="141" height="32"/>
|
||||||
<buttonCell key="cell" type="push" title="Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
|
<buttonCell key="cell" type="push" title="[i18n] Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
<string key="keyEquivalent" base64-UTF8="YES">
|
<string key="keyEquivalent" base64-UTF8="YES">
|
||||||
@@ -664,11 +680,11 @@ DQ
|
|||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
||||||
<rect key="frame" x="13" y="13" width="94" height="32"/>
|
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
|
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
<string key="keyEquivalent" base64-UTF8="YES">
|
<string key="keyEquivalent" base64-UTF8="YES">
|
||||||
@@ -680,8 +696,8 @@ Gw
|
|||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
|
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
|
||||||
<rect key="frame" x="20" y="156" width="440" height="21"/>
|
<rect key="frame" x="20" y="150" width="440" height="21"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a potential domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
@@ -691,16 +707,16 @@ Gw
|
|||||||
</connections>
|
</connections>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
|
||||||
<rect key="frame" x="18" y="134" width="444" height="14"/>
|
<rect key="frame" x="18" y="128" width="444" height="14"/>
|
||||||
<textFieldCell key="cell" title="FOLDER_AVAILABLE" id="bJr-s6-tdP">
|
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KZf-b0-9cm">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KZf-b0-9cm">
|
||||||
<rect key="frame" x="18" y="101" width="227" height="18"/>
|
<rect key="frame" x="18" y="95" width="266" height="18"/>
|
||||||
<buttonCell key="cell" type="check" title="Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ">
|
<buttonCell key="cell" type="check" title="[i18n] Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ">
|
||||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
@@ -709,31 +725,31 @@ Gw
|
|||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
|
||||||
<rect key="frame" x="18" y="66" width="444" height="28"/>
|
<rect key="frame" x="18" y="60" width="444" height="28"/>
|
||||||
<textFieldCell key="cell" title="Securing a site requires administrative privileges.
You will be prompted for your password or Touch ID." id="4gd-KM-5Fu">
|
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="4gd-KM-5Fu">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6JT-Vt-3q0">
|
<pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6JT-Vt-3q0">
|
||||||
<rect key="frame" x="20" y="185" width="440" height="22"/>
|
<rect key="frame" x="20" y="179" width="440" height="22"/>
|
||||||
<pathCell key="cell" selectable="YES" refusesFirstResponder="YES" alignment="left" id="m8d-XF-kh9">
|
<pathCell key="cell" selectable="YES" refusesFirstResponder="YES" alignment="left" id="m8d-XF-kh9">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
<url key="url" string="file:///Users/"/>
|
<url key="url" string="file:///Users/"/>
|
||||||
</pathCell>
|
</pathCell>
|
||||||
</pathControl>
|
</pathControl>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
|
||||||
<rect key="frame" x="18" y="215" width="87" height="16"/>
|
<rect key="frame" x="18" y="209" width="128" height="16"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Link a Folder" id="S4j-ZC-ddT">
|
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
|
||||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
||||||
<rect key="frame" x="229" y="23" width="128" height="14"/>
|
<rect key="frame" x="140" y="23" width="180" height="14"/>
|
||||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That link already exists." id="jOt-n6-TQf">
|
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
@@ -752,6 +768,7 @@ Gw
|
|||||||
<constraint firstItem="900-Z2-tID" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="SwS-o8-pbl" secondAttribute="trailing" constant="8" symbolic="YES" id="IMv-ZD-VXf"/>
|
<constraint firstItem="900-Z2-tID" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="SwS-o8-pbl" secondAttribute="trailing" constant="8" symbolic="YES" id="IMv-ZD-VXf"/>
|
||||||
<constraint firstItem="js9-OW-xzC" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" id="IpM-ot-dBG"/>
|
<constraint firstItem="js9-OW-xzC" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" id="IpM-ot-dBG"/>
|
||||||
<constraint firstItem="VzR-5a-cmT" firstAttribute="leading" secondItem="ZX9-s1-23i" secondAttribute="leading" id="UPN-Ad-j3X"/>
|
<constraint firstItem="VzR-5a-cmT" firstAttribute="leading" secondItem="ZX9-s1-23i" secondAttribute="leading" id="UPN-Ad-j3X"/>
|
||||||
|
<constraint firstItem="SwS-o8-pbl" firstAttribute="top" secondItem="mmQ-7e-dlb" secondAttribute="bottom" constant="20" id="VNW-fB-2Xj"/>
|
||||||
<constraint firstItem="KZf-b0-9cm" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="Vab-wq-9Nc"/>
|
<constraint firstItem="KZf-b0-9cm" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="Vab-wq-9Nc"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="PVw-cM-qAB" secondAttribute="bottom" constant="20" symbolic="YES" id="VsP-Q0-zRW"/>
|
<constraint firstAttribute="bottom" secondItem="PVw-cM-qAB" secondAttribute="bottom" constant="20" symbolic="YES" id="VsP-Q0-zRW"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="PVw-cM-qAB" secondAttribute="trailing" constant="20" symbolic="YES" id="X5z-G4-CBv"/>
|
<constraint firstAttribute="trailing" secondItem="PVw-cM-qAB" secondAttribute="trailing" constant="20" symbolic="YES" id="X5z-G4-CBv"/>
|
||||||
@@ -778,7 +795,7 @@ Gw
|
|||||||
<outlet property="buttonCancel" destination="SwS-o8-pbl" id="N1v-uy-2Mi"/>
|
<outlet property="buttonCancel" destination="SwS-o8-pbl" id="N1v-uy-2Mi"/>
|
||||||
<outlet property="buttonCreateLink" destination="PVw-cM-qAB" id="0Oo-xW-He7"/>
|
<outlet property="buttonCreateLink" destination="PVw-cM-qAB" id="0Oo-xW-He7"/>
|
||||||
<outlet property="buttonSecure" destination="KZf-b0-9cm" id="5A7-Bn-NB7"/>
|
<outlet property="buttonSecure" destination="KZf-b0-9cm" id="5A7-Bn-NB7"/>
|
||||||
<outlet property="linkName" destination="ZX9-s1-23i" id="yT6-80-Zr1"/>
|
<outlet property="inputDomainName" destination="ZX9-s1-23i" id="yT6-80-Zr1"/>
|
||||||
<outlet property="pathControl" destination="6JT-Vt-3q0" id="f5K-8h-VOd"/>
|
<outlet property="pathControl" destination="6JT-Vt-3q0" id="f5K-8h-VOd"/>
|
||||||
<outlet property="previewText" destination="VzR-5a-cmT" id="qwd-wX-645"/>
|
<outlet property="previewText" destination="VzR-5a-cmT" id="qwd-wX-645"/>
|
||||||
<outlet property="textFieldError" destination="900-Z2-tID" id="qUk-FE-IKW"/>
|
<outlet property="textFieldError" destination="900-Z2-tID" id="qUk-FE-IKW"/>
|
||||||
@@ -788,12 +805,12 @@ Gw
|
|||||||
</viewController>
|
</viewController>
|
||||||
<customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="191" y="1098.5"/>
|
<point key="canvasLocation" x="210" y="1128"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Site ListVC-->
|
<!--Domain ListVC-->
|
||||||
<scene sceneID="aZt-6w-TFl">
|
<scene sceneID="aZt-6w-TFl">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController identifier="siteList" storyboardIdentifier="siteList" id="JZI-Vd-9oq" customClass="SiteListVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController identifier="domainList" storyboardIdentifier="domainList" id="JZI-Vd-9oq" customClass="DomainListVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" id="rIZ-4U-bhj">
|
<view key="view" id="rIZ-4U-bhj">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
@@ -804,8 +821,8 @@ Gw
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj">
|
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="662" height="281"/>
|
<rect key="frame" x="0.0" y="0.0" width="626" height="281"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<size key="intercellSpacing" width="17" height="0.0"/>
|
<size key="intercellSpacing" width="17" height="0.0"/>
|
||||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
@@ -825,7 +842,7 @@ Gw
|
|||||||
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Secure"/>
|
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Secure"/>
|
||||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||||
<prototypeCellViews>
|
<prototypeCellViews>
|
||||||
<tableCellView identifier="siteListTLSCell" id="hft-M4-nWb" customClass="SiteListTLSCell" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableCellView identifier="domainListTLSCell" id="hft-M4-nWb" customClass="DomainListTLSCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="18" y="0.0" width="34" height="55"/>
|
<rect key="frame" x="18" y="0.0" width="34" height="55"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
@@ -848,7 +865,7 @@ Gw
|
|||||||
</tableCellView>
|
</tableCellView>
|
||||||
</prototypeCellViews>
|
</prototypeCellViews>
|
||||||
</tableColumn>
|
</tableColumn>
|
||||||
<tableColumn identifier="DOMAIN" width="290" minWidth="250" maxWidth="10000" id="oeH-B2-0rA">
|
<tableColumn identifier="DOMAIN" width="200" minWidth="200" maxWidth="10000" id="oeH-B2-0rA">
|
||||||
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Domain">
|
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Domain">
|
||||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
|
||||||
@@ -861,8 +878,8 @@ Gw
|
|||||||
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Domain"/>
|
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Domain"/>
|
||||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||||
<prototypeCellViews>
|
<prototypeCellViews>
|
||||||
<tableCellView identifier="siteListNameCell" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableCellView identifier="domainListNameCell" wantsLayer="YES" id="5GY-nN-BWd" customClass="DomainListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="69" y="0.0" width="290" height="54"/>
|
<rect key="frame" x="69" y="0.0" width="200" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
||||||
@@ -910,8 +927,8 @@ Gw
|
|||||||
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="PHP"/>
|
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="PHP"/>
|
||||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||||
<prototypeCellViews>
|
<prototypeCellViews>
|
||||||
<tableCellView identifier="siteListPhpCell" wantsLayer="YES" id="T49-0U-d58" customClass="SiteListPhpCell" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableCellView identifier="domainListPhpCell" wantsLayer="YES" id="T49-0U-d58" customClass="DomainListPhpCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="376" y="0.0" width="100" height="54"/>
|
<rect key="frame" x="286" y="0.0" width="100" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
|
||||||
@@ -965,8 +982,8 @@ Gw
|
|||||||
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Kind"/>
|
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Kind"/>
|
||||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||||
<prototypeCellViews>
|
<prototypeCellViews>
|
||||||
<tableCellView identifier="siteListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="SiteListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableCellView identifier="domainListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="DomainListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="493" y="0.0" width="36" height="54"/>
|
<rect key="frame" x="403" y="0.0" width="36" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
|
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
|
||||||
@@ -1002,8 +1019,8 @@ Gw
|
|||||||
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Type"/>
|
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Type"/>
|
||||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||||
<prototypeCellViews>
|
<prototypeCellViews>
|
||||||
<tableCellView identifier="siteListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="SiteListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
|
<tableCellView identifier="domainListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="DomainListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||||
<rect key="frame" x="546" y="0.0" width="97" height="54"/>
|
<rect key="frame" x="456" y="0.0" width="97" height="54"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
||||||
@@ -1060,7 +1077,7 @@ Gw
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
|
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="662" height="28"/>
|
<rect key="frame" x="0.0" y="0.0" width="626" height="28"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</tableHeaderView>
|
</tableHeaderView>
|
||||||
</scrollView>
|
</scrollView>
|
||||||
@@ -1088,12 +1105,377 @@ Gw
|
|||||||
</viewController>
|
</viewController>
|
||||||
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="388" y="715.5"/>
|
<point key="canvasLocation" x="323" y="723"/>
|
||||||
|
</scene>
|
||||||
|
<!--Add ProxyVC-->
|
||||||
|
<scene sceneID="g8z-pE-RL9">
|
||||||
|
<objects>
|
||||||
|
<viewController storyboardIdentifier="newProxyLink" id="dwh-CF-6iv" customClass="AddProxyVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" id="U5U-QR-YXS">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="kkd-UV-SnA">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
|
||||||
|
<view key="contentView" id="IXW-35-8NJ">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
|
||||||
|
<rect key="frame" x="20" y="196" width="440" height="21"/>
|
||||||
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
|
||||||
|
</connections>
|
||||||
|
</textField>
|
||||||
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
|
||||||
|
<rect key="frame" x="18" y="221" width="325" height="14"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Proxy subject (usually: protocol, IP address and port)" id="G1Z-3f-BhL">
|
||||||
|
<font key="font" metaFont="systemMedium" size="11"/>
|
||||||
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
|
||||||
|
<rect key="frame" x="18" y="172" width="112" height="14"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
|
||||||
|
<font key="font" metaFont="systemMedium" size="11"/>
|
||||||
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
|
||||||
|
<rect key="frame" x="20" y="147" width="440" height="21"/>
|
||||||
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="dwh-CF-6iv" id="e9n-PM-7s8"/>
|
||||||
|
</connections>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="SNw-oQ-bnb" secondAttribute="trailing" constant="20" id="2ui-Jg-BUV"/>
|
||||||
|
<constraint firstItem="mlA-Zt-Hu8" firstAttribute="top" secondItem="QCK-Z9-w7g" secondAttribute="bottom" constant="10" id="8sn-dT-SW6"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Uib-vA-HRc" secondAttribute="trailing" constant="20" symbolic="YES" id="Cue-3e-doM"/>
|
||||||
|
<constraint firstItem="QCK-Z9-w7g" firstAttribute="leading" secondItem="SNw-oQ-bnb" secondAttribute="leading" id="N1K-69-wLz"/>
|
||||||
|
<constraint firstItem="mlA-Zt-Hu8" firstAttribute="leading" secondItem="QCK-Z9-w7g" secondAttribute="leading" id="R74-k0-96U"/>
|
||||||
|
<constraint firstItem="SNw-oQ-bnb" firstAttribute="leading" secondItem="IXW-35-8NJ" secondAttribute="leading" constant="20" id="WZR-f8-mgf"/>
|
||||||
|
<constraint firstItem="SNw-oQ-bnb" firstAttribute="top" secondItem="mlA-Zt-Hu8" secondAttribute="bottom" constant="4" id="XDn-h9-dgp"/>
|
||||||
|
<constraint firstItem="QCK-Z9-w7g" firstAttribute="top" secondItem="Uib-vA-HRc" secondAttribute="bottom" constant="4" id="fGU-al-B0w"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="mlA-Zt-Hu8" secondAttribute="trailing" constant="20" symbolic="YES" id="uFE-cU-KOg"/>
|
||||||
|
<constraint firstItem="QCK-Z9-w7g" firstAttribute="trailing" secondItem="SNw-oQ-bnb" secondAttribute="trailing" id="xQE-yY-gPd"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</box>
|
||||||
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Vi-cN-ude">
|
||||||
|
<rect key="frame" x="317" y="13" width="150" height="32"/>
|
||||||
|
<buttonCell key="cell" type="push" title="[i18n] Create Proxy" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="H2Z-c5-5Vk">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<string key="keyEquivalent" base64-UTF8="YES">
|
||||||
|
DQ
|
||||||
|
</string>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="pressedCreateProxy:" target="dwh-CF-6iv" id="wFW-Aw-FOR"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
|
||||||
|
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="uCc-fF-wS2"/>
|
||||||
|
</constraints>
|
||||||
|
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="D8g-GE-7TU">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<string key="keyEquivalent" base64-UTF8="YES">
|
||||||
|
Gw
|
||||||
|
</string>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
|
||||||
|
<rect key="frame" x="18" y="128" width="444" height="14"/>
|
||||||
|
<textFieldCell key="cell" title="[i18n] Preview text here" id="ISE-9R-ncQ">
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rJa-yg-nCn">
|
||||||
|
<rect key="frame" x="18" y="95" width="170" height="18"/>
|
||||||
|
<buttonCell key="cell" type="check" title="[i18n] Secure this proxy" bezelStyle="regularSquare" imagePosition="left" inset="2" id="5LI-lt-Asl">
|
||||||
|
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="pressedSecure:" target="dwh-CF-6iv" id="b74-8T-AzO"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
|
||||||
|
<rect key="frame" x="18" y="60" width="444" height="28"/>
|
||||||
|
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
|
||||||
|
<rect key="frame" x="18" y="250" width="123" height="16"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
|
||||||
|
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||||
|
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
|
||||||
|
<rect key="frame" x="131" y="23" width="180" height="14"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="nC0-dk-QaF" secondAttribute="bottom" constant="20" symbolic="YES" id="3Kk-fY-SB7"/>
|
||||||
|
<constraint firstItem="JSZ-x8-Pqi" firstAttribute="trailing" secondItem="SNw-oQ-bnb" secondAttribute="trailing" id="3So-Wu-1cz"/>
|
||||||
|
<constraint firstItem="DAh-br-Dfx" firstAttribute="top" secondItem="U5U-QR-YXS" secondAttribute="top" constant="20" symbolic="YES" id="3im-Qd-loW"/>
|
||||||
|
<constraint firstItem="kkd-UV-SnA" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" id="6iw-dd-hTX"/>
|
||||||
|
<constraint firstItem="Uib-vA-HRc" firstAttribute="leading" secondItem="DAh-br-Dfx" secondAttribute="leading" id="6jA-Kj-Q7l"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="kkd-UV-SnA" secondAttribute="trailing" id="8YX-CO-sY2"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="5x7-ll-2f7" secondAttribute="trailing" constant="20" symbolic="YES" id="8jr-cl-x78"/>
|
||||||
|
<constraint firstItem="kkd-UV-SnA" firstAttribute="top" secondItem="U5U-QR-YXS" secondAttribute="top" id="Afh-Ur-QgJ"/>
|
||||||
|
<constraint firstItem="4Vi-cN-ude" firstAttribute="leading" secondItem="w0k-CK-0u4" secondAttribute="trailing" constant="15" id="D3C-co-B10"/>
|
||||||
|
<constraint firstItem="w0k-CK-0u4" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="nC0-dk-QaF" secondAttribute="trailing" constant="8" symbolic="YES" id="FGk-wm-1Mu"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="rJa-yg-nCn" secondAttribute="trailing" constant="20" symbolic="YES" id="Fa7-Rc-1lj"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="4Vi-cN-ude" secondAttribute="trailing" constant="20" symbolic="YES" id="Fbg-C8-v6E"/>
|
||||||
|
<constraint firstItem="5x7-ll-2f7" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="Fd0-zd-od8"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="4Vi-cN-ude" secondAttribute="bottom" constant="20" symbolic="YES" id="GyL-uL-sjW"/>
|
||||||
|
<constraint firstItem="w0k-CK-0u4" firstAttribute="centerY" secondItem="4Vi-cN-ude" secondAttribute="centerY" id="HcL-wb-0s6"/>
|
||||||
|
<constraint firstItem="rJa-yg-nCn" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="IEg-SN-bHB"/>
|
||||||
|
<constraint firstItem="rJa-yg-nCn" firstAttribute="top" secondItem="JSZ-x8-Pqi" secondAttribute="bottom" constant="16" id="IW3-MX-3Kh"/>
|
||||||
|
<constraint firstItem="DAh-br-Dfx" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="LY1-r0-viF"/>
|
||||||
|
<constraint firstItem="nC0-dk-QaF" firstAttribute="top" secondItem="5x7-ll-2f7" secondAttribute="bottom" constant="20" id="OjY-dM-dOG"/>
|
||||||
|
<constraint firstItem="nC0-dk-QaF" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="V6L-YR-ufX"/>
|
||||||
|
<constraint firstItem="JSZ-x8-Pqi" firstAttribute="leading" secondItem="SNw-oQ-bnb" secondAttribute="leading" id="dpc-5M-0Cq"/>
|
||||||
|
<constraint firstItem="5x7-ll-2f7" firstAttribute="top" secondItem="rJa-yg-nCn" secondAttribute="bottom" constant="8" symbolic="YES" id="dzE-Ob-SVG"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="4Vi-cN-ude" secondAttribute="bottom" constant="20" symbolic="YES" id="ny2-RO-bEI"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="kkd-UV-SnA" secondAttribute="bottom" id="oCP-dn-6dx"/>
|
||||||
|
<constraint firstItem="JSZ-x8-Pqi" firstAttribute="top" secondItem="SNw-oQ-bnb" secondAttribute="bottom" constant="5" id="sX3-MK-14k"/>
|
||||||
|
<constraint firstItem="Uib-vA-HRc" firstAttribute="top" secondItem="DAh-br-Dfx" secondAttribute="bottom" constant="15" id="tWI-S8-17J"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="DAh-br-Dfx" secondAttribute="trailing" constant="20" symbolic="YES" id="vDR-5D-1eN"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="buttonCancel" destination="nC0-dk-QaF" id="n5Q-jg-UCe"/>
|
||||||
|
<outlet property="buttonCreateProxy" destination="4Vi-cN-ude" id="rdK-xc-N7F"/>
|
||||||
|
<outlet property="buttonSecure" destination="rJa-yg-nCn" id="WIs-zt-f3a"/>
|
||||||
|
<outlet property="inputDomainName" destination="SNw-oQ-bnb" id="ELH-63-cAe"/>
|
||||||
|
<outlet property="inputProxySubject" destination="QCK-Z9-w7g" id="76U-te-Jzt"/>
|
||||||
|
<outlet property="previewText" destination="JSZ-x8-Pqi" id="Mve-6W-Owd"/>
|
||||||
|
<outlet property="textFieldDomainName" destination="mlA-Zt-Hu8" id="cHL-Yu-Yvx"/>
|
||||||
|
<outlet property="textFieldError" destination="w0k-CK-0u4" id="28h-bn-igB"/>
|
||||||
|
<outlet property="textFieldProxySubject" destination="Uib-vA-HRc" id="5tV-3l-Wbw"/>
|
||||||
|
<outlet property="textFieldSecure" destination="5x7-ll-2f7" id="NlV-g8-rYP"/>
|
||||||
|
<outlet property="textFieldTitle" destination="DAh-br-Dfx" id="8SA-EW-wcq"/>
|
||||||
|
</connections>
|
||||||
|
</viewController>
|
||||||
|
<customObject id="VaP-ZM-OcY" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="210" y="1524"/>
|
||||||
|
</scene>
|
||||||
|
<!--Window Controller-->
|
||||||
|
<scene sceneID="5Gf-7O-tdA">
|
||||||
|
<objects>
|
||||||
|
<windowController storyboardIdentifier="addProxyWindow" id="ogq-ok-UVi" sceneMemberID="viewController">
|
||||||
|
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="SMz-Va-x2z">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
|
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
|
||||||
|
<view key="contentView" id="HsN-qQ-BhO">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="ogq-ok-UVi" id="9CA-sB-ZTD"/>
|
||||||
|
</connections>
|
||||||
|
</window>
|
||||||
|
<connections>
|
||||||
|
<segue destination="dwh-CF-6iv" kind="relationship" relationship="window.shadowedContentViewController" id="My6-qb-eRg"/>
|
||||||
|
</connections>
|
||||||
|
</windowController>
|
||||||
|
<customObject id="5qP-qX-rbc" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="-374" y="1530"/>
|
||||||
|
</scene>
|
||||||
|
<!--SelectionVC-->
|
||||||
|
<scene sceneID="UXm-Ci-yEB">
|
||||||
|
<objects>
|
||||||
|
<viewController storyboardIdentifier="addDomainChoice" id="gOD-Gu-zDG" customClass="SelectionVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" id="ysc-sm-sli">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="540" height="177"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<visualEffectView blendingMode="behindWindow" material="toolTip" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="F37-zt-gM3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="540" height="177"/>
|
||||||
|
<subviews>
|
||||||
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
|
||||||
|
<rect key="frame" x="13" y="13" width="114" height="32"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="Zhu-D8-cLK"/>
|
||||||
|
</constraints>
|
||||||
|
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="LxP-t4-H2W">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<string key="keyEquivalent" base64-UTF8="YES">
|
||||||
|
Gw
|
||||||
|
</string>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="pressedCancel:" target="gOD-Gu-zDG" id="wMp-sM-0A4"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pYe-Qu-qnK">
|
||||||
|
<rect key="frame" x="187" y="20" width="333" height="20"/>
|
||||||
|
<subviews>
|
||||||
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="L5n-Gw-J27">
|
||||||
|
<rect key="frame" x="-7" y="-7" width="172" height="32"/>
|
||||||
|
<buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="8UP-Sw-TP6">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<string key="keyEquivalent">l</string>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="pressedCreateLink:" target="gOD-Gu-zDG" id="77M-Ip-GMi"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="01Z-IV-hv1">
|
||||||
|
<rect key="frame" x="159" y="-7" width="181" height="32"/>
|
||||||
|
<buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="bJ4-q8-1Ej">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<string key="keyEquivalent">p</string>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="pressedCreateProxy:" target="gOD-Gu-zDG" id="UDf-lD-KCS"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<visibilityPriorities>
|
||||||
|
<integer value="1000"/>
|
||||||
|
<integer value="1000"/>
|
||||||
|
</visibilityPriorities>
|
||||||
|
<customSpacing>
|
||||||
|
<real value="3.4028234663852886e+38"/>
|
||||||
|
<real value="3.4028234663852886e+38"/>
|
||||||
|
</customSpacing>
|
||||||
|
</stackView>
|
||||||
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
|
||||||
|
<rect key="frame" x="18" y="138" width="504" height="19"/>
|
||||||
|
<textFieldCell key="cell" selectable="YES" alignment="left" title="[i18n] What kind of domain would you like to set up?" id="agk-Nj-FLd">
|
||||||
|
<font key="font" metaFont="systemBold" size="15"/>
|
||||||
|
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
|
||||||
|
<rect key="frame" x="18" y="60" width="504" height="70"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="tbl-AV-4qB"/>
|
||||||
|
</constraints>
|
||||||
|
<textFieldCell key="cell" selectable="YES" alignment="left" id="3i9-RG-Ift">
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
<string key="title">[i18n] Links are used to directly serve projects. If you have a Laravel, Symfony, WordPress, etc. folder with code, you'll want to create a link and choose the folder where your code lives.
If you are in need of a proxy, you can proxy e.g. a container to a particular domain name. This can be useful in combination with Docker, for example.</string>
|
||||||
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="FhN-AM-SkI" firstAttribute="leading" secondItem="F37-zt-gM3" secondAttribute="leading" constant="20" symbolic="YES" id="3dg-JM-MDr"/>
|
||||||
|
<constraint firstItem="fJK-Ke-IK3" firstAttribute="top" secondItem="F37-zt-gM3" secondAttribute="top" constant="20" symbolic="YES" id="FbX-Le-O7Q"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="pYe-Qu-qnK" secondAttribute="trailing" constant="20" symbolic="YES" id="IJA-vN-Rbv"/>
|
||||||
|
<constraint firstItem="urj-Xq-TrJ" firstAttribute="leading" secondItem="fJK-Ke-IK3" secondAttribute="leading" id="JcY-ae-6ZH"/>
|
||||||
|
<constraint firstItem="urj-Xq-TrJ" firstAttribute="trailing" secondItem="fJK-Ke-IK3" secondAttribute="trailing" id="ZBI-pN-kOz"/>
|
||||||
|
<constraint firstItem="fJK-Ke-IK3" firstAttribute="leading" secondItem="F37-zt-gM3" secondAttribute="leading" constant="20" symbolic="YES" id="d4o-6b-Dho"/>
|
||||||
|
<constraint firstItem="urj-Xq-TrJ" firstAttribute="top" secondItem="fJK-Ke-IK3" secondAttribute="bottom" constant="8" symbolic="YES" id="hOk-eL-Eg0"/>
|
||||||
|
<constraint firstItem="FhN-AM-SkI" firstAttribute="top" secondItem="urj-Xq-TrJ" secondAttribute="bottom" constant="20" id="kCc-Vp-Gvq"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="pYe-Qu-qnK" secondAttribute="bottom" constant="20" id="lPX-ZF-XZN"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="fJK-Ke-IK3" secondAttribute="trailing" constant="20" symbolic="YES" id="spl-Bn-xtw"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="FhN-AM-SkI" secondAttribute="bottom" constant="20" symbolic="YES" id="t5w-aL-tOa"/>
|
||||||
|
<constraint firstItem="pYe-Qu-qnK" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="FhN-AM-SkI" secondAttribute="trailing" constant="8" symbolic="YES" id="y7k-sl-xqe"/>
|
||||||
|
</constraints>
|
||||||
|
</visualEffectView>
|
||||||
|
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cNh-Wc-ADk">
|
||||||
|
<rect key="frame" x="200" y="109" width="0.0" height="48"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="OQ5-hX-qai">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
</buttonCell>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="F37-zt-gM3" secondAttribute="trailing" id="ZRD-3j-s4x"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="F37-zt-gM3" secondAttribute="bottom" id="et1-At-Rgj"/>
|
||||||
|
<constraint firstItem="F37-zt-gM3" firstAttribute="top" secondItem="ysc-sm-sli" secondAttribute="top" id="jp3-eE-mOy"/>
|
||||||
|
<constraint firstItem="F37-zt-gM3" firstAttribute="leading" secondItem="ysc-sm-sli" secondAttribute="leading" id="wIo-zP-KId"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="buttonCancel" destination="FhN-AM-SkI" id="iqV-2E-q7e"/>
|
||||||
|
<outlet property="buttonCreateLink" destination="L5n-Gw-J27" id="SHV-4l-Red"/>
|
||||||
|
<outlet property="buttonCreateProxy" destination="01Z-IV-hv1" id="J1v-7J-4fx"/>
|
||||||
|
<outlet property="textFieldDescription" destination="urj-Xq-TrJ" id="u1w-O0-kI3"/>
|
||||||
|
<outlet property="textFieldTitle" destination="fJK-Ke-IK3" id="x8p-qx-HX4"/>
|
||||||
|
</connections>
|
||||||
|
</viewController>
|
||||||
|
<customObject id="bZa-dD-d4J" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="250" y="1900"/>
|
||||||
|
</scene>
|
||||||
|
<!--Window Controller-->
|
||||||
|
<scene sceneID="HW6-nV-trE">
|
||||||
|
<objects>
|
||||||
|
<windowController storyboardIdentifier="showSelectionWindow" id="t4x-Mh-iya" sceneMemberID="viewController">
|
||||||
|
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="IeW-fo-4yK">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
|
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
|
||||||
|
<view key="contentView" id="Oe0-yv-Jcy">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="t4x-Mh-iya" id="4oO-gI-bd2"/>
|
||||||
|
</connections>
|
||||||
|
</window>
|
||||||
|
<connections>
|
||||||
|
<segue destination="gOD-Gu-zDG" kind="relationship" relationship="window.shadowedContentViewController" id="KRt-OH-8uc"/>
|
||||||
|
</connections>
|
||||||
|
</windowController>
|
||||||
|
<customObject id="hBK-Bw-dwa" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="-374" y="1909"/>
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="Checkmark" width="512" height="512"/>
|
<image name="Checkmark" width="512" height="512"/>
|
||||||
<image name="IconLinked" width="25" height="25"/>
|
<image name="IconLinked" width="25" height="25"/>
|
||||||
|
<image name="IconProxy" width="25" height="25"/>
|
||||||
<image name="Lock" width="30" height="30"/>
|
<image name="Lock" width="30" height="30"/>
|
||||||
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
|
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
|
||||||
<image name="plus" catalog="system" width="14" height="13"/>
|
<image name="plus" catalog="system" width="14" height="13"/>
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@@ -23,13 +23,13 @@ class InterApp {
|
|||||||
|
|
||||||
static func getCommands() -> [InterApp.Action] { return [
|
static func getCommands() -> [InterApp.Action] { return [
|
||||||
InterApp.Action(command: "list", action: { _ in
|
InterApp.Action(command: "list", action: { _ in
|
||||||
SiteListVC.show()
|
DomainListVC.show()
|
||||||
}),
|
}),
|
||||||
InterApp.Action(command: "services/stop", action: { _ in
|
InterApp.Action(command: "services/stop", action: { _ in
|
||||||
MainMenu.shared.stopAllServices()
|
MainMenu.shared.stopValetServices()
|
||||||
}),
|
}),
|
||||||
InterApp.Action(command: "services/restart/all", action: { _ in
|
InterApp.Action(command: "services/restart/all", action: { _ in
|
||||||
MainMenu.shared.restartAllServices()
|
MainMenu.shared.restartValetServices()
|
||||||
}),
|
}),
|
||||||
InterApp.Action(command: "services/restart/nginx", action: { _ in
|
InterApp.Action(command: "services/restart/nginx", action: { _ in
|
||||||
MainMenu.shared.restartNginx()
|
MainMenu.shared.restartNginx()
|
||||||
@@ -56,12 +56,16 @@ class InterApp {
|
|||||||
if PhpEnv.shared.availablePhpVersions.contains(version) {
|
if PhpEnv.shared.availablePhpVersions.contains(version) {
|
||||||
MainMenu.shared.switchToPhpVersion(version)
|
MainMenu.shared.switchToPhpVersion(version)
|
||||||
} else {
|
} else {
|
||||||
BetterAlert().withInformation(
|
DispatchQueue.main.async {
|
||||||
title: "Unsupported version",
|
BetterAlert().withInformation(
|
||||||
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
|
title: "alert.php_switch_unavailable.title".localized,
|
||||||
).withPrimary(text: "OK").show()
|
subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
|
||||||
|
).withPrimary(
|
||||||
|
text: "alert.php_switch_unavailable.ok".localized
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
]}
|
]}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
80
phpmon/Domain/App/ServicesManager.swift
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
//
|
||||||
|
// 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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let userServiceNames = Preferences.custom.services else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -2,7 +2,7 @@
|
|||||||
// Environment.swift
|
// Environment.swift
|
||||||
// PHP Monitor
|
// PHP Monitor
|
||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -17,8 +17,7 @@ class Startup {
|
|||||||
If this method returns false, there was a failed check and an alert was displayed.
|
If this method returns false, there was a failed check and an alert was displayed.
|
||||||
If this method returns true, then all checks succeeded and the app can continue.
|
If this method returns true, then all checks succeeded and the app can continue.
|
||||||
*/
|
*/
|
||||||
func checkEnvironment() async -> Bool
|
func checkEnvironment() async -> Bool {
|
||||||
{
|
|
||||||
// Do the important system setup checks
|
// Do the important system setup checks
|
||||||
Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)")
|
Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)")
|
||||||
|
|
||||||
@@ -147,13 +146,15 @@ class Startup {
|
|||||||
command: { return !Shell.pipe("cat /private/etc/sudoers.d/brew").contains(Paths.brew) },
|
command: { return !Shell.pipe("cat /private/etc/sudoers.d/brew").contains(Paths.brew) },
|
||||||
name: "`/private/etc/sudoers.d/brew` contains brew",
|
name: "`/private/etc/sudoers.d/brew` contains brew",
|
||||||
titleText: "startup.errors.sudoers_brew.title".localized,
|
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(
|
EnvironmentCheck(
|
||||||
command: { return !Shell.pipe("cat /private/etc/sudoers.d/valet").contains(Paths.valet) },
|
command: { return !Shell.pipe("cat /private/etc/sudoers.d/valet").contains(Paths.valet) },
|
||||||
name: "`/private/etc/sudoers.d/valet` contains valet",
|
name: "`/private/etc/sudoers.d/valet` contains valet",
|
||||||
titleText: "startup.errors.sudoers_valet.title".localized,
|
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).
|
// Verify if the Homebrew services are running (as root).
|
||||||
@@ -166,6 +167,18 @@ class Startup {
|
|||||||
descriptionText: "startup.errors.services_json_error.desc".localized
|
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.
|
// Determine that the Valet configuration JSON file is valid.
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
EnvironmentCheck(
|
EnvironmentCheck(
|
||||||
@@ -183,11 +196,51 @@ class Startup {
|
|||||||
descriptionText: "startup.errors.valet_json_invalid.desc".localized
|
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.
|
// Determine the Valet version and ensure it isn't unknown.
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
EnvironmentCheck(
|
EnvironmentCheck(
|
||||||
command: {
|
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
|
return Valet.shared.version == nil
|
||||||
},
|
},
|
||||||
name: "`valet --version` was loaded",
|
name: "`valet --version` was loaded",
|
||||||
@@ -196,42 +249,4 @@ class Startup {
|
|||||||
descriptionText: "startup.errors.valet_version_unknown.desc".localized
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|