🚀 Version 5.4
@ -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.
|
||||
|
@ -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 = "";
|
||||
|
@ -69,8 +69,8 @@
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "PHPMON_MARKETING_MODE"
|
||||
value = "YES"
|
||||
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
|
||||
value = ""
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
|
72
README.md
@ -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>
|
||||
|
||||
|
10
SECURITY.md
@ -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 you’re 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
After Width: | Height: | Size: 18 KiB |
BIN
docs/screenshot-dark.jpg
Normal file
After Width: | Height: | Size: 454 KiB |
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 469 KiB |
@ -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)
|
||||
}
|
||||
|
84
phpmon-tests/Parsers/PhpConfigurationTest.swift
Normal 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"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
34
phpmon-tests/Test Files/phpmon/phpmon-config.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
38
phpmon/Assets.xcassets/AppColor.colorset/Contents.json
Normal 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
|
||||
}
|
||||
}
|
38
phpmon/Assets.xcassets/AppSecondary.colorset/Contents.json
Normal 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
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 854 B |
Before Width: | Height: | Size: 1.3 KiB |
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 826 B |
Before Width: | Height: | Size: 1.2 KiB |
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 819 B |
Before Width: | Height: | Size: 1.2 KiB |
@ -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 {
|
||||
|
24
phpmon/Common/Extensions/ArrayExtension.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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?
|
||||
}
|
||||
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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) })
|
||||
}
|
||||
}
|
||||
|
@ -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?
|
||||
|
225
phpmon/Common/PHP/PhpConfigurationFile.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
15
phpmon/Common/Protocols/CreatedFromFile.swift
Normal 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?
|
||||
|
||||
}
|
@ -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?
|
||||
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
]}
|
||||
|
80
phpmon/Domain/App/ServicesManager.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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!
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -216,6 +216,8 @@ class Valet {
|
||||
sites.insert(site, at: 0)
|
||||
}
|
||||
|
||||
Log.info("\(sites.count) sites & \(proxies.count) proxies have been scanned.")
|
||||
|
||||
isBusy = false
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
295
phpmon/Domain/Menu/MainMenu+Actions.swift
Normal 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()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -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!)
|
||||
|
@ -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)")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
257
phpmon/Domain/Menu/StatusMenu+Items.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
@ -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?
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
271
phpmon/Domain/Presets/Preset.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
37
phpmon/Domain/Presets/PresetHelper.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
27
phpmon/Domain/SwiftUI/Common/SwiftUIHelper.swift
Normal 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
|
||||
}()
|
||||
}
|
195
phpmon/Domain/SwiftUI/Domains/VersionPopoverView.swift
Normal 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")
|
||||
}
|
||||
}
|
47
phpmon/Domain/SwiftUI/Menu/HeaderView.swift
Normal 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)
|
||||
}
|
||||
}
|
22
phpmon/Domain/SwiftUI/Menu/SectionHeaderView.swift
Normal 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)
|
||||
}
|
||||
}
|
172
phpmon/Domain/SwiftUI/Menu/ServicesView.swift
Normal 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)
|
||||
}
|
||||
}
|
66
phpmon/Domain/SwiftUI/Menu/StatsView.swift
Normal 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"
|
||||
)
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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!
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 Monitor’s automatic fixes that’ll 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.";
|
||||
|