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

Compare commits

..

151 Commits
v5.3 ... v5.6.1

Author SHA1 Message Date
a407515534 🚀 Version 5.6.1 2022-09-24 12:56:14 +02:00
b827ffb869 🔧 Bump version number 2022-09-23 16:46:23 +02:00
bdb718598e 🐛 Various bugfixes
- Fixes issue with `scanApps` being non-optional in custom configuration
- Fixes issue with position of separator if Xdebug is not detected
- Ensure that `isRunningSwiftUIPreview` modifier always return false for debug builds
2022-09-23 16:46:13 +02:00
ddfc73e033 🐛 Resolve issue with determining PATH (#194)
In previous builds, PHP Monitor would use interactive mode when opening
a /bin/zsh shell, in order to be able to load the full PATH.

This is a problem because when launching PHP Monitor via the command
line or when you have some interactivity in an actual interactive
shell, it is possible to get stuck waiting for a particular input
which will effectively 'freeze' PHP Monitor's shell.

There are two options to fix this:
1) work with a timeout, which may or may not return a PATH
2) use a non-interactive shell and source .zshrc

I chose option 2, which is the more robust choice. If no .zshrc file
exists, it is also not sourced to avoid warnings or errors from ending
up in the PATH.
2022-09-23 16:44:06 +02:00
cfae520984 👌 Correctly resolve tagged version (#195) 2022-09-22 18:43:09 +02:00
0c176493e5 🔀 Merge branch 'main' into dev/5.6 2022-09-18 14:08:48 +02:00
71da62f954 📝 Update contribution guidelines 2022-09-18 14:08:10 +02:00
d6781568a3 📝 Update README 2022-09-18 13:39:40 +02:00
c9c7e14416 👌 Add support for <= and < version constraints
I am entirely unsure why one would need these, but I figured I'd get
these in the app before I start the work on PHP Monitor 6.0.

This ensures all common version constraints can now be parsed correctly.
2022-09-18 00:05:37 +02:00
0947dc5ecc 🚀 Version 5.6 2022-09-16 19:33:42 +02:00
286cdd00e9 🔧 Prepare for release 2022-09-16 19:31:32 +02:00
42b79d3cb3 🔧 Upgrade to Xcode 14 2022-09-10 21:44:17 +02:00
36aa41568c 🐛 Fix issue with minimum width w/ hidden UI 2022-09-10 21:43:26 +02:00
273c51f702 🔧 Update credits URLs 2022-09-09 21:41:20 +02:00
186f80c90e Allow more UI tweaking 2022-09-08 18:14:22 +02:00
d93af814c9 👌 Fix extension visibility of NSMenu 2022-09-08 17:21:41 +02:00
f4885f7dbc 👌 Update pre-release PHP version notice 2022-09-08 17:20:30 +02:00
805c9f5e6a Add new Preferences panel for UI tweaking 2022-09-07 21:40:43 +02:00
183d0bbc30 Allow hiding of global version switcher 2022-09-07 21:14:56 +02:00
4855c14d28 ♻️ Refactor Preferences & PreferenceName
Also added a few new preferences related to toggling specific menu items
based on your personal choices.

(These new settings still need to be added to the UI.)
2022-09-07 20:33:23 +02:00
588398ea76 🍱 New screenshots with updated UI 2022-09-06 18:02:27 +02:00
d2502cfba2 Improved site isolation switch (#191) 2022-09-05 21:09:31 +02:00
8d46fa8a4e 👌 Right-clicking a row selects it in domain list 2022-09-05 20:05:16 +02:00
e59b89ea49 🔧 Tweak URLs to reference phpmon.app domain 2022-09-05 19:55:41 +02:00
18b103bf9c 🚀 Version 5.5.1
This release fixes an issue that could occur if your username and your
home directory have different names. (#189)
2022-09-01 17:30:55 +02:00
9b8de47f5d Fix file membership so all tests pass again 2022-09-01 17:27:17 +02:00
da2934c2e5 Fix file membership so all tests pass again 2022-08-31 17:08:51 +02:00
9de4fc6712 🐛 Don't use whoami and use NSHomeDirectory()
- This commit replaces the usage of `whoami` with `id -un`.
- This also changes all `~` replacements with the result of calling
  the `NSHomeDirectory()` which may differ from `id -un` (#189)
2022-08-29 18:15:13 +02:00
4f4c950349 🐛 Don't use whoami and use NSHomeDirectory()
- This commit replaces the usage of `whoami` with `id -un`.
- This also changes all `~` replacements with the result of calling
  the `NSHomeDirectory()` which may differ from `id -un` (#189)
2022-08-29 18:06:11 +02:00
331ca8a9a4 🚀 Version 5.5 2022-08-28 15:24:28 +02:00
87e08e4607 🔧 Use icon for stable build 2022-08-24 20:33:35 +02:00
845235a276 🔧 Bump build number for new build 2022-08-24 20:28:12 +02:00
7587126a42 🐛 Potential fix for parsing Valet version (#188) 2022-08-24 20:27:40 +02:00
f4b1e0745a 🐛 Potential fix for parsing Valet version (#188) 2022-08-24 20:27:19 +02:00
c7bb4c1d37 ♻️ Refactor submenu creation 2022-08-22 19:10:36 +02:00
a17512bfad 📝 Update README 2022-08-22 18:48:43 +02:00
a85e016b4a 📝 Update README 2022-08-22 18:47:47 +02:00
1292e91b33 ♻️ Cleanup 2022-08-18 17:38:46 +02:00
8c55fee18d ♻️ Refactor NSMenu structuring 2022-08-18 02:12:24 +02:00
663082d725 ♻️ Refactor NSMenu structuring
* Added a different way to create menus
* Added NoDomainResultsView (WIP)
2022-08-18 01:59:06 +02:00
fcdb4a8993 Allow opening of PHP Doctor via First Aid menu 2022-08-16 19:35:18 +02:00
9134f08ec9 👌 Fix no warnings view 2022-08-16 19:29:15 +02:00
b281df3bbd ♻️ Correctly refresh warnings 2022-08-15 01:57:22 +02:00
a9f9c38e0d ♻️ Rework how the user's PATH is loaded 2022-08-15 01:47:55 +02:00
bbbdce6b44 ♻️ Reworked helper scripts
- Add 'Welcome Tour' to First Aid menu
- Updated 'Welcome Tour'
- Helpers are now always written to ~/.config/phpmon/bin
- Updated helpers (now symlinked)
- Updated checks for when to symlink helpers
2022-08-14 23:57:46 +02:00
237185995d 🔥 Removed assets from tour 2022-08-14 22:23:39 +02:00
48f950fc3a 🐛 Fix translation string (#185)
This ensures that the "failed to check for update" modal now displays
the correct localized string.
2022-08-14 22:22:06 +02:00
2ee0934080 🔧 Make PHP Doctor optional (preference) 2022-08-13 23:16:10 +02:00
cfbb83976d 👌 Tweak preview 2022-08-12 21:07:38 +02:00
4e5b178e36 🏗 WIP: Tweaks to PHP Doctor 2022-08-12 20:50:27 +02:00
0c59d14da5 🐛 Fix isWriteableFile:atPath 2022-08-11 22:41:47 +02:00
2a9c8e6830 Check if /usr/local/bin helper is writable 2022-08-10 21:20:11 +02:00
78e750d764 Check if running the app with Rosetta 2022-08-10 21:04:23 +02:00
c1c7561361 🏗 WIP: Warning manager 2022-08-09 21:55:59 +02:00
f90925ee17 🏗 WIP: Warnings window & views 2022-08-09 20:31:17 +02:00
ccfb15c83c ♻️ Use WindowController instead of WC
- Renamed `ProgressWC` to `TerminalProgressWindowController` so it
  better reflects the functionality of the class.

- Renamed all `-WC` suffixed classes to `-WindowController`. This is
  cleaner because it avoids referencing water closets. 🚽

- Fixed some instances where the copyright notice used the wrong
  filename. All window controller classes are now accurate.
2022-08-09 18:04:18 +02:00
bcc80b5210 👌 More SwiftUI tuning (automatic height) 2022-08-05 20:05:29 +02:00
023043a81d 🍱 Further tweaks to onboarding view 2022-08-03 19:10:13 +02:00
a2c93833df 🍱 Tweaked onboarding view 2022-08-01 22:39:21 +02:00
8b6267f411 👌 Fix WarningView styling 2022-08-01 21:18:20 +02:00
1bff75311b Add WarningView 2022-07-31 21:27:52 +02:00
7a580eef0c 📝 Update README 2022-07-29 18:14:24 +02:00
2306529936 ♻️ Differentiate between folder and file existence
This also fixes #182, because it introduces a check for Valet's config
directory, which does not exist if Valet has not been installed yet.
2022-07-28 21:26:41 +02:00
08fdcdbc6c 👌 Simple cleanup 2022-07-27 20:38:39 +02:00
8cb8e5e409 🍱 Visual changes to onboarding screen 2022-07-26 20:30:19 +02:00
6094f83e98 👌 Do not require window from storyboard 2022-07-26 19:36:27 +02:00
cdb4b60487 👌 Add warning message for WIP launch screen 2022-07-26 19:34:30 +02:00
ae5bed2532 🔀 Merge branch 'feature/onboarding' into dev/5.5 2022-07-26 19:32:45 +02:00
f098ffbf3d ♻️ Consistency in naming of window controllers 2022-07-26 19:30:35 +02:00
0a55b45c60 🔧 Prepare 5.5 branch for dev builds 2022-07-25 21:40:43 +02:00
418d1e2479 Add support for custom environment vars (#183)
This commit adds support for custom environment variables, which can be
set in PHP Monitor's custom configuration file.

Let's say you wish to customize the `COMPOSER_HOME` env variable, like
in #183. You can fix this like so:

```json
{
    "scan_apps": [],
    "services": [],
    "presets": []
    "export": {
        "COMPOSER_HOME": "/absolute/path/to/composer/folder"
    }
}
```

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

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

    ~/.config/phpmon/preset_revert.json

If that file is present and valid, the app should enable the 'revert'
option. (That still needs to be implemented.)
2022-06-01 21:01:18 +02:00
86d74619b1 📝 Use non-standard way to add dark images 2022-06-01 12:47:54 +02:00
0c09e808bd 📝 Test dark mode image 2022-06-01 12:44:22 +02:00
da8659adba Enable version switching in presets
* Moved Preset to dedicated file
* Added async friendly PHP version switch
* Added conditional PHP switch based on Preset
2022-05-31 21:43:24 +02:00
bbebe78997 Updated UI for presets 2022-05-30 19:34:10 +02:00
19aa804cbb 🐛 Alert user about issue #174 2022-05-29 22:30:11 +02:00
64491c6fe1 Allow application of presets 2022-05-29 12:32:48 +02:00
382cb177be Correctly load presets from config file 2022-05-21 15:24:40 +02:00
e9f0d19d9a 🔧 Use dev icon 2022-05-19 20:12:45 +02:00
7709cd9f6c 👌 Cleanup 2022-05-19 20:12:30 +02:00
83eac7bf04 👌 Save multiple Xdebug modes 2022-05-19 20:01:28 +02:00
db8197df3d 👌 Handle multiple modes w/ Xdebug menu item
This commit also fixes the width of the header items.
2022-05-19 19:06:03 +02:00
40e404fe24 👌 Prototype (non-functional) for presets 2022-05-19 01:50:15 +02:00
e7f80ebce8 Switch Xdebug mode 2022-05-19 01:05:06 +02:00
990152d77d Allow replacing of config values 2022-05-19 00:08:26 +02:00
2e61479c75 Allow reading of configuration files 2022-05-18 19:45:16 +02:00
0579ebb1c1 ️ More efficient extension parsing 2022-05-18 19:23:56 +02:00
f9df86851c Tweak description about sudoers files 2022-05-15 15:42:01 +02:00
b0c62e226a 👌 Code cleanup 2022-05-15 15:15:49 +02:00
1392b6e4a0 🔧 New version under development 2022-05-13 17:04:45 +02:00
12163bc87b 🔀 Merge branch 'main' into dev/5.4 2022-05-13 17:04:07 +02:00
f679231ade ♻️ Cleanup 2022-05-05 20:09:40 +02:00
f725e09f55 ♻️ WIP: Parsing logic for configuration file 2022-05-05 20:05:52 +02:00
99881bf4cd ♻️ WIP: Refactor determining PHP configuration
The way .ini files are loaded is changing with this commit. Instead of
directly saving which extensions were found, the extensions loaded are
now determined by reading the .ini file.

However, there are some performance concerns here. Perhaps it is worth
*not* reloading the contents of these files unless absolutely necessary.
2022-05-04 20:25:59 +02:00
127 changed files with 4976 additions and 1826 deletions

23
.github/contributing.md vendored Normal file
View File

@ -0,0 +1,23 @@
# Contribution Guidelines
Thank you for your interest in contributing to PHP Monitor.
I consider this project a bit of a nice side-project to my daily gig, so it is very much a personal affair where I love to tinker around.
**While the code of the latest PHP Monitor release is public, many things are constantly in flux that may not be pushed to this repository yet.**
I don't mean to be rude, but I don't want other people involved with the project beyond simply contributing a few small things here and there, as has been the case in the past.
The extra mental overhead of having additional contributors to report to, whose code will need to be reviewed... it's a lot and it makes working on PHP Monitor less enjoyable for me.
Plus, at this point, the majority of PHP Monitor's main functionality is also done.
As a result, I may refer you to this file at some point. Again, I don't wish to be rude, but this general rule stands:
**Making any changes in a fork and opening a pull request without opening an issue first will most likely result in your PR being closed without mercy.**
To repeat, I am **not opposed** to small contributions and fixes, if they are **meaningful or insightful**.
To learn more, please check out the [pull request template](/.github/pull_request_template.md) which contains more information about my contribution requirements. (This will also show up when you open a new PR.)
Thank you for respecting this!

View File

@ -16,7 +16,7 @@ In short: It is usually best to *get in touch first* if you are making substanti
## About destination branches
Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Merge requests that target `main` will be closed without mercy.**
Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Pull requests that target `main` will be closed without mercy.**
Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released.

View File

@ -41,6 +41,12 @@ If you'd like to create a production build, choose "Any Mac" as the target and s
10. Update Cask with new version + hash
11. Check new version can be installed via Cask
## 🍱 Marketing Mode
You can enable marketing mode by setting the `PHPMON_MARKETING_MODE` environment variable. It preloads a list of (fake) domains in the domain window list for screenshot & marketing purposes.
launchctl setenv PHPMON_MARKETING_MODE true
## 🐛 Symbolication of crashes
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
LastUpgradeVersion = "1400"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@ -69,8 +69,13 @@
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "PHPMON_MARKETING_MODE"
value = "YES"
key = "EXTREME_DOCTOR_MODE"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>

169
README.md
View File

@ -1,16 +1,19 @@
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️
> **Note**
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider [sponsoring](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️
<p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p>
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
<img src="./docs/screenshot.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
@ -21,10 +24,10 @@ You can also add new domains as links, isolate sites, manage various services, a
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
* macOS 11 Big Sur or later
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* Homebrew `php` formula is installed
* Laravel Valet 2.16 or newer (supports Valet 3)
* Laravel Valet 3 recommended (but compatible with Valet 2)
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
@ -103,14 +106,14 @@ Super convenient!
<details>
<summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary>
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it:
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it.
Install [Homebrew](https://brew.sh) first.
**I have also created [a video tutorial](https://www.youtube.com/watch?v=fO3hVhkvm3w) which may be easier to follow. If you just want the terminal commands, keep reading.**
Install PHP, composer, add to path:
Install [Homebrew](https://brew.sh) first. Follow the instructions there first!
Then, you'll need to set up your PATH.
brew install php
brew install composer
nano .zshrc
Make sure the following line is not in the comments:
@ -123,30 +126,61 @@ If you're on an Apple Silicon-based Mac, you'll need to add:
# on an M1 Mac
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
and add the following to your .zshrc, but add this BEFORE the homebrew PATH additions:
and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
If you're adding composer and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
export PATH=$HOME/bin:/usr/local/bin:$PATH
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
If you are *not* on Apple Silicon, you should remove the third line.
Install the `php` and `composer` formulae:
brew install php composer
Make sure PHP is linked correctly:
which php
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php`)
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon)
composer global require laravel/valet
For optimal results, you should lock your PHP platform for global dependencies to the oldest version of PHP you intend to run. If that version is PHP 7.0, your `~/.composer/composer.json` file could look like this (please adjust the version accordingly!):
```
{
"require": {
"laravel/valet": "^3.0",
},
"config": {
"platform": {
"php": "7.0"
}
}
}
```
Run `composer global update` again. This ensures that when you switch to a different global PHP version, [Valet won't break](https://github.com/nicoverbruggen/phpmon/issues/178). If it does, PHP Monitor will let you know what you can do about this.
Then, install Valet:
valet install
This should install `dnsmasq` and set up Valet. Great, almost there!
valet trust
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
You can now install PHP Monitor, if you haven't already:
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
</details>
<details>
@ -297,6 +331,95 @@ The app includes an Internet Access Policy file, so if you're using something li
</details>
<details>
<summary><strong>How do I various presets to show up?</strong></summary>
You must set these presets up in a JSON file, located in `~/.config/phpmon/config.json`.
You must have set up at least one valid preset for this presets to work in PHP Monitor.
Here's an example of a working preset:
<pre>
{
"scan_apps": [],
"services": [],
"presets": [
{
"name": "Legacy Project",
"php": "8.0",
"extensions": {
"xdebug": false
},
"configuration": {
"memory_limit": "128M",
"upload_max_filesize": "128M",
"post_max_size": "128M"
}
}
],
"export": {}
}
</pre>
You can omit the `php` key in the preset if you do not wish for the preset to switch to a given PHP version.
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
<details>
<summary><strong>How do I ensure additional Homebrew services are shown in the app?</strong></summary>
You must set these services up in a JSON file, located in `~/.config/phpmon/config.json`.
You can specify custom services in the configuration file for Homebrew services that run as your own user (not root).
> **Info**
> If your service must run as root, it cannot currently be added to PHP Monitor.
You can find out which services are available by running `brew services list`.
Here's an example where we add the `mailhog` and `mysql` services to PHP Monitor:
<pre>
{
"scan_apps": [],
"services": ["mailhog", "mysql"],
"presets": [],
"export": {}
}
</pre>
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
<details>
<summary><strong>How do I set custom environment variables?</strong></summary>
You must configure these custom environment variables up in a JSON file, located in `~/.config/phpmon/config.json`.
PHP Monitor uses a default Shell environment, with no custom environment variables. You need to set custom environment variables manually. These are then used for e.g. Composer.
Here's an example of a working `COMPOSER_HOME` environment variable which is respected:
<pre>
{
"scan_apps": [],
"services": [],
"presets": [],
"export": {
"COMPOSER_HOME": "/absolute/path/to/composer/folder"
}
}
</pre>
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
<details>
<summary><strong>How do I get various applications to show up in the domain list's right-click menu?</strong></summary>
@ -308,7 +431,7 @@ All of these apps should just be detected correctly, no matter their location on
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
You can add your own apps by creating and editing a `~/.phpmon.conf.json` file, with the following entry:
You can add your own apps by creating and editing a `~/.config/phpmon/config.json` file, and make sure the `scan_apps` key is set:
<pre>
{
@ -317,6 +440,9 @@ You can add your own apps by creating and editing a `~/.phpmon.conf.json` file,
</pre>
You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary.
> **Warning**
> You must restart PHP Monitor for these changes to be detected.
</details>
<details>
@ -406,14 +532,14 @@ Donations really help with the Apple Developer Program cost, and keep me motivat
## 😎 Acknowledgements
While I did make this application during my own free time, PHP Monitor started out from various learning experiments during work hours at my employer, DIVE. I'd also like to shout out the following folks:
Special thanks go out to:
* My colleagues at [DIVE](https://dive.be)
* Everyone supporting me via [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen)
* Everyone who has donated via [my sponsor page](https://nicoverbruggen.be/sponsor)
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors)
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so
* My [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen) and those who have donated
* Everyone who has left feedback and reported bugs (appreciate it!)
* Everyone in the Laravel community who shared the app (thanks!)
* Everyone who has left feedback and reported bugs
* Everyone in the Laravel community who shared the app, especially on Twitter
Thank you very much for your contributions, kind words and support.
@ -447,7 +573,8 @@ If an extension or other process writes to a single file a bunch of times in a s
1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name.
1. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all.
*Note*: If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
> **Note**
> If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly.
### Want to know more?

View File

@ -6,9 +6,9 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 (*) | 3.0 (2.16.2 minimum) |
| 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0)* | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
_(*) Support for PHP 5.6 is only included if you are using Valet 2.x, since support for PHP 5.6 was dropped in Valet 3.0._
_(*) macOS Ventura (13.0) is not officially supported until it officially releases._
## Legacy versions
@ -16,9 +16,9 @@ These versions of PHP Monitor are no longer supported, but if youre using an
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 |
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 | 2.13 |
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |

BIN
docs/notification-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/screenshot-dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 519 KiB

View File

@ -37,43 +37,43 @@ class NginxConfigurationTest: XCTestCase {
func testCanDetermineSiteNameAndTld() throws {
XCTAssertEqual(
"nginx-site",
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
)
XCTAssertEqual(
"test",
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld
)
}
func testCanDetermineIsolation() throws {
XCTAssertNil(
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
)
XCTAssertEqual(
"8.1",
NginxConfiguration.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion
NginxConfigurationFile.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion
)
}
func testCanDetermineProxy() throws {
let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.proxyUrl.path)!
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.proxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
let normal = NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)!
let normal = NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)!
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual(nil, normal.proxy)
}
func testCanDetermineSecuredProxy() throws {
let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.secureProxyUrl.path)!
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.secureProxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
}
func testCanDetermineProxyWithCustomTld() throws {
let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)!
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://localhost:8080", proxied.proxy)
}

View File

@ -0,0 +1,84 @@
//
// PhpConfigurationTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class PhpConfigurationTest: XCTestCase {
static var phpIniFileUrl: URL {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
}
func testCanLoadExtension() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertNotNil(iniFile)
XCTAssertGreaterThan(iniFile.extensions.count, 0)
}
func testCanCheckKeyExistence() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertTrue(iniFile.has(key: "error_reporting"))
XCTAssertTrue(iniFile.has(key: "display_errors"))
XCTAssertFalse(iniFile.has(key: "my_unknown_key"))
}
func testCanCheckKeyValue() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertNotNil(iniFile.get(for: "error_reporting"))
XCTAssert(iniFile.get(for: "error_reporting") == "E_ALL")
XCTAssertNotNil(iniFile.get(for: "display_errors"))
XCTAssert(iniFile.get(for: "display_errors") == "On")
}
func testCanCustomizeConfigurationValue() throws {
let destination = Utility
.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let configurationFile = PhpConfigurationFile
.from(filePath: destination.path)!
// 0. Verify the original value
XCTAssertEqual(configurationFile.get(for: "error_reporting"), "E_ALL")
// 1. Change the value
try! configurationFile.replace(
key: "error_reporting",
value: "E_ALL & ~E_DEPRECATED & ~E_STRICT"
)
XCTAssertEqual(
configurationFile.get(for: "error_reporting"),
"E_ALL & ~E_DEPRECATED & ~E_STRICT"
)
// 2. Ensure that same key and value doesn't break subsequent saves
try! configurationFile.replace(
key: "error_reporting",
value: "error_reporting"
)
XCTAssertEqual(
configurationFile.get(for: "error_reporting"),
"error_reporting"
)
// 3. Verify subsequent saves weren't broken
try! configurationFile.replace(
key: "error_reporting",
value: "E_ALL"
)
XCTAssertEqual(
configurationFile.get(for: "error_reporting"),
"E_ALL"
)
}
}

View File

@ -15,13 +15,13 @@ class PhpExtensionTest: XCTestCase {
}
func testCanLoadExtension() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
XCTAssertGreaterThan(extensions.count, 0)
}
func testExtensionNameIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
let extensionNames = extensions.map { (ext) -> String in
return ext.name
@ -40,7 +40,7 @@ class PhpExtensionTest: XCTestCase {
}
func testExtensionStatusIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
// xdebug should be enabled
XCTAssertEqual(extensions[0].enabled, true)
@ -51,7 +51,7 @@ class PhpExtensionTest: XCTestCase {
func testToggleWorksAsExpected() throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.load(from: destination)
let extensions = PhpExtension.from(filePath: destination.path)
XCTAssertEqual(extensions.count, 6)
// Try to disable xdebug (should be detected first)!
@ -66,12 +66,7 @@ class PhpExtensionTest: XCTestCase {
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
// Make sure if we load the data again, it's disabled
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
}
func testCanRetrieveXdebugMode() throws {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
XCTAssertEqual(value, "coverage")
XCTAssertEqual(PhpExtension.from(filePath: destination.path).first!.enabled, false)
}
}

View File

@ -0,0 +1,34 @@
{
"scan_apps": [],
"presets": [
{
"name": "Default PHP",
"extensions": {
"xdebug": false
},
"configuration": {
"memory_limit": "128M"
}
},
{
"name": "Personal Site",
"extensions": {
"xdebug": true
},
"configuration": {
"xdebug.mode": "coverage",
"memory_limit": "512M"
}
},
{
"name": "PHP Monitor",
"extensions": {
"xdebug": true
},
"configuration": {
"xdebug.mode": "coverage",
"memory_limit": "512M"
}
}
]
}

View File

@ -18,4 +18,30 @@ class AppUpdaterCheckTest: XCTestCase {
XCTAssertNotNil(version)
}
func testTaggedReleaseOmitsZeroPatch() {
let version = AppVersion.from("3.5.0_333")!
XCTAssertEqual(version.tagged, "3.5")
XCTAssertEqual(version.version, "3.5.0")
}
func testTaggedReleaseDoesntOmitNonZeroPatch() {
let version = AppVersion.from("3.5.1_333")!
XCTAssertEqual(version.tagged, "3.5.1")
XCTAssertEqual(version.version, "3.5.1")
}
func testTagTruncationDoesntAffectMajorVersions() {
var version = AppVersion.from("5.0_333")!
XCTAssertEqual(version.tagged, "5.0")
XCTAssertEqual(version.version, "5.0")
version = AppVersion.from("5.0.0_333")!
XCTAssertEqual(version.tagged, "5.0")
XCTAssertEqual(version.version, "5.0.0")
}
}

View File

@ -8,6 +8,7 @@
import XCTest
// swiftlint:disable type_body_length
class PhpVersionNumberTest: XCTestCase {
func testCanDeconstructPhpVersion() throws {
@ -287,4 +288,76 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.3.1", "7.2.9"]).all
)
}
func testCanCheckLessThanOrEqualConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<=7.2", strict: true),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<=7.2.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<=7.2.5", strict: true),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
// Non-strict check (ignoring patch has no effect)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<=7.2.5", strict: false),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
}
func testCanCheckLessThanConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<7.2", strict: true),
PhpVersionNumberCollection
.make(from: ["7.1", "7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<7.2.0", strict: true),
PhpVersionNumberCollection
.make(from: ["7.1", "7.0"]).all
)
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<7.2.5", strict: true),
PhpVersionNumberCollection
.make(from: ["7.2", "7.1", "7.0"]).all
)
// Non-strict check (patch resolves to 7.2.999, which is bigger than 7.2.5)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
.matching(constraint: "<7.2.5", strict: false),
PhpVersionNumberCollection
.make(from: ["7.1", "7.0"]).all
)
}
}

View File

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

View File

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

View File

@ -1,25 +0,0 @@
{
"images" : [
{
"filename" : "ServiceLoading.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ServiceLoading@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,25 +0,0 @@
{
"images" : [
{
"filename" : "ServiceOff.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ServiceOff@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,25 +0,0 @@
{
"images" : [
{
"filename" : "ServiceOn.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ServiceOn@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -24,7 +24,7 @@ class Actions {
brew("services restart dnsmasq", sudo: true)
}
public static func stopAllServices() {
public static func stopValetServices() {
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true)
@ -64,6 +64,29 @@ class Actions {
}
}
// MARK: - Third Party Services
public static func stopService(name: String, completion: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
brew("services stop \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
ServicesManager.loadHomebrewServices(completed: {
DispatchQueue.main.async {
completion()
}
})
}
}
public static func startService(name: String, completion: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
brew("services start \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
ServicesManager.loadHomebrewServices(completed: {
DispatchQueue.main.async {
completion()
}
})
}
}
// MARK: - Finding Config Files
public static func openGenericPhpConfigFolder() {
@ -88,6 +111,12 @@ class Actions {
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpMonitorConfigFile() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/phpmon")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL {

View File

@ -53,18 +53,22 @@ struct Constants {
struct Urls {
static let DonationPayment = URL(
string: "https://nicoverbruggen.be/sponsor#pay-now"
)!
// phpmon.app URLs (these are aliased to redirect correctly)
static let DonationPage = URL(
string: "https://nicoverbruggen.be/sponsor"
string: "https://phpmon.app/sponsor"
)!
static let FrequentlyAskedQuestions = URL(
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
string: "https://phpmon.app/faq"
)!
static let DonationPayment = URL(
string: "https://phpmon.app/sponsor/now"
)!
// GitHub URLs (do not alias these)
static let GitHubReleases = URL(
string: "https://github.com/nicoverbruggen/phpmon/releases"
)!

View File

@ -21,7 +21,7 @@ public class Paths {
init() {
baseDir = App.architecture != "x86_64" ? .opt : .usr
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
userName = String(Shell.pipe("id -un").split(separator: "\n")[0])
}
public func detectBinaryPaths() {
@ -57,6 +57,10 @@ public class Paths {
return shared.userName
}
public static var homePath: String {
return NSHomeDirectory()
}
public static var cellarPath: String {
return "\(shared.baseDir.rawValue)/Cellar"
}

View File

@ -0,0 +1,33 @@
//
// Shell+PATH.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Shell {
var PATH: String {
let task = Process()
task.launchPath = "/bin/zsh"
let command = Filesystem.fileExists("~/.zshrc")
// source the user's .zshrc file if it exists to complete $PATH
? ". ~/.zshrc && echo $PATH"
// otherwise, non-interactive mode is sufficient
: "echo $PATH"
task.arguments = ["--login", "-lc", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: String.Encoding.utf8) ?? ""
}
}

View File

@ -32,6 +32,9 @@ public class Shell {
*/
public var shell: String = "/bin/sh"
/** Additional exports that are sent if `requiresPath` is set to true. */
public var exports: String = ""
/**
Singleton to access a user shell (with --login)
*/
@ -114,13 +117,23 @@ public class Shell {
Creates a new process with the correct PATH and shell.
*/
public func createTask(for command: String, requiresPath: Bool) -> Process {
let tailoredCommand = requiresPath
? "export PATH=\(Paths.binPath):$PATH && \(command)"
: command
var completeCommand = ""
if requiresPath {
// Basic export (PATH)
completeCommand += "export PATH=\(Paths.binPath):$PATH && "
// Put additional exports in between
if !self.exports.isEmpty {
completeCommand += "\(self.exports) && "
}
}
completeCommand += command
let task = Process()
task.launchPath = self.shell
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
task.arguments = ["--noprofile", "-norc", "--login", "-c", completeCommand]
return task
}

View File

@ -0,0 +1,24 @@
//
// ArrayExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Array {
/**
Sourced from Stack Overflow
https://stackoverflow.com/a/33540708
*/
func chunked(by distance: Int) -> [[Element]] {
let indicesSequence = stride(from: startIndex, to: endIndex, by: distance)
let array: [[Element]] = indicesSequence.map {
let newIndex = $0.advanced(by: distance) > endIndex ? endIndex : $0.advanced(by: distance)
return Array(self[$0 ..< newIndex])
}
return array
}
}

View File

@ -9,21 +9,17 @@
import Cocoa
extension NSMenu {
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
newItem.keyEquivalentModifierMask = modifier
self.addItem(newItem)
convenience init(items: [NSMenuItem], target: NSObject? = nil) {
self.init()
self.addItems(items, target: target)
}
}
@IBDesignable class LocalizedMenuItem: NSMenuItem {
@IBInspectable
var localizationKey: String? {
didSet {
self.title = localizationKey?.localized ?? self.title
public func addItems(_ items: [NSMenuItem], target: NSObject? = nil) {
for item in items {
self.addItem(item)
if target != nil {
item.target = target
}
}
}
}

View File

@ -0,0 +1,87 @@
//
// NSMenuItem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 18/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension NSMenuItem {
convenience init(
title: String,
action: Selector? = nil,
keyEquivalent: String = "",
keyModifier: NSEvent.ModifierFlags = [],
toolTip: String? = nil
) {
self.init(title: title, action: action, keyEquivalent: keyEquivalent)
self.keyEquivalentModifierMask = keyModifier
self.toolTip = toolTip
}
convenience init(
title: String,
keyEquivalent: String = "",
keyModifier: NSEvent.ModifierFlags = [],
toolTip: String? = nil,
submenu: [NSMenuItem],
target: NSObject? = nil
) {
self.init(title: title, action: nil, keyEquivalent: keyEquivalent)
self.keyEquivalentModifierMask = keyModifier
self.toolTip = toolTip
self.submenu = NSMenu(items: submenu, target: target)
}
}
// MARK: - NSMenuItem subclasses
@IBDesignable class LocalizedMenuItem: NSMenuItem {
@IBInspectable var localizationKey: String? {
didSet {
self.title = localizationKey?.localized ?? self.title
}
}
}
class PhpMenuItem: NSMenuItem {
var version: String = ""
}
class XdebugMenuItem: NSMenuItem {
var mode: String = ""
}
class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension?
}
class EditorMenuItem: NSMenuItem {
var editor: Application?
}
class PresetMenuItem: NSMenuItem {
var preset: Preset?
static func getAll() -> [NSMenuItem] {
return Preferences.custom.presets!.map { preset in
let presetMenuItem = PresetMenuItem(
title: preset.getMenuItemText(),
action: #selector(MainMenu.togglePreset(sender:))
)
if let attributedString = try? NSMutableAttributedString(
data: preset.getMenuItemText().data(using: .utf8)!,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil
) {
presetMenuItem.attributedTitle = attributedString
}
presetMenuItem.preset = preset
return presetMenuItem
}
}
}

View File

@ -11,6 +11,20 @@ import Cocoa
extension NSWindow {
/**
Centers a window. Taken from: https://stackoverflow.com/a/66140320
*/
public func setCenterPosition(offsetY: CGFloat = 0) {
if let screenSize = screen?.visibleFrame.size {
self.setFrameOrigin(
NSPoint(
x: (screenSize.width - frame.size.width) / 2,
y: (screenSize.height - frame.size.height) / 2 + offsetY
)
)
}
}
/**
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
*/

View File

@ -5,13 +5,23 @@
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import SwiftUI
extension String {
var localized: String {
if #available(macOS 13, *) {
return NSLocalizedString(
self, tableName: nil, bundle: Bundle.main, value: "", comment: ""
).replacingOccurrences(of: "Preferences", with: "Settings")
}
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}
var localizedForSwiftUI: LocalizedStringKey {
return LocalizedStringKey(self.localized)
}
func localized(_ args: CVarArg...) -> String {
String(format: self.localized, arguments: args)
}
@ -32,7 +42,7 @@ extension String {
return count
}
subscript (r: Range<String.Index>) -> String {
subscript(r: Range<String.Index>) -> String {
let start = r.lowerBound
let end = r.upperBound
return String(self[start ..< end])
@ -71,4 +81,22 @@ extension String {
}
}
var stripped: String {
do {
guard let data = self.data(using: .unicode) else {
return ""
}
let attributed = try NSAttributedString(
data: data,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue],
documentAttributes: nil
)
return attributed.string
} catch {
return ""
}
}
}

View File

@ -1,5 +1,5 @@
//
// FileSystem.swift
// Filesystem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
@ -7,17 +7,55 @@
//
import Cocoa
import Foundation
class Filesystem {
/**
Checks if a file exists at the provided path.
Uses `FileManager`.
Checks if a file or directory exists at the provided path.
*/
public static func fileExists(_ path: String) -> Bool {
public static func exists(_ path: String) -> Bool {
return FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath)
)
}
/**
Checks if a file exists at the provided path.
*/
public static func fileExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
isDirectory: &isDirectory
)
return exists && !isDirectory.boolValue
}
/**
Checks if a directory exists at the provided path.
*/
public static func directoryExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
isDirectory: &isDirectory
)
return exists && isDirectory.boolValue
}
/**
Checks if a given file is a symbolic link.
*/
public static func fileIsSymlink(_ path: String) -> Bool {
do {
let attribs = try FileManager.default.attributesOfItem(atPath: path)
return attribs[.type] as! FileAttributeType == FileAttributeType.typeSymbolicLink
} catch {
return false
}
}
}

View File

@ -10,7 +10,11 @@ import UserNotifications
class LocalNotification {
public static func send(title: String, subtitle: String) {
@MainActor public static func send(title: String, subtitle: String, preference: PreferenceName) {
if !Preferences.isEnabled(preference) {
return
}
let content = UNMutableNotificationContent()
content.title = title
content.body = subtitle

View File

@ -20,7 +20,13 @@ class ActivePhpInstallation {
var version: Version!
var limits: Limits!
var extensions: [PhpExtension]!
var iniFiles: [PhpConfigurationFile] = []
var extensions: [PhpExtension] {
return iniFiles.flatMap { initFile in
return initFile.extensions
}
}
// MARK: - Computed
@ -34,16 +40,21 @@ class ActivePhpInstallation {
// Show information about the current version
getVersion()
// Initialize the list of ini files that are loaded
iniFiles = []
// If an error occurred, exit early
if version.error {
limits = Limits()
extensions = []
return
}
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path)
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
iniFiles.append(file)
}
// Get configuration values
limits = Limits(
@ -60,9 +71,8 @@ class ActivePhpInstallation {
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
let loadedExtensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if !loadedExtensions.isEmpty {
extensions.append(contentsOf: loadedExtensions)
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
iniFiles.append(file)
}
}
}

View File

@ -7,20 +7,48 @@
//
import Foundation
import Cocoa
class Xdebug {
public static var enabled: Bool {
return !self.mode.isEmpty
return PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") != nil
}
public static var mode: String {
return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
public static var activeModes: [String] {
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
return []
}
guard let value = file.get(for: "xdebug.mode") else {
return []
}
return value.components(separatedBy: ",").filter { self.modes.contains($0) }
}
public static func asMenuItems() -> [NSMenuItem] {
var items: [NSMenuItem] = []
let activeModes = Self.activeModes
for mode in Self.modes {
let item = XdebugMenuItem(
title: mode,
action: #selector(MainMenu.toggleXdebugMode(sender:)),
keyEquivalent: ""
)
item.state = activeModes.contains(mode) ? .on : .off
item.mode = mode
items.append(item)
}
return items
}
public static var modes: [String] {
return [
"off",
"develop",
"coverage",
"debug",

View File

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

View File

@ -174,4 +174,14 @@ class PhpEnv {
return false
}
/**
Returns the configuration file instance that is used for a specific config value.
You can then use the configuration file instance to change values.
*/
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
return PhpEnv.phpInstall.iniFiles
.reversed()
.first(where: { $0.has(key: key) })
}
}

View File

@ -16,8 +16,18 @@ class PhpHelper {
// Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "")
// Determine the dotless name for this PHP version
let destination = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
// Check if the ~/.config/phpmon/bin directory is in the PATH
let inPath = Shell.user.PATH.contains("\(Paths.homePath)/.config/phpmon/bin")
// Check if we can create symlinks (`/usr/local/bin` must be writable)
let canWriteSymlinks = FileManager.default.isWritableFile(atPath: "/usr/local/bin/")
do {
let destination = "/usr/local/bin/pm\(dotless)"
Shell.run("mkdir -p ~/.config/phpmon/bin")
if FileManager.default.fileExists(atPath: destination) {
let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) {
@ -52,10 +62,40 @@ class PhpHelper {
// Make sure the file is executable
Shell.run("chmod +x \(destination)")
// Create a symlink if the folder is not in the PATH
if !inPath {
// First, check if we can create symlinks at all
if !canWriteSymlinks {
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
return
}
// Write the symlink
self.createSymlink(dotless)
}
} catch {
print(error)
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
}
}
private static func createSymlink(_ dotless: String) {
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
let destination = "/usr/local/bin/pm\(dotless)"
if !Filesystem.fileExists(destination) {
Log.info("Creating new symlink: \(destination)")
Shell.run("ln -s \(source) \(destination)")
return
}
if !Filesystem.fileIsSymlink(destination) {
Log.info("Overwriting existing file with new symlink: \(destination)")
Shell.run("ln -fs \(source) \(destination)")
return
}
Log.info("Symlink in \(destination) already exists, OK.")
}
}

View File

@ -87,11 +87,19 @@ public struct PhpVersionNumberCollection: Equatable {
return self.versions.filter { $0.isNewerThan(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isOlderThan(version, strict)}
}
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThan) {
return self.versions.filter { $0.isOlderThan(version, strict)}
}
return []
}
}
public struct PhpVersionNumber: Equatable {
public struct PhpVersionNumber: Equatable, Hashable {
let major: Int
let minor: Int
let patch: Int?
@ -116,12 +124,8 @@ public struct PhpVersionNumber: Equatable {
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
// TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
/*
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
*/
}
public static func parse(_ text: String) throws -> Self {
@ -175,6 +179,15 @@ public struct PhpVersionNumber: Equatable {
)
}
internal func isOlderThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return (
self.major < version.major ||
self.major == version.major && self.minor < version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) < version.patch(strict)
)
}
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major &&
(

View File

@ -0,0 +1,216 @@
//
// PhpConfigurationFile.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpConfigurationFile: CreatedFromFile {
struct ConfigValue {
let lineIndex: Int
let value: String
}
typealias Section = [String: ConfigValue]
typealias Config = [String: Section]
/// The file where this configuration file was located.
let filePath: String
/// The extensions found in this .ini file.
var extensions: [PhpExtension]
/// The actual, structured content of the configuration file.
var content: Config
/// The original lines of the file.
var lines: [String]
/** Resolves a PHP configuration file (.ini) */
static func from(filePath: String) -> Self? {
let path = filePath.replacingOccurrences(of: "~", with: Paths.homePath)
do {
let fileContents = try String(contentsOfFile: path)
return Self.init(path: path, contents: fileContents)
} catch {
Log.warn("Could not read the PHP configuration file at: `\(filePath)`")
return nil
}
}
required init(path: String, contents: String) {
self.filePath = path
self.lines = contents.components(separatedBy: "\n")
self.extensions = PhpExtension.from(lines, filePath: path)
self.content = Self.parseConfig(lines: lines)
}
// MARK: API
public func has(key: String) -> Bool {
return self.content.contains { (_: String, section: Section) in
return section.keys.contains(key)
}
}
public func get(for key: String) -> String? {
return getConfig(for: key)?.value
}
public func getConfig(for key: String) -> ConfigValue? {
for (_, section) in self.content where section.keys.contains(key) {
return section[key]!
}
return nil
}
enum ReplacementErrors: Error {
case missingKey
}
/**
Replaces the value for a specific (existing) key with a new value.
The key must exist for this to work.
*/
public func replace(key: String, value: String) throws {
// Ensure that the key exists
guard let item = getConfig(for: key) else {
throw ReplacementErrors.missingKey
}
// Figure out what comes after the assignment
var components = self
.lines[item.lineIndex]
.components(separatedBy: "=")
// Replace the value with the new one
components[1] = components[1]
.replacingOccurrences(of: item.value, with: value)
// Replace the specific line
self.lines[item.lineIndex] = components.joined(separator: "=")
// Finally, join the string and save the file atomatically again
try self.lines.joined(separator: "\n")
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
// Reload the original file
self.reload()
}
public func reload() {
self.lines = try! String(contentsOfFile: self.filePath)
.components(separatedBy: "\n")
self.extensions = PhpExtension.from(lines, filePath: self.filePath)
self.content = Self.parseConfig(lines: lines)
}
// MARK: Parsing Logic
// Slightly modified from: https://gist.github.com/jetmind/f776c0d223e4ac6aec1ff9389e874553
/**
Attempts to parse the configuration file, based on an array of strings.
Each string is a line from the configuration file.
*/
private static func parseConfig(lines: [String]) -> Config {
var config = Config()
var currentSectionName = "main"
for (index, line) in lines.enumerated() {
let line = trim(line)
if line.hasPrefix("[") && line.hasSuffix("]") {
currentSectionName = parseSectionHeader(line)
} else if let (key, value) = parseLine(line) {
var section = config[currentSectionName] ?? [:]
section[key] = ConfigValue(
lineIndex: index,
value: value
)
config[currentSectionName] = section
}
}
return config
}
/**
Remove all whitespace and additional characters from individual lines.
*/
private static func trim(_ string: String) -> String {
let whitespaces = CharacterSet(charactersIn: " \n\r\t")
return string.trimmingCharacters(in: whitespaces)
}
/**
It may prove beneficial to strip all comments, which can start with # or ;.
In this case, strip both.
*/
private static func stripComment(_ line: String) -> String {
var line = line
let characters: [String.Element] = ["#", ";"]
for character in characters {
// Only keep checking for comments as long as the line isn't empty
if line.isEmpty {
return line
}
// Check for the next comment character
line = strip(character: character, line)
}
return line
}
/**
Empties a line if it happens to be commented out, causing it to be ignored.
*/
private static func strip(character: String.Element, _ line: String) -> String {
let parts = line.split(
separator: character,
maxSplits: 1,
omittingEmptySubsequences: false
)
if !parts.isEmpty {
return String(parts[0])
}
return ""
}
/**
Attempts to parse a section header. Requires the line to start with [ and end with ].
*/
private static func parseSectionHeader(_ line: String) -> String {
let from = line.index(after: line.startIndex)
let to = line.index(before: line.endIndex)
return line[from..<to]
}
/**
Attempts to parse a regular line, which may contain a configuration value that is being set.
*/
private static func parseLine(_ line: String) -> (String, String)? {
let parts = stripComment(line)
.split(separator: "=", maxSplits: 1)
if parts.count == 2 {
let k = trim(String(parts[0]))
let v = trim(String(parts[1]))
return (k, v)
}
return nil
}
}

View File

@ -89,24 +89,26 @@ class PhpExtension {
// MARK: - Static Methods
/**
This method will attempt to identify all extensions in the .ini file at a certain URL.
*/
static func load(from path: URL) -> [PhpExtension] {
let file = try? String(contentsOf: path, encoding: .utf8)
static func from(_ lines: [String], filePath: String) -> [PhpExtension] {
return lines.filter {
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
}.map {
return PhpExtension($0, file: filePath)
}
}
static func from(filePath: String) -> [PhpExtension] {
let file = try? String(contentsOfFile: filePath)
if file == nil {
Log.err("There was an issue reading the file. Assuming no extensions were found.")
return []
}
return file!.components(separatedBy: "\n")
.filter {
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
}
.map {
return PhpExtension($0, file: path.path)
}
return Self.from(
file!.components(separatedBy: "\n"),
filePath: filePath
)
}
}

View File

@ -23,17 +23,7 @@ class InternalSwitcher: PhpSwitcher {
func performSwitch(to version: String, completion: @escaping () -> Void) {
Log.info("Switching to \(version), unlinking all versions...")
let isolated = Valet.shared.sites.filter { site in
site.isolatedPhpVersion != nil
}.map { site in
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
}
var versions: Set<String> = [version]
if Valet.enabled(feature: .isolatedSites) {
versions = versions.union(isolated)
}
let versions = getVersionsToBeHandled(version)
let group = DispatchGroup()
@ -63,7 +53,28 @@ class InternalSwitcher: PhpSwitcher {
}
}
private func disableDefaultPhpFpmPool(_ version: String) {
func getVersionsToBeHandled(_ primary: String) -> Set<String> {
let isolated = Valet.shared.sites.filter { site in
site.isolatedPhpVersion != nil
}.map { site in
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
}
var versions: Set<String> = [primary]
if Valet.enabled(feature: .isolatedSites) {
versions = versions.union(isolated)
}
return versions
}
func requiresDisablingOfDefaultPhpFpmPool(_ version: String) -> Bool {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
return FileManager.default.fileExists(atPath: pool)
}
func disableDefaultPhpFpmPool(_ version: String) {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileManager.default.fileExists(atPath: pool) {
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
@ -83,14 +94,14 @@ class InternalSwitcher: PhpSwitcher {
}
}
private func stopPhpVersion(_ version: String) {
func stopPhpVersion(_ version: String) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)")
}
private func startPhpVersion(_ version: String, primary: Bool) {
func startPhpVersion(_ version: String, primary: Bool) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
if primary {

View File

@ -0,0 +1,15 @@
//
// CreatedFromFile.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol CreatedFromFile {
static func from(filePath: String) -> Self?
}

View File

@ -13,9 +13,9 @@
</head>
<body>
<br>
<p><b>Want to spread the love?</b> Leave a <a href="https://github.com/nicoverbruggen/phpmon">star on GitHub</a>!</p>
<p><b>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p>
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p>
<p><b>Do you enjoy using the app?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
<p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p>
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
<br>
</body>

View File

@ -51,14 +51,26 @@ class App {
var preferences: [PreferenceName: Bool]!
/** The window controller of the currently active preferences window. */
var preferencesWindowController: PrefsWC?
var preferencesWindowController: PreferencesWindowController?
/** The window controller of the currently active site list window. */
var domainListWindowController: DomainListWC?
var domainListWindowController: DomainListWindowController?
/** The window controller of the onboarding window. */
var onboardingWindowController: OnboardingWindowController?
/** The window controller of the warnings window. */
var warningsWindowController: WarningsWindowController?
/** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = []
/** The services manager, responsible for figuring out what services are active/inactive. */
var services = ServicesManager.shared
/** The warning manager, responsible for keeping track of warnings. */
var warnings = WarningManager.shared
/** Timer that will periodically reload info about the user's PHP installation. */
var timer: Timer?

View File

@ -65,7 +65,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
override init() {
logger.verbosity = .info
#if DEBUG
logger.verbosity = .performance
// logger.verbosity = .performance
#endif
if CommandLine.arguments.contains("--v") {
logger.verbosity = .performance

View File

@ -146,8 +146,9 @@ class AppUpdateChecker {
text: "updater.alerts.buttons.release_notes".localized,
action: { vc in
vc.close(with: .OK)
NSWorkspace.shared.open(
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version.version)\(devSuffix)")
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version.tagged)\(devSuffix)")
)
}
)
@ -161,14 +162,14 @@ class AppUpdateChecker {
private static func notifyAboutConnectionIssue() {
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.errors.cannot_check_for_update.title".localized,
subtitle: "updater.errors.cannot_check_for_update.subtitle".localized,
description: "updater.errors.cannot_check_for_update.description".localized(
title: "updater.alerts.cannot_check_for_update.title".localized,
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
description: "updater.alerts.cannot_check_for_update.description".localized(
App.version
)
)
.withTertiary(
text: "updater.errors.buttons.releases_on_github".localized,
text: "updater.alerts.buttons.releases_on_github".localized,
action: { _ in
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
}

View File

@ -66,6 +66,14 @@ class AppVersion {
return AppVersion.from("\(App.shortVersion)_\(App.bundleVersion)")!
}
var tagged: String {
if version.suffix(2) == ".0" && version.count > 3 {
return String(version.dropLast(2))
}
return version
}
var computerReadable: String {
return "\(version)_\(build ?? "0")"
}

View File

@ -324,36 +324,32 @@
<!--Window Controller-->
<scene sceneID="PQa-AT-b2a">
<objects>
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PrefsWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PreferencesWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="h4c-3b-nko">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="372" y="403" width="480" height="270"/>
<rect key="contentRect" x="372" y="403" width="550" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/>
<view key="contentView" id="2yL-50-11x">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<rect key="frame" x="0.0" y="0.0" width="550" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<toolbar key="toolbar" implicitIdentifier="611E3485-DC7F-46A0-8528-11CF9366370C" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconAndLabel" sizeMode="regular" id="fcq-wR-7iv">
<allowedToolbarItems/>
<defaultToolbarItems/>
</toolbar>
<connections>
<outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/>
</connections>
</window>
<connections>
<segue destination="AW2-rV-rbS" kind="relationship" relationship="window.shadowedContentViewController" id="3dX-9V-eA0"/>
<segue destination="PCI-2c-55Y" kind="relationship" relationship="window.shadowedContentViewController" id="egC-A4-am8"/>
</connections>
</windowController>
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="238"/>
</scene>
<!--Preferences-->
<scene sceneID="iyi-IS-7Ps">
<objects>
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<viewController title="Preferences" identifier="preferencesTemplateVC" storyboardIdentifier="preferencesTemplateVC" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="GenericPreferenceVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" wantsLayer="YES" id="Pf1-A5-3Xz">
<rect key="frame" x="0.0" y="0.0" width="550" height="498"/>
<autoresizingMask key="autoresizingMask"/>
@ -378,12 +374,32 @@
</viewController>
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="260" y="217"/>
<point key="canvasLocation" x="844" y="-153"/>
</scene>
<!--Tab View Controller-->
<scene sceneID="B5x-d3-c7D">
<objects>
<customObject id="pNW-tM-SQu" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<tabViewController tabStyle="toolbar" canPropagateSelectedChildViewControllerTitle="NO" id="PCI-2c-55Y" sceneMemberID="viewController">
<tabView key="tabView" type="noTabsNoBorder" id="l0U-9a-nM6">
<rect key="frame" x="0.0" y="0.0" width="508" height="300"/>
<autoresizingMask key="autoresizingMask"/>
<font key="font" metaFont="message"/>
<connections>
<outlet property="delegate" destination="PCI-2c-55Y" id="6gR-GR-cwq"/>
</connections>
</tabView>
<connections>
<outlet property="tabView" destination="l0U-9a-nM6" id="tfn-UN-1Aa"/>
</connections>
</tabViewController>
</objects>
<point key="canvasLocation" x="283" y="-252"/>
</scene>
<!--Window Controller-->
<scene sceneID="4XS-kY-YIS">
<objects>
<windowController storyboardIdentifier="domainListWindow" id="8Ec-9q-82s" customClass="DomainListWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<windowController storyboardIdentifier="domainListWindow" id="8Ec-9q-82s" customClass="DomainListWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" separatorStyle="line" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
@ -532,7 +548,7 @@ Gw
</connections>
</button>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j">
<rect key="frame" x="13" y="13" width="82" height="32"/>
<rect key="frame" x="13" y="13" width="81" height="32"/>
<buttonCell key="cell" type="push" title="Tertiary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="mzA-Uu-gyf">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -651,7 +667,7 @@ Gw
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
<rect key="frame" x="325" y="13" width="142" height="32"/>
<rect key="frame" x="326" y="13" width="141" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -732,7 +748,7 @@ Gw
</textFieldCell>
</textField>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
<rect key="frame" x="139" y="23" width="180" height="14"/>
<rect key="frame" x="140" y="23" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
@ -805,7 +821,7 @@ Gw
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj">
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="626" height="281"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/>
@ -1381,7 +1397,7 @@ Gw
</constraints>
<textFieldCell key="cell" selectable="YES" alignment="left" id="3i9-RG-Ift">
<font key="font" metaFont="smallSystem"/>
<mutableString key="title">[i18n] Links are used to directly serve projects. If you have a Laravel, Symfony, WordPress, etc. folder with code, you'll want to create a link and choose the folder where your code lives.If you are in need of a proxy, you can proxy e.g. a container to a particular domain name. This can be useful in combination with Docker, for example.</mutableString>
<string key="title">[i18n] Links are used to directly serve projects. If you have a Laravel, Symfony, WordPress, etc. folder with code, you'll want to create a link and choose the folder where your code lives.If you are in need of a proxy, you can proxy e.g. a container to a particular domain name. This can be useful in combination with Docker, for example.</string>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
@ -1399,6 +1415,7 @@ Gw
<constraint firstAttribute="bottom" secondItem="pYe-Qu-qnK" secondAttribute="bottom" constant="20" id="lPX-ZF-XZN"/>
<constraint firstAttribute="trailing" secondItem="fJK-Ke-IK3" secondAttribute="trailing" constant="20" symbolic="YES" id="spl-Bn-xtw"/>
<constraint firstAttribute="bottom" secondItem="FhN-AM-SkI" secondAttribute="bottom" constant="20" symbolic="YES" id="t5w-aL-tOa"/>
<constraint firstItem="pYe-Qu-qnK" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="FhN-AM-SkI" secondAttribute="trailing" constant="8" symbolic="YES" id="y7k-sl-xqe"/>
</constraints>
</visualEffectView>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cNh-Wc-ADk">

View File

@ -0,0 +1,45 @@
//
// EnvironmentCheck.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary.
Checks that require an app restart will always lead to an alert and app termination shortly after.
*/
struct EnvironmentCheck {
let command: () async -> Bool
let name: String
let titleText: String
let subtitleText: String
let descriptionText: String
let buttonText: String
let requiresAppRestart: Bool
init(
command: @escaping () async -> Bool,
name: String,
titleText: String,
subtitleText: String,
descriptionText: String = "",
buttonText: String = "OK",
requiresAppRestart: Bool = false
) {
self.command = command
self.name = name
self.titleText = titleText
self.subtitleText = subtitleText
self.descriptionText = descriptionText
self.buttonText = buttonText
self.requiresAppRestart = requiresAppRestart
}
public func succeeds() async -> Bool {
return await !self.command()
}
}

View File

@ -26,10 +26,10 @@ class InterApp {
DomainListVC.show()
}),
InterApp.Action(command: "services/stop", action: { _ in
MainMenu.shared.stopAllServices()
MainMenu.shared.stopValetServices()
}),
InterApp.Action(command: "services/restart/all", action: { _ in
MainMenu.shared.restartAllServices()
MainMenu.shared.restartValetServices()
}),
InterApp.Action(command: "services/restart/nginx", action: { _ in
MainMenu.shared.restartNginx()
@ -56,10 +56,14 @@ class InterApp {
if PhpEnv.shared.availablePhpVersions.contains(version) {
MainMenu.shared.switchToPhpVersion(version)
} else {
BetterAlert().withInformation(
title: "Unsupported version",
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
).withPrimary(text: "OK").show()
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "alert.php_switch_unavailable.title".localized,
subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
).withPrimary(
text: "alert.php_switch_unavailable.ok".localized
).show()
}
}
})
]}

View File

@ -0,0 +1,80 @@
//
// ServicesManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import SwiftUI
class ServicesManager: ObservableObject {
static var shared = ServicesManager()
@Published var rootServices: [String: HomebrewService] = [:]
@Published var userServices: [String: HomebrewService] = [:]
public static func loadHomebrewServices(completed: (() -> Void)? = nil) {
let rootServiceNames = [
PhpEnv.phpInstall.formula,
"nginx",
"dnsmasq"
]
DispatchQueue.global(qos: .background).async {
let data = Shell
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
.data(using: .utf8)!
let services = try! JSONDecoder()
.decode([HomebrewService].self, from: data)
.filter({ return rootServiceNames.contains($0.name) })
DispatchQueue.main.async {
ServicesManager.shared.rootServices = Dictionary(
uniqueKeysWithValues: services.map { ($0.name, $0) }
)
}
}
guard let userServiceNames = Preferences.custom.services else {
return
}
DispatchQueue.global(qos: .background).async {
let data = Shell
.pipe("\(Paths.brew) services info --all --json", requiresPath: true)
.data(using: .utf8)!
let services = try! JSONDecoder()
.decode([HomebrewService].self, from: data)
.filter({ return userServiceNames.contains($0.name) })
DispatchQueue.main.async {
ServicesManager.shared.userServices = Dictionary(
uniqueKeysWithValues: services.map { ($0.name, $0) }
)
completed?()
}
}
}
func loadData() {
Self.loadHomebrewServices()
}
/**
Dummy data for preview purposes.
*/
func withDummyServices(_ services: [String: Bool]) -> Self {
for (service, enabled) in services {
let item = HomebrewService.dummy(named: service, enabled: enabled)
self.rootServices[service] = item
}
return self
}
}

View File

@ -146,13 +146,15 @@ class Startup {
command: { return !Shell.pipe("cat /private/etc/sudoers.d/brew").contains(Paths.brew) },
name: "`/private/etc/sudoers.d/brew` contains brew",
titleText: "startup.errors.sudoers_brew.title".localized,
subtitleText: "startup.errors.sudoers_brew.subtitle".localized
subtitleText: "startup.errors.sudoers_brew.subtitle".localized,
descriptionText: "startup.errors.sudoers_brew.desc".localized
),
EnvironmentCheck(
command: { return !Shell.pipe("cat /private/etc/sudoers.d/valet").contains(Paths.valet) },
name: "`/private/etc/sudoers.d/valet` contains valet",
titleText: "startup.errors.sudoers_valet.title".localized,
subtitleText: "startup.errors.sudoers_valet.subtitle".localized
subtitleText: "startup.errors.sudoers_valet.subtitle".localized,
descriptionText: "startup.errors.sudoers_valet.desc".localized
),
// =================================================================================
// Verify if the Homebrew services are running (as root).
@ -165,6 +167,18 @@ class Startup {
descriptionText: "startup.errors.services_json_error.desc".localized
),
// =================================================================================
// Determine that Valet is installed
// =================================================================================
EnvironmentCheck(
command: {
return !Filesystem.directoryExists("~/.config/valet")
},
name: "`.config/valet` not empty (Valet installed)",
titleText: "startup.errors.valet_not_installed.title".localized,
subtitleText: "startup.errors.valet_not_installed.subtitle".localized,
descriptionText: "startup.errors.valet_not_installed.desc".localized
),
// =================================================================================
// Determine that the Valet configuration JSON file is valid.
// =================================================================================
EnvironmentCheck(
@ -182,11 +196,51 @@ class Startup {
descriptionText: "startup.errors.valet_json_invalid.desc".localized
),
// =================================================================================
// Check for `which` alias issue
// =================================================================================
EnvironmentCheck(
command: {
return App.architecture == "x86_64"
&& FileManager.default.fileExists(atPath: "/usr/local/bin/which")
&& Shell.pipe("which node", requiresPath: false)
.contains("env: node: No such file or directory")
},
name: "`env: node` issue does not apply",
titleText: "startup.errors.which_alias_issue.title".localized,
subtitleText: "startup.errors.which_alias_issue.subtitle".localized,
descriptionText: "startup.errors.which_alias_issue.desc".localized
),
// =================================================================================
// Determine that Valet works correctly (no issues in platform detected)
// =================================================================================
EnvironmentCheck(
command: {
return valet("--version", sudo: false)
.contains("Composer detected issues in your platform")
},
name: "`no global composer issues",
titleText: "startup.errors.global_composer_platform_issues.title".localized,
subtitleText: "startup.errors.global_composer_platform_issues.subtitle".localized,
descriptionText: "startup.errors.global_composer_platform_issues.desc".localized
),
// =================================================================================
// Determine the Valet version and ensure it isn't unknown.
// =================================================================================
EnvironmentCheck(
command: {
Valet.shared.version = VersionExtractor.from(valet("--version", sudo: false))
let output = valet("--version", sudo: false)
// Failure condition #1: does not contain Laravel Valet
if !output.contains("Laravel Valet") {
return true
}
// Failure condition #2: version cannot be parsed
let versionString = output
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "Laravel Valet")[1]
.trimmingCharacters(in: .whitespaces)
// Extract the version number
Valet.shared.version = VersionExtractor.from(output)
// Get the actual version
return Valet.shared.version == nil
},
name: "`valet --version` was loaded",
@ -195,42 +249,4 @@ class Startup {
descriptionText: "startup.errors.valet_version_unknown.desc".localized
)
]
// MARK: - EnvironmentCheck struct
/**
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary.
Checks that require an app restart will always lead to an alert and app termination shortly after.
*/
struct EnvironmentCheck {
let command: () async -> Bool
let name: String
let titleText: String
let subtitleText: String
let descriptionText: String
let buttonText: String
let requiresAppRestart: Bool
init(
command: @escaping () async -> Bool,
name: String,
titleText: String,
subtitleText: String,
descriptionText: String = "",
buttonText: String = "OK",
requiresAppRestart: Bool = false
) {
self.command = command
self.name = name
self.titleText = titleText
self.subtitleText = subtitleText
self.descriptionText = descriptionText
self.buttonText = buttonText
self.requiresAppRestart = requiresAppRestart
}
public func succeeds() async -> Bool {
return await !self.command()
}
}
}

View File

@ -55,7 +55,7 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
let path = pathControl.url!.path
let name = inputDomainName.stringValue
if !FileManager.default.fileExists(atPath: path) {
if !Filesystem.exists(path) {
Alert.confirm(
onWindow: view.window!,
messageText: "domain_list.alert.folder_missing.title".localized,

View File

@ -8,6 +8,7 @@
import Cocoa
import AppKit
import SwiftUI
class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
static let reusableName = "domainListPhpCell"
@ -20,10 +21,17 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
func populateCell(with site: ValetSite) {
self.site = site
buttonPhpVersion.isHidden = false
imageViewPhpVersionOK.isHidden = false
buttonPhpVersion.title = " PHP \(site.servingPhpVersion)"
imageViewPhpVersionOK.toolTip = nil
imageViewPhpVersionOK.contentTintColor = site.composerPhpCompatibleWithLinked
? NSColor(named: "IconColorGreen")
: NSColor(named: "IconColorRed")
if site.isolatedPhpVersion != nil {
imageViewPhpVersionOK.isHidden = false
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
@ -32,10 +40,8 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
imageViewPhpVersionOK.isHidden = (site.composerPhp == "???" || !site.composerPhpCompatibleWithLinked)
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp)
}
buttonPhpVersion.isHidden = false
imageViewPhpVersionOK.isHidden = false
}
}
func populateCell(with proxy: ValetProxy) {
@ -47,56 +53,25 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
@IBAction func pressedPhpVersion(_ sender: Any) {
guard let site = self.site else { return }
let alert = NSAlert.init()
alert.alertStyle = .informational
var validPhpSuggestions: [PhpVersionNumber] {
if site.isolatedPhpVersion != nil {
return []
}
var information = ""
if self.site?.isolatedPhpVersion != nil {
information += "alert.composer_php_isolated.desc".localized(
self.site!.isolatedPhpVersion!.versionNumber.homebrewVersion,
PhpEnv.phpInstall.version.short
)
information += "\n\n"
}
information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)"
.localized
alert.messageText = "alert.composer_php_requirement.title"
.localized("\(site.name).\(Valet.shared.config.tld)", site.composerPhp)
alert.informativeText = information
alert.addButton(withTitle: "site_link.close".localized)
var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue
var map: [Int: String] = [:]
if site.isolatedPhpVersion == nil {
// Determine which installed versions would be ideal to switch to,
// but make sure to exclude the currently linked version
PhpEnv.shared.validVersions(for: site.composerPhp).filter({ version in
return PhpEnv.shared.validVersions(for: site.composerPhp).filter({ version in
version.homebrewVersion != PhpEnv.phpInstall.version.short
}).forEach { version in
alert.addButton(withTitle: "site_link.switch_to_php".localized(version.homebrewVersion))
map[mapIndex] = version.homebrewVersion
mapIndex += 1
}
// Site is not isolated, show options to switch global PHP version
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) { response in
if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue {
if map.keys.contains(response.rawValue) {
let version = map[response.rawValue]!
Log.info("Pressed button to switch to \(version)")
MainMenu.shared.switchToPhpVersion(version)
}
}
}
} else {
// Site is isolated, do not show any options to switch
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!)
})
}
let button = self.buttonPhpVersion!
let popover = NSPopover()
let view = VersionPopoverView(site: site, validPhpVersions: validPhpSuggestions, parent: popover)
popover.contentViewController = NSHostingController(rootView: view)
popover.behavior = .transient
popover.animates = true
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)
}
}

View File

@ -69,7 +69,8 @@ extension DomainListVC {
.localized(
"\(selectedSite.name).\(Valet.shared.config.tld)",
newState
)
),
preference: .notifyAboutSecureToggle
)
}
@ -116,6 +117,7 @@ extension DomainListVC {
self.performAction(command: command) {
self.selectedSite!.determineIsolated()
self.selectedSite!.determineComposerPhpVersion()
if self.selectedSite!.isolatedPhpVersion == nil {
BetterAlert()
@ -133,6 +135,7 @@ extension DomainListVC {
@objc func removeIsolatedSite() {
self.performAction(command: "sudo \(Paths.valet) unisolate --site '\(self.selectedSite!.name)' && exit;") {
self.selectedSite!.isolatedPhpVersion = nil
self.selectedSite!.determineComposerPhpVersion()
}
}

View File

@ -42,14 +42,15 @@ extension DomainListVC {
addDisabledIsolation(to: menu)
}
addUnlink(to: menu, with: site)
menu.addItem(HeaderView.asMenuItem(text: "domain_list.actions".localized))
addToggleSecure(to: menu, secured: site.secured)
addUnlink(to: menu, with: site)
tableView.menu = menu
}
private func addSystemApps(to menu: NSMenu) {
menu.addItem(withTitle: "domain_list.system_apps".localized, action: nil, keyEquivalent: "")
menu.addItem(HeaderView.asMenuItem(text: "domain_list.system_apps".localized))
menu.addItem(
withTitle: "domain_list.open_in_finder".localized,
action: #selector(self.openInFinder),
@ -70,7 +71,7 @@ extension DomainListVC {
private func addDetectedApps(to menu: NSMenu) {
if !applications.isEmpty {
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: "domain_list.detected_apps".localized, action: nil, keyEquivalent: "")
menu.addItem(HeaderView.asMenuItem(text: "domain_list.detected_apps".localized))
for editor in applications {
let editorMenuItem = EditorMenuItem(
@ -96,38 +97,40 @@ extension DomainListVC {
}
private func addDisabledIsolation(to menu: NSMenu) {
menu.addItem(HeaderView.asMenuItem(text: "domain_list.site_isolation".localized))
menu.addItem(withTitle: "domain_list.isolation_unavailable".localized, action: nil, keyEquivalent: "")
menu.addItem(NSMenuItem.separator())
}
private func addIsolate(to menu: NSMenu, with site: ValetSite) {
if site.isolatedPhpVersion == nil {
// ISOLATION POSSIBLE
let isolationMenuItem = NSMenuItem(title: "domain_list.isolate".localized, action: nil, keyEquivalent: "")
let submenu = NSMenu()
submenu.addItem(withTitle: "Choose a PHP version", action: nil, keyEquivalent: "")
for version in PhpEnv.shared.availablePhpVersions.reversed() {
let item = PhpMenuItem(
title: "Always use PHP \(version)",
action: #selector(self.isolateSite),
keyEquivalent: ""
)
item.version = version
submenu.addItem(item)
}
menu.setSubmenu(submenu, for: isolationMenuItem)
var items: [NSMenuItem] = []
menu.addItem(isolationMenuItem)
menu.addItem(NSMenuItem.separator())
} else {
// REMOVE ISOLATION POSSIBLE
menu.addItem(
withTitle: "domain_list.remove_isolation".localized,
action: #selector(self.removeIsolatedSite),
for version in PhpEnv.shared.availablePhpVersions.reversed() {
let item = PhpMenuItem(
title: "domain_list.always_use_php".localized(version),
action: #selector(self.isolateSite),
keyEquivalent: ""
)
menu.addItem(NSMenuItem.separator())
if site.servingPhpVersion == version && site.isolatedPhpVersion != nil {
item.state = .on
item.action = nil
}
item.version = version
items.append(item)
}
// Add the option to remove site isolation
if site.isolatedPhpVersion != nil {
items.append(NSMenuItem.separator())
items.append(NSMenuItem(
title: "domain_list.remove_isolation".localized,
action: #selector(self.removeIsolatedSite)
))
}
menu.addItem(HeaderView.asMenuItem(text: "domain_list.site_isolation".localized))
menu.addItem(NSMenuItem(title: "domain_list.isolate".localized, submenu: items))
menu.addItem(NSMenuItem.separator())
}
private func addToggleSecure(to menu: NSMenu, secured: Bool) {

View File

@ -13,7 +13,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
// MARK: - Outlets
@IBOutlet weak var tableView: NSTableView!
@IBOutlet weak var tableView: PMTableView!
@IBOutlet weak var progressIndicator: NSProgressIndicator!
// MARK: - Variables
@ -64,17 +64,16 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
let windowController = storyboard.instantiateController(
withIdentifier: "domainListWindow"
) as! DomainListWC
) as! DomainListWindowController
windowController.window!.title = "domain_list.title".localized
windowController.window!.subtitle = "domain_list.subtitle".localized
windowController.window!.delegate = delegate
windowController.window!.styleMask = [
.titled, .closable, .resizable, .miniaturizable
]
windowController.window!.minSize = NSSize(width: 550, height: 200)
windowController.window!.delegate = windowController
windowController.window!.setFrameAutosaveName("domainListWindow")
guard let window = windowController.window else { return }
window.title = "domain_list.title".localized
window.subtitle = "domain_list.subtitle".localized
window.delegate = delegate ?? windowController
window.styleMask = [.titled, .closable, .resizable, .miniaturizable]
window.minSize = NSSize(width: 550, height: 200)
window.setFrameAutosaveName("domainListWindow")
App.shared.domainListWindowController = windowController
}

View File

@ -1,5 +1,5 @@
//
// DomainListWC.swift
// DomainListWindowController.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 03/12/2021.
@ -8,7 +8,7 @@
import Cocoa
class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
class DomainListWindowController: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
// MARK: - Window Identifier
@ -124,8 +124,6 @@ class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate
withIdentifier: "addProxyWindow"
) as! NSWindowController
// let viewController = windowController.window!.contentViewController as! AddSiteVC
self.window?.beginSheet(windowController.window!)
}
}

View File

@ -0,0 +1,27 @@
//
// PMTableView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 05/09/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
/**
This subclassed version of NSTableView selects a row upon right-clicking,
thus making the domain list behave more like you'd expect.
*/
public class PMTableView: NSTableView {
override open func menu(for event: NSEvent) -> NSMenu? {
let row = self.row(at: self.convert(event.locationInWindow, from: nil))
if row >= 0 {
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
return super.menu(for: event)
}
}

View File

@ -11,7 +11,7 @@ import Cocoa
class SelectionVC: NSViewController {
weak var domainListWC: DomainListWC?
weak var domainListWC: DomainListWindowController?
@IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var textFieldDescription: NSTextField!

View File

@ -13,7 +13,7 @@ class ComposerWindow {
private var menu: MainMenu?
private var shouldNotify: Bool! = nil
private var completion: ((Bool) -> Void)! = nil
private var window: ProgressWindowController?
private var window: TerminalProgressWindowController?
/**
Updates the global dependencies and runs the completion callback when done.
@ -25,7 +25,9 @@ class ComposerWindow {
Paths.shared.detectBinaryPaths()
if Paths.composer == nil {
presentMissingAlert()
DispatchQueue.main.async {
self.presentMissingAlert()
}
return
}
@ -33,7 +35,7 @@ class ComposerWindow {
menu?.setBusyImage()
menu?.rebuild()
window = ProgressWindowController.display(
window = TerminalProgressWindowController.display(
title: "alert.composer_progress.title".localized,
description: "alert.composer_progress.info".localized
)
@ -83,7 +85,8 @@ class ComposerWindow {
if shouldNotify {
LocalNotification.send(
title: "alert.composer_success.title".localized,
subtitle: "alert.composer_success.info".localized
subtitle: "alert.composer_success.info".localized,
preference: .notifyAboutGlobalComposerStatus
)
}
window = nil
@ -115,7 +118,7 @@ class ComposerWindow {
// MARK: Alert
private func presentMissingAlert() {
@MainActor private func presentMissingAlert() {
BetterAlert()
.withInformation(
title: "alert.composer_missing.title".localized,

View File

@ -38,8 +38,6 @@ struct PhpFrameworks {
"zendframework/zendframework": "Zend",
"zendframework/zend-mvc": "Zend",
"typo3/cms-core": "Typo3"
// TODO (6.0): Handle these in v6.0
// "magento/*": "Magento",
// "concrete5/*": "Concrete5",
// "contao/*": "Contao",
@ -73,7 +71,7 @@ struct PhpFrameworks {
public static func detectFallbackDependency(_ basePath: String) -> String? {
for entry in Self.FileMapping {
let found = entry.value
.map { path in return Filesystem.fileExists(basePath + path) }
.map { path in return Filesystem.exists(basePath + path) }
.contains(true)
if found {

View File

@ -42,6 +42,35 @@ class HomebrewDiagnostics {
}
}
/**
It is possible to upgrade PHP, but forget running `valet install`.
This results in a scenario where a rogue www.conf file exists.
*/
public static func checkForPhpFpmPoolConflicts() {
Log.info("Checking for PHP-FPM pool conflicts...")
// We'll need to know what the primary PHP version is
let primary = PhpEnv.shared.currentInstall.version.short
// Versions to be handled
let switcher = InternalSwitcher()
var versions = switcher.getVersionsToBeHandled(primary)
versions = versions.filter { version in
return switcher.requiresDisablingOfDefaultPhpFpmPool(version)
}
if versions.isEmpty {
Log.info("No PHP-FPM pools need to be fixed. All OK.")
}
versions.forEach { version in
switcher.disableDefaultPhpFpmPool(version)
switcher.stopPhpVersion(version)
switcher.startPhpVersion(version, primary: version == primary)
}
}
/**
Check if the alias conflict as documented in `checkForCaskConflict` actually occurred.
*/

View File

@ -1,5 +1,5 @@
//
// NginxConfiguration.swift
// NginxConfigurationFile.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/03/2022.
@ -8,36 +8,32 @@
import Foundation
class NginxConfiguration {
class NginxConfigurationFile: CreatedFromFile {
/** Contents of the Nginx file in question, as a string. */
/// Contents of the Nginx file in question, as a string.
var contents: String!
/** The name of the domain, usually derived from the name of the file. */
/// The name of the domain, usually derived from the name of the file.
var domain: String
/** The TLD of the domain, usually derived from the name of the file. */
/// The TLD of the domain, usually derived from the name of the file.
var tld: String
static func from(filePath: String) -> NginxConfiguration? {
let path = filePath.replacingOccurrences(
of: "~",
with: "/Users/\(Paths.whoami)"
)
/** Resolves an nginx configuration file (.conf) */
static func from(filePath: String) -> Self? {
let path = filePath.replacingOccurrences(of: "~", with: Paths.homePath)
do {
let fileContents = try String(contentsOfFile: path)
return NginxConfiguration.init(
path: path,
contents: fileContents
)
return Self.init(path: path, contents: fileContents)
} catch {
Log.warn("Could not read the nginx configuration file at: `\(filePath)`")
return nil
}
}
init(path: String, contents: String) {
required init(path: String, contents: String) {
let domain = String(path.split(separator: "/").last!)
let tld = String(domain.split(separator: ".").last!)
@ -46,9 +42,7 @@ class NginxConfiguration {
self.tld = tld
}
/**
Retrieves what address this domain is proxying.
*/
/** Retrieves what address this domain is proxying. */
lazy var proxy: String? = {
let regex = try! NSRegularExpression(
pattern: #"proxy_pass (?<proxy>.*:\d*)(\/*);"#,
@ -61,9 +55,7 @@ class NginxConfiguration {
return contents[Range(match.range(withName: "proxy"), in: contents)!]
}()
/**
Retrieves which isolated version is active for this domain (if applicable).
*/
/** Retrieves which isolated version is active for this domain (if applicable). */
lazy var isolatedVersion: String? = {
let regex = try! NSRegularExpression(
// PHP versions have (so far) never needed multiple digits for version numbers

View File

@ -13,8 +13,11 @@ class ValetProxyScanner: ProxyScanner {
return try! FileManager
.default
.contentsOfDirectory(atPath: directoryPath)
.filter {
return !$0.starts(with: ".")
}
.compactMap {
return NginxConfiguration.from(filePath: "\(directoryPath)/\($0)")
return NginxConfigurationFile.from(filePath: "\(directoryPath)/\($0)")
}
.filter {
return $0.proxy != nil

View File

@ -14,7 +14,7 @@ class ValetProxy: DomainListable {
var target: String
var secured: Bool = false
init(_ configuration: NginxConfiguration) {
init(_ configuration: NginxConfigurationFile) {
self.domain = configuration.domain
self.tld = configuration.tld
self.target = configuration.proxy!

View File

@ -23,22 +23,26 @@ extension ValetSite {
self.init(name: name, tld: tld, absolutePath: path, aliasPath: nil, makeDeterminations: false)
self.secured = secure
self.composerPhp = constraint
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
.map { string in
return !PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.isEmpty
}.contains(true)
self.composerPhpSource = constraint != "" ? .require : .unknown
self.driver = driver
self.driverDeterminedByComposer = true
if linked {
self.aliasPath = self.absolutePath
}
if let isolated = isolated {
self.isolatedPhpVersion = PhpInstallation(isolated)
}
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
.map { string in
let origin = self.isolatedPhpVersion?.versionNumber.homebrewVersion ?? PhpEnv.phpInstall.version.long
return !PhpVersionNumberCollection.make(from: [origin])
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.isEmpty
}.contains(true)
}
}

View File

@ -20,7 +20,7 @@ class ValetSite: DomainListable {
/// replacing the user's home folder with ~.
lazy var absolutePathRelative: String = {
return self.absolutePath
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
.replacingOccurrences(of: Paths.homePath, with: "~")
}()
/// The TLD used to locate this site.
@ -81,9 +81,9 @@ class ValetSite: DomainListable {
if makeDeterminations {
determineSecured()
determineIsolated()
determineComposerPhpVersion()
determineDriver()
determineIsolated()
}
}
@ -133,7 +133,6 @@ class ValetSite: DomainListable {
with the currently linked version of PHP (see `composerPhpMatchesSystem`).
*/
public func determineComposerPhpVersion() {
self.determineComposerInformation()
self.determineValetPhpFileInfo()
@ -145,7 +144,8 @@ class ValetSite: DomainListable {
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
.map { string in
return !PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
let origin = self.isolatedPhpVersion?.versionNumber.homebrewVersion ?? PhpEnv.phpInstall.version.long
return !PhpVersionNumberCollection.make(from: [origin])
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.isEmpty
}.contains(true)
@ -225,7 +225,7 @@ class ValetSite: DomainListable {
public static func isolatedVersion(_ filePath: String) -> String? {
if Filesystem.fileExists(filePath) {
return NginxConfiguration
return NginxConfigurationFile
.from(filePath: filePath)?
.isolatedVersion ?? nil
}

View File

@ -113,20 +113,11 @@ class Valet {
}
/**
Starts the preload of sites, but only if the maximum amount of sites is 30.
For users with more sites, the site list is loaded when they bring up the site list window.
(This is done to keep the startup speed as fast as possible.)
Starts the preload of sites. In order to make sure PHP Monitor can correctly
handle all PHP versions including isolation, it needs to know about all sites.
*/
public func startPreloadingSites() {
let maximumPreload = 50
let foundSites = self.countPaths()
if foundSites <= maximumPreload {
// Preload the sites and their drivers
Log.info("Fewer than or \(maximumPreload) sites found, preloading list of sites...")
self.reloadSites()
} else {
Log.info("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!")
}
self.reloadSites()
}
/**
@ -192,6 +183,11 @@ class Valet {
}
}
public func hasPlatformIssues() -> Bool {
return valet("--version", sudo: false)
.contains("Composer detected issues in your platform")
}
/**
Returns a count of how many sites are linked and parked.
*/
@ -225,6 +221,8 @@ class Valet {
sites.insert(site, at: 0)
}
Log.info("\(sites.count) sites & \(proxies.count) proxies have been scanned.")
isBusy = false
}

View File

@ -1,25 +0,0 @@
//
// HeaderView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class HeaderView: NSView, XibLoadable {
@IBOutlet weak var textField: NSTextField!
static func asMenuItem(text: String) -> NSMenuItem {
let view = Self.createFromXib()
view!.textField.stringValue = text.uppercased()
let item = NSMenuItem()
item.view = view
item.target = self
return item
}
}

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="HeaderView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="270" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ddg-VQ-cOT">
<rect key="frame" x="12" y="5" width="113" height="15"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="left" title="ACTIVE SERVICES" id="NHz-MZ-8FK">
<font key="font" metaFont="systemBold" size="12"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="ddg-VQ-cOT" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="n4Z-WN-RIh"/>
<constraint firstItem="ddg-VQ-cOT" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="14" id="yuW-pb-GQJ"/>
</constraints>
<connections>
<outlet property="textField" destination="ddg-VQ-cOT" id="aaQ-Xb-o2X"/>
</connections>
<point key="canvasLocation" x="177" y="105"/>
</customView>
</objects>
</document>

View File

@ -0,0 +1,291 @@
//
// MainMenu+Actions.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 19/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension MainMenu {
// MARK: - Actions
@objc func fixHomebrewPermissions() {
if !BetterAlert()
.withInformation(
title: "alert.fix_homebrew_permissions.title".localized,
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
description: "alert.fix_homebrew_permissions.desc".localized
)
.withPrimary(text: "alert.fix_homebrew_permissions.ok".localized)
.withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized)
.didSelectPrimary() {
return
}
asyncExecution {
try Actions.fixHomebrewPermissions()
} success: {
BetterAlert()
.withInformation(
title: "alert.fix_homebrew_permissions_done.title".localized,
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
description: "alert.fix_homebrew_permissions_done.desc".localized
)
.withPrimary(text: "OK")
.show()
} failure: { error in
BetterAlert.show(for: error as! HomebrewPermissionError)
}
}
@objc func restartPhpFpm() {
asyncExecution {
Actions.restartPhpFpm()
}
}
@objc func restartValetServices() {
asyncExecution {
Actions.restartDnsMasq()
Actions.restartPhpFpm()
Actions.restartNginx()
} success: {
LocalNotification.send(
title: "notification.services_restarted".localized,
subtitle: "notification.services_restarted_desc".localized,
preference: .notifyAboutServices
)
}
}
@objc func stopValetServices() {
asyncExecution {
Actions.stopValetServices()
} success: {
LocalNotification.send(
title: "notification.services_stopped".localized,
subtitle: "notification.services_stopped_desc".localized,
preference: .notifyAboutServices
)
}
}
@objc func restartNginx() {
asyncExecution {
Actions.restartNginx()
}
}
@objc func restartDnsMasq() {
asyncExecution {
Actions.restartDnsMasq()
}
}
@objc func disableAllXdebugModes() {
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
Log.info("xdebug.mode could not be found in any .ini file, aborting.")
return
}
do {
try file.replace(key: "xdebug.mode", value: "off")
Log.perf("Refreshing menu...")
MainMenu.shared.rebuild()
restartPhpFpm()
} catch {
Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)")
}
}
@objc func toggleXdebugMode(sender: XdebugMenuItem) {
Log.info("Switching Xdebug to mode: \(sender.mode)")
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
return Log.info("xdebug.mode could not be found in any .ini file, aborting.")
}
do {
var modes = Xdebug.activeModes
if let index = modes.firstIndex(of: sender.mode) {
modes.remove(at: index)
} else {
modes.append(sender.mode)
}
var newValue = modes.joined(separator: ",")
if newValue.isEmpty {
newValue = "off"
}
try file.replace(key: "xdebug.mode", value: newValue)
Log.perf("Refreshing menu...")
MainMenu.shared.rebuild()
restartPhpFpm()
} catch {
Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)")
}
}
@objc func toggleExtension(sender: ExtensionMenuItem) {
asyncExecution {
sender.phpExtension?.toggle()
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
Actions.restartPhpFpm()
}
}
}
private func performRollback() {
asyncExecution {
PresetHelper.rollbackPreset?.apply()
PresetHelper.rollbackPreset = nil
MainMenu.shared.rebuild()
}
}
@MainActor @objc func rollbackPreset() {
guard let preset = PresetHelper.rollbackPreset else {
return
}
BetterAlert().withInformation(
title: "alert.revert_description.title".localized,
subtitle: "alert.revert_description.subtitle".localized(
preset.textDescription
)
)
.withPrimary(text: "alert.revert_description.ok".localized, action: { alert in
alert.close(with: .OK)
self.performRollback()
})
.withSecondary(text: "alert.revert_description.cancel".localized)
.show()
}
@objc func togglePreset(sender: PresetMenuItem) {
asyncExecution {
sender.preset?.apply()
}
}
@MainActor @objc func showPresetHelp() {
BetterAlert().withInformation(
title: "preset_help_title".localized,
subtitle: "preset_help_info".localized,
description: "preset_help_desc".localized
)
.withPrimary(text: "OK")
.withTertiary(text: "", action: { alert in
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
alert.close(with: .OK)
})
.show()
}
@objc func openPhpInfo() {
var url: URL?
asyncWithBusyUI {
url = Actions.createTempPhpInfoFile()
} completion: {
if url != nil { NSWorkspace.shared.open(url!) }
}
}
@objc func updateGlobalComposerDependencies() {
ComposerWindow().updateGlobalDependencies(
notify: true,
completion: { _ in }
)
}
@objc func openActiveConfigFolder() {
if PhpEnv.phpInstall.version.error {
Actions.openGenericPhpConfigFolder()
return
}
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
}
@objc func openPhpMonitorConfigurationFile() {
Actions.openPhpMonitorConfigFile()
}
@objc func openGlobalComposerFolder() {
Actions.openGlobalComposerFolder()
}
@objc func openValetConfigFolder() {
Actions.openValetConfigFolder()
}
@objc func switchToPhpVersion(sender: PhpMenuItem) {
self.switchToPhpVersion(sender.version)
}
@objc func switchToPhpVersion(_ version: String) {
setBusyImage()
PhpEnv.shared.isBusy = true
PhpEnv.shared.delegate = self
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
updatePhpVersionInStatusBar()
rebuild()
PhpEnv.switcher.performSwitch(
to: version,
completion: {
PhpEnv.shared.currentInstall = ActivePhpInstallation()
App.shared.handlePhpConfigWatcher()
PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version)
}
)
}
}
// MARK: - Async
/**
This async-friendly version of the switcher can be invoked elsewhere in the app:
```
Task {
await MainMenu.shared.switchToPhp("8.1")
// thing to do after the switch
}
```
Since this async function uses `withCheckedContinuation`
any code after will run only after the switcher is done.
*/
func switchToPhp(_ version: String) async {
DispatchQueue.main.async { [self] in
setBusyImage()
PhpEnv.shared.isBusy = true
PhpEnv.shared.delegate = self
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
}
return await withCheckedContinuation({ continuation in
updatePhpVersionInStatusBar()
rebuild()
PhpEnv.switcher.performSwitch(
to: version,
completion: {
PhpEnv.shared.currentInstall = ActivePhpInstallation()
App.shared.handlePhpConfigWatcher()
PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version)
continuation.resume()
}
)
})
}
}

View File

@ -36,8 +36,8 @@ extension MainMenu {
*/
func asyncExecution(
_ execute: @escaping () throws -> Void,
success: @escaping () -> Void = {},
failure: @escaping (Error) -> Void = { _ in },
success: @MainActor @escaping () -> Void = {},
failure: @MainActor @escaping (Error) -> Void = { _ in },
behaviours: [AsyncBehaviour] = [
.setsBusyUI,
.reloadsPhpInstallation,
@ -74,10 +74,14 @@ extension MainMenu {
}
if behaviours.contains(.broadcastServicesUpdate) {
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
ServicesManager.shared.loadData()
}
error == nil ? success() : failure(error!)
if error != nil {
return failure(error!)
}
success()
}
}
}

View File

@ -11,7 +11,7 @@ import AppKit
extension MainMenu {
@objc func fixMyValet() {
@MainActor @objc func fixMyValet() {
let previousVersion = PhpEnv.phpInstall.version.short
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
@ -42,7 +42,7 @@ extension MainMenu {
}
}
private func presentAlertForMissingFormula() {
@MainActor private func presentAlertForMissingFormula() {
BetterAlert()
.withInformation(
title: "alert.php_formula_missing.title".localized,
@ -52,7 +52,7 @@ extension MainMenu {
.show()
}
private func presentAlertForSameVersion() {
@MainActor private func presentAlertForSameVersion() {
BetterAlert()
.withInformation(
title: "alert.fix_my_valet_done.title".localized,
@ -63,7 +63,7 @@ extension MainMenu {
.show()
}
private func presentAlertForDifferentVersion(version: String) {
@MainActor private func presentAlertForDifferentVersion(version: String) {
BetterAlert()
.withInformation(
title: "alert.fix_my_valet_done.title".localized,

View File

@ -49,35 +49,26 @@ extension MainMenu {
// Check for an alias conflict
HomebrewDiagnostics.checkForCaskConflict()
// Update the icon
updatePhpVersionInStatusBar()
Log.info("Determining broken PHP-FPM...")
// Attempt to find out if PHP-FPM is broken
Log.info("Determining broken PHP-FPM...")
let installation = PhpEnv.phpInstall
installation.notifyAboutBrokenPhpFpm()
// Set up the config watchers on launch
// (these are automatically updated via delegate methods if the user switches)
// Check for other problems
WarningManager.shared.evaluateWarnings()
// Set up the config watchers on launch (updated automatically when switching)
Log.info("Setting up watchers...")
App.shared.handlePhpConfigWatcher()
// Detect applications (preset + custom)
Log.info("Detecting applications...")
App.shared.detectedApplications = Application.detectPresetApplications()
// Detect built-in and custom applications
detectApplications()
let customApps = Preferences.custom.scanApps.map { appName in
return Application(appName, .user_supplied)
}.filter { app in
return app.isInstalled()
}
App.shared.detectedApplications.append(contentsOf: customApps)
let appNames = App.shared.detectedApplications.map { app in
return app.name
}
Log.info("Detected applications: \(appNames)")
// Load the rollback preset
PresetHelper.loadRollbackPresetFromFile()
// Load the global hotkey
App.shared.loadGlobalHotkey()
@ -85,29 +76,36 @@ extension MainMenu {
// Preload sites
Valet.shared.startPreloadingSites()
// After preloading sites, check for PHP-FPM pool conflicts
HomebrewDiagnostics.checkForPhpFpmPoolConflicts()
// A non-default TLD is not officially supported since Valet 3.2.x
Valet.notifyAboutUnsupportedTLD()
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
// Find out which services are active
ServicesManager.shared.loadData()
// Schedule a request to fetch the PHP version every 60 seconds
DispatchQueue.main.async { [self] in
App.shared.timer = Timer.scheduledTimer(
timeInterval: 60,
target: self,
selector: #selector(refreshActiveInstallation),
userInfo: nil,
repeats: true
)
}
// Start the background refresh timer
startSharedTimer()
// Update the stats
Stats.incrementSuccessfulLaunchCount()
Stats.evaluateSponsorMessageShouldBeDisplayed()
// Present first launch screen if needed
if Stats.successfulLaunchCount == 0 && !isRunningSwiftUIPreview {
Log.info("Should present the first launch screen!")
DispatchQueue.main.async {
OnboardingWindowController.show()
}
}
// Check for updates
DispatchQueue.global(qos: .utility).async {
AppUpdateChecker.checkIfNewerVersionIsAvailable()
}
// We are ready!
Log.info("PHP Monitor is ready to serve!")
}
@ -133,4 +131,42 @@ extension MainMenu {
Task { await startup() }
}
}
/**
Schedule a request to fetch the PHP version every 60 seconds.
*/
private func startSharedTimer() {
DispatchQueue.main.async { [self] in
App.shared.timer = Timer.scheduledTimer(
timeInterval: 60,
target: self,
selector: #selector(refreshActiveInstallation),
userInfo: nil,
repeats: true
)
}
}
/**
Detect which applications are installed that can be used to open a domain's source directory.
*/
private func detectApplications() {
Log.info("Detecting applications...")
App.shared.detectedApplications = Application.detectPresetApplications()
let customApps = Preferences.custom.scanApps?.map { appName in
return Application(appName, .user_supplied)
}.filter { app in
return app.isInstalled()
} ?? []
App.shared.detectedApplications.append(contentsOf: customApps)
let appNames = App.shared.detectedApplications.map { app in
return app.name
}
Log.info("Detected applications: \(appNames)")
}
}

View File

@ -15,12 +15,6 @@ extension MainMenu {
func switcherDidStartSwitching(to version: String) {}
func switcherDidCompleteSwitch(to version: String) {
// Update the PHP version
PhpEnv.shared.currentInstall = ActivePhpInstallation()
// Ensure the config watcher gets reloaded
App.shared.handlePhpConfigWatcher()
// Mark as no longer busy
PhpEnv.shared.isBusy = false
@ -45,22 +39,32 @@ extension MainMenu {
self.notifyAboutVersionChange(to: version)
}
)
} else {
self.notifyAboutVersionChange(to: version)
}
// Check if Valet still works correctly
self.checkForPlatformIssues()
// Update stats
Stats.incrementSuccessfulSwitchCount()
Stats.evaluateSponsorMessageShouldBeDisplayed()
}
}
@MainActor private func checkForPlatformIssues() {
if Valet.shared.hasPlatformIssues() {
Log.info("Composer platform issue(s) detected.")
self.suggestFixMyComposer()
}
}
@MainActor private func suggestFixMyValet(failed version: String) {
let outcome = BetterAlert()
.withInformation(
title: "alert.php_switch_failed.title".localized(version),
subtitle: "alert.php_switch_failed.info".localized(version)
subtitle: "alert.php_switch_failed.info".localized(version),
description: "alert.php_switch_failed.desc".localized()
)
.withPrimary(text: "alert.php_switch_failed.confirm".localized)
.withSecondary(text: "alert.php_switch_failed.cancel".localized)
@ -70,6 +74,32 @@ extension MainMenu {
}
}
@MainActor private func suggestFixMyComposer() {
BetterAlert().withInformation(
title: "alert.global_composer_platform_issues.title".localized,
subtitle: "alert.global_composer_platform_issues.subtitle".localized,
description: "alert.global_composer_platform_issues.desc".localized
)
.withPrimary(text: "alert.global_composer_platform_issues.buttons.update".localized, action: { alert in
alert.close(with: .OK)
Log.info("The user has chosen to update global dependencies.")
ComposerWindow().updateGlobalDependencies(
notify: true,
completion: { success in
Log.info("Dependencies updated successfully: \(success)")
Log.info("Re-checking for platform issue(s)...")
self.checkForPlatformIssues()
}
)
})
.withSecondary(text: "", action: nil)
.withTertiary(text: "alert.global_composer_platform_issues.buttons.quit".localized, action: { alert in
alert.close(with: .OK)
self.terminateApp()
})
.show()
}
private func reloadDomainListData() {
if let window = App.shared.domainListWindowController {
DispatchQueue.main.async {
@ -81,11 +111,14 @@ extension MainMenu {
}
private func notifyAboutVersionChange(to version: String) {
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, version),
subtitle: String(format: "notification.version_changed_desc".localized, version)
)
DispatchQueue.main.async {
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, version),
subtitle: String(format: "notification.version_changed_desc".localized, version),
preference: .notifyAboutVersionChange
)
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
}
}
}

View File

@ -11,12 +11,17 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
static let shared = MainMenu()
override init() {
super.init()
statusItem.isVisible = !isRunningSwiftUIPreview
}
weak var menuDelegate: NSMenuDelegate?
/**
The status bar item with variable length.
*/
let statusItem = NSStatusBar.system.statusItem(
@MainActor let statusItem = NSStatusBar.system.statusItem(
withLength: NSStatusItem.variableLength
)
@ -45,33 +50,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
Use `rebuild(async:)` to ensure the rebuilding happens in the background.
*/
private func rebuildMenu() {
// Create a new menu
let menu = StatusMenu()
// Add the PHP versions (or error messages)
menu.addPhpVersionMenuItems()
menu.addItem(NSMenuItem.separator())
// Add the possible actions
menu.addPhpActionMenuItems()
menu.addItem(NSMenuItem.separator())
// Add Valet interactions
menu.addValetMenuItems()
menu.addItem(NSMenuItem.separator())
// Add services
menu.addRemainingMenuItems()
menu.addItem(NSMenuItem.separator())
// Add about & quit menu items
menu.addCoreMenuItems()
// Make sure every item can be interacted with
menu.addMenuItems()
menu.items.forEach({ (item) in
item.target = self
})
statusItem.menu = menu
statusItem.menu?.delegate = self
}
@ -79,7 +62,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
/**
Sets the status bar image based on a version string.
*/
func setStatusBarImage(version: String) {
@MainActor func setStatusBarImage(version: String) {
setStatusBar(
image: (Preferences.preferences[.iconTypeToDisplay] as! String != MenuBarIcon.noIcon.rawValue)
? MenuBarImageGenerator.textToImageWithIcon(text: version)
@ -91,7 +74,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
Sets the status bar image, based on the provided NSImage.
The image will be used as a template image.
*/
func setStatusBar(image: NSImage) {
@MainActor func setStatusBar(image: NSImage) {
if let button = statusItem.button {
image.isTemplate = true
button.image = image
@ -124,7 +107,17 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
refreshActiveInstallation()
refreshIcon()
rebuild(async: false)
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
ServicesManager.shared.loadData()
}
/**
Shows the Welcome Tour screen, again.
Did this need a comment? No, probably not.
*/
@objc func showWelcomeTour() {
DispatchQueue.main.async {
OnboardingWindowController.show()
}
}
/** Reloads the menu in the background, using `asyncExecution`. */
@ -165,155 +158,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
}
}
// MARK: - Actions
@objc func fixHomebrewPermissions() {
if !BetterAlert()
.withInformation(
title: "alert.fix_homebrew_permissions.title".localized,
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
description: "alert.fix_homebrew_permissions.desc".localized
)
.withPrimary(text: "alert.fix_homebrew_permissions.ok".localized)
.withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized)
.didSelectPrimary() {
return
}
asyncExecution {
try Actions.fixHomebrewPermissions()
} success: {
BetterAlert()
.withInformation(
title: "alert.fix_homebrew_permissions_done.title".localized,
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
description: "alert.fix_homebrew_permissions_done.desc".localized
)
.withPrimary(text: "OK")
.show()
} failure: { error in
BetterAlert.show(for: error as! HomebrewPermissionError)
}
}
@objc func restartPhpFpm() {
asyncExecution {
Actions.restartPhpFpm()
}
}
@objc func restartAllServices() {
asyncExecution {
Actions.restartDnsMasq()
Actions.restartPhpFpm()
Actions.restartNginx()
} success: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_restarted".localized,
subtitle: "notification.services_restarted_desc".localized
)
}
}
}
@objc func stopAllServices() {
asyncExecution {
Actions.stopAllServices()
} success: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_stopped".localized,
subtitle: "notification.services_stopped_desc".localized
)
}
}
}
@objc func restartNginx() {
asyncExecution {
Actions.restartNginx()
}
}
@objc func restartDnsMasq() {
asyncExecution {
Actions.restartDnsMasq()
}
}
@objc func toggleXdebugMode(sender: XdebugMenuItem) {
Log.info("Switching Xdebug to mode: \(sender.mode)")
}
@objc func toggleExtension(sender: ExtensionMenuItem) {
asyncExecution {
sender.phpExtension?.toggle()
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
Actions.restartPhpFpm()
}
}
}
@objc func openPhpInfo() {
var url: URL?
asyncWithBusyUI {
url = Actions.createTempPhpInfoFile()
} completion: {
if url != nil { NSWorkspace.shared.open(url!) }
}
}
@objc func updateGlobalComposerDependencies() {
ComposerWindow().updateGlobalDependencies(
notify: true,
completion: { _ in }
)
}
@objc func openActiveConfigFolder() {
if PhpEnv.phpInstall.version.error {
// php version was not identified
Actions.openGenericPhpConfigFolder()
return
}
// php version was identified
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
}
@objc func openGlobalComposerFolder() {
Actions.openGlobalComposerFolder()
}
@objc func openValetConfigFolder() {
Actions.openValetConfigFolder()
}
@objc func switchToPhpVersion(sender: PhpMenuItem) {
self.switchToPhpVersion(sender.version)
}
@objc func switchToPhpVersion(_ version: String) {
setBusyImage()
PhpEnv.shared.isBusy = true
PhpEnv.shared.delegate = self
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
updatePhpVersionInStatusBar()
rebuild()
PhpEnv.switcher.performSwitch(
to: version,
completion: {
PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version)
}
)
}
}
// MARK: - Menu Item Functionality
@objc func openAbout() {
@ -322,7 +166,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
}
@objc func openPrefs() {
PrefsVC.show()
PreferencesWindowController.show()
}
@objc func openWarnings() {
WarningsWindowController.show()
}
@objc func openDomainList() {
@ -348,7 +196,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
func menuWillOpen(_ menu: NSMenu) {
// Make sure the shortcut key does not trigger this when the menu is open
App.shared.shortcutHotkey?.isPaused = true
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
ServicesManager.shared.loadData()
}
func menuDidClose(_ menu: NSMenu) {

View File

@ -1,93 +0,0 @@
//
// StatsView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
/**
The ServicesView is an example of a view that I consider to be "poorly" set up.
Why ship it like this, then? Well, it works that's reason number one, really.
However, I do believe this should be refactored at some point. Here's why:
this view is responsible for retaining the information about the services status.
The status of the services should live somewhere else, and the fetching of said
service information should also not happen in a view. Yet here we are.
*/
class ServicesView: NSView, XibLoadable {
@IBOutlet weak var imageViewPhp: NSImageView!
@IBOutlet weak var imageViewNginx: NSImageView!
@IBOutlet weak var imageViewDnsmasq: NSImageView!
@IBOutlet weak var textFieldPhp: NSTextField!
static var services: [String: HomebrewService] = [:]
static func asMenuItem() -> NSMenuItem {
let view = Self.createFromXib()!
[view.imageViewPhp, view.imageViewNginx, view.imageViewDnsmasq].forEach { imageView in
imageView?.contentTintColor = NSColor(named: "IconColorNormal")
}
let item = NSMenuItem()
item.view = view
item.target = self
NotificationCenter.default.addObserver(
view, selector: #selector(self.updateInformation),
name: Events.ServicesUpdated,
object: nil
)
return item
}
@objc func updateInformation() {
self.loadData()
}
func loadData() {
self.applyAllInfoFieldsFromCachedValue()
HomebrewService.loadAll { services in
ServicesView.services = Dictionary(uniqueKeysWithValues: services.map { ($0.name, $0) })
self.applyAllInfoFieldsFromCachedValue()
}
}
func applyAllInfoFieldsFromCachedValue() {
if ServicesView.services.keys.isEmpty {
return
}
DispatchQueue.main.async {
self.textFieldPhp.stringValue = PhpEnv.phpInstall.formula.uppercased()
self.applyServiceStyling(PhpEnv.phpInstall.formula, self.imageViewPhp)
self.applyServiceStyling("nginx", self.imageViewNginx)
self.applyServiceStyling("dnsmasq", self.imageViewDnsmasq)
}
}
func applyServiceStyling(_ serviceName: String, _ imageView: NSImageView) {
if ServicesView.services[serviceName] == nil {
imageView.image = NSImage(named: "ServiceLoading")
imageView.contentTintColor = NSColor(named: "IconColorNormal")
return
}
if ServicesView.services[serviceName]!.running {
imageView.image = NSImage(named: "ServiceOn")
imageView.contentTintColor = NSColor(named: "IconColorNormal")
return
}
imageView.image = NSImage(named: "ServiceOff")
imageView.contentTintColor = NSColor(named: "IconColorRed")
}
deinit {
NotificationCenter.default.removeObserver(self, name: Events.ServicesUpdated, object: nil)
}
}

View File

@ -1,150 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView wantsLayer="YES" id="c22-O7-iKe" customClass="ServicesView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="330" height="46"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
<rect key="frame" x="30" y="3" width="270" height="40"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
<rect key="frame" x="0.0" y="4" width="77" height="32"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="At1-ch-qv2">
<rect key="frame" x="23" y="18" width="31" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="PHP" id="LKe-C4-jxo">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="tko-cP-XSz">
<rect key="frame" x="26" y="0.0" width="24" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="Fxu-6h-A2h"/>
<constraint firstAttribute="width" constant="24" id="hOc-Ur-dmA"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="vjB-6Z-3xR"/>
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
</imageView>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fillEqually" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g4d-4N-NkC">
<rect key="frame" x="97" y="4" width="76" height="32"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7um-XA-djV">
<rect key="frame" x="18" y="18" width="40" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="NGINX" id="Qfq-Bl-yuh">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ZqW-6d-vpe">
<rect key="frame" x="30" y="0.0" width="16" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="EPG-jm-7Xs"/>
<constraint firstAttribute="width" constant="16" id="iif-kT-phn"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="JmQ-dU-ip7"/>
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
</imageView>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
<rect key="frame" x="193" y="4" width="77" height="32"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
<rect key="frame" x="8" y="18" width="62" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="DNSMASQ" id="lGh-MT-TgI">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="DcG-x3-lvy">
<rect key="frame" x="31" y="0.0" width="16" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="16" id="AKl-Gq-RtM"/>
<constraint firstAttribute="height" constant="16" id="q2g-Ua-eIJ"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="Ign-Cq-DKf"/>
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
</imageView>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="40" id="2EU-Fd-hMg"/>
<constraint firstItem="nWj-33-m8Q" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="CAY-Pw-B8n"/>
<constraint firstAttribute="bottom" secondItem="doH-ww-BDw" secondAttribute="bottom" constant="4" id="Dq4-M6-1Wf"/>
<constraint firstItem="g4d-4N-NkC" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="bls-fM-H4b"/>
<constraint firstAttribute="bottom" secondItem="nWj-33-m8Q" secondAttribute="bottom" constant="4" id="f6j-eI-wiH"/>
<constraint firstAttribute="bottom" secondItem="g4d-4N-NkC" secondAttribute="bottom" constant="4" id="faS-Mo-Qa2"/>
<constraint firstItem="doH-ww-BDw" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="gL3-5S-OKo"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="TnH-dX-qaQ" secondAttribute="trailing" constant="30" id="3dD-wf-5pS"/>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="3" id="JmY-D0-uAy"/>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="30" id="S8i-CD-j3h"/>
<constraint firstAttribute="bottom" secondItem="TnH-dX-qaQ" secondAttribute="bottom" constant="3" id="fDc-OY-YL0"/>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="fFF-rl-3s4"/>
</constraints>
<connections>
<outlet property="imageViewDnsmasq" destination="DcG-x3-lvy" id="XxJ-kZ-bdO"/>
<outlet property="imageViewNginx" destination="ZqW-6d-vpe" id="Wil-Ug-8Kb"/>
<outlet property="imageViewPhp" destination="tko-cP-XSz" id="q7L-HK-7Pj"/>
<outlet property="textFieldPhp" destination="At1-ch-qv2" id="Guk-hr-f1T"/>
</connections>
<point key="canvasLocation" x="-64" y="195"/>
</customView>
</objects>
<resources>
<image name="ServiceLoading" width="17" height="16"/>
</resources>
</document>

View File

@ -1,36 +0,0 @@
//
// StatsView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class StatsView: NSView, XibLoadable {
@IBOutlet weak var titleMemLimit: NSTextField!
@IBOutlet weak var titleMaxPost: NSTextField!
@IBOutlet weak var titleMaxUpload: NSTextField!
@IBOutlet weak var labelMemLimit: NSTextField!
@IBOutlet weak var labelMaxPost: NSTextField!
@IBOutlet weak var labelMaxUpload: NSTextField!
static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem {
let view = Self.createFromXib()
view!.titleMemLimit.stringValue = "mi_memory_limit".localized.uppercased()
view!.titleMaxPost.stringValue = "mi_post_max_size".localized.uppercased()
view!.titleMaxUpload.stringValue = "mi_upload_max_filesize".localized.uppercased()
view!.labelMemLimit.stringValue = memory
view!.labelMaxPost.stringValue = post
view!.labelMaxUpload.stringValue = upload
let item = NSMenuItem()
item.view = view
item.target = self
return item
}
}

View File

@ -1,144 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="StatsView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="330" height="55"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
<rect key="frame" x="30" y="6" width="270" height="43"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
<rect key="frame" x="0.0" y="4" width="87" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="At1-ch-qv2">
<rect key="frame" x="-2" y="21" width="91" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="MEMORY LIMIT" id="LKe-C4-jxo">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Emt-m3-Dt6">
<rect key="frame" x="16" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="H6T-wY-PIG">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fillEqually" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g4d-4N-NkC">
<rect key="frame" x="107" y="4" width="68" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7um-XA-djV">
<rect key="frame" x="3" y="21" width="63" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX POST" id="Qfq-Bl-yuh">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Vyu-AO-8SH">
<rect key="frame" x="7" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="uH4-Zy-43x">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
<rect key="frame" x="195" y="4" width="75" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
<rect key="frame" x="-2" y="21" width="79" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX UPLOAD" id="lGh-MT-TgI">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eHT-tr-Kwx">
<rect key="frame" x="10" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="1iA-Ri-zYY">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="nWj-33-m8Q" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="CAY-Pw-B8n"/>
<constraint firstAttribute="bottom" secondItem="doH-ww-BDw" secondAttribute="bottom" constant="4" id="Dq4-M6-1Wf"/>
<constraint firstItem="g4d-4N-NkC" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="bls-fM-H4b"/>
<constraint firstAttribute="bottom" secondItem="nWj-33-m8Q" secondAttribute="bottom" constant="4" id="f6j-eI-wiH"/>
<constraint firstAttribute="bottom" secondItem="g4d-4N-NkC" secondAttribute="bottom" constant="4" id="faS-Mo-Qa2"/>
<constraint firstItem="doH-ww-BDw" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="gL3-5S-OKo"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="6" id="1mo-iG-Z0D"/>
<constraint firstAttribute="trailing" secondItem="TnH-dX-qaQ" secondAttribute="trailing" constant="30" id="3dD-wf-5pS"/>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="30" id="S8i-CD-j3h"/>
<constraint firstAttribute="bottom" secondItem="TnH-dX-qaQ" secondAttribute="bottom" constant="6" id="eve-qD-gUH"/>
</constraints>
<connections>
<outlet property="labelMaxPost" destination="Vyu-AO-8SH" id="5Cm-QO-hJQ"/>
<outlet property="labelMaxUpload" destination="eHT-tr-Kwx" id="5pK-FD-c4h"/>
<outlet property="labelMemLimit" destination="Emt-m3-Dt6" id="6nD-Su-XZ6"/>
<outlet property="titleMaxPost" destination="7um-XA-djV" id="5MN-Xb-XwL"/>
<outlet property="titleMaxUpload" destination="Oef-6n-9QI" id="Q61-JI-RJq"/>
<outlet property="titleMemLimit" destination="At1-ch-qv2" id="SQT-B9-sWS"/>
</connections>
<point key="canvasLocation" x="139" y="168"/>
</customView>
</objects>
</document>

View File

@ -0,0 +1,311 @@
//
// StatusMenu+Items.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 18/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
// MARK: - PHP Version
extension StatusMenu {
func addPhpVersionMenuItems() {
if PhpEnv.phpInstall.version.error {
let brokenMenuItems = ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"]
return addItems(brokenMenuItems.map { NSMenuItem(title: $0.localized) })
}
addItem(HeaderView.asMenuItem(
text: "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)",
minimumWidth: 280 // this ensures the menu is at least wide enough not to cause clipping
))
}
func addPhpActionMenuItems() {
if PhpEnv.shared.isBusy {
addItem(NSMenuItem(title: "mi_busy".localized))
return
}
if PhpEnv.shared.availablePhpVersions.isEmpty { return }
addSwitchToPhpMenuItems()
self.addItem(NSMenuItem.separator())
}
func addServicesManagerMenuItem() {
if PhpEnv.shared.isBusy {
return
}
addItems([
ServicesView.asMenuItem(),
NSMenuItem.separator()
])
}
func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
// Get the short and long version
let shortVersion = PhpEnv.shared.availablePhpVersions[index]
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
let versionString = long ? longVersion.toString() : shortVersion
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
let menuItem = PhpMenuItem(
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
action: (shortVersion == PhpEnv.phpInstall.version.short)
? nil
: action, keyEquivalent: "\(shortcutKey)"
)
menuItem.version = shortVersion
shortcutKey += 1
addItem(menuItem)
}
}
func addCoreMenuItems() {
addItems([
NSMenuItem.separator(),
NSMenuItem(title: "mi_preferences".localized,
action: #selector(MainMenu.openPrefs), keyEquivalent: ","),
NSMenuItem(title: "mi_check_for_updates".localized,
action: #selector(MainMenu.checkForUpdates)),
NSMenuItem.separator(),
NSMenuItem(title: "mi_about".localized,
action: #selector(MainMenu.openAbout)),
NSMenuItem(title: "mi_quit".localized,
action: #selector(MainMenu.terminateApp), keyEquivalent: "q")
])
}
// MARK: - Valet
func addValetMenuItems() {
addItems([
HeaderView.asMenuItem(text: "mi_valet".localized),
NSMenuItem(title: "mi_valet_config".localized,
action: #selector(MainMenu.openValetConfigFolder),
keyEquivalent: "v"),
NSMenuItem(title: "mi_domain_list".localized,
action: #selector(MainMenu.openDomainList),
keyEquivalent: "l"),
NSMenuItem.separator()
])
}
// MARK: - PHP Configuration
func addConfigurationMenuItems() {
addItems([
HeaderView.asMenuItem(text: "mi_configuration".localized),
NSMenuItem(title: "mi_php_config".localized,
action: #selector(MainMenu.openActiveConfigFolder),
keyEquivalent: "c"),
NSMenuItem(title: "mi_phpmon_config".localized,
action: #selector(MainMenu.openPhpMonitorConfigurationFile),
keyEquivalent: "y"),
NSMenuItem(title: "mi_phpinfo".localized,
action: #selector(MainMenu.openPhpInfo),
keyEquivalent: "i")
])
}
// MARK: - Composer
func addComposerMenuItems() {
addItems([
HeaderView.asMenuItem(text: "mi_composer".localized),
NSMenuItem(
title: "mi_global_composer".localized,
action: #selector(MainMenu.openGlobalComposerFolder),
keyEquivalent: "g"
),
NSMenuItem(
title: "mi_update_global_composer".localized,
action: PhpEnv.shared.isBusy
? nil
: #selector(MainMenu.updateGlobalComposerDependencies),
keyEquivalent: "g",
keyModifier: [.shift]
)
])
}
// MARK: - Stats
func addStatsMenuItem() {
guard let stats = PhpEnv.phpInstall.limits else { return }
addItem(StatsView.asMenuItem(
memory: stats.memory_limit,
post: stats.post_max_size,
upload: stats.upload_max_filesize)
)
}
// MARK: - Extensions
func addExtensionsMenuItems() {
addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
if PhpEnv.phpInstall.extensions.isEmpty {
addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
}
var shortcutKey = 1
for phpExtension in PhpEnv.phpInstall.extensions {
addExtensionItem(phpExtension, shortcutKey)
shortcutKey += 1
}
}
// MARK: - Presets
func addPresetsMenuItem() {
guard let presets = Preferences.custom.presets else {
addEmptyPresetHelp()
return
}
if presets.isEmpty {
addEmptyPresetHelp()
return
}
addLoadedPresets()
}
private func addEmptyPresetHelp() {
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
NSMenuItem(title: "mi_no_presets".localized),
NSMenuItem.separator(),
NSMenuItem(title: "mi_set_up_presets".localized,
action: #selector(MainMenu.showPresetHelp))
], target: MainMenu.shared))
}
private func addLoadedPresets() {
addItem(NSMenuItem(title: "mi_presets_title".localized, submenu: [
NSMenuItem.separator(),
HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)
] + PresetMenuItem.getAll() + [
NSMenuItem.separator(),
NSMenuItem(title: "mi_revert_to_prev_config".localized,
action: PresetHelper.rollbackPreset != nil ? #selector(MainMenu.rollbackPreset) : nil),
NSMenuItem.separator(),
NSMenuItem(title: "mi_profiles_loaded".localized(Preferences.custom.presets!.count))
], target: MainMenu.shared))
}
// MARK: - Xdebug
func addXdebugMenuItem() {
if !Xdebug.enabled {
return
}
addItems([
NSMenuItem(title: "mi_xdebug_mode".localized, submenu: [
HeaderView.asMenuItem(text: "mi_xdebug_available_modes".localized)
] + Xdebug.asMenuItems() + [
HeaderView.asMenuItem(text: "mi_xdebug_actions".localized),
NSMenuItem(title: "mi_xdebug_disable_all".localized,
action: #selector(MainMenu.disableAllXdebugModes))
], target: MainMenu.shared),
NSMenuItem.separator()
], target: MainMenu.shared)
}
// MARK: - PHP Doctor
func addPhpDoctorMenuItem() {
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
!WarningManager.shared.hasWarnings() {
return
}
addItems([
HeaderView.asMenuItem(text: "mi_php_doctor".localized),
NSMenuItem(title: "mi_recommendations_count".localized(WarningManager.shared.warnings.count)),
NSMenuItem(title: "mi_view_recommendations".localized, action: #selector(MainMenu.openWarnings)),
NSMenuItem.separator()
])
}
// MARK: - First Aid & Services
func addFirstAidAndServicesMenuItems() {
let services = NSMenuItem(title: "mi_other".localized)
let servicesMenu = NSMenu()
servicesMenu.addItems([
// FIRST AID
HeaderView.asMenuItem(text: "mi_first_aid".localized),
NSMenuItem(title: "mi_view_onboarding".localized, action: #selector(MainMenu.showWelcomeTour)),
NSMenuItem(title: "mi_fa_php_doctor".localized, action: #selector(MainMenu.openWarnings)),
NSMenuItem.separator(),
NSMenuItem(title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
action: #selector(MainMenu.fixMyValet),
toolTip: "mi_fix_my_valet_tooltip".localized),
NSMenuItem(title: "mi_fix_brew_permissions".localized(), action: #selector(MainMenu.fixHomebrewPermissions),
toolTip: "mi_fix_brew_permissions_tooltip".localized),
NSMenuItem.separator(),
// SERVICES
HeaderView.asMenuItem(text: "mi_services".localized),
NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq),
keyEquivalent: "d"),
NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm),
keyEquivalent: "p"),
NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx),
keyEquivalent: "n"),
NSMenuItem(title: "mi_restart_valet_services".localized, action: #selector(MainMenu.restartValetServices),
keyEquivalent: "s"),
NSMenuItem(title: "mi_stop_valet_services".localized, action: #selector(MainMenu.stopValetServices),
keyEquivalent: "s",
keyModifier: [.command, .shift]),
NSMenuItem.separator(),
// MANUAL ACTIONS
HeaderView.asMenuItem(text: "mi_manual_actions".localized),
NSMenuItem(title: "mi_php_refresh".localized,
action: #selector(MainMenu.reloadPhpMonitorMenuInForeground),
keyEquivalent: "r")
], target: MainMenu.shared)
setSubmenu(servicesMenu, for: services)
addItem(services)
}
// MARK: - Other helper methods to generate menu items
func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
let menuItem = ExtensionMenuItem(
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
action: #selector(MainMenu.toggleExtension),
keyEquivalent: keyEquivalent
)
if menuItem.keyEquivalent != "" {
menuItem.keyEquivalentModifierMask = [.option]
}
menuItem.state = phpExtension.enabled ? .on : .off
menuItem.phpExtension = phpExtension
addItem(menuItem)
}
}

View File

@ -8,292 +8,63 @@
import Cocoa
class StatusMenu: NSMenu {
func addMenuItems() {
addPhpVersionMenuItems()
addItem(NSMenuItem.separator())
func addPhpVersionMenuItems() {
if PhpEnv.phpInstall.version.error {
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
}
return
if Preferences.isEnabled(.displayGlobalVersionSwitcher) {
addPhpActionMenuItems()
addItem(NSMenuItem.separator())
}
let phpVersionText = "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)"
addItem(HeaderView.asMenuItem(text: phpVersionText))
}
func addPhpActionMenuItems() {
if PhpEnv.shared.isBusy {
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
return
if Preferences.isEnabled(.displayServicesManager) {
addServicesManagerMenuItem()
addItem(NSMenuItem.separator())
}
if PhpEnv.shared.availablePhpVersions.isEmpty {
return
if Preferences.isEnabled(.displayValetIntegration) {
addValetMenuItems()
addItem(NSMenuItem.separator())
}
self.addSwitchToPhpMenuItems()
self.addItem(NSMenuItem.separator())
if Preferences.isEnabled(.displayPhpConfigFinder) {
addConfigurationMenuItems()
addItem(NSMenuItem.separator())
}
self.addItem(ServicesView.asMenuItem())
self.addItem(NSMenuItem.separator())
}
func addValetMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_valet".localized))
self.addItem(NSMenuItem(
title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
self.addItem(NSMenuItem(
title: "mi_domain_list".localized, action: #selector(MainMenu.openDomainList), keyEquivalent: "l"))
self.addItem(NSMenuItem.separator())
}
func addRemainingMenuItems() {
self.addConfigurationMenuItems()
self.addItem(NSMenuItem.separator())
self.addComposerMenuItems()
if Preferences.isEnabled(.displayComposerToolkit) {
addComposerMenuItems()
addItem(NSMenuItem.separator())
}
if PhpEnv.shared.isBusy {
return
}
self.addItem(NSMenuItem.separator())
self.addStatsMenuItem()
self.addItem(NSMenuItem.separator())
self.addExtensionsMenuItems()
self.addItem(NSMenuItem.separator())
// self.addXdebugMenuItem()
self.addFirstAidAndServicesMenuItems()
}
func addCoreMenuItems() {
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_preferences".localized,
action: #selector(MainMenu.openPrefs), keyEquivalent: ","))
self.addItem(NSMenuItem(title: "mi_check_for_updates".localized,
action: #selector(MainMenu.checkForUpdates), keyEquivalent: ""))
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_about".localized,
action: #selector(MainMenu.openAbout), keyEquivalent: ""))
self.addItem(NSMenuItem(title: "mi_quit".localized,
action: #selector(MainMenu.terminateApp), keyEquivalent: "q"))
}
// MARK: Remaining Menu Items
func addConfigurationMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
self.addItem(
NSMenuItem(title: "mi_php_config".localized,
action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c")
)
self.addItem(
NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i")
)
}
func addComposerMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized))
self.addItem(
NSMenuItem(title: "mi_global_composer".localized,
action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g")
)
let composerMenuItem = NSMenuItem(
title: "mi_update_global_composer".localized,
action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies),
keyEquivalent: "g"
)
composerMenuItem.keyEquivalentModifierMask = .shift
self.addItem(composerMenuItem)
}
func addStatsMenuItem() {
guard let stats = PhpEnv.phpInstall.limits else { return }
self.addItem(StatsView.asMenuItem(
memory: stats.memory_limit,
post: stats.post_max_size,
upload: stats.upload_max_filesize)
)
}
func addExtensionsMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
if PhpEnv.phpInstall.extensions.isEmpty {
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
if Preferences.isEnabled(.displayLimitsWidget) {
addStatsMenuItem()
addItem(NSMenuItem.separator())
}
var shortcutKey = 1
for phpExtension in PhpEnv.phpInstall.extensions {
self.addExtensionItem(phpExtension, shortcutKey)
shortcutKey += 1
}
}
if Preferences.isEnabled(.displayExtensions) {
addExtensionsMenuItems()
NSMenuItem.separator()
func addXdebugMenuItem() {
if !Xdebug.enabled {
return
addXdebugMenuItem()
}
let xdebugSwitch = NSMenuItem(
title: "mi_xdebug_mode".localized,
action: nil,
keyEquivalent: ""
)
let xdebugModesMenu = NSMenu()
let xdebugMode = Xdebug.mode
addPhpDoctorMenuItem()
for mode in Xdebug.modes {
let item = XdebugMenuItem(
title: mode,
action: #selector(MainMenu.toggleXdebugMode(sender:)),
keyEquivalent: ""
)
item.state = xdebugMode == mode ? .on : .off
item.mode = mode
xdebugModesMenu.addItem(item)
if Preferences.isEnabled(.displayPresets) {
addPresetsMenuItem()
}
for item in xdebugModesMenu.items {
item.target = MainMenu.shared
if Preferences.isEnabled(.displayMisc) {
addFirstAidAndServicesMenuItems()
}
self.setSubmenu(xdebugModesMenu, for: xdebugSwitch)
self.addItem(xdebugSwitch)
}
addItem(NSMenuItem.separator())
func addFirstAidAndServicesMenuItems() {
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
let servicesMenu = NSMenu()
let fixMyValetMenuItem = NSMenuItem(
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
)
fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized
servicesMenu.addItem(fixMyValetMenuItem)
let fixHomebrewMenuItem = NSMenuItem(
title: "mi_fix_brew_permissions".localized(),
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: ""
)
fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized
servicesMenu.addItem(fixHomebrewMenuItem)
servicesMenu.addItem(NSMenuItem.separator())
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_dnsmasq".localized,
action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_php_fpm".localized,
action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_nginx".localized,
action: #selector(MainMenu.restartNginx), keyEquivalent: "n")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_all_services".localized,
action: #selector(MainMenu.restartAllServices), keyEquivalent: "s")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_stop_all_services".localized,
action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
withKeyModifier: [.command, .shift]
)
servicesMenu.addItem(NSMenuItem.separator())
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized))
servicesMenu.addItem(
NSMenuItem(title: "mi_php_refresh".localized,
action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r")
)
for item in servicesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(servicesMenu, for: services)
self.addItem(services)
}
// MARK: Private Helpers
private func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
// Get the short and long version
let shortVersion = PhpEnv.shared.availablePhpVersions[index]
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.versionNumber
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
let versionString = long ? longVersion.toString() : shortVersion
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
let menuItem = PhpMenuItem(
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
action: (shortVersion == PhpEnv.phpInstall.version.short)
? nil
: action, keyEquivalent: "\(shortcutKey)"
)
menuItem.version = shortVersion
shortcutKey += 1
self.addItem(menuItem)
}
}
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
let menuItem = ExtensionMenuItem(
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
action: #selector(MainMenu.toggleExtension),
keyEquivalent: keyEquivalent
)
if menuItem.keyEquivalent != "" {
menuItem.keyEquivalentModifierMask = [.option]
}
menuItem.state = phpExtension.enabled ? .on : .off
menuItem.phpExtension = phpExtension
self.addItem(menuItem)
addCoreMenuItems()
}
}
// MARK: - NSMenuItem subclasses
class PhpMenuItem: NSMenuItem {
var version: String = ""
}
class XdebugMenuItem: NSMenuItem {
var mode: String = ""
}
class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension?
}
class EditorMenuItem: NSMenuItem {
var editor: Application?
}

View File

@ -91,6 +91,7 @@ class BetterAlert {
NSApp.activate(ignoringOtherApps: true)
windowController.window?.makeKeyAndOrderFront(nil)
windowController.window?.setCenterPosition(offsetY: 70)
return NSApplication.shared.runModal(for: windowController.window!)
}
@ -102,14 +103,14 @@ class BetterAlert {
/**
Shows the modal and does not return anything.
*/
public func show() {
@MainActor public func show() {
_ = self.runModal()
}
/**
Shows the modal for a particular error.
*/
public static func show(for error: Error & AlertableError) {
@MainActor public static func show(for error: Error & AlertableError) {
let key = error.getErrorMessageKey()
return BetterAlert().withInformation(
title: "\(key).title".localized,

View File

@ -0,0 +1,45 @@
//
// OnboardingWindowController.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 25/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
import SwiftUI
class OnboardingWindowController: PMWindowController {
// MARK: - Window Identifier
override var windowName: String {
return "Onboarding"
}
public static func create(delegate: NSWindowDelegate?) {
let windowController = Self()
windowController.window = NSWindow()
guard let window = windowController.window else { return }
window.title = ""
window.styleMask = [.titled, .closable, .miniaturizable]
window.titlebarAppearsTransparent = true
window.delegate = delegate ?? windowController
window.contentView = NSHostingView(rootView: OnboardingView())
window.setContentSize(NSSize(width: 600, height: 600))
App.shared.onboardingWindowController = windowController
}
public static func show(delegate: NSWindowDelegate? = nil) {
if App.shared.onboardingWindowController == nil {
Self.create(delegate: delegate)
}
App.shared.onboardingWindowController?.showWindow(self)
App.shared.onboardingWindowController?.window?.setCenterPosition(offsetY: 70)
NSApp.activate(ignoringOtherApps: true)
}
}

View File

@ -9,9 +9,87 @@
import Foundation
struct CustomPrefs: Decodable {
let scanApps: [String]
let scanApps: [String]?
let presets: [Preset]?
let services: [String]?
let environmentVariables: [String: String]?
public func hasPresets() -> Bool {
return self.presets != nil && !self.presets!.isEmpty
}
public func hasServices() -> Bool {
return self.services != nil && !self.services!.isEmpty
}
public func hasEnvironmentVariables() -> Bool {
return self.environmentVariables != nil && !self.environmentVariables!.keys.isEmpty
}
public func getEnvironmentVariables() -> String {
return self.environmentVariables!.map { (key, value) in
return "export \(key)=\(value)"
}.joined(separator: "&&")
}
private enum CodingKeys: String, CodingKey {
case scanApps = "scan_apps"
case presets = "presets"
case services = "services"
case environmentVariables = "export"
}
}
extension Preferences {
func loadCustomPreferences() {
// Ensure the configuration directory is created if missing
Shell.run("mkdir -p ~/.config/phpmon")
// Move the legacy file
moveOutdatedConfigurationFile()
// Attempt to load the file if it exists
let url = URL(fileURLWithPath: "\(Paths.homePath)/.config/phpmon/config.json")
if Filesystem.fileExists(url.path) {
Log.info("A custom ~/.config/phpmon/config.json file was found. Attempting to parse...")
loadCustomPreferencesFile(url)
} else {
Log.info("There was no /.config/phpmon/config.json file to be loaded.")
}
}
func moveOutdatedConfigurationFile() {
if Filesystem.fileExists("~/.phpmon.conf.json") && !Filesystem.fileExists("~/.config/phpmon/config.json") {
Log.info("An outdated configuration file was found. Moving it...")
Shell.run("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
Log.info("The configuration file was copied successfully!")
}
}
func loadCustomPreferencesFile(_ url: URL) {
do {
customPreferences = try JSONDecoder().decode(
CustomPrefs.self,
from: try! String(contentsOf: url, encoding: .utf8).data(using: .utf8)!
)
Log.info("The ~/.config/phpmon/config.json file was successfully parsed.")
if customPreferences.hasPresets() {
Log.info("There are \(customPreferences.presets!.count) custom presets.")
}
if customPreferences.hasServices() {
Log.info("There are custom services: \(customPreferences.services!)")
}
if customPreferences.hasEnvironmentVariables() {
Log.info("Configuring the additional exports...")
Shell.user.exports = customPreferences.getEnvironmentVariables()
}
} catch {
Log.warn("The ~/.config/phpmon/config.json file seems to be missing or malformed.")
}
}
}

View File

@ -0,0 +1,14 @@
//
// Keys.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 25/07/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
struct Keys {
static let Escape = 53
static let Space = 49
}

View File

@ -0,0 +1,106 @@
//
// PreferenceName.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 07/09/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
/**
These are the keys used for every preference in the app.
*/
enum PreferenceName: String {
// FIRST-TIME LAUNCH
case wasLaunchedBefore = "launched_before"
// GENERAL
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch"
case allowProtocolForIntegrations = "allow_protocol_for_integrations"
case globalHotkey = "global_hotkey"
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
case showPhpDoctorSuggestions = "show_php_doctor_suggestions"
// APPEARANCE
case shouldDisplayDynamicIcon = "use_dynamic_icon"
case iconTypeToDisplay = "icon_type_to_display"
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
// NOTIFICATIONS
case notifyAboutVersionChange = "notify_about_version_change"
case notifyAboutPhpFpmRestart = "notify_about_php_fpm_restart"
case notifyAboutServices = "notify_about_services_restart"
case notifyAboutPresets = "notify_about_presets"
case notifyAboutSecureToggle = "notify_about_secure_toggle"
case notifyAboutGlobalComposerStatus = "notify_about_composer_status"
// MENU CUSTOMIZATION
case displayGlobalVersionSwitcher = "display_global_version_switcher"
case displayServicesManager = "display_services_manager"
case displayValetIntegration = "display_valet_integration"
case displayPhpConfigFinder = "display_php_config_finder"
case displayComposerToolkit = "display_composer_toolkit"
case displayLimitsWidget = "display_limits_widget"
case displayExtensions = "display_extensions"
case displayPresets = "display_presets"
case displayMisc = "display_misc"
/**
What type of data each preference contains.
*/
static var mapping: [PreferenceType: [PreferenceName]] = [
.boolean: [
// Preferences
.shouldDisplayDynamicIcon,
.fullPhpVersionDynamicIcon,
.autoServiceRestartAfterExtensionToggle,
.autoComposerGlobalUpdateAfterSwitch,
.allowProtocolForIntegrations,
.automaticBackgroundUpdateCheck,
.showPhpDoctorSuggestions,
// Notifications
.notifyAboutVersionChange,
.notifyAboutPhpFpmRestart,
.notifyAboutServices,
.notifyAboutPresets,
.notifyAboutSecureToggle,
.notifyAboutGlobalComposerStatus,
// UI Preferences
.displayGlobalVersionSwitcher,
.displayServicesManager,
.displayValetIntegration,
.displayPhpConfigFinder,
.displayComposerToolkit,
.displayLimitsWidget,
.displayExtensions,
.displayPresets,
.displayMisc
],
.string: [
.globalHotkey,
.iconTypeToDisplay
]
]
}
enum PreferenceType {
case boolean, string
}
/**
These are retired preferences that, if present, should be migrated.
*/
enum RetiredPreferenceName: String {
case shouldDisplayPhpHintInIcon = "add_php_to_icon"
}
/**
These are internal stats. They NEVER get shared.
*/
enum InternalStats: String {
case launchCount = "times_launched"
case switchCount = "times_switched_versions"
case didSeeSponsorEncouragement = "did_see_sponsor_encouragement"
}

View File

@ -8,37 +8,6 @@
import Foundation
/**
These are the keys used for every preference in the app.
*/
enum PreferenceName: String {
case wasLaunchedBefore = "launched_before"
case shouldDisplayDynamicIcon = "use_dynamic_icon"
case iconTypeToDisplay = "icon_type_to_display"
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch"
case allowProtocolForIntegrations = "allow_protocol_for_integrations"
case globalHotkey = "global_hotkey"
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
}
/**
These are retired preferences that, if present, should be migrated.
*/
enum RetiredPreferenceName: String {
case shouldDisplayPhpHintInIcon = "add_php_to_icon"
}
/**
These are internal stats. They NEVER get shared.
*/
enum InternalStats: String {
case launchCount = "times_launched"
case switchCount = "times_switched_versions"
case didSeeSponsorEncouragement = "did_see_sponsor_encouragement"
}
class Preferences {
// MARK: - Singleton
@ -52,7 +21,12 @@ class Preferences {
public init() {
Preferences.handleFirstTimeLaunch()
cachedPreferences = Self.cache()
customPreferences = CustomPrefs(scanApps: [])
customPreferences = CustomPrefs(
scanApps: [],
presets: [],
services: [],
environmentVariables: [:]
)
loadCustomPreferences()
}
@ -70,14 +44,37 @@ class Preferences {
*/
static func handleFirstTimeLaunch() {
UserDefaults.standard.register(defaults: [
/// Preferences
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
PreferenceName.iconTypeToDisplay.rawValue: MenuBarIcon.iconPhp.rawValue,
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
/// Preferences: General
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true,
PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue: false,
PreferenceName.allowProtocolForIntegrations.rawValue: true,
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
PreferenceName.showPhpDoctorSuggestions.rawValue: true,
/// Preferences: Appearance
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
PreferenceName.iconTypeToDisplay.rawValue: MenuBarIcon.iconPhp.rawValue,
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
/// Preferences: Notifications
PreferenceName.notifyAboutVersionChange.rawValue: true,
PreferenceName.notifyAboutPhpFpmRestart.rawValue: true,
PreferenceName.notifyAboutServices.rawValue: true,
PreferenceName.notifyAboutPresets.rawValue: true,
PreferenceName.notifyAboutSecureToggle.rawValue: true,
PreferenceName.notifyAboutGlobalComposerStatus.rawValue: true,
/// Preferences: UI Preferences
PreferenceName.displayGlobalVersionSwitcher.rawValue: true,
PreferenceName.displayServicesManager.rawValue: true,
PreferenceName.displayValetIntegration.rawValue: true,
PreferenceName.displayPhpConfigFinder.rawValue: true,
PreferenceName.displayComposerToolkit.rawValue: true,
PreferenceName.displayLimitsWidget.rawValue: true,
PreferenceName.displayExtensions.rawValue: true,
PreferenceName.displayPresets.rawValue: true,
PreferenceName.displayMisc.rawValue: true,
/// Stats
InternalStats.switchCount.rawValue: 0,
InternalStats.launchCount.rawValue: 0,
@ -134,28 +131,18 @@ class Preferences {
// MARK: - Internal Functionality
private static func cache() -> [PreferenceName: Any] {
return [
// Part 1: Always Booleans
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(
forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(
forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(
forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
.autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(
forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any,
.allowProtocolForIntegrations: UserDefaults.standard.bool(
forKey: PreferenceName.allowProtocolForIntegrations.rawValue) as Any,
.automaticBackgroundUpdateCheck: UserDefaults.standard.bool(
forKey: PreferenceName.automaticBackgroundUpdateCheck.rawValue) as Any,
// Part 2: Always Strings
.globalHotkey: UserDefaults.standard.string(
forKey: PreferenceName.globalHotkey.rawValue) as Any,
.iconTypeToDisplay: UserDefaults.standard.string(
forKey: PreferenceName.iconTypeToDisplay.rawValue) as Any
]
private static func cache() -> [PreferenceName: Any?] {
return Dictionary(uniqueKeysWithValues: PreferenceName.mapping
.flatMap { (key: PreferenceType, value: [PreferenceName]) in
value.map { preference -> (PreferenceName, Any?) in
return (preference, { () -> Any? in
switch key {
case .boolean: return UserDefaults.standard.bool(forKey: preference.rawValue)
case .string: return UserDefaults.standard.string(forKey: preference.rawValue)
}
}())
}
})
}
static func update(_ preference: PreferenceName, value: Any?) {
@ -169,29 +156,4 @@ class Preferences {
// Update the preferences cache in memory!
Preferences.shared.cachedPreferences = Preferences.cache()
}
// MARK: - Custom Preferences
private func loadCustomPreferences() {
let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json")
if Filesystem.fileExists(url.path) {
Log.info("A custom .phpmon.conf.json file was found. Attempting to parse...")
loadCustomPreferencesFile(url)
} else {
Log.info("There was no .phpmon.conf.json file to be loaded.")
}
}
private func loadCustomPreferencesFile(_ url: URL) {
do {
customPreferences = try JSONDecoder().decode(
CustomPrefs.self,
from: try! String(contentsOf: url, encoding: .utf8).data(using: .utf8)!
)
Log.info("The .phpmon.conf.json file was successfully parsed.")
} catch {
Log.warn("The .phpmon.conf.json file seems to be missing or malformed.")
}
}
}

View File

@ -0,0 +1,38 @@
//
// PreferencesWindowController+Hotkey.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 25/07/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension PreferencesWindowController {
// MARK: - Key Interaction
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)
guard let tabVC = self.contentViewController as? NSTabViewController else {
return
}
guard let vc = tabVC.tabViewItems[tabVC.selectedTabViewItemIndex].viewController as? GenericPreferenceVC else {
return
}
if vc.listeningForHotkeyView == nil {
return
}
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
Log.info("A blacklisted key was pressed, canceling listen!")
vc.listeningForHotkeyView!.unregister(nil)
} else {
vc.listeningForHotkeyView!.updateShortcut(event)
}
}
}

View File

@ -0,0 +1,107 @@
//
// PreferencesWindowController.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 02/04/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
class PreferencesWindowController: PMWindowController {
// MARK: - Window Identifier
override var windowName: String {
return "Preferences"
}
public static func create(delegate: NSWindowDelegate?) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(
withIdentifier: "preferencesWindow"
) as! PreferencesWindowController
guard let window = windowController.window else { return }
window.title = "prefs.title".localized
window.subtitle = "prefs.subtitle".localized
window.delegate = delegate ?? windowController
window.styleMask = [.titled, .closable, .miniaturizable]
App.shared.preferencesWindowController = windowController
}
public static func show(delegate: NSWindowDelegate? = nil) {
var justCreated = false
if App.shared.preferencesWindowController == nil {
Self.create(delegate: delegate)
guard let preferencesWC = App.shared.preferencesWindowController else {
return
}
guard let tabVC = preferencesWC.contentViewController as? NSTabViewController else {
return
}
for vc in preferencesWC.tabVCs {
tabVC.addChild(vc.viewController)
let item = tabVC.tabViewItem(for: vc.viewController)
item?.image = NSImage(systemSymbolName: vc.icon, accessibilityDescription: "\(vc.label) Icon")
item?.label = vc.label
}
tabVC.preferredContentSize = NSSize(
width: tabVC.view.frame.size.width,
height: tabVC.view.frame.size.height
)
justCreated = true
}
App.shared.preferencesWindowController?.showWindow(self)
if justCreated {
App.shared.preferencesWindowController?.positionWindowInTopLeftCorner()
}
NSApp.activate(ignoringOtherApps: true)
}
// MARK: - Tabs
struct PrefTabView {
let viewController: GenericPreferenceVC
let label: String
let icon: String
}
public lazy var tabVCs: [PrefTabView] = {
return [
PrefTabView(
viewController: GeneralPreferencesVC.fromStoryboard(),
label: "General",
icon: "gearshape"
),
PrefTabView(
viewController: AppearancePreferencesVC.fromStoryboard(),
label: "Appearance",
icon: "paintbrush"
),
PrefTabView(
viewController: MenuStructurePreferencesVC.fromStoryboard(),
label: "Visibility",
icon: "eye"
),
PrefTabView(
viewController: NotificationPreferencesVC.fromStoryboard(),
label: "Notifications",
icon: "bell.badge"
)
]
}()
}

View File

@ -9,56 +9,26 @@
import Cocoa
import Carbon
class PrefsVC: NSViewController {
class GenericPreferenceVC: NSViewController {
// MARK: - Window Identifier
// MARK: - Content
@IBOutlet weak var stackView: NSStackView!
// MARK: - Display
public static func create(delegate: NSWindowDelegate?) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(
withIdentifier: "preferencesWindow"
) as! PrefsWC
windowController.window!.title = "prefs.title".localized
windowController.window!.subtitle = "prefs.subtitle".localized
windowController.window!.delegate = delegate
windowController.window!.styleMask = [.titled, .closable, .miniaturizable]
windowController.window!.delegate = windowController
windowController.positionWindowInTopLeftCorner()
App.shared.preferencesWindowController = windowController
}
public static func show(delegate: NSWindowDelegate? = nil) {
if App.shared.preferencesWindowController == nil {
Self.create(delegate: delegate)
}
App.shared.preferencesWindowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
}
// MARK: - Lifecycle
var views: [NSView] = []
override func viewDidLoad() {
[
getDynamicIconPreferenceView(),
getIconOptionsPreferenceView(),
getIconDensityPreferenceView(),
getAutoRestartPreferenceView(),
getAutomaticComposerUpdatePreferenceView(),
getShortcutPreferenceView(),
getIntegrationsPreferenceView(),
getAutomaticUpdateCheckPreferenceView()
].forEach({ self.stackView.addArrangedSubview($0) })
super.viewDidLoad()
self.views.forEach({ self.stackView.addArrangedSubview($0) })
}
private func getDynamicIconPreferenceView() -> NSView {
// MARK: - Deinitialization
deinit {
Log.perf("PrefsVC deallocated")
}
func getDynamicIconPV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.dynamic_icon".localized,
descriptionText: "prefs.dynamic_icon_desc".localized,
@ -70,7 +40,7 @@ class PrefsVC: NSViewController {
)
}
private func getIconOptionsPreferenceView() -> NSView {
func getIconOptionsPV() -> NSView {
return SelectPreferenceView.make(
sectionText: "",
descriptionText: "prefs.icon_options_desc".localized,
@ -83,7 +53,7 @@ class PrefsVC: NSViewController {
)
}
private func getIconDensityPreferenceView() -> NSView {
func getIconDensityPV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.info_density".localized,
descriptionText: "prefs.display_full_php_version_desc".localized,
@ -96,7 +66,7 @@ class PrefsVC: NSViewController {
)
}
private func getAutoRestartPreferenceView() -> NSView {
func getAutoRestartPV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.services".localized,
descriptionText: "prefs.auto_restart_services_desc".localized,
@ -106,7 +76,7 @@ class PrefsVC: NSViewController {
)
}
private func getAutomaticComposerUpdatePreferenceView() -> NSView {
func getAutomaticComposerUpdatePV() -> NSView {
CheckboxPreferenceView.make(
sectionText: "prefs.switcher".localized,
descriptionText: "prefs.auto_composer_update_desc".localized,
@ -116,15 +86,15 @@ class PrefsVC: NSViewController {
)
}
private func getShortcutPreferenceView() -> NSView {
return HotkeyPreferenceView.make(
sectionText: "prefs.global_shortcut".localized,
descriptionText: "prefs.shortcut_desc".localized,
self
func getShortcutPV() -> NSView {
return HotkeyPreferenceView.make(
sectionText: "prefs.global_shortcut".localized,
descriptionText: "prefs.shortcut_desc".localized,
self
)
}
}
private func getIntegrationsPreferenceView() -> NSView {
func getIntegrationsPV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.integrations".localized,
descriptionText: "prefs.open_protocol_desc".localized,
@ -134,7 +104,7 @@ class PrefsVC: NSViewController {
)
}
private func getAutomaticUpdateCheckPreferenceView() -> NSView {
func getAutomaticUpdateCheckPV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.updates".localized,
descriptionText: "prefs.automatic_update_check_desc".localized,
@ -144,6 +114,97 @@ class PrefsVC: NSViewController {
)
}
func getShowPhpDoctorSuggestionsPV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.php_doctor".localized,
descriptionText: "prefs.php_doctor_suggestions_desc".localized,
checkboxText: "prefs.php_doctor_suggestions_title".localized,
preference: .showPhpDoctorSuggestions,
action: {
MainMenu.shared.refreshIcon()
MainMenu.shared.rebuild()
}
)
}
func getNotifyAboutVersionChangePV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "prefs.notifications".localized,
descriptionText: "prefs.notify_about_version_change_desc".localized,
checkboxText: "prefs.notify_about_version_change".localized,
preference: .notifyAboutVersionChange,
action: {}
)
}
func getNotifyAboutPhpFpmChangePV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "",
descriptionText: "prefs.notify_about_php_fpm_change_desc".localized,
checkboxText: "prefs.notify_about_php_fpm_change".localized,
preference: .notifyAboutPhpFpmRestart,
action: {}
)
}
func getNotifyAboutServicesPV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "",
descriptionText: "prefs.notify_about_services_desc".localized,
checkboxText: "prefs.notify_about_services".localized,
preference: .notifyAboutServices,
action: {}
)
}
func getNotifyAboutPresetsPV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "",
descriptionText: "prefs.notify_about_presets_desc".localized,
checkboxText: "prefs.notify_about_presets".localized,
preference: .notifyAboutPresets,
action: {}
)
}
func getNotifyAboutSecureTogglePV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "",
descriptionText: "prefs.notify_about_secure_status_desc".localized,
checkboxText: "prefs.notify_about_secure_status".localized,
preference: .notifyAboutSecureToggle,
action: {}
)
}
func getNotifyAboutGlobalComposerStatusPV() -> NSView {
return CheckboxPreferenceView.make(
sectionText: "",
descriptionText: "prefs.notify_about_composer_success_desc".localized,
checkboxText: "prefs.notify_about_composer_success".localized,
preference: .notifyAboutGlobalComposerStatus,
action: {}
)
}
func getDisplayMenuSectionPV(
_ localizationKey: String,
_ preference: PreferenceName,
_ first: Bool = false
) -> NSView {
return CheckboxPreferenceView.make(
sectionText: first ? "prefs.menu_contents".localized : "",
descriptionText: "\(localizationKey)_desc".localized,
checkboxText: localizationKey.localized,
preference: preference,
action: {
MainMenu.shared.refreshIcon()
MainMenu.shared.rebuild()
}
)
}
// MARK: - Listening for hotkey delegate
var listeningForHotkeyView: HotkeyPreferenceView?
@ -153,10 +214,84 @@ class PrefsVC: NSViewController {
listeningForHotkeyView = nil
}
}
}
// MARK: - Deinitialization
class GeneralPreferencesVC: GenericPreferenceVC {
deinit {
Log.perf("PrefsVC deallocated")
// MARK: - Lifecycle
public static func fromStoryboard() -> GenericPreferenceVC {
let vc = NSStoryboard(name: "Main", bundle: nil)
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
vc.views = [
vc.getShowPhpDoctorSuggestionsPV(),
vc.getAutoRestartPV(),
vc.getAutomaticComposerUpdatePV(),
vc.getShortcutPV(),
vc.getIntegrationsPV(),
vc.getAutomaticUpdateCheckPV()
]
return vc
}
}
class NotificationPreferencesVC: GenericPreferenceVC {
public static func fromStoryboard() -> GenericPreferenceVC {
let vc = NSStoryboard(name: "Main", bundle: nil)
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
vc.views = [
vc.getNotifyAboutVersionChangePV(),
vc.getNotifyAboutPresetsPV(),
vc.getNotifyAboutSecureTogglePV(),
vc.getNotifyAboutGlobalComposerStatusPV(),
vc.getNotifyAboutServicesPV(),
vc.getNotifyAboutPhpFpmChangePV()
]
return vc
}
}
class MenuStructurePreferencesVC: GenericPreferenceVC {
public static func fromStoryboard() -> GenericPreferenceVC {
let vc = NSStoryboard(name: "Main", bundle: nil)
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
vc.views = [
vc.getDisplayMenuSectionPV("prefs.display_global_version_switcher", .displayGlobalVersionSwitcher, true),
vc.getDisplayMenuSectionPV("prefs.display_services_manager", .displayServicesManager),
vc.getDisplayMenuSectionPV("prefs.display_valet_integration", .displayValetIntegration),
vc.getDisplayMenuSectionPV("prefs.display_php_config_finder", .displayPhpConfigFinder),
vc.getDisplayMenuSectionPV("prefs.display_composer_toolkit", .displayComposerToolkit),
vc.getDisplayMenuSectionPV("prefs.display_limits_widget", .displayLimitsWidget),
vc.getDisplayMenuSectionPV("prefs.display_extensions", .displayExtensions),
vc.getDisplayMenuSectionPV("prefs.display_presets", .displayPresets),
vc.getDisplayMenuSectionPV("prefs.display_misc", .displayMisc)
]
return vc
}
}
class AppearancePreferencesVC: GenericPreferenceVC {
public static func fromStoryboard() -> GenericPreferenceVC {
let vc = NSStoryboard(name: "Main", bundle: nil)
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
vc.views = [
vc.getDynamicIconPV(),
vc.getIconOptionsPV(),
vc.getIconDensityPV()
]
return vc
}
}

View File

@ -1,41 +0,0 @@
//
// PrefsWC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 02/04/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
struct Keys {
static let Escape = 53
static let Space = 49
}
class PrefsWC: PMWindowController {
// MARK: - Window Identifier
override var windowName: String {
return "Preferences"
}
// MARK: - Key Interaction
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)
if let vc = contentViewController as? PrefsVC {
if vc.listeningForHotkeyView != nil {
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
Log.info("A blacklisted key was pressed, canceling listen!")
vc.listeningForHotkeyView = nil
} else {
vc.listeningForHotkeyView!.updateShortcut(event)
}
}
}
}
}

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