Compare commits
214 Commits
Author | SHA1 | Date | |
---|---|---|---|
94da7fb255 | |||
46d2d35c1a | |||
cba41b29bc | |||
7a8f47b995 | |||
40062c5091 | |||
6081ef6b02 | |||
4cffe5a662 | |||
813aec2b42 | |||
f2452bbc70 | |||
28fb685bfc | |||
0661ca00ff | |||
0b04619003 | |||
85d7b6aa57 | |||
bba961269c | |||
d0f7d2c5e9 | |||
eeeb3eb184 | |||
f00f8d26f6 | |||
74817beec6 | |||
7b6809245c | |||
5b40a8fd41 | |||
193f459be1 | |||
c4c19a5b47 | |||
7d103c70e7 | |||
2ffe90948e | |||
8e61aaacde | |||
29c8fcbde2 | |||
8dd21f46aa | |||
e688dde2aa | |||
987e1e1bdb | |||
510257c436 | |||
bb1572f32a | |||
45276034b1 | |||
0d4a144524 | |||
a0e5102ca7 | |||
69c0f5ace9 | |||
d0962c2387 | |||
4670894cfd | |||
a2f6c70a03 | |||
ef469868d8 | |||
c9ba872529 | |||
1e15042be2 | |||
7647978da5 | |||
f82f3052f2 | |||
10b299ff65 | |||
e4ff0418fd | |||
a2b25e31ca | |||
c4772db808 | |||
b59a5d31a5 | |||
e4b1f75c53 | |||
338a87d503 | |||
0f0e91273e | |||
20959501c9 | |||
aeeecd6996 | |||
e9ae989200 | |||
f6378e7b73 | |||
d7e8652f5f | |||
5293c437d1 | |||
f75bfc9c4a | |||
42fc0e3698 | |||
626b7a735d | |||
567373f8da | |||
32e8878a68 | |||
46005a3c68 | |||
03a409281a | |||
e0dd778bb3 | |||
c3f8a53ac3 | |||
d8579bd7d1 | |||
d2cd567fd2 | |||
a5212b436e | |||
b16250c2be | |||
3b4a1a0654 | |||
9ab6231337 | |||
38c2d9131b | |||
1566323fca | |||
dc44538a7b | |||
bf0a923eb2 | |||
04bf5a3251 | |||
23f3204fa8 | |||
6dc74e94aa | |||
422aefe831 | |||
3c3a0c8b45 | |||
0cdeeec0a4 | |||
e4c3e78a8a | |||
7f320897be | |||
9ef184331e | |||
e372480249 | |||
3bca3117f9 | |||
722e082526 | |||
40a0bd6cab | |||
78510ea3fe | |||
8624573e74 | |||
dd251936b9 | |||
c647aee8ea | |||
b7766aeec2 | |||
5af1f09ee1 | |||
6646ceda76 | |||
0b05bb44a2 | |||
1fbb1a8aa8 | |||
8cb2074d76 | |||
c408d62118 | |||
665bba86dd | |||
0d29fbf796 | |||
69042042ea | |||
63f4f8b078 | |||
e76c6e14e4 | |||
ceb168c6cf | |||
a6387e96e7 | |||
2dbf775ad6 | |||
acdcce7f7a | |||
7a3dc9a145 | |||
1ca49f6cbc | |||
fa2de1f77c | |||
fe695bb026 | |||
f82a3bb008 | |||
e7df254dcc | |||
ea9538f116 | |||
0d75e4c3b2 | |||
a90703e525 | |||
f74f9f69b2 | |||
a950587e84 | |||
267a1dac94 | |||
ed49362291 | |||
3f0f070245 | |||
bd79f42e96 | |||
35ae681c2d | |||
313e806414 | |||
a8dc366038 | |||
eaf653e3c0 | |||
5c391917d2 | |||
09b5aa7f93 | |||
66a8c17f1f | |||
adc31984a8 | |||
8114eef381 | |||
9190420c66 | |||
e5ba074936 | |||
e4f1efe26a | |||
498f4e7b79 | |||
d9a526e828 | |||
2f93b4980b | |||
ac0ca06d7f | |||
17320a19cf | |||
3faa251216 | |||
a9f140fabc | |||
b6b5a94bbd | |||
c05f0fe5cb | |||
eaf1423fb1 | |||
7feb13856d | |||
ca2ca9df3b | |||
4259915ff6 | |||
89e7a9b1ea | |||
8c25d23d09 | |||
f44811b9dc | |||
f65fd513f2 | |||
327c88a745 | |||
63aa8c2f44 | |||
afbfc55088 | |||
d13714c1ea | |||
92a6d506dc | |||
e381880675 | |||
7e185154ef | |||
27c25378b1 | |||
1159a6cc2e | |||
489bf13707 | |||
5d3faceb5a | |||
29d34a6b62 | |||
be80d74141 | |||
d37e86ce2c | |||
d8fc857d23 | |||
e0bec333ed | |||
46867ad25e | |||
924edf6f96 | |||
010c8eddde | |||
96602b1a9c | |||
d536499799 | |||
ad016c54b2 | |||
f8b0b38e9e | |||
912e549104 | |||
93bdb0ed7f | |||
87713bbe64 | |||
dce27059ff | |||
c919326480 | |||
1e124a90f3 | |||
d1fc9de4bd | |||
04db3f50ed | |||
4e347adf69 | |||
79de14c9aa | |||
7448e89965 | |||
967743715b | |||
d37913005b | |||
0986b97051 | |||
bbb04f7907 | |||
015f406ddf | |||
e1a97672b5 | |||
493b5945f9 | |||
52606aae8b | |||
2d6ca0f841 | |||
34900f929f | |||
5dbd05fdfb | |||
fe3cf9adb1 | |||
9bc8460cce | |||
4cbd2fd6eb | |||
6fef3fe37a | |||
72a20d1ed9 | |||
73ed80434a | |||
a78672927b | |||
4256eae442 | |||
76412b68f3 | |||
9153bb140a | |||
c9c15d10f9 | |||
e8c2277ef5 | |||
23720c5dc9 | |||
f881f07cba | |||
b072ee8dec | |||
acfbc0b66f |
35
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
Hello there! Thank you for considering a pull request for PHP Monitor.
|
||||
|
||||
Please read the text below first before you submit your PR.
|
||||
|
||||
## Do not PR unless...
|
||||
|
||||
In order to make development and maintenance of PHP Monitor easier, I ask that you _avoid_ making a pull request in the following situations:
|
||||
|
||||
* No issue has been associated with the changes you‘d like to merge
|
||||
* You have not announced you will be addressing a particular issue
|
||||
* The PR is a low effort change: e.g. commits that only fix typos or phrasing may not be accepted
|
||||
|
||||
(If you believe the phrasing of particular text in the app is unclear or incorrect, please open an issue first.)
|
||||
|
||||
In short: It is usually best to *get in touch first* if you are making substantial changes.
|
||||
|
||||
## About destination branches
|
||||
|
||||
Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Merge requests that target `main` will be closed without mercy.**
|
||||
|
||||
Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released.
|
||||
|
||||
There may be a newer branch available, which is an appropriate place for bigger changes, but please keep in mind that it is usually best to announce you‘ll be working on such a change before you spend the time, since as the lead contributor I might not even want said change in the app. Thank you.
|
||||
|
||||
## Your changes
|
||||
|
||||
(feel free to remove the disclaimer above)
|
||||
|
||||
* Affected parts of the app: shared code / UI code / CLI (remove what does not apply)
|
||||
* Estimated impact on performance: none / low / high (remove what does not apply)
|
||||
* Made a new build with Xcode and tested this: yes / no (remove what does not apply)
|
||||
* Tested on macOS version + architecture: (e.g. "Monterey on M1" or "Big Sur on Intel")
|
||||
* References issue(s): (please reference the issue here, using # and the number of the issue)
|
||||
|
||||
(please describe what you have changed here)
|
@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C415D3D52770F341005EF286"
|
||||
BuildableName = "phpmon-cli"
|
||||
BlueprintName = "phpmon-cli"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C415D3D52770F341005EF286"
|
||||
BuildableName = "phpmon-cli"
|
||||
BlueprintName = "phpmon-cli"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "help"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C415D3D52770F341005EF286"
|
||||
BuildableName = "phpmon-cli"
|
||||
BlueprintName = "phpmon-cli"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1220"
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
168
README.md
@ -1,15 +1,17 @@
|
||||
# PHP Monitor
|
||||
|
||||
> If this software has been useful to you, all I ask is that you **please star the repository**, so I know that the software is being used.
|
||||
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project.
|
||||
> You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy.<br>**Thank you!** ⭐️
|
||||
|
||||
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
|
||||
<h1 align="center"><b>PHP Monitor</b> (phpmon)</h1>
|
||||
|
||||
**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 you need to have it set up before you can use this.
|
||||
<p align="center">
|
||||
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
|
||||
</p>
|
||||
|
||||
<img src="./docs/screenshot34.png" width="412px" alt="phpmon screenshot (menu bar app)"/>
|
||||
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this</u>.
|
||||
|
||||
<small><i>Screenshot: A menu showing all of the functionality of PHP Monitor.</i></small>
|
||||
<img src="./docs/screenshot50.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
|
||||
|
||||
<small><i>Screenshot: Showing the key functionality of PHP Monitor. You can also add new domains as links, manage various services, and perform First Aid to fix all kinds of common PHP link issues.</i></small>
|
||||
|
||||
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
|
||||
|
||||
@ -19,18 +21,18 @@ PHP Monitor also gives you quick access to various useful functionality (like ac
|
||||
|
||||
## 🖥 System requirements
|
||||
|
||||
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
|
||||
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
|
||||
|
||||
* macOS 10.14 Mojave or higher (works on macOS 11 Big Sur)
|
||||
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
|
||||
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
|
||||
* The brew formula `php` has to be installed (which version is detected)
|
||||
* Laravel Valet 2.13 or higher
|
||||
* Laravel Valet 2.16.2 or higher (older versions might be compatible but are not supported)
|
||||
|
||||
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released._
|
||||
_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`._
|
||||
|
||||
## 🚀 How to install
|
||||
|
||||
You can install via Homebrew, or may download the latest [release](https://github.com/nicoverbruggen/phpmon/releases).
|
||||
Again, make sure you have **Laravel Valet** installed first. Once that's done, you can install via Homebrew (recommended), or may download the latest release on GitHub.
|
||||
|
||||
To install via Homebrew, run:
|
||||
|
||||
@ -41,7 +43,11 @@ To upgrade your existing installation, run:
|
||||
|
||||
brew upgrade phpmon
|
||||
|
||||
_The app is signed and notarized, meaning all you have to do is approve its first launch._
|
||||
(You may need to run `brew update` first in order to update the cask file if you ran a Homebrew operation recently.)
|
||||
|
||||
## 🔑 Is the app signed & notarized?
|
||||
|
||||
Yes, the app is signed and notarized, meaning all you have to do is approve its first launch (or whenever it updates).
|
||||
|
||||
## 👨💻 Why build this?
|
||||
|
||||
@ -51,10 +57,12 @@ Initially, I had an Alfred workflow for this — but it has now been replaced wi
|
||||
|
||||
## 🤬 The app won't start?!
|
||||
|
||||
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
|
||||
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
|
||||
|
||||
**Follow instructions as specified in the alert in order to resolve any issues.**
|
||||
|
||||
(If the app crashes at launch without showing you any of these messages, you might have a non-standard Homebrew and Valet setup. Those are not supported.)
|
||||
|
||||
## 🙋♂️ FAQ & Troubleshooting
|
||||
|
||||
> If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. To upgrade Valet, run `composer global update`. Don't forget to run `valet install` after upgrading.
|
||||
@ -73,6 +81,7 @@ If you're still having issues, here's a few common questions & answers, as well
|
||||
<li>PHP 7.4</li>
|
||||
<li>PHP 8.0</li>
|
||||
<li>PHP 8.1</li>
|
||||
<li>PHP 8.2 (experimental)</li>
|
||||
</ul>
|
||||
|
||||
For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Constants.swift#L16) file to see which versions are supported.
|
||||
@ -110,9 +119,15 @@ 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:
|
||||
and add the following to your .zshrc, 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:
|
||||
|
||||
export PATH=$HOME/bin:/usr/local/bin:$PATH
|
||||
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
||||
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
|
||||
|
||||
Make sure PHP is linked correctly:
|
||||
|
||||
@ -230,18 +245,95 @@ PHP Monitor itself doesn't do any network requests. Feel free to check the sourc
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>After running PHP Monitor, Homebrew sometimes has issues with `brew upgrade`!</strong></summary>
|
||||
<summary><strong>How do I get various applications to show up in the domain list's right-click menu?</strong></summary>
|
||||
|
||||
This is a security feature of Brew. When you start a service as an administrator, the root user becomes the owner of relevant binaries.
|
||||
When you select and right-click on a domain, you can open these directories with various applications. This can help speed up your workflow. However, for these apps to show up, they must be detected first.
|
||||
|
||||
You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder).
|
||||
The supported apps are: <i>PhpStorm, Visual Studio Code, Sublime Text, Sublime Merge, iTerm</i>.
|
||||
|
||||
All of these apps should just be detected correctly, no matter their location on your system. If you can open it using `open -a "appname"`, the app should be detected and work. If you have renamed the app, there might be an issue getting it detected.
|
||||
|
||||
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:
|
||||
|
||||
<pre>
|
||||
{
|
||||
"scan_apps": ["Xcode", "Kraken"]
|
||||
}
|
||||
</pre>
|
||||
|
||||
You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How can the app integrate with third party tools, like Alfred?</strong></summary>
|
||||
|
||||
There's an Alfred workflow usually included in the release list, you can grab it by going to releases and downloading the asset `phpmon.alfredworkflow`.
|
||||
|
||||
If you'd like to integrate something yourself, all you need to to is use the `phpmon://` protocol and ensure that third party app integrations are enabled in Preferences (in PHP Monitor).
|
||||
|
||||
Using app callbacks, macOS and PHP Monitor allow for the following to be called:
|
||||
|
||||
* phpmon://list
|
||||
* phpmon://services/stop
|
||||
* phpmon://services/restart/all
|
||||
* phpmon://services/restart/nginx
|
||||
* phpmon://services/restart/php
|
||||
* phpmon://services/restart/dnsmasq
|
||||
* phpmon://locate/config
|
||||
* phpmon://locate/composer
|
||||
* phpmon://locate/valet
|
||||
* phpmon://phpinfo
|
||||
* phpmon://switch/php/{version}
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How does the app know what PHP version is required for my app?</strong></summary>
|
||||
|
||||
The `composer.json` file in the root of the folder (if it exists) is scanned and interpreted.
|
||||
|
||||
If the version is set in `platform`, it takes precendence.
|
||||
If the version is not set in `platform` but it is in `require` (most common) then that version is used.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>What do the checkmarks next to the PHP version mean in the site list?</strong></summary>
|
||||
|
||||
You'll see a checkmark next to the version number if the currently enabled PHP version is compatible with the version required to run the site.
|
||||
|
||||
This is determined by evaluating the PHP requirement constraint (e.g. `^8.0`, `~8.0` or a specific version: `8.0`).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Why can't I see the driver type any more? It says "Project Type" now.</strong></summary>
|
||||
|
||||
PHP Monitor currently checks your `composer.json` file to try to figure out what project you are running.
|
||||
|
||||
This approach is a lot faster than asking for a driver when you have many sites linked, but is slightly less reliable since the framework or type of project inferred via `composer.json` might not be 100% accurate.
|
||||
|
||||
You can always still ask Valet using the command line, should it be necessary. In my experience fetching the drivers slowed down the app unnecessarily.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>After running PHP Monitor, Homebrew sometimes has issues with `brew upgrade` or `brew cleanup`!</strong></summary>
|
||||
|
||||
This is a security feature of Homebrew. When you start a service as an administrator, the root user becomes the owner of relevant binaries. You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder).
|
||||
|
||||
If you would like to know more, consult [this issue](https://github.com/nicoverbruggen/phpmon/issues/85) for more information.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>The app has crashed!</strong></summary>
|
||||
|
||||
Please get in touch and open an issue. PHP Monitor shouldn't crash :)
|
||||
Please get in touch and open an issue. PHP Monitor shouldn't crash... (unless you are actually removing PHP *while* the app is running, that’s considered normal behaviour!)
|
||||
|
||||
If you would like to report a crash, please include the associated **log files** so I can find out what exactly went wrong.
|
||||
|
||||
To find the logs, take a look in `~/Library/Logs/DiagnosticReports` (in Finder) and see if there's any (log) files that start with "PHP Monitor".
|
||||
|
||||
</details>
|
||||
|
||||
@ -262,11 +354,10 @@ Donations really help with the Apple Developer Program cost, and keep me motivat
|
||||
While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks:
|
||||
|
||||
* My colleagues at [DIVE](https://dive.be)
|
||||
* The [Homebrew](https://brew.sh/) team who maintain
|
||||
* The [developers & maintainers of Valet](https://github.com/laravel/valet/graphs/contributors)
|
||||
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors)
|
||||
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so
|
||||
* Everyone in the Laravel community who shared the app (thanks!)
|
||||
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot)
|
||||
* Everyone who left feedback via issues
|
||||
* Everyone who left feedback via issues & who donated to keep the project up and running
|
||||
|
||||
Thank you very much for your contributions, kind words and support.
|
||||
|
||||
@ -274,7 +365,7 @@ Thank you very much for your contributions, kind words and support.
|
||||
|
||||
### Loading info about PHP in the background
|
||||
|
||||
This utility runs `php-config --version'` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
|
||||
This utility runs `php-config --version` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
|
||||
|
||||
In order to save power, this only happens once every 60 seconds.
|
||||
|
||||
@ -282,26 +373,35 @@ In order to save power, this only happens once every 60 seconds.
|
||||
|
||||
This utility will detect which PHP versions you have installed via Homebrew, and then allows you to switch between them.
|
||||
|
||||
This means:
|
||||
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be much faster than Valet’s switcher.
|
||||
|
||||
- You have at least the latest version of PHP installed (`php`)
|
||||
- You have installed Laravel Valet (`which valet` returns `/usr/local/bin/valet`)
|
||||
- You ran `valet trust`, which means Valet commands can be run without using sudo
|
||||
### Config change detection
|
||||
|
||||
The utility runs the following commands:
|
||||
PHP Monitor watches your filesystem in the relevant `conf.d` directory for the currently linked PHP version.
|
||||
|
||||
- Unlink all detected PHP versions & stop the respective `php@X.X` services
|
||||
- Link the desired version of PHP, and start the associated service
|
||||
Whenever an .ini file is modified, PHP Monitor will attempt to reload the current information about the active PHP installation.
|
||||
|
||||
If an extension or other process writes to a single file a bunch of times in a short span of time (< 1 sec), PHP Monitor will only reload the active configuration information after a while (with a slight delay).
|
||||
|
||||
### Site detection
|
||||
|
||||
1. **Location of your sites**: PHP Monitor uses the Valet configuration file to determine which folders to look into. Each folder is scanned and then PHP Monitor will validate if a composer.json file exists to determine the desired PHP version.
|
||||
1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name.
|
||||
1. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all.
|
||||
|
||||
*Note*: If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
|
||||
|
||||
### Want to know more?
|
||||
|
||||
If you want to know more about how this works, I recommend you check out the source code.
|
||||
If you want to know more about how this works, I recommend you check out the source code.
|
||||
|
||||
This app isn't very complicated after all. In the end, this just (conveniently) executes some shell commands.
|
||||
I have done my best to annotate as much as humanly possible, and have avoided using an overly complicated architecture to keep the code as easy to maintain as possible. The code isn't perfect by a long shot (lots of cleanup can still happen!) but the application works well.
|
||||
|
||||
I also have a few tests for key parts of the application that I found needed to be tested. In the future, I would like to add even more tests for some of the UI stuff, but for now the tests are more unit tests than feature tests.
|
||||
|
||||
## 🔧 Build instructions
|
||||
|
||||
<img src="./docs/build.png" width="320px" alt="build button in Xcode"/>
|
||||
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
|
||||
|
||||
If you'd like to build PHP Monitor yourself, you need:
|
||||
|
||||
|
27
SECURITY.md
@ -2,15 +2,26 @@
|
||||
|
||||
## Supported versions
|
||||
|
||||
Generally speaking, only the latest version of **PHP Monitor** is supported:
|
||||
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
|
||||
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- |
|
||||
| 3.x | ✅ Universal binary | ✅ | Big Sur (11.0) | macOS 10.14+ |
|
||||
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ |
|
||||
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
|
||||
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
|
||||
| < 2.4 | ❌ Intel binary<br/>`/usr/local/homebrew` installations only | ❌ | Catalina (10.15) | macOS 10.14+ |
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 5.0 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||
|
||||
## Legacy versions
|
||||
|
||||
These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |
|
||||
| < 2.4 | ❌ Intel binary<br/>`/usr/local/homebrew` installations only | ❌ | Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
|
BIN
docs/build.png
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 130 KiB |
BIN
docs/screenshot50.jpg
Normal file
After Width: | Height: | Size: 370 KiB |
BIN
integrations/phpmon.alfredworkflow
Normal file
26
phpmon-cli/AllowedArguments.swift
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// AllowedArguments.swift
|
||||
// phpmon-cli
|
||||
//
|
||||
// Created by Nico Verbruggen on 20/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AllowedArguments: String, CaseIterable {
|
||||
case use = "use"
|
||||
case performSwitch = "switch"
|
||||
case fix = "fix"
|
||||
case help = "help"
|
||||
|
||||
static func has(_ string: String) -> Bool {
|
||||
return Self.allCases.contains { arg in
|
||||
return arg.rawValue == string
|
||||
}
|
||||
}
|
||||
|
||||
static var rawValues: [String] {
|
||||
return Self.allCases.map { $0.rawValue }
|
||||
}
|
||||
}
|
103
phpmon-cli/main.swift
Normal file
@ -0,0 +1,103 @@
|
||||
//
|
||||
// main.swift
|
||||
// phpmon-cli
|
||||
//
|
||||
// Created by Nico Verbruggen on 20/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
let toolver = "0.1 (early access)"
|
||||
|
||||
let log = Log.shared
|
||||
log.verbosity = .info
|
||||
|
||||
if CommandLine.arguments.contains("-q") || CommandLine.arguments.contains("--quiet") {
|
||||
Log.shared.verbosity = .warning
|
||||
}
|
||||
if CommandLine.arguments.contains("-p") || CommandLine.arguments.contains("--performance") {
|
||||
Log.shared.verbosity = .performance
|
||||
}
|
||||
|
||||
var argument = "help"
|
||||
if CommandLine.arguments.count > 1 {
|
||||
argument = CommandLine.arguments[1]
|
||||
}
|
||||
|
||||
if !AllowedArguments.has(argument) {
|
||||
Log.err("The supported arguments are: \(AllowedArguments.rawValues)")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let action = AllowedArguments.init(rawValue: argument)
|
||||
|
||||
switch action {
|
||||
case .use, .performSwitch:
|
||||
if !Shell.fileExists("\(Paths.binPath)/php") {
|
||||
Log.err("PHP is currently not linked. Attempting quick fix...")
|
||||
_ = Shell.user.executeSynchronously("brew link php", requiresPath: true)
|
||||
}
|
||||
|
||||
let phpenv = PhpEnv.shared
|
||||
PhpEnv.detectPhpVersions()
|
||||
|
||||
if CommandLine.arguments.count < 3 {
|
||||
Log.err("You must enter at least two additional arguments when using this command.")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let version = CommandLine.arguments[2].replacingOccurrences(of: "php@", with: "")
|
||||
if phpenv.availablePhpVersions.contains(version) {
|
||||
Log.info("Switching to PHP \(version)...")
|
||||
Actions.switchToPhpVersion(
|
||||
version: version,
|
||||
availableVersions: phpenv.availablePhpVersions,
|
||||
completed: {
|
||||
Log.info("The switch has been completed.")
|
||||
exit(0)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Log.err("A PHP installation with version \(version) is not installed.")
|
||||
Log.err("The installed versions are: \(phpenv.availablePhpVersions.joined(separator: ", ")).")
|
||||
Log.err("If this version is available, you may be able to install it by using `brew install php@\(version)`.")
|
||||
exit(1)
|
||||
}
|
||||
case .fix:
|
||||
Log.info("Fixing your PHP installation...")
|
||||
Actions.fixMyPhp()
|
||||
Log.info("All operations completed. You can check which version of PHP is linked by using `php -v`.")
|
||||
exit(0)
|
||||
case .help:
|
||||
print("""
|
||||
===============================================================
|
||||
PHP MONITOR CLI \(toolver)
|
||||
by Nico Verbruggen
|
||||
===============================================================
|
||||
|
||||
Gives access to the quick version switcher from PHP Monitor,
|
||||
but without the GUI and 100% of the speed!
|
||||
|
||||
SUPPORTED COMMANDS
|
||||
|
||||
* use {version}: Switch to a specific version of PHP.
|
||||
(e.g. `phpmon-cli use 8.0`)
|
||||
* switch {version}: Alias for the `use` command.
|
||||
* fix Attempts to unlink all PHP versions,
|
||||
and link the latest version of PHP.
|
||||
* help: Show this help.
|
||||
|
||||
SUPPORTED FLAGS
|
||||
|
||||
* `-q / --quiet`: Silences all logs except for warnings and exceptions.
|
||||
* `-p / --perf`: Enables performance mode.
|
||||
|
||||
""")
|
||||
exit(0)
|
||||
case .none:
|
||||
Log.err("Action not recognized!")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
RunLoop.main.run()
|
124
phpmon-common/Core/Actions.swift
Normal file
@ -0,0 +1,124 @@
|
||||
//
|
||||
// Services.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
class Actions {
|
||||
|
||||
// MARK: - Services
|
||||
|
||||
public static func restartPhpFpm()
|
||||
{
|
||||
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
|
||||
}
|
||||
|
||||
public static func restartNginx()
|
||||
{
|
||||
brew("services restart nginx", sudo: true)
|
||||
}
|
||||
|
||||
public static func restartDnsMasq()
|
||||
{
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
}
|
||||
|
||||
public static func stopAllServices()
|
||||
{
|
||||
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
|
||||
brew("services stop nginx", sudo: true)
|
||||
brew("services stop dnsmasq", sudo: true)
|
||||
}
|
||||
|
||||
/**
|
||||
Kindly asks Valet to switch to a specific PHP version.
|
||||
*/
|
||||
public static func switchToPhpVersionUsingValet(
|
||||
version: String,
|
||||
availableVersions: [String],
|
||||
completed: @escaping () -> Void
|
||||
) {
|
||||
Log.info("Switching to \(version) using Valet")
|
||||
Log.info(valet("use php@\(version)"))
|
||||
completed()
|
||||
}
|
||||
|
||||
// MARK: - Finding Config Files
|
||||
|
||||
public static func openGenericPhpConfigFolder()
|
||||
{
|
||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
|
||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||
}
|
||||
|
||||
public static func openGlobalComposerFolder()
|
||||
{
|
||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".composer/composer.json")
|
||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||
}
|
||||
|
||||
public static func openPhpConfigFolder(version: String)
|
||||
{
|
||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
|
||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||
}
|
||||
|
||||
public static func openValetConfigFolder()
|
||||
{
|
||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".config/valet")
|
||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||
}
|
||||
|
||||
// MARK: - Other Actions
|
||||
|
||||
public static func createTempPhpInfoFile() -> URL
|
||||
{
|
||||
// Write a file called `phpmon_phpinfo.php` to /tmp
|
||||
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
||||
|
||||
// Tell php-cgi to run the PHP and output as an .html file
|
||||
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
||||
|
||||
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
|
||||
}
|
||||
|
||||
// MARK: - Fix My Valet
|
||||
|
||||
/**
|
||||
Detects all currently available PHP versions,
|
||||
and unlinks each and every one of them.
|
||||
|
||||
After this, the brew services are also stopped,
|
||||
the latest PHP version is linked, and php + nginx are restarted.
|
||||
|
||||
If this does not solve the issue, the user may need to install additional
|
||||
extensions and/or run `composer global update`.
|
||||
*/
|
||||
public static func fixMyValet()
|
||||
{
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
|
||||
PhpEnv.shared.detectPhpVersions().forEach { (version) in
|
||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
brew("unlink php@\(version)")
|
||||
brew("services stop \(formula)")
|
||||
brew("services stop \(formula)", sudo: true)
|
||||
}
|
||||
|
||||
brew("services stop dnsmasq")
|
||||
brew("services stop php")
|
||||
brew("services stop nginx")
|
||||
|
||||
brew("link php --overwrite --force")
|
||||
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
brew("services restart php", sudo: true)
|
||||
brew("services restart nginx", sudo: true)
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Command {
|
||||
public class Command {
|
||||
|
||||
/**
|
||||
Immediately executes a command.
|
||||
@ -34,7 +34,7 @@ class Command {
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
return output;
|
||||
return output
|
||||
}
|
||||
|
||||
}
|
59
phpmon-common/Core/Constants.swift
Normal file
@ -0,0 +1,59 @@
|
||||
//
|
||||
// Constants.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Constants {
|
||||
|
||||
/**
|
||||
* The latest PHP version that is considered to be stable at the time of release.
|
||||
* This version number is currently not used (only as a default fallback).
|
||||
*/
|
||||
static let LatestStablePhpVersion = "8.1"
|
||||
|
||||
/**
|
||||
The minimum version of Valet that is recommended.
|
||||
If the installed version is older, a notification will be shown
|
||||
every time the app launches (with a recommendation to upgrade).
|
||||
|
||||
The minimum requirement is currently synced to PHP 8.1 compatibility.
|
||||
See also: https://github.com/laravel/valet/releases/tag/v2.16.2
|
||||
*/
|
||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||
|
||||
/**
|
||||
* The PHP versions supported by this application.
|
||||
* Versions that do not appear in this array are omitted from the list.
|
||||
*/
|
||||
static let SupportedPhpVersions = [
|
||||
// ====================
|
||||
// STABLE RELEASES
|
||||
// ====================
|
||||
// Versions of PHP that are stable and are supported.
|
||||
"5.6",
|
||||
"7.0",
|
||||
"7.1",
|
||||
"7.2",
|
||||
"7.3",
|
||||
"7.4",
|
||||
"8.0",
|
||||
"8.1",
|
||||
|
||||
// ====================
|
||||
// EXPERIMENTAL SUPPORT
|
||||
// ====================
|
||||
// Every release that supports the next release will always support the next
|
||||
// dev release. In this case, that means that the version below is detected.
|
||||
"8.2"
|
||||
]
|
||||
|
||||
/**
|
||||
The URL that people can visit if they wish to help support the project.
|
||||
*/
|
||||
static let DonationUrl = URL(string: "https://nicoverbruggen.be/sponsor#pay-now")!
|
||||
|
||||
}
|
15
phpmon-common/Core/Events.swift
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// Events.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 23/01/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Events {
|
||||
|
||||
static let ServicesUpdated = Notification.Name("ServicesUpdated")
|
||||
|
||||
}
|
55
phpmon-common/Core/Helpers.swift
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// Helpers.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 24/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
// MARK: Common Shell Commands
|
||||
|
||||
/**
|
||||
Runs a `valet` command.
|
||||
*/
|
||||
func valet(_ command: String) -> String
|
||||
{
|
||||
return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true)
|
||||
}
|
||||
|
||||
/**
|
||||
Runs a `brew` command. Can run as superuser.
|
||||
*/
|
||||
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)
|
||||
{
|
||||
// Escape slashes (or `sed` won't work)
|
||||
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
|
||||
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
|
||||
|
||||
// Check if gsed exists; it is able to follow symlinks,
|
||||
// which we want to do to toggle the extension
|
||||
if Shell.fileExists("\(Paths.binPath)/gsed") {
|
||||
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
} else {
|
||||
Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Uses `grep` to determine whether a particular query string can be found in a particular file.
|
||||
*/
|
||||
func grepContains(file: String, query: String) -> Bool
|
||||
{
|
||||
return Shell.pipe("""
|
||||
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
|
||||
""")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.contains("YES")
|
||||
}
|
52
phpmon-common/Core/Logger.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// Logger.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 21/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Log {
|
||||
|
||||
static var shared = Log()
|
||||
|
||||
enum Verbosity: Int {
|
||||
case error = 1,
|
||||
warning = 2,
|
||||
info = 3,
|
||||
performance = 4
|
||||
|
||||
public func isApplicable() -> Bool {
|
||||
return Log.shared.verbosity.rawValue >= self.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var verbosity: Verbosity = .warning
|
||||
|
||||
static func err(_ item: Any) {
|
||||
if Verbosity.error.isApplicable() {
|
||||
print(item)
|
||||
}
|
||||
}
|
||||
|
||||
static func warn(_ item: Any) {
|
||||
if Verbosity.warning.isApplicable() {
|
||||
print(item)
|
||||
}
|
||||
}
|
||||
|
||||
static func info(_ item: Any) {
|
||||
if Verbosity.info.isApplicable() {
|
||||
print(item)
|
||||
}
|
||||
}
|
||||
|
||||
static func perf(_ item: Any) {
|
||||
if Verbosity.performance.isApplicable() {
|
||||
print(item)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
70
phpmon-common/Core/Paths.swift
Normal file
@ -0,0 +1,70 @@
|
||||
//
|
||||
// Paths.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
The `Paths` class is used to locate various binaries on the system.
|
||||
The path to the Homebrew directory and the user's name are fetched only once, at boot.
|
||||
*/
|
||||
public class Paths {
|
||||
|
||||
public static let shared = Paths()
|
||||
|
||||
private var baseDir : Paths.HomebrewDir
|
||||
|
||||
private var userName : String
|
||||
|
||||
init() {
|
||||
baseDir = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr
|
||||
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
|
||||
}
|
||||
|
||||
// - MARK: Binaries
|
||||
|
||||
public static var valet: String {
|
||||
return "\(binPath)/valet"
|
||||
}
|
||||
|
||||
public static var brew: String {
|
||||
return "\(binPath)/brew"
|
||||
}
|
||||
|
||||
public static var php: String {
|
||||
return "\(binPath)/php"
|
||||
}
|
||||
|
||||
public static var phpConfig: String {
|
||||
return "\(binPath)/php-config"
|
||||
}
|
||||
|
||||
// - MARK: Paths
|
||||
|
||||
public static var whoami: String {
|
||||
return shared.userName
|
||||
}
|
||||
|
||||
public static var binPath: String {
|
||||
return "\(shared.baseDir.rawValue)/bin"
|
||||
}
|
||||
|
||||
public static var optPath: String {
|
||||
return "\(shared.baseDir.rawValue)/opt"
|
||||
}
|
||||
|
||||
public static var etcPath: String {
|
||||
return "\(shared.baseDir.rawValue)/etc"
|
||||
}
|
||||
|
||||
// MARK: - Enum
|
||||
|
||||
public enum HomebrewDir: String {
|
||||
case opt = "/opt/homebrew"
|
||||
case usr = "/usr/local"
|
||||
}
|
||||
|
||||
}
|
180
phpmon-common/Core/Shell.swift
Normal file
@ -0,0 +1,180 @@
|
||||
//
|
||||
// Shell.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
public class Shell {
|
||||
|
||||
// MARK: - Invoke static functions
|
||||
|
||||
public static func run(
|
||||
_ command: String,
|
||||
requiresPath: Bool = false
|
||||
) {
|
||||
Shell.user.run(command, requiresPath: requiresPath)
|
||||
}
|
||||
|
||||
public static func pipe(
|
||||
_ command: String,
|
||||
requiresPath: Bool = false
|
||||
) -> String {
|
||||
return Shell.user.pipe(command, requiresPath: requiresPath)
|
||||
}
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
/**
|
||||
We now require macOS 11, so no need to detect which terminal to use.
|
||||
*/
|
||||
public var shell: String = "/bin/sh"
|
||||
|
||||
/**
|
||||
Singleton to access a user shell (with --login)
|
||||
*/
|
||||
public static let user = Shell()
|
||||
|
||||
/**
|
||||
Runs a shell command without using the output.
|
||||
Uses the default shell.
|
||||
|
||||
- Parameter command: The command to run
|
||||
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
|
||||
*/
|
||||
private func run(
|
||||
_ command: String,
|
||||
requiresPath: Bool = false
|
||||
) {
|
||||
// Equivalent of piping to /dev/null; don't do anything with the string
|
||||
_ = Shell.pipe(command, requiresPath: requiresPath)
|
||||
}
|
||||
|
||||
/**
|
||||
Runs a shell command and returns the output.
|
||||
|
||||
- Parameter command: The command to run
|
||||
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
|
||||
*/
|
||||
private func pipe(
|
||||
_ command: String,
|
||||
requiresPath: Bool = false
|
||||
) -> String {
|
||||
let shellOutput = self.executeSynchronously(command, requiresPath: requiresPath)
|
||||
let hasError = (
|
||||
shellOutput.standardOutput == ""
|
||||
&& shellOutput.errorOutput.lengthOfBytes(using: .utf8) > 0
|
||||
)
|
||||
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
|
||||
}
|
||||
|
||||
/**
|
||||
Runs the command and returns a `ShellOutput` object, which contains info about the process.
|
||||
|
||||
- Parameter command: The command to run
|
||||
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
|
||||
- Parameter waitUntilExit: Waits for the command to complete before returning the `ShellOutput`
|
||||
*/
|
||||
public func executeSynchronously(
|
||||
_ command: String,
|
||||
requiresPath: Bool = false
|
||||
) -> Shell.Output {
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
let task = self.createTask(for: command, requiresPath: requiresPath)
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
return Shell.Output(
|
||||
standardOutput: String(
|
||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!,
|
||||
errorOutput: String(
|
||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!,
|
||||
task: task
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if a file exists at the provided path.
|
||||
Uses `/bin/echo` instead of the `builtin` (which does not support `-n`).
|
||||
*/
|
||||
public static func fileExists(_ path: String) -> Bool {
|
||||
let escapedPath = path.replacingOccurrences(of: " ", with: "\\ ")
|
||||
return Shell.pipe("if [ -f \(escapedPath) ]; then /bin/echo -n \"0\"; fi") == "0"
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new process with the correct PATH and shell.
|
||||
*/
|
||||
public func createTask(for command: String, requiresPath: Bool) -> Process {
|
||||
let tailoredCommand = requiresPath
|
||||
? "export PATH=\(Paths.binPath):$PATH && \(command)"
|
||||
: command
|
||||
|
||||
let task = Process()
|
||||
task.launchPath = self.shell
|
||||
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
public static func captureOutput(
|
||||
_ task: Process,
|
||||
didReceiveStdOutData: @escaping (String) -> Void,
|
||||
didReceiveStdErrData: @escaping (String) -> Void
|
||||
) {
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
|
||||
[(outputPipe, didReceiveStdOutData), (errorPipe, didReceiveStdErrData)].forEach {
|
||||
(pipe: Pipe, callback: @escaping (String) -> Void) in
|
||||
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSNotification.Name.NSFileHandleDataAvailable,
|
||||
object: pipe.fileHandleForReading,
|
||||
queue: nil
|
||||
) { notification in
|
||||
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
|
||||
callback(outputString)
|
||||
}
|
||||
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func haltCapturingOutput(_ task: Process) {
|
||||
if let pipe = task.standardOutput as? Pipe {
|
||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||
}
|
||||
if let pipe = task.standardError as? Pipe {
|
||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||
}
|
||||
}
|
||||
|
||||
public class Output {
|
||||
public let standardOutput: String
|
||||
public let errorOutput: String
|
||||
public let task: Process
|
||||
|
||||
init(standardOutput: String,
|
||||
errorOutput: String,
|
||||
task: Process) {
|
||||
self.standardOutput = standardOutput
|
||||
self.errorOutput = errorOutput
|
||||
self.task = task
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// PhpInstallation.swift
|
||||
// ActivePhpInstallation.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
@ -7,27 +7,36 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpInstallation {
|
||||
/**
|
||||
An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory.
|
||||
|
||||
When initialized, that version's .ini files are also scanned (for active or inactive extensions).
|
||||
Integrity checks can be performed to determine whether PHP-FPM is configured correctly.
|
||||
|
||||
- Note: Each installation has a separate version number.
|
||||
Using `version.short` is advisable if you want to interact with Homebrew.
|
||||
*/
|
||||
class ActivePhpInstallation {
|
||||
|
||||
var version: Version!
|
||||
var configuration: Configuration!
|
||||
var limits: Limits!
|
||||
var extensions: [PhpExtension]!
|
||||
|
||||
// MARK: - Computed
|
||||
|
||||
var formula: String {
|
||||
return (version.short == App.shared.brewPhpVersion) ? "php" : "php@\(version.short)"
|
||||
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
|
||||
}
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init() {
|
||||
// Show information about the current version
|
||||
self.getVersion()
|
||||
getVersion()
|
||||
|
||||
// If an error occurred, exit early
|
||||
if (version.error) {
|
||||
configuration = Configuration()
|
||||
limits = Limits()
|
||||
extensions = []
|
||||
return
|
||||
}
|
||||
@ -37,10 +46,10 @@ class PhpInstallation {
|
||||
extensions = PhpExtension.load(from: path)
|
||||
|
||||
// Get configuration values
|
||||
configuration = Configuration(
|
||||
memory_limit: self.getByteCount(key: "memory_limit"),
|
||||
upload_max_filesize: self.getByteCount(key: "upload_max_filesize"),
|
||||
post_max_size: self.getByteCount(key: "post_max_size")
|
||||
limits = Limits(
|
||||
memory_limit: getByteCount(key: "memory_limit"),
|
||||
upload_max_filesize: getByteCount(key: "upload_max_filesize"),
|
||||
post_max_size: getByteCount(key: "post_max_size")
|
||||
)
|
||||
|
||||
// Return a list of .ini files parsed after php.ini
|
||||
@ -51,9 +60,9 @@ class PhpInstallation {
|
||||
|
||||
// See if any extensions are present in said .ini files
|
||||
paths.forEach { (iniFilePath) in
|
||||
let extensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
||||
if extensions.count > 0 {
|
||||
self.extensions.append(contentsOf: extensions)
|
||||
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
||||
if exts.count > 0 {
|
||||
extensions.append(contentsOf: exts)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -92,8 +101,10 @@ class PhpInstallation {
|
||||
* 10000: an integer = amount of bytes
|
||||
* 1K, 1M, 1G = shorthand for kilobytes, megabytes and gigabytes
|
||||
|
||||
If none of these notations are used, the _fallback_ value is used. We'll show an emoji to indicate something has gone wrong here.
|
||||
To clarify, B gets appended to valid values. As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes ⚠️.
|
||||
If none of these notations are used, the _fallback_ value is used.
|
||||
We'll show an emoji to indicate something has gone wrong here.
|
||||
To clarify, B gets appended to valid values.
|
||||
As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes ⚠️.
|
||||
|
||||
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
|
||||
*/
|
||||
@ -111,18 +122,14 @@ class PhpInstallation {
|
||||
return (match == nil) ? "⚠️" : "\(value)B"
|
||||
}
|
||||
|
||||
public func notifyAboutBrokenPhpFpm() {
|
||||
if !self.checkPhpFpmStatus() {
|
||||
DispatchQueue.main.async {
|
||||
Alert.notify(
|
||||
message: "alert.php_fpm_broken.title".localized,
|
||||
info: "alert.php_fpm_broken.info".localized
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func checkPhpFpmStatus() -> Bool {
|
||||
/**
|
||||
Determine if PHP-FPM is configured correctly.
|
||||
|
||||
For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent
|
||||
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
|
||||
that means that Valet won't work properly.
|
||||
*/
|
||||
func checkPhpFpmStatus() -> Bool {
|
||||
if self.version.short == "5.6" {
|
||||
// The main PHP config file should contain `valet.sock` and then we're probably fine?
|
||||
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
|
||||
@ -134,16 +141,27 @@ class PhpInstallation {
|
||||
}
|
||||
|
||||
// MARK: - Structs
|
||||
|
||||
|
||||
/**
|
||||
Struct containing information about the version number of the current PHP installation.
|
||||
Also includes information about whether the install is considered "broken" or not.
|
||||
If an error was found in the terminal output, `error` is set to `true` and the installation
|
||||
can be considered broken. (The app will display this as well.)
|
||||
*/
|
||||
struct Version {
|
||||
var short = "???"
|
||||
var long = "???"
|
||||
var error = false
|
||||
}
|
||||
|
||||
struct Configuration {
|
||||
/**
|
||||
Struct containing information about the limits of the current PHP installation.
|
||||
Includes: memory limit, max upload size and max post size.
|
||||
*/
|
||||
struct Limits {
|
||||
var memory_limit = "???"
|
||||
var upload_max_filesize = "???"
|
||||
var post_max_size = "???"
|
||||
}
|
||||
|
||||
}
|
30
phpmon-common/PHP/Homebrew/HomebrewPackage.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// HomebrewPackage.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct HomebrewPackage: Decodable {
|
||||
|
||||
let name: String
|
||||
let full_name: String
|
||||
let aliases: [String]
|
||||
let installed: [HomebrewInstalled]
|
||||
let linked_keg: String?
|
||||
|
||||
public var version: String {
|
||||
return aliases.first!
|
||||
.replacingOccurrences(of: "php@", with: "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct HomebrewInstalled: Decodable {
|
||||
let version: String
|
||||
let built_as_bottle: Bool
|
||||
let installed_as_dependency: Bool
|
||||
let installed_on_request: Bool
|
||||
}
|
21
phpmon-common/PHP/Homebrew/HomebrewService.swift
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// HomebrewService.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 11/01/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct HomebrewService: Decodable, Equatable {
|
||||
let name: String
|
||||
let service_name: String
|
||||
let running: Bool
|
||||
let loaded: Bool
|
||||
let pid: Int?
|
||||
let user: String?
|
||||
let status: String?
|
||||
let log_path: String?
|
||||
let error_log_path: String?
|
||||
}
|
170
phpmon-common/PHP/PHP Version/PhpEnv.swift
Normal file
@ -0,0 +1,170 @@
|
||||
//
|
||||
// PhpSwitcher.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 21/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol PhpSwitcherDelegate: AnyObject {
|
||||
func switcherDidStartSwitching()
|
||||
func switcherDidCompleteSwitch()
|
||||
}
|
||||
|
||||
class PhpEnv {
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init() {
|
||||
self.currentInstall = ActivePhpInstallation()
|
||||
|
||||
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
|
||||
|
||||
self.homebrewPackage = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self,
|
||||
from: brewPhpAlias.data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/** The delegate that is informed of updates. */
|
||||
weak var delegate: PhpSwitcherDelegate?
|
||||
|
||||
/** The static app instance. Accessible at any time. */
|
||||
static let shared = PhpEnv()
|
||||
|
||||
/** Whether the switcher is busy performing any actions. */
|
||||
var isBusy: Bool = false
|
||||
|
||||
/** All available versions of PHP. */
|
||||
var availablePhpVersions: [String] = []
|
||||
|
||||
/** Cached information about the PHP installations. */
|
||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||
|
||||
/** Information about the currently linked PHP installation. */
|
||||
var currentInstall: ActivePhpInstallation
|
||||
|
||||
/**
|
||||
The version that the `php` formula via Brew is aliased to on the current system.
|
||||
|
||||
If you're up to date, `php` will be aliased to the latest version,
|
||||
but that might not be the case since not everyone keeps their
|
||||
software up-to-date.
|
||||
|
||||
As such, we take that information from Homebrew.
|
||||
*/
|
||||
static var brewPhpVersion: String {
|
||||
return Self.shared.homebrewPackage.version
|
||||
}
|
||||
|
||||
/**
|
||||
The currently linked and active PHP installation.
|
||||
*/
|
||||
static var phpInstall: ActivePhpInstallation {
|
||||
return Self.shared.currentInstall
|
||||
}
|
||||
|
||||
/**
|
||||
Information we were able to discern from the Homebrew info command.
|
||||
*/
|
||||
var homebrewPackage: HomebrewPackage! = nil
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
public static var switcher: PhpSwitcher {
|
||||
return InternalSwitcher()
|
||||
}
|
||||
|
||||
public static func detectPhpVersions() -> Void {
|
||||
_ = Self.shared.detectPhpVersions()
|
||||
}
|
||||
|
||||
/**
|
||||
Detects which versions of PHP are installed.
|
||||
*/
|
||||
public func detectPhpVersions() -> [String]
|
||||
{
|
||||
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
|
||||
|
||||
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
|
||||
|
||||
// Make sure the aliased version is detected
|
||||
// The user may have `php` installed, but not e.g. `php@8.0`
|
||||
// We should also detect that as a version that is installed
|
||||
let phpAlias = homebrewPackage.version
|
||||
|
||||
// Avoid inserting a duplicate
|
||||
if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) {
|
||||
versionsOnly.append(phpAlias)
|
||||
}
|
||||
|
||||
Log.info("The PHP versions that were detected are: \(versionsOnly)")
|
||||
|
||||
availablePhpVersions = versionsOnly
|
||||
|
||||
var mappedVersions: [String: PhpInstallation] = [:]
|
||||
|
||||
availablePhpVersions.forEach { version in
|
||||
mappedVersions[version] = PhpInstallation(version)
|
||||
}
|
||||
|
||||
cachedPhpInstallations = mappedVersions
|
||||
|
||||
return versionsOnly
|
||||
}
|
||||
|
||||
/**
|
||||
Extracts valid PHP versions from an array of strings.
|
||||
This array of strings is usually retrieved from `grep`.
|
||||
*/
|
||||
public func extractPhpVersions(
|
||||
from versions: [String],
|
||||
checkBinaries: Bool = true
|
||||
) -> [String] {
|
||||
var output : [String] = []
|
||||
|
||||
versions.filter { (version) -> Bool in
|
||||
// Omit everything that doesn't start with php@
|
||||
// (e.g. something-php@8.0 won't be detected)
|
||||
return version.starts(with: "php@")
|
||||
}.forEach { (string) in
|
||||
let version = string.components(separatedBy: "php@")[1]
|
||||
// Only append the version if it doesn't already exist (avoid dupes),
|
||||
// is supported and where the binary exists (avoids broken installs)
|
||||
if !output.contains(version)
|
||||
&& Constants.SupportedPhpVersions.contains(version)
|
||||
&& (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
|
||||
{
|
||||
output.append(version)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
public func validVersions(for constraint: String) -> [PhpVersionNumber] {
|
||||
constraint.split(separator: "|").flatMap {
|
||||
return PhpVersionNumberCollection
|
||||
.make(from: self.availablePhpVersions)
|
||||
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Validates whether the currently running version matches the provided version.
|
||||
*/
|
||||
public func validate(_ version: String) -> Bool {
|
||||
if self.currentInstall.version.short == version {
|
||||
print("Switching to version \(version) seems to have succeeded. Validation passed.")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
176
phpmon-common/PHP/PHP Version/PhpVersionNumber.swift
Normal file
@ -0,0 +1,176 @@
|
||||
//
|
||||
// PhpVersionNumber.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 23/01/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct PhpVersionNumberCollection: Equatable {
|
||||
let versions: [PhpVersionNumber]
|
||||
|
||||
public static func make(from versions: [String]) -> Self {
|
||||
return PhpVersionNumberCollection(
|
||||
versions: versions.map { PhpVersionNumber.make(from: $0)! }
|
||||
)
|
||||
}
|
||||
|
||||
public var first: PhpVersionNumber? {
|
||||
return self.versions.first
|
||||
}
|
||||
|
||||
public var all: [PhpVersionNumber] {
|
||||
return self.versions
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if any versions of PHP are valid for the constraint provided.
|
||||
Due to the complexity of evaluating these, a important test is maintained.
|
||||
More information on these constraints can be found here:
|
||||
https://getcomposer.org/doc/articles/versions.md#writing-version-constraints
|
||||
|
||||
- Parameter constraint: The full constraint as a string (e.g. "^7.0")
|
||||
- Parameter strict: Whether the patch version check is strict. See more below.
|
||||
|
||||
The strict mode does not matter if a patch version is provided for all versions in the collection.
|
||||
|
||||
Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred
|
||||
from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise,
|
||||
assumes that the patch version is .999, which means that in all cases the patch version check is
|
||||
always going to pass.
|
||||
|
||||
**STRICT MODE (= patch precision on)**
|
||||
|
||||
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will
|
||||
be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK).
|
||||
When checking against actual PHP versions installed by the user (with patch precision), use
|
||||
strict mode.
|
||||
|
||||
**NON-STRICT MODE (= patch precision off)**
|
||||
|
||||
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0
|
||||
is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version.
|
||||
In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde).
|
||||
If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since
|
||||
the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.)
|
||||
*/
|
||||
public func matching(constraint: String, strict: Bool = false) -> [PhpVersionNumber] {
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .versionOnly) {
|
||||
// Strict constraint (e.g. "7.0") -> returns specific version
|
||||
return self.versions.filter { $0.isSameAs(version, strict) }
|
||||
}
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
|
||||
// Caret range means that the major version is never higher but minor version can be higher
|
||||
// ^7.2 will be compatible with all versions between 7.2 and 8.0
|
||||
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
|
||||
}
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
|
||||
// Tilde range means that most specific digit is used as the basis.
|
||||
return self.versions.filter {
|
||||
version.patch != nil
|
||||
// If a patch is provided then the minor version cannot be bumped.
|
||||
? $0.hasSameMajorAndMinorButNewerOrSamePatch(version, strict)
|
||||
// If a patch is not provided then the major version cannot be bumped.
|
||||
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
|
||||
}
|
||||
}
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
|
||||
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
|
||||
}
|
||||
|
||||
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
|
||||
return self.versions.filter { $0.isNewerThan(version, strict) }
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public struct PhpVersionNumber: Equatable {
|
||||
let major: Int
|
||||
let minor: Int
|
||||
let patch: Int?
|
||||
|
||||
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
|
||||
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
|
||||
}
|
||||
|
||||
public var homebrewVersion: String {
|
||||
return "\(major).\(minor)"
|
||||
}
|
||||
|
||||
public enum MatchType: String {
|
||||
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
|
||||
// TODO: (5.1) Handle these cases (even though I suspect these are uncommon)
|
||||
/*
|
||||
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
*/
|
||||
}
|
||||
|
||||
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
|
||||
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
|
||||
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
|
||||
|
||||
if match != nil {
|
||||
let major = Int(
|
||||
versionString[Range(match!.range(withName: "major"), in: versionString)!]
|
||||
)!
|
||||
let minor = Int(
|
||||
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
|
||||
)!
|
||||
var patch: Int? = nil
|
||||
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
|
||||
patch = Int(versionString[minorRange])
|
||||
}
|
||||
return Self(major: major, minor: minor, patch: patch)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Comparison Logic
|
||||
|
||||
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return self.major == version.major
|
||||
&& self.minor == version.minor
|
||||
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
|
||||
}
|
||||
|
||||
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return (
|
||||
self.major > version.major ||
|
||||
self.major == version.major && self.minor > version.minor ||
|
||||
self.major == version.major && self.minor == version.minor
|
||||
&& self.patch(strict) > version.patch(strict)
|
||||
)
|
||||
}
|
||||
|
||||
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return self.major == version.major &&
|
||||
(
|
||||
(self.minor == version.minor && self.patch(strict) >= version.patch(strict, self))
|
||||
|| self.minor > version.minor
|
||||
)
|
||||
}
|
||||
|
||||
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return self.major == version.major && self.minor == version.minor
|
||||
&& self.patch(strict, version) >= version.patch(strict)
|
||||
}
|
||||
|
||||
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
|
||||
return self.major == version.major
|
||||
&& self.minor >= version.minor
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ class PhpExtension {
|
||||
|
||||
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
||||
*/
|
||||
static let extensionRegex = #"^(extension=|zend_extension=|; extension=|; zend_extension=)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
||||
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
||||
|
||||
/**
|
||||
When registering an extension, we do that based on the line found inside the .ini file.
|
||||
@ -61,6 +61,7 @@ class PhpExtension {
|
||||
let fullPath = String(line[range])
|
||||
.replacingOccurrences(of: "\"", with: "") // replace excess "
|
||||
.replacingOccurrences(of: ".so", with: "") // replace excess .so
|
||||
|
||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||
|
||||
self.enabled = !line.contains(";")
|
||||
@ -71,12 +72,15 @@ 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.
|
||||
*/
|
||||
func toggle() {
|
||||
Actions.sed(
|
||||
file: file,
|
||||
original: line,
|
||||
replacement: enabled ? "; \(line)" : line.replacingOccurrences(of: "; ", with: "")
|
||||
)
|
||||
enabled = !enabled
|
||||
let newLine = enabled
|
||||
// DISABLED: Commented out line
|
||||
? "; \(line)"
|
||||
// ENABLED: Line where the comment delimiter (;) is removed
|
||||
: line.replacingOccurrences(of: "; ", with: "")
|
||||
|
||||
sed(file: file, original: line, replacement: newLine)
|
||||
|
||||
enabled.toggle()
|
||||
}
|
||||
|
||||
// MARK: - Static Methods
|
||||
@ -88,16 +92,17 @@ class PhpExtension {
|
||||
let file = try? String(contentsOf: path, encoding: .utf8)
|
||||
|
||||
if (file == nil) {
|
||||
print("There was an issue reading the file. Assuming no extensions were found.")
|
||||
Log.err("There was an issue reading the file. Assuming no extensions were found.")
|
||||
return []
|
||||
}
|
||||
|
||||
return file!.components(separatedBy: "\n")
|
||||
.filter({ (line) -> Bool in
|
||||
return line.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||
})
|
||||
.map { (line) -> PhpExtension in
|
||||
return PhpExtension(line, file: path.path)
|
||||
.filter {
|
||||
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||
}
|
||||
.map {
|
||||
return PhpExtension($0, file: path.path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
36
phpmon-common/PHP/PhpInstallation.swift
Normal file
@ -0,0 +1,36 @@
|
||||
//
|
||||
// PhpInstallation.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 28/11/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpInstallation {
|
||||
|
||||
var longVersion: PhpVersionNumber
|
||||
|
||||
/**
|
||||
In order to determine details about a PHP installation, we’ll simply run `php-config --version`
|
||||
in the relevant directory.
|
||||
*/
|
||||
init(_ version: String) {
|
||||
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
||||
self.longVersion = PhpVersionNumber.make(from: version)!
|
||||
|
||||
if Shell.fileExists(phpConfigExecutablePath) {
|
||||
let longVersionString = Command.execute(
|
||||
path: phpConfigExecutablePath,
|
||||
arguments: ["--version"]
|
||||
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
self.longVersion = PhpVersionNumber.make(
|
||||
from: String(longVersionString.split(separator: "-")[0])
|
||||
)!
|
||||
}
|
||||
}
|
||||
|
||||
}
|
61
phpmon-common/PHP/Switcher/InternalSwitcher.swift
Normal file
@ -0,0 +1,61 @@
|
||||
//
|
||||
// InternalSwitcher.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 24/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class InternalSwitcher: PhpSwitcher {
|
||||
|
||||
/**
|
||||
Switching to a new PHP version involves:
|
||||
- unlinking the current version
|
||||
- stopping the active services
|
||||
- linking the new desired version
|
||||
|
||||
Please note that depending on which version is installed,
|
||||
the version that is switched to may or may not be identical to `php`
|
||||
(without @version).
|
||||
*/
|
||||
func performSwitch(to version: String, completion: @escaping () -> Void)
|
||||
{
|
||||
Log.info("Switching to \(version), unlinking all versions...")
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
PhpEnv.shared.availablePhpVersions.forEach { (available) in
|
||||
group.enter()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let formula = (available == PhpEnv.brewPhpVersion)
|
||||
? "php" : "php@\(available)"
|
||||
|
||||
brew("unlink \(formula)")
|
||||
brew("services stop \(formula)", sudo: true)
|
||||
|
||||
Log.perf("Unlinked and stopped services for \(formula)")
|
||||
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .global(qos: .userInitiated)) {
|
||||
Log.info("All versions have been unlinked!")
|
||||
Log.info("Linking the new version!")
|
||||
|
||||
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
brew("link \(formula) --overwrite --force")
|
||||
brew("services start \(formula)", sudo: true)
|
||||
|
||||
Log.info("Restarting nginx, just to be sure!")
|
||||
brew("services restart nginx", sudo: true)
|
||||
|
||||
Log.info("The new version has been linked!")
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
15
phpmon-common/PHP/Switcher/PhpSwitcher.swift
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// PhpVersionSwitchContract.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 24/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol PhpSwitcher {
|
||||
|
||||
func performSwitch(to version: String, completion: @escaping () -> Void)
|
||||
|
||||
}
|
@ -9,20 +9,77 @@
|
||||
import XCTest
|
||||
|
||||
class BrewJsonParserTest: XCTestCase {
|
||||
|
||||
// - MARK: SYNTHETIC TESTS
|
||||
|
||||
static var jsonBrewFile: URL {
|
||||
return Bundle(for: Self.self).url(forResource: "brew", withExtension: "json")!
|
||||
return Bundle(for: Self.self)
|
||||
.url(forResource: "brew", withExtension: "json")!
|
||||
}
|
||||
|
||||
func testCanLoadExtension() throws {
|
||||
let json = try? String(contentsOf: Self.jsonBrewFile, encoding: .utf8)
|
||||
func testCanLoadExtensionJson() throws {
|
||||
let json = try! String(contentsOf: Self.jsonBrewFile, encoding: .utf8)
|
||||
let package = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self, from: json!.data(using: .utf8)!
|
||||
[HomebrewPackage].self, from: json.data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
XCTAssertEqual(package.name, "php")
|
||||
XCTAssertEqual(package.full_name, "php")
|
||||
XCTAssertEqual(package.aliases.first!, "php@8.0")
|
||||
XCTAssertEqual(package.installed.contains(where: { installed in
|
||||
installed.version.starts(with: "8.0")
|
||||
}), true)
|
||||
}
|
||||
|
||||
static var jsonBrewServicesFile: URL {
|
||||
return Bundle(for: Self.self)
|
||||
.url(forResource: "brew-services", withExtension: "json")!
|
||||
}
|
||||
|
||||
func testCanParseServicesJson() throws {
|
||||
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
|
||||
let services = try! JSONDecoder().decode(
|
||||
[HomebrewService].self, from: json.data(using: .utf8)!
|
||||
)
|
||||
|
||||
XCTAssertGreaterThan(services.count, 0)
|
||||
XCTAssertEqual(services.first?.name, "dnsmasq")
|
||||
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
|
||||
}
|
||||
|
||||
// - MARK: LIVE TESTS
|
||||
|
||||
/// This test requires that you have a valid Homebrew installation set up,
|
||||
/// and requires the Valet services to be installed: php, nginx and dnsmasq.
|
||||
/// If this test fails, there is an issue with your Homebrew installation
|
||||
/// or the JSON API of the Homebrew output may have changed.
|
||||
func testCanParseServicesJsonFromCliOutput() throws {
|
||||
let services = try! JSONDecoder().decode(
|
||||
[HomebrewService].self,
|
||||
from: Shell.pipe(
|
||||
"sudo \(Paths.brew) services info --all --json",
|
||||
requiresPath: true
|
||||
).data(using: .utf8)!
|
||||
).filter({ service in
|
||||
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"} ))
|
||||
XCTAssertEqual(services.count, 3)
|
||||
}
|
||||
|
||||
/// This test requires that you have a valid Homebrew installation set up,
|
||||
/// and requires the `php` formula to be installed.
|
||||
/// If this test fails, there is an issue with your Homebrew installation
|
||||
/// or the JSON API of the Homebrew output may have changed.
|
||||
func testCanLoadExtensionJsonFromCliOutput() throws {
|
||||
let package = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self,
|
||||
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
XCTAssertTrue(package.name == "php")
|
||||
}
|
||||
}
|
||||
|
@ -27,11 +27,16 @@ class ExtensionParserTest: XCTestCase {
|
||||
return ext.name
|
||||
}
|
||||
|
||||
// These 6 should be found
|
||||
XCTAssertTrue(extensionNames.contains("xdebug"))
|
||||
XCTAssertTrue(extensionNames.contains("imagick"))
|
||||
XCTAssertTrue(extensionNames.contains("sodium-next"))
|
||||
XCTAssertTrue(extensionNames.contains("opcache"))
|
||||
XCTAssertTrue(extensionNames.contains("yaml"))
|
||||
XCTAssertTrue(extensionNames.contains("custom"))
|
||||
|
||||
XCTAssertFalse(extensionNames.contains("fake"))
|
||||
XCTAssertFalse(extensionNames.contains("nice"))
|
||||
}
|
||||
|
||||
func testExtensionStatusIsCorrect() throws {
|
||||
@ -47,7 +52,7 @@ class ExtensionParserTest: XCTestCase {
|
||||
func testToggleWorksAsExpected() throws {
|
||||
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
||||
let extensions = PhpExtension.load(from: destination)
|
||||
XCTAssertEqual(extensions.count, 4)
|
||||
XCTAssertEqual(extensions.count, 6)
|
||||
|
||||
// Try to disable xdebug (should be detected first)!
|
||||
let xdebug = extensions.first!
|
||||
|
@ -11,7 +11,7 @@ import XCTest
|
||||
class PhpVersionDetectionTest: XCTestCase {
|
||||
|
||||
func testCanDetectValidPhpVersions() throws {
|
||||
let outcome = Actions.extractPhpVersions(from: [
|
||||
let outcome = PhpEnv.shared.extractPhpVersions(from: [
|
||||
"", // empty lines should be omitted
|
||||
"php@8.0",
|
||||
"php@8.0", // should only be detected once
|
||||
@ -26,5 +26,4 @@ class PhpVersionDetectionTest: XCTestCase {
|
||||
|
||||
XCTAssertEqual(outcome, ["8.0", "7.0", "5.6"])
|
||||
}
|
||||
|
||||
}
|
||||
|
276
phpmon-tests/PhpVersionNumberTest.swift
Normal file
@ -0,0 +1,276 @@
|
||||
//
|
||||
// PhpVersionNumberTest.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 23/01/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class PhpVersionNumberTest: XCTestCase {
|
||||
|
||||
func testCanDeconstructPhpVersion() throws {
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumber.make(from: "8.0.11"),
|
||||
PhpVersionNumber(major: 8, minor: 0, patch: 11)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumber.make(from: "7.4.2"),
|
||||
PhpVersionNumber(major: 7, minor: 4, patch: 2)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumber.make(from: "7.4"),
|
||||
PhpVersionNumber(major: 7, minor: 4, patch: nil)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumber.make(from: "7"),
|
||||
nil
|
||||
)
|
||||
}
|
||||
|
||||
func testCanCheckFixedConstraints() throws {
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "7.0"),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"])
|
||||
.matching(constraint: "7.0.3"),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0.3"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "7.0.3", strict: false),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "7.0.3", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: []).all
|
||||
)
|
||||
}
|
||||
|
||||
func testCanCheckCaretConstraints() throws {
|
||||
// 1. Imprecise checks
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "^7.0", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
// 2. Imprecise check with precise constraint (lenient AKA not strict)
|
||||
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "^7.0.1", strict: false),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
// 3. Imprecise check with precise constraint (strict mode)
|
||||
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "^7.0.1", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
|
||||
)
|
||||
|
||||
// 4. Precise members and constraint all around
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
|
||||
.matching(constraint: "^7.0.1", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
|
||||
// 5. Precise members but imprecise constraint (strict mode)
|
||||
// In strict mode the constraint's patch version is assumed to be 0
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
|
||||
.matching(constraint: "^7.0", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
|
||||
// 6. Precise members but imprecise constraint (lenient mode)
|
||||
// In lenient mode the constraint's patch version is assumed to be equal
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
|
||||
.matching(constraint: "^7.0", strict: false),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
}
|
||||
|
||||
func testCanCheckTildeConstraints() throws {
|
||||
// 1. Imprecise checks
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "~7.0", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
// 2. Imprecise check with precise constraint (lenient AKA not strict)
|
||||
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "~7.0.1", strict: false),
|
||||
// One result because 7.0.1 to 7.0.x is expected.
|
||||
// 7.0.999 (assumed due to no strictness) is valid.
|
||||
// 7.1.0 and up are not valid (minor version is too high).
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0"]).all
|
||||
)
|
||||
|
||||
// 3. Imprecise check with precise constraint (strict mode)
|
||||
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: "~7.0.1", strict: true),
|
||||
// No results because 7.0.1 to 7.0.x is expected.
|
||||
// 7.0.0 (assumed due to strictness) is not valid.
|
||||
// 7.1.0 and up are also not valid (minor version is too high).
|
||||
PhpVersionNumberCollection
|
||||
.make(from: []).all
|
||||
)
|
||||
|
||||
// 4. Precise members and constraint all around
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
|
||||
.matching(constraint: "~7.0.1", strict: true),
|
||||
// Only 7.0 with a patch version of .1 or higher is OK.
|
||||
// In this example, 7.0.10 is OK but all other versions are too new.
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.0.10"]).all
|
||||
)
|
||||
|
||||
// 5. Precise members but imprecise constraint (strict mode)
|
||||
// In strict mode the constraint's patch version is assumed to be 0.
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
|
||||
.matching(constraint: "~7.0", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
|
||||
// 6. Precise members but imprecise constraint (lenient mode)
|
||||
// In lenient mode the constraint's patch version is assumed to be equal.
|
||||
// (Strictness does not make any difference here, but both should be tested.)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"])
|
||||
.matching(constraint: "~7.0", strict: false),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
|
||||
)
|
||||
}
|
||||
|
||||
func testCanCheckGreaterThanOrEqualConstraints() throws {
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: ">=7.0", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: ">=7.0.0", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
|
||||
)
|
||||
|
||||
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: ">=7.2.5", strict: true),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3"]).all
|
||||
)
|
||||
|
||||
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: ">=7.2.5", strict: false),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2"]).all
|
||||
)
|
||||
}
|
||||
|
||||
func testCanCheckGreaterThanConstraints() throws {
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: ">7.0"),
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: ">7.2.5"),
|
||||
// 7.2 will be valid due to non-strict mode (resolves to 7.2.999)
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
|
||||
.matching(constraint: ">7.2.5", strict: true),
|
||||
// 7.2 will not be valid due to strict mode (resolves to 7.2.0)
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.4", "7.3"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
|
||||
.matching(constraint: ">7.2.8"),
|
||||
// 7.2 will be valid due to non-strict mode (resolves to 7.2.999)
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.3.1", "7.2.9", "7.2"]).all
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
|
||||
.matching(constraint: ">7.2.8", strict: true),
|
||||
// 7.2 will not be valid due to strict mode (resolves to 7.2.0)
|
||||
PhpVersionNumberCollection
|
||||
.make(from: ["7.3.1", "7.2.9"]).all
|
||||
)
|
||||
}
|
||||
}
|
135
phpmon-tests/Test Files/brew-services.json
Normal file
@ -0,0 +1,135 @@
|
||||
[
|
||||
{
|
||||
"name": "dnsmasq",
|
||||
"service_name": "homebrew.mxcl.dnsmasq",
|
||||
"running": true,
|
||||
"loaded": true,
|
||||
"schedulable": false,
|
||||
"pid": 106,
|
||||
"exit_code": 0,
|
||||
"user": "root",
|
||||
"status": "started",
|
||||
"file": "/Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist",
|
||||
"command": "/opt/homebrew/opt/dnsmasq/sbin/dnsmasq --keep-in-foreground -C /opt/homebrew/etc/dnsmasq.conf -7 /opt/homebrew/etc/dnsmasq.d,*.conf",
|
||||
"working_dir": null,
|
||||
"root_dir": null,
|
||||
"log_path": null,
|
||||
"error_log_path": null,
|
||||
"interval": null,
|
||||
"cron": null
|
||||
},
|
||||
{
|
||||
"name": "httpd",
|
||||
"service_name": "homebrew.mxcl.httpd",
|
||||
"running": false,
|
||||
"loaded": false,
|
||||
"schedulable": false,
|
||||
"pid": null,
|
||||
"exit_code": null,
|
||||
"user": null,
|
||||
"status": "none",
|
||||
"file": "/opt/homebrew/opt/httpd/homebrew.mxcl.httpd.plist",
|
||||
"command": "/opt/homebrew/opt/httpd/bin/httpd -D FOREGROUND",
|
||||
"working_dir": null,
|
||||
"root_dir": null,
|
||||
"log_path": null,
|
||||
"error_log_path": null,
|
||||
"interval": null,
|
||||
"cron": null
|
||||
},
|
||||
{
|
||||
"name": "mailhog",
|
||||
"service_name": "homebrew.mxcl.mailhog",
|
||||
"running": false,
|
||||
"loaded": false,
|
||||
"schedulable": false,
|
||||
"pid": null,
|
||||
"exit_code": null,
|
||||
"user": null,
|
||||
"status": "none",
|
||||
"file": "/opt/homebrew/opt/mailhog/homebrew.mxcl.mailhog.plist",
|
||||
"command": "/opt/homebrew/opt/mailhog/bin/MailHog",
|
||||
"working_dir": null,
|
||||
"root_dir": null,
|
||||
"log_path": "/opt/homebrew/var/log/mailhog.log",
|
||||
"error_log_path": "/opt/homebrew/var/log/mailhog.log",
|
||||
"interval": null,
|
||||
"cron": null
|
||||
},
|
||||
{
|
||||
"name": "nginx",
|
||||
"service_name": "homebrew.mxcl.nginx",
|
||||
"running": true,
|
||||
"loaded": true,
|
||||
"schedulable": false,
|
||||
"pid": 116,
|
||||
"exit_code": 0,
|
||||
"user": "root",
|
||||
"status": "started",
|
||||
"file": "/Library/LaunchDaemons/homebrew.mxcl.nginx.plist",
|
||||
"command": "/opt/homebrew/opt/nginx/bin/nginx -g daemon off;",
|
||||
"working_dir": "/opt/homebrew",
|
||||
"root_dir": null,
|
||||
"log_path": null,
|
||||
"error_log_path": null,
|
||||
"interval": null,
|
||||
"cron": null
|
||||
},
|
||||
{
|
||||
"name": "php",
|
||||
"service_name": "homebrew.mxcl.php",
|
||||
"running": true,
|
||||
"loaded": true,
|
||||
"schedulable": false,
|
||||
"pid": 142,
|
||||
"exit_code": 0,
|
||||
"user": "root",
|
||||
"status": "started",
|
||||
"file": "/Library/LaunchDaemons/homebrew.mxcl.php.plist",
|
||||
"command": "/opt/homebrew/opt/php/sbin/php-fpm --nodaemonize",
|
||||
"working_dir": "/opt/homebrew/var",
|
||||
"root_dir": null,
|
||||
"log_path": null,
|
||||
"error_log_path": "/opt/homebrew/var/log/php-fpm.log",
|
||||
"interval": null,
|
||||
"cron": null
|
||||
},
|
||||
{
|
||||
"name": "php@8.0",
|
||||
"service_name": "homebrew.mxcl.php@8.0",
|
||||
"running": false,
|
||||
"loaded": false,
|
||||
"schedulable": false,
|
||||
"pid": null,
|
||||
"exit_code": null,
|
||||
"user": null,
|
||||
"status": "none",
|
||||
"file": "/opt/homebrew/opt/php@8.0/homebrew.mxcl.php@8.0.plist",
|
||||
"command": "/opt/homebrew/opt/php@8.0/sbin/php-fpm --nodaemonize",
|
||||
"working_dir": "/opt/homebrew/var",
|
||||
"root_dir": null,
|
||||
"log_path": null,
|
||||
"error_log_path": "/opt/homebrew/var/log/php-fpm.log",
|
||||
"interval": null,
|
||||
"cron": null
|
||||
},
|
||||
{
|
||||
"name": "unbound",
|
||||
"service_name": "homebrew.mxcl.unbound",
|
||||
"running": false,
|
||||
"loaded": false,
|
||||
"schedulable": false,
|
||||
"pid": null,
|
||||
"exit_code": null,
|
||||
"user": null,
|
||||
"status": "none",
|
||||
"file": "/opt/homebrew/opt/unbound/homebrew.mxcl.unbound.plist",
|
||||
"command": "/opt/homebrew/opt/unbound/sbin/unbound -d -c /opt/homebrew/etc/unbound/unbound.conf",
|
||||
"working_dir": null,
|
||||
"root_dir": null,
|
||||
"log_path": null,
|
||||
"error_log_path": null,
|
||||
"interval": null,
|
||||
"cron": null
|
||||
}
|
||||
]
|
@ -1,8 +1,16 @@
|
||||
# These should be detected
|
||||
|
||||
zend_extension="xdebug.so"
|
||||
; zend_extension="imagick.so"
|
||||
zend_extension=/opt/homebrew/opt/php/lib/php/20200930/opcache.so
|
||||
zend_extension="/opt/homebrew/opt/php/lib/php/20200930/yaml.so"
|
||||
#zend_extension="/opt/homebrew/opt/php/lib/php/20200930/fake.so"
|
||||
;zend_extension="sodium-next.so"
|
||||
extension = custom.so
|
||||
|
||||
# These should not be detected
|
||||
|
||||
#zend_extension="/opt/homebrew/opt/php/lib/php/20200930/commented.so"
|
||||
hextension = nice.so
|
||||
|
||||
[PHP]
|
||||
|
8
phpmon-tests/Test Files/valet-config.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"tld": "test",
|
||||
"paths": [
|
||||
"/Users/username/.config/valet/Sites",
|
||||
"/Users/username/Sites"
|
||||
],
|
||||
"loopback": "127.0.0.1"
|
||||
}
|
@ -19,7 +19,7 @@ class Utility {
|
||||
try FileManager.default.copyItem(at: bundleURL, to: targetURL)
|
||||
return targetURL
|
||||
} catch let error {
|
||||
print("Unable to copy file: \(error)")
|
||||
Log.err("Unable to copy file: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
38
phpmon-tests/ValetConfigParserTest.swift
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// ValetConfigParserTest.swift
|
||||
// phpmon-tests
|
||||
//
|
||||
// Created by Nico Verbruggen on 29/11/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class ValetConfigParserTest: XCTestCase {
|
||||
|
||||
static var jsonConfigFileUrl: URL {
|
||||
return Bundle(for: Self.self).url(
|
||||
forResource: "valet-config",
|
||||
withExtension: "json"
|
||||
)!
|
||||
}
|
||||
|
||||
func testCanLoadConfigFile() throws {
|
||||
let json = try? String(
|
||||
contentsOf: Self.jsonConfigFileUrl,
|
||||
encoding: .utf8
|
||||
)
|
||||
let config = try! JSONDecoder().decode(
|
||||
Valet.Configuration.self,
|
||||
from: json!.data(using: .utf8)!
|
||||
)
|
||||
|
||||
XCTAssertEqual(config.tld, "test")
|
||||
XCTAssertEqual(config.paths, [
|
||||
"/Users/username/.config/valet/Sites",
|
||||
"/Users/username/Sites"
|
||||
])
|
||||
XCTAssertEqual(config.loopback, "127.0.0.1")
|
||||
}
|
||||
|
||||
}
|
18
phpmon-tests/ValetTest.swift
Normal file
@ -0,0 +1,18 @@
|
||||
//
|
||||
// ValetTest.swift
|
||||
// phpmon-tests
|
||||
//
|
||||
// Created by Nico Verbruggen on 29/11/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class ValetTest: XCTestCase {
|
||||
|
||||
func testDetermineValetVersion() {
|
||||
let version = valet("--version")
|
||||
XCTAssert(version.contains("Laravel Valet 2."))
|
||||
}
|
||||
|
||||
}
|
25
phpmon-tests/VersionExtractorTest.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// VersionExtractorTest.swift
|
||||
// phpmon-tests
|
||||
//
|
||||
// Created by Nico Verbruggen on 16/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class VersionExtractorTest: XCTestCase {
|
||||
|
||||
func testExtractVersion() {
|
||||
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
|
||||
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
|
||||
}
|
||||
|
||||
func testVersionComparison() {
|
||||
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
|
||||
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
|
||||
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)
|
||||
XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending)
|
||||
}
|
||||
|
||||
}
|
68
phpmon/Assets.xcassets/AppIconBeta.appiconset/Contents.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon_16x16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_128x128.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 19 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_16x16.png
Normal file
After Width: | Height: | Size: 585 B |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_16x16@2x.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_256x256.png
Normal file
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 52 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_32x32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_32x32@2x.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_512x512.png
Normal file
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 134 KiB |
24
phpmon/Assets.xcassets/Checkmark.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "check.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/Checkmark.imageset/check.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M435.848 83.466L172.804 346.51l-96.652-96.652c-4.686-4.686-12.284-4.686-16.971 0l-28.284 28.284c-4.686 4.686-4.686 12.284 0 16.971l133.421 133.421c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-28.284-28.284c-4.686-4.686-12.284-4.686-16.97 0z"/></svg>
|
After Width: | Height: | Size: 497 B |
38
phpmon/Assets.xcassets/IconColorGreen.colorset/Contents.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.501",
|
||||
"green" : "0.697",
|
||||
"red" : "0.247"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.501",
|
||||
"green" : "0.765",
|
||||
"red" : "0.247"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.000",
|
||||
"red" : "0.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
38
phpmon/Assets.xcassets/IconColorRed.colorset/Contents.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.180",
|
||||
"green" : "0.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.426",
|
||||
"green" : "0.363",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
24
phpmon/Assets.xcassets/IconLinked.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "link.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/IconLinked.imageset/link.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
24
phpmon/Assets.xcassets/IconParked.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "car-alt.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/IconParked.imageset/car-alt.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M438.66 212.33l-11.24-28.1-19.93-49.83C390.38 91.63 349.57 64 303.5 64h-127c-46.06 0-86.88 27.63-103.99 70.4l-19.93 49.83-11.24 28.1C17.22 221.5 0 244.66 0 272v48c0 16.12 6.16 30.67 16 41.93V416c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-32h256v32c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-54.07c9.84-11.25 16-25.8 16-41.93v-48c0-27.34-17.22-50.5-41.34-59.67zm-306.73-54.16c7.29-18.22 24.94-30.17 44.57-30.17h127c19.63 0 37.28 11.95 44.57 30.17L368 208H112l19.93-49.83zM80 319.8c-19.2 0-32-12.76-32-31.9S60.8 256 80 256s48 28.71 48 47.85-28.8 15.95-48 15.95zm320 0c-19.2 0-48 3.19-48-15.95S380.8 256 400 256s32 12.76 32 31.9-12.8 31.9-32 31.9z"/></svg>
|
After Width: | Height: | Size: 918 B |
24
phpmon/Assets.xcassets/Lock.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Locked.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
10
phpmon/Assets.xcassets/Lock.imageset/Locked.svg
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<rect id="Locked" x="0" y="0" width="30" height="30" style="fill:none;"/>
|
||||
<g id="Locked1" serif:id="Locked">
|
||||
<g transform="matrix(0.0468317,0,0,0.0468317,4.50971,3.01112)">
|
||||
<path d="M400,256L152,256L152,152.9C152,113.3 183.7,80.4 223.3,80C263.3,79.6 296,112.1 296,152L296,266.079C296,279.379 376,279.137 376,265.837L376,152C376,68 307.5,-0.3 223.5,0C139.5,0.3 72,69.5 72,153.5L72,256L48,256C21.5,256 0,277.5 0,304L0,464C0,490.5 21.5,512 48,512L400,512C426.5,512 448,490.5 448,464L448,304C448,277.5 426.5,256 400,256Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
24
phpmon/Assets.xcassets/LockUnlocked.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Unlocked.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
10
phpmon/Assets.xcassets/LockUnlocked.imageset/Unlocked.svg
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<rect id="Locked" x="0" y="0" width="30" height="30" style="fill:none;"/>
|
||||
<g id="Locked1" serif:id="Locked">
|
||||
<g transform="matrix(0.0468317,0,0,0.0468317,4.50971,3.01112)">
|
||||
<path d="M400,256L152,256L152,152.9C152,113.3 183.7,80.4 223.3,80C263.3,79.6 296,112.1 296,152L296,168C296,181.3 322.386,192 322.386,192L352,192C365.3,192 376,181.3 376,168L376,152C376,68 307.5,-0.3 223.5,0C139.5,0.3 72,69.5 72,153.5L72,256L48,256C21.5,256 0,277.5 0,304L0,464C0,490.5 21.5,512 48,512L400,512C426.5,512 448,490.5 448,464L448,304C448,277.5 426.5,256 400,256Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
24
phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "question-circle.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/ServiceLoading.imageset/question-circle.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 448c-110.532 0-200-89.431-200-200 0-110.495 89.472-200 200-200 110.491 0 200 89.471 200 200 0 110.53-89.431 200-200 200zm107.244-255.2c0 67.052-72.421 68.084-72.421 92.863V300c0 6.627-5.373 12-12 12h-45.647c-6.627 0-12-5.373-12-12v-8.659c0-35.745 27.1-50.034 47.579-61.516 17.561-9.845 28.324-16.541 28.324-29.579 0-17.246-21.999-28.693-39.784-28.693-23.189 0-33.894 10.977-48.942 29.969-4.057 5.12-11.46 6.071-16.666 2.124l-27.824-21.098c-5.107-3.872-6.251-11.066-2.644-16.363C184.846 131.491 214.94 112 261.794 112c49.071 0 101.45 38.304 101.45 88.8zM298 368c0 23.159-18.841 42-42 42s-42-18.841-42-42 18.841-42 42-42 42 18.841 42 42z"/></svg>
|
After Width: | Height: | Size: 966 B |
24
phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "times-circle.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/ServiceOff.imageset/times-circle.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z"/></svg>
|
After Width: | Height: | Size: 685 B |
24
phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "check-circle.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/ServiceOn.imageset/check-circle.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z"/></svg>
|
After Width: | Height: | Size: 718 B |
Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 353 B |
21
phpmon/Assets.xcassets/StatusBarPHP.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "php@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
phpmon/Assets.xcassets/StatusBarPHP.imageset/php@2x.png
vendored
Normal file
After Width: | Height: | Size: 940 B |
@ -1,27 +0,0 @@
|
||||
//
|
||||
// Constants.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Constants {
|
||||
|
||||
/**
|
||||
* The PHP versions supported by this application.
|
||||
* Versions that do not appear in this array are omitted from the list.
|
||||
*/
|
||||
static let SupportedPhpVersions = [
|
||||
"5.6",
|
||||
"7.0",
|
||||
"7.1",
|
||||
"7.2",
|
||||
"7.3",
|
||||
"7.4",
|
||||
"8.0",
|
||||
"8.1"
|
||||
]
|
||||
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
<p><b>Want to spread the love?</b> Leave a <a href="https://github.com/nicoverbruggen/phpmon">star on GitHub</a>!</p>
|
||||
<p><b>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p>
|
||||
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p>
|
||||
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
|
||||
<br>
|
||||
</body>
|
||||
</html>
|
||||
|
44
phpmon/Domain/Core/App+ActivationPolicy.swift
Normal file
@ -0,0 +1,44 @@
|
||||
//
|
||||
// App+ActivationPolicy.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 05/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
extension App {
|
||||
|
||||
// MARK: - Application State
|
||||
|
||||
/**
|
||||
Registers a window as currently open.
|
||||
*/
|
||||
public func register(window name: String) {
|
||||
if !openWindows.contains(name) {
|
||||
openWindows.append(name)
|
||||
}
|
||||
updateActivationPolicy()
|
||||
}
|
||||
|
||||
/**
|
||||
Removes a window, assuming it was closed.
|
||||
*/
|
||||
public func remove(window name: String) {
|
||||
openWindows.removeAll { window in
|
||||
window == name
|
||||
}
|
||||
updateActivationPolicy()
|
||||
}
|
||||
|
||||
/**
|
||||
If there are any open windows, the app will be a regular 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)
|
||||
}
|
||||
|
||||
}
|
55
phpmon/Domain/Core/App+GlobalHotkey.swift
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// App+GlobalHotkey.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 05/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import HotKey
|
||||
import Cocoa
|
||||
|
||||
extension App {
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
/**
|
||||
On startup, the preferences should be loaded from the .plist,
|
||||
and we'll enable the shortcut if it is set.
|
||||
*/
|
||||
func loadGlobalHotkey() {
|
||||
// Make sure we can retrieve the hotkey from preferences
|
||||
guard let hotkey = Preferences.preferences[.globalHotkey] as? String else {
|
||||
Log.info("No global hotkey was saved in preferences. None set.")
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure we can parse the JSON into the desired format
|
||||
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
|
||||
Log.err("No global hotkey loaded, could not be parsed!")
|
||||
shortcutHotkey = nil
|
||||
return
|
||||
}
|
||||
|
||||
shortcutHotkey = HotKey(keyCombo: KeyCombo(
|
||||
carbonKeyCode: keybindPref.keyCode,
|
||||
carbonModifiers: keybindPref.carbonFlags
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
Sets up the action that needs to occur when the shortcut key is pressed
|
||||
(opens the menu).
|
||||
*/
|
||||
func setupGlobalHotkeyListener() {
|
||||
guard let hotkey = shortcutHotkey else {
|
||||
return
|
||||
}
|
||||
|
||||
hotkey.keyDownHandler = {
|
||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -8,107 +8,85 @@
|
||||
import Cocoa
|
||||
import HotKey
|
||||
|
||||
class App {
|
||||
class App: PhpSwitcherDelegate {
|
||||
|
||||
// MARK: Static Vars
|
||||
|
||||
/** The static app instance. Accessible at any time. */
|
||||
static let shared = App()
|
||||
|
||||
init() {
|
||||
loadGlobalHotkey()
|
||||
}
|
||||
|
||||
static var phpInstall: PhpInstallation? {
|
||||
return App.shared.currentInstall
|
||||
/** Retrieve the version number from the main info dictionary, Info.plist. */
|
||||
static var version: String {
|
||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
|
||||
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
|
||||
return "\(version) (\(build))"
|
||||
}
|
||||
|
||||
/** Whether the app is busy doing something. Used to determine what UI to display. */
|
||||
static var busy: Bool {
|
||||
return App.shared.busy
|
||||
return PhpEnv.shared.isBusy
|
||||
}
|
||||
|
||||
// MARK: Variables
|
||||
|
||||
/** The list of preferences that are currently active. */
|
||||
var preferences: [PreferenceName: Bool]!
|
||||
|
||||
/**
|
||||
The window controller of the currently active window.
|
||||
*/
|
||||
var windowController: NSWindowController? = nil
|
||||
/** The window controller of the currently active preferences window. */
|
||||
var preferencesWindowController: PrefsWC? = nil
|
||||
|
||||
/**
|
||||
Whether the application is busy switching versions.
|
||||
*/
|
||||
var busy: Bool = false
|
||||
/** The window controller of the currently active site list window. */
|
||||
var siteListWindowController: SiteListWC? = nil
|
||||
|
||||
/**
|
||||
The currently active installation of PHP.
|
||||
*/
|
||||
var currentInstall: PhpInstallation? = nil
|
||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||
var detectedApplications: [Application] = []
|
||||
|
||||
/**
|
||||
All available versions of PHP.
|
||||
*/
|
||||
var availablePhpVersions : [String] = []
|
||||
|
||||
/**
|
||||
The timer that will periodically fetch the PHP version that is currently active.
|
||||
*/
|
||||
/** Timer that will periodically reload info about the user's PHP installation. */
|
||||
var timer: Timer?
|
||||
|
||||
/**
|
||||
Information we were able to discern from the Homebrew info command (as JSON).
|
||||
*/
|
||||
var brewPhpPackage: HomebrewPackage? = nil {
|
||||
didSet {
|
||||
brewPhpVersion = brewPhpPackage!.version
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The version that the `php` formula via Brew is aliased to on the current system.
|
||||
|
||||
If you're up to date, `php` will be aliased to the latest version,
|
||||
but that might not be the case.
|
||||
|
||||
We'll technically default to version 8.0, but the information should always be loaded
|
||||
from Homebrew itself upon starting the application.
|
||||
*/
|
||||
var brewPhpVersion: String = "8.0"
|
||||
|
||||
// MARK: - Global Hotkey
|
||||
|
||||
/**
|
||||
The shortcut the user has requested.
|
||||
*/
|
||||
var shortcutHotkey: HotKey? = nil {
|
||||
didSet {
|
||||
self.setupGlobalHotkeyListener()
|
||||
setupGlobalHotkeyListener()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
// MARK: - Activation Policy
|
||||
|
||||
private func loadGlobalHotkey() {
|
||||
let hotkey = Preferences.preferences[.globalHotkey] as! String?
|
||||
if hotkey == nil {
|
||||
return
|
||||
}
|
||||
/**
|
||||
Variable that keeps track of which windows are currently open.
|
||||
(Please note that window controllers remain open in memory once opened.)
|
||||
|
||||
When this list is updated, the app activation policy is re-evaluated.
|
||||
The app activation policy dictates how the app runs
|
||||
(as a normal app or as a toolbar app).
|
||||
*/
|
||||
var openWindows: [String] = []
|
||||
|
||||
// MARK: - App Watchers
|
||||
|
||||
/**
|
||||
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
|
||||
*/
|
||||
var watcher: PhpConfigWatcher!
|
||||
|
||||
// MARK: - PhpSwitcherDelegate
|
||||
|
||||
func switcherDidStartSwitching() {
|
||||
}
|
||||
|
||||
func switcherDidCompleteSwitch() {
|
||||
PhpEnv.shared.currentInstall = ActivePhpInstallation()
|
||||
handlePhpConfigWatcher()
|
||||
|
||||
let keybindPref = GlobalKeybindPreference.fromJson(hotkey!)
|
||||
|
||||
if (keybindPref != nil) {
|
||||
self.shortcutHotkey = HotKey(keyCombo: KeyCombo(
|
||||
carbonKeyCode: keybindPref!.keyCode,
|
||||
carbonModifiers: keybindPref!.carbonFlags
|
||||
))
|
||||
} else {
|
||||
self.shortcutHotkey = nil
|
||||
if let window = siteListWindowController {
|
||||
DispatchQueue.main.async {
|
||||
window.contentVC.reloadSites()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupGlobalHotkeyListener() {
|
||||
guard let hotKey = self.shortcutHotkey else {
|
||||
return
|
||||
}
|
||||
hotKey.keyDownHandler = {
|
||||
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
47
phpmon/Domain/Core/AppDelegate+InterApp.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// AppDelegate+InterApp.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 20/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
extension AppDelegate {
|
||||
|
||||
/**
|
||||
This is an entry point for future development for integrating with the PHP Monitor
|
||||
application URL. You can use the `phpmon://` protocol to communicate with the app.
|
||||
|
||||
At this time you can trigger the site list using Alfred (or some other application)
|
||||
by opening the following URL: `phpmon://list`.
|
||||
|
||||
Please note that PHP Monitor needs to be running in the background for this to work.
|
||||
*/
|
||||
func application(_ application: NSApplication, open urls: [URL]) {
|
||||
|
||||
if !Preferences.isEnabled(.allowProtocolForIntegrations) {
|
||||
Log.info("Acting on commands via phpmon:// has been disabled.")
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = urls.first else { return }
|
||||
|
||||
self.interpretCommand(
|
||||
url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""),
|
||||
commands: InterApp.getCommands()
|
||||
)
|
||||
}
|
||||
|
||||
private func interpretCommand(_ command: String, commands: [InterApp.Action]) {
|
||||
commands.forEach { action in
|
||||
if command.starts(with: action.command) {
|
||||
let lastElement = String(command.split(separator: "/").last!)
|
||||
action.action(lastElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
55
phpmon/Domain/Core/AppDelegate+MenuOutlets.swift
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// AppDelegate+MenuOutlets.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 05/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
/**
|
||||
Any outlets connected to the app's main menu (not the menu that shows when the icon in
|
||||
the menu bar is clicked, but the regular app's main menu) are configured here.
|
||||
|
||||
Default interactions like copy/paste, select all, close window etc. are wired up by
|
||||
default in the storyboard and do not need to be manually added.
|
||||
|
||||
Extra functionality (like the menu item to reload the list of sites) does, however.
|
||||
|
||||
- Note: This menu is only displayed when the app is NOT running in accessory mode.
|
||||
For more information about this, please see the ActivationPolicy-related extension.
|
||||
*/
|
||||
extension AppDelegate {
|
||||
|
||||
// MARK: - Menu Interactions
|
||||
|
||||
@IBAction func addSiteLinkPressed(_ sender: Any) {
|
||||
SiteListVC.show()
|
||||
|
||||
guard let windowController = App.shared.siteListWindowController else { return }
|
||||
windowController.pressedAddLink(nil)
|
||||
}
|
||||
|
||||
@IBAction func reloadSiteListPressed(_ sender: Any) {
|
||||
let vc = App.shared.siteListWindowController?
|
||||
.window?.contentViewController as? SiteListVC
|
||||
|
||||
if vc != nil {
|
||||
// If the view exists, directly reload the list of sites
|
||||
vc!.reloadSites()
|
||||
} else {
|
||||
// If the view does not exist, reload the cached data that was populated when the app initially launched.
|
||||
Valet.shared.reloadSites()
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func focusSearchField(_ sender: Any) {
|
||||
SiteListVC.show()
|
||||
|
||||
guard let windowController = App.shared.siteListWindowController else { return }
|
||||
windowController.searchToolbarItem.searchField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
}
|
46
phpmon/Domain/Core/AppDelegate+Notifications.swift
Normal file
@ -0,0 +1,46 @@
|
||||
//
|
||||
// AppDelegate+Notifications.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 06/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
extension AppDelegate {
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
/**
|
||||
Sets up notifications. That does mean we need to ask for permission first.
|
||||
If we cannot get permission, we should log this.
|
||||
*/
|
||||
public func setupNotifications() {
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.delegate = self
|
||||
notificationCenter.requestAuthorization(options: [.alert], completionHandler: { granted, error in
|
||||
if !granted {
|
||||
Log.warn("PHP Monitor does not have permission to show notifications.")
|
||||
}
|
||||
if let error = error {
|
||||
Log.err("PHP Monitor encounted an error determining notification permissions:")
|
||||
Log.err(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Ensure that the application displays notifications even when the app is active.
|
||||
*/
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler:
|
||||
@escaping (UNNotificationPresentationOptions) -> Void
|
||||
) {
|
||||
completionHandler([.banner])
|
||||
}
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ import Cocoa
|
||||
import UserNotifications
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
@ -18,25 +18,40 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
|
||||
(invoked by PHP Monitor) shell commands. It is used to
|
||||
invoke all commands in this application.
|
||||
*/
|
||||
let sharedShell : Shell
|
||||
let sharedShell: Shell
|
||||
|
||||
/**
|
||||
The App singleton contains information about the state of
|
||||
the application and global variables.
|
||||
*/
|
||||
let state : App
|
||||
let state: App
|
||||
|
||||
/**
|
||||
The MainMenu singleton is responsible for rendering the
|
||||
menu bar item and its menu, as well as its actions.
|
||||
*/
|
||||
let menu : MainMenu
|
||||
let menu: MainMenu
|
||||
|
||||
/**
|
||||
The paths singleton that determines where Homebrew is installed,
|
||||
and where to look for binaries.
|
||||
*/
|
||||
let paths : Paths
|
||||
let paths: Paths
|
||||
|
||||
/**
|
||||
The Valet singleton that determines all information
|
||||
about Valet and its current configuration.
|
||||
*/
|
||||
let valet: Valet
|
||||
|
||||
/**
|
||||
The PhpSwitcher singleton that handles PHP version
|
||||
detection, as well as switching. It is initialized
|
||||
when the app is ready and passed all checks.
|
||||
*/
|
||||
var switcher: PhpEnv! = nil
|
||||
|
||||
var logger = Log.shared
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
@ -44,37 +59,36 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
|
||||
When the application initializes, create all singletons.
|
||||
*/
|
||||
override init() {
|
||||
logger.verbosity = .performance
|
||||
Log.info("==================================")
|
||||
Log.info("PHP MONITOR by Nico Verbruggen")
|
||||
Log.info("Version \(App.version)")
|
||||
Log.info("==================================")
|
||||
self.sharedShell = Shell.user
|
||||
self.state = App.shared
|
||||
self.menu = MainMenu.shared
|
||||
self.paths = Paths.shared
|
||||
self.valet = Valet.shared
|
||||
super.init()
|
||||
}
|
||||
|
||||
func initializeSwitcher() {
|
||||
self.switcher = PhpEnv.shared
|
||||
self.switcher.delegate = self.state
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/**
|
||||
When the application has finished launching, we'll want to set up
|
||||
the user notification center delegate, and kickoff the menu
|
||||
the user notification center permissions, and kickoff the menu
|
||||
startup procedure.
|
||||
*/
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
NSUserNotificationCenter.default.delegate = self
|
||||
self.menu.startup()
|
||||
// Make sure notifications will work
|
||||
setupNotifications()
|
||||
// Make sure the menu performs its initial checks
|
||||
menu.startup()
|
||||
}
|
||||
|
||||
// MARK: - NSUserNotificationCenterDelegate
|
||||
|
||||
/**
|
||||
When a notification is sent, the delegate of the notification center
|
||||
is asked whether the notification should be presented or not. Since
|
||||
the user can now disable notifications per application since macOS
|
||||
Catalina, any and all notifications should be displayed.
|
||||
*/
|
||||
func userNotificationCenter(
|
||||
_ center: NSUserNotificationCenter,
|
||||
shouldPresent notification: NSUserNotification
|
||||
) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@ -31,6 +34,269 @@
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="File" id="XRy-v5-KNb">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="File" id="zA7-mh-f1x">
|
||||
<items>
|
||||
<menuItem title="Close" keyEquivalent="w" id="2FI-pQ-tuO">
|
||||
<connections>
|
||||
<action selector="performClose:" target="Ady-hI-5gd" id="ZHq-so-Sba"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Sites" id="9gy-d3-Pos">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Sites" id="YTZ-bb-TOG">
|
||||
<items>
|
||||
<menuItem title="add-as-link" keyEquivalent="n" id="du1-bO-N2U" userLabel="Add Link" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_add_folder_as_link"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="addSiteLinkPressed:" target="Voe-Tx-rLC" id="DzS-MY-6g0"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="reloadSiteListPressed:" 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"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="r2Z-pR-umI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="8Pm-83-BlM">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="jCt-Yf-FSE">
|
||||
<connections>
|
||||
<action selector="undo:" target="Ady-hI-5gd" id="O3z-27-Ug0"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="fCh-1M-Qyg">
|
||||
<connections>
|
||||
<action selector="redo:" target="Ady-hI-5gd" id="utE-Bv-fdY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="7Ja-wX-Yyy"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="wud-nd-1nZ">
|
||||
<connections>
|
||||
<action selector="cut:" target="Ady-hI-5gd" id="C3e-e7-Z50"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="V42-o1-WHL">
|
||||
<connections>
|
||||
<action selector="copy:" target="Ady-hI-5gd" id="ec3-KB-YgV"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="aBF-dz-Blf">
|
||||
<connections>
|
||||
<action selector="paste:" target="Ady-hI-5gd" id="BHd-PO-XsH"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="EgA-GE-99p">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="ls4-pp-hcL"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="smI-vK-hCc">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="Ady-hI-5gd" id="iNe-gC-rFo"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="29b-s6-UmK">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="Ady-hI-5gd" id="b6J-NN-IIc"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uFh-RS-XNP"/>
|
||||
<menuItem title="Find" id="Dvh-pB-nbE">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="QlO-5L-pAZ">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="m08-yq-ZGg">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="W9P-aN-Jes"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="pjr-Fe-SEl">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="XVP-he-TQd"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="Zpc-8S-9bB">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="oRy-fc-1aa"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="GDM-nF-rG0">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="x6a-fg-4qv"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="6fa-55-D8I">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="EGI-VW-wxB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="H8e-pj-DLt">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="oI9-dt-1tg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="RMo-NJ-dGJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="4PN-Vd-GBg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="2c8-04-pLg">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="hyy-YK-6Bw"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="ZBj-z6-5YX">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="Ady-hI-5gd" id="21B-wo-C7b"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="APF-Br-Trc"/>
|
||||
<menuItem title="Check Spelling While Typing" id="knZ-NA-0Jb">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="32z-g2-SCz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="v6M-1d-el3">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="1YL-19-eUI"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="qg8-Mm-AiQ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="zdy-r0-ioM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="SW4-hB-QOQ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="EmO-8n-AsV">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="rvM-Vq-p0Y">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="SjT-fP-U8q"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="TTo-oL-4Pj"/>
|
||||
<menuItem title="Smart Copy/Paste" id="op9-oC-x65">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="82G-c7-eEX"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="Sg4-Dr-IyH">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="tf9-2j-dbm"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="Uop-B5-hKQ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="2jO-5h-PhN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="G9f-Tv-imo">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="ryX-Py-Jan"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="9sq-LY-oWc">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="ps3-Vn-32V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="AQ0-Wh-nkQ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="nEj-vL-yg2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="BLU-2S-dqL">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="lFI-Ry-XFg">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="bx6-aZ-THy">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="tyN-SK-Cgt"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="Ks8-z7-N7j">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="0fo-Fo-xfq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="Lv4-Up-dyv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="Bqs-0x-WzX"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="cTl-lQ-Mg9">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="4c5-we-5Vo">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="YPC-zf-2Xh">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="Ady-hI-5gd" id="VRy-Kb-4cG"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="4YM-9V-tLE">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="KHB-GE-En3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
@ -49,11 +315,11 @@
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<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="-343" y="-16"/>
|
||||
<point key="canvasLocation" x="-495" y="-44"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="PQa-AT-b2a">
|
||||
@ -68,6 +334,10 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" 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>
|
||||
@ -85,12 +355,139 @@
|
||||
<objects>
|
||||
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" 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="574" height="189"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="550" height="498"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="GSr-K5-3yw">
|
||||
<rect key="frame" x="485" y="13" width="76" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="CLOSE" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ocw-Rx-gyh">
|
||||
<stackView distribution="fillEqually" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="k57-O3-Yyj">
|
||||
<rect key="frame" x="0.0" y="15" width="550" height="468"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="550" id="eOC-yS-nl6"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="k57-O3-Yyj" secondAttribute="bottom" constant="15" id="ECF-1q-1zc"/>
|
||||
<constraint firstItem="k57-O3-Yyj" firstAttribute="top" secondItem="Pf1-A5-3Xz" secondAttribute="top" constant="15" id="HwH-HC-MSf"/>
|
||||
<constraint firstAttribute="trailing" secondItem="k57-O3-Yyj" secondAttribute="trailing" id="M7l-W4-EDv"/>
|
||||
<constraint firstItem="k57-O3-Yyj" firstAttribute="leading" secondItem="Pf1-A5-3Xz" secondAttribute="leading" id="ctd-MO-fe1"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="stackView" destination="k57-O3-Yyj" id="fF8-8n-bc9"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="251" y="205"/>
|
||||
</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 & Parked" 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="550" height="263"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
|
||||
<view key="contentView" id="uVx-Da-x4I">
|
||||
<rect key="frame" x="0.0" y="0.0" width="550" height="263"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
<toolbar key="toolbar" implicitIdentifier="594015E3-8428-4926-9341-4B8CE4C7E373" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconOnly" sizeMode="regular" id="OOz-oZ-vlN">
|
||||
<allowedToolbarItems>
|
||||
<toolbarItem implicitItemIdentifier="5B9DBBA8-D173-4EAF-807C-C6EA0B4D806A" label="Add Link" paletteLabel="Add Link" tag="-1" bordered="YES" sizingBehavior="auto" id="GsC-ra-40U">
|
||||
<imageReference key="image" image="plus" catalog="system" symbolScale="medium"/>
|
||||
<connections>
|
||||
<action selector="pressedAddLink:" target="8Ec-9q-82s" id="H0o-No-x4M"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="B734CDE2-70E9-45A8-B1B3-5A5DE156621D" label="Reload" paletteLabel="Reload" tag="-1" bordered="YES" sizingBehavior="auto" id="YtK-vM-5y7">
|
||||
<imageReference key="image" image="arrow.clockwise" catalog="system" symbolScale="medium"/>
|
||||
<connections>
|
||||
<action selector="pressedReload:" target="8Ec-9q-82s" id="fLc-bD-oYQ"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<searchToolbarItem implicitItemIdentifier="629F0782-3C5F-4CD0-9396-3A054A422180" label="Search" paletteLabel="Search" visibilityPriority="1001" id="Q7Z-fw-lB9">
|
||||
<nil key="toolTip"/>
|
||||
<searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="oWA-TH-Pm7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="3NO-6x-aLc">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</searchFieldCell>
|
||||
</searchField>
|
||||
</searchToolbarItem>
|
||||
</allowedToolbarItems>
|
||||
<defaultToolbarItems>
|
||||
<toolbarItem reference="GsC-ra-40U"/>
|
||||
<toolbarItem reference="YtK-vM-5y7"/>
|
||||
<searchToolbarItem reference="Q7Z-fw-lB9"/>
|
||||
</defaultToolbarItems>
|
||||
</toolbar>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="8Ec-9q-82s" id="xEM-aj-eHL"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<outlet property="searchToolbarItem" destination="Q7Z-fw-lB9" id="J5o-oh-VhO"/>
|
||||
<segue destination="JZI-Vd-9oq" kind="relationship" relationship="window.shadowedContentViewController" id="9Gy-Gw-hPH"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-374" y="758"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="HTI-x5-rOp">
|
||||
<objects>
|
||||
<windowController storyboardIdentifier="addSiteWindow" id="N1O-Nj-C2V" sceneMemberID="viewController">
|
||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="yLy-XT-fuq">
|
||||
<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="7Is-aK-lDv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="N1O-Nj-C2V" id="CvY-PZ-Y6C"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="glS-wF-sEU" kind="relationship" relationship="window.shadowedContentViewController" id="6Sa-w0-Uov"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-339" y="1147"/>
|
||||
</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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
|
||||
<rect key="frame" x="13" y="13" width="103" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="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">
|
||||
DQ
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="pressedCreateLink:" target="glS-wF-sEU" id="Vh7-K5-ubM"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
||||
<rect key="frame" x="391" y="13" width="76" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="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">
|
||||
@ -98,129 +495,325 @@ Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="pressed:" target="AW2-rV-rbS" id="8dA-y4-voq"/>
|
||||
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MEf-MN-oXt">
|
||||
<rect key="frame" x="148" y="152" width="406" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="DYN_ICON" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="m5s-qp-Iaj">
|
||||
<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">
|
||||
<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="glS-wF-sEU" id="Dyf-0M-Gwj"/>
|
||||
</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">
|
||||
<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">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="toggledDynamicIcon:" target="AW2-rV-rbS" id="cuJ-mt-agf"/>
|
||||
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JrH-aa-AzL">
|
||||
<rect key="frame" x="148" y="131" width="408" height="14"/>
|
||||
<textFieldCell key="cell" title="DYN_ICON_DESC" id="MHA-Xt-xgF">
|
||||
<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">
|
||||
<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="V7b-jv-oCB">
|
||||
<rect key="frame" x="143" y="75" width="184" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="170" id="9jD-Bf-T2M"/>
|
||||
</constraints>
|
||||
<backgroundFilters>
|
||||
<ciFilter name="CIDotScreen">
|
||||
<configuration>
|
||||
<real key="inputAngle" value="0.0"/>
|
||||
<ciVector key="inputCenter">
|
||||
<real value="150"/>
|
||||
<real value="150"/>
|
||||
</ciVector>
|
||||
<null key="inputImage"/>
|
||||
<real key="inputSharpness" value="0.69999999999999996"/>
|
||||
<real key="inputWidth" value="6"/>
|
||||
</configuration>
|
||||
</ciFilter>
|
||||
</backgroundFilters>
|
||||
<buttonCell key="cell" type="push" title="SET_SHORTCUT" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="R63-tN-KVQ">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="register:" target="AW2-rV-rbS" id="4Mj-eM-4eW"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YsQ-AZ-Aei">
|
||||
<rect key="frame" x="325" y="75" width="138" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="CLEAR_SHORTCUT" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nvE-5d-VOS">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system" size="11"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="unregister:" target="AW2-rV-rbS" id="2RI-4w-6Td"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5ZK-BG-o1t">
|
||||
<rect key="frame" x="42" y="85" width="100" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="PREF_GLOSHO:" id="xiD-8H-p5s">
|
||||
<pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6JT-Vt-3q0">
|
||||
<rect key="frame" x="20" y="185" 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/nicoverbruggen/Code/nicoverbruggen.be/"/>
|
||||
</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">
|
||||
<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 horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="31d-gd-auR">
|
||||
<rect key="frame" x="18" y="153" width="124" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="120" id="8dt-Pg-wFI"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="PREF_DYN_ICON:" id="E10-ss-Cdz">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1TO-9H-z2d">
|
||||
<rect key="frame" x="148" y="60" width="101" height="14"/>
|
||||
<textFieldCell key="cell" title="SHORTCUT_DESC" id="nYP-yi-DBf">
|
||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
||||
<rect key="frame" x="115" y="23" width="128" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That link already exists." id="jOt-n6-TQf">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="JrH-aa-AzL" secondAttribute="trailing" constant="20" symbolic="YES" id="8iM-Xf-ShU"/>
|
||||
<constraint firstAttribute="trailing" secondItem="GSr-K5-3yw" secondAttribute="trailing" constant="20" symbolic="YES" id="AT9-5F-6g1"/>
|
||||
<constraint firstItem="YsQ-AZ-Aei" firstAttribute="leading" secondItem="V7b-jv-oCB" secondAttribute="trailing" constant="12" symbolic="YES" id="Bk6-4V-GLk"/>
|
||||
<constraint firstItem="31d-gd-auR" firstAttribute="top" secondItem="Pf1-A5-3Xz" secondAttribute="top" constant="20" symbolic="YES" id="C3K-NX-BBl"/>
|
||||
<constraint firstItem="YsQ-AZ-Aei" firstAttribute="top" secondItem="V7b-jv-oCB" secondAttribute="top" id="DY5-za-saX"/>
|
||||
<constraint firstItem="MEf-MN-oXt" firstAttribute="leading" secondItem="31d-gd-auR" secondAttribute="trailing" constant="10" id="G5S-JV-re3"/>
|
||||
<constraint firstItem="V7b-jv-oCB" firstAttribute="firstBaseline" secondItem="5ZK-BG-o1t" secondAttribute="firstBaseline" id="H5D-2D-DLH"/>
|
||||
<constraint firstItem="1TO-9H-z2d" firstAttribute="leading" secondItem="V7b-jv-oCB" secondAttribute="leading" id="Imk-o0-2fS"/>
|
||||
<constraint firstItem="JrH-aa-AzL" firstAttribute="leading" secondItem="MEf-MN-oXt" secondAttribute="leading" id="K2H-Af-2qK"/>
|
||||
<constraint firstItem="5ZK-BG-o1t" firstAttribute="top" secondItem="JrH-aa-AzL" secondAttribute="bottom" constant="30" id="NMk-yt-fha"/>
|
||||
<constraint firstItem="JrH-aa-AzL" firstAttribute="top" secondItem="MEf-MN-oXt" secondAttribute="bottom" constant="8" symbolic="YES" id="Vf8-fx-H50"/>
|
||||
<constraint firstItem="MEf-MN-oXt" firstAttribute="firstBaseline" secondItem="31d-gd-auR" secondAttribute="firstBaseline" id="W36-bE-iAT"/>
|
||||
<constraint firstItem="1TO-9H-z2d" firstAttribute="firstBaseline" secondItem="V7b-jv-oCB" secondAttribute="baseline" constant="25" id="bJG-ed-pch"/>
|
||||
<constraint firstItem="V7b-jv-oCB" firstAttribute="leading" secondItem="JrH-aa-AzL" secondAttribute="leading" id="bUY-uH-N7A"/>
|
||||
<constraint firstItem="5ZK-BG-o1t" firstAttribute="trailing" secondItem="31d-gd-auR" secondAttribute="trailing" id="c4g-jO-JUm"/>
|
||||
<constraint firstAttribute="bottom" secondItem="GSr-K5-3yw" secondAttribute="bottom" constant="20" symbolic="YES" id="dAS-yW-vua"/>
|
||||
<constraint firstItem="GSr-K5-3yw" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="mTE-WD-54L"/>
|
||||
<constraint firstItem="31d-gd-auR" firstAttribute="leading" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="o0J-yT-TDX"/>
|
||||
<constraint firstAttribute="trailing" secondItem="MEf-MN-oXt" secondAttribute="trailing" constant="20" symbolic="YES" id="pJg-zj-cBs"/>
|
||||
<constraint firstItem="GSr-K5-3yw" firstAttribute="top" secondItem="1TO-9H-z2d" secondAttribute="bottom" constant="20" id="pMZ-Gx-Jmm"/>
|
||||
<constraint firstItem="VzR-5a-cmT" firstAttribute="trailing" secondItem="ZX9-s1-23i" secondAttribute="trailing" id="06B-dj-IBU"/>
|
||||
<constraint firstItem="ZX9-s1-23i" firstAttribute="top" secondItem="6JT-Vt-3q0" secondAttribute="bottom" constant="8" symbolic="YES" id="0QU-nI-sYv"/>
|
||||
<constraint firstAttribute="bottom" secondItem="SwS-o8-pbl" secondAttribute="bottom" constant="20" symbolic="YES" id="24Z-vC-4E8"/>
|
||||
<constraint firstItem="900-Z2-tID" firstAttribute="centerY" secondItem="PVw-cM-qAB" secondAttribute="centerY" id="578-2f-4x8"/>
|
||||
<constraint firstItem="ZX9-s1-23i" firstAttribute="leading" secondItem="6JT-Vt-3q0" secondAttribute="trailing" constant="-440" id="6eF-GS-Xcn"/>
|
||||
<constraint firstItem="SwS-o8-pbl" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="900-Z2-tID" secondAttribute="trailing" constant="10" id="9uc-R7-CZk"/>
|
||||
<constraint firstItem="6JT-Vt-3q0" firstAttribute="top" secondItem="P0B-Ht-R8n" secondAttribute="bottom" constant="8" symbolic="YES" id="DGN-4k-X0h"/>
|
||||
<constraint firstItem="P0B-Ht-R8n" firstAttribute="top" secondItem="JJJ-T9-Yuv" secondAttribute="top" constant="20" symbolic="YES" id="F2r-6E-qxh"/>
|
||||
<constraint firstItem="mmQ-7e-dlb" firstAttribute="top" secondItem="KZf-b0-9cm" secondAttribute="bottom" constant="8" symbolic="YES" id="G21-Vd-tgl"/>
|
||||
<constraint firstItem="900-Z2-tID" firstAttribute="leading" secondItem="PVw-cM-qAB" secondAttribute="trailing" constant="8" symbolic="YES" id="QzV-vP-fbq"/>
|
||||
<constraint firstItem="VzR-5a-cmT" firstAttribute="leading" secondItem="ZX9-s1-23i" secondAttribute="leading" id="UPN-Ad-j3X"/>
|
||||
<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 firstItem="ZX9-s1-23i" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="bJ4-Yr-4ah"/>
|
||||
<constraint firstItem="KZf-b0-9cm" firstAttribute="top" secondItem="VzR-5a-cmT" secondAttribute="bottom" constant="16" id="bdw-P7-FLz"/>
|
||||
<constraint firstAttribute="trailing" secondItem="SwS-o8-pbl" secondAttribute="trailing" constant="20" symbolic="YES" id="bkx-g2-WCM"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6JT-Vt-3q0" secondAttribute="trailing" constant="20" symbolic="YES" id="ctg-Gt-34Y"/>
|
||||
<constraint firstItem="PVw-cM-qAB" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="fE5-T7-e8z"/>
|
||||
<constraint firstItem="mmQ-7e-dlb" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="fKH-1r-MIf"/>
|
||||
<constraint firstAttribute="trailing" secondItem="mmQ-7e-dlb" secondAttribute="trailing" constant="20" symbolic="YES" id="hjv-Xq-cxV"/>
|
||||
<constraint firstItem="6JT-Vt-3q0" firstAttribute="leading" secondItem="P0B-Ht-R8n" secondAttribute="leading" id="jxP-vM-eA9"/>
|
||||
<constraint firstItem="P0B-Ht-R8n" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="msC-eG-Fop"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="P0B-Ht-R8n" secondAttribute="trailing" constant="20" symbolic="YES" id="nvj-Ij-dcd"/>
|
||||
<constraint firstItem="VzR-5a-cmT" firstAttribute="top" secondItem="ZX9-s1-23i" secondAttribute="bottom" constant="8" symbolic="YES" id="sVP-EV-07F"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ZX9-s1-23i" secondAttribute="trailing" constant="20" symbolic="YES" id="tZ3-2X-JC9"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="KZf-b0-9cm" secondAttribute="trailing" constant="20" symbolic="YES" id="zq0-Ce-sCs"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="buttonClearShortcut" destination="YsQ-AZ-Aei" id="1xo-hk-HgM"/>
|
||||
<outlet property="buttonClose" destination="GSr-K5-3yw" id="d4I-Cf-gXD"/>
|
||||
<outlet property="buttonDynamicIcon" destination="MEf-MN-oXt" id="qEN-Vg-EZS"/>
|
||||
<outlet property="buttonSetShortcut" destination="V7b-jv-oCB" id="2aS-S4-cKR"/>
|
||||
<outlet property="labelDynamicIcon" destination="JrH-aa-AzL" id="CFc-fF-oPq"/>
|
||||
<outlet property="labelShortcut" destination="1TO-9H-z2d" id="paF-hK-78x"/>
|
||||
<outlet property="leftLabelDynamicIcon" destination="31d-gd-auR" id="ANZ-Zs-4d7"/>
|
||||
<outlet property="leftLabelGlobalShortcut" destination="5ZK-BG-o1t" id="73E-9i-cg8"/>
|
||||
<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="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"/>
|
||||
<outlet property="textFieldSecure" destination="mmQ-7e-dlb" id="LeA-YS-hRM"/>
|
||||
<outlet property="textFieldTitle" destination="P0B-Ht-R8n" id="Qh8-qv-6iR"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="264" y="399.5"/>
|
||||
<point key="canvasLocation" x="277" y="1137.5"/>
|
||||
</scene>
|
||||
<!--Site 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">
|
||||
<view key="view" id="rIZ-4U-bhj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<customView id="j65-Lf-0lG">
|
||||
<rect key="frame" x="9" y="0.0" width="581" height="203"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
</customView>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/>
|
||||
<clipView key="contentView" id="6IL-DW-37w">
|
||||
<rect key="frame" x="1" y="1" width="598" height="307"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" rowHeight="54" rowSizeStyle="automatic" viewBased="YES" id="cp3-34-pQj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="598" height="307"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<size key="intercellSpacing" width="17" height="0.0"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||
<tableColumns>
|
||||
<tableColumn width="586" minWidth="40" maxWidth="10000" id="oeH-B2-0rA">
|
||||
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
|
||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
|
||||
</tableHeaderCell>
|
||||
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="Ith-sv-3bo">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<tableCellView identifier="siteItem" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="0.0" width="581" height="54"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
||||
<rect key="frame" x="38" y="26" width="145" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
|
||||
<font key="font" metaFont="systemSemibold" size="13"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
|
||||
<rect key="frame" x="38" y="12" width="75" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
|
||||
<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>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QPX-eu-eV8">
|
||||
<rect key="frame" x="10" y="22" width="20" height="20"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="20" id="Bmk-CN-Yyn"/>
|
||||
<constraint firstAttribute="height" constant="20" id="d4z-lb-Ww0"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Lock" id="aJ0-ia-YrZ"/>
|
||||
</imageView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jKi-Ls-7FZ">
|
||||
<rect key="frame" x="474" y="28" width="64" height="11"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="DRIVER TYPE" id="fjd-eb-itv">
|
||||
<font key="font" metaFont="miniSystem"/>
|
||||
<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="TbX-e2-3QL">
|
||||
<rect key="frame" x="474" y="15" width="36" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Driver" id="GMt-SG-vFl">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="syz-LF-l6P">
|
||||
<rect key="frame" x="0.0" y="-2" width="581" height="5"/>
|
||||
</box>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0NQ-ZD-CqD">
|
||||
<rect key="frame" x="450" y="18" width="18" height="18"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="18" id="Suw-gm-AEi"/>
|
||||
<constraint firstAttribute="height" constant="18" id="qO6-vg-5nC"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="IconLinked" id="2ng-pK-kvv"/>
|
||||
<color key="contentTintColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
</imageView>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="3xt-wC-hUJ">
|
||||
<rect key="frame" x="363" y="18" width="75" height="18"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="75" id="VI8-MP-7Hv"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="inline" title=" PHP X.X" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="anZ-hP-G0R">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="smallSystemBold"/>
|
||||
</buttonCell>
|
||||
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<connections>
|
||||
<action selector="pressedPhpVersion:" target="5GY-nN-BWd" id="mB5-WD-aZy"/>
|
||||
</connections>
|
||||
</button>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5aN-ZI-D7U">
|
||||
<rect key="frame" x="341" y="20" width="14" height="14"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="14" id="NKD-Pc-okU"/>
|
||||
<constraint firstAttribute="width" constant="14" id="wrl-lJ-3eN"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Checkmark" id="R5o-Cd-a91"/>
|
||||
<color key="contentTintColor" name="IconColorGreen"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" secondItem="3xt-wC-hUJ" secondAttribute="trailing" constant="12" id="2G8-Ow-FTu"/>
|
||||
<constraint firstItem="3xt-wC-hUJ" firstAttribute="leading" secondItem="5aN-ZI-D7U" secondAttribute="trailing" constant="8" symbolic="YES" id="39Z-nB-kXx"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="TbX-e2-3QL" secondAttribute="trailing" constant="20" symbolic="YES" id="3vE-LR-S7N"/>
|
||||
<constraint firstItem="TbX-e2-3QL" firstAttribute="leading" secondItem="0NQ-ZD-CqD" secondAttribute="trailing" constant="8" symbolic="YES" id="4cb-D9-8d1"/>
|
||||
<constraint firstItem="XJL-Uw-frD" firstAttribute="leading" secondItem="QPX-eu-eV8" secondAttribute="trailing" constant="10" id="55y-3V-RYt"/>
|
||||
<constraint firstItem="syz-LF-l6P" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" id="8QK-nf-Fiw"/>
|
||||
<constraint firstItem="QPX-eu-eV8" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="top" id="9QB-jZ-k1V"/>
|
||||
<constraint firstItem="QPX-eu-eV8" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" constant="10" id="GOj-sw-ZlZ"/>
|
||||
<constraint firstItem="TbX-e2-3QL" firstAttribute="top" secondItem="jKi-Ls-7FZ" secondAttribute="bottom" constant="-1" id="J29-wT-Uex"/>
|
||||
<constraint firstItem="CXK-Q9-CpO" firstAttribute="leading" secondItem="XJL-Uw-frD" secondAttribute="leading" id="Ojw-VZ-3EG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="syz-LF-l6P" secondAttribute="trailing" id="PWd-5k-AlD"/>
|
||||
<constraint firstItem="XJL-Uw-frD" firstAttribute="top" secondItem="5GY-nN-BWd" secondAttribute="top" constant="12" id="QeE-c7-I9U"/>
|
||||
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="Utr-aa-tqX"/>
|
||||
<constraint firstItem="CXK-Q9-CpO" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="bottom" id="VKg-Vq-sYa"/>
|
||||
<constraint firstItem="5aN-ZI-D7U" firstAttribute="centerY" secondItem="3xt-wC-hUJ" secondAttribute="centerY" id="a6n-E2-i2x"/>
|
||||
<constraint firstItem="TbX-e2-3QL" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" constant="5" id="cN8-zO-fnc"/>
|
||||
<constraint firstAttribute="bottom" secondItem="syz-LF-l6P" secondAttribute="bottom" id="gj7-cJ-Lle"/>
|
||||
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="CXK-Q9-CpO" secondAttribute="trailing" constant="8" symbolic="YES" id="iEd-Y3-zhp"/>
|
||||
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="8" symbolic="YES" id="lLA-Jx-Q4W"/>
|
||||
<constraint firstItem="3xt-wC-hUJ" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="vhb-WC-3NC"/>
|
||||
<constraint firstAttribute="trailing" secondItem="jKi-Ls-7FZ" secondAttribute="trailing" constant="45" id="vwD-Sg-Lzc"/>
|
||||
<constraint firstItem="jKi-Ls-7FZ" firstAttribute="leading" secondItem="TbX-e2-3QL" secondAttribute="leading" id="zjN-s3-2Ww"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="buttonPhpVersion" destination="3xt-wC-hUJ" id="LpB-7n-qUr"/>
|
||||
<outlet property="imageViewLock" destination="QPX-eu-eV8" id="Nnh-kB-adG"/>
|
||||
<outlet property="imageViewPhpVersionOK" destination="5aN-ZI-D7U" id="ePz-Cb-dWk"/>
|
||||
<outlet property="imageViewType" destination="0NQ-ZD-CqD" id="Cph-FN-LaY"/>
|
||||
<outlet property="labelDriver" destination="TbX-e2-3QL" id="qJh-Ak-Dge"/>
|
||||
<outlet property="labelDriverType" destination="jKi-Ls-7FZ" id="ZTq-pP-qUC"/>
|
||||
<outlet property="labelPathName" destination="CXK-Q9-CpO" id="iVZ-cL-azB"/>
|
||||
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
|
||||
</connections>
|
||||
</tableCellView>
|
||||
</prototypeCellViews>
|
||||
</tableColumn>
|
||||
</tableColumns>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="JZI-Vd-9oq" id="sbf-YF-ENF"/>
|
||||
<outlet property="delegate" destination="JZI-Vd-9oq" id="kal-o7-c23"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
</subviews>
|
||||
</clipView>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="300" id="R3Z-g3-tYQ"/>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="600" id="iRQ-sz-oyv"/>
|
||||
</constraints>
|
||||
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
|
||||
<rect key="frame" x="1" y="293" width="598" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
|
||||
<rect key="frame" x="558" y="29" width="15" height="225"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
|
||||
<rect key="frame" x="285" y="150" width="30" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
|
||||
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
||||
</constraints>
|
||||
</progressIndicator>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/>
|
||||
<constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/>
|
||||
<constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/>
|
||||
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/>
|
||||
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerX" secondItem="rIZ-4U-bhj" secondAttribute="centerX" id="eD8-TV-7dF"/>
|
||||
<constraint firstAttribute="trailing" secondItem="p0j-eB-I2i" secondAttribute="trailing" id="zWH-TD-RZv"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/>
|
||||
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="251" y="741.5"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Checkmark" width="512" height="512"/>
|
||||
<image name="IconLinked" width="512" height="512"/>
|
||||
<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"/>
|
||||
<namedColor name="IconColorGreen">
|
||||
<color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// HomebrewPackage.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct HomebrewPackage : Decodable {
|
||||
let name: String
|
||||
let full_name: String
|
||||
let aliases: [String]
|
||||
|
||||
public var version: String {
|
||||
return aliases.first!.replacingOccurrences(of: "php@", with: "")
|
||||
}
|
||||
}
|
64
phpmon/Domain/Core/InterAppHandler.swift
Normal file
@ -0,0 +1,64 @@
|
||||
//
|
||||
// InterAppHandler.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 28/01/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class InterApp {
|
||||
|
||||
public static var bindings: [Action] = []
|
||||
|
||||
public static func register(_ action: Action) {
|
||||
self.bindings.append(action)
|
||||
}
|
||||
|
||||
public struct Action {
|
||||
let command: String
|
||||
let action: (String) -> Void
|
||||
}
|
||||
|
||||
static func getCommands() -> [InterApp.Action] { return [
|
||||
InterApp.Action(command: "list", action: { _ in
|
||||
SiteListVC.show()
|
||||
}),
|
||||
InterApp.Action(command: "services/stop", action: { _ in
|
||||
MainMenu.shared.stopAllServices()
|
||||
}),
|
||||
InterApp.Action(command: "services/restart/all", action: { _ in
|
||||
MainMenu.shared.restartAllServices()
|
||||
}),
|
||||
InterApp.Action(command: "services/restart/nginx", action: { _ in
|
||||
MainMenu.shared.restartNginx()
|
||||
}),
|
||||
InterApp.Action(command: "services/restart/php", action: { _ in
|
||||
MainMenu.shared.restartPhpFpm()
|
||||
}),
|
||||
InterApp.Action(command: "services/restart/dnsmasq", action: { _ in
|
||||
MainMenu.shared.restartDnsMasq()
|
||||
}),
|
||||
InterApp.Action(command: "locate/config", action: { _ in
|
||||
MainMenu.shared.openActiveConfigFolder()
|
||||
}),
|
||||
InterApp.Action(command: "locate/composer", action: { _ in
|
||||
MainMenu.shared.openGlobalComposerFolder()
|
||||
}),
|
||||
InterApp.Action(command: "locate/valet", action: { _ in
|
||||
MainMenu.shared.openValetConfigFolder()
|
||||
}),
|
||||
InterApp.Action(command: "phpinfo", action: { _ in
|
||||
MainMenu.shared.openPhpInfo()
|
||||
}),
|
||||
InterApp.Action(command: "switch/php/", action: { version in
|
||||
if PhpEnv.shared.availablePhpVersions.contains(version) {
|
||||
MainMenu.shared.switchToPhpVersion(version)
|
||||
} else {
|
||||
Alert.notify(message: "Unsupported version", info: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available.")
|
||||
}
|
||||
}),
|
||||
]}
|
||||
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
class Startup {
|
||||
|
||||
@ -26,7 +27,7 @@ class Startup {
|
||||
performEnvironmentCheck(
|
||||
!Shell.fileExists("\(Paths.binPath)/php"),
|
||||
messageText: "startup.errors.php_binary.title".localized,
|
||||
informativeText: "startup.errors.php_binary_desc".localized,
|
||||
informativeText: "startup.errors.php_binary.desc".localized,
|
||||
breaking: true
|
||||
)
|
||||
|
||||
@ -48,6 +49,21 @@ class Startup {
|
||||
breaking: true
|
||||
)
|
||||
|
||||
Valet.shared.version = VersionExtractor.from(valet("--version"))
|
||||
performEnvironmentCheck(
|
||||
Valet.shared.version == nil,
|
||||
messageText: "startup.errors.valet_version_unknown.title".localized,
|
||||
informativeText: "startup.errors.valet_version_unknown.desc".localized,
|
||||
breaking: true
|
||||
)
|
||||
|
||||
performEnvironmentCheck(
|
||||
HomebrewDiagnostics.cannotLoadService(),
|
||||
messageText: "startup.errors.services_json_error.title".localized,
|
||||
informativeText: "startup.errors.services_json_error.desc".localized,
|
||||
breaking: true
|
||||
)
|
||||
|
||||
performEnvironmentCheck(
|
||||
!Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"),
|
||||
messageText: "startup.errors.sudoers_brew.title".localized,
|
||||
@ -56,10 +72,9 @@ class Startup {
|
||||
)
|
||||
|
||||
performEnvironmentCheck(
|
||||
// Check for Valet; it can be symlinked or in .composer/vendor/bin
|
||||
// Check for Valet; it MUST be symlinked thanks to sudoers
|
||||
!(Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet")
|
||||
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/opt/homebrew/bin/valet")
|
||||
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains(".composer/vendor/bin/valet")
|
||||
),
|
||||
messageText: "startup.errors.sudoers_valet.title".localized,
|
||||
informativeText: "startup.errors.sudoers_valet.desc".localized,
|
||||
@ -75,37 +90,30 @@ class Startup {
|
||||
)
|
||||
|
||||
if (!failed) {
|
||||
determineBrewAliasVersion()
|
||||
initializeSwitcher()
|
||||
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
|
||||
success()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to avoid having to hard-code which version of PHP is aliased to what specific subversion,
|
||||
* PHP Monitor now determines the alias by checking the user's system.
|
||||
Because the Switcher requires various environment guarantees, the switcher is only
|
||||
initialized when it is done working.
|
||||
*/
|
||||
private func determineBrewAliasVersion()
|
||||
{
|
||||
print("PHP Monitor has determined the application has successfully passed all checks.")
|
||||
print("Determining which version of PHP is aliased to `php` via Homebrew...")
|
||||
|
||||
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
|
||||
|
||||
App.shared.brewPhpPackage = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self,
|
||||
from: brewPhpAlias.data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
print("When on your system, the `php` formula means version \(App.shared.brewPhpVersion)!")
|
||||
private func initializeSwitcher() {
|
||||
DispatchQueue.main.async {
|
||||
let appDelegate = NSApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.initializeSwitcher()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
|
||||
*
|
||||
* - Parameter condition: Fail condition to check for; if this returns `true`, the alert will be shown
|
||||
* - Parameter messageText: Short description of what is wrong
|
||||
* - Parameter informativeText: Expanded description of the environment check that failed
|
||||
* - Parameter breaking: If the application should terminate afterwards
|
||||
Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
|
||||
|
||||
- Parameter condition: Fail condition to check for; if this returns `true`, the alert will be shown
|
||||
- Parameter messageText: Short description of what is wrong
|
||||
- Parameter informativeText: Expanded description of the environment check that failed
|
||||
- Parameter breaking: If the application should terminate afterwards
|
||||
*/
|
||||
private func performEnvironmentCheck(
|
||||
_ condition: Bool,
|
||||
@ -119,9 +127,14 @@ class Startup {
|
||||
|
||||
DispatchQueue.main.async { [self] in
|
||||
// Present the information to the user
|
||||
Alert.notify(message: messageText, info: informativeText)
|
||||
Alert.notify(
|
||||
message: messageText,
|
||||
info: informativeText,
|
||||
style: breaking ? .critical : .warning
|
||||
)
|
||||
// Only breaking issues will throw the extra retry modal
|
||||
breaking ? failureCallback() : ()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -7,11 +7,12 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension Date
|
||||
{
|
||||
extension Date {
|
||||
|
||||
func toString() -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return dateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,3 +16,14 @@ extension NSMenu {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@IBDesignable class LocalizedMenuItem: NSMenuItem {
|
||||
|
||||
@IBInspectable
|
||||
var localizationKey: String? {
|
||||
didSet {
|
||||
self.title = localizationKey?.localized ?? self.title
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,6 +12,10 @@ extension String {
|
||||
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
|
||||
}
|
||||
|
||||
func localized(_ args: CVarArg...) -> String {
|
||||
String(format: self.localized, arguments: args)
|
||||
}
|
||||
|
||||
func countInstances(of stringToFind: String) -> Int {
|
||||
if (stringToFind.isEmpty) {
|
||||
return 0
|
||||
@ -33,4 +37,38 @@ extension String {
|
||||
let end = r.upperBound
|
||||
return String(self[start ..< end])
|
||||
}
|
||||
|
||||
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
|
||||
/*
|
||||
<1> We split the version by period (.).
|
||||
<2> Then, we find the difference of digit that we will zero pad.
|
||||
<3> If there are no differences, we don't need to do anything and use simple .compare.
|
||||
<4> We populate an array of missing zero.
|
||||
<5> We add zero pad array to a version with a fewer period and zero.
|
||||
<6> We user array components to build back our versions from components and compare them.
|
||||
This time it will have the same period and number of digit.
|
||||
*/
|
||||
func versionCompare(_ otherVersion: String) -> ComparisonResult {
|
||||
let versionDelimiter = "."
|
||||
|
||||
var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
|
||||
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
|
||||
|
||||
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
|
||||
|
||||
if zeroDiff == 0 { // <3>
|
||||
// Same format, compare normally
|
||||
return self.compare(otherVersion, options: .numeric)
|
||||
} else {
|
||||
let zeros = Array(repeating: "0", count: abs(zeroDiff)) // <4>
|
||||
if zeroDiff > 0 {
|
||||
otherVersionComponents.append(contentsOf: zeros) // <5>
|
||||
} else {
|
||||
versionComponents.append(contentsOf: zeros)
|
||||
}
|
||||
return versionComponents.joined(separator: versionDelimiter)
|
||||
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,17 +7,19 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Cocoa
|
||||
|
||||
// Adapted from: https://stackoverflow.com/a/46268778
|
||||
|
||||
protocol XibLoadable {
|
||||
|
||||
static var xibName: String? { get }
|
||||
static func createFromXib(in bundle: Bundle) -> Self?
|
||||
|
||||
}
|
||||
|
||||
extension XibLoadable where Self: NSView {
|
||||
|
||||
static var xibName: String? {
|
||||
return String(describing: Self.self)
|
||||
}
|
||||
@ -30,4 +32,5 @@ extension XibLoadable where Self: NSView {
|
||||
let views = Array<Any>(results).filter { $0 is Self }
|
||||
return views.last as? Self
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,9 +13,11 @@ class Alert {
|
||||
messageText: String,
|
||||
informativeText: String,
|
||||
buttonTitle: String = "OK",
|
||||
secondButtonTitle: String = ""
|
||||
secondButtonTitle: String = "",
|
||||
style: NSAlert.Style = .informational
|
||||
) -> Bool {
|
||||
let alert = NSAlert.init()
|
||||
alert.alertStyle = style
|
||||
alert.messageText = messageText
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: buttonTitle)
|
||||
@ -25,8 +27,37 @@ class Alert {
|
||||
return alert.runModal() == .alertFirstButtonReturn
|
||||
}
|
||||
|
||||
public static func notify(message: String, info: String) {
|
||||
_ = self.present(messageText: message, informativeText: info, buttonTitle: "OK", secondButtonTitle: "")
|
||||
public static func confirm(
|
||||
onWindow window: NSWindow,
|
||||
messageText: String,
|
||||
informativeText: String,
|
||||
buttonTitle: String = "OK",
|
||||
secondButtonTitle: String = "Cancel",
|
||||
style: NSAlert.Style = .warning,
|
||||
onFirstButtonPressed: @escaping (() -> Void)
|
||||
) {
|
||||
let alert = NSAlert.init()
|
||||
alert.alertStyle = style
|
||||
alert.messageText = messageText
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: buttonTitle)
|
||||
if (!secondButtonTitle.isEmpty) {
|
||||
alert.addButton(withTitle: secondButtonTitle)
|
||||
}
|
||||
alert.beginSheetModal(for: window) { response in
|
||||
if response == .alertFirstButtonReturn {
|
||||
onFirstButtonPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func notify(message: String, info: String, style: NSAlert.Style = .informational) {
|
||||
_ = present(
|
||||
messageText: message,
|
||||
informativeText: info,
|
||||
buttonTitle: "OK",
|
||||
secondButtonTitle: "",
|
||||
style: style
|
||||
)
|
||||
}
|
||||
}
|
||||
|
63
phpmon/Domain/Helpers/Application.swift
Normal file
@ -0,0 +1,63 @@
|
||||
//
|
||||
// Editor.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 07/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// An application that is capable of opening a particular directory (usually of a PHP project).
|
||||
/// In most cases this is going to be a code editor, but it could also be another application
|
||||
/// that supports opening those directories, like a visual Git client or a terminal app.
|
||||
class Application {
|
||||
|
||||
enum AppType {
|
||||
case editor, browser, git_gui, terminal, user_supplied
|
||||
}
|
||||
|
||||
/// Name of the app. Used for display purposes and to determine `name.app` exists.
|
||||
let name: String
|
||||
|
||||
/// Application type. Depending on the type, a different action might occur.
|
||||
let type: AppType
|
||||
|
||||
/// Initializer. Used to detect a specific app of a specific type.
|
||||
init(_ name: String, _ type: AppType) {
|
||||
self.name = name
|
||||
self.type = type
|
||||
}
|
||||
|
||||
/**
|
||||
Attempt to open a specific directory in the app of choice.
|
||||
(This will open the app if it isn't open yet.)
|
||||
*/
|
||||
@objc public func openDirectory(file: String) {
|
||||
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
|
||||
}
|
||||
|
||||
/** Checks if the app is installed. */
|
||||
func isInstalled() -> Bool {
|
||||
// If this script does not complain, the app exists!
|
||||
return Shell.user.executeSynchronously(
|
||||
"/usr/bin/open -Ra \"\(name)\"",
|
||||
requiresPath: false
|
||||
).task.terminationStatus == 0
|
||||
}
|
||||
|
||||
/**
|
||||
Detect which apps are available to open a specific directory.
|
||||
*/
|
||||
static public func detectPresetApplications() -> [Application] {
|
||||
return [
|
||||
Application("PhpStorm", .editor),
|
||||
Application("Visual Studio Code", .editor),
|
||||
Application("Sublime Text", .editor),
|
||||
Application("Sublime Merge", .git_gui),
|
||||
Application("iTerm", .terminal)
|
||||
].filter {
|
||||
return $0.isInstalled()
|
||||
}
|
||||
}
|
||||
}
|
29
phpmon/Domain/Helpers/Async.swift
Normal file
@ -0,0 +1,29 @@
|
||||
//
|
||||
// Async.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 23/01/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This generic async helper is something I'd like to use in more places.
|
||||
|
||||
The `DispatchQueue.global` into `DispatchQueue.main.async` logic is common in the app.
|
||||
|
||||
I could also use try `async` support which was introduced in Swift but that would
|
||||
require too much refactoring at this time to consider. I also need to read up on async
|
||||
in order to properly grasp all the gotchas. Looking into that later at some point.
|
||||
*/
|
||||
public func runAsync(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
|
||||
{
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
execute()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
23
phpmon/Domain/Helpers/Filesystem.swift
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// FileSystem.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 07/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Filesystem {
|
||||
|
||||
/**
|
||||
Checks if a file exists at the provided path.
|
||||
Uses `FileManager`.
|
||||
*/
|
||||
public static func fileExists(_ path: String) -> Bool {
|
||||
return FileManager.default.fileExists(
|
||||
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -6,14 +6,28 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
class LocalNotification {
|
||||
|
||||
public static func send(title: String, subtitle: String) {
|
||||
let notification = NSUserNotification()
|
||||
notification.title = title
|
||||
notification.subtitle = subtitle
|
||||
NSUserNotificationCenter.default.deliver(notification)
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.body = subtitle
|
||||
|
||||
let uuidString = UUID().uuidString
|
||||
let request = UNNotificationRequest(
|
||||
identifier: uuidString,
|
||||
content: content,
|
||||
trigger: nil
|
||||
)
|
||||
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.add(request) { (error) in
|
||||
if error != nil {
|
||||
Log.err(error!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ class MenuBarImageGenerator {
|
||||
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
|
||||
|
||||
let targetImage: NSImage = NSImage(size: image.size)
|
||||
|
||||
let rep: NSBitmapImageRep = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil,
|
||||
pixelsWide: Int(image.size.width),
|
||||
@ -56,7 +57,7 @@ class MenuBarImageGenerator {
|
||||
|
||||
targetImage.addRepresentation(rep)
|
||||
targetImage.lockFocus()
|
||||
|
||||
|
||||
image.draw(in: imageRect)
|
||||
text.draw(in: textRect, withAttributes: textFontAttributes)
|
||||
|
||||
@ -64,4 +65,37 @@ class MenuBarImageGenerator {
|
||||
return targetImage
|
||||
}
|
||||
|
||||
/**
|
||||
The same as before, but also attempts to add an icon to the left.
|
||||
*/
|
||||
public static func textToImageWithIcon(text: String) -> NSImage {
|
||||
let textImage = self.textToImage(text: text)
|
||||
let iconImage = NSImage(named: "StatusBarPHP")!
|
||||
let iconWidthSize = iconImage.size.width
|
||||
let divider = iconWidthSize
|
||||
|
||||
let imageRect = CGRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: textImage.size.width + divider,
|
||||
height: textImage.size.height
|
||||
)
|
||||
|
||||
let image: NSImage = NSImage(size: imageRect.size)
|
||||
image.lockFocus()
|
||||
|
||||
let difference = imageRect.size.width - textImage.size.width
|
||||
|
||||
textImage.draw(in: imageRect, from: NSRect(
|
||||
x: -difference,
|
||||
y: 0, width: textImage.size.width + difference,
|
||||
height: textImage.size.height
|
||||
), operation: .overlay, fraction: 1)
|
||||
|
||||
iconImage.draw(in: imageRect, from: NSRect(x: 0, y: 0, width: imageRect.size.width * 1.6, height: imageRect.size.height * 2.0), operation: .overlay, fraction: 1)
|
||||
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}
|
||||
|
||||
}
|
||||
|
52
phpmon/Domain/Helpers/PMWindowController.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// PMWindowController.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 05/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
/**
|
||||
This window class keeps track of which windows are currently visible, and reports this info back to the App class.
|
||||
For more information, check the `windows` property on `App`.
|
||||
|
||||
- Note: This class does make a simple assumption: each window controller corresponds to a single view.
|
||||
*/
|
||||
class PMWindowController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
public var windowName: String {
|
||||
fatalError("Please specify a window name")
|
||||
}
|
||||
|
||||
override func showWindow(_ sender: Any?) {
|
||||
super.showWindow(sender)
|
||||
App.shared.register(window: windowName)
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
App.shared.remove(window: windowName)
|
||||
}
|
||||
|
||||
deinit {
|
||||
Log.perf("Window controller '\(windowName)' was deinitialized")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NSWindowController {
|
||||
|
||||
public func positionWindowInTopLeftCorner() {
|
||||
guard let frame = NSScreen.main?.frame else { return }
|
||||
guard let window = self.window else { return }
|
||||
|
||||
window.setFrame(NSRect(
|
||||
x: frame.size.width - window.frame.size.width - 20,
|
||||
y: frame.size.height - window.frame.size.height - 40,
|
||||
width: window.frame.width,
|
||||
height: window.frame.height
|
||||
), display: true)
|
||||
}
|
||||
|
||||
}
|
32
phpmon/Domain/Helpers/Timer.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// BenchmarkTimer.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 10/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class BenchmarkTimer {
|
||||
let startTime: CFAbsoluteTime
|
||||
var endTime: CFAbsoluteTime?
|
||||
|
||||
init() {
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
|
||||
func stop() -> CFAbsoluteTime {
|
||||
endTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
return duration!
|
||||
}
|
||||
|
||||
var duration: CFAbsoluteTime? {
|
||||
if let endTime = endTime {
|
||||
return endTime - startTime
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|