diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 0e779d5..0c151ef 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -24,22 +24,49 @@ 54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */; }; C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */; }; C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; }; + C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; }; + C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; }; + C40B24F327A310780018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; }; + C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; }; + C40B24F527A3108B0018C7D2 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; }; + C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; }; + C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; }; + C40C7F202772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; }; + C40C7F2227721F8200DDDCDC /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; }; + C40C7F2327721F8200DDDCDC /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; }; + C40C7F2427721F8200DDDCDC /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; }; + C40C7F2527721F9800DDDCDC /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; }; + C40C7F2627721FA200DDDCDC /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; }; + C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; }; + C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; }; + C40C7F2B2772201C00DDDCDC /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; }; + C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; }; + C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; }; + C40C7F3227722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; }; C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; }; + C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; }; + C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; }; + C415D3B72770F294005EF286 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; }; + C415D3B82770F294005EF286 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; }; + C415D3E12770F34D005EF286 /* AllowedArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3DE2770F34D005EF286 /* AllowedArguments.swift */; }; + C415D3E62770F540005EF286 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3E52770F540005EF286 /* main.swift */; }; + C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */; }; + C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */; }; + C417DC74277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; }; + C417DC75277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; }; + C417DC76277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; }; C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; }; C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; }; C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; }; C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; }; C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; }; - C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; }; C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; }; C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; }; - C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; }; C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; }; C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; }; C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; }; C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; }; C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; }; - C42295DD2358D02000E263B2 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; }; C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; }; C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; }; C42759682627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; }; @@ -48,6 +75,10 @@ C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; }; C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; }; C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */; }; + C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* ProgressWindow.swift */; }; + C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* ProgressWindow.swift */; }; + C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C44C1990276E44CB0072762D /* ProgressWindow.storyboard */; }; + C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C44C1990276E44CB0072762D /* ProgressWindow.storyboard */; }; C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */; }; C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */; }; C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* SiteListVC.swift */; }; @@ -68,10 +99,16 @@ 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 */; }; + C48D6C72279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */; }; + C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */; }; + C493084A279F331F009C240B /* AddSiteVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4930849279F331F009C240B /* AddSiteVC.swift */; }; + C493084B279F331F009C240B /* AddSiteVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4930849279F331F009C240B /* AddSiteVC.swift */; }; C4998F0626175E7200B2526E /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = C4998F0526175E7200B2526E /* HotKey */; }; C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; }; C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; }; - C49EAB46259FC305007F6C3B /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; }; + C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49E171E27A5736E00787921 /* PMServicesView.swift */; }; C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; }; C4AF9F71275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; }; C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; }; @@ -82,28 +119,67 @@ C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; }; C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; }; C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */; }; + C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; }; + C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; }; + C4B585402770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; }; + C4B585412770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; }; + C4B585422770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; }; + C4B585432770FE3900DA4FBE /* 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 */; }; + C4B585462770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.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 */; }; C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */; }; C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; }; C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; }; + C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; + C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; }; + C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; + C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; + C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; }; + C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; }; C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; }; C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; }; C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; }; + C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; }; + C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; }; + C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; }; + C4D9ADC1277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; }; + C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; }; + C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; }; + C4D9ADCA277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; }; + C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; }; + C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; }; + C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; }; + C4EC1E6D279DF87A0010F296 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; }; + C4EC1E6E279DF87A0010F296 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.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 */; }; C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; }; C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; }; C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; }; - C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; }; + C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; }; + C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; }; + C4F30B05278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; }; + C4F30B07278E195800755FCE /* brew-services.json in Resources */ = {isa = PBXBuildFile; fileRef = C4F30B06278E195800755FCE /* brew-services.json */; }; + C4F30B08278E195800755FCE /* brew-services.json in Resources */ = {isa = PBXBuildFile; fileRef = C4F30B06278E195800755FCE /* brew-services.json */; }; + C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; }; + C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; }; + C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; }; - C4F7809F25D8037C000DBC97 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; }; - C4F780A225D804AA000DBC97 /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; }; C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; }; C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */; }; C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; }; - C4F780B425D80B51000DBC97 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; }; C4F780B725D80B5D000DBC97 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; }; C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; }; C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; }; @@ -112,7 +188,6 @@ 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 */; }; - C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.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 */; }; @@ -134,6 +209,18 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + C415D3D42770F341005EF286 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = ""; }; 5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; @@ -145,7 +232,17 @@ 54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotkeyPreferenceView.swift; sourceTree = ""; }; C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = ""; }; C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = ""; }; + C40C7F1D2772136000DDDCDC /* PhpEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpEnv.swift; sourceTree = ""; }; + C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActivePhpInstallation+Checks.swift"; sourceTree = ""; }; + C40C7F2F27722E8D00DDDCDC /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = ""; }; + C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFrameworks.swift; sourceTree = ""; }; + C415D3B62770F294005EF286 /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = ""; }; + C415D3D62770F341005EF286 /* phpmon-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "phpmon-cli"; sourceTree = BUILT_PRODUCTS_DIR; }; + C415D3DE2770F34D005EF286 /* AllowedArguments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllowedArguments.swift; sourceTree = ""; }; + C415D3E52770F540005EF286 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+InterApp.swift"; sourceTree = ""; }; + C417DC73277614690015E6EE /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; C4188988275FE8CB001EF227 /* Filesystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filesystem.swift; sourceTree = ""; }; C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; }; C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -153,20 +250,19 @@ C41C1B3D22B0098000E7CF16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; C41C1B3F22B0098000E7CF16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = ""; }; - C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = ""; }; C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = ""; }; C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = ""; }; - C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = ""; }; C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+Actions.swift"; sourceTree = ""; }; C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = ""; }; C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.swift"; sourceTree = ""; }; - C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = ""; }; C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = ""; }; C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.swift"; sourceTree = ""; }; C43A8A1925D9CD1000591B77 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = ""; }; C43A8A1F25D9D1D700591B77 /* brew.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = brew.json; sourceTree = ""; }; C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewJsonParserTest.swift; sourceTree = ""; }; + C44C198C276E3A1C0072762D /* ProgressWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressWindow.swift; sourceTree = ""; }; + C44C1990276E44CB0072762D /* ProgressWindow.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ProgressWindow.storyboard; sourceTree = ""; }; C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListWC.swift; sourceTree = ""; }; C464ADAE275A7A69003FCD53 /* SiteListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListVC.swift; sourceTree = ""; }; C464ADB1275A87CA003FCD53 /* SiteListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListCell.swift; sourceTree = ""; }; @@ -182,8 +278,11 @@ C48D0C9525CC80B100CC7490 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; C48D0C9925CC888B00CC7490 /* HeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HeaderView.xib; sourceTree = ""; }; C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = ""; }; + C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpVersionNumber.swift; sourceTree = ""; }; + C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionNumberTest.swift; sourceTree = ""; }; + C4930849279F331F009C240B /* AddSiteVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteVC.swift; sourceTree = ""; }; C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = ""; }; - C49EAB45259FC305007F6C3B /* Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = ""; }; + C49E171E27A5736E00787921 /* PMServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMServicesView.swift; sourceTree = ""; }; C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = ""; }; C4AF9F70275445FF00D44ED0 /* valet-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "valet-config.json"; sourceTree = ""; }; C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigParserTest.swift; sourceTree = ""; }; @@ -191,16 +290,37 @@ C4AF9F7C275454A900D44ED0 /* ValetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetTest.swift; sourceTree = ""; }; C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.swift; sourceTree = ""; }; C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionExtractorTest.swift; sourceTree = ""; }; + C4B5853B2770FE3900DA4FBE /* Paths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = ""; }; + C4B5853C2770FE3900DA4FBE /* Shell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = ""; }; + C4B5853D2770FE3900DA4FBE /* Command.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MenuOutlets.swift"; sourceTree = ""; }; C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+ActivationPolicy.swift"; sourceTree = ""; }; C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+GlobalHotkey.swift"; sourceTree = ""; }; + C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Startup.swift"; sourceTree = ""; }; + C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = ""; }; + C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = ""; }; + C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpConfigWatcher.swift; sourceTree = ""; }; C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = ""; }; C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = ""; }; + C4D89BC52783C99400A02B68 /* ComposerJson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerJson.swift; sourceTree = ""; }; + C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpSwitcher.swift; sourceTree = ""; }; + C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalSwitcher.swift; sourceTree = ""; }; + C4DEB7D327A5D60B00834718 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = ""; }; C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = ""; }; C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = ""; }; + C4EC1E65279DE0380010F296 /* ServicesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ServicesView.xib; sourceTree = ""; }; + C4EC1E67279DE0540010F296 /* ServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServicesView.swift; sourceTree = ""; }; + C4EC1E6C279DF87A0010F296 /* Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Async.swift; sourceTree = ""; }; + C4EC1E72279DFCF40010F296 /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = ""; }; C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + C4EE55A627708B9E001DF387 /* PMHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMHeaderView.swift; sourceTree = ""; }; + C4EE55A727708B9E001DF387 /* Preview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preview.swift; sourceTree = ""; }; + C4EE55A827708B9E001DF387 /* PMStatsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMStatsView.swift; sourceTree = ""; }; + C4EED88827A48778006D7272 /* InterAppHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterAppHandler.swift; sourceTree = ""; }; C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = ""; }; C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = ""; }; + C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = ""; }; + C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = ""; }; C4F7807425D7F7E5000DBC97 /* RELEASE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = RELEASE.md; sourceTree = ""; }; 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 = ""; }; @@ -213,6 +333,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + C415D3D32770F341005EF286 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C41C1B3022B0097F00E7CF16 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -237,6 +364,8 @@ C4998F092617633900B2526E /* PrefsWC.swift */, 5420395826135DC100FB00FA /* PrefsVC.swift */, 5420395E2613607600FB00FA /* Preferences.swift */, + C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */, + C4DEB7D327A5D60B00834718 /* Stats.swift */, C41CD0272628D8E20065BBED /* Keybinds */, 54FCFD28276C88C0004CE748 /* Views */, ); @@ -246,6 +375,9 @@ 54B20EDF263AA22C00D3250E /* PHP */ = { isa = PBXGroup; children = ( + C48D6C6E279CD29C00F26D7E /* PHP Version */, + C4D9ADC2277610E4007277F4 /* Switcher */, + C4F30B01278E169B00755FCE /* Homebrew */, C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */, C4F2E4392752F7D00020E974 /* PhpInstallation.swift */, C4ACA38E25C754C100060C66 /* PhpExtension.swift */, @@ -273,6 +405,41 @@ path = IAP; sourceTree = ""; }; + C40C7F1C27720E1400DDDCDC /* Test Files */ = { + isa = PBXGroup; + children = ( + C4AF9F70275445FF00D44ED0 /* valet-config.json */, + C43A8A1F25D9D1D700591B77 /* brew.json */, + C4F30B06278E195800755FCE /* brew-services.json */, + C4F780A725D80AE8000DBC97 /* php.ini */, + ); + path = "Test Files"; + sourceTree = ""; + }; + C40C7F2127721F7300DDDCDC /* Core */ = { + isa = PBXGroup; + children = ( + C415D3B62770F294005EF286 /* Actions.swift */, + C4EE188322D3386B00E126E5 /* Constants.swift */, + C4EC1E72279DFCF40010F296 /* Events.swift */, + C4B5853D2770FE3900DA4FBE /* Command.swift */, + C4B5853B2770FE3900DA4FBE /* Paths.swift */, + C4B5853C2770FE3900DA4FBE /* Shell.swift */, + C40C7F2F27722E8D00DDDCDC /* Logger.swift */, + C417DC73277614690015E6EE /* Helpers.swift */, + ); + path = Core; + sourceTree = ""; + }; + C415D3D72770F341005EF286 /* phpmon-cli */ = { + isa = PBXGroup; + children = ( + C415D3E52770F540005EF286 /* main.swift */, + C415D3DE2770F34D005EF286 /* AllowedArguments.swift */, + ); + path = "phpmon-cli"; + sourceTree = ""; + }; C41C1B2A22B0097F00E7CF16 = { isa = PBXGroup; children = ( @@ -282,7 +449,10 @@ C4E713572570151400007428 /* docs */, C41C1B3522B0097F00E7CF16 /* phpmon */, C4F7807A25D7F84B000DBC97 /* phpmon-tests */, + C415D3D72770F341005EF286 /* phpmon-cli */, + C4B5853A2770FE2500DA4FBE /* phpmon-common */, C41C1B3422B0097F00E7CF16 /* Products */, + C4D309E72770EF2F00958BCF /* Frameworks */, ); sourceTree = ""; }; @@ -291,6 +461,7 @@ children = ( C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */, C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */, + C415D3D62770F341005EF286 /* phpmon-cli */, ); name = Products; sourceTree = ""; @@ -298,7 +469,6 @@ C41C1B3522B0097F00E7CF16 /* phpmon */ = { isa = PBXGroup; children = ( - C4EE188322D3386B00E126E5 /* Constants.swift */, C41E181722CB61EB0072CF09 /* Domain */, C41C1B3F22B0098000E7CF16 /* Info.plist */, C4232EE42612526500158FC6 /* Credits.html */, @@ -323,17 +493,28 @@ children = ( C4AF9F6B275445D300D44ED0 /* Integrations */, C4B13B1D25C4915000548C3A /* Core */, - 54B20EDF263AA22C00D3250E /* PHP */, - C4F7808A25D7F918000DBC97 /* Terminal */, + C4D9ADBD27761084007277F4 /* PHP */, C47331A0247093AC009A0597 /* Menu */, C464ADAA275A7A25003FCD53 /* SiteList */, 5420395726135DB800FB00FA /* Preferences */, + C44C198F276E3A380072762D /* Progress */, + C4C8E81D276F5686003AC782 /* Watcher */, C4811D2822D70D9C00B5F6B3 /* Helpers */, C4F8C0A222D4F100002EFE61 /* Extensions */, + C4EE55B027708BB2001DF387 /* SwiftUI */, ); path = Domain; sourceTree = ""; }; + C44C198F276E3A380072762D /* Progress */ = { + isa = PBXGroup; + children = ( + C44C198C276E3A1C0072762D /* ProgressWindow.swift */, + C44C1990276E44CB0072762D /* ProgressWindow.storyboard */, + ); + path = Progress; + sourceTree = ""; + }; C464ADAA275A7A25003FCD53 /* SiteList */ = { isa = PBXGroup; children = ( @@ -341,6 +522,7 @@ C464ADAE275A7A69003FCD53 /* SiteListVC.swift */, C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */, C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */, + C4930849279F331F009C240B /* AddSiteVC.swift */, C464ADB1275A87CA003FCD53 /* SiteListCell.swift */, ); path = SiteList; @@ -350,11 +532,14 @@ isa = PBXGroup; children = ( C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */, + C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */, C47331A1247093B7009A0597 /* StatusMenu.swift */, C48D0C9525CC80B100CC7490 /* HeaderView.swift */, C48D0C9925CC888B00CC7490 /* HeaderView.xib */, C48D0CA225CC992000CC7490 /* StatsView.swift */, C48D0C8F25CC7FD000CC7490 /* StatsView.xib */, + C4EC1E67279DE0540010F296 /* ServicesView.swift */, + C4EC1E65279DE0380010F296 /* ServicesView.xib */, ); path = Menu; sourceTree = ""; @@ -370,10 +555,20 @@ C4CCBA6B275C567B008C7055 /* PMWindowController.swift */, 54AB03252763858F00A29D5F /* Timer.swift */, C4B5635D276AB09000F12CCB /* VersionExtractor.swift */, + C4EC1E6C279DF87A0010F296 /* Async.swift */, ); path = Helpers; sourceTree = ""; }; + C48D6C6E279CD29C00F26D7E /* PHP Version */ = { + isa = PBXGroup; + children = ( + C40C7F1D2772136000DDDCDC /* PhpEnv.swift */, + C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */, + ); + path = "PHP Version"; + sourceTree = ""; + }; C4AF9F6A275445C900D44ED0 /* Valet */ = { isa = PBXGroup; children = ( @@ -385,6 +580,7 @@ C4AF9F6B275445D300D44ED0 /* Integrations */ = { isa = PBXGroup; children = ( + C4D89BC42783C98800A02B68 /* Composer */, C4AF9F6C275445D900D44ED0 /* Homebrew */, C4AF9F6A275445C900D44ED0 /* Valet */, ); @@ -395,7 +591,6 @@ isa = PBXGroup; children = ( C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */, - C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */, ); path = Homebrew; sourceTree = ""; @@ -407,26 +602,97 @@ C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */, C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */, C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */, + C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */, C4811D2322D70A4700B5F6B3 /* App.swift */, C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */, C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */, C4D8016522B1584700C6DA1B /* Startup.swift */, - C41C1B4C22B0215A00E7CF16 /* Actions.swift */, + C4EED88827A48778006D7272 /* InterAppHandler.swift */, ); path = Core; sourceTree = ""; }; + C4B5853A2770FE2500DA4FBE /* phpmon-common */ = { + isa = PBXGroup; + children = ( + C40C7F2127721F7300DDDCDC /* Core */, + 54B20EDF263AA22C00D3250E /* PHP */, + ); + path = "phpmon-common"; + sourceTree = ""; + }; + C4C8E81D276F5686003AC782 /* Watcher */ = { + isa = PBXGroup; + children = ( + C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */, + C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */, + ); + path = Watcher; + sourceTree = ""; + }; + C4D309E72770EF2F00958BCF /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + C4D89BC42783C98800A02B68 /* Composer */ = { + isa = PBXGroup; + children = ( + C4D89BC52783C99400A02B68 /* ComposerJson.swift */, + C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */, + ); + path = Composer; + sourceTree = ""; + }; + C4D9ADBD27761084007277F4 /* PHP */ = { + isa = PBXGroup; + children = ( + C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */, + ); + path = PHP; + sourceTree = ""; + }; + C4D9ADC2277610E4007277F4 /* Switcher */ = { + isa = PBXGroup; + children = ( + C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */, + C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */, + ); + path = Switcher; + sourceTree = ""; + }; + C4EE55B027708BB2001DF387 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + C49E171E27A5736E00787921 /* PMServicesView.swift */, + C4EE55A627708B9E001DF387 /* PMHeaderView.swift */, + C4EE55A827708B9E001DF387 /* PMStatsView.swift */, + C4EE55A727708B9E001DF387 /* Preview.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + C4F30B01278E169B00755FCE /* Homebrew */ = { + isa = PBXGroup; + children = ( + C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */, + C4F30B02278E16BA00755FCE /* HomebrewService.swift */, + ); + path = Homebrew; + sourceTree = ""; + }; C4F7807A25D7F84B000DBC97 /* phpmon-tests */ = { isa = PBXGroup; children = ( - C4AF9F70275445FF00D44ED0 /* valet-config.json */, - C43A8A1F25D9D1D700591B77 /* brew.json */, - C4F780A725D80AE8000DBC97 /* php.ini */, C4F7807D25D7F84B000DBC97 /* Info.plist */, + C40C7F1C27720E1400DDDCDC /* Test Files */, C4F7809B25D80344000DBC97 /* CommandTest.swift */, C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */, C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */, C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */, + C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */, C43A8A1925D9CD1000591B77 /* Utility.swift */, C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */, C4AF9F7C275454A900D44ED0 /* ValetTest.swift */, @@ -435,16 +701,6 @@ path = "phpmon-tests"; sourceTree = ""; }; - C4F7808A25D7F918000DBC97 /* Terminal */ = { - isa = PBXGroup; - children = ( - C49EAB45259FC305007F6C3B /* Paths.swift */, - C42295DC2358D02000E263B2 /* Command.swift */, - C41C1B4622B009A400E7CF16 /* Shell.swift */, - ); - path = Terminal; - sourceTree = ""; - }; C4F8C0A222D4F100002EFE61 /* Extensions */ = { isa = PBXGroup; children = ( @@ -459,6 +715,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + C415D3D52770F341005EF286 /* phpmon-cli */ = { + isa = PBXNativeTarget; + buildConfigurationList = C415D3DA2770F341005EF286 /* Build configuration list for PBXNativeTarget "phpmon-cli" */; + buildPhases = ( + C415D3D22770F341005EF286 /* Sources */, + C415D3D32770F341005EF286 /* Frameworks */, + C415D3D42770F341005EF286 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "phpmon-cli"; + productName = "phpmon-cli"; + productReference = C415D3D62770F341005EF286 /* phpmon-cli */; + productType = "com.apple.product-type.tool"; + }; C41C1B3222B0097F00E7CF16 /* PHP Monitor */ = { isa = PBXNativeTarget; buildConfigurationList = C41C1B4322B0098000E7CF16 /* Build configuration list for PBXNativeTarget "PHP Monitor" */; @@ -503,10 +776,13 @@ C41C1B2B22B0097F00E7CF16 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1240; - LastUpgradeCheck = 1220; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; ORGANIZATIONNAME = "Nico Verbruggen"; TargetAttributes = { + C415D3D52770F341005EF286 = { + CreatedOnToolsVersion = 13.2.1; + }; C41C1B3222B0097F00E7CF16 = { CreatedOnToolsVersion = 10.2.1; }; @@ -534,6 +810,7 @@ targets = ( C41C1B3222B0097F00E7CF16 /* PHP Monitor */, C4F7807825D7F84B000DBC97 /* phpmon-tests */, + C415D3D52770F341005EF286 /* phpmon-cli */, ); }; /* End PBXProject section */ @@ -548,9 +825,12 @@ C4AF9F71275445FF00D44ED0 /* valet-config.json in Resources */, C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */, C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */, + C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */, C4232EE52612526500158FC6 /* Credits.html in Resources */, 54FCFD26276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */, C473319F2470923A009A0597 /* Localizable.strings in Resources */, + C4F30B07278E195800755FCE /* brew-services.json in Resources */, + C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */, 54FCFD2D276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */, C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */, C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */, @@ -566,61 +846,116 @@ C4F780A825D80AE8000DBC97 /* php.ini in Resources */, C43A8A2025D9D1D700591B77 /* brew.json in Resources */, C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */, + C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */, + C4F30B08278E195800755FCE /* brew-services.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + C415D3D22770F341005EF286 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C40C7F2427721F8200DDDCDC /* PhpExtension.swift in Sources */, + C40C7F2627721FA200DDDCDC /* Constants.swift in Sources */, + C4B585402770FE3900DA4FBE /* Paths.swift in Sources */, + C415D3E62770F540005EF286 /* main.swift in Sources */, + C40B24F327A310780018C7D2 /* Events.swift in Sources */, + C40C7F2227721F8200DDDCDC /* PhpInstallation.swift in Sources */, + C4B585432770FE3900DA4FBE /* Shell.swift in Sources */, + C4D9ADC1277610E1007277F4 /* PhpSwitcher.swift in Sources */, + C4F30B05278E16BA00755FCE /* HomebrewService.swift in Sources */, + C4EC1E6E279DF87A0010F296 /* Async.swift in Sources */, + C40C7F2327721F8200DDDCDC /* ActivePhpInstallation.swift in Sources */, + C4B585462770FE3900DA4FBE /* Command.swift in Sources */, + C4D9ADCA277611A0007277F4 /* InternalSwitcher.swift in Sources */, + C48D6C72279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, + C40C7F2527721F9800DDDCDC /* HomebrewPackage.swift in Sources */, + C417DC76277614690015E6EE /* Helpers.swift in Sources */, + C40C7F3227722E8D00DDDCDC /* Logger.swift in Sources */, + C40C7F2B2772201C00DDDCDC /* Actions.swift in Sources */, + C415D3E12770F34D005EF286 /* AllowedArguments.swift in Sources */, + C40C7F202772136000DDDCDC /* PhpEnv.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C41C1B2F22B0097F00E7CF16 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */, C4D8016622B1584700C6DA1B /* Startup.swift in Sources */, + C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, + C4B585412770FE3900DA4FBE /* Shell.swift in Sources */, C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */, C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */, C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */, 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */, C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, + C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */, + C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */, + C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, 54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, - C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */, + C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */, C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */, - C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */, C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */, C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */, + C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */, + C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */, C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */, C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */, + C4B585442770FE3900DA4FBE /* Command.swift in Sources */, C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */, - C42295DD2358D02000E263B2 /* Command.swift in Sources */, + C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */, + C415D3B72770F294005EF286 /* Actions.swift in Sources */, + C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */, + C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */, 54B48B5F275F66AE006D90C5 /* Application.swift in Sources */, C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */, + C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */, C4811D2422D70A4700B5F6B3 /* App.swift in Sources */, C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */, + C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */, 5420395F2613607600FB00FA /* Preferences.swift in Sources */, C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */, 54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */, C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */, + C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */, C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */, C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */, + C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */, 54AB03262763858F00A29D5F /* Timer.swift in Sources */, + C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */, + C417DC74277614690015E6EE /* Helpers.swift in Sources */, + C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */, C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */, C42759672627662800093CAE /* NSMenuExtension.swift in Sources */, C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */, + C4EC1E6D279DF87A0010F296 /* Async.swift in Sources */, + C4EC1E73279DFCF40010F296 /* Events.swift in Sources */, + C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */, C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */, - C49EAB46259FC305007F6C3B /* Paths.swift in Sources */, C4188989275FE8CB001EF227 /* Filesystem.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 */, + C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */, C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */, C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */, + C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */, + C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */, C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */, C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */, C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */, C464ADB2275A87CA003FCD53 /* SiteListCell.swift in Sources */, C4EE188422D3386B00E126E5 /* Constants.swift in Sources */, + C493084A279F331F009C240B /* AddSiteVC.swift in Sources */, + C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -629,58 +964,82 @@ buildActionMask = 2147483647; files = ( 54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */, + C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */, C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */, C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */, 54AB03272763858F00A29D5F /* Timer.swift in Sources */, 54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */, + C415D3B82770F294005EF286 /* Actions.swift in Sources */, 54B48B60275F66AE006D90C5 /* Application.swift in Sources */, C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */, + C493084B279F331F009C240B /* AddSiteVC.swift in Sources */, + C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */, C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */, C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */, C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */, + C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */, C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */, C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */, C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */, + C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */, + C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */, C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, + C40B24F527A3108B0018C7D2 /* Async.swift in Sources */, C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */, C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */, C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */, - C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */, + C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, + C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */, + C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */, C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C42759682627662800093CAE /* NSMenuExtension.swift in Sources */, C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */, C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */, C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */, C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */, + C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */, + C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */, C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */, + C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */, C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */, + C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */, + C417DC75277614690015E6EE /* Helpers.swift in Sources */, C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */, C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */, C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */, C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */, 54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */, - C4F780A225D804AA000DBC97 /* Paths.swift in Sources */, C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */, C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */, C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */, - C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */, + C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */, + C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */, + C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */, + C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */, + C40B24F227A310770018C7D2 /* Events.swift in Sources */, + C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */, C4AF9F7D275454A900D44ED0 /* ValetTest.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 */, + C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */, + C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */, C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */, + C4B585422770FE3900DA4FBE /* Shell.swift in Sources */, C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */, + C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */, C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */, C464ADB0275A7A6A003FCD53 /* SiteListVC.swift in Sources */, C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */, C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */, C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */, - C4F7809F25D8037C000DBC97 /* Command.swift in Sources */, - C4F780B425D80B51000DBC97 /* Actions.swift in Sources */, + C4EE55AA27708B9E001DF387 /* PMHeaderView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -706,6 +1065,34 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + C415D3DB2770F341005EF286 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + C415D3DC2770F341005EF286 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; C41C1B4122B0098000E7CF16 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -831,7 +1218,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 140; + CURRENT_PROJECT_VERSION = 560; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = phpmon/Info.plist; @@ -840,7 +1227,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.2.0; + MARKETING_VERSION = 5.0; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -856,7 +1243,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 140; + CURRENT_PROJECT_VERSION = 560; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = phpmon/Info.plist; @@ -865,7 +1252,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.2.0; + MARKETING_VERSION = 5.0; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -918,6 +1305,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + C415D3DA2770F341005EF286 /* Build configuration list for PBXNativeTarget "phpmon-cli" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C415D3DB2770F341005EF286 /* Debug */, + C415D3DC2770F341005EF286 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C41C1B2E22B0097F00E7CF16 /* Build configuration list for PBXProject "PHP Monitor" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor CLI.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor CLI.xcscheme new file mode 100644 index 0000000..41be842 --- /dev/null +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor CLI.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme index a78bcde..cb237ab 100644 --- a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme @@ -1,6 +1,6 @@ If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project. > You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy.
**Thank you!** ⭐️ -phpmon icon +

PHP Monitor (phpmon)

-**PHP Monitor** (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so you need to have it set up before you can use this. +

+ phpmon icon +

-phpmon screenshot (menu bar app) +**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so you need to have it set up before you can use this. -Screenshot: A menu showing all of the functionality of PHP Monitor. +phpmon screenshot (menu bar app) + +Screenshot: Showing the key functionality of PHP Monitor. You can also add new domains as links, manage various services, and perform First Aid to fix all kinds of common PHP link issues. It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)! @@ -19,7 +21,7 @@ PHP Monitor also gives you quick access to various useful functionality (like ac ## 🖥 System requirements -PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs. +PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs. * macOS 11 Big Sur or higher (supports macOS 12 Monterey) * Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew` @@ -30,7 +32,7 @@ _You may need to update your Valet installation to keep everything working if a ## 🚀 How to install -You can install via Homebrew (recommended), or may download the latest release on GitHub. +Again, make sure you have **Laravel Valet** installed first. Once that's done, you can install via Homebrew (recommended), or may download the latest release on GitHub. To install via Homebrew, run: @@ -41,7 +43,11 @@ To upgrade your existing installation, run: brew upgrade phpmon -_The app is signed and notarized, meaning all you have to do is approve its first launch._ +(You may need to run `brew update` first in order to update the cask file if you ran a Homebrew operation recently.) + +## 🔑 Is the app signed & notarized? + +Yes, the app is signed and notarized, meaning all you have to do is approve its first launch (or whenever it updates). ## 👨‍💻 Why build this? @@ -51,10 +57,12 @@ Initially, I had an Alfred workflow for this — but it has now been replaced wi ## 🤬 The app won't start?! -PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios. +PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios. **Follow instructions as specified in the alert in order to resolve any issues.** +(If the app crashes at launch without showing you any of these messages, you might have a non-standard Homebrew and Valet setup. Those are not supported.) + ## 🙋‍♂️ FAQ & Troubleshooting > If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. To upgrade Valet, run `composer global update`. Don't forget to run `valet install` after upgrading. @@ -246,6 +254,67 @@ The supported apps are: PhpStorm, Visual Studio Code, Sublime Text, Sublime M All of these apps should just be detected correctly, no matter their location on your system. If you can open it using `open -a "appname"`, the app should be detected and work. If you have renamed the app, there might be an issue getting it detected. To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift). + +You can add your own apps by creating and editing a `~/.phpmon.conf.json` file, with the following entry: + +
+{
+    "scan_apps": ["Xcode", "Kraken"]
+}
+
+ +You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary. + + +
+How can the app integrate with third party tools, like Alfred? + +There's an Alfred workflow usually included in the release list, you can grab it by going to releases and downloading the asset `phpmon.alfredworkflow`. + +If you'd like to integrate something yourself, all you need to to is use the `phpmon://` protocol and ensure that third party app integrations are enabled in Preferences (in PHP Monitor). + +Using app callbacks, macOS and PHP Monitor allow for the following to be called: + +* phpmon://list +* phpmon://services/stop +* phpmon://services/restart/all +* phpmon://services/restart/nginx +* phpmon://services/restart/php +* phpmon://services/restart/dnsmasq +* phpmon://locate/config +* phpmon://locate/composer +* phpmon://locate/valet +* phpmon://phpinfo +* phpmon://switch/php/{version} + +
+ +
+How does the app know what PHP version is required for my app? + +The `composer.json` file in the root of the folder (if it exists) is scanned and interpreted. + +If the version is set in `platform`, it takes precendence. +If the version is not set in `platform` but it is in `require` (most common) then that version is used. +
+ +
+What do the checkmarks next to the PHP version mean in the site list? + +You'll see a checkmark next to the version number if the currently enabled PHP version is compatible with the version required to run the site. + +This is determined by evaluating the PHP requirement constraint (e.g. `^8.0`, `~8.0` or a specific version: `8.0`). +
+ +
+Why can't I see the driver type any more? It says "Project Type" now. + +PHP Monitor currently checks your `composer.json` file to try to figure out what project you are running. + +This approach is a lot faster than asking for a driver when you have many sites linked, but is slightly less reliable since the framework or type of project inferred via `composer.json` might not be 100% accurate. + +You can always still ask Valet using the command line, should it be necessary. In my experience fetching the drivers slowed down the app unnecessarily. +
@@ -253,6 +322,8 @@ To see which files are checked to determine availability, see [this file](./phpm This is a security feature of Homebrew. When you start a service as an administrator, the root user becomes the owner of relevant binaries. You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder). +If you would like to know more, consult [this issue](https://github.com/nicoverbruggen/phpmon/issues/85) for more information. +
@@ -260,6 +331,10 @@ This is a security feature of Homebrew. When you start a service as an administr Please get in touch and open an issue. PHP Monitor shouldn't crash... (unless you are actually removing PHP *while* the app is running, that’s considered normal behaviour!) +If you would like to report a crash, please include the associated **log files** so I can find out what exactly went wrong. + +To find the logs, take a look in `~/Library/Logs/DiagnosticReports` (in Finder) and see if there's any (log) files that start with "PHP Monitor". +
## 📝 Having another issue? @@ -279,11 +354,10 @@ Donations really help with the Apple Developer Program cost, and keep me motivat While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks: * My colleagues at [DIVE](https://dive.be) -* The [Homebrew](https://brew.sh/) team who maintain -* The [developers & maintainers of Valet](https://github.com/laravel/valet/graphs/contributors) +* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors) +* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so * Everyone in the Laravel community who shared the app (thanks!) -* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) -* Everyone who left feedback via issues +* Everyone who left feedback via issues & who donated to keep the project up and running Thank you very much for your contributions, kind words and support. @@ -301,11 +375,29 @@ This utility will detect which PHP versions you have installed via Homebrew, and The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be much faster than Valet’s switcher. +### Config change detection + +PHP Monitor watches your filesystem in the relevant `conf.d` directory for the currently linked PHP version. + +Whenever an .ini file is modified, PHP Monitor will attempt to reload the current information about the active PHP installation. + +If an extension or other process writes to a single file a bunch of times in a short span of time (< 1 sec), PHP Monitor will only reload the active configuration information after a while (with a slight delay). + +### Site detection + +1. **Location of your sites**: PHP Monitor uses the Valet configuration file to determine which folders to look into. Each folder is scanned and then PHP Monitor will validate if a composer.json file exists to determine the desired PHP version. +1. **Sites secured or not secured**: Whether the directory has been secured is determined by checking if a matching certificate exists under Valet's `Certificates` directory for that site name. +1. **Project type**: PHP Monitor checks your `composer.json` file for "notable dependencies". If you have `laravel/framework` in your `require`, there's a good chance the project type is `Laravel`, after all. + +*Note*: If you have linked a folder in Documents, Desktop or Downloads you might need to grant PHP Monitor access to those directories for PHP Monitor to work correctly. + ### Want to know more? -If you want to know more about how this works, I recommend you check out the source code. +If you want to know more about how this works, I recommend you check out the source code. -This app isn't very complicated after all. In the end, this just (conveniently) executes some shell commands. +I have done my best to annotate as much as humanly possible, and have avoided using an overly complicated architecture to keep the code as easy to maintain as possible. The code isn't perfect by a long shot (lots of cleanup can still happen!) but the application works well. + +I also have a few tests for key parts of the application that I found needed to be tested. In the future, I would like to add even more tests for some of the UI stuff, but for now the tests are more unit tests than feature tests. ## 🔧 Build instructions diff --git a/SECURITY.md b/SECURITY.md index c0a3e75..67dfa51 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,16 +4,17 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up): -| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version | +| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version | | ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- -| 4.1 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 | +| 5.0 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 | ## Legacy versions These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions. -| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version | +| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version | | ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- +| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 | | 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 | | 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 | | 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 | diff --git a/docs/screenshot41.jpg b/docs/screenshot41.jpg deleted file mode 100644 index 0912a1f..0000000 Binary files a/docs/screenshot41.jpg and /dev/null differ diff --git a/docs/screenshot50.jpg b/docs/screenshot50.jpg new file mode 100644 index 0000000..0ecb439 Binary files /dev/null and b/docs/screenshot50.jpg differ diff --git a/integrations/phpmon.alfredworkflow b/integrations/phpmon.alfredworkflow new file mode 100644 index 0000000..6c7a248 Binary files /dev/null and b/integrations/phpmon.alfredworkflow differ diff --git a/phpmon-cli/AllowedArguments.swift b/phpmon-cli/AllowedArguments.swift new file mode 100644 index 0000000..e12a279 --- /dev/null +++ b/phpmon-cli/AllowedArguments.swift @@ -0,0 +1,26 @@ +// +// AllowedArguments.swift +// phpmon-cli +// +// Created by Nico Verbruggen on 20/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +enum AllowedArguments: String, CaseIterable { + case use = "use" + case performSwitch = "switch" + case fix = "fix" + case help = "help" + + static func has(_ string: String) -> Bool { + return Self.allCases.contains { arg in + return arg.rawValue == string + } + } + + static var rawValues: [String] { + return Self.allCases.map { $0.rawValue } + } +} diff --git a/phpmon-cli/main.swift b/phpmon-cli/main.swift new file mode 100644 index 0000000..8b986a2 --- /dev/null +++ b/phpmon-cli/main.swift @@ -0,0 +1,103 @@ +// +// main.swift +// phpmon-cli +// +// Created by Nico Verbruggen on 20/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +let toolver = "0.1 (early access)" + +let log = Log.shared +log.verbosity = .info + +if CommandLine.arguments.contains("-q") || CommandLine.arguments.contains("--quiet") { + Log.shared.verbosity = .warning +} +if CommandLine.arguments.contains("-p") || CommandLine.arguments.contains("--performance") { + Log.shared.verbosity = .performance +} + +var argument = "help" +if CommandLine.arguments.count > 1 { + argument = CommandLine.arguments[1] +} + +if !AllowedArguments.has(argument) { + Log.err("The supported arguments are: \(AllowedArguments.rawValues)") + exit(1) +} + +let action = AllowedArguments.init(rawValue: argument) + +switch action { +case .use, .performSwitch: + if !Shell.fileExists("\(Paths.binPath)/php") { + Log.err("PHP is currently not linked. Attempting quick fix...") + _ = Shell.user.executeSynchronously("brew link php", requiresPath: true) + } + + let phpenv = PhpEnv.shared + PhpEnv.detectPhpVersions() + + if CommandLine.arguments.count < 3 { + Log.err("You must enter at least two additional arguments when using this command.") + exit(1) + } + + let version = CommandLine.arguments[2].replacingOccurrences(of: "php@", with: "") + if phpenv.availablePhpVersions.contains(version) { + Log.info("Switching to PHP \(version)...") + Actions.switchToPhpVersion( + version: version, + availableVersions: phpenv.availablePhpVersions, + completed: { + Log.info("The switch has been completed.") + exit(0) + } + ) + } else { + Log.err("A PHP installation with version \(version) is not installed.") + Log.err("The installed versions are: \(phpenv.availablePhpVersions.joined(separator: ", ")).") + Log.err("If this version is available, you may be able to install it by using `brew install php@\(version)`.") + exit(1) + } +case .fix: + Log.info("Fixing your PHP installation...") + Actions.fixMyPhp() + Log.info("All operations completed. You can check which version of PHP is linked by using `php -v`.") + exit(0) +case .help: + print(""" + =============================================================== + PHP MONITOR CLI \(toolver) + by Nico Verbruggen + =============================================================== + + Gives access to the quick version switcher from PHP Monitor, + but without the GUI and 100% of the speed! + + SUPPORTED COMMANDS + + * use {version}: Switch to a specific version of PHP. + (e.g. `phpmon-cli use 8.0`) + * switch {version}: Alias for the `use` command. + * fix Attempts to unlink all PHP versions, + and link the latest version of PHP. + * help: Show this help. + + SUPPORTED FLAGS + + * `-q / --quiet`: Silences all logs except for warnings and exceptions. + * `-p / --perf`: Enables performance mode. + + """) + exit(0) +case .none: + Log.err("Action not recognized!") + exit(1) +} + +RunLoop.main.run() diff --git a/phpmon-common/Core/Actions.swift b/phpmon-common/Core/Actions.swift new file mode 100644 index 0000000..fc41ec7 --- /dev/null +++ b/phpmon-common/Core/Actions.swift @@ -0,0 +1,124 @@ +// +// Services.swift +// PHP Monitor +// +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation +import AppKit + +class Actions { + + // MARK: - Services + + public static func restartPhpFpm() + { + brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true) + } + + public static func restartNginx() + { + brew("services restart nginx", sudo: true) + } + + public static func restartDnsMasq() + { + brew("services restart dnsmasq", sudo: true) + } + + public static func stopAllServices() + { + brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true) + brew("services stop nginx", sudo: true) + brew("services stop dnsmasq", sudo: true) + } + + /** + Kindly asks Valet to switch to a specific PHP version. + */ + public static func switchToPhpVersionUsingValet( + version: String, + availableVersions: [String], + completed: @escaping () -> Void + ) { + Log.info("Switching to \(version) using Valet") + Log.info(valet("use php@\(version)")) + completed() + } + + // MARK: - Finding Config Files + + public static func openGenericPhpConfigFolder() + { + let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]; + NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) + } + + public static func openGlobalComposerFolder() + { + let file = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".composer/composer.json") + NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) + } + + public static func openPhpConfigFolder(version: String) + { + let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]; + NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) + } + + public static func openValetConfigFolder() + { + let file = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".config/valet") + NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) + } + + // MARK: - Other Actions + + public static func createTempPhpInfoFile() -> URL + { + // Write a file called `phpmon_phpinfo.php` to /tmp + try! " /tmp/phpmon_phpinfo.html") + + return URL(string: "file:///private/tmp/phpmon_phpinfo.html")! + } + + // MARK: - Fix My Valet + + /** + Detects all currently available PHP versions, + and unlinks each and every one of them. + + After this, the brew services are also stopped, + the latest PHP version is linked, and php + nginx are restarted. + + If this does not solve the issue, the user may need to install additional + extensions and/or run `composer global update`. + */ + public static func fixMyValet() + { + brew("services restart dnsmasq", sudo: true) + + PhpEnv.shared.detectPhpVersions().forEach { (version) in + let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)" + brew("unlink php@\(version)") + brew("services stop \(formula)") + brew("services stop \(formula)", sudo: true) + } + + brew("services stop dnsmasq") + brew("services stop php") + brew("services stop nginx") + + brew("link php --overwrite --force") + + brew("services restart dnsmasq", sudo: true) + brew("services restart php", sudo: true) + brew("services restart nginx", sudo: true) + } +} diff --git a/phpmon/Domain/Terminal/Command.swift b/phpmon-common/Core/Command.swift similarity index 97% rename from phpmon/Domain/Terminal/Command.swift rename to phpmon-common/Core/Command.swift index 31a075a..8da8bd2 100644 --- a/phpmon/Domain/Terminal/Command.swift +++ b/phpmon-common/Core/Command.swift @@ -7,7 +7,7 @@ import Cocoa -class Command { +public class Command { /** Immediately executes a command. diff --git a/phpmon/Constants.swift b/phpmon-common/Core/Constants.swift similarity index 89% rename from phpmon/Constants.swift rename to phpmon-common/Core/Constants.swift index 6bb3f62..1c67126 100644 --- a/phpmon/Constants.swift +++ b/phpmon-common/Core/Constants.swift @@ -51,4 +51,9 @@ class Constants { "8.2" ] + /** + The URL that people can visit if they wish to help support the project. + */ + static let DonationUrl = URL(string: "https://nicoverbruggen.be/sponsor#pay-now")! + } diff --git a/phpmon-common/Core/Events.swift b/phpmon-common/Core/Events.swift new file mode 100644 index 0000000..fbc803b --- /dev/null +++ b/phpmon-common/Core/Events.swift @@ -0,0 +1,15 @@ +// +// Events.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class Events { + + static let ServicesUpdated = Notification.Name("ServicesUpdated") + +} diff --git a/phpmon-common/Core/Helpers.swift b/phpmon-common/Core/Helpers.swift new file mode 100644 index 0000000..369b605 --- /dev/null +++ b/phpmon-common/Core/Helpers.swift @@ -0,0 +1,55 @@ +// +// Helpers.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 24/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +// MARK: Common Shell Commands + +/** + Runs a `valet` command. + */ +func valet(_ command: String) -> String +{ + return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true) +} + +/** + Runs a `brew` command. Can run as superuser. + */ +func brew(_ command: String, sudo: Bool = false) +{ + Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)") +} + +/** + Runs `sed` in order to replace all occurrences of a string in a specific file with another. + */ +func sed(file: String, original: String, replacement: String) +{ + // Escape slashes (or `sed` won't work) + let e_original = original.replacingOccurrences(of: "/", with: "\\/") + let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/") + + // Check if gsed exists; it is able to follow symlinks, + // which we want to do to toggle the extension + if Shell.fileExists("\(Paths.binPath)/gsed") { + Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)") + } else { + Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)") + } +} + +/** + Uses `grep` to determine whether a particular query string can be found in a particular file. + */ +func grepContains(file: String, query: String) -> Bool +{ + return Shell.pipe(""" + grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO" + """) + .trimmingCharacters(in: .whitespacesAndNewlines) + .contains("YES") +} diff --git a/phpmon-common/Core/Logger.swift b/phpmon-common/Core/Logger.swift new file mode 100644 index 0000000..87f2e81 --- /dev/null +++ b/phpmon-common/Core/Logger.swift @@ -0,0 +1,52 @@ +// +// Logger.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class Log { + + static var shared = Log() + + enum Verbosity: Int { + case error = 1, + warning = 2, + info = 3, + performance = 4 + + public func isApplicable() -> Bool { + return Log.shared.verbosity.rawValue >= self.rawValue + } + } + + var verbosity: Verbosity = .warning + + static func err(_ item: Any) { + if Verbosity.error.isApplicable() { + print(item) + } + } + + static func warn(_ item: Any) { + if Verbosity.warning.isApplicable() { + print(item) + } + } + + static func info(_ item: Any) { + if Verbosity.info.isApplicable() { + print(item) + } + } + + static func perf(_ item: Any) { + if Verbosity.performance.isApplicable() { + print(item) + } + } + +} diff --git a/phpmon-common/Core/Paths.swift b/phpmon-common/Core/Paths.swift new file mode 100644 index 0000000..9083041 --- /dev/null +++ b/phpmon-common/Core/Paths.swift @@ -0,0 +1,70 @@ +// +// Paths.swift +// PHP Monitor +// +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +/** + The `Paths` class is used to locate various binaries on the system. + The path to the Homebrew directory and the user's name are fetched only once, at boot. + */ +public class Paths { + + public static let shared = Paths() + + private var baseDir : Paths.HomebrewDir + + private var userName : String + + init() { + baseDir = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr + userName = String(Shell.pipe("whoami").split(separator: "\n")[0]) + } + + // - MARK: Binaries + + public static var valet: String { + return "\(binPath)/valet" + } + + public static var brew: String { + return "\(binPath)/brew" + } + + public static var php: String { + return "\(binPath)/php" + } + + public static var phpConfig: String { + return "\(binPath)/php-config" + } + + // - MARK: Paths + + public static var whoami: String { + return shared.userName + } + + public static var binPath: String { + return "\(shared.baseDir.rawValue)/bin" + } + + public static var optPath: String { + return "\(shared.baseDir.rawValue)/opt" + } + + public static var etcPath: String { + return "\(shared.baseDir.rawValue)/etc" + } + + // MARK: - Enum + + public enum HomebrewDir: String { + case opt = "/opt/homebrew" + case usr = "/usr/local" + } + +} diff --git a/phpmon/Domain/Terminal/Shell.swift b/phpmon-common/Core/Shell.swift similarity index 54% rename from phpmon/Domain/Terminal/Shell.swift rename to phpmon-common/Core/Shell.swift index 9cc958a..a1ae6c1 100644 --- a/phpmon/Domain/Terminal/Shell.swift +++ b/phpmon-common/Core/Shell.swift @@ -7,7 +7,7 @@ import Cocoa -class Shell { +public class Shell { // MARK: - Invoke static functions @@ -30,12 +30,12 @@ class Shell { /** We now require macOS 11, so no need to detect which terminal to use. */ - var shell: String = "/bin/sh" + public var shell: String = "/bin/sh" /** Singleton to access a user shell (with --login) */ - static let user = Shell() + public static let user = Shell() /** Runs a shell command without using the output. @@ -44,7 +44,7 @@ class Shell { - Parameter command: The command to run - Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this */ - func run( + private func run( _ command: String, requiresPath: Bool = false ) { @@ -58,11 +58,11 @@ class Shell { - Parameter command: The command to run - Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this */ - func pipe( + private func pipe( _ command: String, requiresPath: Bool = false ) -> String { - let shellOutput = self.execute(command, requiresPath: requiresPath) + let shellOutput = self.executeSynchronously(command, requiresPath: requiresPath) let hasError = ( shellOutput.standardOutput == "" && shellOutput.errorOutput.lengthOfBytes(using: .utf8) > 0 @@ -77,35 +77,28 @@ class Shell { - Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this - Parameter waitUntilExit: Waits for the command to complete before returning the `ShellOutput` */ - func execute( + public func executeSynchronously( _ command: String, - requiresPath: Bool = false, - waitUntilExit: Bool = false - ) -> ShellOutput { - let task = Process() + requiresPath: Bool = false + ) -> Shell.Output { + let outputPipe = Pipe() let errorPipe = Pipe() - let tailoredCommand = requiresPath - ? "export PATH=\(Paths.binPath):$PATH && \(command)" - : command - - task.launchPath = self.shell - task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand] + let task = self.createTask(for: command, requiresPath: requiresPath) task.standardOutput = outputPipe task.standardError = errorPipe task.launch() - - if waitUntilExit { - task.waitUntilExit() - } + task.waitUntilExit() - return ShellOutput( + return Shell.Output( standardOutput: String( - data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8 + data: outputPipe.fileHandleForReading.readDataToEndOfFile(), + encoding: .utf8 )!, errorOutput: String( - data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8 + data: errorPipe.fileHandleForReading.readDataToEndOfFile(), + encoding: .utf8 )!, task: task ) @@ -119,18 +112,69 @@ class Shell { let escapedPath = path.replacingOccurrences(of: " ", with: "\\ ") return Shell.pipe("if [ -f \(escapedPath) ]; then /bin/echo -n \"0\"; fi") == "0" } -} - -class ShellOutput { - let standardOutput: String - let errorOutput: String - let task: Process - init(standardOutput: String, - errorOutput: String, - task: Process) { - self.standardOutput = standardOutput - self.errorOutput = errorOutput - self.task = task + /** + Creates a new process with the correct PATH and shell. + */ + public func createTask(for command: String, requiresPath: Bool) -> Process { + let tailoredCommand = requiresPath + ? "export PATH=\(Paths.binPath):$PATH && \(command)" + : command + + let task = Process() + task.launchPath = self.shell + task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand] + + return task + } + + public static func captureOutput( + _ task: Process, + didReceiveStdOutData: @escaping (String) -> Void, + didReceiveStdErrData: @escaping (String) -> Void + ) { + let outputPipe = Pipe() + let errorPipe = Pipe() + + task.standardOutput = outputPipe + task.standardError = errorPipe + + [(outputPipe, didReceiveStdOutData), (errorPipe, didReceiveStdErrData)].forEach { + (pipe: Pipe, callback: @escaping (String) -> Void) in + pipe.fileHandleForReading.waitForDataInBackgroundAndNotify() + NotificationCenter.default.addObserver( + forName: NSNotification.Name.NSFileHandleDataAvailable, + object: pipe.fileHandleForReading, + queue: nil + ) { notification in + if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) { + callback(outputString) + } + pipe.fileHandleForReading.waitForDataInBackgroundAndNotify() + } + } + } + + public static func haltCapturingOutput(_ task: Process) { + if let pipe = task.standardOutput as? Pipe { + NotificationCenter.default.removeObserver(pipe.fileHandleForReading) + } + if let pipe = task.standardError as? Pipe { + NotificationCenter.default.removeObserver(pipe.fileHandleForReading) + } + } + + public class Output { + public let standardOutput: String + public let errorOutput: String + public let task: Process + + init(standardOutput: String, + errorOutput: String, + task: Process) { + self.standardOutput = standardOutput + self.errorOutput = errorOutput + self.task = task + } } } diff --git a/phpmon/Domain/PHP/ActivePhpInstallation.swift b/phpmon-common/PHP/ActivePhpInstallation.swift similarity index 85% rename from phpmon/Domain/PHP/ActivePhpInstallation.swift rename to phpmon-common/PHP/ActivePhpInstallation.swift index 2ab3579..f4b124e 100644 --- a/phpmon/Domain/PHP/ActivePhpInstallation.swift +++ b/phpmon-common/PHP/ActivePhpInstallation.swift @@ -25,7 +25,7 @@ class ActivePhpInstallation { // MARK: - Computed var formula: String { - return (version.short == App.shared.brewPhpVersion) ? "php" : "php@\(version.short)" + return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)" } // MARK: - Initializer @@ -122,26 +122,6 @@ class ActivePhpInstallation { return (match == nil) ? "⚠️" : "\(value)B" } - /** - It is always possible that the system configuration for PHP-FPM has not been set up for Valet. - This can occur when a user manually installs a new PHP version, but does not run `valet install`. - In that case, we should alert the user! - - - Important: The underlying check is `checkPhpFpmStatus`, which can be run multiple times. - This method actively presents a modal if said checks fails, so don't call this method too many times. - */ - public func notifyAboutBrokenPhpFpm() { - if !self.checkPhpFpmStatus() { - DispatchQueue.main.async { - Alert.notify( - message: "alert.php_fpm_broken.title".localized, - info: "alert.php_fpm_broken.info".localized, - style: .critical - ) - } - } - } - /** Determine if PHP-FPM is configured correctly. @@ -149,7 +129,7 @@ class ActivePhpInstallation { versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails, that means that Valet won't work properly. */ - private func checkPhpFpmStatus() -> Bool { + func checkPhpFpmStatus() -> Bool { if self.version.short == "5.6" { // The main PHP config file should contain `valet.sock` and then we're probably fine? let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf" diff --git a/phpmon/Domain/Integrations/Homebrew/HomebrewPackage.swift b/phpmon-common/PHP/Homebrew/HomebrewPackage.swift similarity index 100% rename from phpmon/Domain/Integrations/Homebrew/HomebrewPackage.swift rename to phpmon-common/PHP/Homebrew/HomebrewPackage.swift diff --git a/phpmon-common/PHP/Homebrew/HomebrewService.swift b/phpmon-common/PHP/Homebrew/HomebrewService.swift new file mode 100644 index 0000000..6c21af4 --- /dev/null +++ b/phpmon-common/PHP/Homebrew/HomebrewService.swift @@ -0,0 +1,21 @@ +// +// HomebrewService.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 11/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +struct HomebrewService: Decodable, Equatable { + let name: String + let service_name: String + let running: Bool + let loaded: Bool + let pid: Int? + let user: String? + let status: String? + let log_path: String? + let error_log_path: String? +} diff --git a/phpmon-common/PHP/PHP Version/PhpEnv.swift b/phpmon-common/PHP/PHP Version/PhpEnv.swift new file mode 100644 index 0000000..3d6c159 --- /dev/null +++ b/phpmon-common/PHP/PHP Version/PhpEnv.swift @@ -0,0 +1,170 @@ +// +// PhpSwitcher.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +protocol PhpSwitcherDelegate: AnyObject { + func switcherDidStartSwitching() + func switcherDidCompleteSwitch() +} + +class PhpEnv { + + // MARK: - Initializer + + init() { + self.currentInstall = ActivePhpInstallation() + + let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json"); + + self.homebrewPackage = try! JSONDecoder().decode( + [HomebrewPackage].self, + from: brewPhpAlias.data(using: .utf8)! + ).first! + + Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!") + } + + // MARK: - Properties + + /** The delegate that is informed of updates. */ + weak var delegate: PhpSwitcherDelegate? + + /** The static app instance. Accessible at any time. */ + static let shared = PhpEnv() + + /** Whether the switcher is busy performing any actions. */ + var isBusy: Bool = false + + /** All available versions of PHP. */ + var availablePhpVersions: [String] = [] + + /** Cached information about the PHP installations. */ + var cachedPhpInstallations: [String: PhpInstallation] = [:] + + /** Information about the currently linked PHP installation. */ + var currentInstall: ActivePhpInstallation + + /** + The version that the `php` formula via Brew is aliased to on the current system. + + If you're up to date, `php` will be aliased to the latest version, + but that might not be the case since not everyone keeps their + software up-to-date. + + As such, we take that information from Homebrew. + */ + static var brewPhpVersion: String { + return Self.shared.homebrewPackage.version + } + + /** + The currently linked and active PHP installation. + */ + static var phpInstall: ActivePhpInstallation { + return Self.shared.currentInstall + } + + /** + Information we were able to discern from the Homebrew info command. + */ + var homebrewPackage: HomebrewPackage! = nil + + // MARK: - Methods + + public static var switcher: PhpSwitcher { + return InternalSwitcher() + } + + public static func detectPhpVersions() -> Void { + _ = Self.shared.detectPhpVersions() + } + + /** + Detects which versions of PHP are installed. + */ + public func detectPhpVersions() -> [String] + { + let files = Shell.pipe("ls \(Paths.optPath) | grep php@") + + var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n")) + + // Make sure the aliased version is detected + // The user may have `php` installed, but not e.g. `php@8.0` + // We should also detect that as a version that is installed + let phpAlias = homebrewPackage.version + + // Avoid inserting a duplicate + if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) { + versionsOnly.append(phpAlias) + } + + Log.info("The PHP versions that were detected are: \(versionsOnly)") + + availablePhpVersions = versionsOnly + + var mappedVersions: [String: PhpInstallation] = [:] + + availablePhpVersions.forEach { version in + mappedVersions[version] = PhpInstallation(version) + } + + cachedPhpInstallations = mappedVersions + + return versionsOnly + } + + /** + Extracts valid PHP versions from an array of strings. + This array of strings is usually retrieved from `grep`. + */ + public func extractPhpVersions( + from versions: [String], + checkBinaries: Bool = true + ) -> [String] { + var output : [String] = [] + + versions.filter { (version) -> Bool in + // Omit everything that doesn't start with php@ + // (e.g. something-php@8.0 won't be detected) + return version.starts(with: "php@") + }.forEach { (string) in + let version = string.components(separatedBy: "php@")[1] + // Only append the version if it doesn't already exist (avoid dupes), + // is supported and where the binary exists (avoids broken installs) + if !output.contains(version) + && Constants.SupportedPhpVersions.contains(version) + && (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) + { + output.append(version) + } + } + + return output + } + + public func validVersions(for constraint: String) -> [PhpVersionNumber] { + constraint.split(separator: "|").flatMap { + return PhpVersionNumberCollection + .make(from: self.availablePhpVersions) + .matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines)) + } + } + + /** + Validates whether the currently running version matches the provided version. + */ + public func validate(_ version: String) -> Bool { + if self.currentInstall.version.short == version { + print("Switching to version \(version) seems to have succeeded. Validation passed.") + return true + } + + return false + } +} diff --git a/phpmon-common/PHP/PHP Version/PhpVersionNumber.swift b/phpmon-common/PHP/PHP Version/PhpVersionNumber.swift new file mode 100644 index 0000000..624e829 --- /dev/null +++ b/phpmon-common/PHP/PHP Version/PhpVersionNumber.swift @@ -0,0 +1,176 @@ +// +// PhpVersionNumber.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +public struct PhpVersionNumberCollection: Equatable { + let versions: [PhpVersionNumber] + + public static func make(from versions: [String]) -> Self { + return PhpVersionNumberCollection( + versions: versions.map { PhpVersionNumber.make(from: $0)! } + ) + } + + public var first: PhpVersionNumber? { + return self.versions.first + } + + public var all: [PhpVersionNumber] { + return self.versions + } + + /** + Checks if any versions of PHP are valid for the constraint provided. + Due to the complexity of evaluating these, a important test is maintained. + More information on these constraints can be found here: + https://getcomposer.org/doc/articles/versions.md#writing-version-constraints + + - Parameter constraint: The full constraint as a string (e.g. "^7.0") + - Parameter strict: Whether the patch version check is strict. See more below. + + The strict mode does not matter if a patch version is provided for all versions in the collection. + + Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred + from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise, + assumes that the patch version is .999, which means that in all cases the patch version check is + always going to pass. + + **STRICT MODE (= patch precision on)** + + Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will + be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK). + When checking against actual PHP versions installed by the user (with patch precision), use + strict mode. + + **NON-STRICT MODE (= patch precision off)** + + Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0 + is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version. + In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde). + If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since + the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.) + */ + public func matching(constraint: String, strict: Bool = false) -> [PhpVersionNumber] { + if let version = PhpVersionNumber.make(from: constraint, type: .versionOnly) { + // Strict constraint (e.g. "7.0") -> returns specific version + return self.versions.filter { $0.isSameAs(version, strict) } + } + + if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) { + // Caret range means that the major version is never higher but minor version can be higher + // ^7.2 will be compatible with all versions between 7.2 and 8.0 + return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) } + } + + if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) { + // Tilde range means that most specific digit is used as the basis. + return self.versions.filter { + version.patch != nil + // If a patch is provided then the minor version cannot be bumped. + ? $0.hasSameMajorAndMinorButNewerOrSamePatch(version, strict) + // If a patch is not provided then the major version cannot be bumped. + : $0.hasSameMajorButNewerOrSameMinor(version, strict) + } + } + + if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) { + return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) } + } + + if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) { + return self.versions.filter { $0.isNewerThan(version, strict) } + } + + return [] + } +} + +public struct PhpVersionNumber: Equatable { + let major: Int + let minor: Int + let patch: Int? + + public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int { + return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999) + } + + public var homebrewVersion: String { + return "\(major).\(minor)" + } + + public enum MatchType: String { + case versionOnly = #"^(?\d+).(?\d+).?(?\d+)?\z"# + case caretVersionRange = #"^\^(?\d+).(?\d+).?(?\d+)?\z"# + case tildeVersionRange = #"^~(?\d+).(?\d+).?(?\d+)?\z"# + case greaterThanOrEqual = #"^>=(?\d+).(?\d+).?(?\d+)?\z"# + case greaterThan = #"^>(?\d+).(?\d+).?(?\d+)?\z"# + + // TODO: (5.1) Handle these cases (even though I suspect these are uncommon) + /* + case smallerThanOrEqual = #"^<=(?\d+).(?\d+).?(?\d+)?\z"# + case smallerThan = #"^<(?\d+).(?\d+).?(?\d+)?\z"# + */ + } + + public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? { + let regex = try! NSRegularExpression(pattern: type.rawValue, options: []) + let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first + + if match != nil { + let major = Int( + versionString[Range(match!.range(withName: "major"), in: versionString)!] + )! + let minor = Int( + versionString[Range(match!.range(withName: "minor"), in: versionString)!] + )! + var patch: Int? = nil + if let minorRange = Range(match!.range(withName: "patch"), in: versionString) { + patch = Int(versionString[minorRange]) + } + return Self(major: major, minor: minor, patch: patch) + } + + return nil + } + + // MARK: Comparison Logic + + internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { + return self.major == version.major + && self.minor == version.minor + && (strict ? self.patch(strict, version) == version.patch(strict) : true) + } + + internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { + return ( + self.major > version.major || + self.major == version.major && self.minor > version.minor || + self.major == version.major && self.minor == version.minor + && self.patch(strict) > version.patch(strict) + ) + } + + internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { + return self.major == version.major && + ( + (self.minor == version.minor && self.patch(strict) >= version.patch(strict, self)) + || self.minor > version.minor + ) + } + + internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { + return self.major == version.major && self.minor == version.minor + && self.patch(strict, version) >= version.patch(strict) + } + + internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { + return self.major == version.major + && self.minor >= version.minor + } +} diff --git a/phpmon/Domain/PHP/PhpExtension.swift b/phpmon-common/PHP/PhpExtension.swift similarity index 95% rename from phpmon/Domain/PHP/PhpExtension.swift rename to phpmon-common/PHP/PhpExtension.swift index 5268649..05f1a06 100644 --- a/phpmon/Domain/PHP/PhpExtension.swift +++ b/phpmon-common/PHP/PhpExtension.swift @@ -78,7 +78,7 @@ class PhpExtension { // ENABLED: Line where the comment delimiter (;) is removed : line.replacingOccurrences(of: "; ", with: "") - Actions.sed(file: file, original: line, replacement: newLine) + sed(file: file, original: line, replacement: newLine) enabled.toggle() } @@ -92,7 +92,7 @@ class PhpExtension { let file = try? String(contentsOf: path, encoding: .utf8) if (file == nil) { - print("There was an issue reading the file. Assuming no extensions were found.") + Log.err("There was an issue reading the file. Assuming no extensions were found.") return [] } diff --git a/phpmon/Domain/PHP/PhpInstallation.swift b/phpmon-common/PHP/PhpInstallation.swift similarity index 62% rename from phpmon/Domain/PHP/PhpInstallation.swift rename to phpmon-common/PHP/PhpInstallation.swift index 02577e2..4ecc010 100644 --- a/phpmon/Domain/PHP/PhpInstallation.swift +++ b/phpmon-common/PHP/PhpInstallation.swift @@ -10,20 +10,26 @@ import Foundation class PhpInstallation { - var longVersion: String + var longVersion: PhpVersionNumber /** In order to determine details about a PHP installation, we’ll simply run `php-config --version` in the relevant directory. */ init(_ version: String) { + let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config" - self.longVersion = version + self.longVersion = PhpVersionNumber.make(from: version)! + if Shell.fileExists(phpConfigExecutablePath) { - self.longVersion = Command.execute( + let longVersionString = Command.execute( path: phpConfigExecutablePath, arguments: ["--version"] - ) + ).trimmingCharacters(in: .whitespacesAndNewlines) + + self.longVersion = PhpVersionNumber.make( + from: String(longVersionString.split(separator: "-")[0]) + )! } } diff --git a/phpmon-common/PHP/Switcher/InternalSwitcher.swift b/phpmon-common/PHP/Switcher/InternalSwitcher.swift new file mode 100644 index 0000000..4c313ad --- /dev/null +++ b/phpmon-common/PHP/Switcher/InternalSwitcher.swift @@ -0,0 +1,61 @@ +// +// InternalSwitcher.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 24/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class InternalSwitcher: PhpSwitcher { + + /** + Switching to a new PHP version involves: + - unlinking the current version + - stopping the active services + - linking the new desired version + + Please note that depending on which version is installed, + the version that is switched to may or may not be identical to `php` + (without @version). + */ + func performSwitch(to version: String, completion: @escaping () -> Void) + { + Log.info("Switching to \(version), unlinking all versions...") + + let group = DispatchGroup() + + PhpEnv.shared.availablePhpVersions.forEach { (available) in + group.enter() + + DispatchQueue.global(qos: .userInitiated).async { + let formula = (available == PhpEnv.brewPhpVersion) + ? "php" : "php@\(available)" + + brew("unlink \(formula)") + brew("services stop \(formula)", sudo: true) + + Log.perf("Unlinked and stopped services for \(formula)") + + group.leave() + } + } + + group.notify(queue: .global(qos: .userInitiated)) { + Log.info("All versions have been unlinked!") + Log.info("Linking the new version!") + + let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)" + brew("link \(formula) --overwrite --force") + brew("services start \(formula)", sudo: true) + + Log.info("Restarting nginx, just to be sure!") + brew("services restart nginx", sudo: true) + + Log.info("The new version has been linked!") + completion() + } + } + +} diff --git a/phpmon-common/PHP/Switcher/PhpSwitcher.swift b/phpmon-common/PHP/Switcher/PhpSwitcher.swift new file mode 100644 index 0000000..e167e15 --- /dev/null +++ b/phpmon-common/PHP/Switcher/PhpSwitcher.swift @@ -0,0 +1,15 @@ +// +// PhpVersionSwitchContract.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 24/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +protocol PhpSwitcher { + + func performSwitch(to version: String, completion: @escaping () -> Void) + +} diff --git a/phpmon-tests/BrewJsonParserTest.swift b/phpmon-tests/BrewJsonParserTest.swift index a91cbee..a12cb3f 100644 --- a/phpmon-tests/BrewJsonParserTest.swift +++ b/phpmon-tests/BrewJsonParserTest.swift @@ -9,15 +9,18 @@ import XCTest class BrewJsonParserTest: XCTestCase { + + // - MARK: SYNTHETIC TESTS static var jsonBrewFile: URL { - return Bundle(for: Self.self).url(forResource: "brew", withExtension: "json")! + return Bundle(for: Self.self) + .url(forResource: "brew", withExtension: "json")! } - func testCanLoadExtension() throws { - let json = try? String(contentsOf: Self.jsonBrewFile, encoding: .utf8) + func testCanLoadExtensionJson() throws { + let json = try! String(contentsOf: Self.jsonBrewFile, encoding: .utf8) let package = try! JSONDecoder().decode( - [HomebrewPackage].self, from: json!.data(using: .utf8)! + [HomebrewPackage].self, from: json.data(using: .utf8)! ).first! XCTAssertEqual(package.name, "php") @@ -27,5 +30,56 @@ class BrewJsonParserTest: XCTestCase { installed.version.starts(with: "8.0") }), true) } + + static var jsonBrewServicesFile: URL { + return Bundle(for: Self.self) + .url(forResource: "brew-services", withExtension: "json")! + } + + func testCanParseServicesJson() throws { + let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8) + let services = try! JSONDecoder().decode( + [HomebrewService].self, from: json.data(using: .utf8)! + ) + + XCTAssertGreaterThan(services.count, 0) + XCTAssertEqual(services.first?.name, "dnsmasq") + XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq") + } + + // - MARK: LIVE TESTS + /// This test requires that you have a valid Homebrew installation set up, + /// and requires the Valet services to be installed: php, nginx and dnsmasq. + /// If this test fails, there is an issue with your Homebrew installation + /// or the JSON API of the Homebrew output may have changed. + func testCanParseServicesJsonFromCliOutput() throws { + let services = try! JSONDecoder().decode( + [HomebrewService].self, + from: Shell.pipe( + "sudo \(Paths.brew) services info --all --json", + requiresPath: true + ).data(using: .utf8)! + ).filter({ service in + return ["php", "nginx", "dnsmasq"].contains(service.name) + }) + + XCTAssertTrue(services.contains(where: {$0.name == "php"} )) + XCTAssertTrue(services.contains(where: {$0.name == "nginx"} )) + XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} )) + XCTAssertEqual(services.count, 3) + } + + /// This test requires that you have a valid Homebrew installation set up, + /// and requires the `php` formula to be installed. + /// If this test fails, there is an issue with your Homebrew installation + /// or the JSON API of the Homebrew output may have changed. + func testCanLoadExtensionJsonFromCliOutput() throws { + let package = try! JSONDecoder().decode( + [HomebrewPackage].self, + from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)! + ).first! + + XCTAssertTrue(package.name == "php") + } } diff --git a/phpmon-tests/PhpVersionDetectionTest.swift b/phpmon-tests/PhpVersionDetectionTest.swift index 660e128..fedd72d 100644 --- a/phpmon-tests/PhpVersionDetectionTest.swift +++ b/phpmon-tests/PhpVersionDetectionTest.swift @@ -11,7 +11,7 @@ import XCTest class PhpVersionDetectionTest: XCTestCase { func testCanDetectValidPhpVersions() throws { - let outcome = Actions.extractPhpVersions(from: [ + let outcome = PhpEnv.shared.extractPhpVersions(from: [ "", // empty lines should be omitted "php@8.0", "php@8.0", // should only be detected once @@ -26,5 +26,4 @@ class PhpVersionDetectionTest: XCTestCase { XCTAssertEqual(outcome, ["8.0", "7.0", "5.6"]) } - } diff --git a/phpmon-tests/PhpVersionNumberTest.swift b/phpmon-tests/PhpVersionNumberTest.swift new file mode 100644 index 0000000..2a059e3 --- /dev/null +++ b/phpmon-tests/PhpVersionNumberTest.swift @@ -0,0 +1,276 @@ +// +// PhpVersionNumberTest.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import XCTest + +class PhpVersionNumberTest: XCTestCase { + + func testCanDeconstructPhpVersion() throws { + XCTAssertEqual( + PhpVersionNumber.make(from: "8.0.11"), + PhpVersionNumber(major: 8, minor: 0, patch: 11) + ) + XCTAssertEqual( + PhpVersionNumber.make(from: "7.4.2"), + PhpVersionNumber(major: 7, minor: 4, patch: 2) + ) + XCTAssertEqual( + PhpVersionNumber.make(from: "7.4"), + PhpVersionNumber(major: 7, minor: 4, patch: nil) + ) + XCTAssertEqual( + PhpVersionNumber.make(from: "7"), + nil + ) + } + + func testCanCheckFixedConstraints() throws { + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: "7.0"), + PhpVersionNumberCollection + .make(from: ["7.0"]).all + ) + + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"]) + .matching(constraint: "7.0.3"), + PhpVersionNumberCollection + .make(from: ["7.0.3"]).all + ) + + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: "7.0.3", strict: false), + PhpVersionNumberCollection + .make(from: ["7.0"]).all + ) + + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: "7.0.3", strict: true), + PhpVersionNumberCollection + .make(from: []).all + ) + } + + func testCanCheckCaretConstraints() throws { + // 1. Imprecise checks + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: "^7.0", strict: true), + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all + ) + + // 2. Imprecise check with precise constraint (lenient AKA not strict) + // These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc. + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: "^7.0.1", strict: false), + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all + ) + + // 3. Imprecise check with precise constraint (strict mode) + // These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc. + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: "^7.0.1", strict: true), + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1"]).all + ) + + // 4. Precise members and constraint all around + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]) + .matching(constraint: "^7.0.1", strict: true), + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all + ) + + // 5. Precise members but imprecise constraint (strict mode) + // In strict mode the constraint's patch version is assumed to be 0 + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]) + .matching(constraint: "^7.0", strict: true), + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all + ) + + // 6. Precise members but imprecise constraint (lenient mode) + // In lenient mode the constraint's patch version is assumed to be equal + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]) + .matching(constraint: "^7.0", strict: false), + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all + ) + } + + func testCanCheckTildeConstraints() throws { + // 1. Imprecise checks + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: "~7.0", strict: true), + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all + ) + + // 2. Imprecise check with precise constraint (lenient AKA not strict) + // These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc. + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: "~7.0.1", strict: false), + // One result because 7.0.1 to 7.0.x is expected. + // 7.0.999 (assumed due to no strictness) is valid. + // 7.1.0 and up are not valid (minor version is too high). + PhpVersionNumberCollection + .make(from: ["7.0"]).all + ) + + // 3. Imprecise check with precise constraint (strict mode) + // These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc. + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: "~7.0.1", strict: true), + // No results because 7.0.1 to 7.0.x is expected. + // 7.0.0 (assumed due to strictness) is not valid. + // 7.1.0 and up are also not valid (minor version is too high). + PhpVersionNumberCollection + .make(from: []).all + ) + + // 4. Precise members and constraint all around + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]) + .matching(constraint: "~7.0.1", strict: true), + // Only 7.0 with a patch version of .1 or higher is OK. + // In this example, 7.0.10 is OK but all other versions are too new. + PhpVersionNumberCollection + .make(from: ["7.0.10"]).all + ) + + // 5. Precise members but imprecise constraint (strict mode) + // In strict mode the constraint's patch version is assumed to be 0. + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]) + .matching(constraint: "~7.0", strict: true), + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all + ) + + // 6. Precise members but imprecise constraint (lenient mode) + // In lenient mode the constraint's patch version is assumed to be equal. + // (Strictness does not make any difference here, but both should be tested.) + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]) + .matching(constraint: "~7.0", strict: false), + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all + ) + } + + func testCanCheckGreaterThanOrEqualConstraints() throws { + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: ">=7.0", strict: true), + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all + ) + + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: ">=7.0.0", strict: true), + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all + ) + + // Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0) + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: ">=7.2.5", strict: true), + PhpVersionNumberCollection + .make(from: ["7.4", "7.3"]).all + ) + + // Non-strict check (ignoring patch, 7.2 resolves to 7.2.999) + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: ">=7.2.5", strict: false), + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2"]).all + ) + } + + func testCanCheckGreaterThanConstraints() throws { + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: ">7.0"), + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1"]).all + ) + + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: ">7.2.5"), + // 7.2 will be valid due to non-strict mode (resolves to 7.2.999) + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2"]).all + ) + + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) + .matching(constraint: ">7.2.5", strict: true), + // 7.2 will not be valid due to strict mode (resolves to 7.2.0) + PhpVersionNumberCollection + .make(from: ["7.4", "7.3"]).all + ) + + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"]) + .matching(constraint: ">7.2.8"), + // 7.2 will be valid due to non-strict mode (resolves to 7.2.999) + PhpVersionNumberCollection + .make(from: ["7.3.1", "7.2.9", "7.2"]).all + ) + + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"]) + .matching(constraint: ">7.2.8", strict: true), + // 7.2 will not be valid due to strict mode (resolves to 7.2.0) + PhpVersionNumberCollection + .make(from: ["7.3.1", "7.2.9"]).all + ) + } +} diff --git a/phpmon-tests/Test Files/brew-services.json b/phpmon-tests/Test Files/brew-services.json new file mode 100644 index 0000000..52dc263 --- /dev/null +++ b/phpmon-tests/Test Files/brew-services.json @@ -0,0 +1,135 @@ +[ + { + "name": "dnsmasq", + "service_name": "homebrew.mxcl.dnsmasq", + "running": true, + "loaded": true, + "schedulable": false, + "pid": 106, + "exit_code": 0, + "user": "root", + "status": "started", + "file": "/Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist", + "command": "/opt/homebrew/opt/dnsmasq/sbin/dnsmasq --keep-in-foreground -C /opt/homebrew/etc/dnsmasq.conf -7 /opt/homebrew/etc/dnsmasq.d,*.conf", + "working_dir": null, + "root_dir": null, + "log_path": null, + "error_log_path": null, + "interval": null, + "cron": null + }, + { + "name": "httpd", + "service_name": "homebrew.mxcl.httpd", + "running": false, + "loaded": false, + "schedulable": false, + "pid": null, + "exit_code": null, + "user": null, + "status": "none", + "file": "/opt/homebrew/opt/httpd/homebrew.mxcl.httpd.plist", + "command": "/opt/homebrew/opt/httpd/bin/httpd -D FOREGROUND", + "working_dir": null, + "root_dir": null, + "log_path": null, + "error_log_path": null, + "interval": null, + "cron": null + }, + { + "name": "mailhog", + "service_name": "homebrew.mxcl.mailhog", + "running": false, + "loaded": false, + "schedulable": false, + "pid": null, + "exit_code": null, + "user": null, + "status": "none", + "file": "/opt/homebrew/opt/mailhog/homebrew.mxcl.mailhog.plist", + "command": "/opt/homebrew/opt/mailhog/bin/MailHog", + "working_dir": null, + "root_dir": null, + "log_path": "/opt/homebrew/var/log/mailhog.log", + "error_log_path": "/opt/homebrew/var/log/mailhog.log", + "interval": null, + "cron": null + }, + { + "name": "nginx", + "service_name": "homebrew.mxcl.nginx", + "running": true, + "loaded": true, + "schedulable": false, + "pid": 116, + "exit_code": 0, + "user": "root", + "status": "started", + "file": "/Library/LaunchDaemons/homebrew.mxcl.nginx.plist", + "command": "/opt/homebrew/opt/nginx/bin/nginx -g daemon off;", + "working_dir": "/opt/homebrew", + "root_dir": null, + "log_path": null, + "error_log_path": null, + "interval": null, + "cron": null + }, + { + "name": "php", + "service_name": "homebrew.mxcl.php", + "running": true, + "loaded": true, + "schedulable": false, + "pid": 142, + "exit_code": 0, + "user": "root", + "status": "started", + "file": "/Library/LaunchDaemons/homebrew.mxcl.php.plist", + "command": "/opt/homebrew/opt/php/sbin/php-fpm --nodaemonize", + "working_dir": "/opt/homebrew/var", + "root_dir": null, + "log_path": null, + "error_log_path": "/opt/homebrew/var/log/php-fpm.log", + "interval": null, + "cron": null + }, + { + "name": "php@8.0", + "service_name": "homebrew.mxcl.php@8.0", + "running": false, + "loaded": false, + "schedulable": false, + "pid": null, + "exit_code": null, + "user": null, + "status": "none", + "file": "/opt/homebrew/opt/php@8.0/homebrew.mxcl.php@8.0.plist", + "command": "/opt/homebrew/opt/php@8.0/sbin/php-fpm --nodaemonize", + "working_dir": "/opt/homebrew/var", + "root_dir": null, + "log_path": null, + "error_log_path": "/opt/homebrew/var/log/php-fpm.log", + "interval": null, + "cron": null + }, + { + "name": "unbound", + "service_name": "homebrew.mxcl.unbound", + "running": false, + "loaded": false, + "schedulable": false, + "pid": null, + "exit_code": null, + "user": null, + "status": "none", + "file": "/opt/homebrew/opt/unbound/homebrew.mxcl.unbound.plist", + "command": "/opt/homebrew/opt/unbound/sbin/unbound -d -c /opt/homebrew/etc/unbound/unbound.conf", + "working_dir": null, + "root_dir": null, + "log_path": null, + "error_log_path": null, + "interval": null, + "cron": null + } +] diff --git a/phpmon-tests/brew.json b/phpmon-tests/Test Files/brew.json similarity index 100% rename from phpmon-tests/brew.json rename to phpmon-tests/Test Files/brew.json diff --git a/phpmon-tests/php.ini b/phpmon-tests/Test Files/php.ini similarity index 100% rename from phpmon-tests/php.ini rename to phpmon-tests/Test Files/php.ini diff --git a/phpmon-tests/valet-config.json b/phpmon-tests/Test Files/valet-config.json similarity index 100% rename from phpmon-tests/valet-config.json rename to phpmon-tests/Test Files/valet-config.json diff --git a/phpmon-tests/Utility.swift b/phpmon-tests/Utility.swift index 218a459..ac55f18 100644 --- a/phpmon-tests/Utility.swift +++ b/phpmon-tests/Utility.swift @@ -19,7 +19,7 @@ class Utility { try FileManager.default.copyItem(at: bundleURL, to: targetURL) return targetURL } catch let error { - print("Unable to copy file: \(error)") + Log.err("Unable to copy file: \(error)") } } diff --git a/phpmon-tests/ValetTest.swift b/phpmon-tests/ValetTest.swift index 6d5b31d..b7713c4 100644 --- a/phpmon-tests/ValetTest.swift +++ b/phpmon-tests/ValetTest.swift @@ -11,7 +11,7 @@ import XCTest class ValetTest: XCTestCase { func testDetermineValetVersion() { - let version = Actions.valet("--version") + let version = valet("--version") XCTAssert(version.contains("Laravel Valet 2.")) } diff --git a/phpmon/Assets.xcassets/Checkmark.imageset/Contents.json b/phpmon/Assets.xcassets/Checkmark.imageset/Contents.json new file mode 100644 index 0000000..9048d37 --- /dev/null +++ b/phpmon/Assets.xcassets/Checkmark.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "check.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/phpmon/Assets.xcassets/Checkmark.imageset/check.svg b/phpmon/Assets.xcassets/Checkmark.imageset/check.svg new file mode 100644 index 0000000..316a362 --- /dev/null +++ b/phpmon/Assets.xcassets/Checkmark.imageset/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phpmon/Assets.xcassets/IconColorGreen.colorset/Contents.json b/phpmon/Assets.xcassets/IconColorGreen.colorset/Contents.json new file mode 100644 index 0000000..fdb176a --- /dev/null +++ b/phpmon/Assets.xcassets/IconColorGreen.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.501", + "green" : "0.697", + "red" : "0.247" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.501", + "green" : "0.765", + "red" : "0.247" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phpmon/Assets.xcassets/IconColorNormal.colorset/Contents.json b/phpmon/Assets.xcassets/IconColorNormal.colorset/Contents.json new file mode 100644 index 0000000..d890719 --- /dev/null +++ b/phpmon/Assets.xcassets/IconColorNormal.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phpmon/Assets.xcassets/IconColorRed.colorset/Contents.json b/phpmon/Assets.xcassets/IconColorRed.colorset/Contents.json new file mode 100644 index 0000000..7274621 --- /dev/null +++ b/phpmon/Assets.xcassets/IconColorRed.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.180", + "green" : "0.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.426", + "green" : "0.363", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json b/phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json new file mode 100644 index 0000000..b7c29f9 --- /dev/null +++ b/phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "question-circle.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/phpmon/Assets.xcassets/ServiceLoading.imageset/question-circle.svg b/phpmon/Assets.xcassets/ServiceLoading.imageset/question-circle.svg new file mode 100644 index 0000000..df1f089 --- /dev/null +++ b/phpmon/Assets.xcassets/ServiceLoading.imageset/question-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json b/phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json new file mode 100644 index 0000000..5aba3d7 --- /dev/null +++ b/phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "times-circle.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/phpmon/Assets.xcassets/ServiceOff.imageset/times-circle.svg b/phpmon/Assets.xcassets/ServiceOff.imageset/times-circle.svg new file mode 100644 index 0000000..b5d5eba --- /dev/null +++ b/phpmon/Assets.xcassets/ServiceOff.imageset/times-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json b/phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json new file mode 100644 index 0000000..e3569a7 --- /dev/null +++ b/phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "check-circle.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/phpmon/Assets.xcassets/ServiceOn.imageset/check-circle.svg b/phpmon/Assets.xcassets/ServiceOn.imageset/check-circle.svg new file mode 100644 index 0000000..5673a79 --- /dev/null +++ b/phpmon/Assets.xcassets/ServiceOn.imageset/check-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phpmon/Credits.html b/phpmon/Credits.html index 4549bd7..40bcc83 100644 --- a/phpmon/Credits.html +++ b/phpmon/Credits.html @@ -16,6 +16,7 @@

Want to spread the love? Leave a star on GitHub!

Having issues? Consult the FAQ & Troubleshooting section.

Want to support me? You can financially support the continued development of this app.

+

Get the latest on Twitter Give me a follow on Twitter to learn about the latest and greatest updates of this app.


diff --git a/phpmon/Domain/Core/Actions.swift b/phpmon/Domain/Core/Actions.swift deleted file mode 100644 index 5bbc098..0000000 --- a/phpmon/Domain/Core/Actions.swift +++ /dev/null @@ -1,270 +0,0 @@ -// -// Services.swift -// PHP Monitor -// -// Copyright © 2021 Nico Verbruggen. All rights reserved. -// - -import Foundation -import AppKit - -class Actions { - - // MARK: - Detect PHP Versions - - public static func detectPhpVersions() -> [String] - { - let files = Shell.pipe("ls \(Paths.optPath) | grep php@") - var versionsOnly = Self.extractPhpVersions(from: files.components(separatedBy: "\n")) - - // Make sure the aliased version is detected - // The user may have `php` installed, but not e.g. `php@8.0` - // We should also detect that as a version that is installed - let phpAlias = App.shared.brewPhpVersion - - // Avoid inserting a duplicate - if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) { - versionsOnly.append(phpAlias); - } - - print("The PHP versions that were detected are: \(versionsOnly)") - - App.shared.availablePhpVersions = versionsOnly - Actions.extractPhpLongVersions() - - return versionsOnly - } - - /** - This method extracts the PHP full version number after finding the php installation folders. - To be refactored at some later point, I'd like to cache the `PhpInstallation` objects instead of just the version number at some point. - */ - public static func extractPhpLongVersions() - { - var mappedVersions: [String: PhpInstallation] = [:] - App.shared.availablePhpVersions.forEach { version in - mappedVersions[version] = PhpInstallation(version) - } - - App.shared.cachedPhpInstallations = mappedVersions - } - - /** - Extracts valid PHP versions from an array of strings. - This array of strings is usually retrieved from `grep`. - */ - public static func extractPhpVersions( - from versions: [String], - checkBinaries: Bool = true - ) -> [String] { - var output : [String] = [] - - versions.filter { (version) -> Bool in - // Omit everything that doesn't start with php@ - // (e.g. something-php@8.0 won't be detected) - return version.starts(with: "php@") - }.forEach { (string) in - let version = string.components(separatedBy: "php@")[1] - // Only append the version if it doesn't already exist (avoid dupes), - // is supported and where the binary exists (avoids broken installs) - if !output.contains(version) - && Constants.SupportedPhpVersions.contains(version) - && (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) - { - output.append(version) - } - } - - return output - } - - // MARK: - Services - - public static func restartPhpFpm() - { - brew("services restart \(App.phpInstall!.formula)", sudo: true) - } - - public static func restartNginx() - { - brew("services restart nginx", sudo: true) - } - - public static func restartDnsMasq() - { - brew("services restart dnsmasq", sudo: true) - } - - public static func stopAllServices() - { - brew("services stop \(App.phpInstall!.formula)", sudo: true) - brew("services stop nginx", sudo: true) - brew("services stop dnsmasq", sudo: true) - } - - /** - Kindly asks Valet to switch to a specific PHP version. - */ - public static func switchToPhpVersionUsingValet( - version: String, - availableVersions: [String], - completed: @escaping () -> Void - ) { - print("Switching to \(version) using Valet") - print(valet("use php@\(version)")) - completed() - } - - /** - Switching to a new PHP version involves: - - unlinking the current version - - stopping the active services - - linking the new desired version - - Please note that depending on which version is installed, - the version that is switched to may or may not be identical to `php` (without @version). - */ - public static func switchToPhpVersion( - version: String, - availableVersions: [String], - completed: @escaping () -> Void - ) { - print("Switching to \(version), unlinking all versions...") - - let group = DispatchGroup() - - availableVersions.forEach { (available) in - group.enter() - - DispatchQueue.global(qos: .userInitiated).async { - let formula = (available == App.shared.brewPhpVersion) - ? "php" : "php@\(available)" - - brew("unlink \(formula)") - brew("services stop \(formula)", sudo: true) - - group.leave() - } - } - - group.notify(queue: .global(qos: .userInitiated)) { - print("All versions have been unlinked!") - print("Linking the new version!") - - let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)" - brew("link \(formula) --overwrite --force") - brew("services start \(formula)", sudo: true) - - print("The new version has been linked!") - completed() - } - } - - // MARK: - Finding Config Files - - public static func openGenericPhpConfigFolder() - { - let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]; - NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) - } - - public static func openGlobalComposerFolder() - { - let file = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent(".composer/composer.json") - NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) - } - - public static func openPhpConfigFolder(version: String) - { - let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]; - NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) - } - - public static func openValetConfigFolder() - { - let file = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent(".config/valet") - NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) - } - - // MARK: - Quick Fix - - /** - Detects all currently available PHP versions, - and unlinks each and every one of them. - - After this, the brew services are also stopped, - the latest PHP version is linked, and php + nginx are restarted. - - If this does not solve the issue, the user may need to install additional - extensions and/or run `composer global update`. - */ - public static func fixMyPhp() - { - brew("services restart dnsmasq", sudo: true) - - detectPhpVersions().forEach { (version) in - let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)" - brew("unlink php@\(version)") - brew("services stop \(formula)") - brew("services stop \(formula)", sudo: true) - } - - brew("services stop php") - brew("services stop nginx") - brew("link php") - brew("services restart dnsmasq", sudo: true) - brew("services stop php", sudo: true) - brew("services stop nginx", sudo: true) - } - - // MARK: Common Shell Commands - - /** - Runs a `valet` command. - */ - public static func valet(_ command: String) -> String - { - return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true) - } - - /** - Runs a `brew` command. Can run as superuser. - */ - public static func brew(_ command: String, sudo: Bool = false) - { - Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)") - } - - /** - Runs `sed` in order to replace all occurrences of a string in a specific file with another. - */ - public static func sed(file: String, original: String, replacement: String) - { - // Escape slashes (or `sed` won't work) - let e_original = original.replacingOccurrences(of: "/", with: "\\/") - let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/") - - // Check if gsed exists; it is able to follow symlinks, - // which we want to do to toggle the extension - if Shell.fileExists("\(Paths.binPath)/gsed") { - Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)") - } else { - Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)") - } - } - - /** - Uses `grep` to determine whether a particular query string can be found in a particular file. - */ - public static func grepContains(file: String, query: String) -> Bool - { - return Shell.pipe(""" - grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO" - """) - .trimmingCharacters(in: .whitespacesAndNewlines) - .contains("YES") - } - -} diff --git a/phpmon/Domain/Core/App+GlobalHotkey.swift b/phpmon/Domain/Core/App+GlobalHotkey.swift index 8f4f54c..bea998e 100644 --- a/phpmon/Domain/Core/App+GlobalHotkey.swift +++ b/phpmon/Domain/Core/App+GlobalHotkey.swift @@ -20,13 +20,13 @@ extension App { func loadGlobalHotkey() { // Make sure we can retrieve the hotkey from preferences guard let hotkey = Preferences.preferences[.globalHotkey] as? String else { - print("No global hotkey was saved in preferences. None set.") + Log.info("No global hotkey was saved in preferences. None set.") return } // Make sure we can parse the JSON into the desired format guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else { - print("No global hotkey loaded, could not be parsed!") + Log.err("No global hotkey loaded, could not be parsed!") shortcutHotkey = nil return } diff --git a/phpmon/Domain/Core/App.swift b/phpmon/Domain/Core/App.swift index 73ec015..c41fbd2 100644 --- a/phpmon/Domain/Core/App.swift +++ b/phpmon/Domain/Core/App.swift @@ -8,7 +8,7 @@ import Cocoa import HotKey -class App { +class App: PhpSwitcherDelegate { // MARK: Static Vars @@ -22,14 +22,9 @@ class App { return "\(version) (\(build))" } - /** Information about the currently linked PHP installation. */ - static var phpInstall: ActivePhpInstallation? { - return App.shared.currentInstall - } - /** Whether the app is busy doing something. Used to determine what UI to display. */ static var busy: Bool { - return App.shared.busy + return PhpEnv.shared.isBusy } // MARK: Variables @@ -43,42 +38,12 @@ class App { /** The window controller of the currently active site list window. */ var siteListWindowController: SiteListWC? = nil - /** Whether the application is busy switching versions. */ - var busy: Bool = false - - /** The currently active installation of PHP. */ - var currentInstall: ActivePhpInstallation? = nil - - /** All available versions of PHP. */ - var availablePhpVersions: [String] = [] - - /** Cached information about the PHP installations. */ - var cachedPhpInstallations: [String: PhpInstallation] = [:] - /** List of detected (installed) applications that PHP Monitor can work with. */ var detectedApplications: [Application] = [] /** Timer that will periodically reload info about the user's PHP installation. */ var timer: Timer? - - /** Information we were able to discern from the Homebrew info command (as JSON). */ - var brewPhpPackage: HomebrewPackage! = nil { - didSet { - brewPhpVersion = brewPhpPackage!.version - } - } - - /** - The version that the `php` formula via Brew is aliased to on the current system. - - If you're up to date, `php` will be aliased to the latest version, - but that might not be the case. - - We'll technically default to the version in Constants.swift, but the information - should always be loaded from Homebrew itself upon startup. - */ - var brewPhpVersion: String = Constants.LatestStablePhpVersion - + // MARK: - Global Hotkey /** @@ -102,4 +67,26 @@ class App { */ var openWindows: [String] = [] + // MARK: - App Watchers + + /** + The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder. + */ + var watcher: PhpConfigWatcher! + + // MARK: - PhpSwitcherDelegate + + func switcherDidStartSwitching() { + } + + func switcherDidCompleteSwitch() { + PhpEnv.shared.currentInstall = ActivePhpInstallation() + handlePhpConfigWatcher() + + if let window = siteListWindowController { + DispatchQueue.main.async { + window.contentVC.reloadSites() + } + } + } } diff --git a/phpmon/Domain/Core/AppDelegate+InterApp.swift b/phpmon/Domain/Core/AppDelegate+InterApp.swift new file mode 100644 index 0000000..60bc11a --- /dev/null +++ b/phpmon/Domain/Core/AppDelegate+InterApp.swift @@ -0,0 +1,47 @@ +// +// AppDelegate+InterApp.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 20/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Cocoa +import Foundation + +extension AppDelegate { + + /** + This is an entry point for future development for integrating with the PHP Monitor + application URL. You can use the `phpmon://` protocol to communicate with the app. + + At this time you can trigger the site list using Alfred (or some other application) + by opening the following URL: `phpmon://list`. + + Please note that PHP Monitor needs to be running in the background for this to work. + */ + func application(_ application: NSApplication, open urls: [URL]) { + + if !Preferences.isEnabled(.allowProtocolForIntegrations) { + Log.info("Acting on commands via phpmon:// has been disabled.") + return + } + + guard let url = urls.first else { return } + + self.interpretCommand( + url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""), + commands: InterApp.getCommands() + ) + } + + private func interpretCommand(_ command: String, commands: [InterApp.Action]) { + commands.forEach { action in + if command.starts(with: action.command) { + let lastElement = String(command.split(separator: "/").last!) + action.action(lastElement) + } + } + } +} + diff --git a/phpmon/Domain/Core/AppDelegate+MenuOutlets.swift b/phpmon/Domain/Core/AppDelegate+MenuOutlets.swift index 3d13d92..123dc94 100644 --- a/phpmon/Domain/Core/AppDelegate+MenuOutlets.swift +++ b/phpmon/Domain/Core/AppDelegate+MenuOutlets.swift @@ -7,6 +7,7 @@ // import Foundation +import AppKit /** Any outlets connected to the app's main menu (not the menu that shows when the icon in @@ -24,6 +25,13 @@ extension AppDelegate { // MARK: - Menu Interactions + @IBAction func addSiteLinkPressed(_ sender: Any) { + SiteListVC.show() + + guard let windowController = App.shared.siteListWindowController else { return } + windowController.pressedAddLink(nil) + } + @IBAction func reloadSiteListPressed(_ sender: Any) { let vc = App.shared.siteListWindowController? .window?.contentViewController as? SiteListVC @@ -37,4 +45,11 @@ extension AppDelegate { } } + @IBAction func focusSearchField(_ sender: Any) { + SiteListVC.show() + + guard let windowController = App.shared.siteListWindowController else { return } + windowController.searchToolbarItem.searchField.becomeFirstResponder() + } + } diff --git a/phpmon/Domain/Core/AppDelegate+Notifications.swift b/phpmon/Domain/Core/AppDelegate+Notifications.swift index 3feff20..6337381 100644 --- a/phpmon/Domain/Core/AppDelegate+Notifications.swift +++ b/phpmon/Domain/Core/AppDelegate+Notifications.swift @@ -13,16 +13,20 @@ extension AppDelegate { // MARK: - Notifications + /** + Sets up notifications. That does mean we need to ask for permission first. + If we cannot get permission, we should log this. + */ public func setupNotifications() { let notificationCenter = UNUserNotificationCenter.current() notificationCenter.delegate = self notificationCenter.requestAuthorization(options: [.alert], completionHandler: { granted, error in if !granted { - print("PHP Monitor does not have permission to show notifications.") + Log.warn("PHP Monitor does not have permission to show notifications.") } if let error = error { - print("PHP Monitor encounted an error determining notification permissions:") - print(error) + Log.err("PHP Monitor encounted an error determining notification permissions:") + Log.err(error) } }) } diff --git a/phpmon/Domain/Core/AppDelegate.swift b/phpmon/Domain/Core/AppDelegate.swift index a963b6e..3b50045 100644 --- a/phpmon/Domain/Core/AppDelegate.swift +++ b/phpmon/Domain/Core/AppDelegate.swift @@ -44,16 +44,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele */ let valet: Valet + /** + The PhpSwitcher singleton that handles PHP version + detection, as well as switching. It is initialized + when the app is ready and passed all checks. + */ + var switcher: PhpEnv! = nil + + var logger = Log.shared + // MARK: - Initializer /** When the application initializes, create all singletons. */ override init() { - print("==================================") - print("PHP MONITOR by Nico Verbruggen") - print("Version \(App.version)") - print("==================================") + logger.verbosity = .performance + Log.info("==================================") + Log.info("PHP MONITOR by Nico Verbruggen") + Log.info("Version \(App.version)") + Log.info("==================================") self.sharedShell = Shell.user self.state = App.shared self.menu = MainMenu.shared @@ -62,6 +72,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele super.init() } + func initializeSwitcher() { + self.switcher = PhpEnv.shared + self.switcher.delegate = self.state + } + // MARK: - Lifecycle /** diff --git a/phpmon/Domain/Core/Base.lproj/Main.storyboard b/phpmon/Domain/Core/Base.lproj/Main.storyboard index 64ecce7..e65e6f7 100644 --- a/phpmon/Domain/Core/Base.lproj/Main.storyboard +++ b/phpmon/Domain/Core/Base.lproj/Main.storyboard @@ -4,6 +4,7 @@ + @@ -49,11 +50,31 @@ - + + + + + + + + + + + + + + + + + + + + + @@ -298,7 +319,7 @@ - + @@ -334,11 +355,14 @@ - + - + + + + @@ -371,6 +395,12 @@ + + + + + + @@ -391,6 +421,7 @@ + @@ -406,30 +437,199 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + - + - + - + - + @@ -442,7 +642,7 @@ - + @@ -470,7 +670,7 @@ - + @@ -478,7 +678,7 @@ - + @@ -486,10 +686,10 @@ - + - + @@ -497,59 +697,63 @@ - - - - - - - - - + + + + + + + + + - + + - - - + - - + + - + + + - @@ -564,10 +768,10 @@ - + - + @@ -599,13 +803,17 @@ - + + - + + + + diff --git a/phpmon/Domain/Core/InterAppHandler.swift b/phpmon/Domain/Core/InterAppHandler.swift new file mode 100644 index 0000000..cc8576e --- /dev/null +++ b/phpmon/Domain/Core/InterAppHandler.swift @@ -0,0 +1,64 @@ +// +// InterAppHandler.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 28/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class InterApp { + + public static var bindings: [Action] = [] + + public static func register(_ action: Action) { + self.bindings.append(action) + } + + public struct Action { + let command: String + let action: (String) -> Void + } + + static func getCommands() -> [InterApp.Action] { return [ + InterApp.Action(command: "list", action: { _ in + SiteListVC.show() + }), + InterApp.Action(command: "services/stop", action: { _ in + MainMenu.shared.stopAllServices() + }), + InterApp.Action(command: "services/restart/all", action: { _ in + MainMenu.shared.restartAllServices() + }), + InterApp.Action(command: "services/restart/nginx", action: { _ in + MainMenu.shared.restartNginx() + }), + InterApp.Action(command: "services/restart/php", action: { _ in + MainMenu.shared.restartPhpFpm() + }), + InterApp.Action(command: "services/restart/dnsmasq", action: { _ in + MainMenu.shared.restartDnsMasq() + }), + InterApp.Action(command: "locate/config", action: { _ in + MainMenu.shared.openActiveConfigFolder() + }), + InterApp.Action(command: "locate/composer", action: { _ in + MainMenu.shared.openGlobalComposerFolder() + }), + InterApp.Action(command: "locate/valet", action: { _ in + MainMenu.shared.openValetConfigFolder() + }), + InterApp.Action(command: "phpinfo", action: { _ in + MainMenu.shared.openPhpInfo() + }), + InterApp.Action(command: "switch/php/", action: { version in + if PhpEnv.shared.availablePhpVersions.contains(version) { + MainMenu.shared.switchToPhpVersion(version) + } else { + Alert.notify(message: "Unsupported version", info: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available.") + } + }), + ]} + +} diff --git a/phpmon/Domain/Core/Startup.swift b/phpmon/Domain/Core/Startup.swift index d85df5f..9fff546 100644 --- a/phpmon/Domain/Core/Startup.swift +++ b/phpmon/Domain/Core/Startup.swift @@ -6,6 +6,7 @@ // import Foundation +import AppKit class Startup { @@ -48,6 +49,21 @@ class Startup { breaking: true ) + Valet.shared.version = VersionExtractor.from(valet("--version")) + performEnvironmentCheck( + Valet.shared.version == nil, + messageText: "startup.errors.valet_version_unknown.title".localized, + informativeText: "startup.errors.valet_version_unknown.desc".localized, + breaking: true + ) + + performEnvironmentCheck( + HomebrewDiagnostics.cannotLoadService(), + messageText: "startup.errors.services_json_error.title".localized, + informativeText: "startup.errors.services_json_error.desc".localized, + breaking: true + ) + performEnvironmentCheck( !Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"), messageText: "startup.errors.sudoers_brew.title".localized, @@ -56,7 +72,7 @@ class Startup { ) performEnvironmentCheck( - // Check for Valet; it can be symlinked or in .composer/vendor/bin + // Check for Valet; it MUST be symlinked thanks to sudoers !(Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet") || Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/opt/homebrew/bin/valet") ), @@ -74,37 +90,30 @@ class Startup { ) if (!failed) { - determineBrewAliasVersion() + initializeSwitcher() + Log.info("PHP Monitor has determined the application has successfully passed all checks.") success() } } /** - * In order to avoid having to hard-code which version of PHP is aliased to what specific subversion, - * PHP Monitor now determines the alias by checking the user's system. + Because the Switcher requires various environment guarantees, the switcher is only + initialized when it is done working. */ - private func determineBrewAliasVersion() - { - print("PHP Monitor has determined the application has successfully passed all checks.") - print("Determining which version of PHP is aliased to `php` via Homebrew...") - - let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json"); - - App.shared.brewPhpPackage = try! JSONDecoder().decode( - [HomebrewPackage].self, - from: brewPhpAlias.data(using: .utf8)! - ).first! - - print("When on your system, the `php` formula means version \(App.shared.brewPhpVersion)!") + private func initializeSwitcher() { + DispatchQueue.main.async { + let appDelegate = NSApplication.shared.delegate as! AppDelegate + appDelegate.initializeSwitcher() + } } - + /** - * Perform an environment check. Will cause the application to terminate, if `breaking` is set to true. - * - * - Parameter condition: Fail condition to check for; if this returns `true`, the alert will be shown - * - Parameter messageText: Short description of what is wrong - * - Parameter informativeText: Expanded description of the environment check that failed - * - Parameter breaking: If the application should terminate afterwards + Perform an environment check. Will cause the application to terminate, if `breaking` is set to true. + + - Parameter condition: Fail condition to check for; if this returns `true`, the alert will be shown + - Parameter messageText: Short description of what is wrong + - Parameter informativeText: Expanded description of the environment check that failed + - Parameter breaking: If the application should terminate afterwards */ private func performEnvironmentCheck( _ condition: Bool, diff --git a/phpmon/Domain/Extensions/NSMenuExtension.swift b/phpmon/Domain/Extensions/NSMenuExtension.swift index adff639..9ba1a31 100644 --- a/phpmon/Domain/Extensions/NSMenuExtension.swift +++ b/phpmon/Domain/Extensions/NSMenuExtension.swift @@ -16,3 +16,14 @@ extension NSMenu { } } + +@IBDesignable class LocalizedMenuItem: NSMenuItem { + + @IBInspectable + var localizationKey: String? { + didSet { + self.title = localizationKey?.localized ?? self.title + } + } + +} diff --git a/phpmon/Domain/Helpers/Alert.swift b/phpmon/Domain/Helpers/Alert.swift index d59c2f2..897e310 100644 --- a/phpmon/Domain/Helpers/Alert.swift +++ b/phpmon/Domain/Helpers/Alert.swift @@ -59,6 +59,5 @@ class Alert { secondButtonTitle: "", style: style ) - } - + } } diff --git a/phpmon/Domain/Helpers/Application.swift b/phpmon/Domain/Helpers/Application.swift index 6632a9c..6cb2bfe 100644 --- a/phpmon/Domain/Helpers/Application.swift +++ b/phpmon/Domain/Helpers/Application.swift @@ -14,7 +14,7 @@ import Foundation class Application { enum AppType { - case editor, browser, git_gui, terminal + case editor, browser, git_gui, terminal, user_supplied } /// Name of the app. Used for display purposes and to determine `name.app` exists. @@ -40,10 +40,9 @@ class Application { /** Checks if the app is installed. */ func isInstalled() -> Bool { // If this script does not complain, the app exists! - return Shell.user.execute( + return Shell.user.executeSynchronously( "/usr/bin/open -Ra \"\(name)\"", - requiresPath: false, - waitUntilExit: true + requiresPath: false ).task.terminationStatus == 0 } diff --git a/phpmon/Domain/Helpers/Async.swift b/phpmon/Domain/Helpers/Async.swift new file mode 100644 index 0000000..515c805 --- /dev/null +++ b/phpmon/Domain/Helpers/Async.swift @@ -0,0 +1,29 @@ +// +// Async.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +/** + This generic async helper is something I'd like to use in more places. + + The `DispatchQueue.global` into `DispatchQueue.main.async` logic is common in the app. + + I could also use try `async` support which was introduced in Swift but that would + require too much refactoring at this time to consider. I also need to read up on async + in order to properly grasp all the gotchas. Looking into that later at some point. + */ +public func runAsync(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {}) +{ + DispatchQueue.global(qos: .userInitiated).async { + execute() + + DispatchQueue.main.async { + completion() + } + } +} diff --git a/phpmon/Domain/Helpers/LocalNotification.swift b/phpmon/Domain/Helpers/LocalNotification.swift index 8bf0da2..0c391a3 100644 --- a/phpmon/Domain/Helpers/LocalNotification.swift +++ b/phpmon/Domain/Helpers/LocalNotification.swift @@ -25,7 +25,7 @@ class LocalNotification { let notificationCenter = UNUserNotificationCenter.current() notificationCenter.add(request) { (error) in if error != nil { - print(error!) + Log.err(error!) } } } diff --git a/phpmon/Domain/Helpers/MenuBarImageGenerator.swift b/phpmon/Domain/Helpers/MenuBarImageGenerator.swift index ee95f99..21621a7 100644 --- a/phpmon/Domain/Helpers/MenuBarImageGenerator.swift +++ b/phpmon/Domain/Helpers/MenuBarImageGenerator.swift @@ -65,6 +65,9 @@ class MenuBarImageGenerator { return targetImage } + /** + The same as before, but also attempts to add an icon to the left. + */ public static func textToImageWithIcon(text: String) -> NSImage { let textImage = self.textToImage(text: text) let iconImage = NSImage(named: "StatusBarPHP")! diff --git a/phpmon/Domain/Helpers/PMWindowController.swift b/phpmon/Domain/Helpers/PMWindowController.swift index 3165a36..9b26a14 100644 --- a/phpmon/Domain/Helpers/PMWindowController.swift +++ b/phpmon/Domain/Helpers/PMWindowController.swift @@ -25,6 +25,18 @@ class PMWindowController: NSWindowController, NSWindowDelegate { App.shared.register(window: windowName) } + func windowWillClose(_ notification: Notification) { + App.shared.remove(window: windowName) + } + + deinit { + Log.perf("Window controller '\(windowName)' was deinitialized") + } + +} + +extension NSWindowController { + public func positionWindowInTopLeftCorner() { guard let frame = NSScreen.main?.frame else { return } guard let window = self.window else { return } @@ -37,12 +49,4 @@ class PMWindowController: NSWindowController, NSWindowDelegate { ), display: true) } - func windowWillClose(_ notification: Notification) { - App.shared.remove(window: windowName) - } - - deinit { - print("Window controller '\(windowName)' was deinitialized") - } - } diff --git a/phpmon/Domain/Helpers/VersionExtractor.swift b/phpmon/Domain/Helpers/VersionExtractor.swift index 81bb05b..69abe9b 100644 --- a/phpmon/Domain/Helpers/VersionExtractor.swift +++ b/phpmon/Domain/Helpers/VersionExtractor.swift @@ -10,28 +10,35 @@ import Foundation class VersionExtractor { + /** + This attempts to extract the version number from the command line output of Valet. + */ public static func from(_ string: String) -> String? { - let regex = try! NSRegularExpression( - pattern: #"Laravel Valet (?(\d+)(.)(\d+)((.)(\d+))?)"#, - options: [] - ) - - let match = regex.matches( - in: string, - options: [], - range: NSMakeRange(0, string.count) - ).first - - guard let match = match else { + do { + let regex = try NSRegularExpression( + pattern: #"Laravel Valet (?(\d+)(.)(\d+)((.)(\d+))?)"#, + options: [] + ) + + let match = regex.matches( + in: string, + options: [], + range: NSMakeRange(0, string.count) + ).first + + guard let match = match else { + return nil + } + + let range = Range( + match.range(withName: "version"), + in: string + )! + + return String(string[range]) + } catch { return nil } - - let range = Range( - match.range(withName: "version"), - in: string - )! - - return String(string[range]) } } diff --git a/phpmon/Domain/Integrations/Composer/ComposerJson.swift b/phpmon/Domain/Integrations/Composer/ComposerJson.swift new file mode 100644 index 0000000..30f0b10 --- /dev/null +++ b/phpmon/Domain/Integrations/Composer/ComposerJson.swift @@ -0,0 +1,78 @@ +// +// ComposerJson.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 04/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +/** + This `Decodable` class is used to directly map `composer.json` + to this object. + */ +struct ComposerJson: Decodable { + + // MARK: - JSON structure + + let dependencies: Dictionary? + let devDependencies: Dictionary? + let configuration: Config? + + private enum CodingKeys: String, CodingKey { + case dependencies = "require" + case devDependencies = "require-dev" + case configuration = "config" + } + + struct Config: Decodable { + let platform: Platform? + } + struct Platform: Decodable { + let php: String? + } + + // MARK: - Helpers + + /** + Checks what the PHP version constraint is. + Returns a tuple (constraint, location of constraint). + */ + public func getPhpVersion() -> (String, String) { + // Check if in platform + if configuration?.platform?.php != nil { + return (configuration!.platform!.php!, "platform") + } + + // Check if in dependencies + if dependencies?["php"] != nil { + return (dependencies!["php"]!, "require") + } + + // Unknown! + return ("???", "unknown") + } + + /** + Checks if any notable dependencies can be resolved. + Only notable dependencies are saved. + */ + public func getNotableDependencies() -> [String: String] { + var notable: [String: String] = [:] + + var scan = Array(PhpFrameworks.DependencyList.keys) + scan.append("php") + + scan.forEach { dependency in + if dependencies?[dependency] != nil { + notable[dependency] = dependencies![dependency] + } + } + + return notable + } + +} + + diff --git a/phpmon/Domain/Integrations/Composer/PhpFrameworks.swift b/phpmon/Domain/Integrations/Composer/PhpFrameworks.swift new file mode 100644 index 0000000..028c706 --- /dev/null +++ b/phpmon/Domain/Integrations/Composer/PhpFrameworks.swift @@ -0,0 +1,82 @@ +// +// Frameworks.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 26/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +struct PhpFrameworks { + + /** + This list should probably be reversed when checked, because some of these + will also require either `laravel/framework` or `symfony/symfony`. + */ + public static let DependencyList = [ + + // COMMON FRAMEWORKS + "laravel/framework" : "Laravel", + "symfony/symfony": "Symfony", + "laravel/lumen": "Lumen", + + // VARIOUS CMS + "roots/bedrock": "Bedrock", + "cakephp/app": "CakePHP", + "craftcms/craft": "Craft", + "drupal/core": "Drupal", + "flarum/core": "Flarum", + "tightenco/jigsaw": "Jigsaw", + "joomla/uri": "Joomla", + "themsaid/katana": "Katana", + "getkirby/cms": "Kirby", + "october/october": "OctoberCMS", + "sculpin/sculpin": "Sculpin", + "statamic/cms": "Statamic", + "johnpbloch/wordpress-core": "WordPress", + "zendframework/zendframework": "Zend", + "zendframework/zend-mvc": "Zend" + + // TODO (5.1): Handle these in v5.1 + // "magento/*": "Magento", + // "concrete5/*": "Concrete5", + // "contao/*": "Contao", + // "slim/*": "Slim", + ] + + public static let FileMapping: [String: [String]] = [ + "Drupal": [ + // Legacy installations + "/misc/drupal.js", + "/core/lib/Drupal.php", + // The default (new) installation w/ Composer puts the modules in /web + "/web/misc/drupal.js", + "/web/core/lib/Drupal.php" + ], + "WordPress": [ + "/wp-config.php", + "/wp-config-sample.php" + ], + ] + + /** + There are two cases where users are unlikely to use `composer`, + when setting up a Drupal or a WordPress project. For performance + reasons, we only check that here! + */ + public static func detectFallbackDependency(_ basePath: String) -> String? { + for entry in Self.FileMapping { + let found = entry.value + .map { path in return Filesystem.fileExists(basePath + path) } + .contains(true) + + if found { + return entry.key + } + } + + return nil + } + +} diff --git a/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift b/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift index fd327a8..0ec9eb6 100644 --- a/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift +++ b/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift @@ -10,19 +10,6 @@ import Foundation class HomebrewDiagnostics { - enum Errors: String { - case aliasConflict = "alias_conflict" - } - - static let shared = HomebrewDiagnostics() - var errors: [HomebrewDiagnostics.Errors] = [] - - init() { - if determineAliasConflicts() { - errors.append(.aliasConflict) - } - } - /** It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated. This will then result in two different aliases claiming to point to the same formula (`php`). @@ -30,41 +17,58 @@ class HomebrewDiagnostics { This check only needs to be performed if the `shivammathur/php` tap is active. */ - public func determineAliasConflicts() -> Bool + public static func hasAliasConflict() -> Bool { let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json") if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") { - print("The user does not appear to have tapped: shivammathur/php") + Log.info("The user does not appear to have tapped: shivammathur/php") return false } else { - print("The user DOES have the following tapped: shivammathur/php") - print("Checking for `php` formula conflicts...") + Log.info("The user DOES have the following tapped: shivammathur/php") + Log.info("Checking for `php` formula conflicts...") let tapPhp = try! JSONDecoder().decode( [HomebrewPackage].self, from: tapAlias.data(using: .utf8)! ).first! - if tapPhp.version != App.shared.brewPhpVersion { - print("The `php` formula alias seems to be the different between the tap and core. This could be a problem!") - print("Determining whether both of these versions are installed...") + if tapPhp.version != PhpEnv.brewPhpVersion { + Log.warn("The `php` formula alias seems to be the different between the tap and core. This could be a problem!") + Log.info("Determining whether both of these versions are installed...") - let bothInstalled = App.shared.availablePhpVersions.contains(tapPhp.version) - && App.shared.availablePhpVersions.contains(App.shared.brewPhpVersion) + let bothInstalled = PhpEnv.shared.availablePhpVersions.contains(tapPhp.version) + && PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) if bothInstalled { - print("Both conflicting aliases seem to be installed, warning the user!") + Log.warn("Both conflicting aliases seem to be installed, warning the user!") } else { - print("Conflicting aliases are not both installed, seems fine!") + Log.info("Conflicting aliases are not both installed, seems fine!") } return bothInstalled } - print("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).") + Log.info("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).") return false } } + + /** + In order to see if we support the --json syntax, we'll query nginx. + If the JSON response cannot be parsed, Homebrew is probably out of date. + */ + public static func cannotLoadService(_ name: String = "nginx") -> Bool + { + let serviceInfo = try? JSONDecoder().decode( + [HomebrewService].self, + from: Shell.pipe( + "sudo \(Paths.brew) services info \(name) --json", + requiresPath: true + ).data(using: .utf8)! + ) + + return serviceInfo == nil + } } diff --git a/phpmon/Domain/Integrations/Valet/Valet.swift b/phpmon/Domain/Integrations/Valet/Valet.swift index 6910758..8001ef9 100644 --- a/phpmon/Domain/Integrations/Valet/Valet.swift +++ b/phpmon/Domain/Integrations/Valet/Valet.swift @@ -13,58 +13,84 @@ class Valet { static let shared = Valet() /// The version of Valet that was detected. - var version: String + var version: String! = nil /// The Valet configuration file. - var config: Valet.Configuration + var config: Valet.Configuration! /// A cached list of sites that were detected after analyzing the paths set up for Valet. var sites: [Site] = [] + /// Whether we're busy with some blocking operation. + var isBusy: Bool = false + + /// When initialising the Valet singleton, extract the Valet version and assume no sites loaded. init() { - version = VersionExtractor.from(Actions.valet("--version")) - ?? "UNKNOWN" - + self.version = nil + self.sites = [] + } + + /** + We don't want to load the initial config.json file as soon as the class is initialised. + Instead, we'll defer the loading of the configuration file once the initial app checks + have passed: if the user does not have Valet installed, we'll crash the app because we + force unwrap the file. Currently, this does also mean that if the JSON is invalid or + incompatible with the `Decodable` `Valet.Configuration` class, that the app will crash. + */ + public func loadConfiguration() { let file = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent(".config/valet/config.json") + // TODO: (5.1) Fix loading of invalid JSON: do not crash the app config = try! JSONDecoder().decode( Valet.Configuration.self, from: try! String(contentsOf: file, encoding: .utf8).data(using: .utf8)! ) - - self.sites = [] } + /** + Starts the preload of sites, but only if the maximum amount of sites is 30. + For users with more sites, the site list is loaded when they bring up the site list window. + (This is done to keep the startup speed as fast as possible.) + */ public func startPreloadingSites() { - let maximumPreload = 10 + let maximumPreload = 30 let foundSites = self.countPaths() if foundSites <= maximumPreload { // Preload the sites and their drivers - print("Fewer than or \(maximumPreload) sites found, preloading list of sites...") + Log.info("Fewer than or \(maximumPreload) sites found, preloading list of sites...") self.reloadSites() } else { - print("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!") + Log.info("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!") } } + /** + Reloads the list of sites, assuming that the list isn't being reloaded at the time. + We don't want to do duplicate or parallel work! + */ public func reloadSites() { + if (isBusy) { + return + } + resolvePaths(tld: config.tld) } + /** + Checks if the version of Valet is more recent than the minimum version required for PHP Monitor to function. + Should this procedure fail, the user will get an alert notifying them that the version of Valet they have + installed is not recent enough. + */ public func validateVersion() -> Void { - if version == "UNKNOWN" { - return print("The Valet version could not be extracted... that does not bode well.") - } - if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending { let version = version - print("Valet version \(version) is too old! (recommended: \(Constants.MinimumRecommendedValetVersion))") + Log.warn("Valet version \(version!) is too old! (recommended: \(Constants.MinimumRecommendedValetVersion))") DispatchQueue.main.async { - Alert.notify(message: "alert.min_valet_version.title".localized, info: "alert.min_valet_version.info".localized(version, Constants.MinimumRecommendedValetVersion)) + Alert.notify(message: "alert.min_valet_version.title".localized, info: "alert.min_valet_version.info".localized(version!, Constants.MinimumRecommendedValetVersion)) } } else { - print("Valet version \(version) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))") + Log.info("Valet version \(version!) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))") } } @@ -88,6 +114,8 @@ class Valet { Resolves all paths and creates linked or parked site instances that can be referenced later. */ private func resolvePaths(tld: String) { + isBusy = true + sites = [] for path in config.paths { @@ -96,6 +124,10 @@ class Valet { resolvePath(entry, forPath: path, tld: tld) } } + + sites = sites.sorted { $0.absolutePath < $1.absolutePath } + + isBusy = false } /** @@ -151,6 +183,13 @@ class Valet { /// The absolute path to the directory that is served. var absolutePath: String! + /// The absolute path to the directory that is served, + /// replacing the user's home folder with ~. + lazy var absolutePathRelative: String = { + return self.absolutePath + .replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~") + }() + /// Location of the alias. If set, this is a linked domain. var aliasPath: String? @@ -160,6 +199,21 @@ class Valet { /// What driver is currently in use. If not detected, defaults to nil. var driver: String? = nil + /// Whether the driver was determined by checking the Composer file. + var driverDeterminedByComposer: Bool = false + + /// A list of notable Composer dependencies. + var notableComposerDependencies: [String: String] = [:] + + /// The PHP version as discovered in `composer.json`. + var composerPhp: String = "???" + + /// Check whether the PHP version is valid for the currently linked version. + var composerPhpCompatibleWithLinked: Bool = false + + /// How the PHP version was determined. + var composerPhpSource: String = "unknown" + init() {} convenience init(absolutePath: String, tld: String) { @@ -168,6 +222,7 @@ class Valet { self.name = URL(fileURLWithPath: absolutePath).lastPathComponent self.aliasPath = nil determineSecured(tld) + determineComposerPhpVersion() determineDriver() } @@ -177,18 +232,93 @@ class Valet { self.name = URL(fileURLWithPath: aliasPath).lastPathComponent self.aliasPath = aliasPath determineSecured(tld) + determineComposerPhpVersion() determineDriver() } + /** + Checks if a certificate file can be found in the `valet/Certificates` directory. + - Note: The file is not validated, only its presence is checked. + */ public func determineSecured(_ tld: String) { secured = Shell.fileExists("~/.config/valet/Certificates/\(self.name!).\(tld).key") } + /** + Checks if `composer.json` exists in the folder, and extracts notable information: + + - The PHP version required (the constraint, so it could be `^8.0`, for example) + - Where the PHP version was found (`require` or `platform`) + - Notable PHP dependencies (determined via `PhpFrameworks.DependencyList`) + + The method then also checks if the determined constraint (if found) is compatible + with the currently linked version of PHP (see `composerPhpMatchesSystem`). + */ + public func determineComposerPhpVersion() { + let path = "\(absolutePath!)/composer.json" + + do { + if Filesystem.fileExists(path) { + let decoded = try JSONDecoder().decode( + ComposerJson.self, + from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)! + ) + + (self.composerPhp, self.composerPhpSource) = decoded.getPhpVersion() + self.notableComposerDependencies = decoded.getNotableDependencies() + } + } catch { + Log.err("Something went wrong reading the composer JSON file.") + } + + if self.composerPhp == "???" { + return + } + + // Split the composer list (on "|") to evaluate multiple constraints + // 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]) + .matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines)) + .count > 0 + }.contains(true) + } + + /** + Determine the driver to be displayed in the list of sites. In v5.0, this has been changed + to load the "framework" or "project type" instead. + */ public func determineDriver() { + self.determineDriverViaComposer() + + if self.driver == nil { + self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath) + } + } + + /** + Check the dependency list and see if a particular dependency can't be found. + We'll revert the dependency list so that Laravel and Symfony are detected last. + + (Some other frameworks might use Laravel, so if we found it first the detection would be incorrect: + this would happen with Statamic, for example.) + */ + private func determineDriverViaComposer() { + self.driverDeterminedByComposer = true + + PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in + if self.notableComposerDependencies.keys.contains(key) { + self.driver = value + } + } + } + + @available(*, deprecated, renamed: "determineDriver") + private func determineDriverViaValet() { let driver = Shell.pipe("cd '\(absolutePath!)' && valet which", requiresPath: true) if driver.contains("This site is served by") { self.driver = driver - // TODO: Use a regular expression to retrieve the driver instead? .replacingOccurrences(of: "This site is served by [", with: "") .replacingOccurrences(of: "ValetDriver].\n", with: "") } else { @@ -205,8 +335,8 @@ class Valet { /// The paths that need to be checked. let paths: [String] - /// The loopback address. - let loopback: String + /// The loopback address. Optional. + let loopback: String? /// The default site that is served if the domain is not found. Optional. let defaultSite: String? diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift new file mode 100644 index 0000000..66ab435 --- /dev/null +++ b/phpmon/Domain/Menu/MainMenu+Startup.swift @@ -0,0 +1,118 @@ +// +// MainMenu+Startup.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 03/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Cocoa + +extension MainMenu { + /** + Kick off the startup of the rendering of the main menu. + */ + func startup() { + // Start with the icon + setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) + + // Perform environment boot checks + DispatchQueue.global(qos: .userInitiated).async { [unowned self] in + Startup().checkEnvironment(success: { onEnvironmentPass() }, + failure: { onEnvironmentFail() } + ) + } + } + + /** + When the environment is all clear and the app can run, let's go. + */ + private func onEnvironmentPass() { + PhpEnv.detectPhpVersions() + + if HomebrewDiagnostics.hasAliasConflict() { + DispatchQueue.main.async { + Alert.notify( + message: "alert.php_alias_conflict.title".localized, + info: "alert.php_alias_conflict.info".localized, + style: .critical + ) + } + } + + updatePhpVersionInStatusBar() + + Log.info("Determining broken PHP-FPM...") + // Attempt to find out if PHP-FPM is broken + let installation = PhpEnv.phpInstall + installation.notifyAboutBrokenPhpFpm() + + // Set up the config watchers on launch (these are automatically updated via delegate methods if the user switches) + Log.info("Setting up watchers...") + App.shared.handlePhpConfigWatcher() + + // Detect applications (preset + custom) + Log.info("Detecting applications...") + App.shared.detectedApplications = Application.detectPresetApplications() + let customApps = Preferences.custom.scanApps.map { appName in + return Application(appName, .user_supplied) + }.filter { app in + return app.isInstalled() + } + App.shared.detectedApplications.append(contentsOf: customApps) + let appNames = App.shared.detectedApplications.map { app in + return app.name + } + Log.info("Detected applications: \(appNames)") + + // Load the global hotkey + App.shared.loadGlobalHotkey() + + // Attempt to find out more info about Valet + if Valet.shared.version != nil { + Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)") + } + + Valet.shared.loadConfiguration() + Valet.shared.validateVersion() + Valet.shared.startPreloadingSites() + + NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil) + + Log.info("PHP Monitor is ready to serve!") + + // Schedule a request to fetch the PHP version every 60 seconds + DispatchQueue.main.async { [self] in + App.shared.timer = Timer.scheduledTimer( + timeInterval: 60, + target: self, + selector: #selector(refreshActiveInstallation), + userInfo: nil, + repeats: true + ) + } + + Stats.incrementSuccessfulLaunchCount() + Stats.evaluateSponsorMessageShouldBeDisplayed() + } + + /** + When the environment is not OK, present an alert to inform the user. + */ + private func onEnvironmentFail() { + DispatchQueue.main.async { [self] in + let close = Alert.present( + messageText: "alert.cannot_start.title".localized, + informativeText: "alert.cannot_start.info".localized, + buttonTitle: "alert.cannot_start.close".localized, + secondButtonTitle: "alert.cannot_start.retry".localized + ) + + if (close) { + exit(1) + } + + startup() + } + } +} diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index 0a6936c..7f09176 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -22,97 +22,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { // MARK: - UI related - /** - Kick off the startup of the rendering of the main menu. - */ - func startup() { - // Start with the icon - setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) - - // Perform environment boot checks - DispatchQueue.global(qos: .userInitiated).async { [unowned self] in - Startup().checkEnvironment(success: { onEnvironmentPass() }, - failure: { onEnvironmentFail() } - ) - } - } - - /** - When the environment is all clear and the app can run, let's go. - */ - private func onEnvironmentPass() { - _ = Actions.detectPhpVersions() - - if HomebrewDiagnostics.shared.errors.contains(.aliasConflict) { - DispatchQueue.main.async { - Alert.notify( - message: "alert.php_alias_conflict.title".localized, - info: "alert.php_alias_conflict.info".localized, - style: .critical - ) - } - } - - updatePhpVersionInStatusBar() - - print("Determining broken PHP-FPM...") - // Attempt to find out if PHP-FPM is broken - let installation = App.phpInstall! - installation.notifyAboutBrokenPhpFpm() - - print("Detecting applications...") - // Attempt to load list of applications - App.shared.detectedApplications = Application.detectPresetApplications() - let appNames = App.shared.detectedApplications.map { app in - return app.name - } - print("Detected applications: \(appNames)") - - // Load the global hotkey - App.shared.loadGlobalHotkey() - - // Attempt to find out more info about Valet - print("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version)") - Valet.shared.validateVersion() - Valet.shared.startPreloadingSites() - print("PHP Monitor is ready to serve!") - - // Schedule a request to fetch the PHP version every 60 seconds - DispatchQueue.main.async { [self] in - App.shared.timer = Timer.scheduledTimer( - timeInterval: 60, - target: self, - selector: #selector(updatePhpVersionInStatusBar), - userInfo: nil, - repeats: true - ) - } - } - - /** - When the environment is not OK, present an alert to inform the user. - */ - private func onEnvironmentFail() { - DispatchQueue.main.async { [self] in - let close = Alert.present( - messageText: "alert.cannot_start.title".localized, - informativeText: "alert.cannot_start.info".localized, - buttonTitle: "alert.cannot_start.close".localized, - secondButtonTitle: "alert.cannot_start.retry".localized - ) - - if (close) { - exit(1) - } - - startup() - } - } - /** Update the menu's contents, based on what's going on. + This will rebuild the entire menu, so this can take a few moments. */ - func update() { + func rebuild() { // Update the menu item on the main thread DispatchQueue.main.async { [self] in // Create a new menu @@ -131,10 +45,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { menu.addItem(NSMenuItem.separator()) // Add services - menu.addServicesMenuItems() - menu.addItem(NSMenuItem.separator()) - - // Add information about services & actions menu.addPhpConfigurationMenuItems() menu.addItem(NSMenuItem.separator()) @@ -158,7 +68,9 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { */ func setStatusBarImage(version: String) { setStatusBar( - image: MenuBarImageGenerator.textToImageWithIcon(text: version) + image: Preferences.isEnabled(.shouldDisplayPhpHintInIcon) + ? MenuBarImageGenerator.textToImageWithIcon(text: version) + : MenuBarImageGenerator.textToImage(text: version) ) } @@ -185,16 +97,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { */ private func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {}) { - App.shared.busy = true + PhpEnv.shared.isBusy = true setBusyImage() DispatchQueue.global(qos: .userInitiated).async { [unowned self] in - update() execute() - App.shared.busy = false + PhpEnv.shared.isBusy = false DispatchQueue.main.async { [self] in + PhpEnv.shared.currentInstall = ActivePhpInstallation() updatePhpVersionInStatusBar() - update() + NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil) completion() } } @@ -202,10 +114,18 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { // MARK: - User Interface + @objc func refreshActiveInstallation() { + if !PhpEnv.shared.isBusy { + PhpEnv.shared.currentInstall = ActivePhpInstallation() + updatePhpVersionInStatusBar() + } else { + Log.perf("Skipping version refresh due to busy status") + } + } + @objc func updatePhpVersionInStatusBar() { - App.shared.currentInstall = ActivePhpInstallation() refreshIcon() - update() + rebuild() } func refreshIcon() { @@ -219,22 +139,23 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { } else { // The dynamic icon has been requested let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool - setStatusBarImage(version: long ? App.phpInstall!.version.long : App.phpInstall!.version.short) + setStatusBarImage(version: long ? PhpEnv.phpInstall.version.long : PhpEnv.phpInstall.version.short) } } } } + @objc func reloadPhpMonitorMenuInBackground() { + waitAndExecute { + // This automatically reloads the menu + Log.info("Reloading information about the PHP installation (in the background)...") + } + } + @objc func reloadPhpMonitorMenu() { waitAndExecute { // This automatically reloads the menu - print("Reloading information about the PHP installation...") - } completion: { - // Add a slight delay to make sure it loads the new menu - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - // Open the menu again - MainMenu.shared.statusItem.button?.performClick(nil) - } + Log.info("Reloading information about the PHP installation...") } } @@ -296,46 +217,59 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { waitAndExecute { sender.phpExtension?.toggle() - if Preferences.preferences[.autoServiceRestartAfterExtensionToggle] as! Bool == true { + if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) { Actions.restartPhpFpm() } } } @objc func openPhpInfo() { + var url: URL? = nil + waitAndExecute { - // Write a file called `phpmon_phpinfo.php` to /tmp - try! " /tmp/phpmon_phpinfo.html") + url = Actions.createTempPhpInfoFile() } completion: { // When this has been completed, open the URL to the file in the browser - NSWorkspace.shared.open(URL(string: "file:///private/tmp/phpmon_phpinfo.html")!) + NSWorkspace.shared.open(url!) } } - @objc func forceRestartLatestPhp() { + @objc func fixMyValet() { // Tell the user the switch is about to occur - Alert.notify(message: "alert.force_reload.title".localized, info: "alert.force_reload.info".localized) - - // Start switching - waitAndExecute { - Actions.fixMyPhp() - } completion: { - Alert.notify(message: "alert.force_reload_done.title".localized, info: "alert.force_reload_done.info".localized) + if Alert.present( + messageText: "alert.fix_my_valet.title".localized, + informativeText: "alert.fix_my_valet.info".localized(PhpEnv.brewPhpVersion), + buttonTitle: "alert.fix_my_valet.ok".localized, + secondButtonTitle: "alert.fix_my_valet.cancel".localized, + style: .warning + ) { + // Start the fix + waitAndExecute { + Actions.fixMyValet() + } completion: { + Alert.notify( + message: "alert.fix_my_valet_done.title".localized, + info: "alert.fix_my_valet_done.info".localized + ) + } + } else { + Log.info("The user has chosen to abort Fix My Valet") } } + @objc func updateGlobalComposerDependencies() { + self.updateGlobalDependencies(notify: true, completion: { _ in }) + } + @objc func openActiveConfigFolder() { - if (App.phpInstall!.version.error) { + if (PhpEnv.phpInstall.version.error) { // php version was not identified Actions.openGenericPhpConfigFolder() return } // php version was identified - Actions.openPhpConfigFolder(version: App.phpInstall!.version.short) + Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short) } @objc func openGlobalComposerFolder() { @@ -347,53 +281,70 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { } @objc func switchToPhpVersion(sender: PhpMenuItem) { + self.switchToPhpVersion(sender.version) + } + + // TODO (5.1): Investigate if `waitAndExecute` cannot be used here + @objc func switchToPhpVersion(_ version: String) { setBusyImage() - App.shared.busy = true + PhpEnv.shared.isBusy = true DispatchQueue.global(qos: .userInitiated).async { [unowned self] in // Update the PHP version in the status bar updatePhpVersionInStatusBar() // Update the menu - update() + rebuild() + let sendLocalNotification = { + LocalNotification.send( + title: String(format: "notification.version_changed_title".localized, version), + subtitle: String(format: "notification.version_changed_desc".localized, version) + ) + PhpEnv.phpInstall.notifyAboutBrokenPhpFpm() + } + let completion = { + // Fire off the delegate method + PhpEnv.shared.delegate?.switcherDidCompleteSwitch() + // Mark as no longer busy - App.shared.busy = false + PhpEnv.shared.isBusy = false // Perform UI updates on main thread DispatchQueue.main.async { [self] in updatePhpVersionInStatusBar() - update() + rebuild() - // Send a notification that the switch has been completed - LocalNotification.send( - title: String(format: "notification.version_changed_title".localized, sender.version), - subtitle: String(format: "notification.version_changed_desc".localized, sender.version) - ) + if !PhpEnv.shared.validate(version) { + let outcome = Alert.present( + messageText: "alert.php_switch_failed.title".localized(version), + informativeText: "alert.php_switch_failed.info".localized(version), + buttonTitle: "alert.php_switch_failed.confirm".localized, + secondButtonTitle: "alert.php_switch_failed.cancel".localized, style: .informational) + if outcome { + MainMenu.shared.fixMyValet() + } + return + } - App.phpInstall?.notifyAboutBrokenPhpFpm() + // Run composer updates + if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) { + self.updateGlobalDependencies(notify: false, completion: { _ in sendLocalNotification() }) + } else { + sendLocalNotification() + } + + // Update stats + Stats.incrementSuccessfulSwitchCount() + Stats.evaluateSponsorMessageShouldBeDisplayed() } } - /* DISABLED UNTIL VALET SWITCHING IS OK (see #34) - if Preferences.preferences[.useInternalSwitcher] as! Bool == false { - // 1. Default switcher using Valet - // Will cause less issues, but is slower - Actions.switchToPhpVersionUsingValet( - version: sender.version, - availableVersions: App.shared.availablePhpVersions, - completed: completion - ) - } else { */ - // 2. Custom switcher (internal) - // Will cause more issues with Homebrew and is faster - Actions.switchToPhpVersion( - version: sender.version, - availableVersions: App.shared.availablePhpVersions, - completed: completion - ) - /* } */ + PhpEnv.switcher.performSwitch( + to: version, + completion: completion + ) } } @@ -410,6 +361,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { SiteListVC.show() } + @objc func openDonate() { + NSWorkspace.shared.open(Constants.DonationUrl) + } + @objc func terminateApp() { NSApplication.shared.terminate(nil) } @@ -425,4 +380,91 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { // When the menu is closed, allow the shortcut to work again App.shared.shortcutHotkey?.isPaused = false } + + // MARK: - Private Methods + + /** + Updates the global dependencies and runs the completion callback when done. + */ + private func updateGlobalDependencies(notify: Bool, completion: @escaping (Bool) -> Void) { + if !Shell.fileExists("/usr/local/bin/composer") { + Alert.notify( + message: "alert.composer_missing.title".localized, + info: "alert.composer_missing.info".localized + ) + return + } + + PhpEnv.shared.isBusy = true + setBusyImage() + self.rebuild() + + let noLongerBusy = { + PhpEnv.shared.isBusy = false + DispatchQueue.main.async { [self] in + self.updatePhpVersionInStatusBar() + self.rebuild() + } + } + + var window: ProgressWindowController? = ProgressWindowController.display( + title: "alert.composer_progress.title".localized, + description: "alert.composer_progress.info".localized + ) + window?.setType(info: true) + + DispatchQueue.global(qos: .userInitiated).async { + let task = Shell.user.createTask( + for: "/usr/local/bin/composer global update", requiresPath: true + ) + + DispatchQueue.main.async { + window?.addToConsole("/usr/local/bin/composer global update\n") + } + + Shell.captureOutput( + task, + didReceiveStdOutData: { string in + DispatchQueue.main.async { + window?.addToConsole(string) + } + Log.perf("\(string.trimmingCharacters(in: .newlines))") + }, + didReceiveStdErrData: { string in + DispatchQueue.main.async { + window?.addToConsole(string) + } + Log.perf("\(string.trimmingCharacters(in: .newlines))") + } + ) + + task.launch() + task.waitUntilExit() + Shell.haltCapturingOutput(task) + + DispatchQueue.main.async { + if task.terminationStatus <= 0 { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + window?.close() + if (notify) { + LocalNotification.send( + title: "alert.composer_success.title".localized, + subtitle: "alert.composer_success.info".localized + ) + } + window = nil + noLongerBusy() + completion(true) + } + } else { + window?.setType(info: false) + window?.progressView?.labelTitle.stringValue = "alert.composer_failure.title".localized + window?.progressView?.labelDescription.stringValue = "alert.composer_failure.info".localized + window = nil + noLongerBusy() + completion(false) + } + } + } + } } diff --git a/phpmon/Domain/Menu/ServicesView.swift b/phpmon/Domain/Menu/ServicesView.swift new file mode 100644 index 0000000..f879f4e --- /dev/null +++ b/phpmon/Domain/Menu/ServicesView.swift @@ -0,0 +1,114 @@ +// +// StatsView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 04/02/2021. +// Copyright © 2021 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 + } + + override func viewWillDraw() { + super.viewWillDraw() + self.loadData() + } + + @objc func updateInformation() { + self.loadData() + } + + // TODO: (5.1) Move data fetching, caching & retrieval somewhere else + func loadData() { + // Use stale data + self.applyAllInfoFieldsFromCachedValue() + + // Re-fetch services + runAsync { + let servicesList = try! JSONDecoder().decode( + [HomebrewService].self, + from: Shell.pipe( + "sudo \(Paths.brew) services info --all --json", + requiresPath: true + ).data(using: .utf8)! + ).filter({ service in + return [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"].contains(service.name) + }) + + ServicesView.services = Dictionary(uniqueKeysWithValues: servicesList.map{ ($0.name, $0) }) + } completion: { + // Use fresh data + 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) + } +} diff --git a/phpmon/Domain/Menu/ServicesView.xib b/phpmon/Domain/Menu/ServicesView.xib new file mode 100644 index 0000000..c1371ff --- /dev/null +++ b/phpmon/Domain/Menu/ServicesView.xib @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpmon/Domain/Menu/StatsView.xib b/phpmon/Domain/Menu/StatsView.xib index 1ad4bd9..fd6bca0 100644 --- a/phpmon/Domain/Menu/StatsView.xib +++ b/phpmon/Domain/Menu/StatsView.xib @@ -1,8 +1,8 @@ - + - + @@ -10,11 +10,11 @@ - + - + @@ -75,10 +75,10 @@ - + - + @@ -86,7 +86,7 @@ - + @@ -138,7 +138,7 @@ - + diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index 5f4662c..56ae048 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -9,18 +9,14 @@ import Cocoa class StatusMenu : NSMenu { func addPhpVersionMenuItems() { - if App.shared.currentInstall == nil { - return - } - - if App.phpInstall!.version.error { + if PhpEnv.phpInstall.version.error { for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] { addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: "")) } return } - let phpVersionText = "\("mi_php_version".localized) \(App.phpInstall!.version.long)" + let phpVersionText = "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)" addItem(HeaderView.asMenuItem(text: phpVersionText)) } @@ -30,47 +26,56 @@ class StatusMenu : NSMenu { return } - if App.shared.availablePhpVersions.count == 0 { + if PhpEnv.shared.availablePhpVersions.count == 0 { return } self.addSwitchToPhpMenuItems() self.addItem(NSMenuItem.separator()) + + self.addItem(ServicesView.asMenuItem()) + self.addItem(NSMenuItem.separator()) } - func addServicesMenuItems() { - self.addItem(HeaderView.asMenuItem(text: "mi_active_services".localized)) - - let services = NSMenuItem(title: "mi_manage_services".localized, action: nil, keyEquivalent: "") + func addOtherMenuItems() { + let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "") let servicesMenu = NSMenu() - 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_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"), - withKeyModifier: [.command, .shift]) - servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s")) - for item in servicesMenu.items { - item.target = MainMenu.shared - } - self.setSubmenu(servicesMenu, for: services) - self.addForceLoadLatestVersion() - self.addItem(services) - - } - - func addForceLoadLatestVersion() { - if !App.shared.availablePhpVersions.contains(App.shared.brewPhpVersion) { - self.addItem(NSMenuItem( - title: "mi_force_load_latest_unavailable".localized(App.shared.brewPhpVersion), + servicesMenu.addItem(NSMenuItem(title: "mi_help".localized, action: nil, keyEquivalent: "")) + + if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) { + servicesMenu.addItem(NSMenuItem( + title: "mi_fix_my_valet_unavailable".localized(PhpEnv.brewPhpVersion), action: nil, keyEquivalent: "f" )) } else { - self.addItem(NSMenuItem( - title: "mi_force_load_latest".localized(App.shared.brewPhpVersion), - action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: "f")) + servicesMenu.addItem(NSMenuItem( + title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion), + action: #selector(MainMenu.fixMyValet), keyEquivalent: "f")) } + + servicesMenu.addItem(NSMenuItem(title: "mi_services".localized, action: nil, keyEquivalent: "")) + + 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_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"), + withKeyModifier: [.command, .shift]) + + servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s")) + + servicesMenu.addItem(NSMenuItem(title: "mi_manual_actions".localized, action: nil, keyEquivalent: "")) + + servicesMenu.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r")) + + for item in servicesMenu.items { + item.target = MainMenu.shared + } + + self.setSubmenu(servicesMenu, for: services) + self.addItem(services) } func addValetMenuItems() { @@ -81,21 +86,26 @@ class StatusMenu : NSMenu { } func addPhpConfigurationMenuItems() { - if App.shared.currentInstall == nil { - return - } - // Configuration self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized)) - self.addItem(NSMenuItem(title: "mi_global_composer".localized, action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g")) 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")) - if (App.shared.busy) { + // Composer + self.addItem(NSMenuItem.separator()) + 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) + + if (PhpEnv.shared.isBusy) { return } - let stats = App.phpInstall!.limits + let stats = PhpEnv.phpInstall.limits // Stats self.addItem(NSMenuItem.separator()) @@ -109,37 +119,37 @@ class StatusMenu : NSMenu { self.addItem(NSMenuItem.separator()) self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized)) - if (App.phpInstall!.extensions.count == 0) { + if (PhpEnv.phpInstall.extensions.count == 0) { self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: "")) } var shortcutKey = 1 - for phpExtension in App.phpInstall!.extensions { + for phpExtension in PhpEnv.phpInstall.extensions { self.addExtensionItem(phpExtension, shortcutKey) shortcutKey += 1 } + // Other self.addItem(NSMenuItem.separator()) - - self.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r")) + self.addOtherMenuItems() } private func addSwitchToPhpMenuItems() { var shortcutKey = 1 - for index in (0.. Bool { + if let bool = Preferences.preferences[preference] as? Bool { + return bool == true + } else { + fatalError("\(preference) is not a valid boolean preference!") + } + } + // MARK: - Internal Functionality - static func cache() -> [PreferenceName: Any] { + private static func cache() -> [PreferenceName: Any] { return [ // Part 1: Always Booleans .shouldDisplayDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any, + .shouldDisplayPhpHintInIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayPhpHintInIcon.rawValue) as Any, .fullPhpVersionDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any, .autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any, - .useInternalSwitcher: UserDefaults.standard.bool(forKey: PreferenceName.useInternalSwitcher.rawValue) as Any, + .autoComposerGlobalUpdateAfterSwitch: UserDefaults.standard.bool(forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any, + .allowProtocolForIntegrations: UserDefaults.standard.bool(forKey: PreferenceName.allowProtocolForIntegrations.rawValue) as Any, // Part 2: Always Strings .globalHotkey: UserDefaults.standard.string(forKey: PreferenceName.globalHotkey.rawValue) as Any, @@ -91,4 +135,28 @@ class Preferences { Preferences.shared.cachedPreferences = Preferences.cache() } + // MARK: - Custom Preferences + + private func loadCustomPreferences() { + let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json") + if Filesystem.fileExists(url.path) { + Log.info("A custom .phpmon.conf.json file was found. Attempting to parse...") + loadCustomPreferencesFile(url) + } else { + Log.info("There was no .phpmon.conf.json file to be loaded.") + } + } + + private func loadCustomPreferencesFile(_ url: URL) { + do { + customPreferences = try JSONDecoder().decode( + CustomPrefs.self, + from: try! String(contentsOf: url, encoding: .utf8).data(using: .utf8)! + ) + Log.info("The .phpmon.conf.json file was successfully parsed.") + } catch { + Log.warn("The .phpmon.conf.json file seems to be missing or malformed.") + } + } + } diff --git a/phpmon/Domain/Preferences/PrefsVC.swift b/phpmon/Domain/Preferences/PrefsVC.swift index ee3208d..13b1e9d 100644 --- a/phpmon/Domain/Preferences/PrefsVC.swift +++ b/phpmon/Domain/Preferences/PrefsVC.swift @@ -59,12 +59,21 @@ class PrefsVC: NSViewController { ), CheckboxPreferenceView.make( sectionText: "", + descriptionText: "prefs.icon_hint_desc".localized, + checkboxText: "prefs.icon_hint_title".localized, + preference: .shouldDisplayPhpHintInIcon, + action: { + MainMenu.shared.refreshIcon() + } + ), + CheckboxPreferenceView.make( + sectionText: "prefs.info_density".localized, descriptionText: "prefs.display_full_php_version_desc".localized, checkboxText: "prefs.display_full_php_version".localized, preference: .fullPhpVersionDynamicIcon, action: { MainMenu.shared.refreshIcon() - MainMenu.shared.update() + MainMenu.shared.rebuild() } ), CheckboxPreferenceView.make( @@ -74,23 +83,29 @@ class PrefsVC: NSViewController { preference: .autoServiceRestartAfterExtensionToggle, action: {} ), - /* DISABLED UNTIL VALET SWITCHING IS OK (see #34) CheckboxPreferenceView.make( - sectionText: "", - descriptionText: "prefs.use_internal_switcher_desc".localized, - checkboxText: "prefs.use_internal_switcher".localized, - preference: .useInternalSwitcher, + sectionText: "prefs.switcher".localized, + descriptionText: "prefs.auto_composer_update_desc".localized, + checkboxText: "prefs.auto_composer_update_title".localized, + preference: .autoComposerGlobalUpdateAfterSwitch, action: {} - ), */ + ), HotkeyPreferenceView.make( sectionText: "prefs.global_shortcut".localized, descriptionText: "prefs.shortcut_desc".localized, self - ) + ), + CheckboxPreferenceView.make( + sectionText: "prefs.integrations".localized, + descriptionText: "prefs.open_protocol_desc".localized, + checkboxText: "prefs.open_protocol_title".localized, + preference: .allowProtocolForIntegrations, + action: {} + ), ].forEach({ self.stackView.addArrangedSubview($0) }) } - // MARK: - Listening for hotkey dleegate + // MARK: - Listening for hotkey delegate var listeningForHotkeyView: HotkeyPreferenceView? = nil @@ -103,6 +118,6 @@ class PrefsVC: NSViewController { // MARK: - Deinitialization deinit { - print("VC deallocated") + Log.perf("PrefsVC deallocated") } } diff --git a/phpmon/Domain/Preferences/PrefsWC.swift b/phpmon/Domain/Preferences/PrefsWC.swift index a2c69c3..6cd24ba 100644 --- a/phpmon/Domain/Preferences/PrefsWC.swift +++ b/phpmon/Domain/Preferences/PrefsWC.swift @@ -21,12 +21,6 @@ class PrefsWC: PMWindowController { return "Preferences" } - // MARK: - Window Lifecycle - - override func windowDidLoad() { - super.windowDidLoad() - } - // MARK: - Key Interaction override func keyDown(with event: NSEvent) { @@ -35,7 +29,7 @@ class PrefsWC: PMWindowController { if let vc = contentViewController as? PrefsVC { if vc.listeningForHotkeyView != nil { if event.keyCode == Keys.Escape || event.keyCode == Keys.Space { - print("A blacklisted key was pressed, canceling listen") + Log.info("A blacklisted key was pressed, canceling listen!") vc.listeningForHotkeyView = nil } else { vc.listeningForHotkeyView!.updateShortcut(event) diff --git a/phpmon/Domain/Preferences/Stats.swift b/phpmon/Domain/Preferences/Stats.swift new file mode 100644 index 0000000..f8449e0 --- /dev/null +++ b/phpmon/Domain/Preferences/Stats.swift @@ -0,0 +1,116 @@ +// +// Stats.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 29/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation +import Cocoa + +class Stats { + + /** + Keep track of how many times the app has been successfully launched. + + This is used to determine whether it is time to show the sponsor + encouragement alert, but I'd like to include this stat somewhere + else as well. + */ + public static var successfulLaunchCount: Int { + UserDefaults.standard.integer( + forKey: InternalStats.launchCount.rawValue + ) + } + + /** + Keep track of how many times the app has successfully switched + between different PHP versions. + + This is used to determine whether it is time to show the sponsor + encouragement alert, but I'd like to include this stat somewhere + else as well. + */ + public static var successfulSwitchCount: Int { + UserDefaults.standard.integer( + forKey: InternalStats.switchCount.rawValue + ) + } + + /** + Did the user see the sponsor encouragement / thank you message? + Annoying the user is the worst, so let's not show the message twice. + */ + public static var didSeeSponsorEncouragement: Bool { + UserDefaults.standard.bool( + forKey: InternalStats.didSeeSponsorEncouragement.rawValue + ) + } + + /** + Increment the successful launch count. This should only be + called when the user has not encountered ANY issues starting + up the application. + */ + public static func incrementSuccessfulLaunchCount() { + UserDefaults.standard.set( + Stats.successfulLaunchCount + 1, + forKey: InternalStats.launchCount.rawValue + ) + } + + /** + Increment the successful switch count. + */ + public static func incrementSuccessfulSwitchCount() { + UserDefaults.standard.set( + Stats.successfulSwitchCount + 1, + forKey: InternalStats.switchCount.rawValue + ) + } + + /** + Determine if the sponsor message should be displayed. + + The rationale behind this is simple, some of the stats + increasing beyond a certain point indicate the app + is being used. + + We evaluate, first: + - Successful version switches + OR + - Successful starts of the application + + AND, of course, you must never have seen the alert before. + (see `didSeeSponsorEncouragement`) + */ + public static func evaluateSponsorMessageShouldBeDisplayed() { + if Bundle.main.bundleIdentifier?.contains("beta") ?? false { + return Log.info("Sponsor messages never apply to beta builds.") + } + + if Stats.didSeeSponsorEncouragement { + return Log.info("Awesome, the user has already seen the sponsor message.") + } + + if Stats.successfulLaunchCount < 7 && Stats.successfulSwitchCount < 40 { + return Log.info("It is too soon to see the sponsor message (launched \(Stats.successfulLaunchCount) times, switched \(Stats.successfulSwitchCount) times).") + } + + DispatchQueue.main.async { + let donate = Alert.present( + messageText: "startup.sponsor_encouragement.title".localized, + informativeText: "startup.sponsor_encouragement.desc".localized, + buttonTitle: "startup.sponsor_encouragement.accept".localized, + secondButtonTitle: "startup.sponsor_encouragement.skip".localized, + style: .informational) + if donate { + Log.info("The user is an absolute badass for choosing this option. Thank you.") + NSWorkspace.shared.open(Constants.DonationUrl) + } + UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue) + } + } + +} diff --git a/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift b/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift index 2736e08..7040cb6 100644 --- a/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift +++ b/phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift @@ -21,8 +21,7 @@ class CheckboxPreferenceView: NSView, XibLoadable { var preference: PreferenceName! { didSet { - let shouldDisplay = Preferences.preferences[self.preference] as! Bool == true - self.buttonCheckbox.state = shouldDisplay ? .on : .off + self.buttonCheckbox.state = Preferences.isEnabled(self.preference) ? .on : .off } } diff --git a/phpmon/Domain/Preferences/Views/HotkeyPreferenceView.xib b/phpmon/Domain/Preferences/Views/HotkeyPreferenceView.xib index 08ea47d..aa835db 100644 --- a/phpmon/Domain/Preferences/Views/HotkeyPreferenceView.xib +++ b/phpmon/Domain/Preferences/Views/HotkeyPreferenceView.xib @@ -63,7 +63,7 @@ - + diff --git a/phpmon/Domain/Progress/ProgressWindow.storyboard b/phpmon/Domain/Progress/ProgressWindow.storyboard new file mode 100644 index 0000000..05cd210 --- /dev/null +++ b/phpmon/Domain/Progress/ProgressWindow.storyboard @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpmon/Domain/Progress/ProgressWindow.swift b/phpmon/Domain/Progress/ProgressWindow.swift new file mode 100644 index 0000000..0030f93 --- /dev/null +++ b/phpmon/Domain/Progress/ProgressWindow.swift @@ -0,0 +1,76 @@ +// +// ProgressView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 18/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation +import AppKit + +class ProgressWindowController: NSWindowController, NSWindowDelegate { + + static func display(title: String, description: String) -> ProgressWindowController { + let storyboard = NSStoryboard(name: "ProgressWindow" , bundle : nil) + + let windowController = storyboard.instantiateController( + withIdentifier: "progressWindow" + ) as! ProgressWindowController + + windowController.showWindow(windowController) + windowController.window?.makeKeyAndOrderFront(nil) + windowController.positionWindowInTopLeftCorner() + + windowController.progressView?.labelTitle.stringValue = title + windowController.progressView?.labelDescription.stringValue = description + + NSApp.activate(ignoringOtherApps: true) + + return windowController + } + + var progressView: ProgressViewController? { + return self.contentViewController as? ProgressViewController + } + + public func addToConsole(_ string: String) { + guard let textView = self.progressView?.textView else { + return + } + + textView.string = textView.string + string + textView.scrollToEndOfDocument(nil) + } + + public func setType(info: Bool = true) { + guard let imageView = self.progressView?.imageViewType else { + return + } + + imageView.image = NSImage(named: info ? "NSInfo" : "NSCaution") + } + + func windowWillClose(_ notification: Notification) { + self.contentViewController = nil + } + + deinit { + Log.perf("Deinitializing ProgressWindowController") + } + +} + +class ProgressViewController: NSViewController { + + @IBOutlet weak var labelTitle: NSTextField! + @IBOutlet weak var labelDescription: NSTextField! + + @IBOutlet var textView: NSTextView! + @IBOutlet weak var imageViewType: NSImageView! + + deinit { + Log.perf("Deinitializing ProgressViewController") + } + +} diff --git a/phpmon/Domain/SiteList/AddSiteVC.swift b/phpmon/Domain/SiteList/AddSiteVC.swift new file mode 100644 index 0000000..40f64f7 --- /dev/null +++ b/phpmon/Domain/SiteList/AddSiteVC.swift @@ -0,0 +1,139 @@ +// +// AddSiteVC.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 24/01/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation +import Cocoa + +class AddSiteVC: NSViewController, NSTextFieldDelegate { + + @IBOutlet weak var pathControl: NSPathControl! + @IBOutlet weak var linkName: NSTextField! + @IBOutlet weak var previewText: NSTextField! + @IBOutlet weak var buttonSecure: NSButton! + @IBOutlet weak var buttonCreateLink: NSButton! + @IBOutlet weak var buttonCancel: NSButton! + + @IBOutlet weak var textFieldTitle: NSTextField! + @IBOutlet weak var textFieldSecure: NSTextField! + @IBOutlet weak var textFieldError: NSTextField! + + // MARK: - View Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + loadStaticLocalisedStrings() + } + + private func dismissView(outcome: NSApplication.ModalResponse) { + guard let window = self.view.window, let parent = window.sheetParent else { return } + parent.endSheet(window, returnCode: outcome) + } + + // MARK: - Localisation + + func loadStaticLocalisedStrings() { + textFieldTitle.stringValue = "site_list.add.link_folder".localized + linkName.placeholderString = "site_list.add.domain_name_placeholder".localized + textFieldSecure.stringValue = "site_list.add.secure_description".localized + buttonCancel.stringValue = "site_list.add.cancel".localized + } + + // MARK: - Outlet Interactions + + @IBAction func pressedCreateLink(_ sender: Any) { + let path = self.pathControl.url!.path + let name = self.linkName.stringValue + + if !FileManager.default.fileExists(atPath: path) { + Alert.confirm( + onWindow: self.view.window!, + messageText: "site_list.alert.folder_missing.title".localized, + informativeText: "site_list.alert.folder_missing.desc".localized, + buttonTitle: "site_list.alert.folder_missing.cancel".localized, + secondButtonTitle: "site_list.alert.folder_missing.return".localized, + onFirstButtonPressed: { + self.dismissView(outcome: .cancel) + } + ) + return + } + + Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)'", requiresPath: true) + + self.dismissView(outcome: .OK) + + // Reset search + App.shared.siteListWindowController? + .searchToolbarItem + .searchField.stringValue = "" + + // Add the new item and scrolls to it + App.shared.siteListWindowController? + .contentVC + .addedNewSite( + name: name, + secure: buttonSecure.state == .on + ) + } + + @IBAction func pressedCancel(_ sender: Any) { + self.dismissView(outcome: .cancel) + } + + @IBAction func pressedSecure(_ sender: Any) { + updatePreview() + } + + // MARK: - Text Field Delegate + + func controlTextDidChange(_ obj: Notification) { + updateTextField() + } + + // MARK: - Helper Methods + + private func isValidLinkName(_ name: String) -> Bool { + if self.linkName.stringValue.isEmpty { + self.textFieldError.isHidden = false + self.textFieldError.stringValue = "site_list.add.errors.empty".localized + return false + } + + if Valet.shared.sites.contains(where: { $0.name == name }) { + self.textFieldError.isHidden = false + self.textFieldError.stringValue = "site_list.add.errors.already_exists".localized + return false + } + + self.textFieldError.isHidden = true + return true + } + + func updateTextField() { + self.linkName.stringValue = self.linkName.stringValue + .replacingOccurrences(of: " ", with: "-") + + buttonCreateLink.isEnabled = isValidLinkName(self.linkName.stringValue) + self.updatePreview() + } + + func updatePreview() { + buttonSecure.title = "site_list.add.secure_after_creation" + .localized( + self.linkName.stringValue, + Valet.shared.config.tld + ) + + previewText.stringValue = "site_list.add.folder_available" + .localized( + self.buttonSecure.state == .on ? "https" : "http", + self.linkName.stringValue, + Valet.shared.config.tld + ) + } +} diff --git a/phpmon/Domain/SiteList/SiteListCell.swift b/phpmon/Domain/SiteList/SiteListCell.swift index 4af0450..bf95985 100644 --- a/phpmon/Domain/SiteList/SiteListCell.swift +++ b/phpmon/Domain/SiteList/SiteListCell.swift @@ -11,33 +11,32 @@ import AppKit class SiteListCell: NSTableCellView { + var site: Valet.Site? = nil + @IBOutlet weak var labelSiteName: NSTextField! @IBOutlet weak var labelPathName: NSTextField! + @IBOutlet weak var labelDriverType: NSTextField! @IBOutlet weak var imageViewLock: NSImageView! @IBOutlet weak var imageViewType: NSImageView! @IBOutlet weak var labelDriver: NSTextField! - @IBOutlet weak var buttonWarning: NSButton! - @IBOutlet weak var labelWarning: NSTextField! + @IBOutlet weak var buttonPhpVersion: NSButton! + @IBOutlet weak var imageViewPhpVersionOK: NSImageView! override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) } func populateCell(with site: Valet.Site) { + self.site = site + // Make sure to show the TLD labelSiteName.stringValue = "\(site.name!).\(Valet.shared.config.tld)" - let isProblematic = site.name.contains(" ") - buttonWarning.isHidden = !isProblematic - labelWarning.isHidden = !isProblematic - labelWarning.stringValue = "site_list.warning.spaces".localized - // Show the absolute path, except make sure to replace the /Users/username segment with ~ for readability - labelPathName.stringValue = site.absolutePath - .replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~") + labelPathName.stringValue = site.absolutePathRelative // If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked). imageViewType.image = NSImage( @@ -48,12 +47,66 @@ class SiteListCell: NSTableCellView imageViewType.contentTintColor = NSColor.tertiaryLabelColor // Show the green or red lock based on whether the site was secured - imageViewLock.image = NSImage(named: site.secured ? "Lock" : "LockUnlocked") + // imageViewLock.image = NSImage(named: site.secured ? "Lock" : "LockUnlocked") imageViewLock.contentTintColor = site.secured ? - NSColor.init(red: 63/255, green: 195/255, blue: 128/255, alpha: 1.0) // green - : NSColor.init(red: 246/255, green: 71/255, blue: 71/255, alpha: 1.0) // red + NSColor(named: "IconColorGreen") // green + : NSColor(named: "IconColorRed") // Show the current driver - labelDriver.stringValue = site.driver ?? "???" + labelDriverType.stringValue = site.driverDeterminedByComposer + ? "Project Type".uppercased() + : "Driver Type".uppercased() + + labelDriver.stringValue = site.driver ?? "driver.not_detected".localized + + // Determine the Laravel version + if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") { + let constraint = site.notableComposerDependencies["laravel/framework"]! + labelDriver.stringValue = "Laravel (\(constraint))" + } + + // Show the PHP version + buttonPhpVersion.title = " PHP \(site.composerPhp) " + buttonPhpVersion.isHidden = (site.composerPhp == "???") + + + imageViewPhpVersionOK.isHidden = (site.composerPhp == "???" || !site.composerPhpCompatibleWithLinked) + } + + @IBAction func pressedPhpVersion(_ sender: Any) { + guard let site = self.site else { return } + + let alert = NSAlert.init() + alert.alertStyle = .informational + + alert.messageText = "alert.composer_php_requirement.title" + .localized("\(site.name!).\(Valet.shared.config.tld)", site.composerPhp) + alert.informativeText = "alert.composer_php_requirement.info" + .localized(site.composerPhpSource) + + alert.addButton(withTitle: "site_link.close".localized) + + var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue + var map: [Int: String] = [:] + + // 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 + 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 + } + + alert.beginSheetModal(for: App.shared.siteListWindowController!.window!) { response in + if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue { + if map.keys.contains(response.rawValue) { + let version = map[response.rawValue]! + print("Pressed button to switch to \(version)") + MainMenu.shared.switchToPhpVersion(version) + } + } + } } } diff --git a/phpmon/Domain/SiteList/SiteListVC.swift b/phpmon/Domain/SiteList/SiteListVC.swift index 78ea030..b441153 100644 --- a/phpmon/Domain/SiteList/SiteListVC.swift +++ b/phpmon/Domain/SiteList/SiteListVC.swift @@ -56,7 +56,7 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource { ] windowController.window!.minSize = NSSize(width: 550, height: 200) windowController.window!.delegate = windowController - windowController.positionWindowInTopLeftCorner() + windowController.window!.setFrameAutosaveName("siteListWindow") App.shared.siteListWindowController = windowController } @@ -93,6 +93,7 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource { progressIndicator.startAnimation(nil) tableView.alphaValue = 0.3 tableView.isEnabled = false + tableView.selectRowIndexes([], byExtendingSelection: true) } /** @@ -135,6 +136,28 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource { } } + func addedNewSite(name: String, secure: Bool) { + waitAndExecute { + Valet.shared.reloadSites() + } completion: { [self] in + find(name, secure) + } + } + + private func find(_ name: String, _ secure: Bool = false) { + sites = Valet.shared.sites + searchedFor(text: "") + if let site = sites.enumerated().first(where: { $0.element.name == name }) { + DispatchQueue.main.async { + self.tableView.selectRowIndexes([site.offset], byExtendingSelection: false) + self.tableView.scrollRowToVisible(site.offset) + if (secure && !site.element.secured) { + self.toggleSecure() + } + } + } + } + // MARK: - Table View Delegate func numberOfRows(in tableView: NSTableView) -> Int { @@ -172,20 +195,31 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource { if searchString.isEmpty { sites = Valet.shared.sites - tableView.reloadData() + + DispatchQueue.main.async { + self.tableView.reloadData() + } return } + let splitSearchString: [String] = searchString + .split(separator: " ") + .map { return String($0) } + sites = Valet.shared.sites.filter({ site in - return site.name.lowercased().contains(searchString) + return !splitSearchString.map { searchString in + return site.name.lowercased().contains(searchString) + }.contains(false) }) - tableView.reloadData() + DispatchQueue.main.async { + self.tableView.reloadData() + } } // MARK: - Deinitialization deinit { - print("VC deallocated") + Log.perf("SiteListVC deallocated") } } diff --git a/phpmon/Domain/SiteList/SiteListWC.swift b/phpmon/Domain/SiteList/SiteListWC.swift index d652dad..4d85cac 100644 --- a/phpmon/Domain/SiteList/SiteListWC.swift +++ b/phpmon/Domain/SiteList/SiteListWC.swift @@ -34,17 +34,61 @@ class SiteListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate { return self.contentViewController as! SiteListVC } + var searchTimer: Timer? + func controlTextDidChange(_ notification: Notification) { guard let searchField = notification.object as? NSSearchField else { return } - contentVC.searchedFor(text: searchField.stringValue) + self.searchTimer?.invalidate() + + searchTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false, block: { _ in + self.contentVC.searchedFor(text: searchField.stringValue) + }) } // MARK: - Reload functionality - @IBAction func pressedReload(_ sender: Any) { + @IBAction func pressedReload(_ sender: Any?) { contentVC.reloadSites() } + + @IBAction func pressedAddLink(_ sender: Any?) { + selectFolder() + } + + // MARK: - Add a new site + + func selectFolder() { + let dialog = NSOpenPanel() + dialog.message = "site_list.add.modal_description".localized + dialog.showsResizeIndicator = true + dialog.showsHiddenFiles = false + dialog.allowsMultipleSelection = false + dialog.canChooseDirectories = true + dialog.canChooseFiles = false + dialog.beginSheetModal(for: self.window!) { response in + let result = dialog.url + if (result != nil && response == .OK) { + let path: String = result!.path + self.showSitePopup(path) + } + } + } + + func showSitePopup(_ folder: String) { + let storyboard = NSStoryboard(name: "Main", bundle : nil) + + let windowController = storyboard.instantiateController( + withIdentifier: "addSiteWindow" + ) as! NSWindowController + + let viewController = windowController.window!.contentViewController as! AddSiteVC + viewController.pathControl.url = URL(fileURLWithPath: folder) + viewController.linkName.stringValue = String(folder.split(separator: "/").last!) + viewController.updateTextField() + + self.window?.beginSheet(windowController.window!) + } } diff --git a/phpmon/Domain/SwiftUI/PMHeaderView.swift b/phpmon/Domain/SwiftUI/PMHeaderView.swift new file mode 100644 index 0000000..ac2317c --- /dev/null +++ b/phpmon/Domain/SwiftUI/PMHeaderView.swift @@ -0,0 +1,29 @@ +// +// PMHeaderView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 15/04/2021. +// Copyright © 2021 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) {} +} diff --git a/phpmon/Domain/SwiftUI/PMServicesView.swift b/phpmon/Domain/SwiftUI/PMServicesView.swift new file mode 100644 index 0000000..94fb013 --- /dev/null +++ b/phpmon/Domain/SwiftUI/PMServicesView.swift @@ -0,0 +1,25 @@ +// +// PMHeaderView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 15/04/2021. +// Copyright © 2021 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) {} +} diff --git a/phpmon/Domain/SwiftUI/PMStatsView.swift b/phpmon/Domain/SwiftUI/PMStatsView.swift new file mode 100644 index 0000000..151bedf --- /dev/null +++ b/phpmon/Domain/SwiftUI/PMStatsView.swift @@ -0,0 +1,29 @@ +// +// PMStatsView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 15/04/2021. +// Copyright © 2021 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) + } +} diff --git a/phpmon/Domain/SwiftUI/Preview.swift b/phpmon/Domain/SwiftUI/Preview.swift new file mode 100644 index 0000000..ffd3eb8 --- /dev/null +++ b/phpmon/Domain/SwiftUI/Preview.swift @@ -0,0 +1,20 @@ +// +// Preview.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 15/04/2021. +// Copyright © 2021 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! + } +} diff --git a/phpmon/Domain/Watcher/App+ConfigWatch.swift b/phpmon/Domain/Watcher/App+ConfigWatch.swift new file mode 100644 index 0000000..82a60e1 --- /dev/null +++ b/phpmon/Domain/Watcher/App+ConfigWatch.swift @@ -0,0 +1,47 @@ +// +// App+ConfigWatch.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 30/03/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +extension App { + + func startWatcher(_ url: URL) { + Log.info("No watcher currently active...") + self.watcher = PhpConfigWatcher(for: url) + + self.watcher.didChange = { url in + Log.info("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...") + MainMenu.shared.reloadPhpMonitorMenuInBackground() + self.watcher.lastUpdate = Date().timeIntervalSince1970 + } + } + } + + func handlePhpConfigWatcher(forceReload: Bool = false) { + let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(PhpEnv.phpInstall.version.short)") + + // Watcher needs to be created + if self.watcher == nil { + startWatcher(url) + } + + // Watcher needs to be updated + if self.watcher.url != url || forceReload { + self.watcher.disable() + self.watcher = nil + Log.info("Watcher has stopped watching files. Starting new one...") + startWatcher(url) + } + } + +} diff --git a/phpmon/Domain/Watcher/PhpConfigWatcher.swift b/phpmon/Domain/Watcher/PhpConfigWatcher.swift new file mode 100644 index 0000000..e97cfe5 --- /dev/null +++ b/phpmon/Domain/Watcher/PhpConfigWatcher.swift @@ -0,0 +1,130 @@ +// +// FolderWatcher.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 30/03/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class PhpConfigWatcher { + + let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent) + + let url: URL + + var didChange: ((URL) -> Void)? + + var lastUpdate: TimeInterval? = nil + + var watchers: [FSWatcher] = [] + + init(for url: URL) { + self.url = url + + // Add a watcher for php.ini + self.addWatcher(for: self.url.appendingPathComponent("php.ini"), eventMask: .write) + + // Add a watcher for conf.d (in case a new file is added or a file is deleted) + // This watcher, when triggered, will restart all watchers + self.addWatcher(for: self.url.appendingPathComponent("conf.d"), eventMask: .all, behaviour: .reloadsWatchers) + + // Scan the conf.d folder for .ini files, and add a watcher for each file + let enumerator = FileManager.default.enumerator(atPath: self.url.appendingPathComponent("conf.d").path) + let filePaths = enumerator?.allObjects as! [String] + + // Loop over the .ini files that we discovered + filePaths.filter { $0.contains(".ini") }.forEach { (file) in + // Add a watcher for each file we have discovered + self.addWatcher(for: self.url.appendingPathComponent("conf.d/\(file)"), eventMask: .write) + } + + Log.info("A watcher exists for the following config paths:") + Log.info(self.watchers.map({ watcher in + return watcher.url.relativePath + })) + } + + func addWatcher(for url: URL, eventMask: DispatchSource.FileSystemEvent, behaviour: FSWatcherBehaviour = .reloadsMenu) { + let watcher = FSWatcher(for: url, eventMask: eventMask, parent: self, behaviour: behaviour) + self.watchers.append(watcher) + } + + func disable() { + Log.info("Turning off existing watchers...") + self.watchers.forEach { (watcher) in + watcher.stopMonitoring() + } + } + + deinit { + Log.perf("A PhpConfigWatcher has been deinitialized.") + } + +} + +enum FSWatcherBehaviour { + case reloadsMenu + case reloadsWatchers +} + +class FSWatcher { + + private var parent: PhpConfigWatcher! + + private var monitoredFolderFileDescriptor: CInt = -1 + + private var folderMonitorSource: DispatchSourceFileSystemObject? + + let url: URL + + init(for url: URL, eventMask: DispatchSource.FileSystemEvent, parent: PhpConfigWatcher, behaviour: FSWatcherBehaviour = .reloadsMenu) { + self.url = url + self.parent = parent + self.startMonitoring(eventMask, behaviour: behaviour) + } + + func startMonitoring(_ eventMask: DispatchSource.FileSystemEvent, behaviour: FSWatcherBehaviour) { + guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else { + return + } + + // Open the file or folder referenced by URL for monitoring only. + monitoredFolderFileDescriptor = open(url.path, O_EVTONLY) + folderMonitorSource = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: monitoredFolderFileDescriptor, + eventMask: eventMask, + queue: parent.folderMonitorQueue + ) + + // Define the block to call when a file change is detected. + folderMonitorSource?.setEventHandler { [weak self] in + // The default behaviour is to reload the menu + switch behaviour { + case .reloadsMenu: + // Default behaviour: reload the menu items + self?.parent.didChange?(self!.url) + case .reloadsWatchers: + // Alternative behaviour: reload all watchers + App.shared.handlePhpConfigWatcher(forceReload: true) + } + } + + // Define a cancel handler to ensure the directory is closed when the source is cancelled. + folderMonitorSource?.setCancelHandler { [weak self] in + guard let self = self else { return } + close(self.monitoredFolderFileDescriptor) + self.monitoredFolderFileDescriptor = -1 + self.folderMonitorSource = nil + } + + // Start monitoring the directory via the source. + folderMonitorSource?.resume() + } + + func stopMonitoring() { + folderMonitorSource?.cancel() + self.parent = nil + } +} diff --git a/phpmon/Info.plist b/phpmon/Info.plist index 4fc1031..7918ee3 100644 --- a/phpmon/Info.plist +++ b/phpmon/Info.plist @@ -18,6 +18,19 @@ APPL CFBundleShortVersionString $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + com.nicoverbruggen.phpmon + CFBundleURLSchemes + + phpmon + + + CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType @@ -27,7 +40,7 @@ LSUIElement NSHumanReadableCopyright - Copyright © 2021 Nico Verbruggen. All rights reserved. + Copyright © 2019-2022 Nico Verbruggen. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 8c6cbf8..a1c6136 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -19,36 +19,49 @@ "mi_diagnostics" = "Diagnostics"; "mi_active_services" = "Active Services"; -"mi_restart_php_fpm" = "Restart service: php"; -"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_force_load_latest" = "Force load (latest) PHP %@"; -"mi_force_load_latest_unavailable" = "Force load unavailable (PHP %@ not installed)"; -"mi_php_refresh" = "Refresh information"; +"mi_restart_php_fpm" = "Restart Service: php"; +"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_fix_my_valet" = "Fix My Valet (PHP & Services)"; +"mi_fix_my_valet_unavailable" = "Fix My Valet Unavailable"; +"mi_php_refresh" = "Refresh Information"; -"mi_configuration" = "Configuration"; +"mi_configuration" = "PHP Configuration"; "mi_limits" = "Limits Configuration"; "mi_memory_limit" = "Memory Limit"; "mi_post_max_size" = "Max POST"; "mi_upload_max_filesize" = "Max Upload"; +"mi_manual_actions" = "Manual Actions"; +"mi_services" = "Services"; +"mi_help" = "First Aid"; +"mi_other" = "First Aid & Services"; +"mi_composer" = "Composer"; "mi_valet_config" = "Locate Valet folder (.config/valet)"; -"mi_php_config" = "Locate PHP configuration file (php.ini)"; -"mi_global_composer" = "Locate global composer.json file (.composer)"; -"mi_phpinfo" = "Show current configuration (phpinfo)"; +"mi_php_config" = "Locate PHP Configuration File (php.ini)"; +"mi_global_composer" = "Locate Global Composer File (.composer)"; +"mi_phpinfo" = "Show Current Configuration (phpinfo)"; +"mi_update_global_composer" = "Update Global Composer Dependencies..."; "mi_detected_extensions" = "Detected Extensions"; "mi_no_extensions_detected" = "No additional extensions detected."; "mi_valet" = "Laravel Valet"; -"mi_sitelist" = "View linked & parked domains..."; +"mi_sitelist" = "View Linked and Parked Domains..."; "mi_preferences" = "Preferences..."; +"mi_donate" = "Donate..."; "mi_quit" = "Quit PHP Monitor"; "mi_about" = "About PHP Monitor"; +// MENU ITEMS (if window is open) + +"mm_add_folder_as_link" = "Add Folder as Link..."; +"mm_reload_site_list" = "Reload Site List"; +"mm_find_in_site_list" = "Search in Site List"; + // SITE LIST "site_list.title" = "Domains"; @@ -61,7 +74,32 @@ "site_list.alerts_status_changed.desc" = "The domain '%@' is now %@."; "site_list.confirm_unlink" = "Are you sure you want to unlink '%@'?"; -"site_link.confirm_link" = "No files will be removed. If needed, the site will need to be relinked via the command line."; +"site_link.confirm_link" = "No files will be removed. You can always link the folder again by clicking on the + button and selecting the original folder."; + +"site_link.close" = "Close"; +"site_link.switch_to_php" = "Switch to PHP %@"; + +// ADD SITE TO SITE LIST + +"site_list.add.link_folder" = "Link a Folder"; +"site_list.add.domain_name_placeholder" = "Enter a domain name here"; +"site_list.add.secure_after_creation" = "Secure %@.%@ after creation"; +"site_list.add.secure_description" = "Securing a site requires administrative privileges.\nYou will be prompted for your password or Touch ID."; +"site_list.add.create_link" = "Create Link"; +"site_list.add.cancel" = "Cancel"; +"site_list.add.folder_available" = "This site will be available via the following URL: %@://%@.%@"; + +"site_list.add.errors.empty" = "You must enter a name."; +"site_list.add.errors.already_exists" = "A link with that name already exists."; + +// ADD SITE ERROR: FOLDER MISSING SINCE SELECTION + +"site_list.alert.folder_missing.desc" = "The folder you chose no longer seems to exist. Do you want to cancel adding this folder? If you moved the folder, you could always put it back and try again."; +"site_list.alert.folder_missing.title" = "Folder missing!"; +"site_list.alert.folder_missing.cancel" = "Cancel Link"; +"site_list.alert.folder_missing.return" = "OK"; + +"site_list.add.modal_description" = "First, select which folder you would like to link."; // SITE LIST ACTIONS @@ -79,6 +117,10 @@ "site_list.alert.invalid_folder_name" = "Invalid folder name"; "site_list.alert.invalid_folder_name_desc" = "This folder could not be resolved to a valid URL. This is usually because there’s a space in the folder name. Please rename the folder, reload the list of sites, and try again."; +// DRIVERS + +"driver.not_detected" = "Other"; + // EDITORS "editors.alert.try_again" = "Try Again"; @@ -91,31 +133,34 @@ "prefs.close" = "Close"; "prefs.global_shortcut" = "Global shortcut:"; -"prefs.dynamic_icon" = "Dynamic icon:"; +"prefs.dynamic_icon" = "Icon customization:"; +"prefs.info_density" = "Info density:"; "prefs.services" = "Services:"; +"prefs.switcher" = "Switcher:"; +"prefs.integrations" = "Integrations:"; "prefs.auto_restart_services_title" = "Auto-restart PHP-FPM"; -"prefs.auto_restart_services_desc" = "When checked, will automatically restart PHP-FPM when\nyou check or uncheck an extension. Slightly slower when enabled, \nbut this applies the extension change immediately for all sites \nyou're serving, no need to restart PHP-FPM manually."; +"prefs.auto_restart_services_desc" = "When checked, will automatically restart PHP-FPM when you check or uncheck an extension. Slightly slower when enabled, but this applies the extension change immediately for all sites you're serving, no need to restart PHP-FPM manually."; "prefs.dynamic_icon_title" = "Display dynamic icon in menu bar"; -"prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible.\nIf checked, it will display the major version number of the\ncurrently linked PHP version."; +"prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible. If checked, it will display the major version number of the currently linked PHP version."; -"prefs.display_full_php_version" = "Display full PHP version in menu bar and dropdown"; -"prefs.display_full_php_version_desc" = "Display the full version instead of the major version only.\n(This may be undesirable on smaller displays,\nso this is disabled by default.)"; +"prefs.icon_hint_title" = "Display PHP hint next to version number"; +"prefs.icon_hint_desc" = "If you uncheck this box, the icon in the menu bar will only show the version number and not the PHP hint. This preference does not do anything if the dynamic icon has been disabled."; -"prefs.use_internal_switcher" = "Use PHP Monitor’s own version switcher"; -"prefs.use_internal_switcher_desc" = -"By default, PHP Monitor will attempt to use Laravel Valet -in order to switch PHP versions. If you prefer a different -switcher or are having issues with `valet use php`, you may -use PHP Monitor's own switcher which is slightly faster, -but might cause issues with permissions in your Homebrew -directories, since PHP Monitor controls the services."; +"prefs.display_full_php_version" = "Display full PHP version everywhere"; +"prefs.display_full_php_version_desc" = "Display the full version instead of the major version displayed in the menu bar and the dropdown menu. (This may be undesirable on smaller displays, so this is disabled by default.)"; + +"prefs.auto_composer_update_title" = "Automatically update global dependencies"; +"prefs.auto_composer_update_desc" = "When checked, will automatically ask Composer to run `composer global update` whenever you switch between different PHP versions. You will be able to see what changes are being made, or should this fail."; + +"prefs.open_protocol_title" = "Allow third-party integrations"; +"prefs.open_protocol_desc" = "When checked, this will allow the interaction with third party utilities to work (e.g. Alfred, Raycast). If you disable this, PHP Monitor will still receive the commands, but will not act upon them."; "prefs.shortcut_set" = "Set global shortcut"; "prefs.shortcut_listening" = ""; "prefs.shortcut_clear" = "Clear"; -"prefs.shortcut_desc" = "If a shortcut combination is set up, you can toggle PHP Monitor\nwherever you are by pressing the key combination you chose.\n(Cancel choosing a shortcut by pressing the spacebar.)"; +"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.)"; // NOTIFICATIONS @@ -133,13 +178,40 @@ directories, since PHP Monitor controls the services."; // ALERTS -// Force Reload Started -"alert.force_reload.title" = "PHP Monitor will force reload the latest version of PHP"; -"alert.force_reload.info" = "This can take a while. You'll get another alert when the force reload has completed."; +// Composer Update +"alert.composer_missing.title" = "Composer not found!"; +"alert.composer_missing.info" = "Make sure you have Composer available in `/usr/local/bin/composer`. If Composer is located somewhere else, please create a symlink, like so (make sure to use the correct path):\n\n`ln -s /path/to/composer /user/local/bin`."; -// Force Reload Done -"alert.force_reload_done.title" = "PHP has been force reloaded"; -"alert.force_reload_done.info" = "All appropriate services have been restarted, and the latest version of PHP is now active. You can now try switching to another version of PHP. If visiting sites still does not work, you may try running `valet install` again, this can fix a 502 issue (Bad Gateway)."; +"alert.composer_progress.title" = "Updating global dependencies..."; +"alert.composer_progress.info" = "You can see the progress in the terminal output below."; + +"alert.composer_failure.title" = "Something went wrong!"; +"alert.composer_failure.info" = "Your global Composer dependencies could not be updated. +You can find more information in the terminal output below. You’ll have to fix this +problem manually, using your own Terminal app (this just shows you the output)."; + +"alert.composer_success.title" = "Composer’s done updating!"; +"alert.composer_success.info" = "Your global Composer dependencies have been successfully updated."; + +// Composer Version +"alert.composer_php_requirement.title" = "`%@` has the following PHP requirement: \"php\":\n\"%@\"."; +"alert.composer_php_requirement.info" = "This required PHP version was determined by checking the `%@` field in the `composer.json` file when the site list was last refreshed."; + +// 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.confirm" = "Yes, run \"Fix My Valet\""; +"alert.php_switch_failed.cancel" = "Do Not Run"; + +// Fix My Valet Started +"alert.fix_my_valet.title" = "Having issues? Fix My Valet is ready to commence!"; +"alert.fix_my_valet.info" = "This can take a while. Please be patient.\n\nWhen this is done, all other services will be halted and PHP %@ will be linked. You will be able to switch to your desired version of PHP once this operation has completed.\n\n(You'll get another alert once Fix My Valet is done.)"; +"alert.fix_my_valet.ok" = "Continue"; +"alert.fix_my_valet.cancel" = "Abort"; + +// Fix My Valet Done +"alert.fix_my_valet_done.title" = "Fix My Valet has completed its operations."; +"alert.fix_my_valet_done.info" = "All appropriate services have been stopped and the correct ones restarted, and the latest version of PHP should now be active. You can now try switching to another version of PHP.\n\nIf visiting sites still does not work, you may try running `valet install` again, this can fix a 502 issue (Bad Gateway).\n\nIf Valet is broken and you cannot run `valet install`, you may need to run `composer global update`. Please consult the FAQ on GitHub if you have further issues."; // PHP FPM Broken "alert.php_fpm_broken.title" = "PHP-FPM configuration is incorrect"; @@ -172,10 +244,18 @@ You can do this by running `composer global update` in your terminal. After that "startup.errors.php_opt.title" = "PHP is not correctly installed"; "startup.errors.php_opt.desc" = "PHP alias was not found in `/usr/local/opt` or `/opt/homebrew/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php` in order for PHP Monitor to detect this installation."; -/// 3. Valet not installed +/// 3a. Valet not installed "startup.errors.valet_executable.title" = "Laravel Valet is not correctly installed"; "startup.errors.valet_executable.desc" = "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet` or `/opt/homebrew/bin/valet`. The app will not work correctly until you resolve this issue. (PHP Monitor checks for the existence of `valet` in either of these paths.)"; +/// 3b. Valet configuration file missing [currently not enabled] +"startup.errors.valet_config.title" = "Laravel Valet configuration file missing"; +"startup.errors.valet_config.desc" = "PHP Monitor needs to be able to read the configuration file in `~/.config/valet/config.json`."; + +/// 3c. Valet version not readable +"startup.errors.valet_version_unknown.title" = "Your Valet version could not be read (`valet --version` failed)"; +"startup.errors.valet_version_unknown.desc" = "Make sure your Valet installation works and is up-to-date.\n\nTry running `valet --version` in a terminal to find out what's going on."; + /// 4. Brew & sudoers "startup.errors.sudoers_brew.title" = "Brew has not been added to sudoers.d"; "startup.errors.sudoers_brew.desc" = "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."; @@ -184,6 +264,17 @@ You can do this by running `composer global update` in your terminal. After that "startup.errors.sudoers_valet.title" = "Valet has not been added to sudoers.d"; "startup.errors.sudoers_valet.desc" = "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."; -/// 6. Multiple services active +/// 6. Cannot retrieve services +"startup.errors.services_json_error.title" = "Cannot determine services status"; +"startup.errors.services_json_error.desc" = "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. This can happen if your Homebrew installation is out of date, in which case Homebrew won't return JSON yet.\n\nYou can usually fix this by running `brew update`."; + +/// 7. Multiple services active "startup.errors.services.title" = "Multiple PHP services are active"; -"startup.errors.services.desc" = "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes.\n\nThe easiest solution is to choose the option 'Force load latest PHP version' in the menu bar.\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies).\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue.\n\nFor more information about this issue, please see the README.md file in the repository on GitHub."; +"startup.errors.services.desc" = "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes.\n\nThe easiest solution is to choose the option 'First Aid & Services > Fix My Valet' in the menu bar.\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies).\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue.\n\nFor more information about this issue, please see the README.md file in the repository on GitHub."; + +// SPONSOR ENCOURAGEMENT + +"startup.sponsor_encouragement.title" = "If PHP Monitor has been useful to you or your company, please consider leaving a tip."; +"startup.sponsor_encouragement.desc" = "If you have already donated, then YOU are the reason why the app was able to get all these new features. In that case, this is a THANK YOU message to you.\n\nTo be 100% transparent: I plan to keep PHP Monitor open source and free. Your support makes this decision very easy.\n\n(You will only see this prompt once.)"; +"startup.sponsor_encouragement.accept" = "Yes, I would like to sponsor"; +"startup.sponsor_encouragement.skip" = "Nevermind";