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

🚀 Version 5.3

Merge branch 'dev/5.3'
This commit is contained in:
2022-05-13 16:33:32 +02:00
141 changed files with 4358 additions and 2090 deletions

15
.swiftlint.yml Normal file
View File

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

View File

@ -1,5 +1,19 @@
# DEVELOPER README
## ✅ Linting
This project uses the [SwiftLint](https://github.com/realm/SwiftLint) linter. You must install it and can run it like so:
```
swiftlint
```
It also automatically runs when you try to build the project. You'll get a warning if `swiftlint` is not installed, though. You can attempt to automatically fix issues:
```
swiftlint --fix
```
## 🔧 Build instructions
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>

View File

@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
54A18D40282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test in Resources */ = {isa = PBXBuildFile; fileRef = 54A18D3F282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test */; };
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
54B48B60275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
54D9E0B227E4F51E003B9AD9 /* HotKeysController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AC27E4F51E003B9AD9 /* HotKeysController.swift */; };
@ -51,6 +52,9 @@
C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; };
C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
C40FE737282ABA4F00A302C2 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE736282ABA4F00A302C2 /* AppVersion.swift */; };
C40FE738282ABA4F00A302C2 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE736282ABA4F00A302C2 /* AppVersion.swift */; };
C40FE73B282ABB2E00A302C2 /* AppVersionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE739282ABB2E00A302C2 /* AppVersionTest.swift */; };
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; };
C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; };
@ -71,32 +75,38 @@
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; };
C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; };
C41CA5ED2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* DomainListVC+Actions.swift */; };
C41CA5EE2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* DomainListVC+Actions.swift */; };
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; };
C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; };
C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* DomainListVC+ContextMenu.swift */; };
C41E871B2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* DomainListVC+ContextMenu.swift */; };
C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4205A7D27F4D21800191A39 /* ValetProxy.swift */; };
C4205A7F27F4D21800191A39 /* ValetProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4205A7D27F4D21800191A39 /* ValetProxy.swift */; };
C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; };
C42337A3281F19F000459A48 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; };
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */; };
C42CFB1627DFDE7900862737 /* nicoverbruggen.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1527DFDE7900862737 /* nicoverbruggen.test */; };
C42CFB1827DFDFDC00862737 /* nicoverbruggen_isolated.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1727DFDFDC00862737 /* nicoverbruggen_isolated.test */; };
C42CFB1A27DFE8BD00862737 /* NginxConfigParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42CFB1927DFE8BD00862737 /* NginxConfigParserTest.swift */; };
C42CFB1627DFDE7900862737 /* nginx-site.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1527DFDE7900862737 /* nginx-site.test */; };
C42CFB1827DFDFDC00862737 /* nginx-site-isolated.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1727DFDFDC00862737 /* nginx-site-isolated.test */; };
C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */; };
C42F26732805B4B400938AC7 /* DomainListable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42F26722805B4B400938AC7 /* DomainListable.swift */; };
C42F26742805B4B400938AC7 /* DomainListable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42F26722805B4B400938AC7 /* DomainListable.swift */; };
C42F26762805FEE200938AC7 /* nginx-secure-proxy.test in Resources */ = {isa = PBXBuildFile; fileRef = C42F26752805FEE200938AC7 /* nginx-secure-proxy.test */; };
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; };
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; };
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; };
C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; };
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */; };
C44067F527E2582B0045BD4E /* SiteListNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F427E2582B0045BD4E /* SiteListNameCell.swift */; };
C44067F727E258410045BD4E /* SiteListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* SiteListPhpCell.swift */; };
C44067F927E2585E0045BD4E /* SiteListTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F827E2585E0045BD4E /* SiteListTypeCell.swift */; };
C44067FB27E25FD70045BD4E /* SiteListTLSCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067FA27E25FD70045BD4E /* SiteListTLSCell.swift */; };
C449B4F027EE7FB800C47E8A /* SiteListTLSCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067FA27E25FD70045BD4E /* SiteListTLSCell.swift */; };
C449B4F127EE7FC200C47E8A /* SiteListNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F427E2582B0045BD4E /* SiteListNameCell.swift */; };
C449B4F227EE7FC400C47E8A /* SiteListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* SiteListPhpCell.swift */; };
C449B4F327EE7FC600C47E8A /* SiteListTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F827E2585E0045BD4E /* SiteListTypeCell.swift */; };
C449B4F427EE7FC800C47E8A /* SiteListKindCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC51FB27E27F47008528CA /* SiteListKindCell.swift */; };
C43A8A2025D9D1D700591B77 /* brew-formula.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew-formula.json */; };
C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */; };
C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F427E2582B0045BD4E /* DomainListNameCell.swift */; };
C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* DomainListPhpCell.swift */; };
C44067F927E2585E0045BD4E /* DomainListTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F827E2585E0045BD4E /* DomainListTypeCell.swift */; };
C44067FB27E25FD70045BD4E /* DomainListTLSCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */; };
C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */; };
C449B4F127EE7FC200C47E8A /* DomainListNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F427E2582B0045BD4E /* DomainListNameCell.swift */; };
C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* DomainListPhpCell.swift */; };
C449B4F327EE7FC600C47E8A /* DomainListTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F827E2585E0045BD4E /* DomainListTypeCell.swift */; };
C449B4F427EE7FC800C47E8A /* DomainListKindCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */; };
C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* ProgressWindow.swift */; };
C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* ProgressWindow.swift */; };
C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C44C1990276E44CB0072762D /* ProgressWindow.storyboard */; };
@ -105,11 +115,15 @@
C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */; };
C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; };
C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; };
C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */; };
C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */; };
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* SiteListVC.swift */; };
C464ADB0275A7A6A003FCD53 /* SiteListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* SiteListVC.swift */; };
C464ADB2275A87CA003FCD53 /* SiteListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* SiteListCellProtocol.swift */; };
C459B4BD27F6093700E9B4B4 /* nginx-proxy.test in Resources */ = {isa = PBXBuildFile; fileRef = C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */; };
C464ADAC275A7A3F003FCD53 /* DomainListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */; };
C464ADAD275A7A3F003FCD53 /* DomainListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */; };
C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* DomainListVC.swift */; };
C464ADB0275A7A6A003FCD53 /* DomainListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* DomainListVC.swift */; };
C464ADB2275A87CA003FCD53 /* DomainListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */; };
C46E206D28299B3800D909D6 /* AppUpdateChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E206C28299B3800D909D6 /* AppUpdateChecker.swift */; };
C46E206E28299B3800D909D6 /* AppUpdateChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E206C28299B3800D909D6 /* AppUpdateChecker.swift */; };
C46E20702829D27F00D909D6 /* AppUpdaterCheckTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E206F2829D27F00D909D6 /* AppUpdaterCheckTest.swift */; };
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
@ -119,6 +133,8 @@
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
C484437B2804BB560041A78A /* ValetProxyScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C484437A2804BB560041A78A /* ValetProxyScanner.swift */; };
C484437C2804BB560041A78A /* ValetProxyScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C484437A2804BB560041A78A /* ValetProxyScanner.swift */; };
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C8F25CC7FD000CC7490 /* StatsView.xib */; };
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
@ -134,11 +150,11 @@
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49E171E27A5736E00787921 /* PMServicesView.swift */; };
C4AC51FC27E27F47008528CA /* SiteListKindCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC51FB27E27F47008528CA /* SiteListKindCell.swift */; };
C4AC51FC27E27F47008528CA /* DomainListKindCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */; };
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4AF9F71275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */; };
C4AF9F78275447F100D44ED0 /* ValetConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */; };
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; };
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; };
C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */; };
@ -158,6 +174,14 @@
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; };
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; };
C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */; };
C4C0E8DF27F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C0E8DE27F88AEB002D32A9 /* FakeSiteScanner.swift */; };
C4C0E8E027F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C0E8DE27F88AEB002D32A9 /* FakeSiteScanner.swift */; };
C4C0E8E227F88B13002D32A9 /* ValetSiteScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C0E8E127F88B13002D32A9 /* ValetSiteScanner.swift */; };
C4C0E8E327F88B13002D32A9 /* ValetSiteScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C0E8E127F88B13002D32A9 /* ValetSiteScanner.swift */; };
C4C0E8E727F88B41002D32A9 /* ProxyScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C0E8E627F88B41002D32A9 /* ProxyScanner.swift */; };
C4C0E8E827F88B41002D32A9 /* ProxyScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C0E8E627F88B41002D32A9 /* ProxyScanner.swift */; };
C4C0E8EA27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C0E8E927F88B80002D32A9 /* ValetProxy+Fake.swift */; };
C4C0E8EB27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C0E8E927F88B80002D32A9 /* ValetProxy+Fake.swift */; };
C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C1019A27C65C6F001FACC2 /* Process.swift */; };
C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C1019A27C65C6F001FACC2 /* Process.swift */; };
C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
@ -172,17 +196,19 @@
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */; };
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */; };
C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */; };
C4D5CFCA27E0F9CD00035329 /* NginxConfigParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigParser.swift */; };
C4D5CFCB27E0F9CD00035329 /* NginxConfigParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigParser.swift */; };
C4D5CFCA27E0F9CD00035329 /* NginxConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */; };
C4D5CFCB27E0F9CD00035329 /* NginxConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */; };
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
C4D936C927E3EB6100BD69FE /* PhpHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D936C827E3EB6100BD69FE /* PhpHelper.swift */; };
C4D936CA27E3EB6100BD69FE /* PhpHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D936C827E3EB6100BD69FE /* PhpHelper.swift */; };
C4D936CB27E3EE4A00BD69FE /* SiteListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* SiteListCellProtocol.swift */; };
C4D936CB27E3EE4A00BD69FE /* DomainListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */; };
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4D9F24B280B69E100DCD39A /* AddProxyVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9F24A280B69E100DCD39A /* AddProxyVC.swift */; };
C4D9F24C280B69E100DCD39A /* AddProxyVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9F24A280B69E100DCD39A /* AddProxyVC.swift */; };
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; };
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; };
@ -211,9 +237,10 @@
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; };
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */; };
C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */; };
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4F780B725D80B5D000DBC97 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
@ -232,6 +259,8 @@
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; };
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */; };
C4FE011128084FC200D1DE6D /* SelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FE011028084FC200D1DE6D /* SelectionVC.swift */; };
C4FE011228084FC200D1DE6D /* SelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FE011028084FC200D1DE6D /* SelectionVC.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -247,6 +276,7 @@
/* Begin PBXFileReference section */
5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = "<group>"; };
5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
54A18D3F282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-secure-proxy-custom-tld.test"; sourceTree = "<group>"; };
54B48B5E275F66AE006D90C5 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
54D9E0AC27E4F51E003B9AD9 /* HotKeysController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKeysController.swift; sourceTree = "<group>"; };
54D9E0AD27E4F51E003B9AD9 /* Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Key.swift; sourceTree = "<group>"; };
@ -269,6 +299,8 @@
C40C7F1D2772136000DDDCDC /* PhpEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpEnv.swift; sourceTree = "<group>"; };
C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActivePhpInstallation+Checks.swift"; sourceTree = "<group>"; };
C40C7F2F27722E8D00DDDCDC /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
C40FE736282ABA4F00A302C2 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = "<group>"; };
C40FE739282ABB2E00A302C2 /* AppVersionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionTest.swift; sourceTree = "<group>"; };
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = "<group>"; };
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFrameworks.swift; sourceTree = "<group>"; };
C415D3B62770F294005EF286 /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
@ -286,30 +318,37 @@
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = "<group>"; };
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = "<group>"; };
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = "<group>"; };
C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+Actions.swift"; sourceTree = "<group>"; };
C41CA5EC2774F8EE00A2C80E /* DomainListVC+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DomainListVC+Actions.swift"; sourceTree = "<group>"; };
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = "<group>"; };
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.swift"; sourceTree = "<group>"; };
C41E87192763D42300161EE0 /* DomainListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DomainListVC+ContextMenu.swift"; sourceTree = "<group>"; };
C4205A7D27F4D21800191A39 /* ValetProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetProxy.swift; sourceTree = "<group>"; };
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
C42337A2281F19F000459A48 /* Xdebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xdebug.swift; sourceTree = "<group>"; };
C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = "<group>"; };
C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+FixMyValet.swift"; sourceTree = "<group>"; };
C42CFB1527DFDE7900862737 /* nicoverbruggen.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = nicoverbruggen.test; sourceTree = "<group>"; };
C42CFB1727DFDFDC00862737 /* nicoverbruggen_isolated.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = nicoverbruggen_isolated.test; sourceTree = "<group>"; };
C42CFB1927DFE8BD00862737 /* NginxConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigParserTest.swift; sourceTree = "<group>"; };
C42CFB1527DFDE7900862737 /* nginx-site.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-site.test"; sourceTree = "<group>"; };
C42CFB1727DFDFDC00862737 /* nginx-site-isolated.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-site-isolated.test"; sourceTree = "<group>"; };
C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigurationTest.swift; sourceTree = "<group>"; };
C42F26722805B4B400938AC7 /* DomainListable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListable.swift; sourceTree = "<group>"; };
C42F26752805FEE200938AC7 /* nginx-secure-proxy.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-secure-proxy.test"; sourceTree = "<group>"; };
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.swift"; sourceTree = "<group>"; };
C43A8A1925D9CD1000591B77 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = "<group>"; };
C43A8A1F25D9D1D700591B77 /* brew.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = brew.json; sourceTree = "<group>"; };
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewJsonParserTest.swift; sourceTree = "<group>"; };
C44067F427E2582B0045BD4E /* SiteListNameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListNameCell.swift; sourceTree = "<group>"; };
C44067F627E258410045BD4E /* SiteListPhpCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListPhpCell.swift; sourceTree = "<group>"; };
C44067F827E2585E0045BD4E /* SiteListTypeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteListTypeCell.swift; sourceTree = "<group>"; };
C44067FA27E25FD70045BD4E /* SiteListTLSCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteListTLSCell.swift; sourceTree = "<group>"; };
C43A8A1F25D9D1D700591B77 /* brew-formula.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "brew-formula.json"; sourceTree = "<group>"; };
C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackageTest.swift; sourceTree = "<group>"; };
C44067F427E2582B0045BD4E /* DomainListNameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListNameCell.swift; sourceTree = "<group>"; };
C44067F627E258410045BD4E /* DomainListPhpCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListPhpCell.swift; sourceTree = "<group>"; };
C44067F827E2585E0045BD4E /* DomainListTypeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTypeCell.swift; sourceTree = "<group>"; };
C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTLSCell.swift; sourceTree = "<group>"; };
C44C198C276E3A1C0072762D /* ProgressWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressWindow.swift; sourceTree = "<group>"; };
C44C1990276E44CB0072762D /* ProgressWindow.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ProgressWindow.storyboard; sourceTree = "<group>"; };
C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableError.swift; sourceTree = "<group>"; };
C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Async.swift"; sourceTree = "<group>"; };
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListWC.swift; sourceTree = "<group>"; };
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListVC.swift; sourceTree = "<group>"; };
C464ADB1275A87CA003FCD53 /* SiteListCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListCellProtocol.swift; sourceTree = "<group>"; };
C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-proxy.test"; sourceTree = "<group>"; };
C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListWC.swift; sourceTree = "<group>"; };
C464ADAE275A7A69003FCD53 /* DomainListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListVC.swift; sourceTree = "<group>"; };
C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListCellProtocol.swift; sourceTree = "<group>"; };
C46E206C28299B3800D909D6 /* AppUpdateChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateChecker.swift; sourceTree = "<group>"; };
C46E206F2829D27F00D909D6 /* AppUpdaterCheckTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppUpdaterCheckTest.swift; sourceTree = "<group>"; };
C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = "<group>"; };
@ -317,6 +356,7 @@
C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
C4811D2322D70A4700B5F6B3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
C484437A2804BB560041A78A /* ValetProxyScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetProxyScanner.swift; sourceTree = "<group>"; };
C48D0C8F25CC7FD000CC7490 /* StatsView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatsView.xib; sourceTree = "<group>"; };
C48D0C9225CC804200CC7490 /* XibLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XibLoadable.swift; sourceTree = "<group>"; };
C48D0C9525CC80B100CC7490 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
@ -328,10 +368,10 @@
C4930849279F331F009C240B /* AddSiteVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteVC.swift; sourceTree = "<group>"; };
C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = "<group>"; };
C49E171E27A5736E00787921 /* PMServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMServicesView.swift; sourceTree = "<group>"; };
C4AC51FB27E27F47008528CA /* SiteListKindCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteListKindCell.swift; sourceTree = "<group>"; };
C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListKindCell.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
C4AF9F70275445FF00D44ED0 /* valet-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "valet-config.json"; sourceTree = "<group>"; };
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigParserTest.swift; sourceTree = "<group>"; };
C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigurationTest.swift; sourceTree = "<group>"; };
C4AF9F792754499000D44ED0 /* Valet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.swift; sourceTree = "<group>"; };
C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetVersionExtractorTest.swift; sourceTree = "<group>"; };
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.swift; sourceTree = "<group>"; };
@ -342,6 +382,10 @@
C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MenuOutlets.swift"; sourceTree = "<group>"; };
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+ActivationPolicy.swift"; sourceTree = "<group>"; };
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+GlobalHotkey.swift"; sourceTree = "<group>"; };
C4C0E8DE27F88AEB002D32A9 /* FakeSiteScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeSiteScanner.swift; sourceTree = "<group>"; };
C4C0E8E127F88B13002D32A9 /* ValetSiteScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetSiteScanner.swift; sourceTree = "<group>"; };
C4C0E8E627F88B41002D32A9 /* ProxyScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyScanner.swift; sourceTree = "<group>"; };
C4C0E8E927F88B80002D32A9 /* ValetProxy+Fake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValetProxy+Fake.swift"; sourceTree = "<group>"; };
C4C1019A27C65C6F001FACC2 /* Process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Process.swift; sourceTree = "<group>"; };
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Startup.swift"; sourceTree = "<group>"; };
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = "<group>"; };
@ -350,12 +394,13 @@
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = "<group>"; };
C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Switcher.swift"; sourceTree = "<group>"; };
C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerWindow.swift; sourceTree = "<group>"; };
C4D5CFC927E0F9CD00035329 /* NginxConfigParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigParser.swift; sourceTree = "<group>"; };
C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfiguration.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = "<group>"; };
C4D89BC52783C99400A02B68 /* ComposerJson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerJson.swift; sourceTree = "<group>"; };
C4D936C827E3EB6100BD69FE /* PhpHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpHelper.swift; sourceTree = "<group>"; };
C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpSwitcher.swift; sourceTree = "<group>"; };
C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalSwitcher.swift; sourceTree = "<group>"; };
C4D9F24A280B69E100DCD39A /* AddProxyVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddProxyVC.swift; sourceTree = "<group>"; };
C4DEB7D327A5D60B00834718 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = "<group>"; };
C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWindowExtension.swift; sourceTree = "<group>"; };
C4E4404527C56F4700D225E1 /* ValetSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetSite.swift; sourceTree = "<group>"; };
@ -373,14 +418,16 @@
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = "<group>"; };
C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = "<group>"; };
C4F5FBCC28218C93001065C5 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
C4F780A725D80AE8000DBC97 /* php.ini */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = php.ini; sourceTree = "<group>"; };
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionParserTest.swift; sourceTree = "<group>"; };
C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtensionTest.swift; sourceTree = "<group>"; };
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
C4F8C0A522D4FA41002EFE61 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionDetectionTest.swift; sourceTree = "<group>"; };
C4FE011028084FC200D1DE6D /* SelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionVC.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -422,6 +469,7 @@
C48D6C6E279CD29C00F26D7E /* PHP Version */,
C4D9ADC2277610E4007277F4 /* Switcher */,
C4F30B01278E169B00755FCE /* Homebrew */,
C42337A1281F19DC00459A48 /* Extensions */,
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */,
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */,
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
@ -484,12 +532,10 @@
C40C7F1C27720E1400DDDCDC /* Test Files */ = {
isa = PBXGroup;
children = (
C42CFB1527DFDE7900862737 /* nicoverbruggen.test */,
C42CFB1727DFDFDC00862737 /* nicoverbruggen_isolated.test */,
C4AF9F70275445FF00D44ED0 /* valet-config.json */,
C43A8A1F25D9D1D700591B77 /* brew.json */,
C4F30B06278E195800755FCE /* brew-services.json */,
C4F780A725D80AE8000DBC97 /* php.ini */,
C459B4C127F6097E00E9B4B4 /* php */,
C459B4C027F6096300E9B4B4 /* valet */,
C459B4BF27F6094100E9B4B4 /* brew */,
C459B4BE27F6093A00E9B4B4 /* nginx */,
);
path = "Test Files";
sourceTree = "<group>";
@ -517,6 +563,7 @@
C4E713562570150F00007428 /* SECURITY.md */,
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
54D9E0C027E4F5E9003B9AD9 /* LICENSE */,
C4F5FBCC28218C93001065C5 /* .swiftlint.yml */,
C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */,
C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
@ -566,7 +613,7 @@
C4B13B1D25C4915000548C3A /* App */,
C4D9ADBD27761084007277F4 /* PHP */,
C47331A0247093AC009A0597 /* Menu */,
C464ADAA275A7A25003FCD53 /* SiteList */,
C464ADAA275A7A25003FCD53 /* DomainList */,
5420395726135DB800FB00FA /* Preferences */,
C44C198F276E3A380072762D /* Progress */,
C4C8E81D276F5686003AC782 /* Watcher */,
@ -575,15 +622,23 @@
path = Domain;
sourceTree = "<group>";
};
C42337A1281F19DC00459A48 /* Extensions */ = {
isa = PBXGroup;
children = (
C42337A2281F19F000459A48 /* Xdebug.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
C44067F327E256560045BD4E /* Cells */ = {
isa = PBXGroup;
children = (
C464ADB1275A87CA003FCD53 /* SiteListCellProtocol.swift */,
C44067FA27E25FD70045BD4E /* SiteListTLSCell.swift */,
C44067F427E2582B0045BD4E /* SiteListNameCell.swift */,
C44067F627E258410045BD4E /* SiteListPhpCell.swift */,
C44067F827E2585E0045BD4E /* SiteListTypeCell.swift */,
C4AC51FB27E27F47008528CA /* SiteListKindCell.swift */,
C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */,
C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */,
C44067F427E2582B0045BD4E /* DomainListNameCell.swift */,
C44067F627E258410045BD4E /* DomainListPhpCell.swift */,
C44067F827E2585E0045BD4E /* DomainListTypeCell.swift */,
C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */,
);
path = Cells;
sourceTree = "<group>";
@ -606,17 +661,56 @@
path = Errors;
sourceTree = "<group>";
};
C464ADAA275A7A25003FCD53 /* SiteList */ = {
C459B4BE27F6093A00E9B4B4 /* nginx */ = {
isa = PBXGroup;
children = (
C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */,
C42F26752805FEE200938AC7 /* nginx-secure-proxy.test */,
54A18D3F282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test */,
C42CFB1527DFDE7900862737 /* nginx-site.test */,
C42CFB1727DFDFDC00862737 /* nginx-site-isolated.test */,
);
path = nginx;
sourceTree = "<group>";
};
C459B4BF27F6094100E9B4B4 /* brew */ = {
isa = PBXGroup;
children = (
C43A8A1F25D9D1D700591B77 /* brew-formula.json */,
C4F30B06278E195800755FCE /* brew-services.json */,
);
path = brew;
sourceTree = "<group>";
};
C459B4C027F6096300E9B4B4 /* valet */ = {
isa = PBXGroup;
children = (
C4AF9F70275445FF00D44ED0 /* valet-config.json */,
);
path = valet;
sourceTree = "<group>";
};
C459B4C127F6097E00E9B4B4 /* php */ = {
isa = PBXGroup;
children = (
C4F780A725D80AE8000DBC97 /* php.ini */,
);
path = php;
sourceTree = "<group>";
};
C464ADAA275A7A25003FCD53 /* DomainList */ = {
isa = PBXGroup;
children = (
C44067F327E256560045BD4E /* Cells */,
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */,
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */,
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */,
C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */,
C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */,
C464ADAE275A7A69003FCD53 /* DomainListVC.swift */,
C41E87192763D42300161EE0 /* DomainListVC+ContextMenu.swift */,
C41CA5EC2774F8EE00A2C80E /* DomainListVC+Actions.swift */,
C4FE011028084FC200D1DE6D /* SelectionVC.swift */,
C4930849279F331F009C240B /* AddSiteVC.swift */,
C4D9F24A280B69E100DCD39A /* AddProxyVC.swift */,
);
path = SiteList;
path = DomainList;
sourceTree = "<group>";
};
C47331A0247093AC009A0597 /* Menu */ = {
@ -666,10 +760,9 @@
isa = PBXGroup;
children = (
C4AF9F792754499000D44ED0 /* Valet.swift */,
C4E4404527C56F4700D225E1 /* ValetSite.swift */,
C41C02A827E61A65009F26CB /* ValetSite+Fake.swift */,
C41C02A527E60D7A009F26CB /* SiteScanner.swift */,
C4D5CFC927E0F9CD00035329 /* NginxConfigParser.swift */,
C42F26722805B4B400938AC7 /* DomainListable.swift */,
C4C0E8D927F887BD002D32A9 /* Proxies */,
C4C0E8D827F887A5002D32A9 /* Sites */,
);
path = Valet;
sourceTree = "<group>";
@ -677,6 +770,7 @@
C4AF9F6B275445D300D44ED0 /* Integrations */ = {
isa = PBXGroup;
children = (
C4C0E8DA27F887CC002D32A9 /* Nginx */,
C4D89BC42783C98800A02B68 /* Composer */,
C4AF9F6C275445D900D44ED0 /* Homebrew */,
C4AF9F6A275445C900D44ED0 /* Valet */,
@ -705,6 +799,8 @@
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
C4EED88827A48778006D7272 /* InterAppHandler.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
C46E206C28299B3800D909D6 /* AppUpdateChecker.swift */,
C40FE736282ABA4F00A302C2 /* AppVersion.swift */,
);
path = App;
sourceTree = "<group>";
@ -721,13 +817,60 @@
path = Common;
sourceTree = "<group>";
};
C4C0E8D827F887A5002D32A9 /* Sites */ = {
isa = PBXGroup;
children = (
C4E4404527C56F4700D225E1 /* ValetSite.swift */,
C41C02A827E61A65009F26CB /* ValetSite+Fake.swift */,
C4C0E8E427F88B1F002D32A9 /* SiteScanner */,
);
path = Sites;
sourceTree = "<group>";
};
C4C0E8D927F887BD002D32A9 /* Proxies */ = {
isa = PBXGroup;
children = (
C4205A7D27F4D21800191A39 /* ValetProxy.swift */,
C4C0E8E927F88B80002D32A9 /* ValetProxy+Fake.swift */,
C4C0E8E527F88B36002D32A9 /* ProxyScanner */,
);
path = Proxies;
sourceTree = "<group>";
};
C4C0E8DA27F887CC002D32A9 /* Nginx */ = {
isa = PBXGroup;
children = (
C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */,
);
path = Nginx;
sourceTree = "<group>";
};
C4C0E8E427F88B1F002D32A9 /* SiteScanner */ = {
isa = PBXGroup;
children = (
C41C02A527E60D7A009F26CB /* SiteScanner.swift */,
C4C0E8E127F88B13002D32A9 /* ValetSiteScanner.swift */,
C4C0E8DE27F88AEB002D32A9 /* FakeSiteScanner.swift */,
);
path = SiteScanner;
sourceTree = "<group>";
};
C4C0E8E527F88B36002D32A9 /* ProxyScanner */ = {
isa = PBXGroup;
children = (
C4C0E8E627F88B41002D32A9 /* ProxyScanner.swift */,
C484437A2804BB560041A78A /* ValetProxyScanner.swift */,
);
path = ProxyScanner;
sourceTree = "<group>";
};
C4C1019727C65A11001FACC2 /* Parsers */ = {
isa = PBXGroup;
children = (
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */,
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
C42CFB1927DFE8BD00862737 /* NginxConfigParserTest.swift */,
C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */,
C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */,
C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */,
C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */,
);
path = Parsers;
sourceTree = "<group>";
@ -739,6 +882,8 @@
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */,
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */,
C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */,
C40FE739282ABB2E00A302C2 /* AppVersionTest.swift */,
C46E206F2829D27F00D909D6 /* AppUpdaterCheckTest.swift */,
);
path = Versions;
sourceTree = "<group>";
@ -849,6 +994,7 @@
C41C1B2F22B0097F00E7CF16 /* Sources */,
C41C1B3022B0097F00E7CF16 /* Frameworks */,
C41C1B3122B0097F00E7CF16 /* Resources */,
C4F5FBCB28216985001065C5 /* Run `swiftlint` */,
);
buildRules = (
);
@ -949,19 +1095,43 @@
files = (
54FCFD27276C883F004CE748 /* SelectPreferenceView.xib in Resources */,
54FCFD2E276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C42CFB1827DFDFDC00862737 /* nicoverbruggen_isolated.test in Resources */,
C42CFB1827DFDFDC00862737 /* nginx-site-isolated.test in Resources */,
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
C4068CA527B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */,
C43A8A2025D9D1D700591B77 /* brew.json in Resources */,
C43A8A2025D9D1D700591B77 /* brew-formula.json in Resources */,
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */,
C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */,
C42F26762805FEE200938AC7 /* nginx-secure-proxy.test in Resources */,
C4F30B08278E195800755FCE /* brew-services.json in Resources */,
C42CFB1627DFDE7900862737 /* nicoverbruggen.test in Resources */,
54A18D40282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test in Resources */,
C42CFB1627DFDE7900862737 /* nginx-site.test in Resources */,
C459B4BD27F6093700E9B4B4 /* nginx-proxy.test in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
C4F5FBCB28216985001065C5 /* Run `swiftlint` */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run `swiftlint`";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C41C1B2F22B0097F00E7CF16 /* Sources */ = {
isa = PBXSourcesBuildPhase;
@ -975,6 +1145,7 @@
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */,
C4C0E8EA27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */,
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
@ -982,27 +1153,32 @@
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */,
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */,
C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */,
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */,
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */,
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
C4D9F24B280B69E100DCD39A /* AddProxyVC.swift in Sources */,
C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */,
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */,
C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */,
C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */,
C41C02A927E61A65009F26CB /* ValetSite+Fake.swift in Sources */,
C4C0E8DF27F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */,
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */,
C40FE737282ABA4F00A302C2 /* AppVersion.swift in Sources */,
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */,
C4B585442770FE3900DA4FBE /* Command.swift in Sources */,
C44067F527E2582B0045BD4E /* SiteListNameCell.swift in Sources */,
C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */,
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */,
C44067F727E258410045BD4E /* SiteListPhpCell.swift in Sources */,
C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */,
C415D3B72770F294005EF286 /* Actions.swift in Sources */,
C4AC51FC27E27F47008528CA /* SiteListKindCell.swift in Sources */,
C4AC51FC27E27F47008528CA /* DomainListKindCell.swift in Sources */,
C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */,
54D9E0B827E4F51E003B9AD9 /* KeyCombo.swift in Sources */,
C4C0E8E727F88B41002D32A9 /* ProxyScanner.swift in Sources */,
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */,
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */,
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
@ -1012,29 +1188,34 @@
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */,
54D9E0B427E4F51E003B9AD9 /* Key.swift in Sources */,
C4C0E8E227F88B13002D32A9 /* ValetSiteScanner.swift in Sources */,
C42F26732805B4B400938AC7 /* DomainListable.swift in Sources */,
5420395F2613607600FB00FA /* Preferences.swift in Sources */,
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
54D9E0B227E4F51E003B9AD9 /* HotKeysController.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */,
C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
C41CA5ED2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */,
C46E206D28299B3800D909D6 /* AppUpdateChecker.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */,
C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */,
C417DC74277614690015E6EE /* Helpers.swift in Sources */,
C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C484437B2804BB560041A78A /* ValetProxyScanner.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */,
C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */,
C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */,
C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */,
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */,
C44067FB27E25FD70045BD4E /* SiteListTLSCell.swift in Sources */,
C44067FB27E25FD70045BD4E /* DomainListTLSCell.swift in Sources */,
C4927F0B27B2DFC200C55AFD /* Errors.swift in Sources */,
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
C4FE011128084FC200D1DE6D /* SelectionVC.swift in Sources */,
C44CCD4027AFE2FC00CE40E5 /* AlertableError.swift in Sources */,
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */,
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
@ -1043,7 +1224,7 @@
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
C4D5CFCA27E0F9CD00035329 /* NginxConfigParser.swift in Sources */,
C4D5CFCA27E0F9CD00035329 /* NginxConfiguration.swift in Sources */,
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */,
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */,
@ -1051,15 +1232,16 @@
54D9E0B627E4F51E003B9AD9 /* HotKey.swift in Sources */,
C4D936C927E3EB6100BD69FE /* PhpHelper.swift in Sources */,
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
C44067F927E2585E0045BD4E /* SiteListTypeCell.swift in Sources */,
C44067F927E2585E0045BD4E /* DomainListTypeCell.swift in Sources */,
54D9E0BA27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */,
C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */,
C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */,
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */,
C42337A3281F19F000459A48 /* Xdebug.swift in Sources */,
C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C41C02A627E60D7A009F26CB /* SiteScanner.swift in Sources */,
C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
C464ADB2275A87CA003FCD53 /* SiteListCellProtocol.swift in Sources */,
C464ADAC275A7A3F003FCD53 /* DomainListWC.swift in Sources */,
C464ADB2275A87CA003FCD53 /* DomainListCellProtocol.swift in Sources */,
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
C493084A279F331F009C240B /* AddSiteVC.swift in Sources */,
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */,
@ -1070,15 +1252,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C449B4F427EE7FC800C47E8A /* SiteListKindCell.swift in Sources */,
C449B4F427EE7FC800C47E8A /* DomainListKindCell.swift in Sources */,
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */,
C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
C41CA5EE2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */,
54D9E0B727E4F51E003B9AD9 /* HotKey.swift in Sources */,
C4205A7F27F4D21800191A39 /* ValetProxy.swift in Sources */,
C42F26742805B4B400938AC7 /* DomainListable.swift in Sources */,
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
C415D3B82770F294005EF286 /* Actions.swift in Sources */,
54B48B60275F66AE006D90C5 /* Application.swift in Sources */,
C4FE011228084FC200D1DE6D /* SelectionVC.swift in Sources */,
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
C493084B279F331F009C240B /* AddSiteVC.swift in Sources */,
C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */,
@ -1087,13 +1272,14 @@
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
54D9E0BB27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */,
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
C4D5CFCB27E0F9CD00035329 /* NginxConfigParser.swift in Sources */,
C4D5CFCB27E0F9CD00035329 /* NginxConfiguration.swift in Sources */,
C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */,
C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */,
C449B4F027EE7FB800C47E8A /* SiteListTLSCell.swift in Sources */,
C4C0E8E827F88B41002D32A9 /* ProxyScanner.swift in Sources */,
C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */,
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */,
C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */,
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */,
C4F319C927B034A500AFF46F /* Stats.swift in Sources */,
@ -1102,27 +1288,31 @@
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
C4C0E8E327F88B13002D32A9 /* ValetSiteScanner.swift in Sources */,
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */,
C4C0E8EB27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */,
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */,
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */,
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */,
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */,
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
C4D936CB27E3EE4A00BD69FE /* SiteListCellProtocol.swift in Sources */,
C4D936CB27E3EE4A00BD69FE /* DomainListCellProtocol.swift in Sources */,
C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */,
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */,
C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
C41E871B2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */,
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */,
C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */,
C40FE738282ABA4F00A302C2 /* AppVersion.swift in Sources */,
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */,
C484437C2804BB560041A78A /* ValetProxyScanner.swift in Sources */,
C4AF9F78275447F100D44ED0 /* ValetConfigurationTest.swift in Sources */,
C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */,
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */,
C417DC75277614690015E6EE /* Helpers.swift in Sources */,
@ -1131,10 +1321,11 @@
54D9E0B327E4F51E003B9AD9 /* HotKeysController.swift in Sources */,
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */,
C46E20702829D27F00D909D6 /* AppUpdaterCheckTest.swift in Sources */,
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */,
C4D936CA27E3EB6100BD69FE /* PhpHelper.swift in Sources */,
C449B4F127EE7FC200C47E8A /* SiteListNameCell.swift in Sources */,
C449B4F127EE7FC200C47E8A /* DomainListNameCell.swift in Sources */,
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
@ -1144,11 +1335,13 @@
C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */,
C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */,
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C449B4F227EE7FC400C47E8A /* SiteListPhpCell.swift in Sources */,
C42CFB1A27DFE8BD00862737 /* NginxConfigParserTest.swift in Sources */,
C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */,
C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */,
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */,
C40B24F227A310770018C7D2 /* Events.swift in Sources */,
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
C4C0E8E027F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */,
C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */,
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
C4B585452770FE3900DA4FBE /* Command.swift in Sources */,
@ -1158,22 +1351,25 @@
C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */,
C4E4404727C56F4700D225E1 /* ValetSite.swift in Sources */,
C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */,
C449B4F327EE7FC600C47E8A /* SiteListTypeCell.swift in Sources */,
C449B4F327EE7FC600C47E8A /* DomainListTypeCell.swift in Sources */,
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C41C02AB27E61CB3009F26CB /* ValetSite+Fake.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
C4D9F24C280B69E100DCD39A /* AddProxyVC.swift in Sources */,
C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */,
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */,
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
C4B585422770FE3900DA4FBE /* Shell.swift in Sources */,
C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
C464ADAD275A7A3F003FCD53 /* DomainListWC.swift in Sources */,
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */,
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */,
C464ADB0275A7A6A003FCD53 /* SiteListVC.swift in Sources */,
C464ADB0275A7A6A003FCD53 /* DomainListVC.swift in Sources */,
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */,
C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */,
C40FE73B282ABB2E00A302C2 /* AppVersionTest.swift in Sources */,
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */,
C4EE55AA27708B9E001DF387 /* PMHeaderView.swift in Sources */,
C46E206E28299B3800D909D6 /* AppUpdateChecker.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1324,7 +1520,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 761;
CURRENT_PROJECT_VERSION = 786;
DEBUG = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
@ -1334,7 +1530,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 5.2.2;
MARKETING_VERSION = 5.3;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1350,7 +1546,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 761;
CURRENT_PROJECT_VERSION = 786;
DEBUG = NO;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
@ -1360,7 +1556,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 5.2.2;
MARKETING_VERSION = 5.3;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

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

View File

@ -149,6 +149,15 @@ This should install `dnsmasq` and set up Valet. Great, almost there!
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
</details>
<details>
<summary><strong>How frequently does PHP Monitor check for updates?</strong></summary>
PHP Monitor will check if an update is available every time you start the app.
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". You can always check for updates manually.
</details>
<details>
<summary><strong>I have PHP Monitor installed, and it works. I want to upgrade my PHP installations to the latest version, what's the best way to do this?</strong></summary>
@ -278,9 +287,13 @@ PHP Monitor is a universal app and supports both architectures, so [find out her
<details>
<summary><strong>Why is the app doing network requests?</strong></summary>
It's Homebrew. I can't prevent `brew` from doing things via the network when I invoke it.
The app will automatically check for updates, which is the most likely culprit.
PHP Monitor itself doesn't do any network requests. Feel free to check the source code or intercept the traffic, if you don't believe me.
This happens at launch (unless disabled), and the app directly checks the Caskfile hosted on GitHub. This data is not, and will not be used for analytics (and, as far as I can tell, cannot).
I also can't prevent `brew` from doing things via the network when PHP Monitor uses the binary.
The app includes an Internet Access Policy file, so if you're using something like Little Snitch there should be a description why these calls occur.
</details>

Binary file not shown.

View File

@ -3,7 +3,7 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
@ -15,11 +15,11 @@ class CommandTest: XCTestCase {
path: Paths.php,
arguments: ["-v"]
)
XCTAssert(version.contains("(cli)"))
XCTAssert(version.contains("NTS"))
XCTAssert(version.contains("built"))
XCTAssert(version.contains("Zend"))
}
}

View File

@ -3,18 +3,18 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class BrewJsonParserTest: XCTestCase {
class HomebrewPackageTest: XCTestCase {
// - MARK: SYNTHETIC TESTS
static var jsonBrewFile: URL {
return Bundle(for: Self.self)
.url(forResource: "brew", withExtension: "json")!
.url(forResource: "brew-formula", withExtension: "json")!
}
func testCanLoadExtensionJson() throws {
@ -22,7 +22,7 @@ class BrewJsonParserTest: XCTestCase {
let package = try! JSONDecoder().decode(
[HomebrewPackage].self, from: json.data(using: .utf8)!
).first!
XCTAssertEqual(package.name, "php")
XCTAssertEqual(package.full_name, "php")
XCTAssertEqual(package.aliases.first!, "php@8.0")
@ -30,23 +30,23 @@ class BrewJsonParserTest: XCTestCase {
installed.version.starts(with: "8.0")
}), true)
}
static var jsonBrewServicesFile: URL {
return Bundle(for: Self.self)
.url(forResource: "brew-services", withExtension: "json")!
}
func testCanParseServicesJson() throws {
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
let services = try! JSONDecoder().decode(
[HomebrewService].self, from: json.data(using: .utf8)!
)
XCTAssertGreaterThan(services.count, 0)
XCTAssertEqual(services.first?.name, "dnsmasq")
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
}
// - MARK: LIVE TESTS
/// This test requires that you have a valid Homebrew installation set up,
@ -63,13 +63,13 @@ class BrewJsonParserTest: XCTestCase {
).filter({ service in
return ["php", "nginx", "dnsmasq"].contains(service.name)
})
XCTAssertTrue(services.contains(where: {$0.name == "php"} ))
XCTAssertTrue(services.contains(where: {$0.name == "nginx"} ))
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} ))
XCTAssertTrue(services.contains(where: {$0.name == "php"}))
XCTAssertTrue(services.contains(where: {$0.name == "nginx"}))
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"}))
XCTAssertEqual(services.count, 3)
}
/// This test requires that you have a valid Homebrew installation set up,
/// and requires the `php` formula to be installed.
/// If this test fails, there is an issue with your Homebrew installation
@ -79,7 +79,7 @@ class BrewJsonParserTest: XCTestCase {
[HomebrewPackage].self,
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
).first!
XCTAssertTrue(package.name == "php")
}
}

View File

@ -1,32 +0,0 @@
//
// NginxConfigParserTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class NginxConfigParserTest: XCTestCase {
static var regularUrl: URL {
return Bundle(for: Self.self).url(forResource: "nicoverbruggen", withExtension: "test")!
}
static var isolatedUrl: URL {
return Bundle(for: Self.self).url(forResource: "nicoverbruggen_isolated", withExtension: "test")!
}
func testCanDetermineIsolation() throws {
XCTAssertNil(
NginxConfigParser(filePath: NginxConfigParserTest.regularUrl.path).isolatedVersion
)
XCTAssertEqual(
"8.1",
NginxConfigParser(filePath: NginxConfigParserTest.isolatedUrl.path).isolatedVersion
)
}
}

View File

@ -0,0 +1,81 @@
//
// NginxConfigurationTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class NginxConfigurationTest: XCTestCase {
// MARK: - Test Files
static var regularUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")!
}
static var isolatedUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")!
}
static var proxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")!
}
static var secureProxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")!
}
static var customTldProxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy-custom-tld", withExtension: "test")!
}
// MARK: - Tests
func testCanDetermineSiteNameAndTld() throws {
XCTAssertEqual(
"nginx-site",
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
)
XCTAssertEqual(
"test",
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld
)
}
func testCanDetermineIsolation() throws {
XCTAssertNil(
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
)
XCTAssertEqual(
"8.1",
NginxConfiguration.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion
)
}
func testCanDetermineProxy() throws {
let proxied = NginxConfiguration.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)!
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual(nil, normal.proxy)
}
func testCanDetermineSecuredProxy() throws {
let proxied = NginxConfiguration.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)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://localhost:8080", proxied.proxy)
}
}

View File

@ -3,30 +3,30 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class ExtensionParserTest: XCTestCase {
class PhpExtensionTest: XCTestCase {
static var phpIniFileUrl: URL {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
}
func testCanLoadExtension() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
XCTAssertGreaterThan(extensions.count, 0)
}
func testExtensionNameIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensionNames = extensions.map { (ext) -> String in
return ext.name
}
// These 6 should be found
XCTAssertTrue(extensionNames.contains("xdebug"))
XCTAssertTrue(extensionNames.contains("imagick"))
@ -34,39 +34,44 @@ class ExtensionParserTest: XCTestCase {
XCTAssertTrue(extensionNames.contains("opcache"))
XCTAssertTrue(extensionNames.contains("yaml"))
XCTAssertTrue(extensionNames.contains("custom"))
XCTAssertFalse(extensionNames.contains("fake"))
XCTAssertFalse(extensionNames.contains("nice"))
}
func testExtensionStatusIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
// xdebug should be enabled
XCTAssertEqual(extensions[0].enabled, true)
// imagick should be disabled
XCTAssertEqual(extensions[1].enabled, false)
}
func testToggleWorksAsExpected() throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.load(from: destination)
XCTAssertEqual(extensions.count, 6)
// Try to disable xdebug (should be detected first)!
let xdebug = extensions.first!
XCTAssertTrue(xdebug.name == "xdebug")
XCTAssertEqual(xdebug.enabled, true)
xdebug.toggle()
XCTAssertEqual(xdebug.enabled, false)
// Check if the file contains the appropriate data
let file = try! String(contentsOf: destination, encoding: .utf8)
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")
}
}

View File

@ -3,20 +3,20 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class ValetConfigParserTest: XCTestCase {
class ValetConfigurationTest: XCTestCase {
static var jsonConfigFileUrl: URL {
return Bundle(for: Self.self).url(
forResource: "valet-config",
withExtension: "json"
)!
}
func testCanLoadConfigFile() throws {
let json = try? String(
contentsOf: Self.jsonConfigFileUrl,
@ -26,7 +26,7 @@ class ValetConfigParserTest: XCTestCase {
Valet.Configuration.self,
from: json!.data(using: .utf8)!
)
XCTAssertEqual(config.tld, "test")
XCTAssertEqual(config.paths, [
"/Users/username/.config/valet/Sites",
@ -35,5 +35,5 @@ class ValetConfigParserTest: XCTestCase {
XCTAssertEqual(config.defaultSite, "/Users/username/default-site")
XCTAssertEqual(config.loopback, "127.0.0.1")
}
}

View File

@ -0,0 +1,81 @@
# valet stub: proxy.valet.conf
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
root /;
charset utf-8;
client_max_body_size 128M;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://127.0.0.1:90;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}
server {
listen 127.0.0.1:60;
#listen 127.0.0.1:60; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
root /;
charset utf-8;
client_max_body_size 128M;
add_header X-Robots-Tag 'noindex, nofollow, nosnippet, noarchive';
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://127.0.0.1:90;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -0,0 +1,57 @@
# valet stub: secure.proxy.valet.conf
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:443 ssl http2;
#listen 127.0.0.1:443 ssl http2; # valet loopback
server_name live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com;
root /;
charset utf-8;
client_max_body_size 128M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.crt";
ssl_certificate_key "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.key";
access_log off;
error_log "/Users/phpmon/.config/valet/Log/live.whatagraph.dev.com-error.log";
error_page 404 "/Users/phpmon/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -0,0 +1,57 @@
# valet stub: secure.proxy.valet.conf
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:443 ssl http2;
#listen 127.0.0.1:443 ssl http2; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
root /;
charset utf-8;
client_max_body_size 128M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.crt";
ssl_certificate_key "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.key";
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://127.0.0.1:90;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -3,18 +3,18 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class Utility {
public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? {
if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) {
let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)")
do {
try FileManager.default.copyItem(at: bundleURL, to: targetURL)
return targetURL
@ -22,7 +22,7 @@ class Utility {
Log.err("Unable to copy file: \(error)")
}
}
return nil
}
}

View File

@ -0,0 +1,21 @@
//
// AppUpdaterCheckTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 10/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class AppUpdaterCheckTest: XCTestCase {
func testCanRetrieveVersionFromCask() {
let caskVersion = AppUpdateChecker.retrieveVersionFromCask()
let version = VersionExtractor.from(caskVersion)
XCTAssertNotNil(version)
}
}

View File

@ -0,0 +1,62 @@
//
// AppVersionTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class AppVersionTest: XCTestCase {
func testCanRetrieveInternalAppVersion() {
XCTAssertNotNil(AppVersion.fromCurrentVersion())
}
func testCanParseNormalVersionString() {
let version = AppVersion.from("1.0.0")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual(nil, version?.build)
XCTAssertEqual(nil, version?.suffix)
}
func testCanParseCaskVersionString() {
let version = AppVersion.from("1.0.0_600")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual("600", version?.build)
XCTAssertEqual(nil, version?.suffix)
}
func testCanParseDevVersionStringWithoutBuildNumber() {
let version = AppVersion.from("1.0.0-dev")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual(nil, version?.build)
XCTAssertEqual("dev", version?.suffix)
}
func testCanParseDevVersionStringWithBuildNumber() {
let version = AppVersion.from("1.0.0-dev,870")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual("870", version?.build)
XCTAssertEqual("dev", version?.suffix)
}
func testCanParseUnderscoresAsBuildSeparatorToo() {
let version = AppVersion.from("1.0.0-dev_870")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual("870", version?.build)
XCTAssertEqual("dev", version?.suffix)
}
}

View File

@ -3,7 +3,7 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 01/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
@ -23,7 +23,7 @@ class PhpVersionDetectionTest: XCTestCase {
"php@5.6",
"php@5.4" // should be omitted, not supported
], checkBinaries: false, generateHelpers: false)
XCTAssertEqual(outcome, ["8.0", "7.0"])
}
}

View File

@ -36,13 +36,13 @@ class PhpVersionNumberTest: XCTestCase {
nil
)
}
func testPhpVersionNumberParse() throws {
XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
XCTAssertTrue(error is VersionParseError)
}
}
func testCanCheckFixedConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
@ -51,7 +51,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"])
@ -59,7 +59,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.0.3"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -67,7 +67,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -76,7 +76,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: []).all
)
}
func testCanCheckCaretConstraints() throws {
// 1. Imprecise checks
XCTAssertEqual(
@ -86,7 +86,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
// 2. Imprecise check with precise constraint (lenient AKA not strict)
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual(
@ -96,7 +96,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
// 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual(
@ -106,7 +106,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
)
// 4. Precise members and constraint all around
XCTAssertEqual(
PhpVersionNumberCollection
@ -115,7 +115,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
// 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0
XCTAssertEqual(
@ -125,7 +125,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
// 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal
XCTAssertEqual(
@ -136,7 +136,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
}
func testCanCheckTildeConstraints() throws {
// 1. Imprecise checks
XCTAssertEqual(
@ -146,7 +146,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
// 2. Imprecise check with precise constraint (lenient AKA not strict)
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual(
@ -159,7 +159,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.0"]).all
)
// 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual(
@ -172,7 +172,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: []).all
)
// 4. Precise members and constraint all around
XCTAssertEqual(
PhpVersionNumberCollection
@ -183,7 +183,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.0.10"]).all
)
// 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0.
XCTAssertEqual(
@ -193,7 +193,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
// 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal.
// (Strictness does not make any difference here, but both should be tested.)
@ -205,7 +205,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
)
}
func testCanCheckGreaterThanOrEqualConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
@ -214,7 +214,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -222,7 +222,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
)
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual(
PhpVersionNumberCollection
@ -231,7 +231,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all
)
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
XCTAssertEqual(
PhpVersionNumberCollection
@ -241,7 +241,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4", "7.3", "7.2"]).all
)
}
func testCanCheckGreaterThanConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
@ -250,7 +250,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -259,7 +259,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -268,7 +268,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
@ -277,7 +277,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2"]).all
)
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])

View File

@ -3,7 +3,7 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
@ -14,5 +14,5 @@ class ValetVersionExtractorTest: XCTestCase {
let version = valet("--version", sudo: false)
XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3"))
}
}

View File

@ -3,7 +3,7 @@
// phpmon-tests
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
@ -14,12 +14,12 @@ class VersionExtractorTest: XCTestCase {
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
}
func testVersionComparison() {
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)
XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending)
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -2,49 +2,44 @@
// Services.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class Actions {
// MARK: - Services
public static func restartPhpFpm()
{
public static func restartPhpFpm() {
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
}
public static func restartNginx()
{
public static func restartNginx() {
brew("services restart nginx", sudo: true)
}
public static func restartDnsMasq()
{
public static func restartDnsMasq() {
brew("services restart dnsmasq", sudo: true)
}
public static func stopAllServices()
{
public static func stopAllServices() {
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true)
}
public static func fixHomebrewPermissions() throws
{
public static func fixHomebrewPermissions() throws {
var servicesCommands = [
"\(Paths.brew) services stop nginx",
"\(Paths.brew) services stop dnsmasq",
"\(Paths.brew) services stop dnsmasq"
]
var cellarCommands = [
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq"
]
PhpEnv.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnv.brewPhpVersion
? "php"
@ -52,66 +47,61 @@ class Actions {
servicesCommands.append("\(Paths.brew) services stop \(formula)")
cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
}
let script =
servicesCommands.joined(separator: " && ")
+ " && "
+ cellarCommands.joined(separator: " && ")
let appleScript = NSAppleScript(
source: "do shell script \"\(script)\" with administrator privileges"
)
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
if (eventResult == nil) {
if eventResult == nil {
throw HomebrewPermissionError(kind: .applescriptNilError)
}
}
// MARK: - Finding Config Files
public static func openGenericPhpConfigFolder()
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
public static func openGenericPhpConfigFolder() {
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder()
{
public static func openGlobalComposerFolder() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpConfigFolder(version: String)
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
public static func openPhpConfigFolder(version: String) {
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openValetConfigFolder()
{
public static func openValetConfigFolder() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL
{
public static func createTempPhpInfoFile() -> URL {
// Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
// Tell php-cgi to run the PHP and output as an .html file
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
}
// MARK: - Fix My Valet
/**
Detects all currently available PHP versions,
and unlinks each and every one of them.
@ -124,8 +114,7 @@ class Actions {
If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`.
*/
public static func fixMyValet(completed: @escaping () -> Void)
{
public static func fixMyValet(completed: @escaping () -> Void) {
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true)

View File

@ -2,13 +2,13 @@
// Command.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
public class Command {
/**
Immediately executes a command.
@ -20,21 +20,21 @@ public class Command {
let task = Process()
task.launchPath = path
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
if (trimNewlines) {
if trimNewlines {
return output.components(separatedBy: .newlines)
.filter({ !$0.isEmpty })
.joined(separator: "\n")
}
return output
}
}

View File

@ -2,19 +2,19 @@
// Constants.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
struct Constants {
/**
* The latest PHP version that is considered to be stable at the time of release.
* This version number is currently not used (only as a default fallback).
*/
static let LatestStablePhpVersion = "8.1"
/**
The minimum version of Valet that is recommended.
If the installed version is older, a notification will be shown
@ -24,7 +24,7 @@ struct Constants {
See also: https://github.com/laravel/valet/releases/tag/v2.16.2
*/
static let MinimumRecommendedValetVersion = "2.16.2"
/**
* The PHP versions supported by this application.
* Versions that do not appear in this array are omitted from the list.
@ -42,7 +42,7 @@ struct Constants {
"7.4",
"8.0",
"8.1",
// ====================
// EXPERIMENTAL SUPPORT
// ====================
@ -50,19 +50,33 @@ struct Constants {
// dev release. In this case, that means that the version below is detected.
"8.2"
]
struct Urls {
static let DonationPayment = URL(
string: "https://nicoverbruggen.be/sponsor#pay-now"
)!
static let DonationPage = URL(
string: "https://nicoverbruggen.be/sponsor"
)!
static let FrequentlyAskedQuestions = URL(
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
)!
static let GitHubReleases = URL(
string: "https://github.com/nicoverbruggen/phpmon/releases"
)!
static let StableBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb"
)!
static let DevBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
)!
}
}

View File

@ -9,7 +9,7 @@
import Foundation
class Events {
static let ServicesUpdated = Notification.Name("ServicesUpdated")
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
// MARK: Common Shell Commands
@ -11,28 +11,25 @@
/**
Runs a `valet` command. Defaults to running as superuser.
*/
func valet(_ command: String, sudo: Bool = true) -> String
{
func valet(_ command: String, sudo: Bool = true) -> String {
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
}
/**
Runs a `brew` command. Can run as superuser.
*/
func brew(_ command: String, sudo: Bool = false)
{
func brew(_ command: String, sudo: Bool = false) {
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
}
/**
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
*/
func sed(file: String, original: String, replacement: String)
{
func sed(file: String, original: String, replacement: String) {
// Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
// Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension
if Filesystem.fileExists("\(Paths.binPath)/gsed") {
@ -45,8 +42,7 @@ func sed(file: String, original: String, replacement: String)
/**
Uses `grep` to determine whether a particular query string can be found in a particular file.
*/
func grepContains(file: String, query: String) -> Bool
{
func grepContains(file: String, query: String) -> Bool {
return Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""")

View File

@ -3,56 +3,56 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class Log {
static var shared = Log()
enum Verbosity: Int {
case error = 1,
warning = 2,
info = 3,
performance = 4
public func isApplicable() -> Bool {
return Log.shared.verbosity.rawValue >= self.rawValue
}
}
var verbosity: Verbosity = .warning
static func err(_ item: Any) {
if Verbosity.error.isApplicable() {
print("[E] \(item)")
}
}
static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() {
print("[W] \(item)")
}
}
static func info(_ item: Any) {
if Verbosity.info.isApplicable() {
print("\(item)")
}
}
static func perf(_ item: Any) {
if Verbosity.performance.isApplicable() {
print("[P] \(item)")
}
}
static func separator(as verbosity: Verbosity = .info) {
if verbosity.isApplicable() {
print("==================================")
}
}
}

View File

@ -2,7 +2,7 @@
// Paths.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -12,71 +12,71 @@ import Foundation
The path to the Homebrew directory and the user's name are fetched only once, at boot.
*/
public class Paths {
public static let shared = Paths()
internal var baseDir: Paths.HomebrewDir
private var userName: String
init() {
baseDir = App.architecture != "x86_64" ? .opt : .usr
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
}
public func detectBinaryPaths() {
detectComposerBinary()
}
// - MARK: Binaries
public static var valet: String {
return "\(binPath)/valet"
}
public static var brew: String {
return "\(binPath)/brew"
}
public static var php: String {
return "\(binPath)/php"
}
public static var phpConfig: String {
return "\(binPath)/php-config"
}
// - MARK: Detected Binaries
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
public static var composer: String? = nil
public static var composer: String?
// - MARK: Paths
public static var whoami: String {
return shared.userName
}
public static var cellarPath: String {
return "\(shared.baseDir.rawValue)/Cellar"
}
public static var binPath: String {
return "\(shared.baseDir.rawValue)/bin"
}
public static var optPath: String {
return "\(shared.baseDir.rawValue)/opt"
}
public static var etcPath: String {
return "\(shared.baseDir.rawValue)/etc"
}
// MARK: - Flexible Binaries
// (these can be in multiple locations, so we scan common places because)
// (PHP Monitor will not use the user's own PATH)
private func detectComposerBinary() {
if Filesystem.fileExists("/usr/local/bin/composer") {
Paths.composer = "/usr/local/bin/composer"
@ -87,12 +87,12 @@ public class Paths {
Log.warn("Composer was not found.")
}
}
// MARK: - Enum
public enum HomebrewDir: String {
case opt = "/opt/homebrew"
case usr = "/usr/local"
}
}

View File

@ -9,7 +9,7 @@
import Foundation
extension Process {
/**
When a process is running in the background, it can send content to standard
output or standard error, just like it would in a terminal. Using `listen`
@ -22,10 +22,10 @@ extension Process {
) {
let outputPipe = Pipe()
let errorPipe = Pipe()
self.standardOutput = outputPipe
self.standardError = errorPipe
[
(outputPipe, didReceiveStandardOutputData),
(errorPipe, didReceiveStandardErrorData)
@ -35,15 +35,18 @@ extension Process {
forName: NSNotification.Name.NSFileHandleDataAvailable,
object: pipe.fileHandleForReading,
queue: nil
) { notification in
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
) { _ in
if let outputString = String(
data: pipe.fileHandleForReading.availableData,
encoding: String.Encoding.utf8
) {
callback(outputString)
}
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
}
}
}
/**
After the process is done running, you'll want to stop listening.
*/
@ -55,5 +58,5 @@ extension Process {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
}
}

View File

@ -2,41 +2,41 @@
// Shell.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
public class Shell {
// MARK: - Invoke static functions
public static func run(
_ command: String,
requiresPath: Bool = false
) {
Shell.user.run(command, requiresPath: requiresPath)
}
public static func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
return Shell.user.pipe(command, requiresPath: requiresPath)
}
// MARK: - Singleton
/**
We now require macOS 11, so no need to detect which terminal to use.
*/
public var shell: String = "/bin/sh"
/**
Singleton to access a user shell (with --login)
*/
public static let user = Shell()
/**
Runs a shell command without using the output.
Uses the default shell.
@ -51,7 +51,7 @@ public class Shell {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = Shell.pipe(command, requiresPath: requiresPath)
}
/**
Runs a shell command and returns the output.
@ -69,7 +69,7 @@ public class Shell {
)
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
}
/**
Runs the command and returns a `ShellOutput` object, which contains info about the process.
@ -81,17 +81,17 @@ public class Shell {
_ command: String,
requiresPath: Bool = false
) -> Shell.Output {
let outputPipe = Pipe()
let errorPipe = Pipe()
let task = self.createTask(for: command, requiresPath: requiresPath)
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
return Shell.Output(
let output = Shell.Output(
standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
@ -102,8 +102,14 @@ public class Shell {
)!,
task: task
)
if CommandLine.arguments.contains("--v") {
log(task: task, output: output)
}
return output
}
/**
Creates a new process with the correct PATH and shell.
*/
@ -111,19 +117,36 @@ public class Shell {
let tailoredCommand = requiresPath
? "export PATH=\(Paths.binPath):$PATH && \(command)"
: command
let task = Process()
task.launchPath = self.shell
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
return task
}
/**
Verbose logging for PHP Monitor's synchronous shell output.
*/
private func log(task: Process, output: Output) {
Log.info("")
Log.info("==== COMMAND ====")
Log.info("")
Log.info("\(self.shell) \(task.arguments?.joined(separator: " ") ?? "")")
Log.info("")
Log.info("==== OUTPUT ====")
Log.info("")
dump(output)
Log.info("")
Log.info("==== END OUTPUT ====")
Log.info("")
}
public class Output {
public let standardOutput: String
public let errorOutput: String
public let task: Process
init(standardOutput: String,
errorOutput: String,
task: Process) {

View File

@ -15,9 +15,9 @@ struct HomebrewPermissionError: Error, AlertableError {
enum Kind: String {
case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
}
let kind: Kind
func getErrorMessageKey() -> String {
return "alert.errors.\(self.kind.rawValue)"
}

View File

@ -2,17 +2,17 @@
// Date.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension Date {
func toString() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.string(from: self)
}
}

View File

@ -3,27 +3,27 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 14/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension NSMenu {
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
newItem.keyEquivalentModifierMask = modifier
self.addItem(newItem)
}
}
@IBDesignable class LocalizedMenuItem: NSMenuItem {
@IBInspectable
var localizationKey: String? {
didSet {
self.title = localizationKey?.localized ?? self.title
}
}
}

View File

@ -10,29 +10,29 @@ import Foundation
import Cocoa
extension NSWindow {
/**
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
*/
func shake(){
func shake() {
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
let frame: CGRect = self.frame
let shakeAnimation :CAKeyframeAnimation = CAKeyframeAnimation()
let shakeAnimation: CAKeyframeAnimation = CAKeyframeAnimation()
let shakePath = CGMutablePath()
shakePath.move( to: CGPoint(x:NSMinX(frame), y:NSMinY(frame)))
shakePath.move( to: CGPoint(x: frame.minX, y: frame.minY))
for _ in 0...numberOfShakes-1 {
shakePath.addLine(to: CGPoint(x:NSMinX(frame) - frame.size.width * vigourOfShake, y:NSMinY(frame)))
shakePath.addLine(to: CGPoint(x:NSMinX(frame) + frame.size.width * vigourOfShake, y:NSMinY(frame)))
shakePath.addLine(to: CGPoint(x: frame.minX - frame.size.width * vigourOfShake, y: frame.minY))
shakePath.addLine(to: CGPoint(x: frame.minX + frame.size.width * vigourOfShake, y: frame.minY))
}
shakePath.closeSubpath()
shakeAnimation.path = shakePath
shakeAnimation.duration = durationOfShake
self.animations = ["frameOrigin":shakeAnimation]
self.animations = ["frameOrigin": shakeAnimation]
self.animator().setFrameOrigin(self.frame.origin)
}
}

View File

@ -2,33 +2,33 @@
// StringExtension.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension String {
var localized: String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}
func localized(_ args: CVarArg...) -> String {
String(format: self.localized, arguments: args)
}
func countInstances(of stringToFind: String) -> Int {
if (stringToFind.isEmpty) {
if stringToFind.isEmpty {
return 0
}
var count = 0
var searchRange: Range<String.Index>?
while let foundRange = range(of: stringToFind, options: [], range: searchRange) {
count += 1
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
}
return count
}
@ -37,7 +37,7 @@ extension String {
let end = r.upperBound
return String(self[start ..< end])
}
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
/*
<1> We split the version by period (.).
@ -50,12 +50,12 @@ extension String {
*/
func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "."
var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
if zeroDiff == 0 { // <3>
// Same format, compare normally
return self.compare(otherVersion, options: .numeric)
@ -70,5 +70,5 @@ extension String {
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
}
}
}

View File

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

View File

@ -2,13 +2,13 @@
// Alert.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Alert {
public static func confirm(
onWindow window: NSWindow,
messageText: String,
@ -21,13 +21,13 @@ class Alert {
if !Thread.isMainThread {
fatalError("You should always present alerts on the main thread!")
}
let alert = NSAlert.init()
alert.alertStyle = style
alert.messageText = messageText
alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) {
if !secondButtonTitle.isEmpty {
alert.addButton(withTitle: secondButtonTitle)
}
alert.beginSheetModal(for: window) { response in
@ -36,5 +36,5 @@ class Alert {
}
}
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -12,23 +12,23 @@ import Foundation
/// In most cases this is going to be a code editor, but it could also be another application
/// that supports opening those directories, like a visual Git client or a terminal app.
class Application {
enum AppType {
case editor, browser, git_gui, terminal, user_supplied
}
/// Name of the app. Used for display purposes and to determine `name.app` exists.
let name: String
/// Application type. Depending on the type, a different action might occur.
let type: AppType
/// Initializer. Used to detect a specific app of a specific type.
init(_ name: String, _ type: AppType) {
self.name = name
self.type = type
}
/**
Attempt to open a specific directory in the app of choice.
(This will open the app if it isn't open yet.)
@ -36,7 +36,7 @@ class Application {
@objc public func openDirectory(file: String) {
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
}
/** Checks if the app is installed. */
func isInstalled() -> Bool {
// If this script does not complain, the app exists!
@ -45,7 +45,7 @@ class Application {
requiresPath: false
).task.terminationStatus == 0
}
/**
Detect which apps are available to open a specific directory.
*/

View File

@ -3,13 +3,13 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Filesystem {
/**
Checks if a file exists at the provided path.
Uses `FileManager`.
@ -19,5 +19,5 @@ class Filesystem {
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
)
}
}

View File

@ -2,26 +2,26 @@
// LocalNotification.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import UserNotifications
class LocalNotification {
public static func send(title: String, subtitle: String) {
let content = UNMutableNotificationContent()
content.title = title
content.body = subtitle
let uuidString = UUID().uuidString
let request = UNNotificationRequest(
identifier: uuidString,
content: content,
trigger: nil
)
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) { (error) in
if error != nil {
@ -29,5 +29,5 @@ class LocalNotification {
}
}
}
}

View File

@ -2,46 +2,46 @@
// ImageGenerator.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
class MenuBarImageGenerator {
/**
Takes a string and converts it to an image that can be displayed in the menu bar.
The width of the NSImage depends on the length of the text.
*/
public static func textToImage(text: String) -> NSImage {
let font = NSFont.systemFont(ofSize: 14, weight: .medium)
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: NSColor.black,
NSAttributedString.Key.paragraphStyle: textStyle
]
let padding : CGFloat = 2.0;
let padding: CGFloat = 2.0
// Create an attributed string so we'll know how wide the item will need to be
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
let textSize = attributedString.size()
// Add padding to the width of the menu bar item
let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height)
let image = NSImage(size: size)
// Set the image rect with the appropriate dimensions
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
// Position the text inside the image rect
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
let targetImage: NSImage = NSImage(size: image.size)
let representation: NSBitmapImageRep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(image.size.width),
@ -54,40 +54,40 @@ class MenuBarImageGenerator {
bytesPerRow: 0,
bitsPerPixel: 0
)!
targetImage.addRepresentation(representation)
targetImage.lockFocus()
image.draw(in: imageRect)
text.draw(in: textRect, withAttributes: textFontAttributes)
targetImage.unlockFocus()
return targetImage
}
/**
The same as before, but also attempts to add an icon to the left.
*/
public static func textToImageWithIcon(text: String) -> NSImage {
// We'll start out with the image containing the text
let textImage = self.textToImage(text: text)
// Then we'll fetch the image we want on the left
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
if iconType == nil {
Log.warn("Invalid icon type found, using the default")
iconType = MenuBarIcon.iconPhp.rawValue
}
let iconImage = NSImage(named: "MenuBar_\(iconType!)")!
// We'll need to reference the width of the icon a bunch of times
let iconWidthSize = iconImage.size.width
// There will also be an additional divider between the image and the text (image)
let divider: CGFloat = 3
// Use a fixed size for the height of the menu bar (18pt)
let imageRect = CGRect(
x: 0,
@ -95,14 +95,14 @@ class MenuBarImageGenerator {
width: textImage.size.width + iconWidthSize + divider,
height: 18
)
// Create a new image, we'll draw the text and our icon in there
let image: NSImage = NSImage(size: imageRect.size)
image.lockFocus()
// Calculate the offset between the image and the text
let offset = imageRect.size.width - textImage.size.width
// Draw the text with a negative x offset (so there is room on the left for the icon)
textImage.draw(
in: imageRect,
@ -115,7 +115,7 @@ class MenuBarImageGenerator {
operation: .overlay,
fraction: 1
)
// Draw the icon directly in the left of the imageRect (where we left space)
iconImage.draw(
in: imageRect,
@ -128,11 +128,11 @@ class MenuBarImageGenerator {
operation: .overlay,
fraction: 1
)
// We're done with this image
image.unlockFocus()
return image
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -15,32 +15,32 @@ import Cocoa
- Note: This class does make a simple assumption: each window controller corresponds to a single view.
*/
class PMWindowController: NSWindowController, NSWindowDelegate {
public var windowName: String {
fatalError("Please specify a window name")
}
override func showWindow(_ sender: Any?) {
super.showWindow(sender)
App.shared.register(window: windowName)
}
func windowWillClose(_ notification: Notification) {
App.shared.remove(window: windowName)
}
deinit {
Log.perf("Window controller '\(windowName)' was deinitialized")
}
}
extension NSWindowController {
public func positionWindowInTopLeftCorner() {
guard let frame = NSScreen.main?.frame else { return }
guard let window = self.window else { return }
window.setFrame(NSRect(
x: frame.size.width - window.frame.size.width - 20,
y: frame.size.height - window.frame.size.height - 40,
@ -48,5 +48,5 @@ extension NSWindowController {
height: window.frame.height
), display: true)
}
}

View File

@ -3,13 +3,13 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class VersionExtractor {
/**
This attempts to extract the version number from any given string.
*/
@ -19,26 +19,26 @@ class VersionExtractor {
pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
options: []
)
let match = regex.matches(
in: string,
options: [],
range: NSMakeRange(0, string.count)
range: NSRange(location: 0, length: string.count)
).first
guard let match = match else {
return nil
}
let range = Range(
match.range(withName: "version"),
in: string
)!
return String(string[range])
} catch {
return nil
}
}
}

View File

@ -2,7 +2,7 @@
// ActivePhpInstallation.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -21,78 +21,78 @@ class ActivePhpInstallation {
var version: Version!
var limits: Limits!
var extensions: [PhpExtension]!
// MARK: - Computed
var formula: String {
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
}
// MARK: - Initializer
init() {
// Show information about the current version
getVersion()
// If an error occurred, exit early
if (version.error) {
if version.error {
limits = Limits()
extensions = []
return
}
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path)
// Get configuration values
limits = Limits(
memory_limit: getByteCount(key: "memory_limit"),
upload_max_filesize: getByteCount(key: "upload_max_filesize"),
post_max_size: getByteCount(key: "post_max_size")
)
// Return a list of .ini files parsed after php.ini
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
.replacingOccurrences(of: "\n", with: "")
.split(separator: ",")
.map { String($0) }
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if exts.count > 0 {
extensions.append(contentsOf: exts)
let loadedExtensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if !loadedExtensions.isEmpty {
extensions.append(contentsOf: loadedExtensions)
}
}
}
/**
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
*/
private func getVersion() -> Void {
private func getVersion() {
self.version = Version()
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
if (version == "" || version.contains("Warning") || version.contains("Error")) {
if version == "" || version.contains("Warning") || version.contains("Error") {
self.version.short = "💩 BROKEN"
self.version.long = ""
self.version.error = true
return
}
// That's the long version
self.version.long = version
// Next up, let's strip away the minor version number
let segments = self.version.long.components(separatedBy: ".")
// Get the first two elements
self.version.short = segments[0...1].joined(separator: ".")
}
/**
Retrieves the display value for a specific key in the `.ini` file.
@ -110,18 +110,18 @@ class ActivePhpInstallation {
*/
private func getByteCount(key: String) -> String {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
// Check if the value is unlimited
if (value == "-1") {
if value == "-1" {
return ""
}
// Check if the syntax is valid otherwise
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
let match = regex.matches(in: value, options: [], range: NSRange(location: 0, length: value.count)).first
return (match == nil) ? "⚠️" : "\(value)B"
}
/**
Determine if PHP-FPM is configured correctly.
@ -135,11 +135,11 @@ class ActivePhpInstallation {
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
return Shell.pipe("cat \(fileName)").contains("valet.sock")
}
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
return Filesystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
}
// MARK: - Structs
/**
@ -153,7 +153,7 @@ class ActivePhpInstallation {
var long = "???"
var error = false
}
/**
Struct containing information about the limits of the current PHP installation.
Includes: memory limit, max upload size and max post size.
@ -163,5 +163,5 @@ class ActivePhpInstallation {
var upload_max_filesize = "???"
var post_max_size = "???"
}
}

View File

@ -0,0 +1,33 @@
//
// Xdebug.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class Xdebug {
public static var enabled: Bool {
return !self.mode.isEmpty
}
public static var mode: String {
return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
}
public static var modes: [String] {
return [
"off",
"develop",
"coverage",
"debug",
"gcstats",
"profile",
"trace"
]
}
}

View File

@ -2,24 +2,24 @@
// HomebrewPackage.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
struct HomebrewPackage: Decodable {
let name: String
let full_name: String
let aliases: [String]
let installed: [HomebrewInstalled]
let linked_keg: String?
public var version: String {
return aliases.first!
.replacingOccurrences(of: "php@", with: "")
}
}
struct HomebrewInstalled: Decodable {

View File

@ -18,7 +18,7 @@ struct HomebrewService: Decodable, Equatable {
let status: String?
let log_path: String?
let error_log_path: String?
public static func loadAll(
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
completion: @escaping ([HomebrewService]) -> Void
@ -27,11 +27,11 @@ struct HomebrewService: Decodable, Equatable {
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)
}
}

View File

@ -3,48 +3,48 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpEnv {
// MARK: - Initializer
init() {
self.currentInstall = ActivePhpInstallation()
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json")
self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
}
// MARK: - Properties
/** The delegate that is informed of updates. */
weak var delegate: PhpSwitcherDelegate?
/** The static app instance. Accessible at any time. */
static let shared = PhpEnv()
/** Whether the switcher is busy performing any actions. */
var isBusy: Bool = false
/** All available versions of PHP. */
var availablePhpVersions: [String] = []
/** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */
var currentInstall: ActivePhpInstallation
/**
The version that the `php` formula via Brew is aliased to on the current system.
@ -57,63 +57,62 @@ class PhpEnv {
static var brewPhpVersion: String {
return Self.shared.homebrewPackage.version
}
/**
The currently linked and active PHP installation.
*/
static var phpInstall: ActivePhpInstallation {
return Self.shared.currentInstall
}
/**
Information we were able to discern from the Homebrew info command.
*/
var homebrewPackage: HomebrewPackage! = nil
// MARK: - Methods
public static var switcher: PhpSwitcher {
return InternalSwitcher()
}
public static func detectPhpVersions() -> Void {
public static func detectPhpVersions() {
_ = Self.shared.detectPhpVersions()
}
/**
Detects which versions of PHP are installed.
*/
public func detectPhpVersions() -> [String]
{
public func detectPhpVersions() -> [String] {
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed
let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate
if (!versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php")) {
if !versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php") {
versionsOnly.append(phpAlias)
}
Log.info("The PHP versions that were detected are: \(versionsOnly)")
availablePhpVersions = versionsOnly
var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version)
}
cachedPhpInstallations = mappedVersions
return versionsOnly
}
/**
Extracts valid PHP versions from an array of strings.
This array of strings is usually retrieved from `grep`.
@ -126,14 +125,14 @@ class PhpEnv {
checkBinaries: Bool = true,
generateHelpers: Bool = true
) -> [String] {
var output : [String] = []
var output: [String] = []
var supported = Constants.SupportedPhpVersions
if !Valet.enabled(feature: .supportForPhp56) {
supported.removeAll { $0 == "5.6" }
}
versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected)
@ -144,19 +143,18 @@ class PhpEnv {
// is supported and where the binary exists (avoids broken installs)
if !output.contains(version)
&& supported.contains(version)
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
{
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
output.append(version)
}
}
if generateHelpers {
output.forEach { PhpHelper.generate(for: $0) }
}
return output
}
public func validVersions(for constraint: String) -> [PhpVersionNumber] {
constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection
@ -164,7 +162,7 @@ class PhpEnv {
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
/**
Validates whether the currently running version matches the provided version.
*/
@ -173,7 +171,7 @@ class PhpEnv {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
return true
}
return false
}
}

View File

@ -9,27 +9,28 @@
import Foundation
class PhpHelper {
static let keyPhrase = "This file was automatically generated by PHP Monitor."
public static func generate(for version: String) {
// Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "")
do {
let destination = "/usr/local/bin/pm\(dotless)"
if FileManager.default.fileExists(atPath: destination) {
let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) {
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor (or is unreadable). Not updating this file.")
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
+ "(or is unreadable). Not updating this file.")
return
}
}
// Let's follow the symlink to the PHP binary folder
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path
// The contents of the script!
let script = """
#!/bin/zsh
@ -41,14 +42,14 @@ class PhpHelper {
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH
"""
// Write to the destination
try script.write(
to: URL(fileURLWithPath: destination),
atomically: true,
encoding: String.Encoding.utf8
)
// Make sure the file is executable
Shell.run("chmod +x \(destination)")
} catch {
@ -56,5 +57,5 @@ class PhpHelper {
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
}
}
}

View File

@ -10,21 +10,21 @@ import Foundation
public struct PhpVersionNumberCollection: Equatable {
let versions: [PhpVersionNumber]
public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection(
versions: versions.map { try! PhpVersionNumber.parse($0) }
)
}
public var first: PhpVersionNumber? {
return self.versions.first
}
public var all: [PhpVersionNumber] {
return self.versions
}
/**
Checks if any versions of PHP are valid for the constraint provided.
Due to the complexity of evaluating these, a important test is maintained.
@ -61,13 +61,13 @@ public struct PhpVersionNumberCollection: Equatable {
// Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter { $0.isSameAs(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
// Caret range means that the major version is never higher but minor version can be higher
// ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis.
return self.versions.filter {
@ -78,11 +78,11 @@ public struct PhpVersionNumberCollection: Equatable {
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
}
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) }
}
@ -95,47 +95,52 @@ public struct PhpVersionNumber: Equatable {
let major: Int
let minor: Int
let patch: Int?
public func toString() -> String {
return self.patch == nil
? "\(major).\(minor)"
: "\(major).\(minor).\(patch!)"
}
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
}
public var homebrewVersion: String {
return "\(major).\(minor)"
}
public enum MatchType: String {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
// TODO: (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 {
guard let versionText = VersionExtractor.from(text) else {
throw VersionParseError()
}
return Self.make(from: versionText)!
}
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
let match = regex.matches(
in: versionString,
options: [],
range: NSRange(location: 0, length: versionString.count)
).first
if match != nil {
let major = Int(
versionString[Range(match!.range(withName: "major"), in: versionString)!]
@ -143,24 +148,24 @@ public struct PhpVersionNumber: Equatable {
let minor = Int(
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
)!
var patch: Int? = nil
var patch: Int?
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
patch = Int(versionString[minorRange])
}
return Self(major: major, minor: minor, patch: patch)
}
return nil
}
// MARK: Comparison Logic
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
}
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return (
self.major > version.major ||
@ -169,7 +174,7 @@ public struct PhpVersionNumber: Equatable {
&& self.patch(strict) > version.patch(strict)
)
}
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major &&
(
@ -177,12 +182,12 @@ public struct PhpVersionNumber: Equatable {
|| self.minor > version.minor
)
}
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict)
}
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor >= version.minor

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 31/01/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -16,24 +16,26 @@ import Foundation
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
*/
class PhpExtension {
/// The file where this extension was located.
var file: String
/// The original string that was used to determine this extension is active.
var line: String
/// The name of the extension. This is always identical to the name found in the original string. If you want to display this name, capitalize this.
/// The name of the extension. This is always identical to the name found in the original string.
/// If you want to display this name, capitalize this.
var name: String
/// Whether the extension has been enabled.
var enabled: Bool
/// The file where this extension was located, but only the filename, not the full path to the .ini file.
var fileNameOnly: String {
return String(file.split(separator: "/").last ?? "php.ini")
}
// swiftlint:disable line_length
/**
This regular expression will allow us to identify lines which activate an extension.
@ -47,29 +49,31 @@ class PhpExtension {
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
*/
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
// swiftlint:enable line_length
/**
When registering an extension, we do that based on the line found inside the .ini file.
*/
init(_ line: String, file: String) {
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
let match = regex.matches(in: line, options: [], range: NSMakeRange(0, line.count)).first
let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
let range = Range(match!.range(withName: "name"), in: line)!
self.line = line
let fullPath = String(line[range])
.replacingOccurrences(of: "\"", with: "") // replace excess "
.replacingOccurrences(of: ".so", with: "") // replace excess .so
self.name = String(fullPath.split(separator: "/").last!) // take last segment
self.enabled = !line.contains(";")
self.file = file
}
/**
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply.
This simply toggles the extension in the .ini file.
You may need to restart the other services in order for this change to apply.
*/
func toggle() {
let newLine = enabled
@ -77,25 +81,25 @@ class PhpExtension {
? "; \(line)"
// ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "")
sed(file: file, original: line, replacement: newLine)
enabled.toggle()
}
// MARK: - Static Methods
/**
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)
if (file == nil) {
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
@ -104,5 +108,5 @@ class PhpExtension {
return PhpExtension($0, file: path.path)
}
}
}

View File

@ -3,34 +3,34 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 28/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpInstallation {
var versionNumber: PhpVersionNumber
/**
In order to determine details about a PHP installation, well simply run `php-config --version`
in the relevant directory.
*/
init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
self.versionNumber = PhpVersionNumber.make(from: version)!
if Filesystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute(
path: phpConfigExecutablePath,
arguments: ["--version"]
).trimmingCharacters(in: .whitespacesAndNewlines)
// The parser should always work, or the string has to be very unusual.
// If so, the app SHOULD crash, so that the users report what's up.
self.versionNumber = try! PhpVersionNumber.parse(longVersionString)
}
}
}

View File

@ -3,13 +3,13 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class InternalSwitcher: PhpSwitcher {
/**
Switching to a new PHP version involves:
- unlinking the current version
@ -20,50 +20,49 @@ class InternalSwitcher: PhpSwitcher {
the version that is switched to may or may not be identical to `php`
(without @version).
*/
func performSwitch(to version: String, completion: @escaping () -> Void)
{
func performSwitch(to version: String, completion: @escaping () -> Void) {
Log.info("Switching to \(version), unlinking all versions...")
let isolated = Valet.shared.sites.filter { site in
site.isolatedPhpVersion != nil
}.map { site in
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
}
var versions: Set<String> = [version]
if (Valet.enabled(feature: .isolatedSites)) {
if Valet.enabled(feature: .isolatedSites) {
versions = versions.union(isolated)
}
let group = DispatchGroup()
PhpEnv.shared.availablePhpVersions.forEach { (available) in
group.enter()
DispatchQueue.global(qos: .userInitiated).async {
self.disableDefaultPhpFpmPool(available)
self.stopPhpVersion(available)
group.leave()
}
}
group.notify(queue: .global(qos: .userInitiated)) {
Log.info("All versions have been unlinked!")
Log.info("Linking the new version!")
for formula in versions {
self.startPhpVersion(formula, primary: (version == formula))
}
Log.info("Restarting nginx, just to be sure!")
brew("services restart nginx", sudo: true)
Log.info("The new version(s) have been linked!")
completion()
}
}
private func disableDefaultPhpFpmPool(_ version: String) {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileManager.default.fileExists(atPath: pool) {
@ -71,8 +70,9 @@ class InternalSwitcher: PhpSwitcher {
let existing = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf")!
let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
do {
if (FileManager.default.fileExists(atPath: new.path)) {
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), cleaning up so the newer `www.conf` can be moved again.")
if FileManager.default.fileExists(atPath: new.path) {
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
+ "cleaning up so the newer `www.conf` can be moved again.")
try FileManager.default.removeItem(at: new)
}
try FileManager.default.moveItem(at: existing, to: new)
@ -82,26 +82,26 @@ class InternalSwitcher: PhpSwitcher {
}
}
}
private 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) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
if (primary) {
if primary {
Log.info("\(formula) is the primary formula, linking and starting services...")
brew("link \(formula) --overwrite --force")
} else {
Log.info("\(formula) is an isolated PHP version, starting services only...")
}
brew("services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "")
Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
@ -109,5 +109,5 @@ class InternalSwitcher: PhpSwitcher {
}
}
}

View File

@ -3,21 +3,21 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol PhpSwitcherDelegate: AnyObject {
func switcherDidStartSwitching(to version: String)
func switcherDidCompleteSwitch(to version: String)
}
protocol PhpSwitcher {
func performSwitch(to version: String, completion: @escaping () -> Void)
}

View File

@ -3,16 +3,16 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
import Foundation
extension App {
// MARK: - Application State
/**
Registers a window as currently open.
*/
@ -22,7 +22,7 @@ extension App {
}
updateActivationPolicy()
}
/**
Removes a window, assuming it was closed.
*/
@ -32,13 +32,13 @@ extension App {
}
updateActivationPolicy()
}
/**
If there are any open windows, the app will be a regular app.
If there are no windows open, the app will be an accessory (toolbar) app.
*/
public func updateActivationPolicy() {
NSApp.setActivationPolicy(openWindows.count > 0 ? .regular : .accessory)
NSApp.setActivationPolicy(!openWindows.isEmpty ? .regular : .accessory)
}
}

View File

@ -3,15 +3,15 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension App {
// MARK: - Methods
/**
On startup, the preferences should be loaded from the .plist,
and we'll enable the shortcut if it is set.
@ -22,20 +22,20 @@ extension App {
Log.info("No global hotkey was saved in preferences. None set.")
return
}
// Make sure we can parse the JSON into the desired format
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
Log.err("No global hotkey loaded, could not be parsed!")
shortcutHotkey = nil
return
}
shortcutHotkey = HotKey(keyCombo: KeyCombo(
carbonKeyCode: keybindPref.keyCode,
carbonModifiers: keybindPref.carbonFlags
))
}
/**
Sets up the action that needs to occur when the shortcut key is pressed
(opens the menu).
@ -44,11 +44,11 @@ extension App {
guard let hotkey = shortcutHotkey else {
return
}
hotkey.keyDownHandler = {
MainMenu.shared.statusItem.button?.performClick(nil)
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
}

View File

@ -2,25 +2,35 @@
// StateManager.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
class App {
// MARK: Static Vars
/** The static app instance. Accessible at any time. */
static let shared = App()
/** Retrieve the version number from the main info dictionary, Info.plist. */
static var version: String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
return "\(version) (\(build))"
}
/** Just the bundle version (build). */
static var bundleVersion: String {
return Bundle.main.infoDictionary?["CFBundleVersion"] as! String
}
/** Just the version number. */
static var shortVersion: String {
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
}
static var architecture: String {
var systeminfo = utsname()
uname(&systeminfo)
@ -34,37 +44,37 @@ class App {
}
return machine
}
// MARK: Variables
/** The list of preferences that are currently active. */
var preferences: [PreferenceName: Bool]!
/** The window controller of the currently active preferences window. */
var preferencesWindowController: PrefsWC? = nil
var preferencesWindowController: PrefsWC?
/** The window controller of the currently active site list window. */
var siteListWindowController: SiteListWC? = nil
var domainListWindowController: DomainListWC?
/** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = []
/** Timer that will periodically reload info about the user's PHP installation. */
var timer: Timer?
// MARK: - Global Hotkey
/**
The shortcut the user has requested.
*/
var shortcutHotkey: HotKey? = nil {
var shortcutHotkey: HotKey? {
didSet {
setupGlobalHotkeyListener()
}
}
// MARK: - Activation Policy
/**
Variable that keeps track of which windows are currently open.
(Please note that window controllers remain open in memory once opened.)
@ -74,9 +84,9 @@ class App {
(as a normal app or as a toolbar app).
*/
var openWindows: [String] = []
// MARK: - App Watchers
/**
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
*/

View File

@ -3,14 +3,14 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 20/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
import Foundation
extension AppDelegate {
/**
This is an entry point for future development for integrating with the PHP Monitor
application URL. You can use the `phpmon://` protocol to communicate with the app.
@ -21,20 +21,20 @@ extension AppDelegate {
Please note that PHP Monitor needs to be running in the background for this to work.
*/
func application(_ application: NSApplication, open urls: [URL]) {
if !Preferences.isEnabled(.allowProtocolForIntegrations) {
Log.info("Acting on commands via phpmon:// has been disabled.")
return
}
guard let url = urls.first else { return }
self.interpretCommand(
url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""),
commands: InterApp.getCommands()
)
}
private func interpretCommand(_ command: String, commands: [InterApp.Action]) {
commands.forEach { action in
if command.starts(with: action.command) {
@ -44,4 +44,3 @@ extension AppDelegate {
}
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -22,34 +22,34 @@ import AppKit
For more information about this, please see the ActivationPolicy-related extension.
*/
extension AppDelegate {
// MARK: - Menu Interactions
@IBAction func addSiteLinkPressed(_ sender: Any) {
SiteListVC.show()
guard let windowController = App.shared.siteListWindowController else { return }
DomainListVC.show()
guard let windowController = App.shared.domainListWindowController else { return }
windowController.pressedAddLink(nil)
}
@IBAction func reloadSiteListPressed(_ sender: Any) {
let vc = App.shared.siteListWindowController?
.window?.contentViewController as? SiteListVC
@IBAction func reloadDomainListPressed(_ sender: Any) {
let vc = App.shared.domainListWindowController?
.window?.contentViewController as? DomainListVC
if vc != nil {
// If the view exists, directly reload the list of sites
vc!.reloadSites()
vc!.reloadDomains()
} else {
// If the view does not exist, reload the cached data that was populated when the app initially launched.
Valet.shared.reloadSites()
}
}
@IBAction func focusSearchField(_ sender: Any) {
SiteListVC.show()
guard let windowController = App.shared.siteListWindowController else { return }
DomainListVC.show()
guard let windowController = App.shared.domainListWindowController else { return }
windowController.searchToolbarItem.searchField.becomeFirstResponder()
}
}

View File

@ -3,16 +3,16 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 06/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import UserNotifications
extension AppDelegate {
// MARK: - Notifications
/**
Sets up notifications. That does mean we need to ask for permission first.
If we cannot get permission, we should log this.
@ -30,7 +30,7 @@ extension AppDelegate {
}
})
}
/**
Ensure that the application displays notifications even when the app is active.
*/
@ -42,5 +42,5 @@ extension AppDelegate {
) {
completionHandler([.banner])
}
}

View File

@ -2,7 +2,7 @@
// AppDelegate.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -10,55 +10,55 @@ import UserNotifications
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
// MARK: - Variables
/**
The Shell singleton that keeps track of the history of all
(invoked by PHP Monitor) shell commands. It is used to
invoke all commands in this application.
*/
let sharedShell: Shell
/**
The App singleton contains information about the state of
the application and global variables.
*/
let state: App
/**
The MainMenu singleton is responsible for rendering the
menu bar item and its menu, as well as its actions.
*/
let menu: MainMenu
/**
The paths singleton that determines where Homebrew is installed,
and where to look for binaries.
*/
let paths: Paths
/**
The Valet singleton that determines all information
about Valet and its current configuration.
*/
let valet: Valet
/**
The PhpEnv singleton that handles PHP version
detection, as well as switching. It is initialized
when the app is ready and passed all checks.
*/
var phpEnvironment: PhpEnv! = nil
/**
The logger is responsible for different levels of logging.
You can tweak the verbosity in the `init` method here.
*/
var logger = Log.shared
// MARK: - Initializer
/**
When the application initializes, create all singletons.
*/
@ -67,6 +67,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
#if DEBUG
logger.verbosity = .performance
#endif
if CommandLine.arguments.contains("--v") {
logger.verbosity = .performance
Log.info("Extra verbose mode has been activated.")
}
Log.separator(as: .info)
Log.info("PHP MONITOR by Nico Verbruggen")
Log.info("Version \(App.version)")
@ -78,13 +82,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
self.valet = Valet.shared
super.init()
}
func initializeSwitcher() {
self.phpEnvironment = PhpEnv.shared
}
// MARK: - Lifecycle
/**
When the application has finished launching, we'll want to set up
the user notification center permissions, and kickoff the menu
@ -96,5 +100,5 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// Make sure the menu performs its initial checks
Task { await menu.startup() }
}
}

View File

@ -0,0 +1,181 @@
//
// Updater.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 09/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class AppUpdateChecker {
public static var enabled: Bool = {
return Preferences.isEnabled(.automaticBackgroundUpdateCheck)
}()
public static var isDev: Bool = {
return App.version.contains("-dev")
}()
public static func retrieveVersionFromCask(
_ initiatedFromBackground: Bool = true
) -> String {
let caskFile = App.version.contains("-dev")
? Constants.Urls.DevBuildCaskFile.absoluteString
: Constants.Urls.StableBuildCaskFile.absoluteString
var command = "curl -s"
if initiatedFromBackground {
command = "curl -s --max-time 5"
}
return Shell.pipe(
"\(command) '\(caskFile)' | grep version"
)
}
public static func checkIfNewerVersionIsAvailable(
initiatedFromBackground: Bool = true
) {
if initiatedFromBackground {
if !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
Log.info("Automatic updates are disabled. No check will be performed.")
return
}
Log.info("Automatic updates are enabled, a check will be performed.")
}
let versionString = retrieveVersionFromCask(initiatedFromBackground)
guard let onlineVersion = AppVersion.from(versionString) else {
Log.err("We couldn't check for updates!")
// Only notify about connection issues if the request to check for updates was explicit
if !initiatedFromBackground {
notifyAboutConnectionIssue()
}
return
}
let currentVersion = AppVersion.fromCurrentVersion()
handleVersionComparison(
currentVersion,
onlineVersion,
initiatedFromBackground
)
}
private static func handleVersionComparison(
_ currentVersion: AppVersion,
_ onlineVersion: AppVersion,
_ background: Bool
) {
switch onlineVersion.version.versionCompare(currentVersion.version) {
case .orderedAscending:
Log.info("You are running a newer version of PHP Monitor "
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
if !background { notifyVersionDoesNotNeedUpgrade() }
case .orderedDescending:
Log.info("There is a newer version (\(onlineVersion)) available! "
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
notifyAboutNewerVersion(version: onlineVersion)
case .orderedSame:
if currentVersion.build != nil
&& onlineVersion.build != nil
&& buildDiffers(currentVersion, onlineVersion, background) {
return
}
Log.info("The installed version (\(currentVersion.computerReadable)) matches the latest release "
+ "(\(onlineVersion.computerReadable)).")
if !background { notifyVersionDoesNotNeedUpgrade() }
}
}
private static func buildDiffers(
_ currentVersion: AppVersion,
_ onlineVersion: AppVersion,
_ background: Bool
) -> Bool {
if Int(onlineVersion.build!)! > Int(currentVersion.build!)! {
Log.info("There is a newer build of PHP Monitor available! "
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
notifyAboutNewerVersion(version: onlineVersion)
return true
} else if Int(onlineVersion.build!)! < Int(currentVersion.build!)! {
Log.info("You are running a newer build of PHP Monitor "
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
if !background { notifyVersionDoesNotNeedUpgrade() }
return true
}
return false
}
private static func notifyVersionDoesNotNeedUpgrade() {
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.alerts.is_latest_version.title".localized,
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
description: ""
)
.withPrimary(text: "OK")
.show()
}
}
private static func notifyAboutNewerVersion(version: AppVersion) {
let devSuffix = isDev ? "-dev" : ""
let command = isDev ? "brew upgrade phpmon-dev" : "brew upgrade phpmon"
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.alerts.newer_version_available.title".localized(version.humanReadable),
subtitle: "updater.alerts.newer_version_available.subtitle".localized,
description: HomebrewDiagnostics.customCaskInstalled
? "updater.installation_source.brew".localized(command)
: "updater.installation_source.direct".localized
)
.withPrimary(
text: "updater.alerts.buttons.release_notes".localized,
action: { vc in
vc.close(with: .OK)
NSWorkspace.shared.open(
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version.version)\(devSuffix)")
)
}
)
.withTertiary(text: "Dismiss", action: { vc in
vc.close(with: .OK)
})
.show()
}
}
private static func notifyAboutConnectionIssue() {
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.errors.cannot_check_for_update.title".localized,
subtitle: "updater.errors.cannot_check_for_update.subtitle".localized,
description: "updater.errors.cannot_check_for_update.description".localized(
App.version
)
)
.withTertiary(
text: "updater.errors.buttons.releases_on_github".localized,
action: { _ in
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
}
)
.withPrimary(text: "OK")
.show()
}
}
}

View File

@ -0,0 +1,77 @@
//
// AppVersion.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class AppVersion {
var version: String
var build: String?
var suffix: String?
init(version: String, build: String?, suffix: String? = nil) {
self.version = version
self.build = build
self.suffix = suffix
}
public static func from(_ string: String) -> AppVersion? {
do {
let regex = try NSRegularExpression(
pattern: #"(?<version>(\d+)[.](\d+)([.](\d+))?)(-(?<suffix>[a-z]+)){0,1}((,|_)(?<build>\d+)){0,1}"#,
options: []
)
let match = regex.matches(
in: string,
options: [],
range: NSRange(location: 0, length: string.count)
).first
guard let match = match else {
return nil
}
var version: String = ""
var build: String?
var suffix: String?
if let versionRange = Range(match.range(withName: "version"), in: string) {
version = String(string[versionRange])
}
if let buildRange = Range(match.range(withName: "build"), in: string) {
build = String(string[buildRange])
}
if let suffixRange = Range(match.range(withName: "suffix"), in: string) {
suffix = String(string[suffixRange])
}
return AppVersion(
version: version,
build: build,
suffix: suffix
)
} catch {
return nil
}
}
public static func fromCurrentVersion() -> AppVersion {
return AppVersion.from("\(App.shortVersion)_\(App.bundleVersion)")!
}
var computerReadable: String {
return "\(version)_\(build ?? "0")"
}
var humanReadable: String {
return "\(version) (\(build ?? "???"))"
}
}

View File

@ -60,16 +60,16 @@
</menuItem>
<menuItem title="reload-list" keyEquivalent="r" id="Ema-AU-Nbr" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_site_list"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_domain_list"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="reloadSiteListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
<action selector="reloadDomainListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="2ux-8Q-UjK"/>
<menuItem title="focus-find" keyEquivalent="f" id="I95-fb-EL7" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_site_list"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_domain_list"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
@ -319,7 +319,7 @@
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="-495" y="-44"/>
<point key="canvasLocation" x="-360" y="-94"/>
</scene>
<!--Window Controller-->
<scene sceneID="PQa-AT-b2a">
@ -348,7 +348,7 @@
</windowController>
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="327"/>
<point key="canvasLocation" x="-374" y="238"/>
</scene>
<!--Preferences-->
<scene sceneID="iyi-IS-7Ps">
@ -378,13 +378,13 @@
</viewController>
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="251" y="205"/>
<point key="canvasLocation" x="260" y="217"/>
</scene>
<!--Window Controller-->
<scene sceneID="4XS-kY-YIS">
<objects>
<windowController storyboardIdentifier="siteListWindow" id="8Ec-9q-82s" customClass="SiteListWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" title="Domains" subtitle="Linked &amp; Parked" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
<windowController storyboardIdentifier="domainListWindow" id="8Ec-9q-82s" customClass="DomainListWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" separatorStyle="line" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="425" y="461" width="600" height="263"/>
@ -437,7 +437,7 @@
</windowController>
<customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="746"/>
<point key="canvasLocation" x="-374" y="745.5"/>
</scene>
<!--Window Controller-->
<scene sceneID="HTI-x5-rOp">
@ -462,7 +462,7 @@
</windowController>
<customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-409" y="1137"/>
<point key="canvasLocation" x="-374" y="1137"/>
</scene>
<!--Window Controller-->
<scene sceneID="BD0-La-ygq">
@ -486,7 +486,7 @@
</windowController>
<customObject id="i3j-z8-nxv" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-575" y="1624"/>
<point key="canvasLocation" x="-374" y="2267"/>
</scene>
<!--Better AlertVC-->
<scene sceneID="y9E-bB-wIG">
@ -632,27 +632,27 @@ Gw
</viewController>
<customObject id="5Ts-EZ-bJh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="38" y="1624"/>
<point key="canvasLocation" x="230" y="2267"/>
</scene>
<!--Add SiteVC-->
<scene sceneID="6JC-H6-u4K">
<objects>
<viewController storyboardIdentifier="newSiteLink" id="glS-wF-sEU" customClass="AddSiteVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="JJJ-T9-Yuv">
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="js9-OW-xzC">
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
<view key="contentView" id="HRC-RT-LxR">
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
<rect key="frame" x="363" y="13" width="104" height="32"/>
<buttonCell key="cell" type="push" title="Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
<rect key="frame" x="325" y="13" width="142" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
@ -664,11 +664,11 @@ DQ
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
<rect key="frame" x="13" y="13" width="94" height="32"/>
<rect key="frame" x="13" y="13" width="114" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
</constraints>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
@ -680,8 +680,8 @@ Gw
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
<rect key="frame" x="20" y="156" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a potential domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
<rect key="frame" x="20" y="150" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -691,16 +691,16 @@ Gw
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
<rect key="frame" x="18" y="134" width="444" height="14"/>
<textFieldCell key="cell" title="FOLDER_AVAILABLE" id="bJr-s6-tdP">
<rect key="frame" x="18" y="128" width="444" height="14"/>
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KZf-b0-9cm">
<rect key="frame" x="18" y="101" width="227" height="18"/>
<buttonCell key="cell" type="check" title="Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ">
<rect key="frame" x="18" y="95" width="266" height="18"/>
<buttonCell key="cell" type="check" title="[i18n] Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
@ -709,31 +709,31 @@ Gw
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
<rect key="frame" x="18" y="66" width="444" height="28"/>
<textFieldCell key="cell" title="Securing a site requires administrative privileges. You will be prompted for your password or Touch ID." id="4gd-KM-5Fu">
<rect key="frame" x="18" y="60" width="444" height="28"/>
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges. You may be prompted for your password or Touch ID." id="4gd-KM-5Fu">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6JT-Vt-3q0">
<rect key="frame" x="20" y="185" width="440" height="22"/>
<rect key="frame" x="20" y="179" width="440" height="22"/>
<pathCell key="cell" selectable="YES" refusesFirstResponder="YES" alignment="left" id="m8d-XF-kh9">
<font key="font" metaFont="system"/>
<url key="url" string="file:///Users/"/>
</pathCell>
</pathControl>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
<rect key="frame" x="18" y="215" width="87" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Link a Folder" id="S4j-ZC-ddT">
<rect key="frame" x="18" y="209" width="128" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
<rect key="frame" x="229" y="23" width="128" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That link already exists." id="jOt-n6-TQf">
<rect key="frame" x="139" y="23" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -752,6 +752,7 @@ Gw
<constraint firstItem="900-Z2-tID" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="SwS-o8-pbl" secondAttribute="trailing" constant="8" symbolic="YES" id="IMv-ZD-VXf"/>
<constraint firstItem="js9-OW-xzC" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" id="IpM-ot-dBG"/>
<constraint firstItem="VzR-5a-cmT" firstAttribute="leading" secondItem="ZX9-s1-23i" secondAttribute="leading" id="UPN-Ad-j3X"/>
<constraint firstItem="SwS-o8-pbl" firstAttribute="top" secondItem="mmQ-7e-dlb" secondAttribute="bottom" constant="20" id="VNW-fB-2Xj"/>
<constraint firstItem="KZf-b0-9cm" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="Vab-wq-9Nc"/>
<constraint firstAttribute="bottom" secondItem="PVw-cM-qAB" secondAttribute="bottom" constant="20" symbolic="YES" id="VsP-Q0-zRW"/>
<constraint firstAttribute="trailing" secondItem="PVw-cM-qAB" secondAttribute="trailing" constant="20" symbolic="YES" id="X5z-G4-CBv"/>
@ -778,7 +779,7 @@ Gw
<outlet property="buttonCancel" destination="SwS-o8-pbl" id="N1v-uy-2Mi"/>
<outlet property="buttonCreateLink" destination="PVw-cM-qAB" id="0Oo-xW-He7"/>
<outlet property="buttonSecure" destination="KZf-b0-9cm" id="5A7-Bn-NB7"/>
<outlet property="linkName" destination="ZX9-s1-23i" id="yT6-80-Zr1"/>
<outlet property="inputDomainName" destination="ZX9-s1-23i" id="yT6-80-Zr1"/>
<outlet property="pathControl" destination="6JT-Vt-3q0" id="f5K-8h-VOd"/>
<outlet property="previewText" destination="VzR-5a-cmT" id="qwd-wX-645"/>
<outlet property="textFieldError" destination="900-Z2-tID" id="qUk-FE-IKW"/>
@ -788,12 +789,12 @@ Gw
</viewController>
<customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="191" y="1098.5"/>
<point key="canvasLocation" x="210" y="1128"/>
</scene>
<!--Site ListVC-->
<!--Domain ListVC-->
<scene sceneID="aZt-6w-TFl">
<objects>
<viewController identifier="siteList" storyboardIdentifier="siteList" id="JZI-Vd-9oq" customClass="SiteListVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<viewController identifier="domainList" storyboardIdentifier="domainList" id="JZI-Vd-9oq" customClass="DomainListVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="rIZ-4U-bhj">
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<autoresizingMask key="autoresizingMask"/>
@ -805,7 +806,7 @@ Gw
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj">
<rect key="frame" x="0.0" y="0.0" width="662" height="281"/>
<rect key="frame" x="0.0" y="0.0" width="626" height="281"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -825,7 +826,7 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Secure"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListTLSCell" id="hft-M4-nWb" customClass="SiteListTLSCell" customModule="PHP_Monitor" customModuleProvider="target">
<tableCellView identifier="domainListTLSCell" id="hft-M4-nWb" customClass="DomainListTLSCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="18" y="0.0" width="34" height="55"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
@ -848,7 +849,7 @@ Gw
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="DOMAIN" width="290" minWidth="250" maxWidth="10000" id="oeH-B2-0rA">
<tableColumn identifier="DOMAIN" width="200" minWidth="200" maxWidth="10000" id="oeH-B2-0rA">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Domain">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
@ -861,8 +862,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Domain"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListNameCell" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="69" y="0.0" width="290" height="54"/>
<tableCellView identifier="domainListNameCell" wantsLayer="YES" id="5GY-nN-BWd" customClass="DomainListNameCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="69" y="0.0" width="200" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
@ -910,8 +911,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="PHP"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListPhpCell" wantsLayer="YES" id="T49-0U-d58" customClass="SiteListPhpCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="376" y="0.0" width="100" height="54"/>
<tableCellView identifier="domainListPhpCell" wantsLayer="YES" id="T49-0U-d58" customClass="DomainListPhpCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="286" y="0.0" width="100" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZXQ-bg-Xba">
@ -965,8 +966,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Kind"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="SiteListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="493" y="0.0" width="36" height="54"/>
<tableCellView identifier="domainListKindCell" wantsLayer="YES" id="AhT-xR-16a" customClass="DomainListKindCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="403" y="0.0" width="36" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sYR-vb-OW1">
@ -1002,8 +1003,8 @@ Gw
<sortDescriptor key="sortDescriptorPrototype" selector="compare:" sortKey="Type"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="SiteListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="546" y="0.0" width="97" height="54"/>
<tableCellView identifier="domainListTypeCell" wantsLayer="YES" id="ntU-Rl-ciP" customClass="DomainListTypeCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="456" y="0.0" width="97" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
@ -1060,7 +1061,7 @@ Gw
<autoresizingMask key="autoresizingMask"/>
</scroller>
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
<rect key="frame" x="0.0" y="0.0" width="662" height="28"/>
<rect key="frame" x="0.0" y="0.0" width="626" height="28"/>
<autoresizingMask key="autoresizingMask"/>
</tableHeaderView>
</scrollView>
@ -1088,12 +1089,376 @@ Gw
</viewController>
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="388" y="715.5"/>
<point key="canvasLocation" x="323" y="723"/>
</scene>
<!--Add ProxyVC-->
<scene sceneID="g8z-pE-RL9">
<objects>
<viewController storyboardIdentifier="newProxyLink" id="dwh-CF-6iv" customClass="AddProxyVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="U5U-QR-YXS">
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="kkd-UV-SnA">
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
<view key="contentView" id="IXW-35-8NJ">
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
<rect key="frame" x="20" y="196" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
<rect key="frame" x="18" y="221" width="325" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Proxy subject (usually: protocol, IP address and port)" id="G1Z-3f-BhL">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
<rect key="frame" x="18" y="172" width="112" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
<rect key="frame" x="20" y="147" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="dwh-CF-6iv" id="e9n-PM-7s8"/>
</connections>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="SNw-oQ-bnb" secondAttribute="trailing" constant="20" id="2ui-Jg-BUV"/>
<constraint firstItem="mlA-Zt-Hu8" firstAttribute="top" secondItem="QCK-Z9-w7g" secondAttribute="bottom" constant="10" id="8sn-dT-SW6"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Uib-vA-HRc" secondAttribute="trailing" constant="20" symbolic="YES" id="Cue-3e-doM"/>
<constraint firstItem="QCK-Z9-w7g" firstAttribute="leading" secondItem="SNw-oQ-bnb" secondAttribute="leading" id="N1K-69-wLz"/>
<constraint firstItem="mlA-Zt-Hu8" firstAttribute="leading" secondItem="QCK-Z9-w7g" secondAttribute="leading" id="R74-k0-96U"/>
<constraint firstItem="SNw-oQ-bnb" firstAttribute="leading" secondItem="IXW-35-8NJ" secondAttribute="leading" constant="20" id="WZR-f8-mgf"/>
<constraint firstItem="SNw-oQ-bnb" firstAttribute="top" secondItem="mlA-Zt-Hu8" secondAttribute="bottom" constant="4" id="XDn-h9-dgp"/>
<constraint firstItem="QCK-Z9-w7g" firstAttribute="top" secondItem="Uib-vA-HRc" secondAttribute="bottom" constant="4" id="fGU-al-B0w"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="mlA-Zt-Hu8" secondAttribute="trailing" constant="20" symbolic="YES" id="uFE-cU-KOg"/>
<constraint firstItem="QCK-Z9-w7g" firstAttribute="trailing" secondItem="SNw-oQ-bnb" secondAttribute="trailing" id="xQE-yY-gPd"/>
</constraints>
</view>
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Vi-cN-ude">
<rect key="frame" x="317" y="13" width="150" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create Proxy" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="H2Z-c5-5Vk">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="pressedCreateProxy:" target="dwh-CF-6iv" id="wFW-Aw-FOR"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
<rect key="frame" x="13" y="13" width="114" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="uCc-fF-wS2"/>
</constraints>
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="D8g-GE-7TU">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
<rect key="frame" x="18" y="128" width="444" height="14"/>
<textFieldCell key="cell" title="[i18n] Preview text here" id="ISE-9R-ncQ">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rJa-yg-nCn">
<rect key="frame" x="18" y="95" width="170" height="18"/>
<buttonCell key="cell" type="check" title="[i18n] Secure this proxy" bezelStyle="regularSquare" imagePosition="left" inset="2" id="5LI-lt-Asl">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="pressedSecure:" target="dwh-CF-6iv" id="b74-8T-AzO"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
<rect key="frame" x="18" y="60" width="444" height="28"/>
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges. You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
<rect key="frame" x="18" y="250" width="123" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
<rect key="frame" x="131" y="23" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="nC0-dk-QaF" secondAttribute="bottom" constant="20" symbolic="YES" id="3Kk-fY-SB7"/>
<constraint firstItem="JSZ-x8-Pqi" firstAttribute="trailing" secondItem="SNw-oQ-bnb" secondAttribute="trailing" id="3So-Wu-1cz"/>
<constraint firstItem="DAh-br-Dfx" firstAttribute="top" secondItem="U5U-QR-YXS" secondAttribute="top" constant="20" symbolic="YES" id="3im-Qd-loW"/>
<constraint firstItem="kkd-UV-SnA" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" id="6iw-dd-hTX"/>
<constraint firstItem="Uib-vA-HRc" firstAttribute="leading" secondItem="DAh-br-Dfx" secondAttribute="leading" id="6jA-Kj-Q7l"/>
<constraint firstAttribute="trailing" secondItem="kkd-UV-SnA" secondAttribute="trailing" id="8YX-CO-sY2"/>
<constraint firstAttribute="trailing" secondItem="5x7-ll-2f7" secondAttribute="trailing" constant="20" symbolic="YES" id="8jr-cl-x78"/>
<constraint firstItem="kkd-UV-SnA" firstAttribute="top" secondItem="U5U-QR-YXS" secondAttribute="top" id="Afh-Ur-QgJ"/>
<constraint firstItem="4Vi-cN-ude" firstAttribute="leading" secondItem="w0k-CK-0u4" secondAttribute="trailing" constant="15" id="D3C-co-B10"/>
<constraint firstItem="w0k-CK-0u4" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="nC0-dk-QaF" secondAttribute="trailing" constant="8" symbolic="YES" id="FGk-wm-1Mu"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="rJa-yg-nCn" secondAttribute="trailing" constant="20" symbolic="YES" id="Fa7-Rc-1lj"/>
<constraint firstAttribute="trailing" secondItem="4Vi-cN-ude" secondAttribute="trailing" constant="20" symbolic="YES" id="Fbg-C8-v6E"/>
<constraint firstItem="5x7-ll-2f7" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="Fd0-zd-od8"/>
<constraint firstAttribute="bottom" secondItem="4Vi-cN-ude" secondAttribute="bottom" constant="20" symbolic="YES" id="GyL-uL-sjW"/>
<constraint firstItem="w0k-CK-0u4" firstAttribute="centerY" secondItem="4Vi-cN-ude" secondAttribute="centerY" id="HcL-wb-0s6"/>
<constraint firstItem="rJa-yg-nCn" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="IEg-SN-bHB"/>
<constraint firstItem="rJa-yg-nCn" firstAttribute="top" secondItem="JSZ-x8-Pqi" secondAttribute="bottom" constant="16" id="IW3-MX-3Kh"/>
<constraint firstItem="DAh-br-Dfx" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="LY1-r0-viF"/>
<constraint firstItem="nC0-dk-QaF" firstAttribute="top" secondItem="5x7-ll-2f7" secondAttribute="bottom" constant="20" id="OjY-dM-dOG"/>
<constraint firstItem="nC0-dk-QaF" firstAttribute="leading" secondItem="U5U-QR-YXS" secondAttribute="leading" constant="20" symbolic="YES" id="V6L-YR-ufX"/>
<constraint firstItem="JSZ-x8-Pqi" firstAttribute="leading" secondItem="SNw-oQ-bnb" secondAttribute="leading" id="dpc-5M-0Cq"/>
<constraint firstItem="5x7-ll-2f7" firstAttribute="top" secondItem="rJa-yg-nCn" secondAttribute="bottom" constant="8" symbolic="YES" id="dzE-Ob-SVG"/>
<constraint firstAttribute="bottom" secondItem="4Vi-cN-ude" secondAttribute="bottom" constant="20" symbolic="YES" id="ny2-RO-bEI"/>
<constraint firstAttribute="bottom" secondItem="kkd-UV-SnA" secondAttribute="bottom" id="oCP-dn-6dx"/>
<constraint firstItem="JSZ-x8-Pqi" firstAttribute="top" secondItem="SNw-oQ-bnb" secondAttribute="bottom" constant="5" id="sX3-MK-14k"/>
<constraint firstItem="Uib-vA-HRc" firstAttribute="top" secondItem="DAh-br-Dfx" secondAttribute="bottom" constant="15" id="tWI-S8-17J"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="DAh-br-Dfx" secondAttribute="trailing" constant="20" symbolic="YES" id="vDR-5D-1eN"/>
</constraints>
</view>
<connections>
<outlet property="buttonCancel" destination="nC0-dk-QaF" id="n5Q-jg-UCe"/>
<outlet property="buttonCreateProxy" destination="4Vi-cN-ude" id="rdK-xc-N7F"/>
<outlet property="buttonSecure" destination="rJa-yg-nCn" id="WIs-zt-f3a"/>
<outlet property="inputDomainName" destination="SNw-oQ-bnb" id="ELH-63-cAe"/>
<outlet property="inputProxySubject" destination="QCK-Z9-w7g" id="76U-te-Jzt"/>
<outlet property="previewText" destination="JSZ-x8-Pqi" id="Mve-6W-Owd"/>
<outlet property="textFieldDomainName" destination="mlA-Zt-Hu8" id="cHL-Yu-Yvx"/>
<outlet property="textFieldError" destination="w0k-CK-0u4" id="28h-bn-igB"/>
<outlet property="textFieldProxySubject" destination="Uib-vA-HRc" id="5tV-3l-Wbw"/>
<outlet property="textFieldSecure" destination="5x7-ll-2f7" id="NlV-g8-rYP"/>
<outlet property="textFieldTitle" destination="DAh-br-Dfx" id="8SA-EW-wcq"/>
</connections>
</viewController>
<customObject id="VaP-ZM-OcY" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="210" y="1524"/>
</scene>
<!--Window Controller-->
<scene sceneID="5Gf-7O-tdA">
<objects>
<windowController storyboardIdentifier="addProxyWindow" id="ogq-ok-UVi" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="SMz-Va-x2z">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="HsN-qQ-BhO">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="ogq-ok-UVi" id="9CA-sB-ZTD"/>
</connections>
</window>
<connections>
<segue destination="dwh-CF-6iv" kind="relationship" relationship="window.shadowedContentViewController" id="My6-qb-eRg"/>
</connections>
</windowController>
<customObject id="5qP-qX-rbc" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="1530"/>
</scene>
<!--SelectionVC-->
<scene sceneID="UXm-Ci-yEB">
<objects>
<viewController storyboardIdentifier="addDomainChoice" id="gOD-Gu-zDG" customClass="SelectionVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="ysc-sm-sli">
<rect key="frame" x="0.0" y="0.0" width="540" height="177"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<visualEffectView blendingMode="behindWindow" material="toolTip" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="F37-zt-gM3">
<rect key="frame" x="0.0" y="0.0" width="540" height="177"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
<rect key="frame" x="13" y="13" width="114" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="Zhu-D8-cLK"/>
</constraints>
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="LxP-t4-H2W">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="pressedCancel:" target="gOD-Gu-zDG" id="wMp-sM-0A4"/>
</connections>
</button>
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pYe-Qu-qnK">
<rect key="frame" x="187" y="20" width="333" height="20"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="L5n-Gw-J27">
<rect key="frame" x="-7" y="-7" width="172" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="8UP-Sw-TP6">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">l</string>
</buttonCell>
<connections>
<action selector="pressedCreateLink:" target="gOD-Gu-zDG" id="77M-Ip-GMi"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="01Z-IV-hv1">
<rect key="frame" x="159" y="-7" width="181" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="bJ4-q8-1Ej">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">p</string>
</buttonCell>
<connections>
<action selector="pressedCreateProxy:" target="gOD-Gu-zDG" id="UDf-lD-KCS"/>
</connections>
</button>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
<rect key="frame" x="18" y="138" width="504" height="19"/>
<textFieldCell key="cell" selectable="YES" alignment="left" title="[i18n] What kind of domain would you like to set up?" id="agk-Nj-FLd">
<font key="font" metaFont="systemBold" size="15"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
<rect key="frame" x="18" y="60" width="504" height="70"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="tbl-AV-4qB"/>
</constraints>
<textFieldCell key="cell" selectable="YES" alignment="left" id="3i9-RG-Ift">
<font key="font" metaFont="smallSystem"/>
<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>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="FhN-AM-SkI" firstAttribute="leading" secondItem="F37-zt-gM3" secondAttribute="leading" constant="20" symbolic="YES" id="3dg-JM-MDr"/>
<constraint firstItem="fJK-Ke-IK3" firstAttribute="top" secondItem="F37-zt-gM3" secondAttribute="top" constant="20" symbolic="YES" id="FbX-Le-O7Q"/>
<constraint firstAttribute="trailing" secondItem="pYe-Qu-qnK" secondAttribute="trailing" constant="20" symbolic="YES" id="IJA-vN-Rbv"/>
<constraint firstItem="urj-Xq-TrJ" firstAttribute="leading" secondItem="fJK-Ke-IK3" secondAttribute="leading" id="JcY-ae-6ZH"/>
<constraint firstItem="urj-Xq-TrJ" firstAttribute="trailing" secondItem="fJK-Ke-IK3" secondAttribute="trailing" id="ZBI-pN-kOz"/>
<constraint firstItem="fJK-Ke-IK3" firstAttribute="leading" secondItem="F37-zt-gM3" secondAttribute="leading" constant="20" symbolic="YES" id="d4o-6b-Dho"/>
<constraint firstItem="urj-Xq-TrJ" firstAttribute="top" secondItem="fJK-Ke-IK3" secondAttribute="bottom" constant="8" symbolic="YES" id="hOk-eL-Eg0"/>
<constraint firstItem="FhN-AM-SkI" firstAttribute="top" secondItem="urj-Xq-TrJ" secondAttribute="bottom" constant="20" id="kCc-Vp-Gvq"/>
<constraint firstAttribute="bottom" secondItem="pYe-Qu-qnK" secondAttribute="bottom" constant="20" id="lPX-ZF-XZN"/>
<constraint firstAttribute="trailing" secondItem="fJK-Ke-IK3" secondAttribute="trailing" constant="20" symbolic="YES" id="spl-Bn-xtw"/>
<constraint firstAttribute="bottom" secondItem="FhN-AM-SkI" secondAttribute="bottom" constant="20" symbolic="YES" id="t5w-aL-tOa"/>
</constraints>
</visualEffectView>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cNh-Wc-ADk">
<rect key="frame" x="200" y="109" width="0.0" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="OQ5-hX-qai">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="F37-zt-gM3" secondAttribute="trailing" id="ZRD-3j-s4x"/>
<constraint firstAttribute="bottom" secondItem="F37-zt-gM3" secondAttribute="bottom" id="et1-At-Rgj"/>
<constraint firstItem="F37-zt-gM3" firstAttribute="top" secondItem="ysc-sm-sli" secondAttribute="top" id="jp3-eE-mOy"/>
<constraint firstItem="F37-zt-gM3" firstAttribute="leading" secondItem="ysc-sm-sli" secondAttribute="leading" id="wIo-zP-KId"/>
</constraints>
</view>
<connections>
<outlet property="buttonCancel" destination="FhN-AM-SkI" id="iqV-2E-q7e"/>
<outlet property="buttonCreateLink" destination="L5n-Gw-J27" id="SHV-4l-Red"/>
<outlet property="buttonCreateProxy" destination="01Z-IV-hv1" id="J1v-7J-4fx"/>
<outlet property="textFieldDescription" destination="urj-Xq-TrJ" id="u1w-O0-kI3"/>
<outlet property="textFieldTitle" destination="fJK-Ke-IK3" id="x8p-qx-HX4"/>
</connections>
</viewController>
<customObject id="bZa-dD-d4J" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="250" y="1900"/>
</scene>
<!--Window Controller-->
<scene sceneID="HW6-nV-trE">
<objects>
<windowController storyboardIdentifier="showSelectionWindow" id="t4x-Mh-iya" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="IeW-fo-4yK">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="Oe0-yv-Jcy">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="t4x-Mh-iya" id="4oO-gI-bd2"/>
</connections>
</window>
<connections>
<segue destination="gOD-Gu-zDG" kind="relationship" relationship="window.shadowedContentViewController" id="KRt-OH-8uc"/>
</connections>
</windowController>
<customObject id="hBK-Bw-dwa" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="1909"/>
</scene>
</scenes>
<resources>
<image name="Checkmark" width="512" height="512"/>
<image name="IconLinked" width="25" height="25"/>
<image name="IconProxy" width="25" height="25"/>
<image name="Lock" width="30" height="30"/>
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
<image name="plus" catalog="system" width="14" height="13"/>

View File

@ -9,21 +9,21 @@
import Foundation
class InterApp {
public static var bindings: [Action] = []
public static func register(_ action: Action) {
self.bindings.append(action)
}
public struct Action {
let command: String
let action: (String) -> Void
}
static func getCommands() -> [InterApp.Action] { return [
InterApp.Action(command: "list", action: { _ in
SiteListVC.show()
DomainListVC.show()
}),
InterApp.Action(command: "services/stop", action: { _ in
MainMenu.shared.stopAllServices()
@ -61,7 +61,7 @@ class InterApp {
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
).withPrimary(text: "OK").show()
}
}),
})
]}
}

View File

@ -2,14 +2,14 @@
// Environment.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class Startup {
/**
Checks the user's environment and checks if PHP Monitor can be used properly.
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
@ -17,30 +17,29 @@ class Startup {
If this method returns false, there was a failed check and an alert was displayed.
If this method returns true, then all checks succeeded and the app can continue.
*/
func checkEnvironment() async -> Bool
{
func checkEnvironment() async -> Bool {
// Do the important system setup checks
Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)")
for check in self.checks {
if await check.succeeds() {
Log.info("[OK] \(check.name)")
continue
}
// If we get here, something's gone wrong and the check has failed...
Log.info("[FAIL] \(check.name)")
showAlert(for: check)
return false
}
// If we get here, nothing has gone wrong. That's what we want!
initializeSwitcher()
Log.separator(as: .info)
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
return true
}
/**
Displays an alert for a particular check. There are two types of alerts:
- ones that require an app restart, which prompt the user to exit the app
@ -59,7 +58,7 @@ class Startup {
exit(1)
}).show()
}
BetterAlert()
.withInformation(
title: check.titleText,
@ -70,7 +69,7 @@ class Startup {
.show()
}
}
/**
Because the Switcher requires various environment guarantees, the switcher is only
initialized when it is done working. The switcher must be initialized on the main thread.
@ -81,9 +80,9 @@ class Startup {
appDelegate.initializeSwitcher()
}
}
// MARK: - Check (List)
public var checks: [EnvironmentCheck] = [
// =================================================================================
// The Homebrew binary must exist.
@ -196,9 +195,9 @@ 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.
@ -211,7 +210,7 @@ class Startup {
let descriptionText: String
let buttonText: String
let requiresAppRestart: Bool
init(
command: @escaping () async -> Bool,
name: String,
@ -229,7 +228,7 @@ class Startup {
self.buttonText = buttonText
self.requiresAppRestart = requiresAppRestart
}
public func succeeds() async -> Bool {
return await !self.command()
}

View File

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

View File

@ -11,130 +11,141 @@ import Cocoa
class AddSiteVC: NSViewController, NSTextFieldDelegate {
// MARK: - Outlets
@IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var pathControl: NSPathControl!
@IBOutlet weak var linkName: NSTextField!
@IBOutlet weak var inputDomainName: NSTextField!
@IBOutlet weak var previewText: NSTextField!
@IBOutlet weak var buttonSecure: NSButton!
@IBOutlet weak var buttonCreateLink: NSButton!
@IBOutlet weak var buttonCancel: NSButton!
@IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var textFieldSecure: NSTextField!
@IBOutlet weak var textFieldError: NSTextField!
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
loadStaticLocalisedStrings()
}
private func dismissView(outcome: NSApplication.ModalResponse) {
guard let window = self.view.window, let parent = window.sheetParent else { return }
parent.endSheet(window, returnCode: outcome)
}
// MARK: - Localisation
func loadStaticLocalisedStrings() {
textFieldTitle.stringValue = "site_list.add.link_folder".localized
linkName.placeholderString = "site_list.add.domain_name_placeholder".localized
textFieldSecure.stringValue = "site_list.add.secure_description".localized
buttonCancel.stringValue = "site_list.add.cancel".localized
textFieldTitle.stringValue = "domain_list.add.link_folder".localized
inputDomainName.placeholderString = "domain_list.add.domain_name_placeholder".localized
textFieldSecure.stringValue = "domain_list.add.secure_description".localized
buttonCancel.title = "domain_list.add.cancel".localized
buttonCreateLink.title = "domain_list.add.create_link".localized
}
// MARK: - Outlet Interactions
@IBAction func pressedCreateLink(_ sender: Any) {
let path = self.pathControl.url!.path
let name = self.linkName.stringValue
let path = pathControl.url!.path
let name = inputDomainName.stringValue
if !FileManager.default.fileExists(atPath: path) {
Alert.confirm(
onWindow: self.view.window!,
messageText: "site_list.alert.folder_missing.title".localized,
informativeText: "site_list.alert.folder_missing.desc".localized,
buttonTitle: "site_list.alert.folder_missing.cancel".localized,
secondButtonTitle: "site_list.alert.folder_missing.return".localized,
onFirstButtonPressed: {
self.dismissView(outcome: .cancel)
onWindow: view.window!,
messageText: "domain_list.alert.folder_missing.title".localized,
informativeText: "domain_list.alert.folder_missing.desc".localized,
buttonTitle: "domain_list.alert.folder_missing.cancel".localized,
secondButtonTitle: "domain_list.alert.folder_missing.return".localized,
onFirstButtonPressed: { [self] in
dismissView(outcome: .cancel)
}
)
return
}
// Adding `valet links` is a workaround for Valet malforming the config.json file
// TODO: I will have to investigate and report this behaviour if possible
Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links", requiresPath: true)
self.dismissView(outcome: .OK)
dismissView(outcome: .OK)
// Reset search
App.shared.siteListWindowController?
App.shared.domainListWindowController?
.searchToolbarItem
.searchField.stringValue = ""
// Add the new item and scrolls to it
App.shared.siteListWindowController?
App.shared.domainListWindowController?
.contentVC
.addedNewSite(
name: name,
secure: buttonSecure.state == .on
)
}
@IBAction func pressedCancel(_ sender: Any) {
self.dismissView(outcome: .cancel)
dismissView(outcome: .cancel)
}
@IBAction func pressedSecure(_ sender: Any) {
updatePreview()
}
// MARK: - Text Field Delegate
func controlTextDidChange(_ obj: Notification) {
updateTextField()
}
// MARK: - Helper Methods
private func isValidLinkName(_ name: String) -> Bool {
if self.linkName.stringValue.isEmpty {
self.textFieldError.isHidden = false
self.textFieldError.stringValue = "site_list.add.errors.empty".localized
if name.isEmpty {
textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.empty".localized
return false
}
if Valet.shared.sites.contains(where: { $0.name == name }) {
self.textFieldError.isHidden = false
self.textFieldError.stringValue = "site_list.add.errors.already_exists".localized
textFieldError.isHidden = false
textFieldError.stringValue = "domain_list.add.errors.already_exists".localized
return false
}
self.textFieldError.isHidden = true
textFieldError.isHidden = true
return true
}
func updateTextField() {
self.linkName.stringValue = self.linkName.stringValue
inputDomainName.stringValue = inputDomainName.stringValue
.replacingOccurrences(of: " ", with: "-")
buttonCreateLink.isEnabled = isValidLinkName(self.linkName.stringValue)
self.updatePreview()
buttonCreateLink.isEnabled = isValidLinkName(inputDomainName.stringValue)
updatePreview()
}
func updatePreview() {
buttonSecure.title = "site_list.add.secure_after_creation"
buttonSecure.title = "domain_list.add.secure_after_creation"
.localized(
self.linkName.stringValue,
inputDomainName.stringValue,
Valet.shared.config.tld
)
previewText.stringValue = "site_list.add.folder_available"
if inputDomainName.stringValue.isEmpty {
previewText.stringValue = "domain_list.add.empty_fields".localized
return
}
previewText.stringValue = "domain_list.add.folder_available"
.localized(
self.buttonSecure.state == .on ? "https" : "http",
self.linkName.stringValue,
buttonSecure.state == .on ? "https" : "http",
inputDomainName.stringValue,
Valet.shared.config.tld
)
}

View File

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

View File

@ -1,5 +1,5 @@
//
// SiteListTypeCell.swift
// DomainListTypeCell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/03/2022.
@ -9,12 +9,11 @@
import Cocoa
import AppKit
class SiteListKindCell: NSTableCellView, SiteListCellProtocol
{
static let reusableName = "siteListKindCell"
class DomainListKindCell: NSTableCellView, DomainListCellProtocol {
static let reusableName = "domainListKindCell"
@IBOutlet weak var imageViewType: NSImageView!
func populateCell(with site: ValetSite) {
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
imageViewType.image = NSImage(
@ -22,12 +21,16 @@ class SiteListKindCell: NSTableCellView, SiteListCellProtocol
? "IconParked"
: "IconLinked"
)
// Unless, of course, this is a default site
if site.absolutePath == Valet.shared.config.defaultSite {
imageViewType.image = NSImage(named: "IconDefault")
}
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
}
func populateCell(with proxy: ValetProxy) {
imageViewType.image = NSImage(named: "IconProxy")
}
}

View File

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

View File

@ -1,5 +1,5 @@
//
// SiteListPhpCell.swift
// DomainListPhpCell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/03/2022.
@ -9,57 +9,69 @@
import Cocoa
import AppKit
class SiteListPhpCell: NSTableCellView, SiteListCellProtocol
{
static let reusableName = "siteListPhpCell"
var site: ValetSite? = nil
class DomainListPhpCell: NSTableCellView, DomainListCellProtocol {
static let reusableName = "domainListPhpCell"
var site: ValetSite?
@IBOutlet weak var buttonPhpVersion: NSButton!
@IBOutlet weak var imageViewPhpVersionOK: NSImageView!
func populateCell(with site: ValetSite) {
self.site = site
buttonPhpVersion.title = " PHP \(site.servingPhpVersion)"
imageViewPhpVersionOK.toolTip = nil
if site.isolatedPhpVersion != nil {
imageViewPhpVersionOK.isHidden = false
imageViewPhpVersionOK.image = NSImage(named: "Isolated")
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.isolated".localized(site.servingPhpVersion)
} else {
imageViewPhpVersionOK.isHidden = (site.composerPhp == "???" || !site.composerPhpCompatibleWithLinked)
imageViewPhpVersionOK.image = NSImage(named: "Checkmark")
imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp)
}
buttonPhpVersion.isHidden = false
imageViewPhpVersionOK.isHidden = false
}
func populateCell(with proxy: ValetProxy) {
buttonPhpVersion.isHidden = true
imageViewPhpVersionOK.isHidden = true
return
}
@IBAction func pressedPhpVersion(_ sender: Any) {
guard let site = self.site else { return }
let alert = NSAlert.init()
alert.alertStyle = .informational
var information = ""
if (self.site?.isolatedPhpVersion != nil) {
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
@ -70,9 +82,9 @@ class SiteListPhpCell: NSTableCellView, SiteListCellProtocol
map[mapIndex] = version.homebrewVersion
mapIndex += 1
}
// Site is not isolated, show options to switch global PHP version
alert.beginSheetModal(for: App.shared.siteListWindowController!.window!) { response in
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]!
@ -83,8 +95,8 @@ class SiteListPhpCell: NSTableCellView, SiteListCellProtocol
}
} else {
// Site is isolated, do not show any options to switch
alert.beginSheetModal(for: App.shared.siteListWindowController!.window!)
alert.beginSheetModal(for: App.shared.domainListWindowController!.window!)
}
}
}

View File

@ -0,0 +1,28 @@
//
// DomainListNameCell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
import AppKit
class DomainListTLSCell: NSTableCellView, DomainListCellProtocol {
static let reusableName = "domainListTLSCell"
@IBOutlet weak var imageViewLock: NSImageView!
func populateCell(with site: ValetSite) {
imageViewLock.contentTintColor = site.secured
? NSColor(named: "IconColorGreen")
: NSColor(named: "IconColorRed")
}
func populateCell(with proxy: ValetProxy) {
imageViewLock.contentTintColor = proxy.secured
? NSColor(named: "IconColorGreen")
: NSColor(named: "IconColorRed")
}
}

View File

@ -1,5 +1,5 @@
//
// SiteListTypeCell.swift
// DomainListTypeCell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/03/2022.
@ -9,23 +9,28 @@
import Cocoa
import AppKit
class SiteListTypeCell: NSTableCellView, SiteListCellProtocol
{
static let reusableName = "siteListTypeCell"
class DomainListTypeCell: NSTableCellView, DomainListCellProtocol {
static let reusableName = "domainListTypeCell"
@IBOutlet weak var labelDriver: NSTextField!
@IBOutlet weak var labelPhpVersion: NSTextField!
func populateCell(with site: ValetSite) {
labelDriver.stringValue = site.driver ?? "driver.not_detected".localized
// Determine the Laravel version
if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") {
let constraint = site.notableComposerDependencies["laravel/framework"]!
labelDriver.stringValue = "Laravel (\(constraint))"
}
// PHP version
labelPhpVersion.stringValue = site.composerPhp == "???" ? "Any PHP" : "PHP \(site.composerPhp)"
}
func populateCell(with proxy: ValetProxy) {
labelDriver.stringValue = "Proxy"
labelPhpVersion.stringValue = "Active"
return
}
}

View File

@ -1,23 +1,54 @@
//
// SiteListVC+Actions.swift
// DomainListVC+Actions.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
extension SiteListVC {
extension DomainListVC {
@objc func toggleSecure() {
if selected is ValetSite {
toggleSecureForSite()
} else {
toggleSecureForProxy()
}
}
func toggleSecureForProxy() {
let originalSecureStatus = selectedProxy!.secured
let selectedProxy = selectedProxy!
self.waitAndExecute {
// 1. Remove the original proxy
Shell.run("\(Paths.valet) unproxy \(selectedProxy.domain)", requiresPath: true)
// 2. Add a new proxy, which is either secured/unsecured
let secure = originalSecureStatus ? "" : " --secure"
Shell.run("\(Paths.valet) proxy \(selectedProxy.domain) \(selectedProxy.target)\(secure)",
requiresPath: true)
// 3. Restart nginx
Actions.restartNginx()
// 4. Reload site list
DispatchQueue.main.async {
App.shared.domainListWindowController?.pressedReload(nil)
}
}
}
func toggleSecureForSite() {
let rowToReload = tableView.selectedRow
let originalSecureStatus = selectedSite!.secured
let action = selectedSite!.secured ? "unsecure" : "secure"
let selectedSite = selectedSite!
let command = "cd '\(selectedSite.absolutePath)' && sudo \(Paths.valet) \(action) && exit;"
waitAndExecute {
Shell.run(command, requiresPath: true)
} completion: { [self] in
@ -25,109 +56,137 @@ extension SiteListVC {
if selectedSite.secured == originalSecureStatus {
BetterAlert()
.withInformation(
title: "site_list.alerts_status_not_changed.title".localized,
subtitle: "site_list.alerts_status_not_changed.desc".localized(command)
title: "domain_list.alerts_status_not_changed.title".localized,
subtitle: "domain_list.alerts_status_not_changed.desc".localized(command)
)
.withPrimary(text: "OK")
.show()
} else {
let newState = selectedSite.secured ? "secured" : "unsecured"
LocalNotification.send(
title: "site_list.alerts_status_changed.title".localized,
subtitle: "site_list.alerts_status_changed.desc"
title: "domain_list.alerts_status_changed.title".localized,
subtitle: "domain_list.alerts_status_changed.desc"
.localized(
"\(selectedSite.name).\(Valet.shared.config.tld)",
newState
)
)
}
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4])
tableView.deselectRow(rowToReload)
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
}
}
@objc func openInBrowser() {
let prefix = selectedSite!.secured ? "https://" : "http://"
let url = URL(string: "\(prefix)\(selectedSite!.name).\(Valet.shared.config.tld)")
if url != nil {
NSWorkspace.shared.open(url!)
} else {
guard let selected = self.selected else {
return
}
guard let url = selected.getListableUrl() else {
BetterAlert()
.withInformation(
title: "site_list.alert.invalid_folder_name".localized,
subtitle: "site_list.alert.invalid_folder_name_desc".localized
title: "domain_list.alert.invalid_folder_name".localized,
subtitle: "domain_list.alert.invalid_folder_name_desc".localized
)
.withPrimary(text: "OK")
.show()
return
}
NSWorkspace.shared.open(url)
}
@objc func openInFinder() {
Shell.run("open '\(selectedSite!.absolutePath)'")
}
@objc func openInTerminal() {
Shell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath)'")
}
@objc func openWithEditor(sender: EditorMenuItem) {
guard let editor = sender.editor else { return }
editor.openDirectory(file: selectedSite!.absolutePath)
}
@objc func isolateSite(sender: PhpMenuItem) {
let command = "sudo \(Paths.valet) isolate php@\(sender.version) --site '\(self.selectedSite!.name)' && exit;"
self.performAction(command: command) {
self.selectedSite!.determineIsolated()
if self.selectedSite!.isolatedPhpVersion == nil {
BetterAlert()
.withInformation(
title: "site_list.alerts_isolation_failed.title".localized,
subtitle: "site_list.alerts_isolation_failed.subtitle".localized,
description: "site_list.alerts_isolation_failed.desc".localized(command)
title: "domain_list.alerts_isolation_failed.title".localized,
subtitle: "domain_list.alerts_isolation_failed.subtitle".localized,
description: "domain_list.alerts_isolation_failed.desc".localized(command)
)
.withPrimary(text: "OK")
.show()
}
}
}
@objc func removeIsolatedSite() {
self.performAction(command: "sudo \(Paths.valet) unisolate --site '\(self.selectedSite!.name)' && exit;") {
self.selectedSite!.isolatedPhpVersion = nil
}
}
@objc func unlinkSite() {
guard let site = selectedSite else {
return
}
if site.aliasPath == nil {
return
}
Alert.confirm(
onWindow: view.window!,
messageText: "site_list.confirm_unlink".localized(site.name),
informativeText: "site_link.confirm_link".localized,
buttonTitle: "site_list.unlink".localized,
messageText: "domain_list.confirm_unlink".localized(site.name),
informativeText: "domain_list.confirm_unlink_desc".localized,
buttonTitle: "domain_list.unlink".localized,
secondButtonTitle: "Cancel",
style: .critical,
onFirstButtonPressed: {
Shell.run("valet unlink '\(site.name)'", requiresPath: true)
self.reloadSites()
self.waitAndExecute {
Shell.run("valet unlink '\(site.name)'", requiresPath: true)
} completion: {
self.reloadDomains()
}
}
)
}
@objc func removeProxy() {
guard let proxy = selectedProxy else {
return
}
Alert.confirm(
onWindow: view.window!,
messageText: "domain_list.confirm_unproxy".localized("\(proxy.domain).\(proxy.tld)"),
informativeText: "domain_list.confirm_unproxy_desc".localized,
buttonTitle: "domain_list.unproxy".localized,
secondButtonTitle: "Cancel",
style: .critical,
onFirstButtonPressed: {
self.waitAndExecute {
Shell.run("valet unproxy '\(proxy.domain)'", requiresPath: true)
} completion: {
self.reloadDomains()
}
}
)
}
private func performAction(command: String, beforeCellReload: @escaping () -> Void) {
let rowToReload = tableView.selectedRow
waitAndExecute {
Shell.run(command, requiresPath: true)
} completion: { [self] in
@ -137,5 +196,5 @@ extension SiteListVC {
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
}
}
}

View File

@ -1,65 +1,78 @@
//
// SiteListVC+ContextMenu.swift
// DomainListVC+ContextMenu.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension SiteListVC {
extension DomainListVC {
internal func reloadContextMenu() {
guard let site = selectedSite else {
guard let selected = selected else {
tableView.menu = nil
return
}
if let selected = selected as? ValetSite {
addMenuItemsForSite(selected)
return
}
if let selected = selected as? ValetProxy {
addMenuItemsForProxy(selected)
return
}
}
// MARK: - Menu Items for Site
private func addMenuItemsForSite(_ site: ValetSite) {
let menu = NSMenu()
addSystemApps(to: menu)
addSeparator(to: menu)
addDetectedApps(to: menu)
addSeparator(to: menu)
if Valet.enabled(feature: .isolatedSites) {
addIsolate(to: menu, with: site)
} else {
addDisabledIsolation(to: menu)
}
addUnlink(to: menu, with: site)
addToggleSecure(to: menu, with: site)
addToggleSecure(to: menu, secured: site.secured)
tableView.menu = menu
}
private func addSystemApps(to menu: NSMenu) {
menu.addItem(withTitle: "site_list.system_apps".localized, action: nil, keyEquivalent: "")
menu.addItem(withTitle: "domain_list.system_apps".localized, action: nil, keyEquivalent: "")
menu.addItem(
withTitle: "site_list.open_in_finder".localized,
withTitle: "domain_list.open_in_finder".localized,
action: #selector(self.openInFinder),
keyEquivalent: "F"
)
menu.addItem(
withTitle: "site_list.open_in_terminal".localized,
withTitle: "domain_list.open_in_terminal".localized,
action: #selector(self.openInTerminal),
keyEquivalent: "T"
)
menu.addItem(
withTitle: "site_list.open_in_browser".localized,
withTitle: "domain_list.open_in_browser".localized,
action: #selector(self.openInBrowser),
keyEquivalent: "B"
)
}
private func addDetectedApps(to menu: NSMenu) {
if (applications.count > 0) {
if !applications.isEmpty {
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: "site_list.detected_apps".localized, action: nil, keyEquivalent: "")
for (_, editor) in applications.enumerated() {
menu.addItem(withTitle: "domain_list.detected_apps".localized, action: nil, keyEquivalent: "")
for editor in applications {
let editorMenuItem = EditorMenuItem(
title: "Open with \(editor.name)",
action: #selector(self.openWithEditor(sender:)),
@ -70,61 +83,94 @@ extension SiteListVC {
}
}
}
private func addUnlink(to menu: NSMenu, with site: ValetSite) {
if (site.aliasPath != nil) {
if site.aliasPath != nil {
menu.addItem(
withTitle: "site_list.unlink".localized,
withTitle: "domain_list.unlink".localized,
action: #selector(self.unlinkSite),
keyEquivalent: ""
)
menu.addItem(NSMenuItem.separator())
}
}
private func addDisabledIsolation(to menu: NSMenu) {
menu.addItem(withTitle: "site_list.isolation_unavailable".localized, action: nil, keyEquivalent: "")
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:"site_list.isolate".localized, action: nil, keyEquivalent: "")
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: "")
let item = PhpMenuItem(
title: "Always use PHP \(version)",
action: #selector(self.isolateSite),
keyEquivalent: ""
)
item.version = version
submenu.addItem(item)
}
menu.setSubmenu(submenu, for: isolationMenuItem)
menu.addItem(isolationMenuItem)
menu.addItem(NSMenuItem.separator())
} else {
// REMOVE ISOLATION POSSIBLE
menu.addItem(
withTitle: "site_list.remove_isolation".localized,
withTitle: "domain_list.remove_isolation".localized,
action: #selector(self.removeIsolatedSite),
keyEquivalent: ""
)
menu.addItem(NSMenuItem.separator())
}
}
private func addToggleSecure(to menu: NSMenu, with site: ValetSite) {
private func addToggleSecure(to menu: NSMenu, secured: Bool) {
menu.addItem(
withTitle: site.secured
? "site_list.unsecure".localized
: "site_list.secure".localized,
withTitle: secured
? "domain_list.unsecure".localized
: "domain_list.secure".localized,
action: #selector(toggleSecure),
keyEquivalent: ""
)
}
// MARK: - Menu Items for Proxy
private func addMenuItemsForProxy(_ proxy: ValetProxy) {
let menu = NSMenu()
addOpenProxyInBrowser(to: menu)
addSeparator(to: menu)
addToggleSecure(to: menu, secured: proxy.secured)
addRemoveProxy(to: menu)
tableView.menu = menu
}
private func addOpenProxyInBrowser(to menu: NSMenu) {
menu.addItem(
withTitle: "domain_list.open_in_browser".localized,
action: #selector(self.openInBrowser),
keyEquivalent: "B"
)
}
private func addRemoveProxy(to menu: NSMenu) {
menu.addItem(
withTitle: "domain_list.unproxy".localized,
action: #selector(self.removeProxy),
keyEquivalent: ""
)
}
// MARK: - Shared
private func addSeparator(to menu: NSMenu) {
menu.addItem(NSMenuItem.separator())
}
}

View File

@ -1,119 +1,134 @@
//
// SiteListVC.swift
// DomainListVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
import Carbon
class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
// MARK: - Outlets
@IBOutlet weak var tableView: NSTableView!
@IBOutlet weak var progressIndicator: NSProgressIndicator!
// MARK: - Variables
/// List of sites that will be displayed in this view. Originates from the `Valet` object.
var sites: [ValetSite] = []
var domains: [DomainListable] = []
/// Array that contains various apps that might open a particular site directory.
var applications: [Application] {
return App.shared.detectedApplications
}
/// The last sort descriptor used.
var sortDescriptor: NSSortDescriptor? = nil
var sortDescriptor: NSSortDescriptor?
/// String that was last searched for. Empty by default.
var lastSearchedFor = ""
// MARK: - Helper Variables
var selectedSite: ValetSite? {
if tableView.selectedRow == -1 {
return nil
}
return sites[tableView.selectedRow]
return domains[tableView.selectedRow] as? ValetSite
}
var timer: Timer? = nil
var selectedProxy: ValetProxy? {
if tableView.selectedRow == -1 {
return nil
}
return domains[tableView.selectedRow] as? ValetProxy
}
var selected: DomainListable? {
if tableView.selectedRow == -1 {
return nil
}
return domains[tableView.selectedRow]
}
var timer: Timer?
// MARK: - Display
public static func create(delegate: NSWindowDelegate?) {
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(
withIdentifier: "siteListWindow"
) as! SiteListWC
windowController.window!.title = "site_list.title".localized
windowController.window!.subtitle = "site_list.subtitle".localized
withIdentifier: "domainListWindow"
) as! DomainListWC
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("siteListWindow")
App.shared.siteListWindowController = windowController
windowController.window!.setFrameAutosaveName("domainListWindow")
App.shared.domainListWindowController = windowController
}
public static func show(delegate: NSWindowDelegate? = nil) {
if (App.shared.siteListWindowController == nil) {
if App.shared.domainListWindowController == nil {
Self.create(delegate: delegate)
}
App.shared.siteListWindowController!.showWindow(self)
App.shared.domainListWindowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
}
// MARK: - Lifecycle
override func viewDidLoad() {
tableView.doubleAction = #selector(self.doubleClicked(sender:))
if !Valet.shared.sites.isEmpty {
// Preloaded list
sites = Valet.shared.sites
domains = Valet.getDomainListable()
searchedFor(text: lastSearchedFor)
} else {
reloadSites()
reloadDomains()
}
}
// MARK: - Async Operations
/**
Disables the UI so the user cannot interact with it.
Also shows a spinner to indicate that we're busy.
*/
private func setUIBusy() {
public func setUIBusy() {
// If it takes more than 0.5s to set the UI to not busy, show a spinner
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in
self.progressIndicator.startAnimation(true)
})
tableView.alphaValue = 0.3
tableView.isEnabled = false
tableView.selectRowIndexes([], byExtendingSelection: true)
}
/**
Re-enables the UI so the user can interact with it.
*/
private func setUINotBusy() {
public func setUINotBusy() {
timer?.invalidate()
progressIndicator.stopAnimation(nil)
tableView.alphaValue = 1.0
tableView.isEnabled = true
}
/**
Executes a specific callback and fires the completion callback,
while updating the UI as required. As long as the completion callback
@ -122,12 +137,11 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
- Parameter execute: Callback of the work that needs to happen.
- Parameter completion: Callback that is fired when the work is done.
*/
internal func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
{
internal func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {}) {
setUIBusy()
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
execute()
// For a smoother animation, expect at least a 0.2 second delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
completion()
@ -135,40 +149,35 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
}
}
}
// MARK: - Site Data Loading
func reloadSites() {
func reloadDomains() {
waitAndExecute {
Valet.shared.reloadSites()
} completion: { [self] in
sites = Valet.shared.sites
domains = Valet.shared.sites
searchedFor(text: lastSearchedFor)
}
}
func applySortDescriptor(_ descriptor: NSSortDescriptor) {
sortDescriptor = descriptor
var sorted = self.sites
var sorted = self.domains
switch descriptor.key {
case "Secure":
sorted = self.sites.sorted { $0.secured && !$1.secured }; break
case "Domain":
sorted = self.sites.sorted { $0.absolutePath < $1.absolutePath }; break
case "PHP":
sorted = self.sites.sorted { $0.servingPhpVersion < $1.servingPhpVersion }; break
case "Kind":
sorted = self.sites.sorted { ($0.aliasPath == nil) && !($1.aliasPath == nil) }; break
case "Type":
sorted = self.sites.sorted { $0.driver ?? "ZZZ" < $1.driver ?? "ZZZ" }; break
default: break;
case "Secure": sorted = self.domains.sorted { $0.getListableSecured() && !$1.getListableSecured() }
case "Domain": sorted = self.domains.sorted { $0.getListableAbsolutePath() < $1.getListableAbsolutePath() }
case "PHP": sorted = self.domains.sorted { $0.getListablePhpVersion() < $1.getListablePhpVersion() }
case "Kind": sorted = self.domains.sorted { $0.getListableKind() < $1.getListableKind() }
case "Type": sorted = self.domains.sorted { $0.getListableType() < $1.getListableType() }
default: break
}
self.sites = descriptor.ascending ? sorted.reversed() : sorted
self.domains = descriptor.ascending ? sorted.reversed() : sorted
}
func addedNewSite(name: String, secure: Bool) {
waitAndExecute {
Valet.shared.reloadSites()
@ -176,27 +185,27 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
find(name, secure)
}
}
private func find(_ name: String, _ secure: Bool = false) {
sites = Valet.shared.sites
domains = Valet.getDomainListable()
searchedFor(text: "")
if let site = sites.enumerated().first(where: { $0.element.name == name }) {
if let site = domains.enumerated().first(where: { $0.element.getListableName() == name }) {
DispatchQueue.main.async {
self.tableView.selectRowIndexes([site.offset], byExtendingSelection: false)
self.tableView.scrollRowToVisible(site.offset)
if (secure && !site.element.secured) {
if secure && !site.element.getListableSecured() {
self.toggleSecure()
}
}
}
}
// MARK: - Table View Delegate
func numberOfRows(in tableView: NSTableView) -> Int {
return sites.count
return domains.count
}
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
guard let sortDescriptor = tableView.sortDescriptors.first else { return }
// Kinda scuffed way of applying sort descriptors here, but it works.
@ -204,82 +213,86 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
applySortDescriptor(sortDescriptor)
searchedFor(text: lastSearchedFor)
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let mapping: [String: String] = [
"TLS": SiteListTLSCell.reusableName,
"DOMAIN": SiteListNameCell.reusableName,
"ENVIRONMENT": SiteListPhpCell.reusableName,
"KIND": SiteListKindCell.reusableName,
"TYPE": SiteListTypeCell.reusableName,
"TLS": DomainListTLSCell.reusableName,
"DOMAIN": DomainListNameCell.reusableName,
"ENVIRONMENT": DomainListPhpCell.reusableName,
"KIND": DomainListKindCell.reusableName,
"TYPE": DomainListTypeCell.reusableName
]
let columnName = tableColumn!.identifier.rawValue
let identifier = NSUserInterfaceItemIdentifier(rawValue: mapping[columnName]!)
guard let userCell = tableView.makeView(withIdentifier: identifier, owner: self)
as? SiteListCellProtocol else { return nil }
userCell.populateCell(with: sites[row])
as? DomainListCellProtocol else { return nil }
if let site = domains[row] as? ValetSite {
userCell.populateCell(with: site)
}
if let proxy = domains[row] as? ValetProxy {
userCell.populateCell(with: proxy)
}
return userCell as? NSView
}
func tableViewSelectionDidChange(_ notification: Notification) {
reloadContextMenu()
}
@objc func doubleClicked(sender: Any) {
guard self.selectedSite != nil else {
guard self.selected != nil else {
return
}
self.openInBrowser()
}
// MARK: - (Search) Text Field Delegate
func searchedFor(text: String) {
lastSearchedFor = text
let searchString = text.lowercased()
if searchString.isEmpty {
sites = Valet.shared.sites
if let sortDescriptor = sortDescriptor {
self.applySortDescriptor(sortDescriptor)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
return
}
let splitSearchString: [String] = searchString
.split(separator: " ")
.map { return String($0) }
sites = Valet.shared.sites.filter({ site in
return !splitSearchString.map { searchString in
return site.name.lowercased().contains(searchString)
}.contains(false)
})
func reloadTable() {
if let sortDescriptor = sortDescriptor {
self.applySortDescriptor(sortDescriptor)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
func searchedFor(text: String) {
lastSearchedFor = text
let searchString = text.lowercased()
if searchString.isEmpty {
domains = Valet.getDomainListable()
reloadTable()
return
}
let splitSearchString: [String] = searchString
.split(separator: " ")
.map { return String($0) }
domains = Valet.getDomainListable().filter({ site in
return !splitSearchString.map { searchString in
return site.getListableName().lowercased().contains(searchString)
}.contains(false)
})
reloadTable()
}
// MARK: - Deinitialization
deinit {
Log.perf("SiteListVC deallocated")
Log.perf("DomainListVC deallocated")
}
}

View File

@ -1,68 +1,93 @@
//
// SiteListWC.swift
// DomainListWC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 03/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
class SiteListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
class DomainListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
// MARK: - Window Identifier
override var windowName: String {
return "SiteList"
return "DomainList"
}
// MARK: - Outlets
@IBOutlet weak var searchToolbarItem: NSSearchToolbarItem!
// MARK: - Window Lifecycle
override func windowDidLoad() {
super.windowDidLoad()
self.searchToolbarItem.searchField.delegate = self
self.searchToolbarItem.searchField.becomeFirstResponder()
}
// MARK: - Search functionality
var contentVC: SiteListVC {
return self.contentViewController as! SiteListVC
var contentVC: DomainListVC {
return self.contentViewController as! DomainListVC
}
var searchTimer: Timer?
func controlTextDidChange(_ notification: Notification) {
guard let searchField = notification.object as? NSSearchField else {
return
}
self.searchTimer?.invalidate()
searchTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false, block: { _ in
self.contentVC.searchedFor(text: searchField.stringValue)
})
}
// MARK: - Reload functionality
@IBAction func pressedReload(_ sender: Any?) {
contentVC.reloadSites()
contentVC.reloadDomains()
}
@IBAction func pressedAddLink(_ sender: Any?) {
selectFolder()
showSelectionWindow()
}
// MARK: - Add a new site
func selectFolder() {
func showSelectionWindow() {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(
withIdentifier: "showSelectionWindow"
) as! NSWindowController
let viewController = windowController.window!
.contentViewController as! SelectionVC
viewController.domainListWC = self
self.window?.beginSheet(windowController.window!)
}
func startCreateLinkFlow() {
self.showFolderSelectionForLink()
}
func startCreateProxyFlow() {
self.showProxyPopup()
}
// MARK: - Popups
private func showFolderSelectionForLink() {
let dialog = NSOpenPanel()
dialog.message = "site_list.add.modal_description".localized
dialog.message = "domain_list.add.modal_description".localized
dialog.showsResizeIndicator = true
dialog.showsHiddenFiles = false
dialog.allowsMultipleSelection = false
@ -70,25 +95,37 @@ class SiteListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
dialog.canChooseFiles = false
dialog.beginSheetModal(for: self.window!) { response in
let result = dialog.url
if (result != nil && response == .OK) {
if result != nil && response == .OK {
let path: String = result!.path
self.showSitePopup(path)
self.showLinkPopup(path)
}
}
}
func showSitePopup(_ folder: String) {
let storyboard = NSStoryboard(name: "Main", bundle : nil)
private func showLinkPopup(_ folder: String) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(
withIdentifier: "addSiteWindow"
) as! NSWindowController
let viewController = windowController.window!.contentViewController as! AddSiteVC
viewController.pathControl.url = URL(fileURLWithPath: folder)
viewController.linkName.stringValue = String(folder.split(separator: "/").last!)
viewController.inputDomainName.stringValue = String(folder.split(separator: "/").last!)
viewController.updateTextField()
self.window?.beginSheet(windowController.window!)
}
private func showProxyPopup() {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(
withIdentifier: "addProxyWindow"
) as! NSWindowController
// let viewController = windowController.window!.contentViewController as! AddSiteVC
self.window?.beginSheet(windowController.window!)
}
}

View File

@ -0,0 +1,62 @@
//
// SelectionVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 14/04/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class SelectionVC: NSViewController {
weak var domainListWC: DomainListWC?
@IBOutlet weak var textFieldTitle: NSTextField!
@IBOutlet weak var textFieldDescription: NSTextField!
@IBOutlet weak var buttonCreateLink: NSButton!
@IBOutlet weak var buttonCreateProxy: NSButton!
@IBOutlet weak var buttonCancel: NSButton!
override func viewDidLoad() {
super.viewDidLoad()
loadStaticLocalisedStrings()
}
override func viewDidAppear() {
view.window?.makeFirstResponder(buttonCreateLink)
}
private func dismissView(outcome: NSApplication.ModalResponse) {
guard let window = self.view.window, let parent = window.sheetParent else { return }
parent.endSheet(window, returnCode: outcome)
}
// MARK: - Localisation
func loadStaticLocalisedStrings() {
textFieldTitle.stringValue = "selection.title".localized
textFieldDescription.stringValue = "selection.description".localized
buttonCancel.title = "selection.cancel".localized
buttonCreateLink.title = "selection.create_link".localized
buttonCreateProxy.title = "selection.create_proxy".localized
}
// MARK: - Outlet Interactions
@IBAction func pressedCreateLink(_ sender: Any) {
self.dismissView(outcome: .continue)
domainListWC?.startCreateLinkFlow()
}
@IBAction func pressedCreateProxy(_ sender: Any) {
self.dismissView(outcome: .continue)
domainListWC?.startCreateProxyFlow()
}
@IBAction func pressedCancel(_ sender: Any) {
self.dismissView(outcome: .cancel)
}
}

View File

@ -13,59 +13,58 @@ import Foundation
to this object.
*/
struct ComposerJson: Decodable {
// MARK: - JSON structure
let dependencies: Dictionary<String, String>?
let devDependencies: Dictionary<String, String>?
let dependencies: [String: String]?
let devDependencies: [String: String]?
let configuration: Config?
private enum CodingKeys: String, CodingKey {
case dependencies = "require"
case devDependencies = "require-dev"
case configuration = "config"
}
struct Config: Decodable {
let platform: Platform?
}
struct Platform: Decodable {
let php: String?
}
// MARK: - Helpers
/**
Checks what the PHP version constraint is.
Returns a tuple (constraint, location of constraint).
*/
public func getPhpVersion() -> (String, ValetSite.VersionSource)
{
public func getPhpVersion() -> (String, ValetSite.VersionSource) {
// Check if in platform
if configuration?.platform?.php != nil {
return (configuration!.platform!.php!, .platform)
}
// Check if in dependencies
if dependencies?["php"] != nil {
return (dependencies!["php"]!, .require)
}
// Unknown!
return ("???", .unknown)
}
/**
Checks if any notable dependencies can be resolved.
Only notable dependencies are saved.
*/
public func getNotableDependencies() -> [String: String] {
var notable: [String: String] = [:]
var scan = Array(PhpFrameworks.DependencyList.keys)
scan.append("php")
scan.forEach { dependency in
if dependencies?[dependency] != nil {
notable[dependency] = dependencies![dependency]
@ -74,7 +73,5 @@ struct ComposerJson: Decodable {
return notable
}
}

View File

@ -10,11 +10,11 @@ import Foundation
class ComposerWindow {
private var menu: MainMenu? = nil
private var menu: MainMenu?
private var shouldNotify: Bool! = nil
private var completion: ((Bool) -> Void)! = nil
private var window: ProgressWindowController? = nil
private var window: ProgressWindowController?
/**
Updates the global dependencies and runs the completion callback when done.
*/
@ -22,33 +22,33 @@ class ComposerWindow {
self.menu = MainMenu.shared
self.shouldNotify = notify
self.completion = completion
Paths.shared.detectBinaryPaths()
if Paths.composer == nil {
presentMissingAlert()
return
}
PhpEnv.shared.isBusy = true
menu?.setBusyImage()
menu?.rebuild()
window = ProgressWindowController.display(
title: "alert.composer_progress.title".localized,
description: "alert.composer_progress.info".localized
)
window?.setType(info: true)
DispatchQueue.global(qos: .userInitiated).async { [self] in
let task = Shell.user.createTask(
for: "\(Paths.composer!) global update", requiresPath: true
)
DispatchQueue.main.async {
self.window?.addToConsole("\(Paths.composer!) global update\n")
}
task.listen(
didReceiveStandardOutputData: { string in
DispatchQueue.main.async {
@ -63,11 +63,11 @@ class ComposerWindow {
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
}
)
task.launch()
task.waitUntilExit()
task.haltListening()
if task.terminationStatus <= 0 {
composerUpdateSucceeded()
} else {
@ -75,12 +75,12 @@ class ComposerWindow {
}
}
}
private func composerUpdateSucceeded() {
// Closing the window should happen after a slight delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in
window?.close()
if (shouldNotify) {
if shouldNotify {
LocalNotification.send(
title: "alert.composer_success.title".localized,
subtitle: "alert.composer_success.info".localized
@ -91,7 +91,7 @@ class ComposerWindow {
completion(true)
}
}
private func composerUpdateFailed() {
// Showing that something failed should be shown immediately
DispatchQueue.main.async { [self] in
@ -103,18 +103,18 @@ class ComposerWindow {
completion(false)
}
}
// MARK: Main Menu Update
private func removeBusyStatus() {
PhpEnv.shared.isBusy = false
DispatchQueue.main.async { [self] in
menu?.updatePhpVersionInStatusBar()
}
}
// MARK: Alert
private func presentMissingAlert() {
BetterAlert()
.withInformation(

View File

@ -9,7 +9,7 @@
import Foundation
struct PhpFrameworks {
/**
This list should probably be reversed when checked, because some of these
will also require either `laravel/framework` or `symfony/symfony`.
@ -17,10 +17,10 @@ struct PhpFrameworks {
public static let DependencyList = [
// COMMON FRAMEWORKS
"laravel/framework" : "Laravel",
"laravel/framework": "Laravel",
"symfony/symfony": "Symfony",
"laravel/lumen": "Lumen",
// VARIOUS CMS
"roots/bedrock": "Bedrock",
"cakephp/app": "CakePHP",
@ -37,15 +37,15 @@ struct PhpFrameworks {
"johnpbloch/wordpress-core": "WordPress",
"zendframework/zendframework": "Zend",
"zendframework/zend-mvc": "Zend",
"typo3/cms-core": "Typo3",
// TODO (5.1): Handle these in v5.1
"typo3/cms-core": "Typo3"
// TODO (6.0): Handle these in v6.0
// "magento/*": "Magento",
// "concrete5/*": "Concrete5",
// "contao/*": "Contao",
// "slim/*": "Slim",
]
public static let FileMapping: [String: [String]] = [
"Drupal": [
// Legacy installations
@ -61,10 +61,10 @@ struct PhpFrameworks {
],
"Typo3": [
"/typo3",
"/public/typo3",
"/public/typo3"
]
]
/**
There are two cases where users are unlikely to use `composer`,
when setting up a Drupal or a WordPress project. For performance
@ -75,13 +75,13 @@ struct PhpFrameworks {
let found = entry.value
.map { path in return Filesystem.fileExists(basePath + path) }
.contains(true)
if found {
return entry.key
}
}
return nil
}
}

View File

@ -3,64 +3,106 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 28/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class HomebrewDiagnostics {
/**
Determines the Homebrew taps the user has installed.
*/
public static var installedTaps: [String] = {
return Shell
.pipe("\(Paths.brew) tap")
.split(separator: "\n")
.map { string in
return String(string)
}
}()
/**
Determines whether the PHP Monitor Cask is installed.
*/
public static var customCaskInstalled: Bool = {
return installedTaps.contains("nicoverbruggen/cask")
}()
/**
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
This will then result in two different aliases claiming to point to the same formula (`php`).
This will break all linking functionality in PHP Monitor, and the user needs to be informed of this.
This check only needs to be performed if the `shivammathur/php` tap is active.
*/
public static func hasAliasConflict() -> Bool
{
public static func checkForCaskConflict() {
if hasAliasConflict() {
presentAlertAboutConflict()
}
}
/**
Check if the alias conflict as documented in `checkForCaskConflict` actually occurred.
*/
private static func hasAliasConflict() -> Bool {
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
Log.info("The user does not appear to have tapped: shivammathur/php")
return false
} else {
Log.info("The user DOES have the following tapped: shivammathur/php")
Log.info("Checking for `php` formula conflicts...")
let tapPhp = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: tapAlias.data(using: .utf8)!
).first!
if tapPhp.version != PhpEnv.brewPhpVersion {
Log.warn("The `php` formula alias seems to be the different between the tap and core. This could be a problem!")
Log.warn("The `php` formula alias seems to be the different between the tap and core. "
+ "This could be a problem!")
Log.info("Determining whether both of these versions are installed...")
let bothInstalled = PhpEnv.shared.availablePhpVersions.contains(tapPhp.version)
&& PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion)
if bothInstalled {
Log.warn("Both conflicting aliases seem to be installed, warning the user!")
} else {
Log.info("Conflicting aliases are not both installed, seems fine!")
}
return bothInstalled
}
Log.info("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
return false
}
}
/**
Show this alert in case the tapped Cask does cause issues because of the conflict.
*/
private static func presentAlertAboutConflict() {
DispatchQueue.main.async {
BetterAlert()
.withInformation(
title: "alert.php_alias_conflict.title".localized,
subtitle: "alert.php_alias_conflict.info".localized
)
.withPrimary(text: "OK")
.show()
}
}
/**
In order to see if we support the --json syntax, we'll query nginx.
If the JSON response cannot be parsed, Homebrew is probably out of date.
*/
public static func cannotLoadService(_ name: String = "nginx") -> Bool
{
public static func cannotLoadService(_ name: String = "nginx") -> Bool {
let serviceInfo = try? JSONDecoder().decode(
[HomebrewService].self,
from: Shell.pipe(
@ -68,7 +110,7 @@ class HomebrewDiagnostics {
requiresPath: true
).data(using: .utf8)!
)
return serviceInfo == nil
}
}

View File

@ -0,0 +1,82 @@
//
// NginxConfiguration.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class NginxConfiguration {
/** 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. */
var domain: String
/** 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)"
)
do {
let fileContents = try String(contentsOfFile: path)
return NginxConfiguration.init(
path: path,
contents: fileContents
)
} catch {
Log.warn("Could not read the nginx configuration file at: `\(filePath)`")
return nil
}
}
init(path: String, contents: String) {
let domain = String(path.split(separator: "/").last!)
let tld = String(domain.split(separator: ".").last!)
self.contents = contents
self.domain = domain.replacingOccurrences(of: ".\(tld)", with: "")
self.tld = tld
}
/**
Retrieves what address this domain is proxying.
*/
lazy var proxy: String? = {
let regex = try! NSRegularExpression(
pattern: #"proxy_pass (?<proxy>.*:\d*)(\/*);"#,
options: []
)
guard let match = regex.firstMatch(in: contents, range: NSRange(location: 0, length: contents.count))
else { return nil }
return contents[Range(match.range(withName: "proxy"), in: contents)!]
}()
/**
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
pattern: #"(ISOLATED_PHP_VERSION=(php)?(@)?)((?<major>\d)(.)?(?<minor>\d))"#,
options: []
)
guard let match = regex.firstMatch(in: contents, range: NSRange(location: 0, length: contents.count))
else { return nil }
let major: String = contents[Range(match.range(withName: "major"), in: contents)!],
minor: String = contents[Range(match.range(withName: "minor"), in: contents)!]
return "\(major).\(minor)"
}()
}

View File

@ -0,0 +1,27 @@
//
// DomainListable.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/04/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol DomainListable {
func getListableName() -> String
func getListableSecured() -> Bool
func getListableAbsolutePath() -> String
func getListablePhpVersion() -> String
func getListableKind() -> String
func getListableType() -> String
func getListableUrl() -> URL?
}

View File

@ -1,39 +0,0 @@
//
// NginxConfigParser.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class NginxConfigParser {
var contents: String!
init(filePath: String) {
self.contents = try! String(contentsOfFile: filePath
.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
)
}
lazy var isolatedVersion: String? = {
let regex = try! NSRegularExpression(
// PHP versions have (so far) never needed multiple digits for version numbers
pattern: #"(ISOLATED_PHP_VERSION=(php)?(@)?)((?<major>\d)(.)?(?<minor>\d))"#,
options: []
)
let match = regex.firstMatch(in: contents, range: NSMakeRange(0, contents.count))
if match == nil {
return nil
}
let major: String = contents[Range(match!.range(withName: "major"), in: contents)!]
let minor: String = contents[Range(match!.range(withName: "minor"), in: contents)!]
return "\(major).\(minor)"
}()
}

View File

@ -0,0 +1,15 @@
//
// ProxyScanner.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 02/04/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol ProxyScanner {
func resolveProxies(directoryPath: String) -> [ValetProxy]
}

View File

@ -0,0 +1,26 @@
//
// ValetProxyScanner.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/04/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class ValetProxyScanner: ProxyScanner {
func resolveProxies(directoryPath: String) -> [ValetProxy] {
return try! FileManager
.default
.contentsOfDirectory(atPath: directoryPath)
.compactMap {
return NginxConfiguration.from(filePath: "\(directoryPath)/\($0)")
}
.filter {
return $0.proxy != nil
}
.map {
return ValetProxy($0)
}
}
}

View File

@ -0,0 +1,13 @@
//
// ValetProxy+Fake.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 02/04/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension ValetProxy {
}

View File

@ -0,0 +1,53 @@
//
// ValetProxy.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class ValetProxy: DomainListable {
var domain: String
var tld: String
var target: String
var secured: Bool = false
init(_ configuration: NginxConfiguration) {
self.domain = configuration.domain
self.tld = configuration.tld
self.target = configuration.proxy!
self.secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
}
// MARK: - DomainListable Protocol
func getListableName() -> String {
return self.domain
}
func getListableSecured() -> Bool {
return self.secured
}
func getListableAbsolutePath() -> String {
return self.domain
}
func getListablePhpVersion() -> String {
return ""
}
func getListableKind() -> String {
return "proxy"
}
func getListableType() -> String {
return "proxy"
}
func getListableUrl() -> URL? {
return URL(string: "\(self.secured ? "https://" : "http://")\(self.domain).\(self.tld)")
}
}

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