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

🚀 Version 5.4

This commit is contained in:
2022-06-28 12:58:36 +02:00
84 changed files with 2862 additions and 1329 deletions

View File

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

View File

@ -9,6 +9,8 @@
/* 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 */; };
5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.swift */; };
5489625928313231004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.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 */; };
@ -43,9 +45,10 @@
C4080FF727BD8C6400BF2C6B /* BetterAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */; };
C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */; };
C4080FFB27BD956700BF2C6B /* BetterAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */; };
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
C40C5C9C2846A40600E28255 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; };
C40C5C9D2846A40600E28255 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; };
C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; };
@ -86,6 +89,8 @@
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 */; };
C42800AA28452AA10099C999 /* StatusMenu+Items.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42800A928452AA10099C999 /* StatusMenu+Items.swift */; };
C42800AB28452AA50099C999 /* StatusMenu+Items.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42800A928452AA10099C999 /* StatusMenu+Items.swift */; };
C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.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 */; };
@ -102,6 +107,8 @@
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 */; };
C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */; };
C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BF2850BD2A007400F1 /* VersionPopoverView.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 */; };
@ -115,7 +122,12 @@
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 */; };
C44F868E2835BD8D005C353A /* phpmon-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C44F868D2835BD8D005C353A /* phpmon-config.json */; };
C459B4BD27F6093700E9B4B4 /* nginx-proxy.test in Resources */ = {isa = PBXBuildFile; fileRef = C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */; };
C45E76142854A65300B4FE0C /* ServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E76132854A65300B4FE0C /* ServicesManager.swift */; };
C45E76152854A65300B4FE0C /* ServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E76132854A65300B4FE0C /* ServicesManager.swift */; };
C463E380284930EE00422731 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; };
C463E381284930EE00422731 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; };
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 */; };
@ -125,6 +137,10 @@
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 */; };
C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; };
C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; };
C46FA98C2822F08F00D78807 /* PhpConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA98A2822F08F00D78807 /* PhpConfigurationTest.swift */; };
C4709CA228524B3400088BB8 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4709CA128524B3400088BB8 /* StatsView.swift */; };
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; };
@ -135,11 +151,7 @@
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 */; };
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C9925CC888B00CC7490 /* HeaderView.xib */; };
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */; };
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */; };
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */; };
@ -149,7 +161,6 @@
C493084B279F331F009C240B /* AddSiteVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4930849279F331F009C240B /* AddSiteVC.swift */; };
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 /* DomainListKindCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */; };
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
@ -166,6 +177,8 @@
C4B585422770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; };
C4B585442770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
C4B585452770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
C4B6091A2853AAD300C95265 /* SectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B609192853AAD300C95265 /* SectionHeaderView.swift */; };
C4B6091D2853AB9700C95265 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B6091C2853AB9700C95265 /* ServicesView.swift */; };
C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; };
C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; };
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */; };
@ -195,8 +208,8 @@
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 /* NginxConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */; };
C4D5CFCB27E0F9CD00035329 /* NginxConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */; };
C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */; };
C4D5CFCB27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.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 */; };
@ -213,15 +226,10 @@
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; };
C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* ValetSite.swift */; };
C4E4404727C56F4700D225E1 /* ValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* ValetSite.swift */; };
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; };
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C4EB53E528551F9B006F9937 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EB53E428551F9B006F9937 /* HeaderView.swift */; };
C4EB53E728553117006F9937 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EB53E628553117006F9937 /* ArrayExtension.swift */; };
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A627708B9E001DF387 /* PMHeaderView.swift */; };
C4EE55AA27708B9E001DF387 /* PMHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A627708B9E001DF387 /* PMHeaderView.swift */; };
C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A727708B9E001DF387 /* Preview.swift */; };
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A827708B9E001DF387 /* PMStatsView.swift */; };
C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A827708B9E001DF387 /* PMStatsView.swift */; };
C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EED88827A48778006D7272 /* InterAppHandler.swift */; };
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EED88827A48778006D7272 /* InterAppHandler.swift */; };
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
@ -235,6 +243,7 @@
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 */; };
C4F361612836BFD9003598CC /* MainMenu+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F361602836BFD9003598CC /* MainMenu+Actions.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 */; };
@ -244,19 +253,18 @@
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
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 */; };
C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F361602836BFD9003598CC /* MainMenu+Actions.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 */
@ -274,6 +282,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>"; };
5489625728312FAD004F647A /* CreatedFromFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatedFromFile.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>"; };
@ -294,6 +303,7 @@
C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarIcons.swift; sourceTree = "<group>"; };
C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlert.swift; sourceTree = "<group>"; };
C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlertVC.swift; sourceTree = "<group>"; };
C40C5C9B2846A40600E28255 /* Preset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preset.swift; sourceTree = "<group>"; };
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>"; };
@ -323,6 +333,7 @@
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>"; };
C42800A928452AA10099C999 /* StatusMenu+Items.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Items.swift"; sourceTree = "<group>"; };
C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+FixMyValet.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>"; };
@ -337,17 +348,25 @@
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>"; };
C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHelper.swift; sourceTree = "<group>"; };
C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionPopoverView.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>"; };
C44F868D2835BD8D005C353A /* phpmon-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "phpmon-config.json"; sourceTree = "<group>"; };
C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-proxy.test"; sourceTree = "<group>"; };
C45E76132854A65300B4FE0C /* ServicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesManager.swift; sourceTree = "<group>"; };
C463E37F284930EE00422731 /* PresetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetHelper.swift; 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>"; };
C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigurationFile.swift; sourceTree = "<group>"; };
C46FA98A2822F08F00D78807 /* PhpConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigurationTest.swift; sourceTree = "<group>"; };
C4709CA128524B3400088BB8 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.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>"; };
C474B00524C0E98C00066A22 /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = "<group>"; };
@ -355,17 +374,12 @@
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>"; };
C48D0C9925CC888B00CC7490 /* HeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HeaderView.xib; sourceTree = "<group>"; };
C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpVersionNumber.swift; sourceTree = "<group>"; };
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionNumberTest.swift; sourceTree = "<group>"; };
C4927F0A27B2DFC200C55AFD /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = "<group>"; };
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 /* 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>"; };
@ -377,6 +391,8 @@
C4B5853B2770FE3900DA4FBE /* Paths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
C4B5853C2770FE3900DA4FBE /* Shell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
C4B5853D2770FE3900DA4FBE /* Command.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
C4B609192853AAD300C95265 /* SectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderView.swift; sourceTree = "<group>"; };
C4B6091C2853AB9700C95265 /* ServicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesView.swift; sourceTree = "<group>"; };
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>"; };
@ -392,7 +408,7 @@
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 /* NginxConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfiguration.swift; sourceTree = "<group>"; };
C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigurationFile.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>"; };
@ -404,18 +420,16 @@
C4E4404527C56F4700D225E1 /* ValetSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetSite.swift; sourceTree = "<group>"; };
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
C4EC1E65279DE0380010F296 /* ServicesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ServicesView.xib; sourceTree = "<group>"; };
C4EC1E67279DE0540010F296 /* ServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServicesView.swift; sourceTree = "<group>"; };
C4EB53E428551F9B006F9937 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
C4EB53E628553117006F9937 /* ArrayExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = "<group>"; };
C4EC1E72279DFCF40010F296 /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = "<group>"; };
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
C4EE55A627708B9E001DF387 /* PMHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMHeaderView.swift; sourceTree = "<group>"; };
C4EE55A727708B9E001DF387 /* Preview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preview.swift; sourceTree = "<group>"; };
C4EE55A827708B9E001DF387 /* PMStatsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMStatsView.swift; sourceTree = "<group>"; };
C4EED88827A48778006D7272 /* InterAppHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterAppHandler.swift; sourceTree = "<group>"; };
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = "<group>"; };
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>"; };
C4F361602836BFD9003598CC /* MainMenu+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Actions.swift"; 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>"; };
@ -461,6 +475,14 @@
path = Preferences;
sourceTree = "<group>";
};
5489625628312F95004F647A /* Protocols */ = {
isa = PBXGroup;
children = (
5489625728312FAD004F647A /* CreatedFromFile.swift */,
);
path = Protocols;
sourceTree = "<group>";
};
54B20EDF263AA22C00D3250E /* PHP */ = {
isa = PBXGroup;
children = (
@ -471,6 +493,7 @@
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */,
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */,
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */,
);
path = PHP;
sourceTree = "<group>";
@ -527,9 +550,19 @@
path = Notice;
sourceTree = "<group>";
};
C40C5C9E2846A42D00E28255 /* Presets */ = {
isa = PBXGroup;
children = (
C40C5C9B2846A40600E28255 /* Preset.swift */,
C463E37F284930EE00422731 /* PresetHelper.swift */,
);
path = Presets;
sourceTree = "<group>";
};
C40C7F1C27720E1400DDDCDC /* Test Files */ = {
isa = PBXGroup;
children = (
C44F868C2835BD60005C353A /* phpmon */,
C459B4C127F6097E00E9B4B4 /* php */,
C459B4C027F6096300E9B4B4 /* valet */,
C459B4BF27F6094100E9B4B4 /* brew */,
@ -615,6 +648,7 @@
5420395726135DB800FB00FA /* Preferences */,
C44C198F276E3A380072762D /* Progress */,
C4C8E81D276F5686003AC782 /* Watcher */,
C40C5C9E2846A42D00E28255 /* Presets */,
C4EE55B027708BB2001DF387 /* SwiftUI */,
);
path = Domain;
@ -659,6 +693,14 @@
path = Errors;
sourceTree = "<group>";
};
C44F868C2835BD60005C353A /* phpmon */ = {
isa = PBXGroup;
children = (
C44F868D2835BD8D005C353A /* phpmon-config.json */,
);
path = phpmon;
sourceTree = "<group>";
};
C459B4BE27F6093A00E9B4B4 /* nginx */ = {
isa = PBXGroup;
children = (
@ -719,13 +761,9 @@
C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */,
C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */,
C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */,
C4F361602836BFD9003598CC /* MainMenu+Actions.swift */,
C47331A1247093B7009A0597 /* StatusMenu.swift */,
C48D0C9525CC80B100CC7490 /* HeaderView.swift */,
C48D0C9925CC888B00CC7490 /* HeaderView.xib */,
C48D0CA225CC992000CC7490 /* StatsView.swift */,
C48D0C8F25CC7FD000CC7490 /* StatsView.xib */,
C4EC1E67279DE0540010F296 /* ServicesView.swift */,
C4EC1E65279DE0380010F296 /* ServicesView.xib */,
C42800A928452AA10099C999 /* StatusMenu+Items.swift */,
);
path = Menu;
sourceTree = "<group>";
@ -799,6 +837,7 @@
C4D8016522B1584700C6DA1B /* Startup.swift */,
C46E206C28299B3800D909D6 /* AppUpdateChecker.swift */,
C40FE736282ABA4F00A302C2 /* AppVersion.swift */,
C45E76132854A65300B4FE0C /* ServicesManager.swift */,
);
path = App;
sourceTree = "<group>";
@ -811,10 +850,38 @@
C44CCD4327AFE93300CE40E5 /* Errors */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
C4811D2822D70D9C00B5F6B3 /* Helpers */,
5489625628312F95004F647A /* Protocols */,
);
path = Common;
sourceTree = "<group>";
};
C4B609162853AA9A00C95265 /* Common */ = {
isa = PBXGroup;
children = (
C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */,
);
path = Common;
sourceTree = "<group>";
};
C4B609172853AA9E00C95265 /* Menu */ = {
isa = PBXGroup;
children = (
C4B6091C2853AB9700C95265 /* ServicesView.swift */,
C4709CA128524B3400088BB8 /* StatsView.swift */,
C4B609192853AAD300C95265 /* SectionHeaderView.swift */,
C4EB53E428551F9B006F9937 /* HeaderView.swift */,
);
path = Menu;
sourceTree = "<group>";
};
C4B609182853AAA700C95265 /* Domains */ = {
isa = PBXGroup;
children = (
C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */,
);
path = Domains;
sourceTree = "<group>";
};
C4C0E8D827F887A5002D32A9 /* Sites */ = {
isa = PBXGroup;
children = (
@ -838,7 +905,7 @@
C4C0E8DA27F887CC002D32A9 /* Nginx */ = {
isa = PBXGroup;
children = (
C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */,
C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */,
);
path = Nginx;
sourceTree = "<group>";
@ -867,6 +934,7 @@
children = (
C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */,
C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */,
C46FA98A2822F08F00D78807 /* PhpConfigurationTest.swift */,
C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */,
C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */,
);
@ -940,10 +1008,9 @@
C4EE55B027708BB2001DF387 /* SwiftUI */ = {
isa = PBXGroup;
children = (
C49E171E27A5736E00787921 /* PMServicesView.swift */,
C4EE55A627708B9E001DF387 /* PMHeaderView.swift */,
C4EE55A827708B9E001DF387 /* PMStatsView.swift */,
C4EE55A727708B9E001DF387 /* Preview.swift */,
C4B609182853AAA700C95265 /* Domains */,
C4B609172853AA9E00C95265 /* Menu */,
C4B609162853AA9A00C95265 /* Common */,
);
path = SwiftUI;
sourceTree = "<group>";
@ -978,6 +1045,7 @@
C48D0C9225CC804200CC7490 /* XibLoadable.swift */,
C42759662627662800093CAE /* NSMenuExtension.swift */,
C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */,
C4EB53E628553117006F9937 /* ArrayExtension.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1071,17 +1139,14 @@
files = (
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */,
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */,
C4232EE52612526500158FC6 /* Credits.html in Resources */,
54FCFD26276C883F004CE748 /* SelectPreferenceView.xib in Resources */,
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
C4068CA427B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */,
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */,
54FCFD2D276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1094,6 +1159,7 @@
C42CFB1827DFDFDC00862737 /* nginx-site-isolated.test in Resources */,
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
C4068CA527B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */,
C44F868E2835BD8D005C353A /* phpmon-config.json in Resources */,
C43A8A2025D9D1D700591B77 /* brew-formula.json in Resources */,
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */,
C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */,
@ -1139,39 +1205,43 @@
C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C4B585412770FE3900DA4FBE /* Shell.swift in Sources */,
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */,
C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */,
C4C0E8EA27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */,
C4EB53E728553117006F9937 /* ArrayExtension.swift in Sources */,
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */,
C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */,
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 */,
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 */,
C463E380284930EE00422731 /* PresetHelper.swift in Sources */,
C41C02A927E61A65009F26CB /* ValetSite+Fake.swift in Sources */,
C4C0E8DF27F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */,
C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */,
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */,
C4EB53E528551F9B006F9937 /* HeaderView.swift in Sources */,
C40FE737282ABA4F00A302C2 /* AppVersion.swift in Sources */,
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */,
C4B585442770FE3900DA4FBE /* Command.swift in Sources */,
C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */,
C40C5C9C2846A40600E28255 /* Preset.swift in Sources */,
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */,
C4B6091A2853AAD300C95265 /* SectionHeaderView.swift in Sources */,
C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */,
C42800AA28452AA10099C999 /* StatusMenu+Items.swift in Sources */,
C415D3B72770F294005EF286 /* Actions.swift in Sources */,
C4AC51FC27E27F47008528CA /* DomainListKindCell.swift in Sources */,
C4F361612836BFD9003598CC /* MainMenu+Actions.swift in Sources */,
C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */,
54D9E0B827E4F51E003B9AD9 /* KeyCombo.swift in Sources */,
C4C0E8E727F88B41002D32A9 /* ProxyScanner.swift in Sources */,
@ -1196,7 +1266,9 @@
C46E206D28299B3800D909D6 /* AppUpdateChecker.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */,
C45E76142854A65300B4FE0C /* ServicesManager.swift in Sources */,
C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */,
C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */,
C417DC74277614690015E6EE /* Helpers.swift in Sources */,
C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
@ -1212,15 +1284,16 @@
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
C4FE011128084FC200D1DE6D /* SelectionVC.swift in Sources */,
C4709CA228524B3400088BB8 /* StatsView.swift in Sources */,
C44CCD4027AFE2FC00CE40E5 /* AlertableError.swift in Sources */,
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */,
C4B6091D2853AB9700C95265 /* ServicesView.swift in Sources */,
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */,
C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
C4D5CFCA27E0F9CD00035329 /* NginxConfiguration.swift in Sources */,
C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */,
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */,
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */,
@ -1250,7 +1323,6 @@
files = (
C449B4F427EE7FC800C47E8A /* DomainListKindCell.swift in Sources */,
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */,
C41CA5EE2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */,
54D9E0B727E4F51E003B9AD9 /* HotKey.swift in Sources */,
C4205A7F27F4D21800191A39 /* ValetProxy.swift in Sources */,
@ -1268,7 +1340,7 @@
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
54D9E0BB27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */,
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
C4D5CFCB27E0F9CD00035329 /* NginxConfiguration.swift in Sources */,
C4D5CFCB27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */,
C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */,
C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */,
@ -1287,11 +1359,14 @@
C4C0E8E327F88B13002D32A9 /* ValetSiteScanner.swift in Sources */,
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
C463E381284930EE00422731 /* PresetHelper.swift in Sources */,
C46FA98C2822F08F00D78807 /* PhpConfigurationTest.swift in Sources */,
C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */,
C4C0E8EB27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */,
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */,
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */,
54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */,
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */,
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */,
@ -1304,7 +1379,9 @@
C41E871B2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */,
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */,
C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C40C5C9D2846A40600E28255 /* Preset.swift in Sources */,
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */,
C42800AB28452AA50099C999 /* StatusMenu+Items.swift in Sources */,
C40FE738282ABA4F00A302C2 /* AppVersion.swift in Sources */,
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C484437C2804BB560041A78A /* ValetProxyScanner.swift in Sources */,
@ -1314,6 +1391,7 @@
C417DC75277614690015E6EE /* Helpers.swift in Sources */,
C4080FF727BD8C6400BF2C6B /* BetterAlert.swift in Sources */,
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
5489625928313231004F647A /* CreatedFromFile.swift in Sources */,
54D9E0B327E4F51E003B9AD9 /* HotKeysController.swift in Sources */,
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */,
@ -1327,7 +1405,6 @@
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */,
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */,
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */,
C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */,
C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */,
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */,
@ -1341,7 +1418,6 @@
C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */,
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
C4B585452770FE3900DA4FBE /* Command.swift in Sources */,
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */,
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */,
@ -1349,6 +1425,7 @@
C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */,
C449B4F327EE7FC600C47E8A /* DomainListTypeCell.swift in Sources */,
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */,
C41C02AB27E61CB3009F26CB /* ValetSite+Fake.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
C4D9F24C280B69E100DCD39A /* AddProxyVC.swift in Sources */,
@ -1356,15 +1433,14 @@
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */,
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
C4B585422770FE3900DA4FBE /* Shell.swift in Sources */,
C45E76152854A65300B4FE0C /* ServicesManager.swift in Sources */,
C464ADAD275A7A3F003FCD53 /* DomainListWC.swift in Sources */,
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */,
C4F780CB25D80B75000DBC97 /* StatsView.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;
@ -1512,11 +1588,12 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 790;
CURRENT_PROJECT_VERSION = 911;
DEBUG = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
@ -1526,7 +1603,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 5.3.2;
MARKETING_VERSION = 5.4;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1538,11 +1615,12 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor;
CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 790;
CURRENT_PROJECT_VERSION = 911;
DEBUG = NO;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
@ -1552,7 +1630,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 5.3.2;
MARKETING_VERSION = 5.4;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -69,8 +69,8 @@
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "PHPMON_MARKETING_MODE"
value = "YES"
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>

View File

@ -4,13 +4,15 @@
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
<img src="./docs/screenshot.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
@ -21,10 +23,10 @@ You can also add new domains as links, isolate sites, manage various services, a
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
* macOS 11 Big Sur or later
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* Homebrew `php` formula is installed
* Laravel Valet 2.16 or newer (supports Valet 3)
* Laravel Valet 3 recommended (but compatible with Valet 2)
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
@ -105,12 +107,10 @@ Super convenient!
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it:
Install [Homebrew](https://brew.sh) first.
Install [Homebrew](https://brew.sh) first. Follow the instructions there first!
Install PHP, composer, add to path:
Then, you'll need to set up your PATH.
brew install php
brew install composer
nano .zshrc
Make sure the following line is not in the comments:
@ -123,21 +123,27 @@ If you're on an Apple Silicon-based Mac, you'll need to add:
# on an M1 Mac
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
and add the following to your .zshrc, but add this BEFORE the homebrew PATH additions:
and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
If you're adding composer and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
export PATH=$HOME/bin:/usr/local/bin:$PATH
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
If you are *not* on Apple Silicon, you should remove the third line.
Install the `php` and `composer` formulae:
brew install php composer
Make sure PHP is linked correctly:
which php
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php`)
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon)
composer global require laravel/valet
valet install
@ -146,7 +152,12 @@ This should install `dnsmasq` and set up Valet. Great, almost there!
valet trust
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
You can now install PHP Monitor, if you haven't already:
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
</details>
<details>
@ -297,6 +308,38 @@ The app includes an Internet Access Policy file, so if you're using something li
</details>
<details>
<summary><strong>How do I various presets to show up?</strong></summary>
You must set these presets up in a JSON file, located in `~/.config/phpmon/config.json`.
You must have set up at least one valid preset for this presets to work in PHP Monitor.
Here's an example of a working preset:
<pre>
{
"scan_apps": [],
"presets": [
{
"name": "Legacy Project",
"php": "8.0",
"extensions": {
"xdebug": false
},
"configuration": {
"memory_limit": "128M",
"upload_max_filesize": "128M",
"post_max_size": "128M"
}
}
]
}
</pre>
You can omit the `php` key in the preset if you do not wish for the preset to switch to a given PHP version.
</details>
<details>
<summary><strong>How do I get various applications to show up in the domain list's right-click menu?</strong></summary>
@ -308,11 +351,12 @@ All of these apps should just be detected correctly, no matter their location on
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
You can add your own apps by creating and editing a `~/.phpmon.conf.json` file, with the following entry:
You can add your own apps by creating and editing a `~/.config/phpmon/config.json` file, and make sure the `scan_apps` key is set:
<pre>
{
"scan_apps": ["Xcode", "Kraken"]
"scan_apps": ["Xcode", "Kraken"],
"presets": []
}
</pre>

View File

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

BIN
docs/notification-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/screenshot-dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 469 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

View File

@ -27,3 +27,25 @@ extension NSMenu {
}
}
// MARK: - NSMenuItem subclasses
class PhpMenuItem: NSMenuItem {
var version: String = ""
}
class XdebugMenuItem: NSMenuItem {
var mode: String = ""
}
class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension?
}
class EditorMenuItem: NSMenuItem {
var editor: Application?
}
class PresetMenuItem: NSMenuItem {
var preset: Preset?
}

View File

@ -7,8 +7,13 @@
import Foundation
extension String {
var localized: String {
if #available(macOS 13, *) {
return NSLocalizedString(
self, tableName: nil, bundle: Bundle.main, value: "", comment: ""
).replacingOccurrences(of: "Preferences", with: "Settings")
}
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}
@ -32,7 +37,7 @@ extension String {
return count
}
subscript (r: Range<String.Index>) -> String {
subscript(r: Range<String.Index>) -> String {
let start = r.lowerBound
let end = r.upperBound
return String(self[start ..< end])
@ -71,4 +76,22 @@ extension String {
}
}
var stripped: String {
do {
guard let data = self.data(using: .unicode) else {
return ""
}
let attributed = try NSAttributedString(
data: data,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue],
documentAttributes: nil
)
return attributed.string
} catch {
return ""
}
}
}

View File

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

View File

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

View File

@ -11,16 +11,23 @@ import Foundation
class Xdebug {
public static var enabled: Bool {
return !self.mode.isEmpty
return PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") != nil
}
public static var mode: String {
return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
public static var activeModes: [String] {
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
return []
}
guard let value = file.get(for: "xdebug.mode") else {
return []
}
return value.components(separatedBy: ",").filter { self.modes.contains($0) }
}
public static var modes: [String] {
return [
"off",
"develop",
"coverage",
"debug",

View File

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

View File

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

View File

@ -91,7 +91,7 @@ public struct PhpVersionNumberCollection: Equatable {
}
}
public struct PhpVersionNumber: Equatable {
public struct PhpVersionNumber: Equatable, Hashable {
let major: Int
let minor: Int
let patch: Int?

View File

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

View File

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

View File

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

View File

@ -59,6 +59,9 @@ class App {
/** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = []
/** The services manager, responsible for figuring out what services are active/inactive. */
var services = ServicesManager.shared
/** Timer that will periodically reload info about the user's PHP installation. */
var timer: Timer?

View File

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

View File

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

View File

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

View File

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

View File

@ -146,13 +146,15 @@ class Startup {
command: { return !Shell.pipe("cat /private/etc/sudoers.d/brew").contains(Paths.brew) },
name: "`/private/etc/sudoers.d/brew` contains brew",
titleText: "startup.errors.sudoers_brew.title".localized,
subtitleText: "startup.errors.sudoers_brew.subtitle".localized
subtitleText: "startup.errors.sudoers_brew.subtitle".localized,
descriptionText: "startup.errors.sudoers_brew.desc".localized
),
EnvironmentCheck(
command: { return !Shell.pipe("cat /private/etc/sudoers.d/valet").contains(Paths.valet) },
name: "`/private/etc/sudoers.d/valet` contains valet",
titleText: "startup.errors.sudoers_valet.title".localized,
subtitleText: "startup.errors.sudoers_valet.subtitle".localized
subtitleText: "startup.errors.sudoers_valet.subtitle".localized,
descriptionText: "startup.errors.sudoers_valet.desc".localized
),
// =================================================================================
// Verify if the Homebrew services are running (as root).
@ -182,6 +184,21 @@ class Startup {
descriptionText: "startup.errors.valet_json_invalid.desc".localized
),
// =================================================================================
// Check for `which` alias issue
// =================================================================================
EnvironmentCheck(
command: {
return App.architecture == "x86_64"
&& FileManager.default.fileExists(atPath: "/usr/local/bin/which")
&& Shell.pipe("which node", requiresPath: false)
.contains("env: node: No such file or directory")
},
name: "`env: node` issue does not apply",
titleText: "startup.errors.which_alias_issue.title".localized,
subtitleText: "startup.errors.which_alias_issue.subtitle".localized,
descriptionText: "startup.errors.which_alias_issue.desc".localized
),
// =================================================================================
// Determine the Valet version and ensure it isn't unknown.
// =================================================================================
EnvironmentCheck(

View File

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

View File

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

View File

@ -83,7 +83,8 @@ class ComposerWindow {
if shouldNotify {
LocalNotification.send(
title: "alert.composer_success.title".localized,
subtitle: "alert.composer_success.info".localized
subtitle: "alert.composer_success.info".localized,
preference: .notifyAboutGlobalComposerStatus
)
}
window = nil

View File

@ -38,8 +38,6 @@ struct PhpFrameworks {
"zendframework/zendframework": "Zend",
"zendframework/zend-mvc": "Zend",
"typo3/cms-core": "Typo3"
// TODO (6.0): Handle these in v6.0
// "magento/*": "Magento",
// "concrete5/*": "Concrete5",
// "contao/*": "Contao",

View File

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

View File

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

View File

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

View File

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

View File

@ -81,9 +81,9 @@ class ValetSite: DomainListable {
if makeDeterminations {
determineSecured()
determineIsolated()
determineComposerPhpVersion()
determineDriver()
determineIsolated()
}
}
@ -133,7 +133,6 @@ class ValetSite: DomainListable {
with the currently linked version of PHP (see `composerPhpMatchesSystem`).
*/
public func determineComposerPhpVersion() {
self.determineComposerInformation()
self.determineValetPhpFileInfo()
@ -145,7 +144,8 @@ class ValetSite: DomainListable {
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|")
.map { string in
return !PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
let origin = self.isolatedPhpVersion?.versionNumber.homebrewVersion ?? PhpEnv.phpInstall.version.long
return !PhpVersionNumberCollection.make(from: [origin])
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.isEmpty
}.contains(true)
@ -225,7 +225,7 @@ class ValetSite: DomainListable {
public static func isolatedVersion(_ filePath: String) -> String? {
if Filesystem.fileExists(filePath) {
return NginxConfiguration
return NginxConfigurationFile
.from(filePath: filePath)?
.isolatedVersion ?? nil
}

View File

@ -216,6 +216,8 @@ class Valet {
sites.insert(site, at: 0)
}
Log.info("\(sites.count) sites & \(proxies.count) proxies have been scanned.")
isBusy = false
}

View File

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

View File

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

View File

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

View File

@ -74,7 +74,7 @@ extension MainMenu {
}
if behaviours.contains(.broadcastServicesUpdate) {
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
ServicesManager.shared.loadData()
}
error == nil ? success() : failure(error!)

View File

@ -64,6 +64,9 @@ extension MainMenu {
// Detect built-in and custom applications
detectApplications()
// Load the rollback preset
PresetHelper.loadRollbackPresetFromFile()
// Load the global hotkey
App.shared.loadGlobalHotkey()
@ -76,8 +79,7 @@ extension MainMenu {
// A non-default TLD is not officially supported since Valet 3.2.x
Valet.notifyAboutUnsupportedTLD()
// Update the services list in the background
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
ServicesManager.shared.loadData()
// Start the background refresh timer
startSharedTimer()
@ -155,5 +157,4 @@ extension MainMenu {
Log.info("Detected applications: \(appNames)")
}
}

View File

@ -15,12 +15,6 @@ extension MainMenu {
func switcherDidStartSwitching(to version: String) {}
func switcherDidCompleteSwitch(to version: String) {
// Update the PHP version
PhpEnv.shared.currentInstall = ActivePhpInstallation()
// Ensure the config watcher gets reloaded
App.shared.handlePhpConfigWatcher()
// Mark as no longer busy
PhpEnv.shared.isBusy = false
@ -56,11 +50,12 @@ extension MainMenu {
}
}
@MainActor private func suggestFixMyValet(failed version: String) {
private func suggestFixMyValet(failed version: String) {
let outcome = BetterAlert()
.withInformation(
title: "alert.php_switch_failed.title".localized(version),
subtitle: "alert.php_switch_failed.info".localized(version)
subtitle: "alert.php_switch_failed.info".localized(version),
description: "alert.php_switch_failed.desc".localized()
)
.withPrimary(text: "alert.php_switch_failed.confirm".localized)
.withSecondary(text: "alert.php_switch_failed.cancel".localized)
@ -83,7 +78,8 @@ extension MainMenu {
private func notifyAboutVersionChange(to version: String) {
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, version),
subtitle: String(format: "notification.version_changed_desc".localized, version)
subtitle: String(format: "notification.version_changed_desc".localized, version),
preference: .notifyAboutVersionChange
)
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()

View File

@ -11,6 +11,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
static let shared = MainMenu()
override init() {
super.init()
statusItem.isVisible = !isRunningSwiftUIPreview
}
weak var menuDelegate: NSMenuDelegate?
/**
@ -45,29 +50,22 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
Use `rebuild(async:)` to ensure the rebuilding happens in the background.
*/
private func rebuildMenu() {
// Create a new menu
let menu = StatusMenu()
// Add the PHP versions (or error messages)
menu.addPhpVersionMenuItems()
menu.addItem(NSMenuItem.separator())
// Add the possible actions
menu.addPhpActionMenuItems()
menu.addItem(NSMenuItem.separator())
// Add Valet interactions
menu.addValetMenuItems()
menu.addItem(NSMenuItem.separator())
// Add services
menu.addRemainingMenuItems()
menu.addItem(NSMenuItem.separator())
// Add about & quit menu items
menu.addCoreMenuItems()
// Make sure every item can be interacted with
menu.items.forEach({ (item) in
item.target = self
})
@ -124,7 +122,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
refreshActiveInstallation()
refreshIcon()
rebuild(async: false)
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
ServicesManager.shared.loadData()
}
/** Reloads the menu in the background, using `asyncExecution`. */
@ -165,155 +163,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
}
}
// MARK: - Actions
@objc func fixHomebrewPermissions() {
if !BetterAlert()
.withInformation(
title: "alert.fix_homebrew_permissions.title".localized,
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
description: "alert.fix_homebrew_permissions.desc".localized
)
.withPrimary(text: "alert.fix_homebrew_permissions.ok".localized)
.withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized)
.didSelectPrimary() {
return
}
asyncExecution {
try Actions.fixHomebrewPermissions()
} success: {
BetterAlert()
.withInformation(
title: "alert.fix_homebrew_permissions_done.title".localized,
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
description: "alert.fix_homebrew_permissions_done.desc".localized
)
.withPrimary(text: "OK")
.show()
} failure: { error in
BetterAlert.show(for: error as! HomebrewPermissionError)
}
}
@objc func restartPhpFpm() {
asyncExecution {
Actions.restartPhpFpm()
}
}
@objc func restartAllServices() {
asyncExecution {
Actions.restartDnsMasq()
Actions.restartPhpFpm()
Actions.restartNginx()
} success: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_restarted".localized,
subtitle: "notification.services_restarted_desc".localized
)
}
}
}
@objc func stopAllServices() {
asyncExecution {
Actions.stopAllServices()
} success: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_stopped".localized,
subtitle: "notification.services_stopped_desc".localized
)
}
}
}
@objc func restartNginx() {
asyncExecution {
Actions.restartNginx()
}
}
@objc func restartDnsMasq() {
asyncExecution {
Actions.restartDnsMasq()
}
}
@objc func toggleXdebugMode(sender: XdebugMenuItem) {
Log.info("Switching Xdebug to mode: \(sender.mode)")
}
@objc func toggleExtension(sender: ExtensionMenuItem) {
asyncExecution {
sender.phpExtension?.toggle()
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
Actions.restartPhpFpm()
}
}
}
@objc func openPhpInfo() {
var url: URL?
asyncWithBusyUI {
url = Actions.createTempPhpInfoFile()
} completion: {
if url != nil { NSWorkspace.shared.open(url!) }
}
}
@objc func updateGlobalComposerDependencies() {
ComposerWindow().updateGlobalDependencies(
notify: true,
completion: { _ in }
)
}
@objc func openActiveConfigFolder() {
if PhpEnv.phpInstall.version.error {
// php version was not identified
Actions.openGenericPhpConfigFolder()
return
}
// php version was identified
Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short)
}
@objc func openGlobalComposerFolder() {
Actions.openGlobalComposerFolder()
}
@objc func openValetConfigFolder() {
Actions.openValetConfigFolder()
}
@objc func switchToPhpVersion(sender: PhpMenuItem) {
self.switchToPhpVersion(sender.version)
}
@objc func switchToPhpVersion(_ version: String) {
setBusyImage()
PhpEnv.shared.isBusy = true
PhpEnv.shared.delegate = self
PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
updatePhpVersionInStatusBar()
rebuild()
PhpEnv.switcher.performSwitch(
to: version,
completion: {
PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version)
}
)
}
}
// MARK: - Menu Item Functionality
@objc func openAbout() {
@ -322,7 +171,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
}
@objc func openPrefs() {
PrefsVC.show()
PrefsWC.show()
}
@objc func openDomainList() {
@ -348,7 +197,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
func menuWillOpen(_ menu: NSMenu) {
// Make sure the shortcut key does not trigger this when the menu is open
App.shared.shortcutHotkey?.isPaused = true
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
ServicesManager.shared.loadData()
}
func menuDidClose(_ menu: NSMenu) {

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,257 @@
//
// StatusMenu+Items.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension StatusMenu {
// MARK: Remaining Menu Items
func addConfigurationMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
self.addItem(
NSMenuItem(title: "mi_php_config".localized,
action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c")
)
self.addItem(
NSMenuItem(title: "mi_phpmon_config".localized,
action: #selector(MainMenu.openPhpMonitorConfigurationFile), keyEquivalent: "y")
)
self.addItem(
NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i")
)
}
func addComposerMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized))
self.addItem(
NSMenuItem(title: "mi_global_composer".localized,
action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g")
)
let composerMenuItem = NSMenuItem(
title: "mi_update_global_composer".localized,
action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies),
keyEquivalent: "g"
)
composerMenuItem.keyEquivalentModifierMask = .shift
self.addItem(composerMenuItem)
}
func addStatsMenuItem() {
guard let stats = PhpEnv.phpInstall.limits else { return }
self.addItem(StatsView.asMenuItem(
memory: stats.memory_limit,
post: stats.post_max_size,
upload: stats.upload_max_filesize)
)
}
func addExtensionsMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
if PhpEnv.phpInstall.extensions.isEmpty {
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
}
var shortcutKey = 1
for phpExtension in PhpEnv.phpInstall.extensions {
self.addExtensionItem(phpExtension, shortcutKey)
shortcutKey += 1
}
}
func addPresetsMenuItem() {
guard let presets = Preferences.custom.presets else {
addEmptyPresetHelp()
return
}
if presets.isEmpty {
addEmptyPresetHelp()
return
}
addLoadedPresets()
}
func addEmptyPresetHelp() {
let presets = NSMenuItem(title: "mi_presets_title".localized, action: nil, keyEquivalent: "")
let presetsMenu = NSMenu()
presetsMenu.addItem(NSMenuItem(title: "mi_no_presets".localized, action: nil, keyEquivalent: ""))
presetsMenu.addItem(NSMenuItem.separator())
presetsMenu.addItem(NSMenuItem(
title: "mi_set_up_presets".localized,
action: #selector(MainMenu.showPresetHelp),
keyEquivalent: "")
)
presetsMenu.items.forEach { $0.target = MainMenu.shared }
self.setSubmenu(presetsMenu, for: presets)
self.addItem(presets)
return
}
func addLoadedPresets() {
let presets = NSMenuItem(title: "mi_presets_title".localized, action: nil, keyEquivalent: "")
let presetsMenu = NSMenu()
presetsMenu.addItem(NSMenuItem.separator())
presetsMenu.addItem(HeaderView.asMenuItem(text: "mi_apply_presets_title".localized))
for preset in Preferences.custom.presets! {
let presetMenuItem = PresetMenuItem(
title: preset.getMenuItemText(),
action: #selector(MainMenu.togglePreset(sender:)),
keyEquivalent: ""
)
if let attributedString = try? NSMutableAttributedString(
data: preset.getMenuItemText().data(using: .utf8)!,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil
) {
presetMenuItem.attributedTitle = attributedString
}
presetMenuItem.preset = preset
presetsMenu.addItem(presetMenuItem)
}
presetsMenu.addItem(NSMenuItem.separator())
presetsMenu.addItem(NSMenuItem(
title: "mi_revert_to_prev_config".localized,
action: PresetHelper.rollbackPreset != nil
? #selector(MainMenu.rollbackPreset)
: nil,
keyEquivalent: ""
))
presetsMenu.addItem(NSMenuItem.separator())
presetsMenu.addItem(NSMenuItem(
title: "mi_profiles_loaded".localized(
Preferences.custom.presets!.count
),
action: nil, keyEquivalent: "")
)
for item in presetsMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(presetsMenu, for: presets)
self.addItem(presets)
}
func addXdebugMenuItem() {
if !Xdebug.enabled {
return
}
let xdebugSwitch = NSMenuItem(
title: "mi_xdebug_mode".localized,
action: nil,
keyEquivalent: ""
)
let xdebugModesMenu = NSMenu()
let activeModes = Xdebug.activeModes
xdebugModesMenu.addItem(HeaderView.asMenuItem(text: "mi_xdebug_available_modes".localized))
for mode in Xdebug.modes {
let item = XdebugMenuItem(
title: mode,
action: #selector(MainMenu.toggleXdebugMode(sender:)),
keyEquivalent: ""
)
item.state = activeModes.contains(mode) ? .on : .off
item.mode = mode
xdebugModesMenu.addItem(item)
}
xdebugModesMenu.addItem(HeaderView.asMenuItem(text: "mi_xdebug_actions".localized))
xdebugModesMenu.addItem(
withTitle: "mi_xdebug_disable_all".localized,
action: #selector(MainMenu.disableAllXdebugModes),
keyEquivalent: ""
)
for item in xdebugModesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(xdebugModesMenu, for: xdebugSwitch)
self.addItem(xdebugSwitch)
}
func addFirstAidAndServicesMenuItems() {
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
let servicesMenu = NSMenu()
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_first_aid".localized))
let fixMyValetMenuItem = NSMenuItem(
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
)
fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized
servicesMenu.addItem(fixMyValetMenuItem)
let fixHomebrewMenuItem = NSMenuItem(
title: "mi_fix_brew_permissions".localized(),
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: ""
)
fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized
servicesMenu.addItem(fixHomebrewMenuItem)
servicesMenu.addItem(NSMenuItem.separator())
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_dnsmasq".localized,
action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_php_fpm".localized,
action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_nginx".localized,
action: #selector(MainMenu.restartNginx), keyEquivalent: "n")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_valet_services".localized,
action: #selector(MainMenu.restartValetServices), keyEquivalent: "s")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_stop_valet_services".localized,
action: #selector(MainMenu.stopValetServices), keyEquivalent: "s"),
withKeyModifier: [.command, .shift]
)
servicesMenu.addItem(NSMenuItem.separator())
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized))
servicesMenu.addItem(
NSMenuItem(title: "mi_php_refresh".localized,
action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r")
)
for item in servicesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(servicesMenu, for: services)
self.addItem(services)
}
}

View File

@ -68,7 +68,8 @@ class StatusMenu: NSMenu {
self.addItem(NSMenuItem.separator())
// self.addXdebugMenuItem()
self.addXdebugMenuItem()
self.addPresetsMenuItem()
self.addFirstAidAndServicesMenuItems()
}
@ -86,154 +87,9 @@ class StatusMenu: NSMenu {
action: #selector(MainMenu.terminateApp), keyEquivalent: "q"))
}
// MARK: Remaining Menu Items
func addConfigurationMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
self.addItem(
NSMenuItem(title: "mi_php_config".localized,
action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c")
)
self.addItem(
NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i")
)
}
func addComposerMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized))
self.addItem(
NSMenuItem(title: "mi_global_composer".localized,
action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g")
)
let composerMenuItem = NSMenuItem(
title: "mi_update_global_composer".localized,
action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies),
keyEquivalent: "g"
)
composerMenuItem.keyEquivalentModifierMask = .shift
self.addItem(composerMenuItem)
}
func addStatsMenuItem() {
guard let stats = PhpEnv.phpInstall.limits else { return }
self.addItem(StatsView.asMenuItem(
memory: stats.memory_limit,
post: stats.post_max_size,
upload: stats.upload_max_filesize)
)
}
func addExtensionsMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
if PhpEnv.phpInstall.extensions.isEmpty {
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
}
var shortcutKey = 1
for phpExtension in PhpEnv.phpInstall.extensions {
self.addExtensionItem(phpExtension, shortcutKey)
shortcutKey += 1
}
}
func addXdebugMenuItem() {
if !Xdebug.enabled {
return
}
let xdebugSwitch = NSMenuItem(
title: "mi_xdebug_mode".localized,
action: nil,
keyEquivalent: ""
)
let xdebugModesMenu = NSMenu()
let xdebugMode = Xdebug.mode
for mode in Xdebug.modes {
let item = XdebugMenuItem(
title: mode,
action: #selector(MainMenu.toggleXdebugMode(sender:)),
keyEquivalent: ""
)
item.state = xdebugMode == mode ? .on : .off
item.mode = mode
xdebugModesMenu.addItem(item)
}
for item in xdebugModesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(xdebugModesMenu, for: xdebugSwitch)
self.addItem(xdebugSwitch)
}
func addFirstAidAndServicesMenuItems() {
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
let servicesMenu = NSMenu()
let fixMyValetMenuItem = NSMenuItem(
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
)
fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized
servicesMenu.addItem(fixMyValetMenuItem)
let fixHomebrewMenuItem = NSMenuItem(
title: "mi_fix_brew_permissions".localized(),
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: ""
)
fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized
servicesMenu.addItem(fixHomebrewMenuItem)
servicesMenu.addItem(NSMenuItem.separator())
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_dnsmasq".localized,
action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_php_fpm".localized,
action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_nginx".localized,
action: #selector(MainMenu.restartNginx), keyEquivalent: "n")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_restart_all_services".localized,
action: #selector(MainMenu.restartAllServices), keyEquivalent: "s")
)
servicesMenu.addItem(
NSMenuItem(title: "mi_stop_all_services".localized,
action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
withKeyModifier: [.command, .shift]
)
servicesMenu.addItem(NSMenuItem.separator())
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized))
servicesMenu.addItem(
NSMenuItem(title: "mi_php_refresh".localized,
action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r")
)
for item in servicesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(servicesMenu, for: services)
self.addItem(services)
}
// MARK: Private Helpers
private func addSwitchToPhpMenuItems() {
internal func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
@ -260,7 +116,7 @@ class StatusMenu: NSMenu {
}
}
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
internal func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
let menuItem = ExtensionMenuItem(
@ -279,21 +135,3 @@ class StatusMenu: NSMenu {
self.addItem(menuItem)
}
}
// MARK: - NSMenuItem subclasses
class PhpMenuItem: NSMenuItem {
var version: String = ""
}
class XdebugMenuItem: NSMenuItem {
var mode: String = ""
}
class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension?
}
class EditorMenuItem: NSMenuItem {
var editor: Application?
}

View File

@ -10,8 +10,20 @@ import Foundation
struct CustomPrefs: Decodable {
let scanApps: [String]
let presets: [Preset]?
let services: [String]?
public func hasPresets() -> Bool {
return self.presets != nil && !self.presets!.isEmpty
}
public func hasServices() -> Bool {
return self.services != nil && !self.services!.isEmpty
}
private enum CodingKeys: String, CodingKey {
case scanApps = "scan_apps"
case presets = "presets"
case services = "services"
}
}

View File

@ -12,15 +12,28 @@ import Foundation
These are the keys used for every preference in the app.
*/
enum PreferenceName: String {
// FIRST-TIME LAUNCH
case wasLaunchedBefore = "launched_before"
case shouldDisplayDynamicIcon = "use_dynamic_icon"
case iconTypeToDisplay = "icon_type_to_display"
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
// GENERAL
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch"
case allowProtocolForIntegrations = "allow_protocol_for_integrations"
case globalHotkey = "global_hotkey"
case automaticBackgroundUpdateCheck = "backgroundUpdateCheck"
// APPEARANCE
case shouldDisplayDynamicIcon = "use_dynamic_icon"
case iconTypeToDisplay = "icon_type_to_display"
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
// NOTIFICATIONS
case notifyAboutVersionChange = "notify_about_version_change"
case notifyAboutPhpFpmRestart = "notify_about_php_fpm_restart"
case notifyAboutServices = "notify_about_services_restart"
case notifyAboutPresets = "notify_about_presets"
case notifyAboutSecureToggle = "notify_about_secure_toggle"
case notifyAboutGlobalComposerStatus = "notify_about_composer_status"
}
/**
@ -52,7 +65,7 @@ class Preferences {
public init() {
Preferences.handleFirstTimeLaunch()
cachedPreferences = Self.cache()
customPreferences = CustomPrefs(scanApps: [])
customPreferences = CustomPrefs(scanApps: [], presets: [], services: [])
loadCustomPreferences()
}
@ -70,14 +83,25 @@ class Preferences {
*/
static func handleFirstTimeLaunch() {
UserDefaults.standard.register(defaults: [
/// Preferences
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
PreferenceName.iconTypeToDisplay.rawValue: MenuBarIcon.iconPhp.rawValue,
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
/// Preferences: General
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true,
PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue: false,
PreferenceName.allowProtocolForIntegrations.rawValue: true,
PreferenceName.automaticBackgroundUpdateCheck.rawValue: true,
/// Preferences: Appearance
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
PreferenceName.iconTypeToDisplay.rawValue: MenuBarIcon.iconPhp.rawValue,
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
/// Preferences: Notifications
PreferenceName.notifyAboutVersionChange.rawValue: true,
PreferenceName.notifyAboutPhpFpmRestart.rawValue: true,
PreferenceName.notifyAboutServices.rawValue: true,
PreferenceName.notifyAboutPresets.rawValue: true,
PreferenceName.notifyAboutSecureToggle.rawValue: true,
PreferenceName.notifyAboutGlobalComposerStatus.rawValue: true,
/// Stats
InternalStats.switchCount.rawValue: 0,
InternalStats.launchCount.rawValue: 0,
@ -137,7 +161,8 @@ class Preferences {
private static func cache() -> [PreferenceName: Any] {
return [
// Part 1: Always Booleans
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(
.shouldDisplayDynamicIcon:
UserDefaults.standard.bool(
forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(
forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
@ -150,6 +175,19 @@ class Preferences {
.automaticBackgroundUpdateCheck: UserDefaults.standard.bool(
forKey: PreferenceName.automaticBackgroundUpdateCheck.rawValue) as Any,
.notifyAboutVersionChange: UserDefaults.standard.bool(
forKey: PreferenceName.notifyAboutVersionChange.rawValue) as Any,
.notifyAboutPhpFpmRestart: UserDefaults.standard.bool(
forKey: PreferenceName.notifyAboutPhpFpmRestart.rawValue) as Any,
.notifyAboutServices: UserDefaults.standard.bool(
forKey: PreferenceName.notifyAboutServices.rawValue) as Any,
.notifyAboutPresets: UserDefaults.standard.bool(
forKey: PreferenceName.notifyAboutPresets.rawValue) as Any,
.notifyAboutSecureToggle: UserDefaults.standard.bool(
forKey: PreferenceName.notifyAboutSecureToggle.rawValue) as Any,
.notifyAboutGlobalComposerStatus: UserDefaults.standard.bool(
forKey: PreferenceName.notifyAboutGlobalComposerStatus.rawValue) as Any,
// Part 2: Always Strings
.globalHotkey: UserDefaults.standard.string(
forKey: PreferenceName.globalHotkey.rawValue) as Any,
@ -173,12 +211,28 @@ class Preferences {
// MARK: - Custom Preferences
private func loadCustomPreferences() {
let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json")
// Ensure the configuration directory is created if missing
Shell.run("mkdir -p ~/.config/phpmon")
// Move the legacy file
moveOutdatedConfigurationFile()
// Attempt to load the file if it exists
let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.config/phpmon/config.json")
if Filesystem.fileExists(url.path) {
Log.info("A custom .phpmon.conf.json file was found. Attempting to parse...")
Log.info("A custom ~/.config/phpmon/config.json file was found. Attempting to parse...")
loadCustomPreferencesFile(url)
} else {
Log.info("There was no .phpmon.conf.json file to be loaded.")
Log.info("There was no /.config/phpmon/config.json file to be loaded.")
}
}
private func moveOutdatedConfigurationFile() {
if Filesystem.fileExists("~/.phpmon.conf.json") && !Filesystem.fileExists("~/.config/phpmon/config.json") {
Log.info("An outdated configuration file was found. Moving it...")
Shell.run("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
Log.info("The configuration file was copied successfully!")
}
}
@ -188,9 +242,18 @@ class Preferences {
CustomPrefs.self,
from: try! String(contentsOf: url, encoding: .utf8).data(using: .utf8)!
)
Log.info("The .phpmon.conf.json file was successfully parsed.")
Log.info("The ~/.config/phpmon/config.json file was successfully parsed.")
if customPreferences.hasPresets() {
Log.info("There are \(customPreferences.presets!.count) custom presets.")
}
if customPreferences.hasServices() {
Log.info("There are custom services: \(customPreferences.services!)")
}
} catch {
Log.warn("The .phpmon.conf.json file seems to be missing or malformed.")
Log.warn("The ~/.config/phpmon/config.json file seems to be missing or malformed.")
}
}

View File

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

View File

@ -21,16 +21,104 @@ class PrefsWC: PMWindowController {
return "Preferences"
}
public static func create(delegate: NSWindowDelegate?) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(
withIdentifier: "preferencesWindow"
) as! PrefsWC
windowController.window!.title = "prefs.title".localized
windowController.window!.subtitle = "prefs.subtitle".localized
windowController.window!.delegate = delegate
windowController.window!.styleMask = [.titled, .closable, .miniaturizable]
windowController.window!.delegate = windowController
App.shared.preferencesWindowController = windowController
}
public static func show(delegate: NSWindowDelegate? = nil) {
var justCreated = false
if App.shared.preferencesWindowController == nil {
Self.create(delegate: delegate)
guard let preferencesWC = App.shared.preferencesWindowController else {
return
}
guard let tabVC = preferencesWC.contentViewController as? NSTabViewController else {
return
}
for vc in preferencesWC.tabVCs {
tabVC.addChild(vc.viewController)
let item = tabVC.tabViewItem(for: vc.viewController)
item?.image = NSImage(systemSymbolName: vc.icon, accessibilityDescription: "")
item?.label = vc.label
}
tabVC.preferredContentSize = NSSize(
width: tabVC.view.frame.size.width,
height: tabVC.view.frame.size.height
)
justCreated = true
}
App.shared.preferencesWindowController?.showWindow(self)
if justCreated {
App.shared.preferencesWindowController?.positionWindowInTopLeftCorner()
}
NSApp.activate(ignoringOtherApps: true)
}
struct PrefTabView {
let viewController: GenericPreferenceVC
let label: String
let icon: String
}
public lazy var tabVCs: [PrefTabView] = {
return [
PrefTabView(
viewController: GeneralPreferencesVC.fromStoryboard(),
label: "General",
icon: "gearshape"
),
PrefTabView(
viewController: AppearancePreferencesVC.fromStoryboard(),
label: "Appearance",
icon: "paintbrush"
),
PrefTabView(
viewController: NotificationPreferencesVC.fromStoryboard(),
label: "Notifications",
icon: "bell.badge"
)
]
}()
// MARK: - Key Interaction
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)
if let vc = contentViewController as? PrefsVC {
guard let tabVC = self.contentViewController as? NSTabViewController else {
return
}
guard let selected = tabVC.tabViewItems[tabVC.selectedTabViewItemIndex].viewController else {
return
}
if let vc = selected as? GenericPreferenceVC {
if vc.listeningForHotkeyView != nil {
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
Log.info("A blacklisted key was pressed, canceling listen!")
vc.listeningForHotkeyView = nil
vc.listeningForHotkeyView!.unregister(nil)
} else {
vc.listeningForHotkeyView!.updateShortcut(event)
}

View File

@ -11,7 +11,7 @@ import Cocoa
class HotkeyPreferenceView: NSView, XibLoadable {
weak var delegate: PrefsVC?
weak var delegate: GenericPreferenceVC?
@IBOutlet weak var labelSection: NSTextField!
@IBOutlet weak var labelDescription: NSTextField!
@ -19,7 +19,7 @@ class HotkeyPreferenceView: NSView, XibLoadable {
@IBOutlet weak var buttonSetShortcut: NSButton!
@IBOutlet weak var buttonClearShortcut: NSButton!
static func make(sectionText: String, descriptionText: String, _ prefsVC: PrefsVC) -> NSView {
static func make(sectionText: String, descriptionText: String, _ prefsVC: GenericPreferenceVC) -> NSView {
let view = Self.createFromXib()!
view.labelSection.stringValue = sectionText
view.labelDescription.stringValue = descriptionText

View File

@ -0,0 +1,271 @@
//
// Preset.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 31/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
struct Preset: Codable, Equatable {
let name: String
let version: String?
let extensions: [String: Bool]
let configuration: [String: String?]
public enum CodingKeys: String, CodingKey {
case version = "php",
name = "name",
extensions = "extensions",
configuration = "configuration"
}
/**
What the preset does, in text form. Used to display what's going on.
*/
var textDescription: String {
var text = ""
if self.version != nil {
text += "alert.preset_description.switcher_version".localized(self.version!)
}
if !self.extensions.isEmpty {
// Show a subsection header
text += "alert.preset_description.applying_extensions".localized
}
for (ext, extValue) in self.extensions {
// An extension is either enabled or disabled
let status = extValue
? "alert.preset_description.enabled".localized
: "alert.preset_description.disabled".localized
text += "\(ext): \(status)\n"
}
if !self.configuration.isEmpty {
// Extra spacing if the previous section was extensions
if !self.extensions.isEmpty {
text += "\n"
}
// Show a subsection header
text += "alert.preset_description.applying_config".localized
}
for (key, value) in self.configuration {
// A value is either displayed, or the value is "(empty)"
text += "\(key)=\(value ?? "alert.preset_description.empty".localized) \n"
}
return text
}
// MARK: Applying
/**
Applies a given preset.
*/
public func apply() {
Task {
// Was this a rollback?
let wasRollback = (self.name == "AutomaticRevertSnapshot")
// Save the preset that would revert this preset
self.persistRevert()
// Apply the PHP version if is considered a valid version
if self.version != nil {
if await !switchToPhpVersionIfValid() {
PresetHelper.rollbackPreset = nil
Actions.restartPhpFpm()
return
}
}
// Apply the configuration changes first
for conf in configuration {
applyConfigurationValue(key: conf.key, value: conf.value ?? "")
}
// Apply the extension changes in-place afterward
for ext in extensions {
for foundExt in PhpEnv.phpInstall.extensions
where foundExt.name == ext.key && foundExt.enabled != ext.value {
Log.info("Toggling extension \(foundExt.name) in \(foundExt.file)")
foundExt.toggle()
break
}
}
// Reload what rollback file exists
PresetHelper.loadRollbackPresetFromFile()
// Restart PHP FPM process (also reloads menu, which will show the preset rollback)
Actions.restartPhpFpm()
// Show the correct notification
if wasRollback {
LocalNotification.send(
title: "notification.preset_reverted_title".localized,
subtitle: "notification.preset_reverted_desc".localized,
preference: .notifyAboutPresets
)
} else {
LocalNotification.send(
title: "notification.preset_applied_title".localized,
subtitle: "notification.preset_applied_desc".localized(self.name),
preference: .notifyAboutPresets
)
}
}
}
// MARK: - Apply Functionality
private func switchToPhpVersionIfValid() async -> Bool {
if PhpEnv.shared.currentInstall.version.short == self.version! {
Log.info("The version we are supposed to switch to is already active.")
return true
}
if PhpEnv.shared.availablePhpVersions.first(where: { $0 == self.version }) != nil {
await MainMenu.shared.switchToPhp(self.version!)
return true
} else {
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "alert.php_switch_unavailable.title".localized,
subtitle: "alert.php_switch_unavailable.subtitle".localized(version!),
description: "alert.php_switch_unavailable.info".localized(
version!,
PhpEnv.shared.availablePhpVersions.joined(separator: ", ")
)
).withPrimary(
text: "alert.php_switch_unavailable.ok".localized
).show()
}
return false
}
}
private func applyConfigurationValue(key: String, value: String) {
guard let file = PhpEnv.shared.getConfigFile(forKey: key) else {
return
}
do {
if file.has(key: key) {
Log.info("Setting config value \(key) in \(file.filePath)")
try file.replace(key: key, value: value)
}
} catch {
Log.err("Setting \(key) to \(value) failed.")
}
}
// MARK: - Menu Items
public func getMenuItemText() -> String {
var info = extensions.count == 1
? "preset.extension".localized(extensions.count)
: "preset.extensions".localized(extensions.count)
info += ", "
info += configuration.count == 1
? "preset.preference".localized(configuration.count)
: "preset.preferences".localized(configuration.count)
if self.version == nil {
return "<span style=\"font-family: '-apple-system'; font-size: 12px;\">"
+ "<b>\(name.stripped)</b><br/>"
+ "<i style=\"font-size: 11px;\">"
+ info + "</i>"
+ "</span>"
}
return "<span style=\"font-family: '-apple-system'; font-size: 12px;\">"
+ "<b>\(name.stripped)</b><br/>"
+ "<i style=\"font-size: 11px;\">"
+ "Switches to PHP \(version!)<br/>"
+ info + "</i>"
+ "</span>"
}
// MARK: - Reverting
public var revertSnapshot: Preset {
return Preset(
name: "AutomaticRevertSnapshot",
version: diffVersion(),
extensions: diffExtensions(),
configuration: diffConfiguration()
)
}
/**
Returns the version that was previously active, which would revert this preset's version.
Returns nil if the version is not specified or the same.
*/
private func diffVersion() -> String? {
guard let version = self.version else {
return nil
}
if PhpEnv.shared.currentInstall.version.short != version {
return PhpEnv.shared.currentInstall.version.short
} else {
return nil
}
}
/**
Returns a list of extensions which would revert this presets's setup.
*/
private func diffExtensions() -> [String: Bool] {
var items: [String: Bool] = [:]
for (key, value) in self.extensions {
for foundExt in PhpEnv.phpInstall.extensions
where foundExt.name == key && foundExt.enabled != value {
// Save the original value of the extension
items[foundExt.name] = foundExt.enabled
}
}
return items
}
/**
Returns a list of configuration items which would revert this presets's setup.
*/
private func diffConfiguration() -> [String: String?] {
var items: [String: String?] = [:]
for (key, _) in self.configuration {
guard let file = PhpEnv.shared.getConfigFile(forKey: key) else {
break
}
items[key] = file.get(for: key)
}
return items
}
/**
Persists the revert as a JSON file, so it can be read from a file after restarting PHP Monitor.
*/
private func persistRevert() {
let data = try! JSONEncoder().encode(self.revertSnapshot)
Shell.run("mkdir -p ~/.config/phpmon")
try! String(data: data, encoding: .utf8)!
.write(
toFile: "/Users/\(Paths.whoami)/.config/phpmon/preset_revert.json",
atomically: true,
encoding: .utf8
)
}
}

View File

@ -0,0 +1,37 @@
//
// PresetHelper.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 02/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class PresetHelper {
static var rollbackPreset: Preset?
// MARK: - Reloading Configuration
public static func loadRollbackPresetFromFile() {
guard let revert = try? String(
contentsOfFile: "/Users/\(Paths.whoami)/.config/phpmon/preset_revert.json",
encoding: .utf8
) else {
PresetHelper.rollbackPreset = nil
return
}
guard let preset = try? JSONDecoder().decode(
Preset.self,
from: revert.data(using: .utf8)!
) else {
PresetHelper.rollbackPreset = nil
return
}
PresetHelper.rollbackPreset = preset
}
}

View File

@ -0,0 +1,27 @@
//
// SwiftUIHelper.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import SwiftUI
var isRunningSwiftUIPreview: Bool {
return ProcessInfo.processInfo
.environment["XCODE_RUNNING_FOR_PREVIEWS"] != nil
}
extension Color {
public static var appPrimary: Color = Color("AppColor")
public static var appSecondary: Color = Color("AppSecondary")
public static var debug: Color = {
if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil {
return Color.yellow
}
return Color.clear
}()
}

View File

@ -0,0 +1,195 @@
//
// VersionPopoverView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import SwiftUI
struct VersionPopoverView: View {
@State var site: ValetSite
@State var validPhpVersions: [PhpVersionNumber]
@State var parent: NSPopover!
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(getTitleText())
.fontWeight(.bold)
.fixedSize(horizontal: false, vertical: true)
Text(getSourceText())
.fixedSize(horizontal: false, vertical: true)
.font(.subheadline)
if !validPhpVersions.isEmpty {
// Suggestions for alternative PHP versions
VStack(alignment: .leading, spacing: 10) {
DisclaimerView(
iconName: "info.circle.fill",
message: "alert.php_suggestions".localized,
color: Color("AppColor")
)
HStack {
ForEach(validPhpVersions, id: \.self) { version in
Button("site_link.switch_to_php".localized(version.homebrewVersion), action: {
MainMenu.shared.switchToPhpVersion(version.homebrewVersion)
parent?.close()
})
}
}.padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0))
}
} else {
if site.composerPhpSource == .unknown {
// We don't know which PHP version is required
DisclaimerView(
iconName: "questionmark.circle.fill",
message: "alert.unable_to_determine_is_fine".localized
)
} else {
if site.composerPhpCompatibleWithLinked {
DisclaimerView(
iconName: "checkmark.circle.fill",
message: "alert.php_version_ideal".localized,
color: Color("IconColorGreen")
)
} else {
DisclaimerView(
iconName: "exclamationmark.circle.fill",
message: "alert.php_version_incorrect".localized,
color: Color("IconColorRed")
)
}
}
}
}.frame(width: 400, height: nil, alignment: .center)
.padding(20)
.background(
Color(NSColor.windowBackgroundColor)
.padding(-80)
)
}
func getTitleText() -> String {
if site.composerPhpSource == .unknown {
return "alert.composer_php_requirement.unable_to_determine".localized
}
return "alert.composer_php_requirement.title".localized(
"\(site.name).\(Valet.shared.config.tld)",
site.composerPhp
)
}
func getSourceText() -> String {
var information = ""
if site.isolatedPhpVersion != nil {
information += "alert.composer_php_isolated.desc".localized(
site.isolatedPhpVersion!.versionNumber.homebrewVersion,
PhpEnv.phpInstall.version.short
)
information += "\n\n"
}
information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)"
.localized
return information
}
}
struct DisclaimerView: View {
@State var iconName: String
@State var message: String
@State var color: Color = Color.secondary
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 5) {
Image(systemName: iconName)
.renderingMode(.template)
.foregroundColor(color)
Text(message)
.font(.subheadline)
.foregroundColor(color)
}.padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0))
}
}
struct VersionPopoverView_Previews: PreviewProvider {
static var previews: some View {
VersionPopoverView(
site: ValetSite(
fakeWithName: "amazingwebsite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: ""
),
validPhpVersions: [],
parent: nil
)
.previewDisplayName("Unknown Requirement")
VersionPopoverView(
site: ValetSite(
fakeWithName: "amazingwebsite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.1"
),
validPhpVersions: [],
parent: nil
)
.previewDisplayName("Requirement Matches")
VersionPopoverView(
site: ValetSite(
fakeWithName: "anothersite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.0",
isolated: "8.0"
),
validPhpVersions: [],
parent: nil
)
.previewDisplayName("Isolated")
VersionPopoverView(
site: ValetSite(
fakeWithName: "anothersite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.0",
isolated: "7.4"
),
validPhpVersions: [],
parent: nil
)
.previewDisplayName("Isolated Mismatch")
VersionPopoverView(
site: ValetSite(
fakeWithName: "anothersite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.0"
),
validPhpVersions: [
PhpVersionNumber(major: 8, minor: 0, patch: 0),
PhpVersionNumber(major: 8, minor: 1, patch: 0)
],
parent: nil
)
.previewDisplayName("Recommend Alternatives")
}
}

View File

@ -0,0 +1,47 @@
//
// MiniHeaderView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import SwiftUI
struct HeaderView: View {
@State var text: String
var body: some View {
Text(text.uppercased())
.font(.system(size: 12))
.fontWeight(.bold)
.foregroundColor(.appSecondary)
.multilineTextAlignment(.leading)
.padding(.leading, 14.0)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.debug)
}
// MARK: - NSMenuItem
static func asMenuItem(
text: String,
width: Int? = nil
) -> NSMenuItem {
let view = NSHostingView(rootView: Self(text: text))
view.autoresizingMask = [.width, .height]
view.setFrameSize(CGSize(width: view.frame.width, height: 24))
let item = NSMenuItem()
item.view = view
return item
}
}
struct HeaderView_Previews: PreviewProvider {
static var previews: some View {
HeaderView(text: "Hello world")
.frame(width: 330.0)
}
}

View File

@ -0,0 +1,22 @@
//
// MiniHeaderView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import SwiftUI
struct SectionHeaderView: View {
@State var text: String
var body: some View {
Text(text)
.font(.system(size: 11))
.fontWeight(.medium)
.foregroundColor(.appSecondary)
.background(Color.debug)
}
}

View File

@ -0,0 +1,172 @@
//
// ServicesView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import SwiftUI
struct ServicesView: View {
@ObservedObject var manager: ServicesManager
@State var servicesToDisplay: [String]
@State var perRow: Int = 3
static func asMenuItem(perRow: Int = 3) -> NSMenuItem {
let item = NSMenuItem()
var services = [
PhpEnv.phpInstall.formula,
"nginx",
"dnsmasq"
]
if Preferences.custom.hasServices() {
services += Preferences.custom.services!
}
let view = NSHostingView(
rootView: Self(
manager: ServicesManager.shared,
servicesToDisplay: services,
perRow: perRow
)
)
view.autoresizingMask = [.width, .height]
let height = CGFloat(45 * services.chunked(by: perRow).count)
view.setFrameSize(CGSize(width: view.frame.width, height: height))
item.view = view
return item
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
ForEach(servicesToDisplay.chunked(by: self.perRow), id: \.self) { chunk in
HStack {
ForEach(0...self.perRow - 1, id: \.self) { index in
if chunk.indices.contains(index) {
// A service exists to fill the cell
let service = chunk[index]
VStack(alignment: .center, spacing: 3) {
SectionHeaderView(text: service.uppercased())
CheckmarkView(serviceName: service)
.environmentObject(manager)
}.frame(minWidth: 0, maxWidth: .infinity)
} else {
// Empty cell
VStack { EmptyView() }.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
}
}
.padding(10)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.debug)
}
}
struct CheckmarkView: View {
@State var serviceName: String
@State var busy: Bool = false
@EnvironmentObject var manager: ServicesManager
public func hasAnyServices() -> Bool {
return !manager.rootServices.isEmpty
}
public func active() -> Bool? {
if manager.rootServices.keys.contains(serviceName) {
return manager.rootServices[serviceName]!.running
}
if manager.userServices.keys.contains(serviceName) {
return manager.userServices[serviceName]!.running
}
return nil
}
public func toggleService() {
if active()! {
Actions.stopService(name: serviceName, completion: {
busy = false
})
} else {
Actions.startService(name: serviceName, completion: {
busy = false
})
}
}
var body: some View {
if !hasAnyServices() {
Image(systemName: "hourglass.circle")
.resizable()
.frame(width: 16.0, height: 16.0)
.foregroundColor(.appSecondary)
} else {
if busy {
ProgressView()
.scaleEffect(x: 0.5, y: 0.5, anchor: .center)
.frame(width: 16.0, height: 20.0)
} else if active() == nil {
Button { } label: {
Text("?")
}.disabled(true)
} else {
Button {
busy = true
toggleService()
} label: {
Image(systemName: active()! ? "checkmark" : "xmark")
.resizable()
.frame(width: 12.0, height: 12.0)
.foregroundColor(active()! ? Color.primary : Color("IconColorRed"))
}
}
}
}
}
struct ServicesView_Previews: PreviewProvider {
static var previews: some View {
ServicesView(
manager: ServicesManager()
.withDummyServices([:]),
servicesToDisplay: ["php", "nginx", "dnsmasq"]
)
.frame(width: 330.0)
.previewDisplayName("Loading")
ServicesView(
manager: ServicesManager()
.withDummyServices([
"php": false,
"nginx": true,
"dnsmasq": true
]),
servicesToDisplay: ["php", "nginx", "dnsmasq"]
)
.frame(width: 330.0)
.previewDisplayName("Light Mode")
ServicesView(
manager: ServicesManager()
.withDummyServices([
"php": false,
"nginx": true,
"dnsmasq": true,
"mysql": false
]),
servicesToDisplay: ["php", "nginx", "dnsmasq",
"mysql", "redis", "php@7.4"],
perRow: 3
)
.frame(width: 330.0)
.previewDisplayName("Dark Mode")
.preferredColorScheme(.dark)
}
}

View File

@ -0,0 +1,66 @@
//
// StatsView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 09/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import SwiftUI
struct StatsView: View {
static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem {
let item = NSMenuItem()
let view = NSHostingView(
rootView: Self(
memoryLimit: memory,
maxPostSize: post,
maxUploadSize: upload
)
)
view.autoresizingMask = [.width, .height]
view.setFrameSize(CGSize(width: view.frame.width, height: 55))
item.view = view
return item
}
@State var memoryLimit: String
@State var maxPostSize: String
@State var maxUploadSize: String
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 30) {
VStack(alignment: .center, spacing: 3) {
SectionHeaderView(text: "mi_memory_limit".localized.uppercased())
Text(memoryLimit)
.fontWeight(.medium)
.font(.system(size: 16))
}
VStack(alignment: .center, spacing: 3) {
SectionHeaderView(text: "mi_post_max_size".localized.uppercased())
Text(maxPostSize)
.fontWeight(.medium)
.font(.system(size: 16))
}
VStack(alignment: .center, spacing: 3) {
SectionHeaderView(text: "mi_upload_max_filesize".localized.uppercased())
Text(maxUploadSize)
.fontWeight(.medium)
.font(.system(size: 16))
}
}
.padding(10)
.background(Color.debug)
}
}
struct StatsView_Previews: PreviewProvider {
static var previews: some View {
StatsView(
memoryLimit: "1024 MB",
maxPostSize: "1024 MB",
maxUploadSize: "1024 MB"
)
}
}

View File

@ -1,29 +0,0 @@
//
// PMHeaderView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/04/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import SwiftUI
@available(OSX 11.0, *)
struct PMHeaderView: View {
@State var content: String = "Your Title Here"
var body: some View {
PMHeader(labelText: $content).frame(minWidth: 0, maxWidth: 450, minHeight: 0, maxHeight: 50)
}
}
@available(OSX 11.0, *)
struct PMHeader: NSViewRepresentable {
@Binding var labelText: String
func makeNSView(context: Context) -> some NSView {
return HeaderView.asMenuItem(text: labelText).view!
}
func updateNSView(_ nsView: NSViewType, context: Context) {}
}

View File

@ -1,25 +0,0 @@
//
// PMHeaderView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/04/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import SwiftUI
@available(OSX 11.0, *)
struct PMServicesView: View {
var body: some View {
PMServices().frame(minWidth: 0, maxWidth: 450, minHeight: 0, maxHeight: 50)
}
}
@available(OSX 11.0, *)
struct PMServices: NSViewRepresentable {
func makeNSView(context: Context) -> some NSView {
return ServicesView.asMenuItem().view!
}
func updateNSView(_ nsView: NSViewType, context: Context) {}
}

View File

@ -1,29 +0,0 @@
//
// PMStatsView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/04/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import SwiftUI
@available(OSX 11.0, *)
struct PMStats: NSViewRepresentable {
@Binding var labelText: String
func makeNSView(context: Context) -> some NSView {
return StatsView.asMenuItem(memory: labelText, post: labelText, upload: labelText).view!
}
func updateNSView(_ nsView: NSViewType, context: Context) {}
}
@available(OSX 11.0, *)
struct PMStatsView: View {
@State var content: String = "5 MB"
var body: some View {
PMStats(labelText: $content).frame(minWidth: 0, maxWidth: 450, minHeight: 0, maxHeight: 80)
}
}

View File

@ -1,20 +0,0 @@
//
// Preview.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/04/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import SwiftUI
import Cocoa
@available(OSX 11.0, *)
struct Preview_Previews: PreviewProvider {
static var previews: some View {
PMHeaderView(content: "You are running PHP 8.1")
PMStatsView(content: "15 MB")
PMStatsView(content: "2 GB")
PMServicesView() // uses live services data!
}
}

View File

@ -11,16 +11,16 @@ import Foundation
extension App {
func startWatcher(_ url: URL) {
Log.info("No watcher currently active...")
Log.perf("No watcher currently active...")
self.watcher = PhpConfigWatcher(for: url)
self.watcher.didChange = { url in
Log.info("Something has changed in: \(url)")
Log.perf("Something has changed in: \(url)")
// Check if the watcher has last updated the menu less than 0.75s ago
let distance = self.watcher.lastUpdate?.distance(to: Date().timeIntervalSince1970)
if distance == nil || distance != nil && distance! > 0.75 {
Log.info("Refreshing menu...")
Log.perf("Refreshing menu...")
MainMenu.shared.reloadPhpMonitorMenuInBackground()
self.watcher.lastUpdate = Date().timeIntervalSince1970
}
@ -43,7 +43,7 @@ extension App {
if self.watcher.url != url || forceReload {
self.watcher.disable()
self.watcher = nil
Log.info("Watcher has stopped watching files. Starting new one...")
Log.perf("Watcher has stopped watching files. Starting new one...")
self.startWatcher(url)
}
}

View File

@ -23,8 +23,8 @@
"mi_restart_nginx" = "Restart Service: nginx";
"mi_restart_dnsmasq" = "Restart Service: dnsmasq";
"mi_manage_services" = "Manage Services";
"mi_restart_all_services" = "Restart All Services";
"mi_stop_all_services" = "Stop All Services";
"mi_restart_valet_services" = "Restart Valet Services";
"mi_stop_valet_services" = "Stop Valet Services";
"mi_fix_my_valet" = "Fix My Valet...";
"mi_fix_my_valet_tooltip" = "Something wrong with your Valet installation? Try PHP Monitors automatic fixes thatll get you back up and running in no time!";
@ -43,11 +43,12 @@
"mi_other" = "First Aid & Services";
"mi_first_aid" = "First Aid";
"mi_xdebug_mode" = "Switch Xdebug Mode";
"mi_xdebug_mode" = "Manage Xdebug";
"mi_composer" = "Composer";
"mi_valet_config" = "Locate Valet Folder (.config/valet)";
"mi_php_config" = "Locate PHP Configuration File (php.ini)";
"mi_phpmon_config" = "Locate PHP Monitor Folder (.config/phpmon)";
"mi_global_composer" = "Locate Global Composer File (.composer)";
"mi_phpinfo" = "Show Current Configuration (phpinfo)";
"mi_update_global_composer" = "Update Global Composer Dependencies...";
@ -63,6 +64,24 @@
"mi_quit" = "Quit PHP Monitor";
"mi_about" = "About PHP Monitor";
"mi_presets_title" = "Configuration Presets";
"mi_apply_presets_title" = "Apply Configuration Presets";
"mi_revert_to_prev_config" = "Revert to Previous Configuration...";
"mi_profiles_loaded" = "%i profiles loaded from configuration file";
"mi_no_presets" = "No presets available.";
"mi_set_up_presets" = "Learn more about presets...";
"mi_xdebug_available_modes" = "Available Modes";
"mi_xdebug_actions" = "Actions";
"mi_xdebug_disable_all" = "Disable All Modes";
// PRESET LOADING
"preset_help_title" = "Working with Configuration Presets";
"preset_help_info" = "You can set up configuration presets in the config.json file, located in ~/.config/phpmon/config.json. These presets can apply a selection of configuration values all at once. This is a powerful feature, but currently needs to be set up manually.";
"preset_help_desc" = "After restarting PHP Monitor, all presets found in the file will be loaded. If no presets appear, the file probably could not be parsed correctly.\n\nYou can click the question mark in this alert to go to the FAQ on GitHub, where you can find more information about this feature, including a sample file.";
// MENU ITEMS (if window is open)
"mm_add_folder_as_link" = "Add Folder as Link...";
@ -167,6 +186,13 @@
"driver.not_detected" = "Other";
// PRESET
"preset.extension" = "%i extension";
"preset.extensions" = "%i extensions";
"preset.preference" = "%i preference";
"preset.preferences" = "%i preferences";
// EDITORS
"editors.alert.try_again" = "Try Again";
@ -186,6 +212,7 @@
"prefs.switcher" = "Switcher:";
"prefs.integrations" = "Integrations:";
"prefs.updates" = "Updates:";
"prefs.notifications" = "Notifications:";
"prefs.icon_options.php" = "Display PHP Icon";
"prefs.icon_options.elephant" = "Display Elephant Icon";
@ -216,6 +243,24 @@
"prefs.shortcut_clear" = "Clear";
"prefs.shortcut_desc" = "If a shortcut combination is set up, you can toggle PHP Monitor wherever you are by pressing the key combination you chose. (Cancel choosing a shortcut by pressing the spacebar.)";
"prefs.notify_about_version_change_desc" = "Displays a notification whenever the active PHP version changes.";
"prefs.notify_about_version_change" = "Notify about PHP version switch";
"prefs.notify_about_php_fpm_change_desc" = "Displays a notification whenever the active PHP-FPM process has restarted due to a configuration change.";
"prefs.notify_about_php_fpm_change" = "Notify about PHP-FPM restart";
"prefs.notify_about_services_desc" = "Displays a notification whenever any of the Homebrew services (installed and configured by Valet) have been restarted or stopped.";
"prefs.notify_about_services" = "Notify about services status";
"prefs.notify_about_presets_desc" = "Displays a notification whenever a preset has been successfully applied or reverted.";
"prefs.notify_about_presets" = "Notify about applied presets";
"prefs.notify_about_secure_status_desc" = "Displays a notification when a domain has been secured or unsecured.";
"prefs.notify_about_secure_status" = "Notify about secure/unsecure status";
"prefs.notify_about_composer_success_desc" = "Displays a notification when the global composer configuration was successfully updated.";
"prefs.notify_about_composer_success" = "Notify about global composer update";
// NOTIFICATIONS
"notification.version_changed_title" = "PHP %@ now active";
@ -230,6 +275,12 @@
"notification.services_restarted" = "Valet services restarted";
"notification.services_restarted_desc" = "All services have been successfully restarted.";
"notification.preset_applied_title" = "Preset applied";
"notification.preset_applied_desc" = "The preset '%@' has been successfully applied.";
"notification.preset_reverted_title" = "Preset reverted";
"notification.preset_reverted_desc" = "The last preset you applied has been undone. Your previous configuration is now active.";
// Composer Update
"alert.composer_missing.title" = "Composer not found!";
"alert.composer_missing.subtitle" = "PHP Monitor could not find Composer. Make sure that Composer is installed and try again.";
@ -252,16 +303,23 @@ problem manually, using your own Terminal app (this just shows you the output)."
"alert.composer_success.info" = "Your global Composer dependencies have been successfully updated.";
// Composer Version
"alert.composer_php_isolated.desc" = "This site has been isolated, which means that Valet serves PHP %@ for this site specifically (the global version is %@).";
"alert.composer_php_requirement.title" = "`%@` has the following PHP requirement: %@.";
"alert.composer_php_requirement.type.unknown" = "The required PHP version is a mystery.";
"alert.composer_php_isolated.desc" = "This site has been isolated, which means that Valet serves PHP %@ for this site specifically. The global version is currently PHP %@.";
"alert.composer_php_requirement.title" = "'%@' requires PHP %@.";
"alert.composer_php_requirement.unable_to_determine" = "Unable to determine PHP requirement";
"alert.composer_php_requirement.type.unknown" = "PHP Monitor was unable to determine which version of PHP is required for this domain. The constraint may be determined if you have a `composer.json` or a `.valetphprc` file in your project's directory.";
"alert.composer_php_requirement.type.require" = "This required PHP version was determined by checking the `require` field in the `composer.json` file when the site list was last refreshed.";
"alert.composer_php_requirement.type.platform" = "This required PHP version was determined by checking the `platform` field in the `composer.json` file when the site list was last refreshed.";
"alert.composer_php_requirement.type.valetphprc" = "This required PHP version was determined by checking the .valetphprc file in your project's directory.";
"alert.unable_to_determine_is_fine" = "If you have a simple project, there may not be a specified PHP version set as a requirement. In that case, you are free to ignore this warning.";
"alert.php_version_ideal" = "The currently active PHP version is ideal for this site.";
"alert.php_version_incorrect" = "The currently active PHP version does not match the required constraint set for this site.";
"alert.php_suggestions" = "There may be a different PHP version which is closer to the constraint.";
// Suggest Fix My Valet
"alert.php_switch_failed.title" = "Switching to PHP %@ seems to have failed.";
"alert.php_switch_failed.info" = "PHP Monitor has detected that PHP %@ is not active after completing its switching procedure. You can try to run \"Fix My Valet\" and switch again after that. Do you want to try \"Fix My Valet\"?";
"alert.php_switch_failed.info" = "PHP Monitor has detected that PHP %@ is not active after completing its switching procedure. You can try to run \"Fix My Valet\" and switch again after that. Do you want to try this fix?";
"alert.php_switch_failed.desc" = "First off, you should try \"Fix My Valet\" if you haven't tried it yet. If PHP Monitor remains unable to change the active PHP version even after that, you may need to upgrade Valet and the Homebrew packages on your system. You can do this by running `brew update && brew upgrade` as well as upgrading Valet by running `composer global update && valet install`.";
"alert.php_switch_failed.confirm" = "Yes, run \"Fix My Valet\"";
"alert.php_switch_failed.cancel" = "Do Not Run";
@ -291,7 +349,7 @@ problem manually, using your own Terminal app (this just shows you the output)."
"alert.fix_homebrew_permissions_done.title" = "All file and folder permissions for Valet's dependencies have been restored.";
"alert.fix_homebrew_permissions_done.subtitle" = "Because of this, all of Valet's services are currently no longer running. You can now interact with Homebrew, but your Valet sites will be unavailable as all services are disabled.";
"alert.fix_homebrew_permissions_done.desc" = "When you are done with Homebrew (after running `brew upgrade`, for example) you should restart PHP Monitor and select \"Restart All Services\" if you want Valet to work again. It is always recommended to restart PHP Monitor whenever you upgrade PHP versions with `brew upgrade`, or things might break.";
"alert.fix_homebrew_permissions_done.desc" = "When you are done with Homebrew (after running `brew upgrade`, for example) you should restart PHP Monitor and select \"Restart Valet Services\" if you want Valet to work again. It is always recommended to restart PHP Monitor whenever you upgrade PHP versions with `brew upgrade`, or things might break.";
// PHP FPM Broken
"alert.php_fpm_broken.title" = "Your PHP-FPM configuration is not pointing at the Valet socket!";
@ -299,7 +357,7 @@ problem manually, using your own Terminal app (this just shows you the output)."
"alert.php_fpm_broken.description" = "If it's been a while, you can usually fix this by running `valet install`, which updates your PHP-FPM configuration.\n\nIf you are seeing this message and you are trying to run a pre-release version of PHP, it is possible that Valet does not support this pre-release version of PHP yet.\n\nIf that is the case, you can try the following workaround: edit the file at `~/.composer/vendor/laravel/valet/cli/Valet/Brew.php` and add e.g. `php@8.2` to the `SUPPORTED_PHP_VERSIONS` array. After editing the file, try running `valet install`. (This will, if all goes well, set up all the required Valet configuration files.)";
// PHP Monitor Cannot Start
"alert.cannot_start.title" = "PHP Monitor cannot start due to a configuration problem";
"alert.cannot_start.title" = "PHP Monitor cannot start due to a problem with your system configuration";
"alert.cannot_start.subtitle" = "The issue you were just notified about is keeping PHP Monitor from functioning correctly.";
"alert.cannot_start.description" = "You might not need to quit PHP Monitor and restart it. If you have fixed the issue (or don't remember what the exact issue is) you can click on Retry, which will have PHP Monitor retry the startup checks.";
"alert.cannot_start.close" = "Quit";
@ -316,6 +374,26 @@ For optimal support of the latest versions of PHP and proper version switching,
You can do this by running `composer global update` in your terminal. After that, run `valet install` again. For best results, restart PHP Monitor after that.";
// Preset text description
"alert.preset_description.switcher_version" = "Switches to PHP %@.\n\n";
"alert.preset_description.applying_extensions" = "Applies the following extensions:\n";
"alert.preset_description.applying_config" = "Applies the following configuration values:\n";
"alert.preset_description.enabled" = "enabled";
"alert.preset_description.disabled" = "disabled";
"alert.preset_description.empty" = "(empty)";
// PHP version unavailable
"alert.php_switch_unavailable.title" = "Unsupported PHP version";
"alert.php_switch_unavailable.subtitle" = "PHP Monitor can't switch to PHP %@, as it may not be installed or available. Applying this preset has been cancelled.";
"alert.php_switch_unavailable.info" = "Please make sure PHP %@ is installed and you can switch to it in the dropdown. Currently supported versions include PHP: %@.";
"alert.php_switch_unavailable.ok" = "OK";
// Revert
"alert.revert_description.title" = "Revert Configuration?";
"alert.revert_description.subtitle" = "PHP Monitor can revert to the previous configuration that was active. Here's what will be applied: \n\n%@";
"alert.revert_description.ok" = "Revert";
"alert.revert_description.cancel" = "Cancel";
// STARTUP
/// 0. Architecture mismatch
@ -353,16 +431,23 @@ You can do this by running `composer global update` in your terminal. After that
/// Brew & sudoers
"startup.errors.sudoers_brew.title" = "Brew has not been added to sudoers.d";
"startup.errors.sudoers_brew.subtitle" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
"startup.errors.sudoers_brew.desc" = "If you keep seeing this error, it is possible that there is a permission issue where PHP Monitor cannot validate the file, which can usually be resolved by running: `sudo chmod +r /private/etc/sudoers.d/brew`";
/// Valet & sudoers
"startup.errors.sudoers_valet.title" = "Valet has not been added to sudoers.d";
"startup.errors.sudoers_valet.subtitle" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue. If you did this before, please run `sudo valet trust` again.";
"startup.errors.sudoers_valet.desc" = "If you keep seeing this error, it is possible that there is a permission issue where PHP Monitor cannot validate the file, which can usually be resolved by running: `sudo chmod +r /private/etc/sudoers.d/valet`";
/// Cannot retrieve services
"startup.errors.services_json_error.title" = "Cannot determine services status";
"startup.errors.services_json_error.subtitle" = "PHP Monitor usually queries `brew` using the following command to test if the services can be retrieved: `sudo brew services info nginx --json`.\n\nPHP Monitor could not interpret this response.";
"startup.errors.services_json_error.desc" = "This can happen if your Homebrew installation is out of date, in which case Homebrew won't return JSON yet. You can usually fix this by running `brew update`. You can also try running `sudo brew services info nginx --json` in your terminal of choice.";
/// Issue with `which` alias
"startup.errors.which_alias_issue.title" = "A configuration issue was detected";
"startup.errors.which_alias_issue.subtitle" = "It appears that there's a file in `/usr/local/bin/which`. This is usually set up by NodeJS, but `node` isn't in the PATH in `/usr/local/bin`. To fix this, keep reading.";
"startup.errors.which_alias_issue.desc" = "You will need to symlink `node` into the `/usr/local/bin` directory to make sure PHP Monitor can start successfully. For more info, see: https://github.com/nicoverbruggen/phpmon/issues/174";
// SPONSOR ENCOURAGEMENT
"startup.sponsor_encouragement.title" = "If PHP Monitor has been useful to you or your company, please consider leaving a tip.";