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

Compare commits

...

131 Commits

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

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

    ~/.config/phpmon/preset_revert.json

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

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

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

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

- Updated tests to reflect these changes to `PhpVersionNumber`.
2022-04-20 12:19:02 +02:00
eb80214785 ✏️ Updated copy 2022-04-19 20:39:51 +02:00
80a4e361a4 🔧 Prepare for new DEV build 2022-04-19 20:33:19 +02:00
2af88b2bee ✏️ Update copy about non-standard TLDs 2022-04-19 20:26:44 +02:00
5048ccab8c 👌 Update various TODO items 2022-04-18 12:00:54 +02:00
66d13c92d5 Run the valet proxy command (#105) 2022-04-18 11:59:14 +02:00
836b076da9 👌 Cleanup, ensure dynamic form works correctly 2022-04-17 14:33:59 +02:00
1a75838a3b Set up proxy view strings and outlets 2022-04-17 14:02:44 +02:00
a18b7962a7 ♻️ Fix link 2022-04-16 23:21:29 +02:00
84548634ec Add proxy view 2022-04-16 23:19:13 +02:00
419ebe61f7 Add selection view 2022-04-14 14:56:51 +02:00
c45817b127 Added new UI for proxies to storyboard 2022-04-13 19:14:08 +02:00
2c0c0c5a11 Correctly detect secured proxies 2022-04-12 20:43:57 +02:00
1b8d6311ba ♻️ Refactor displaying domains 2022-04-12 17:36:18 +02:00
f0f7a3f7d6 👌 Scan proxies (#105) 2022-04-11 22:56:40 +02:00
8304d774c3 ♻️ Refactoring of files and tests 2022-04-02 15:48:21 +02:00
faeea4e866 Ensure normal nginx file does not have proxy 2022-03-31 18:28:47 +02:00
6470daf7d3 Added test to parse the proxy address 2022-03-31 18:27:26 +02:00
94139a3669 🚚 Moved test files into separate directories 2022-03-31 18:09:50 +02:00
8057019898 🐛 Fix isolation command (#158) 2022-03-31 13:35:23 +02:00
9b59fc5dae 👌 Cleanup proxies 2022-03-31 13:34:56 +02:00
75f4377de8 Added tooltips next to PHP version 2022-03-31 13:29:04 +02:00
d3657716c4 ♻️ Added ValetProxy struct 2022-03-30 21:26:53 +02:00
a13990b96f ♻️ Rename SiteList to DomainList 2022-03-30 19:19:36 +02:00
4c7aa7fead Add UI for displaying proxies (#105) 2022-03-29 21:40:10 +02:00
176 changed files with 7173 additions and 3204 deletions

15
.swiftlint.yml Normal file
View File

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

View File

@ -1,5 +1,19 @@
# 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
<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
11. Check new version can be installed via Cask
## 🍱 Marketing Mode
You can enable marketing mode by setting the `PHPMON_MARKETING_MODE` environment variable. It preloads a list of (fake) domains in the domain window list for screenshot & marketing purposes.
launchctl setenv PHPMON_MARKETING_MODE true
## 🐛 Symbolication of crashes
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.

File diff suppressed because it is too large Load Diff

View File

@ -61,10 +61,16 @@
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "--v"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "PHPMON_MARKETING_MODE"
value = "YES"
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>

113
README.md
View File

@ -4,13 +4,15 @@
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
<img src="./docs/screenshot.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
@ -21,10 +23,10 @@ You can also add new domains as links, isolate sites, manage various services, a
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
* macOS 11 Big Sur or later
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* Homebrew `php` formula is installed
* Laravel Valet 2.16 or newer (supports Valet 3)
* Laravel Valet 3 recommended (but compatible with Valet 2)
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
@ -103,14 +105,14 @@ Super convenient!
<details>
<summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary>
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it:
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it.
Install [Homebrew](https://brew.sh) first.
**I have also created [a video tutorial](https://www.youtube.com/watch?v=fO3hVhkvm3w) which may be easier to follow. If you just want the terminal commands, keep reading.**
Install PHP, composer, add to path:
Install [Homebrew](https://brew.sh) first. Follow the instructions there first!
Then, you'll need to set up your PATH.
brew install php
brew install composer
nano .zshrc
Make sure the following line is not in the comments:
@ -123,30 +125,70 @@ If you're on an Apple Silicon-based Mac, you'll need to add:
# on an M1 Mac
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
and add the following to your .zshrc, but add this BEFORE the homebrew PATH additions:
and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
If you're adding composer and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
export PATH=$HOME/bin:/usr/local/bin:$PATH
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
If you are *not* on Apple Silicon, you should remove the third line.
Install the `php` and `composer` formulae:
brew install php composer
Make sure PHP is linked correctly:
which php
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php`)
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon)
composer global require laravel/valet
For optimal results, you should lock your PHP platform for global dependencies to the oldest version of PHP you intend to run. If that version is PHP 7.0, your `~/.composer/composer.json` file could look like this (please adjust the version accordingly!):
```
{
"require": {
"laravel/valet": "^3.0",
},
"config": {
"platform": {
"php": "7.0"
}
}
}
```
Run `composer global update` again. This ensures that when you switch to a different global PHP version, [Valet won't break](https://github.com/nicoverbruggen/phpmon/issues/178). If it does, PHP Monitor will let you know what you can do about this.
Then, install Valet:
valet install
This should install `dnsmasq` and set up Valet. Great, almost there!
valet trust
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
You can now install PHP Monitor, if you haven't already:
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
</details>
<details>
<summary><strong>How frequently does PHP Monitor check for updates?</strong></summary>
PHP Monitor will check if an update is available every time you start the app.
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". You can always check for updates manually.
</details>
<details>
@ -278,10 +320,46 @@ PHP Monitor is a universal app and supports both architectures, so [find out her
<details>
<summary><strong>Why is the app doing network requests?</strong></summary>
It's Homebrew. I can't prevent `brew` from doing things via the network when I invoke it.
The app will automatically check for updates, which is the most likely culprit.
PHP Monitor itself doesn't do any network requests. Feel free to check the source code or intercept the traffic, if you don't believe me.
This happens at launch (unless disabled), and the app directly checks the Caskfile hosted on GitHub. This data is not, and will not be used for analytics (and, as far as I can tell, cannot).
I also can't prevent `brew` from doing things via the network when PHP Monitor uses the binary.
The app includes an Internet Access Policy file, so if you're using something like Little Snitch there should be a description why these calls occur.
</details>
<details>
<summary><strong>How do I various presets to show up?</strong></summary>
You must set these presets up in a JSON file, located in `~/.config/phpmon/config.json`.
You must have set up at least one valid preset for this presets to work in PHP Monitor.
Here's an example of a working preset:
<pre>
{
"scan_apps": [],
"presets": [
{
"name": "Legacy Project",
"php": "8.0",
"extensions": {
"xdebug": false
},
"configuration": {
"memory_limit": "128M",
"upload_max_filesize": "128M",
"post_max_size": "128M"
}
}
]
}
</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.
</details>
<details>
@ -295,11 +373,12 @@ All of these apps should just be detected correctly, no matter their location on
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
You can add your own apps by creating and editing a `~/.phpmon.conf.json` file, with the following entry:
You can add your own apps by creating and editing a `~/.config/phpmon/config.json` file, and make sure the `scan_apps` key is set:
<pre>
{
"scan_apps": ["Xcode", "Kraken"]
"scan_apps": ["Xcode", "Kraken"],
"presets": []
}
</pre>

View File

@ -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 |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 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
@ -16,9 +16,9 @@ These versions of PHP Monitor are no longer supported, but if youre using an
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 |
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 | 2.13 |
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |

Binary file not shown.

BIN
docs/notification-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/screenshot-dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 469 KiB

View File

@ -3,7 +3,7 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest

View File

@ -3,18 +3,18 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class BrewJsonParserTest: XCTestCase {
class HomebrewPackageTest: XCTestCase {
// - MARK: SYNTHETIC TESTS
static var jsonBrewFile: URL {
return Bundle(for: Self.self)
.url(forResource: "brew", withExtension: "json")!
.url(forResource: "brew-formula", withExtension: "json")!
}
func testCanLoadExtensionJson() throws {
@ -64,9 +64,9 @@ class BrewJsonParserTest: XCTestCase {
return ["php", "nginx", "dnsmasq"].contains(service.name)
})
XCTAssertTrue(services.contains(where: {$0.name == "php"} ))
XCTAssertTrue(services.contains(where: {$0.name == "nginx"} ))
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} ))
XCTAssertTrue(services.contains(where: {$0.name == "php"}))
XCTAssertTrue(services.contains(where: {$0.name == "nginx"}))
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"}))
XCTAssertEqual(services.count, 3)
}

View File

@ -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
)
}
}

View 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)
}
}

View 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"
)
}
}

View File

@ -3,25 +3,25 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class ExtensionParserTest: XCTestCase {
class PhpExtensionTest: XCTestCase {
static var phpIniFileUrl: URL {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
}
func testCanLoadExtension() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
XCTAssertGreaterThan(extensions.count, 0)
}
func testExtensionNameIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
let extensionNames = extensions.map { (ext) -> String in
return ext.name
@ -40,7 +40,7 @@ class ExtensionParserTest: XCTestCase {
}
func testExtensionStatusIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
// xdebug should be enabled
XCTAssertEqual(extensions[0].enabled, true)
@ -51,7 +51,7 @@ class ExtensionParserTest: XCTestCase {
func testToggleWorksAsExpected() throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.load(from: destination)
let extensions = PhpExtension.from(filePath: destination.path)
XCTAssertEqual(extensions.count, 6)
// Try to disable xdebug (should be detected first)!
@ -66,7 +66,7 @@ class ExtensionParserTest: XCTestCase {
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
// Make sure if we load the data again, it's disabled
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
XCTAssertEqual(PhpExtension.from(filePath: destination.path).first!.enabled, false)
}
}

View File

@ -3,12 +3,12 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class ValetConfigParserTest: XCTestCase {
class ValetConfigurationTest: XCTestCase {
static var jsonConfigFileUrl: URL {
return Bundle(for: Self.self).url(

View 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;
}
}

View 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 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;
}
}

View 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;
}
}

View 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"
}
}
]
}

View File

@ -3,7 +3,7 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -0,0 +1,21 @@
//
// 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)
}
}

View 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)
}
}

View File

@ -3,7 +3,7 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 01/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest

View File

@ -11,20 +11,24 @@ import XCTest
class PhpVersionNumberTest: XCTestCase {
func testCanDeconstructPhpVersion() throws {
XCTAssertEqual(
try! PhpVersionNumber.parse("PHP 8.2.0-dev"),
PhpVersionNumber(major: 8, minor: 2, patch: 0)
)
XCTAssertEqual(
try! PhpVersionNumber.parse("PHP 8.1.0RC5-dev"),
PhpVersionNumber(major: 8, minor: 1, patch: 0)
)
XCTAssertEqual(
PhpVersionNumber.make(from: "8.0.11"),
try! PhpVersionNumber.parse("8.0.11"),
PhpVersionNumber(major: 8, minor: 0, patch: 11)
)
XCTAssertEqual(
PhpVersionNumber.make(from: "7.4.2"),
try! PhpVersionNumber.parse("7.4.2"),
PhpVersionNumber(major: 7, minor: 4, patch: 2)
)
XCTAssertEqual(
PhpVersionNumber.make(from: "7.4"),
try! PhpVersionNumber.parse("7.4"),
PhpVersionNumber(major: 7, minor: 4, patch: nil)
)
XCTAssertEqual(

View File

@ -3,7 +3,7 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest

View File

@ -3,7 +3,7 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest

View 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
}
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -2,7 +2,7 @@
// Services.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -12,33 +12,28 @@ class Actions {
// MARK: - Services
public static func restartPhpFpm()
{
public static func restartPhpFpm() {
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
}
public static func restartNginx()
{
public static func restartNginx() {
brew("services restart nginx", sudo: true)
}
public static func restartDnsMasq()
{
public static func restartDnsMasq() {
brew("services restart dnsmasq", sudo: true)
}
public static func stopAllServices()
{
public static func stopValetServices() {
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true)
}
public static func fixHomebrewPermissions() throws
{
public static func fixHomebrewPermissions() throws {
var servicesCommands = [
"\(Paths.brew) services stop nginx",
"\(Paths.brew) services stop dnsmasq",
"\(Paths.brew) services stop dnsmasq"
]
var cellarCommands = [
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
@ -64,43 +59,67 @@ class Actions {
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
if (eventResult == nil) {
if eventResult == nil {
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
public static func openGenericPhpConfigFolder()
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
public static func openGenericPhpConfigFolder() {
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder()
{
public static func openGlobalComposerFolder() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpConfigFolder(version: String)
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
public static func openPhpConfigFolder(version: String) {
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openValetConfigFolder()
{
public static func openValetConfigFolder() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpMonitorConfigFile() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/phpmon")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL
{
public static func createTempPhpInfoFile() -> URL {
// Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
@ -124,8 +143,7 @@ class Actions {
If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`.
*/
public static func fixMyValet(completed: @escaping () -> Void)
{
public static func fixMyValet(completed: @escaping () -> Void) {
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true)

View File

@ -2,7 +2,7 @@
// Command.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -28,7 +28,7 @@ public class Command {
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
if (trimNewlines) {
if trimNewlines {
return output.components(separatedBy: .newlines)
.filter({ !$0.isEmpty })
.joined(separator: "\n")

View File

@ -2,7 +2,7 @@
// Constants.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -56,13 +56,27 @@ struct Constants {
static let DonationPayment = URL(
string: "https://nicoverbruggen.be/sponsor#pay-now"
)!
static let DonationPage = URL(
string: "https://nicoverbruggen.be/sponsor"
)!
static let FrequentlyAskedQuestions = URL(
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
)!
static let GitHubReleases = URL(
string: "https://github.com/nicoverbruggen/phpmon/releases"
)!
static let StableBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb"
)!
static let DevBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
)!
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// 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
@ -11,24 +11,21 @@
/**
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)
}
/**
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)")
}
/**
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)
let e_original = original.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.
*/
func grepContains(file: String, query: String) -> Bool
{
func grepContains(file: String, query: String) -> Bool {
return Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""")

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -2,7 +2,7 @@
// Paths.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -49,7 +49,7 @@ public class Paths {
// - MARK: Detected Binaries
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
public static var composer: String? = nil
public static var composer: String?
// - MARK: Paths

View File

@ -35,8 +35,11 @@ extension Process {
forName: NSNotification.Name.NSFileHandleDataAvailable,
object: pipe.fileHandleForReading,
queue: nil
) { notification in
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
) { _ in
if let outputString = String(
data: pipe.fileHandleForReading.availableData,
encoding: String.Encoding.utf8
) {
callback(outputString)
}
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()

View File

@ -2,7 +2,7 @@
// Shell.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -91,7 +91,7 @@ public class Shell {
task.launch()
task.waitUntilExit()
return Shell.Output(
let output = Shell.Output(
standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
@ -102,6 +102,12 @@ public class Shell {
)!,
task: task
)
if CommandLine.arguments.contains("--v") {
log(task: task, output: output)
}
return output
}
/**
@ -119,6 +125,23 @@ public class Shell {
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 let standardOutput: String
public let errorOutput: String

View 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
}
}

View File

@ -2,7 +2,7 @@
// Date.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 14/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -27,3 +27,25 @@ extension NSMenu {
}
}
// MARK: - NSMenuItem subclasses
class PhpMenuItem: NSMenuItem {
var version: String = ""
}
class XdebugMenuItem: NSMenuItem {
var mode: String = ""
}
class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension?
}
class EditorMenuItem: NSMenuItem {
var editor: Application?
}
class PresetMenuItem: NSMenuItem {
var preset: Preset?
}

View File

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

View File

@ -2,13 +2,18 @@
// StringExtension.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension String {
var localized: String {
if #available(macOS 13, *) {
return NSLocalizedString(
self, tableName: nil, bundle: Bundle.main, value: "", comment: ""
).replacingOccurrences(of: "Preferences", with: "Settings")
}
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}
@ -17,7 +22,7 @@ extension String {
}
func countInstances(of stringToFind: String) -> Int {
if (stringToFind.isEmpty) {
if stringToFind.isEmpty {
return 0
}
@ -32,7 +37,7 @@ extension String {
return count
}
subscript (r: Range<String.Index>) -> String {
subscript(r: Range<String.Index>) -> String {
let start = r.lowerBound
let end = r.upperBound
return String(self[start ..< end])
@ -71,4 +76,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 ""
}
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -26,10 +26,10 @@ extension XibLoadable where Self: NSView {
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
guard let xibName = xibName else { return nil }
var topLevelArray: NSArray? = nil
var topLevelArray: NSArray?
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
guard let results = topLevelArray else { return nil }
let views = Array<Any>(results).filter { $0 is Self }
let views = [Any](results).filter { $0 is Self }
return views.last as? Self
}

View File

@ -2,7 +2,7 @@
// Alert.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -27,7 +27,7 @@ class Alert {
alert.messageText = messageText
alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) {
if !secondButtonTitle.isEmpty {
alert.addButton(withTitle: secondButtonTitle)
}
alert.beginSheetModal(for: window) { response in

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -2,7 +2,7 @@
// LocalNotification.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -10,7 +10,11 @@ import UserNotifications
class LocalNotification {
public static func send(title: String, subtitle: String) {
public static func send(title: String, subtitle: String, preference: PreferenceName) {
if !Preferences.isEnabled(preference) {
return
}
let content = UNMutableNotificationContent()
content.title = title
content.body = subtitle

View File

@ -2,7 +2,7 @@
// ImageGenerator.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -24,7 +24,7 @@ class MenuBarImageGenerator {
NSAttributedString.Key.paragraphStyle: textStyle
]
let padding : CGFloat = 2.0;
let padding: CGFloat = 2.0
// Create an attributed string so we'll know how wide the item will need to be
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -11,7 +11,7 @@ import Foundation
class VersionExtractor {
/**
This attempts to extract the version number from the command line output of Valet.
This attempts to extract the version number from any given string.
*/
public static func from(_ string: String) -> String? {
do {
@ -23,7 +23,7 @@ class VersionExtractor {
let match = regex.matches(
in: string,
options: [],
range: NSMakeRange(0, string.count)
range: NSRange(location: 0, length: string.count)
).first
guard let match = match else {

View File

@ -2,7 +2,7 @@
// ActivePhpInstallation.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -20,7 +20,13 @@ class ActivePhpInstallation {
var version: Version!
var limits: Limits!
var extensions: [PhpExtension]!
var iniFiles: [PhpConfigurationFile] = []
var extensions: [PhpExtension] {
return iniFiles.flatMap { initFile in
return initFile.extensions
}
}
// MARK: - Computed
@ -34,16 +40,21 @@ class ActivePhpInstallation {
// Show information about the current version
getVersion()
// Initialize the list of ini files that are loaded
iniFiles = []
// If an error occurred, exit early
if (version.error) {
if version.error {
limits = Limits()
extensions = []
return
}
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path)
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
iniFiles.append(file)
}
// Get configuration values
limits = Limits(
@ -60,9 +71,8 @@ class ActivePhpInstallation {
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if exts.count > 0 {
extensions.append(contentsOf: exts)
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
iniFiles.append(file)
}
}
}
@ -71,12 +81,12 @@ class ActivePhpInstallation {
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
*/
private func getVersion() -> Void {
private func getVersion() {
self.version = Version()
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.long = ""
self.version.error = true
@ -112,13 +122,13 @@ class ActivePhpInstallation {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
// Check if the value is unlimited
if (value == "-1") {
if value == "-1" {
return ""
}
// Check if the syntax is valid otherwise
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
let match = regex.matches(in: value, options: [], range: NSRange(location: 0, length: value.count)).first
return (match == nil) ? "⚠️" : "\(value)B"
}

View File

@ -0,0 +1,40 @@
//
// Xdebug.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
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 var modes: [String] {
return [
"develop",
"coverage",
"debug",
"gcstats",
"profile",
"trace"
]
}
}

View File

@ -2,7 +2,7 @@
// HomebrewPackage.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -19,20 +19,20 @@ struct HomebrewService: Decodable, Equatable {
let log_path: String?
let error_log_path: String?
public static func loadAll(
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
completion: @escaping ([HomebrewService]) -> Void
) {
DispatchQueue.global(qos: .background).async {
let data = Shell
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
.data(using: .utf8)!
let services = try! JSONDecoder()
.decode([HomebrewService].self, from: data)
.filter({ return filter.contains($0.name) })
completion(services)
}
/**
Dummy data for preview purposes.
*/
public static func dummy(named service: String, enabled: Bool) -> Self {
return HomebrewService(
name: service,
service_name: service,
running: enabled,
loaded: enabled,
pid: nil,
user: nil,
status: nil,
log_path: nil,
error_log_path: nil
)
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -15,7 +15,7 @@ class PhpEnv {
init() {
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(
[HomebrewPackage].self,
@ -76,15 +76,14 @@ class PhpEnv {
return InternalSwitcher()
}
public static func detectPhpVersions() -> Void {
public static func detectPhpVersions() {
_ = Self.shared.detectPhpVersions()
}
/**
Detects which versions of PHP are installed.
*/
public func detectPhpVersions() -> [String]
{
public func detectPhpVersions() -> [String] {
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
@ -95,7 +94,7 @@ class PhpEnv {
let phpAlias = homebrewPackage.version
// 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)
}
@ -126,7 +125,7 @@ class PhpEnv {
checkBinaries: Bool = true,
generateHelpers: Bool = true
) -> [String] {
var output : [String] = []
var output: [String] = []
var supported = Constants.SupportedPhpVersions
@ -144,8 +143,7 @@ class PhpEnv {
// is supported and where the binary exists (avoids broken installs)
if !output.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)
}
}
@ -176,4 +174,14 @@ class PhpEnv {
return false
}
/**
Returns the configuration file instance that is used for a specific config value.
You can then use the configuration file instance to change values.
*/
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
return PhpEnv.phpInstall.iniFiles
.reversed()
.first(where: { $0.has(key: key) })
}
}

View File

@ -21,7 +21,8 @@ class PhpHelper {
if FileManager.default.fileExists(atPath: destination) {
let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) {
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor (or is unreadable). Not updating this file.")
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
+ "(or is unreadable). Not updating this file.")
return
}
}

View File

@ -13,7 +13,7 @@ public struct PhpVersionNumberCollection: Equatable {
public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection(
versions: versions.map { PhpVersionNumber.make(from: $0)! }
versions: versions.map { try! PhpVersionNumber.parse($0) }
)
}
@ -91,7 +91,7 @@ public struct PhpVersionNumberCollection: Equatable {
}
}
public struct PhpVersionNumber: Equatable {
public struct PhpVersionNumber: Equatable, Hashable {
let major: Int
let minor: Int
let patch: Int?
@ -134,7 +134,12 @@ public struct PhpVersionNumber: Equatable {
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
let match = regex.matches(
in: versionString,
options: [],
range: NSRange(location: 0, length: versionString.count)
).first
if match != nil {
let major = Int(
@ -143,7 +148,7 @@ public struct PhpVersionNumber: Equatable {
let minor = Int(
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) {
patch = Int(versionString[minorRange])
}

View File

@ -0,0 +1,225 @@
//
// 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: "/Users/\(Paths.whoami)"
)
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 {
if 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
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 31/01/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -23,7 +23,8 @@ class PhpExtension {
/// The original string that was used to determine this extension is active.
var line: String
/// The name of the extension. This is always identical to the name found in the original string. If you want to display this name, capitalize this.
/// The name of the extension. This is always identical to the name found in the original string.
/// If you want to display this name, capitalize this.
var name: String
/// Whether the extension has been enabled.
@ -34,6 +35,7 @@ class PhpExtension {
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.
@ -47,13 +49,14 @@ class PhpExtension {
- 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)"?)$"#
// swiftlint:enable line_length
/**
When registering an extension, we do that based on the line found inside the .ini file.
*/
init(_ line: String, file: String) {
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
let match = regex.matches(in: line, options: [], range: NSMakeRange(0, line.count)).first
let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
let range = Range(match!.range(withName: "name"), in: 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() {
let newLine = enabled
@ -85,24 +89,26 @@ class PhpExtension {
// MARK: - Static Methods
/**
This method will attempt to identify all extensions in the .ini file at a certain URL.
*/
static func load(from path: URL) -> [PhpExtension] {
let file = try? String(contentsOf: path, encoding: .utf8)
static func from(_ lines: [String], filePath: String) -> [PhpExtension] {
return lines.filter {
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
}.map {
return PhpExtension($0, file: filePath)
}
}
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.")
return []
}
return file!.components(separatedBy: "\n")
.filter {
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
}
.map {
return PhpExtension($0, file: path.path)
}
return Self.from(
file!.components(separatedBy: "\n"),
filePath: filePath
)
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 28/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -20,21 +20,10 @@ class InternalSwitcher: PhpSwitcher {
the version that is switched to may or may not be identical to `php`
(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...")
let isolated = Valet.shared.sites.filter { site in
site.isolatedPhpVersion != nil
}.map { site in
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
}
var versions: Set<String> = [version]
if (Valet.enabled(feature: .isolatedSites)) {
versions = versions.union(isolated)
}
let versions = getVersionsToBeHandled(version)
let group = DispatchGroup()
@ -64,13 +53,39 @@ class InternalSwitcher: PhpSwitcher {
}
}
private func disableDefaultPhpFpmPool(_ version: String) {
func getVersionsToBeHandled(_ primary: String) -> Set<String> {
let isolated = Valet.shared.sites.filter { site in
site.isolatedPhpVersion != nil
}.map { site in
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
}
var versions: Set<String> = [primary]
if Valet.enabled(feature: .isolatedSites) {
versions = versions.union(isolated)
}
return versions
}
func requiresDisablingOfDefaultPhpFpmPool(_ version: String) -> Bool {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
return FileManager.default.fileExists(atPath: pool)
}
func disableDefaultPhpFpmPool(_ version: String) {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileManager.default.fileExists(atPath: pool) {
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
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")!
do {
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.")
try FileManager.default.removeItem(at: new)
}
try FileManager.default.moveItem(at: existing, to: new)
Log.info("Success: A default `www.conf` file was disabled for PHP \(version).")
} catch {
@ -79,17 +94,17 @@ class InternalSwitcher: PhpSwitcher {
}
}
private func stopPhpVersion(_ version: String) {
func stopPhpVersion(_ version: String) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)")
}
private func startPhpVersion(_ version: String, primary: Bool) {
func startPhpVersion(_ version: String, primary: Bool) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
if (primary) {
if primary {
Log.info("\(formula) is the primary formula, linking and starting services...")
brew("link \(formula) --overwrite --force")
} else {

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation

View 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?
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -38,7 +38,7 @@ extension App {
If there are no windows open, the app will be an accessory (toolbar) app.
*/
public func updateActivationPolicy() {
NSApp.setActivationPolicy(openWindows.count > 0 ? .regular : .accessory)
NSApp.setActivationPolicy(!openWindows.isEmpty ? .regular : .accessory)
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -2,7 +2,7 @@
// StateManager.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -21,6 +21,16 @@ class App {
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 {
var systeminfo = utsname()
uname(&systeminfo)
@ -41,14 +51,17 @@ class App {
var preferences: [PreferenceName: Bool]!
/** The window controller of the currently active preferences window. */
var preferencesWindowController: PrefsWC? = nil
var preferencesWindowController: PrefsWC?
/** The window controller of the currently active site list window. */
var siteListWindowController: SiteListWC? = nil
var domainListWindowController: DomainListWC?
/** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = []
/** The services manager, responsible for figuring out what services are active/inactive. */
var services = ServicesManager.shared
/** Timer that will periodically reload info about the user's PHP installation. */
var timer: Timer?
@ -57,7 +70,7 @@ class App {
/**
The shortcut the user has requested.
*/
var shortcutHotkey: HotKey? = nil {
var shortcutHotkey: HotKey? {
didSet {
setupGlobalHotkeyListener()
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 20/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -44,4 +44,3 @@ extension AppDelegate {
}
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -26,19 +26,19 @@ extension AppDelegate {
// MARK: - Menu Interactions
@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)
}
@IBAction func reloadSiteListPressed(_ sender: Any) {
let vc = App.shared.siteListWindowController?
.window?.contentViewController as? SiteListVC
@IBAction func reloadDomainListPressed(_ sender: Any) {
let vc = App.shared.domainListWindowController?
.window?.contentViewController as? DomainListVC
if vc != nil {
// If the view exists, directly reload the list of sites
vc!.reloadSites()
vc!.reloadDomains()
} else {
// If the view does not exist, reload the cached data that was populated when the app initially launched.
Valet.shared.reloadSites()
@ -46,9 +46,9 @@ extension AppDelegate {
}
@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()
}

View File

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

View File

@ -2,7 +2,7 @@
// AppDelegate.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -65,8 +65,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
override init() {
logger.verbosity = .info
#if DEBUG
logger.verbosity = .performance
// logger.verbosity = .performance
#endif
if CommandLine.arguments.contains("--v") {
logger.verbosity = .performance
Log.info("Extra verbose mode has been activated.")
}
Log.separator(as: .info)
Log.info("PHP MONITOR by Nico Verbruggen")
Log.info("Version \(App.version)")

View File

@ -0,0 +1,181 @@
//
// 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.version)\(devSuffix)")
)
}
)
.withTertiary(text: "Dismiss", action: { vc in
vc.close(with: .OK)
})
.show()
}
}
private static func notifyAboutConnectionIssue() {
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.errors.cannot_check_for_update.title".localized,
subtitle: "updater.errors.cannot_check_for_update.subtitle".localized,
description: "updater.errors.cannot_check_for_update.description".localized(
App.version
)
)
.withTertiary(
text: "updater.errors.buttons.releases_on_github".localized,
action: { _ in
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
}
)
.withPrimary(text: "OK")
.show()
}
}
}

View File

@ -0,0 +1,77 @@
//
// 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 computerReadable: String {
return "\(version)_\(build ?? "0")"
}
var humanReadable: String {
return "\(version) (\(build ?? "???"))"
}
}

View File

@ -60,16 +60,16 @@
</menuItem>
<menuItem title="reload-list" keyEquivalent="r" id="Ema-AU-Nbr" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_site_list"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_domain_list"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="reloadSiteListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
<action selector="reloadDomainListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="2ux-8Q-UjK"/>
<menuItem title="focus-find" keyEquivalent="f" id="I95-fb-EL7" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_site_list"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_domain_list"/>
</userDefinedRuntimeAttributes>
<connections>
<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="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="-495" y="-44"/>
<point key="canvasLocation" x="-360" y="-94"/>
</scene>
<!--Window Controller-->
<scene sceneID="PQa-AT-b2a">
<objects>
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PrefsWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="h4c-3b-nko">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="372" y="403" width="480" height="270"/>
<rect key="contentRect" x="372" y="403" width="550" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/>
<view key="contentView" id="2yL-50-11x">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<rect key="frame" x="0.0" y="0.0" width="550" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<toolbar key="toolbar" implicitIdentifier="611E3485-DC7F-46A0-8528-11CF9366370C" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconAndLabel" sizeMode="regular" id="fcq-wR-7iv">
<allowedToolbarItems/>
<defaultToolbarItems/>
</toolbar>
<connections>
<outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/>
</connections>
</window>
<connections>
<segue destination="AW2-rV-rbS" kind="relationship" relationship="window.shadowedContentViewController" id="3dX-9V-eA0"/>
<segue destination="PCI-2c-55Y" kind="relationship" relationship="window.shadowedContentViewController" id="egC-A4-am8"/>
</connections>
</windowController>
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="327"/>
<point key="canvasLocation" x="-374" y="238"/>
</scene>
<!--Preferences-->
<scene sceneID="iyi-IS-7Ps">
<objects>
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<viewController title="Preferences" identifier="preferencesTemplateVC" storyboardIdentifier="preferencesTemplateVC" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="GenericPreferenceVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" wantsLayer="YES" id="Pf1-A5-3Xz">
<rect key="frame" x="0.0" y="0.0" width="550" height="498"/>
<autoresizingMask key="autoresizingMask"/>
@ -378,13 +374,33 @@
</viewController>
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</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>
<!--Window Controller-->
<scene sceneID="4XS-kY-YIS">
<objects>
<windowController storyboardIdentifier="siteListWindow" id="8Ec-9q-82s" customClass="SiteListWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" title="Domains" subtitle="Linked &amp; Parked" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
<windowController storyboardIdentifier="domainListWindow" id="8Ec-9q-82s" customClass="DomainListWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" separatorStyle="line" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="425" y="461" width="600" height="263"/>
@ -437,7 +453,7 @@
</windowController>
<customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="746"/>
<point key="canvasLocation" x="-374" y="745.5"/>
</scene>
<!--Window Controller-->
<scene sceneID="HTI-x5-rOp">
@ -462,7 +478,7 @@
</windowController>
<customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-409" y="1137"/>
<point key="canvasLocation" x="-374" y="1137"/>
</scene>
<!--Window Controller-->
<scene sceneID="BD0-La-ygq">
@ -486,7 +502,7 @@
</windowController>
<customObject id="i3j-z8-nxv" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-575" y="1624"/>
<point key="canvasLocation" x="-374" y="2267"/>
</scene>
<!--Better AlertVC-->
<scene sceneID="y9E-bB-wIG">
@ -532,7 +548,7 @@ Gw
</connections>
</button>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j">
<rect key="frame" x="13" y="13" width="82" height="32"/>
<rect key="frame" x="13" y="13" width="81" height="32"/>
<buttonCell key="cell" type="push" title="Tertiary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="mzA-Uu-gyf">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -632,27 +648,27 @@ Gw
</viewController>
<customObject id="5Ts-EZ-bJh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="38" y="1624"/>
<point key="canvasLocation" x="230" y="2267"/>
</scene>
<!--Add SiteVC-->
<scene sceneID="6JC-H6-u4K">
<objects>
<viewController storyboardIdentifier="newSiteLink" id="glS-wF-sEU" customClass="AddSiteVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<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"/>
<subviews>
<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">
<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"/>
</view>
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
<rect key="frame" x="363" y="13" width="104" height="32"/>
<buttonCell key="cell" type="push" title="Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
<rect key="frame" x="326" y="13" width="141" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
@ -664,11 +680,11 @@ DQ
</connections>
</button>
<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>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
</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"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
@ -680,8 +696,8 @@ Gw
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
<rect key="frame" x="20" y="156" 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">
<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 domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -691,16 +707,16 @@ Gw
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
<rect key="frame" x="18" y="134" width="444" height="14"/>
<textFieldCell key="cell" title="FOLDER_AVAILABLE" id="bJr-s6-tdP">
<rect key="frame" x="18" y="128" width="444" height="14"/>
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
<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="KZf-b0-9cm">
<rect key="frame" x="18" y="101" width="227" height="18"/>
<buttonCell key="cell" type="check" title="Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ">
<rect key="frame" x="18" y="95" width="266" height="18"/>
<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"/>
<font key="font" metaFont="system"/>
</buttonCell>
@ -709,31 +725,31 @@ Gw
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
<rect key="frame" x="18" y="66" 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">
<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="4gd-KM-5Fu">
<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>
<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">
<font key="font" metaFont="system"/>
<url key="url" string="file:///Users/"/>
</pathCell>
</pathControl>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
<rect key="frame" x="18" y="215" width="87" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Link a Folder" id="S4j-ZC-ddT">
<rect key="frame" x="18" y="209" width="128" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
<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="900-Z2-tID">
<rect key="frame" x="229" y="23" width="128" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That link already exists." id="jOt-n6-TQf">
<rect key="frame" x="140" y="23" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
<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="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="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 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"/>
@ -778,7 +795,7 @@ Gw
<outlet property="buttonCancel" destination="SwS-o8-pbl" id="N1v-uy-2Mi"/>
<outlet property="buttonCreateLink" destination="PVw-cM-qAB" id="0Oo-xW-He7"/>
<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="previewText" destination="VzR-5a-cmT" id="qwd-wX-645"/>
<outlet property="textFieldError" destination="900-Z2-tID" id="qUk-FE-IKW"/>
@ -788,12 +805,12 @@ Gw
</viewController>
<customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="191" y="1098.5"/>
<point key="canvasLocation" x="210" y="1128"/>
</scene>
<!--Site ListVC-->
<!--Domain ListVC-->
<scene sceneID="aZt-6w-TFl">
<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">
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<autoresizingMask key="autoresizingMask"/>
@ -805,7 +822,7 @@ Gw
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj">
<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"/>
<size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -825,7 +842,7 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Secure"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<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"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
@ -848,7 +865,7 @@ Gw
</tableCellView>
</prototypeCellViews>
</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">
<color key="textColor" name="headerTextColor" 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"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListNameCell" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="69" y="0.0" width="290" height="54"/>
<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="200" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<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"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListPhpCell" wantsLayer="YES" id="T49-0U-d58" customClass="SiteListPhpCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="376" y="0.0" width="100" height="54"/>
<tableCellView identifier="domainListPhpCell" wantsLayer="YES" id="T49-0U-d58" customClass="DomainListPhpCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="286" y="0.0" width="100" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
@ -965,8 +982,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Kind"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="SiteListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="493" y="0.0" width="36" height="54"/>
<tableCellView identifier="domainListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="DomainListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="403" y="0.0" width="36" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
@ -1002,8 +1019,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Type"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="SiteListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="546" y="0.0" width="97" height="54"/>
<tableCellView identifier="domainListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="DomainListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="456" y="0.0" width="97" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
@ -1060,7 +1077,7 @@ Gw
<autoresizingMask key="autoresizingMask"/>
</scroller>
<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"/>
</tableHeaderView>
</scrollView>
@ -1088,12 +1105,377 @@ Gw
</viewController>
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</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>
</scenes>
<resources>
<image name="Checkmark" width="512" height="512"/>
<image name="IconLinked" width="25" height="25"/>
<image name="IconProxy" width="25" height="25"/>
<image name="Lock" width="30" height="30"/>
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
<image name="plus" catalog="system" width="14" height="13"/>

View File

@ -23,13 +23,13 @@ class InterApp {
static func getCommands() -> [InterApp.Action] { return [
InterApp.Action(command: "list", action: { _ in
SiteListVC.show()
DomainListVC.show()
}),
InterApp.Action(command: "services/stop", action: { _ in
MainMenu.shared.stopAllServices()
MainMenu.shared.stopValetServices()
}),
InterApp.Action(command: "services/restart/all", action: { _ in
MainMenu.shared.restartAllServices()
MainMenu.shared.restartValetServices()
}),
InterApp.Action(command: "services/restart/nginx", action: { _ in
MainMenu.shared.restartNginx()
@ -57,11 +57,13 @@ class InterApp {
MainMenu.shared.switchToPhpVersion(version)
} else {
BetterAlert().withInformation(
title: "Unsupported version",
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
).withPrimary(text: "OK").show()
title: "alert.php_switch_unavailable.title".localized,
subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
).withPrimary(
text: "alert.php_switch_unavailable.ok".localized
).show()
}
}),
})
]}
}

View 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
}
}

View File

@ -2,7 +2,7 @@
// Environment.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
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 true, then all checks succeeded and the app can continue.
*/
func checkEnvironment() async -> Bool
{
func checkEnvironment() async -> Bool {
// Do the important system setup checks
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) },
name: "`/private/etc/sudoers.d/brew` contains brew",
titleText: "startup.errors.sudoers_brew.title".localized,
subtitleText: "startup.errors.sudoers_brew.subtitle".localized
subtitleText: "startup.errors.sudoers_brew.subtitle".localized,
descriptionText: "startup.errors.sudoers_brew.desc".localized
),
EnvironmentCheck(
command: { return !Shell.pipe("cat /private/etc/sudoers.d/valet").contains(Paths.valet) },
name: "`/private/etc/sudoers.d/valet` contains valet",
titleText: "startup.errors.sudoers_valet.title".localized,
subtitleText: "startup.errors.sudoers_valet.subtitle".localized
subtitleText: "startup.errors.sudoers_valet.subtitle".localized,
descriptionText: "startup.errors.sudoers_valet.desc".localized
),
// =================================================================================
// Verify if the Homebrew services are running (as root).
@ -183,12 +184,41 @@ class Startup {
descriptionText: "startup.errors.valet_json_invalid.desc".localized
),
// =================================================================================
// Check for `which` alias issue
// =================================================================================
EnvironmentCheck(
command: {
return App.architecture == "x86_64"
&& FileManager.default.fileExists(atPath: "/usr/local/bin/which")
&& Shell.pipe("which node", requiresPath: false)
.contains("env: node: No such file or directory")
},
name: "`env: node` issue does not apply",
titleText: "startup.errors.which_alias_issue.title".localized,
subtitleText: "startup.errors.which_alias_issue.subtitle".localized,
descriptionText: "startup.errors.which_alias_issue.desc".localized
),
// =================================================================================
// Determine that Valet works correctly (no issues in platform detected)
// =================================================================================
EnvironmentCheck(
command: {
return valet("--version", sudo: false)
.contains("Composer detected issues in your platform")
},
name: "`no global composer issues",
titleText: "startup.errors.global_composer_platform_issues.title".localized,
subtitleText: "startup.errors.global_composer_platform_issues.subtitle".localized,
descriptionText: "startup.errors.global_composer_platform_issues.desc".localized
),
// =================================================================================
// Determine the Valet version and ensure it isn't unknown.
// =================================================================================
EnvironmentCheck(
command: {
Valet.shared.version = VersionExtractor.from(valet("--version", sudo: false))
return Valet.shared.version == nil
let output = valet("--version", sudo: false)
Valet.shared.version = VersionExtractor.from(output)
return Valet.shared.version == nil && output.contains("Laravel Valet")
},
name: "`valet --version` was loaded",
titleText: "startup.errors.valet_version_unknown.title".localized,

View File

@ -0,0 +1,169 @@
//
// AddSiteVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 24/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class AddProxyVC: NSViewController, NSTextFieldDelegate {
// MARK: - Outlets
@IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var textFieldProxySubject: NSTextField!
@IBOutlet weak var textFieldDomainName: NSTextField!
@IBOutlet weak var inputProxySubject: NSTextField!
@IBOutlet weak var inputDomainName: NSTextField!
@IBOutlet weak var previewText: NSTextField!
@IBOutlet weak var buttonSecure: NSButton!
@IBOutlet weak var buttonCreateProxy: NSButton!
@IBOutlet weak var buttonCancel: NSButton!
@IBOutlet weak var textFieldSecure: NSTextField!
@IBOutlet weak var textFieldError: NSTextField!
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
loadStaticLocalisedStrings()
buttonCreateProxy.isEnabled = false
updatePreview()
validate()
}
private func dismissView(outcome: NSApplication.ModalResponse) {
guard let window = view.window, let parent = window.sheetParent else { return }
parent.endSheet(window, returnCode: outcome)
}
// MARK: - Localisation
func loadStaticLocalisedStrings() {
textFieldTitle.stringValue = "domain_list.add.set_up_proxy".localized
textFieldProxySubject.stringValue = "domain_list.add.proxy_subject".localized
textFieldDomainName.stringValue = "domain_list.add.domain_name".localized
textFieldSecure.stringValue = "domain_list.add.secure_description".localized
buttonCancel.title = "domain_list.add.cancel".localized
buttonCreateProxy.title = "domain_list.add.create_proxy".localized
}
// MARK: - Outlet Interactions
@IBAction func pressedSecure(_ sender: Any) {
updatePreview()
}
@IBAction func pressedCreateProxy(_ sender: Any) {
let domain = self.inputDomainName.stringValue
let proxyName = self.inputProxySubject.stringValue
let secure = self.buttonSecure.state == .on ? " --secure" : ""
dismissView(outcome: .OK)
App.shared.domainListWindowController?.contentVC.setUIBusy()
DispatchQueue.global(qos: .userInitiated).async {
Shell.run("\(Paths.valet) proxy \(domain) \(proxyName)\(secure)", requiresPath: true)
Actions.restartNginx()
DispatchQueue.main.async {
App.shared.domainListWindowController?.contentVC.setUINotBusy()
App.shared.domainListWindowController?.pressedReload(nil)
}
}
}
@IBAction func pressedCancel(_ sender: Any) {
dismissView(outcome: .cancel)
}
// MARK: - Text Field Delegate
func controlTextDidChange(_ obj: Notification) {
updateTextField()
}
// MARK: - Helper Methods
private func validate() {
_ = validate(
domain: inputDomainName.stringValue,
proxy: inputProxySubject.stringValue
)
}
private func validate(domain: String, proxy: String) -> Bool {
if proxy.isEmpty {
textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.empty_proxy".localized
return false
}
if proxy.range(of: #"(http:\/\/|https:\/\/)(.+)(:)(\d+)$"#, options: .regularExpression) == nil {
textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.subject_invalid".localized
return false
}
if domain.isEmpty {
textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.empty".localized
return false
}
if Valet.shared.sites.contains(where: { $0.name == domain }) {
textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
return false
}
textFieldError.isHidden = true
return true
}
func updateTextField() {
inputDomainName.stringValue = inputDomainName.stringValue
.replacingOccurrences(of: " ", with: "-")
inputProxySubject.stringValue = inputProxySubject.stringValue
.replacingOccurrences(of: " ", with: "-")
buttonCreateProxy.isEnabled = validate(
domain: inputDomainName.stringValue,
proxy: inputProxySubject.stringValue
)
updatePreview()
}
func updatePreview() {
buttonSecure.title = "domain_list.add.secure_after_creation"
.localized(
inputDomainName.stringValue,
Valet.shared.config.tld
)
if inputProxySubject.stringValue.isEmpty || inputDomainName.stringValue.isEmpty {
previewText.stringValue = "domain_list.add.empty_fields".localized
return
}
previewText.stringValue = "domain_list.add.proxy_available"
.localized(
inputProxySubject.stringValue,
buttonSecure.state == .on ? "https" : "http",
inputDomainName.stringValue,
Valet.shared.config.tld
)
}
}

View File

@ -11,14 +11,19 @@ import Cocoa
class AddSiteVC: NSViewController, NSTextFieldDelegate {
// MARK: - Outlets
@IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var pathControl: NSPathControl!
@IBOutlet weak var linkName: NSTextField!
@IBOutlet weak var inputDomainName: NSTextField!
@IBOutlet weak var previewText: NSTextField!
@IBOutlet weak var buttonSecure: NSButton!
@IBOutlet weak var buttonCreateLink: NSButton!
@IBOutlet weak var buttonCancel: NSButton!
@IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var textFieldSecure: NSTextField!
@IBOutlet weak var textFieldError: NSTextField!
@ -37,27 +42,28 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
// MARK: - Localisation
func loadStaticLocalisedStrings() {
textFieldTitle.stringValue = "site_list.add.link_folder".localized
linkName.placeholderString = "site_list.add.domain_name_placeholder".localized
textFieldSecure.stringValue = "site_list.add.secure_description".localized
buttonCancel.stringValue = "site_list.add.cancel".localized
textFieldTitle.stringValue = "domain_list.add.link_folder".localized
inputDomainName.placeholderString = "domain_list.add.domain_name_placeholder".localized
textFieldSecure.stringValue = "domain_list.add.secure_description".localized
buttonCancel.title = "domain_list.add.cancel".localized
buttonCreateLink.title = "domain_list.add.create_link".localized
}
// MARK: - Outlet Interactions
@IBAction func pressedCreateLink(_ sender: Any) {
let path = self.pathControl.url!.path
let name = self.linkName.stringValue
let path = pathControl.url!.path
let name = inputDomainName.stringValue
if !FileManager.default.fileExists(atPath: path) {
Alert.confirm(
onWindow: self.view.window!,
messageText: "site_list.alert.folder_missing.title".localized,
informativeText: "site_list.alert.folder_missing.desc".localized,
buttonTitle: "site_list.alert.folder_missing.cancel".localized,
secondButtonTitle: "site_list.alert.folder_missing.return".localized,
onFirstButtonPressed: {
self.dismissView(outcome: .cancel)
onWindow: view.window!,
messageText: "domain_list.alert.folder_missing.title".localized,
informativeText: "domain_list.alert.folder_missing.desc".localized,
buttonTitle: "domain_list.alert.folder_missing.cancel".localized,
secondButtonTitle: "domain_list.alert.folder_missing.return".localized,
onFirstButtonPressed: { [self] in
dismissView(outcome: .cancel)
}
)
return
@ -67,15 +73,15 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
// TODO: I will have to investigate and report this behaviour if possible
Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links", requiresPath: true)
self.dismissView(outcome: .OK)
dismissView(outcome: .OK)
// Reset search
App.shared.siteListWindowController?
App.shared.domainListWindowController?
.searchToolbarItem
.searchField.stringValue = ""
// Add the new item and scrolls to it
App.shared.siteListWindowController?
App.shared.domainListWindowController?
.contentVC
.addedNewSite(
name: name,
@ -84,7 +90,7 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
}
@IBAction func pressedCancel(_ sender: Any) {
self.dismissView(outcome: .cancel)
dismissView(outcome: .cancel)
}
@IBAction func pressedSecure(_ sender: Any) {
@ -100,41 +106,46 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
// MARK: - Helper Methods
private func isValidLinkName(_ name: String) -> Bool {
if self.linkName.stringValue.isEmpty {
self.textFieldError.isHidden = false
self.textFieldError.stringValue = "site_list.add.errors.empty".localized
if name.isEmpty {
textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.empty".localized
return false
}
if Valet.shared.sites.contains(where: { $0.name == name }) {
self.textFieldError.isHidden = false
self.textFieldError.stringValue = "site_list.add.errors.already_exists".localized
textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
return false
}
self.textFieldError.isHidden = true
textFieldError.isHidden = true
return true
}
func updateTextField() {
self.linkName.stringValue = self.linkName.stringValue
inputDomainName.stringValue = inputDomainName.stringValue
.replacingOccurrences(of: " ", with: "-")
buttonCreateLink.isEnabled = isValidLinkName(self.linkName.stringValue)
self.updatePreview()
buttonCreateLink.isEnabled = isValidLinkName(inputDomainName.stringValue)
updatePreview()
}
func updatePreview() {
buttonSecure.title = "site_list.add.secure_after_creation"
buttonSecure.title = "domain_list.add.secure_after_creation"
.localized(
self.linkName.stringValue,
inputDomainName.stringValue,
Valet.shared.config.tld
)
previewText.stringValue = "site_list.add.folder_available"
if inputDomainName.stringValue.isEmpty {
previewText.stringValue = "domain_list.add.empty_fields".localized
return
}
previewText.stringValue = "domain_list.add.folder_available"
.localized(
self.buttonSecure.state == .on ? "https" : "http",
self.linkName.stringValue,
buttonSecure.state == .on ? "https" : "http",
inputDomainName.stringValue,
Valet.shared.config.tld
)
}

View File

@ -0,0 +1,15 @@
//
// DomainListCellProtocol.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 03/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
import AppKit
protocol DomainListCellProtocol {
func populateCell(with site: ValetSite)
func populateCell(with proxy: ValetProxy)
}

View File

@ -1,5 +1,5 @@
//
// SiteListTypeCell.swift
// DomainListTypeCell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/03/2022.
@ -9,9 +9,8 @@
import Cocoa
import AppKit
class SiteListKindCell: NSTableCellView, SiteListCellProtocol
{
static let reusableName = "siteListKindCell"
class DomainListKindCell: NSTableCellView, DomainListCellProtocol {
static let reusableName = "domainListKindCell"
@IBOutlet weak var imageViewType: NSImageView!
@ -30,4 +29,8 @@ class SiteListKindCell: NSTableCellView, SiteListCellProtocol
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
}
func populateCell(with proxy: ValetProxy) {
imageViewType.image = NSImage(named: "IconProxy")
}
}

View File

@ -0,0 +1,27 @@
//
// DomainListNameCell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
import AppKit
class DomainListNameCell: NSTableCellView, DomainListCellProtocol {
static let reusableName = "domainListNameCell"
@IBOutlet weak var labelSiteName: NSTextField!
@IBOutlet weak var labelPathName: NSTextField!
func populateCell(with site: ValetSite) {
labelSiteName.stringValue = "\(site.name).\(site.tld)"
labelPathName.stringValue = site.absolutePathRelative
}
func populateCell(with proxy: ValetProxy) {
labelSiteName.stringValue = "\(proxy.domain).\(proxy.tld)"
labelPathName.stringValue = proxy.target
}
}

View File

@ -0,0 +1,77 @@
//
// DomainListPhpCell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
import AppKit
import SwiftUI
class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
static let reusableName = "domainListPhpCell"
var site: ValetSite?
@IBOutlet weak var buttonPhpVersion: NSButton!
@IBOutlet weak var imageViewPhpVersionOK: NSImageView!
func populateCell(with site: ValetSite) {
self.site = site
buttonPhpVersion.isHidden = false
imageViewPhpVersionOK.isHidden = false
buttonPhpVersion.title = " PHP \(site.servingPhpVersion)"
imageViewPhpVersionOK.toolTip = nil
imageViewPhpVersionOK.contentTintColor = site.composerPhpCompatibleWithLinked
? NSColor(named: "IconColorGreen")
: NSColor(named: "IconColorRed")
if site.isolatedPhpVersion != nil {
imageViewPhpVersionOK.isHidden = false
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.isolated".localized(site.servingPhpVersion)
} else {
imageViewPhpVersionOK.isHidden = (site.composerPhp == "???" || !site.composerPhpCompatibleWithLinked)
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp)
}
}
func populateCell(with proxy: ValetProxy) {
buttonPhpVersion.isHidden = true
imageViewPhpVersionOK.isHidden = true
return
}
@IBAction func pressedPhpVersion(_ sender: Any) {
guard let site = self.site else { return }
var validPhpSuggestions: [PhpVersionNumber] {
if site.isolatedPhpVersion != nil {
return []
}
return PhpEnv.shared.validVersions(for: site.composerPhp).filter({ version in
version.homebrewVersion != PhpEnv.phpInstall.version.short
})
}
let button = self.buttonPhpVersion!
let popover = NSPopover()
let view = VersionPopoverView(site: site, validPhpVersions: validPhpSuggestions, parent: popover)
popover.contentViewController = NSHostingController(rootView: view)
popover.behavior = .transient
popover.animates = true
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)
}
}

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