mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-07 03:50:08 +02:00
🚀 Version 5.0
This release brings a plethora of new features to PHP Monitor.
Please check out the release notes for more details.
🔀 Merge branch 'dev/5.x'
This commit is contained in:
@ -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 = "<group>"; };
|
||||
5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
@ -145,7 +232,17 @@
|
||||
54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotkeyPreferenceView.swift; sourceTree = "<group>"; };
|
||||
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = "<group>"; };
|
||||
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
|
||||
C40C7F1D2772136000DDDCDC /* PhpEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpEnv.swift; sourceTree = "<group>"; };
|
||||
C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActivePhpInstallation+Checks.swift"; sourceTree = "<group>"; };
|
||||
C40C7F2F27722E8D00DDDCDC /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = "<group>"; };
|
||||
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFrameworks.swift; sourceTree = "<group>"; };
|
||||
C415D3B62770F294005EF286 /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
C415D3E52770F540005EF286 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+InterApp.swift"; sourceTree = "<group>"; };
|
||||
C417DC73277614690015E6EE /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
||||
C4188988275FE8CB001EF227 /* Filesystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filesystem.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
@ -153,20 +250,19 @@
|
||||
C41C1B3D22B0098000E7CF16 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
C41C1B3F22B0098000E7CF16 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = "<group>"; };
|
||||
C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
|
||||
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = "<group>"; };
|
||||
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = "<group>"; };
|
||||
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
|
||||
C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+Actions.swift"; sourceTree = "<group>"; };
|
||||
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = "<group>"; };
|
||||
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.swift"; sourceTree = "<group>"; };
|
||||
C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
|
||||
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
|
||||
C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = "<group>"; };
|
||||
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.swift"; sourceTree = "<group>"; };
|
||||
C43A8A1925D9CD1000591B77 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = "<group>"; };
|
||||
C43A8A1F25D9D1D700591B77 /* brew.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = brew.json; sourceTree = "<group>"; };
|
||||
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewJsonParserTest.swift; sourceTree = "<group>"; };
|
||||
C44C198C276E3A1C0072762D /* ProgressWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressWindow.swift; sourceTree = "<group>"; };
|
||||
C44C1990276E44CB0072762D /* ProgressWindow.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ProgressWindow.storyboard; sourceTree = "<group>"; };
|
||||
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListWC.swift; sourceTree = "<group>"; };
|
||||
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListVC.swift; sourceTree = "<group>"; };
|
||||
C464ADB1275A87CA003FCD53 /* SiteListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListCell.swift; sourceTree = "<group>"; };
|
||||
@ -182,8 +278,11 @@
|
||||
C48D0C9525CC80B100CC7490 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
|
||||
C48D0C9925CC888B00CC7490 /* HeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HeaderView.xib; sourceTree = "<group>"; };
|
||||
C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
|
||||
C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpVersionNumber.swift; sourceTree = "<group>"; };
|
||||
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionNumberTest.swift; sourceTree = "<group>"; };
|
||||
C4930849279F331F009C240B /* AddSiteVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteVC.swift; sourceTree = "<group>"; };
|
||||
C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = "<group>"; };
|
||||
C49EAB45259FC305007F6C3B /* Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
|
||||
C49E171E27A5736E00787921 /* PMServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMServicesView.swift; sourceTree = "<group>"; };
|
||||
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
|
||||
C4AF9F70275445FF00D44ED0 /* valet-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "valet-config.json"; sourceTree = "<group>"; };
|
||||
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigParserTest.swift; sourceTree = "<group>"; };
|
||||
@ -191,16 +290,37 @@
|
||||
C4AF9F7C275454A900D44ED0 /* ValetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetTest.swift; sourceTree = "<group>"; };
|
||||
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.swift; sourceTree = "<group>"; };
|
||||
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionExtractorTest.swift; sourceTree = "<group>"; };
|
||||
C4B5853B2770FE3900DA4FBE /* Paths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
|
||||
C4B5853C2770FE3900DA4FBE /* Shell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
|
||||
C4B5853D2770FE3900DA4FBE /* Command.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
|
||||
C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MenuOutlets.swift"; sourceTree = "<group>"; };
|
||||
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+ActivationPolicy.swift"; sourceTree = "<group>"; };
|
||||
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+GlobalHotkey.swift"; sourceTree = "<group>"; };
|
||||
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Startup.swift"; sourceTree = "<group>"; };
|
||||
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = "<group>"; };
|
||||
C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = "<group>"; };
|
||||
C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpConfigWatcher.swift; sourceTree = "<group>"; };
|
||||
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = "<group>"; };
|
||||
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = "<group>"; };
|
||||
C4D89BC52783C99400A02B68 /* ComposerJson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerJson.swift; sourceTree = "<group>"; };
|
||||
C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpSwitcher.swift; sourceTree = "<group>"; };
|
||||
C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalSwitcher.swift; sourceTree = "<group>"; };
|
||||
C4DEB7D327A5D60B00834718 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = "<group>"; };
|
||||
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
|
||||
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
|
||||
C4EC1E65279DE0380010F296 /* ServicesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ServicesView.xib; sourceTree = "<group>"; };
|
||||
C4EC1E67279DE0540010F296 /* ServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServicesView.swift; sourceTree = "<group>"; };
|
||||
C4EC1E6C279DF87A0010F296 /* Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Async.swift; sourceTree = "<group>"; };
|
||||
C4EC1E72279DFCF40010F296 /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = "<group>"; };
|
||||
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||
C4EE55A627708B9E001DF387 /* PMHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMHeaderView.swift; sourceTree = "<group>"; };
|
||||
C4EE55A727708B9E001DF387 /* Preview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preview.swift; sourceTree = "<group>"; };
|
||||
C4EE55A827708B9E001DF387 /* PMStatsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMStatsView.swift; sourceTree = "<group>"; };
|
||||
C4EED88827A48778006D7272 /* InterAppHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterAppHandler.swift; sourceTree = "<group>"; };
|
||||
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = "<group>"; };
|
||||
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
|
||||
C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = "<group>"; };
|
||||
C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = "<group>"; };
|
||||
C4F7807425D7F7E5000DBC97 /* RELEASE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = RELEASE.md; sourceTree = "<group>"; };
|
||||
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@ -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 = "<group>";
|
||||
};
|
||||
C40C7F1C27720E1400DDDCDC /* Test Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4AF9F70275445FF00D44ED0 /* valet-config.json */,
|
||||
C43A8A1F25D9D1D700591B77 /* brew.json */,
|
||||
C4F30B06278E195800755FCE /* brew-services.json */,
|
||||
C4F780A725D80AE8000DBC97 /* php.ini */,
|
||||
);
|
||||
path = "Test Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
C415D3D72770F341005EF286 /* phpmon-cli */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C415D3E52770F540005EF286 /* main.swift */,
|
||||
C415D3DE2770F34D005EF286 /* AllowedArguments.swift */,
|
||||
);
|
||||
path = "phpmon-cli";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
@ -291,6 +461,7 @@
|
||||
children = (
|
||||
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */,
|
||||
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */,
|
||||
C415D3D62770F341005EF286 /* phpmon-cli */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@ -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 = "<group>";
|
||||
};
|
||||
C44C198F276E3A380072762D /* Progress */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C44C198C276E3A1C0072762D /* ProgressWindow.swift */,
|
||||
C44C1990276E44CB0072762D /* ProgressWindow.storyboard */,
|
||||
);
|
||||
path = Progress;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
@ -370,10 +555,20 @@
|
||||
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */,
|
||||
54AB03252763858F00A29D5F /* Timer.swift */,
|
||||
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */,
|
||||
C4EC1E6C279DF87A0010F296 /* Async.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C48D6C6E279CD29C00F26D7E /* PHP Version */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C40C7F1D2772136000DDDCDC /* PhpEnv.swift */,
|
||||
C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */,
|
||||
);
|
||||
path = "PHP Version";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
@ -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 = "<group>";
|
||||
};
|
||||
C4B5853A2770FE2500DA4FBE /* phpmon-common */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C40C7F2127721F7300DDDCDC /* Core */,
|
||||
54B20EDF263AA22C00D3250E /* PHP */,
|
||||
);
|
||||
path = "phpmon-common";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4C8E81D276F5686003AC782 /* Watcher */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */,
|
||||
C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */,
|
||||
);
|
||||
path = Watcher;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4D309E72770EF2F00958BCF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4D89BC42783C98800A02B68 /* Composer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4D89BC52783C99400A02B68 /* ComposerJson.swift */,
|
||||
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */,
|
||||
);
|
||||
path = Composer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4D9ADBD27761084007277F4 /* PHP */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */,
|
||||
);
|
||||
path = PHP;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4D9ADC2277610E4007277F4 /* Switcher */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */,
|
||||
C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */,
|
||||
);
|
||||
path = Switcher;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4EE55B027708BB2001DF387 /* SwiftUI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C49E171E27A5736E00787921 /* PMServicesView.swift */,
|
||||
C4EE55A627708B9E001DF387 /* PMHeaderView.swift */,
|
||||
C4EE55A827708B9E001DF387 /* PMStatsView.swift */,
|
||||
C4EE55A727708B9E001DF387 /* Preview.swift */,
|
||||
);
|
||||
path = SwiftUI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4F30B01278E169B00755FCE /* Homebrew */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
|
||||
C4F30B02278E16BA00755FCE /* HomebrewService.swift */,
|
||||
);
|
||||
path = Homebrew;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
C4F7808A25D7F918000DBC97 /* Terminal */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C49EAB45259FC305007F6C3B /* Paths.swift */,
|
||||
C42295DC2358D02000E263B2 /* Command.swift */,
|
||||
C41C1B4622B009A400E7CF16 /* Shell.swift */,
|
||||
);
|
||||
path = Terminal;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = (
|
||||
|
@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C415D3D52770F341005EF286"
|
||||
BuildableName = "phpmon-cli"
|
||||
BlueprintName = "phpmon-cli"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C415D3D52770F341005EF286"
|
||||
BuildableName = "phpmon-cli"
|
||||
BlueprintName = "phpmon-cli"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "help"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C415D3D52770F341005EF286"
|
||||
BuildableName = "phpmon-cli"
|
||||
BlueprintName = "phpmon-cli"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1220"
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
124
README.md
124
README.md
@ -1,15 +1,17 @@
|
||||
# PHP Monitor
|
||||
|
||||
> 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.<br>**Thank you!** ⭐️
|
||||
|
||||
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
|
||||
<h1 align="center"><b>PHP Monitor</b> (phpmon)</h1>
|
||||
|
||||
**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.
|
||||
<p align="center">
|
||||
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
|
||||
</p>
|
||||
|
||||
<img src="./docs/screenshot41.jpg" width="800px" alt="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 <u>you need to have it set up before you can use this</u>.
|
||||
|
||||
<small><i>Screenshot: A menu showing all of the functionality of PHP Monitor.</i></small>
|
||||
<img src="./docs/screenshot50.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
|
||||
|
||||
<small><i>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.</i></small>
|
||||
|
||||
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
|
||||
|
||||
@ -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: <i>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:
|
||||
|
||||
<pre>
|
||||
{
|
||||
"scan_apps": ["Xcode", "Kraken"]
|
||||
}
|
||||
</pre>
|
||||
|
||||
You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor will check for the existence of these apps. You do not need to set the full path, just the name of the app should work. Not all apps support opening a folder, though, so your success might vary.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How can the app integrate with third party tools, like Alfred?</strong></summary>
|
||||
|
||||
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}
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How does the app know what PHP version is required for my app?</strong></summary>
|
||||
|
||||
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.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>What do the checkmarks next to the PHP version mean in the site list?</strong></summary>
|
||||
|
||||
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`).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Why can't I see the driver type any more? It says "Project Type" now.</strong></summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -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".
|
||||
|
||||
</details>
|
||||
|
||||
## 📝 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
|
||||
|
||||
|
@ -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 |
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 396 KiB |
BIN
docs/screenshot50.jpg
Normal file
BIN
docs/screenshot50.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 370 KiB |
BIN
integrations/phpmon.alfredworkflow
Normal file
BIN
integrations/phpmon.alfredworkflow
Normal file
Binary file not shown.
26
phpmon-cli/AllowedArguments.swift
Normal file
26
phpmon-cli/AllowedArguments.swift
Normal file
@ -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 }
|
||||
}
|
||||
}
|
103
phpmon-cli/main.swift
Normal file
103
phpmon-cli/main.swift
Normal file
@ -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()
|
124
phpmon-common/Core/Actions.swift
Normal file
124
phpmon-common/Core/Actions.swift
Normal file
@ -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! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
||||
|
||||
// Tell php-cgi to run the PHP and output as an .html file
|
||||
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
||||
|
||||
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
|
||||
}
|
||||
|
||||
// MARK: - Fix My Valet
|
||||
|
||||
/**
|
||||
Detects all currently available PHP versions,
|
||||
and unlinks each and every one of them.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Command {
|
||||
public class Command {
|
||||
|
||||
/**
|
||||
Immediately executes a command.
|
@ -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")!
|
||||
|
||||
}
|
15
phpmon-common/Core/Events.swift
Normal file
15
phpmon-common/Core/Events.swift
Normal file
@ -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")
|
||||
|
||||
}
|
55
phpmon-common/Core/Helpers.swift
Normal file
55
phpmon-common/Core/Helpers.swift
Normal file
@ -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")
|
||||
}
|
52
phpmon-common/Core/Logger.swift
Normal file
52
phpmon-common/Core/Logger.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
70
phpmon-common/Core/Paths.swift
Normal file
70
phpmon-common/Core/Paths.swift
Normal file
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
21
phpmon-common/PHP/Homebrew/HomebrewService.swift
Normal file
21
phpmon-common/PHP/Homebrew/HomebrewService.swift
Normal file
@ -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?
|
||||
}
|
170
phpmon-common/PHP/PHP Version/PhpEnv.swift
Normal file
170
phpmon-common/PHP/PHP Version/PhpEnv.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
176
phpmon-common/PHP/PHP Version/PhpVersionNumber.swift
Normal file
176
phpmon-common/PHP/PHP Version/PhpVersionNumber.swift
Normal file
@ -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 = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
|
||||
// TODO: (5.1) Handle these cases (even though I suspect these are uncommon)
|
||||
/*
|
||||
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
*/
|
||||
}
|
||||
|
||||
public static func 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
|
||||
}
|
||||
}
|
@ -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 []
|
||||
}
|
||||
|
@ -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])
|
||||
)!
|
||||
}
|
||||
}
|
||||
|
61
phpmon-common/PHP/Switcher/InternalSwitcher.swift
Normal file
61
phpmon-common/PHP/Switcher/InternalSwitcher.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
15
phpmon-common/PHP/Switcher/PhpSwitcher.swift
Normal file
15
phpmon-common/PHP/Switcher/PhpSwitcher.swift
Normal file
@ -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)
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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"])
|
||||
}
|
||||
|
||||
}
|
||||
|
276
phpmon-tests/PhpVersionNumberTest.swift
Normal file
276
phpmon-tests/PhpVersionNumberTest.swift
Normal file
@ -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
|
||||
)
|
||||
}
|
||||
}
|
135
phpmon-tests/Test Files/brew-services.json
Normal file
135
phpmon-tests/Test Files/brew-services.json
Normal file
@ -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
|
||||
}
|
||||
]
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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."))
|
||||
}
|
||||
|
||||
|
24
phpmon/Assets.xcassets/Checkmark.imageset/Contents.json
vendored
Normal file
24
phpmon/Assets.xcassets/Checkmark.imageset/Contents.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/Checkmark.imageset/check.svg
vendored
Normal file
1
phpmon/Assets.xcassets/Checkmark.imageset/check.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M435.848 83.466L172.804 346.51l-96.652-96.652c-4.686-4.686-12.284-4.686-16.971 0l-28.284 28.284c-4.686 4.686-4.686 12.284 0 16.971l133.421 133.421c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-28.284-28.284c-4.686-4.686-12.284-4.686-16.97 0z"/></svg>
|
After Width: | Height: | Size: 497 B |
38
phpmon/Assets.xcassets/IconColorGreen.colorset/Contents.json
Normal file
38
phpmon/Assets.xcassets/IconColorGreen.colorset/Contents.json
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
38
phpmon/Assets.xcassets/IconColorRed.colorset/Contents.json
Normal file
38
phpmon/Assets.xcassets/IconColorRed.colorset/Contents.json
Normal file
@ -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
|
||||
}
|
||||
}
|
24
phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json
vendored
Normal file
24
phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/ServiceLoading.imageset/question-circle.svg
vendored
Normal file
1
phpmon/Assets.xcassets/ServiceLoading.imageset/question-circle.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 448c-110.532 0-200-89.431-200-200 0-110.495 89.472-200 200-200 110.491 0 200 89.471 200 200 0 110.53-89.431 200-200 200zm107.244-255.2c0 67.052-72.421 68.084-72.421 92.863V300c0 6.627-5.373 12-12 12h-45.647c-6.627 0-12-5.373-12-12v-8.659c0-35.745 27.1-50.034 47.579-61.516 17.561-9.845 28.324-16.541 28.324-29.579 0-17.246-21.999-28.693-39.784-28.693-23.189 0-33.894 10.977-48.942 29.969-4.057 5.12-11.46 6.071-16.666 2.124l-27.824-21.098c-5.107-3.872-6.251-11.066-2.644-16.363C184.846 131.491 214.94 112 261.794 112c49.071 0 101.45 38.304 101.45 88.8zM298 368c0 23.159-18.841 42-42 42s-42-18.841-42-42 18.841-42 42-42 42 18.841 42 42z"/></svg>
|
After Width: | Height: | Size: 966 B |
24
phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json
vendored
Normal file
24
phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/ServiceOff.imageset/times-circle.svg
vendored
Normal file
1
phpmon/Assets.xcassets/ServiceOff.imageset/times-circle.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z"/></svg>
|
After Width: | Height: | Size: 685 B |
24
phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json
vendored
Normal file
24
phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/ServiceOn.imageset/check-circle.svg
vendored
Normal file
1
phpmon/Assets.xcassets/ServiceOn.imageset/check-circle.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z"/></svg>
|
After Width: | Height: | Size: 718 B |
@ -16,6 +16,7 @@
|
||||
<p><b>Want to spread the love?</b> Leave a <a href="https://github.com/nicoverbruggen/phpmon">star on GitHub</a>!</p>
|
||||
<p><b>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p>
|
||||
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p>
|
||||
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
|
||||
<br>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
47
phpmon/Domain/Core/AppDelegate+InterApp.swift
Normal file
47
phpmon/Domain/Core/AppDelegate+InterApp.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -4,6 +4,7 @@
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@ -49,11 +50,31 @@
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Sites" id="YTZ-bb-TOG">
|
||||
<items>
|
||||
<menuItem title="Reload Site List" keyEquivalent="r" id="Ema-AU-Nbr">
|
||||
<menuItem title="add-as-link" keyEquivalent="n" id="du1-bO-N2U" userLabel="Add Link" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_add_folder_as_link"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="addSiteLinkPressed:" target="Voe-Tx-rLC" id="DzS-MY-6g0"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="reload-list" keyEquivalent="r" id="Ema-AU-Nbr" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_reload_site_list"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="reloadSiteListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="2ux-8Q-UjK"/>
|
||||
<menuItem title="focus-find" keyEquivalent="f" id="I95-fb-EL7" customClass="LocalizedMenuItem" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizationKey" value="mm_find_in_site_list"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="focusSearchField:" target="Voe-Tx-rLC" id="O8j-1B-hll"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
@ -298,7 +319,7 @@
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-484" y="32"/>
|
||||
<point key="canvasLocation" x="-495" y="-44"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="PQa-AT-b2a">
|
||||
@ -334,11 +355,14 @@
|
||||
<objects>
|
||||
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" wantsLayer="YES" id="Pf1-A5-3Xz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="574" height="498"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="550" height="498"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView distribution="fillEqually" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="k57-O3-Yyj">
|
||||
<rect key="frame" x="0.0" y="15" width="574" height="468"/>
|
||||
<rect key="frame" x="0.0" y="15" width="550" height="468"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="550" id="eOC-yS-nl6"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@ -371,6 +395,12 @@
|
||||
</view>
|
||||
<toolbar key="toolbar" implicitIdentifier="594015E3-8428-4926-9341-4B8CE4C7E373" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconOnly" sizeMode="regular" id="OOz-oZ-vlN">
|
||||
<allowedToolbarItems>
|
||||
<toolbarItem implicitItemIdentifier="5B9DBBA8-D173-4EAF-807C-C6EA0B4D806A" label="Add Link" paletteLabel="Add Link" tag="-1" bordered="YES" sizingBehavior="auto" id="GsC-ra-40U">
|
||||
<imageReference key="image" image="plus" catalog="system" symbolScale="medium"/>
|
||||
<connections>
|
||||
<action selector="pressedAddLink:" target="8Ec-9q-82s" id="H0o-No-x4M"/>
|
||||
</connections>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="B734CDE2-70E9-45A8-B1B3-5A5DE156621D" label="Reload" paletteLabel="Reload" tag="-1" bordered="YES" sizingBehavior="auto" id="YtK-vM-5y7">
|
||||
<imageReference key="image" image="arrow.clockwise" catalog="system" symbolScale="medium"/>
|
||||
<connections>
|
||||
@ -391,6 +421,7 @@
|
||||
</searchToolbarItem>
|
||||
</allowedToolbarItems>
|
||||
<defaultToolbarItems>
|
||||
<toolbarItem reference="GsC-ra-40U"/>
|
||||
<toolbarItem reference="YtK-vM-5y7"/>
|
||||
<searchToolbarItem reference="Q7Z-fw-lB9"/>
|
||||
</defaultToolbarItems>
|
||||
@ -406,30 +437,199 @@
|
||||
</windowController>
|
||||
<customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-374" y="773.5"/>
|
||||
<point key="canvasLocation" x="-374" y="758"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="HTI-x5-rOp">
|
||||
<objects>
|
||||
<windowController storyboardIdentifier="addSiteWindow" id="N1O-Nj-C2V" sceneMemberID="viewController">
|
||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="yLy-XT-fuq">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
|
||||
<view key="contentView" id="7Is-aK-lDv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="N1O-Nj-C2V" id="CvY-PZ-Y6C"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="glS-wF-sEU" kind="relationship" relationship="window.shadowedContentViewController" id="6Sa-w0-Uov"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-339" y="1147"/>
|
||||
</scene>
|
||||
<!--Add SiteVC-->
|
||||
<scene sceneID="6JC-H6-u4K">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="newSiteLink" id="glS-wF-sEU" customClass="AddSiteVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="JJJ-T9-Yuv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
|
||||
<rect key="frame" x="13" y="13" width="103" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="pressedCreateLink:" target="glS-wF-sEU" id="Vh7-K5-ubM"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
||||
<rect key="frame" x="391" y="13" width="76" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
|
||||
<rect key="frame" x="20" y="156" width="440" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a potential domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="glS-wF-sEU" id="Dyf-0M-Gwj"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
|
||||
<rect key="frame" x="18" y="134" width="444" height="14"/>
|
||||
<textFieldCell key="cell" title="FOLDER_AVAILABLE" id="bJr-s6-tdP">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KZf-b0-9cm">
|
||||
<rect key="frame" x="18" y="101" width="227" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
|
||||
<rect key="frame" x="18" y="66" width="444" height="28"/>
|
||||
<textFieldCell key="cell" title="Securing a site requires administrative privileges.
You will be prompted for your password or Touch ID." id="4gd-KM-5Fu">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6JT-Vt-3q0">
|
||||
<rect key="frame" x="20" y="185" width="440" height="22"/>
|
||||
<pathCell key="cell" selectable="YES" refusesFirstResponder="YES" alignment="left" id="m8d-XF-kh9">
|
||||
<font key="font" metaFont="system"/>
|
||||
<url key="url" string="file:///Users/nicoverbruggen/Code/nicoverbruggen.be/"/>
|
||||
</pathCell>
|
||||
</pathControl>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
|
||||
<rect key="frame" x="18" y="215" width="87" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Link a Folder" id="S4j-ZC-ddT">
|
||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
||||
<rect key="frame" x="115" y="23" width="128" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That link already exists." id="jOt-n6-TQf">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="VzR-5a-cmT" firstAttribute="trailing" secondItem="ZX9-s1-23i" secondAttribute="trailing" id="06B-dj-IBU"/>
|
||||
<constraint firstItem="ZX9-s1-23i" firstAttribute="top" secondItem="6JT-Vt-3q0" secondAttribute="bottom" constant="8" symbolic="YES" id="0QU-nI-sYv"/>
|
||||
<constraint firstAttribute="bottom" secondItem="SwS-o8-pbl" secondAttribute="bottom" constant="20" symbolic="YES" id="24Z-vC-4E8"/>
|
||||
<constraint firstItem="900-Z2-tID" firstAttribute="centerY" secondItem="PVw-cM-qAB" secondAttribute="centerY" id="578-2f-4x8"/>
|
||||
<constraint firstItem="ZX9-s1-23i" firstAttribute="leading" secondItem="6JT-Vt-3q0" secondAttribute="trailing" constant="-440" id="6eF-GS-Xcn"/>
|
||||
<constraint firstItem="SwS-o8-pbl" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="900-Z2-tID" secondAttribute="trailing" constant="10" id="9uc-R7-CZk"/>
|
||||
<constraint firstItem="6JT-Vt-3q0" firstAttribute="top" secondItem="P0B-Ht-R8n" secondAttribute="bottom" constant="8" symbolic="YES" id="DGN-4k-X0h"/>
|
||||
<constraint firstItem="P0B-Ht-R8n" firstAttribute="top" secondItem="JJJ-T9-Yuv" secondAttribute="top" constant="20" symbolic="YES" id="F2r-6E-qxh"/>
|
||||
<constraint firstItem="mmQ-7e-dlb" firstAttribute="top" secondItem="KZf-b0-9cm" secondAttribute="bottom" constant="8" symbolic="YES" id="G21-Vd-tgl"/>
|
||||
<constraint firstItem="900-Z2-tID" firstAttribute="leading" secondItem="PVw-cM-qAB" secondAttribute="trailing" constant="8" symbolic="YES" id="QzV-vP-fbq"/>
|
||||
<constraint firstItem="VzR-5a-cmT" firstAttribute="leading" secondItem="ZX9-s1-23i" secondAttribute="leading" id="UPN-Ad-j3X"/>
|
||||
<constraint firstItem="KZf-b0-9cm" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="Vab-wq-9Nc"/>
|
||||
<constraint firstAttribute="bottom" secondItem="PVw-cM-qAB" secondAttribute="bottom" constant="20" symbolic="YES" id="VsP-Q0-zRW"/>
|
||||
<constraint firstItem="ZX9-s1-23i" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="bJ4-Yr-4ah"/>
|
||||
<constraint firstItem="KZf-b0-9cm" firstAttribute="top" secondItem="VzR-5a-cmT" secondAttribute="bottom" constant="16" id="bdw-P7-FLz"/>
|
||||
<constraint firstAttribute="trailing" secondItem="SwS-o8-pbl" secondAttribute="trailing" constant="20" symbolic="YES" id="bkx-g2-WCM"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6JT-Vt-3q0" secondAttribute="trailing" constant="20" symbolic="YES" id="ctg-Gt-34Y"/>
|
||||
<constraint firstItem="PVw-cM-qAB" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="fE5-T7-e8z"/>
|
||||
<constraint firstItem="mmQ-7e-dlb" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="fKH-1r-MIf"/>
|
||||
<constraint firstAttribute="trailing" secondItem="mmQ-7e-dlb" secondAttribute="trailing" constant="20" symbolic="YES" id="hjv-Xq-cxV"/>
|
||||
<constraint firstItem="6JT-Vt-3q0" firstAttribute="leading" secondItem="P0B-Ht-R8n" secondAttribute="leading" id="jxP-vM-eA9"/>
|
||||
<constraint firstItem="P0B-Ht-R8n" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="msC-eG-Fop"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="P0B-Ht-R8n" secondAttribute="trailing" constant="20" symbolic="YES" id="nvj-Ij-dcd"/>
|
||||
<constraint firstItem="VzR-5a-cmT" firstAttribute="top" secondItem="ZX9-s1-23i" secondAttribute="bottom" constant="8" symbolic="YES" id="sVP-EV-07F"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ZX9-s1-23i" secondAttribute="trailing" constant="20" symbolic="YES" id="tZ3-2X-JC9"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="KZf-b0-9cm" secondAttribute="trailing" constant="20" symbolic="YES" id="zq0-Ce-sCs"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="buttonCancel" destination="SwS-o8-pbl" id="N1v-uy-2Mi"/>
|
||||
<outlet property="buttonCreateLink" destination="PVw-cM-qAB" id="0Oo-xW-He7"/>
|
||||
<outlet property="buttonSecure" destination="KZf-b0-9cm" id="5A7-Bn-NB7"/>
|
||||
<outlet property="linkName" destination="ZX9-s1-23i" id="yT6-80-Zr1"/>
|
||||
<outlet property="pathControl" destination="6JT-Vt-3q0" id="f5K-8h-VOd"/>
|
||||
<outlet property="previewText" destination="VzR-5a-cmT" id="qwd-wX-645"/>
|
||||
<outlet property="textFieldError" destination="900-Z2-tID" id="qUk-FE-IKW"/>
|
||||
<outlet property="textFieldSecure" destination="mmQ-7e-dlb" id="LeA-YS-hRM"/>
|
||||
<outlet property="textFieldTitle" destination="P0B-Ht-R8n" id="Qh8-qv-6iR"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="277" y="1137.5"/>
|
||||
</scene>
|
||||
<!--Site ListVC-->
|
||||
<scene sceneID="aZt-6w-TFl">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="siteList" id="JZI-Vd-9oq" customClass="SiteListVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController identifier="siteList" storyboardIdentifier="siteList" id="JZI-Vd-9oq" customClass="SiteListVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="rIZ-4U-bhj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="550" height="309"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<customView id="j65-Lf-0lG">
|
||||
<rect key="frame" x="9" y="0.0" width="581" height="203"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
</customView>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
|
||||
<rect key="frame" x="0.0" y="0.0" width="550" height="309"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="309"/>
|
||||
<clipView key="contentView" id="6IL-DW-37w">
|
||||
<rect key="frame" x="1" y="1" width="548" height="307"/>
|
||||
<rect key="frame" x="1" y="1" width="598" height="307"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" rowHeight="54" rowSizeStyle="automatic" viewBased="YES" id="cp3-34-pQj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="548" height="307"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="598" height="307"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<size key="intercellSpacing" width="17" height="0.0"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||
<tableColumns>
|
||||
<tableColumn width="536" minWidth="40" maxWidth="10000" id="oeH-B2-0rA">
|
||||
<tableColumn width="586" minWidth="40" maxWidth="10000" id="oeH-B2-0rA">
|
||||
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
|
||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
|
||||
@ -442,7 +642,7 @@
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<tableCellView identifier="siteItem" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListCell" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="0.0" width="531" height="54"/>
|
||||
<rect key="frame" x="8" y="0.0" width="581" height="54"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
||||
@ -470,7 +670,7 @@
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Lock" id="aJ0-ia-YrZ"/>
|
||||
</imageView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jKi-Ls-7FZ">
|
||||
<rect key="frame" x="459" y="28" width="64" height="11"/>
|
||||
<rect key="frame" x="474" y="28" width="64" height="11"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="DRIVER TYPE" id="fjd-eb-itv">
|
||||
<font key="font" metaFont="miniSystem"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
@ -478,7 +678,7 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TbX-e2-3QL">
|
||||
<rect key="frame" x="459" y="15" width="36" height="14"/>
|
||||
<rect key="frame" x="474" y="15" width="36" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Driver" id="GMt-SG-vFl">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@ -486,10 +686,10 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="syz-LF-l6P">
|
||||
<rect key="frame" x="0.0" y="-2" width="531" height="5"/>
|
||||
<rect key="frame" x="0.0" y="-2" width="581" height="5"/>
|
||||
</box>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0NQ-ZD-CqD">
|
||||
<rect key="frame" x="435" y="18" width="18" height="18"/>
|
||||
<rect key="frame" x="450" y="18" width="18" height="18"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="18" id="Suw-gm-AEi"/>
|
||||
<constraint firstAttribute="height" constant="18" id="qO6-vg-5nC"/>
|
||||
@ -497,59 +697,63 @@
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="IconLinked" id="2ng-pK-kvv"/>
|
||||
<color key="contentTintColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
</imageView>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="ypa-iv-wLD">
|
||||
<rect key="frame" x="211" y="18" width="18" height="18"/>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="3xt-wC-hUJ">
|
||||
<rect key="frame" x="363" y="18" width="75" height="18"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="18" id="jKJ-Xn-BPA"/>
|
||||
<constraint firstAttribute="height" constant="18" id="lSH-of-WzD"/>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="75" id="VI8-MP-7Hv"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSCaution" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="9XB-KO-aSI">
|
||||
<buttonCell key="cell" type="inline" title=" PHP X.X" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="anZ-hP-G0R">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<font key="font" metaFont="smallSystemBold"/>
|
||||
</buttonCell>
|
||||
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<connections>
|
||||
<action selector="pressedPhpVersion:" target="5GY-nN-BWd" id="mB5-WD-aZy"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="150" translatesAutoresizingMaskIntoConstraints="NO" id="MD8-ef-Ht8">
|
||||
<rect key="frame" x="235" y="16" width="182" height="22"/>
|
||||
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Warning: This is a warning message. Please take this into account." id="iub-KH-clf">
|
||||
<font key="font" metaFont="system" size="9"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5aN-ZI-D7U">
|
||||
<rect key="frame" x="341" y="20" width="14" height="14"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="14" id="NKD-Pc-okU"/>
|
||||
<constraint firstAttribute="width" constant="14" id="wrl-lJ-3eN"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Checkmark" id="R5o-Cd-a91"/>
|
||||
<color key="contentTintColor" name="IconColorGreen"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" secondItem="MD8-ef-Ht8" secondAttribute="trailing" constant="20" id="1Rb-Or-Nnn"/>
|
||||
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" secondItem="3xt-wC-hUJ" secondAttribute="trailing" constant="12" id="2G8-Ow-FTu"/>
|
||||
<constraint firstItem="3xt-wC-hUJ" firstAttribute="leading" secondItem="5aN-ZI-D7U" secondAttribute="trailing" constant="8" symbolic="YES" id="39Z-nB-kXx"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="TbX-e2-3QL" secondAttribute="trailing" constant="20" symbolic="YES" id="3vE-LR-S7N"/>
|
||||
<constraint firstItem="TbX-e2-3QL" firstAttribute="leading" secondItem="0NQ-ZD-CqD" secondAttribute="trailing" constant="8" symbolic="YES" id="4cb-D9-8d1"/>
|
||||
<constraint firstItem="XJL-Uw-frD" firstAttribute="leading" secondItem="QPX-eu-eV8" secondAttribute="trailing" constant="10" id="55y-3V-RYt"/>
|
||||
<constraint firstItem="syz-LF-l6P" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" id="8QK-nf-Fiw"/>
|
||||
<constraint firstItem="QPX-eu-eV8" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="top" id="9QB-jZ-k1V"/>
|
||||
<constraint firstItem="ypa-iv-wLD" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="9d8-P2-iSk"/>
|
||||
<constraint firstItem="MD8-ef-Ht8" firstAttribute="leading" secondItem="ypa-iv-wLD" secondAttribute="trailing" constant="8" symbolic="YES" id="C90-wQ-3Gf"/>
|
||||
<constraint firstItem="QPX-eu-eV8" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" constant="10" id="GOj-sw-ZlZ"/>
|
||||
<constraint firstItem="TbX-e2-3QL" firstAttribute="top" secondItem="jKi-Ls-7FZ" secondAttribute="bottom" constant="-1" id="J29-wT-Uex"/>
|
||||
<constraint firstItem="CXK-Q9-CpO" firstAttribute="leading" secondItem="XJL-Uw-frD" secondAttribute="leading" id="Ojw-VZ-3EG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="syz-LF-l6P" secondAttribute="trailing" id="PWd-5k-AlD"/>
|
||||
<constraint firstItem="XJL-Uw-frD" firstAttribute="top" secondItem="5GY-nN-BWd" secondAttribute="top" constant="12" id="QeE-c7-I9U"/>
|
||||
<constraint firstAttribute="trailing" secondItem="jKi-Ls-7FZ" secondAttribute="trailing" constant="10" id="Uhk-Dy-c65"/>
|
||||
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="Utr-aa-tqX"/>
|
||||
<constraint firstItem="CXK-Q9-CpO" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="bottom" id="VKg-Vq-sYa"/>
|
||||
<constraint firstItem="5aN-ZI-D7U" firstAttribute="centerY" secondItem="3xt-wC-hUJ" secondAttribute="centerY" id="a6n-E2-i2x"/>
|
||||
<constraint firstItem="TbX-e2-3QL" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" constant="5" id="cN8-zO-fnc"/>
|
||||
<constraint firstAttribute="bottom" secondItem="syz-LF-l6P" secondAttribute="bottom" id="gj7-cJ-Lle"/>
|
||||
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="CXK-Q9-CpO" secondAttribute="trailing" constant="8" symbolic="YES" id="iEd-Y3-zhp"/>
|
||||
<constraint firstItem="ypa-iv-wLD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="30" id="koV-Sj-tO8"/>
|
||||
<constraint firstItem="MD8-ef-Ht8" firstAttribute="centerY" secondItem="ypa-iv-wLD" secondAttribute="centerY" id="lIN-pm-mCo"/>
|
||||
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="8" symbolic="YES" id="lLA-Jx-Q4W"/>
|
||||
<constraint firstItem="3xt-wC-hUJ" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="vhb-WC-3NC"/>
|
||||
<constraint firstAttribute="trailing" secondItem="jKi-Ls-7FZ" secondAttribute="trailing" constant="45" id="vwD-Sg-Lzc"/>
|
||||
<constraint firstItem="jKi-Ls-7FZ" firstAttribute="leading" secondItem="TbX-e2-3QL" secondAttribute="leading" id="zjN-s3-2Ww"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="buttonWarning" destination="ypa-iv-wLD" id="NwX-H3-8um"/>
|
||||
<outlet property="buttonPhpVersion" destination="3xt-wC-hUJ" id="LpB-7n-qUr"/>
|
||||
<outlet property="imageViewLock" destination="QPX-eu-eV8" id="Nnh-kB-adG"/>
|
||||
<outlet property="imageViewPhpVersionOK" destination="5aN-ZI-D7U" id="ePz-Cb-dWk"/>
|
||||
<outlet property="imageViewType" destination="0NQ-ZD-CqD" id="Cph-FN-LaY"/>
|
||||
<outlet property="labelDriver" destination="TbX-e2-3QL" id="qJh-Ak-Dge"/>
|
||||
<outlet property="labelDriverType" destination="jKi-Ls-7FZ" id="ZTq-pP-qUC"/>
|
||||
<outlet property="labelPathName" destination="CXK-Q9-CpO" id="iVZ-cL-azB"/>
|
||||
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
|
||||
<outlet property="labelWarning" destination="MD8-ef-Ht8" id="Faw-CY-9R5"/>
|
||||
</connections>
|
||||
</tableCellView>
|
||||
</prototypeCellViews>
|
||||
@ -564,10 +768,10 @@
|
||||
</clipView>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="300" id="R3Z-g3-tYQ"/>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="550" id="iRQ-sz-oyv"/>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="600" id="iRQ-sz-oyv"/>
|
||||
</constraints>
|
||||
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
|
||||
<rect key="frame" x="1" y="293" width="548" height="15"/>
|
||||
<rect key="frame" x="1" y="293" width="598" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
|
||||
@ -576,7 +780,7 @@
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
|
||||
<rect key="frame" x="260" y="150" width="30" height="30"/>
|
||||
<rect key="frame" x="285" y="150" width="30" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
|
||||
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
||||
@ -599,13 +803,17 @@
|
||||
</viewController>
|
||||
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="288" y="764.5"/>
|
||||
<point key="canvasLocation" x="251" y="741.5"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Checkmark" width="512" height="512"/>
|
||||
<image name="IconLinked" width="512" height="512"/>
|
||||
<image name="Lock" width="30" height="30"/>
|
||||
<image name="NSCaution" width="32" height="32"/>
|
||||
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
|
||||
<image name="plus" catalog="system" width="14" height="13"/>
|
||||
<namedColor name="IconColorGreen">
|
||||
<color red="0.24699999392032623" green="0.69700002670288086" blue="0.50099998712539673" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
64
phpmon/Domain/Core/InterAppHandler.swift
Normal file
64
phpmon/Domain/Core/InterAppHandler.swift
Normal file
@ -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.")
|
||||
}
|
||||
}),
|
||||
]}
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -16,3 +16,14 @@ extension NSMenu {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@IBDesignable class LocalizedMenuItem: NSMenuItem {
|
||||
|
||||
@IBInspectable
|
||||
var localizationKey: String? {
|
||||
didSet {
|
||||
self.title = localizationKey?.localized ?? self.title
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -59,6 +59,5 @@ class Alert {
|
||||
secondButtonTitle: "",
|
||||
style: style
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
29
phpmon/Domain/Helpers/Async.swift
Normal file
29
phpmon/Domain/Helpers/Async.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ class LocalNotification {
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.add(request) { (error) in
|
||||
if error != nil {
|
||||
print(error!)
|
||||
Log.err(error!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")!
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 (?<version>(\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 (?<version>(\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])
|
||||
}
|
||||
|
||||
}
|
||||
|
78
phpmon/Domain/Integrations/Composer/ComposerJson.swift
Normal file
78
phpmon/Domain/Integrations/Composer/ComposerJson.swift
Normal file
@ -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<String, String>?
|
||||
let devDependencies: Dictionary<String, String>?
|
||||
let configuration: Config?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case dependencies = "require"
|
||||
case devDependencies = "require-dev"
|
||||
case configuration = "config"
|
||||
}
|
||||
|
||||
struct Config: Decodable {
|
||||
let platform: Platform?
|
||||
}
|
||||
struct Platform: Decodable {
|
||||
let php: String?
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/**
|
||||
Checks what the PHP version constraint is.
|
||||
Returns a tuple (constraint, location of constraint).
|
||||
*/
|
||||
public func getPhpVersion() -> (String, 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
82
phpmon/Domain/Integrations/Composer/PhpFrameworks.swift
Normal file
82
phpmon/Domain/Integrations/Composer/PhpFrameworks.swift
Normal file
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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?
|
||||
|
118
phpmon/Domain/Menu/MainMenu+Startup.swift
Normal file
118
phpmon/Domain/Menu/MainMenu+Startup.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
||||
|
||||
// Tell php-cgi to run the PHP and output as an .html file
|
||||
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
114
phpmon/Domain/Menu/ServicesView.swift
Normal file
114
phpmon/Domain/Menu/ServicesView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
150
phpmon/Domain/Menu/ServicesView.xib
Normal file
150
phpmon/Domain/Menu/ServicesView.xib
Normal file
@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner"/>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customView id="c22-O7-iKe" customClass="ServicesView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="340" height="46"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
|
||||
<rect key="frame" x="30" y="3" width="280" height="40"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
|
||||
<rect key="frame" x="0.0" y="4" width="80" height="32"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="At1-ch-qv2">
|
||||
<rect key="frame" x="25" y="18" width="31" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="PHP" id="LKe-C4-jxo">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="tko-cP-XSz">
|
||||
<rect key="frame" x="28" y="0.0" width="24" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="16" id="Fxu-6h-A2h"/>
|
||||
<constraint firstAttribute="width" constant="24" id="hOc-Ur-dmA"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="vjB-6Z-3xR"/>
|
||||
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<stackView distribution="fillEqually" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g4d-4N-NkC">
|
||||
<rect key="frame" x="100" y="4" width="80" height="32"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7um-XA-djV">
|
||||
<rect key="frame" x="20" y="18" width="40" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="NGINX" id="Qfq-Bl-yuh">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ZqW-6d-vpe">
|
||||
<rect key="frame" x="32" y="0.0" width="16" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="16" id="EPG-jm-7Xs"/>
|
||||
<constraint firstAttribute="width" constant="16" id="iif-kT-phn"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="JmQ-dU-ip7"/>
|
||||
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
|
||||
<rect key="frame" x="200" y="4" width="80" height="32"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
|
||||
<rect key="frame" x="9" y="18" width="62" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="DNSMASQ" id="lGh-MT-TgI">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="DcG-x3-lvy">
|
||||
<rect key="frame" x="32" y="0.0" width="16" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="16" id="AKl-Gq-RtM"/>
|
||||
<constraint firstAttribute="height" constant="16" id="q2g-Ua-eIJ"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ServiceLoading" id="Ign-Cq-DKf"/>
|
||||
<color key="contentTintColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="40" id="2EU-Fd-hMg"/>
|
||||
<constraint firstItem="nWj-33-m8Q" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="CAY-Pw-B8n"/>
|
||||
<constraint firstAttribute="bottom" secondItem="doH-ww-BDw" secondAttribute="bottom" constant="4" id="Dq4-M6-1Wf"/>
|
||||
<constraint firstItem="g4d-4N-NkC" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="bls-fM-H4b"/>
|
||||
<constraint firstAttribute="bottom" secondItem="nWj-33-m8Q" secondAttribute="bottom" constant="4" id="f6j-eI-wiH"/>
|
||||
<constraint firstAttribute="bottom" secondItem="g4d-4N-NkC" secondAttribute="bottom" constant="4" id="faS-Mo-Qa2"/>
|
||||
<constraint firstItem="doH-ww-BDw" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="gL3-5S-OKo"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="TnH-dX-qaQ" secondAttribute="trailing" constant="30" id="3dD-wf-5pS"/>
|
||||
<constraint firstItem="TnH-dX-qaQ" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="3" id="JmY-D0-uAy"/>
|
||||
<constraint firstItem="TnH-dX-qaQ" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="30" id="S8i-CD-j3h"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TnH-dX-qaQ" secondAttribute="bottom" constant="3" id="fDc-OY-YL0"/>
|
||||
<constraint firstItem="TnH-dX-qaQ" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="fFF-rl-3s4"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="imageViewDnsmasq" destination="DcG-x3-lvy" id="XxJ-kZ-bdO"/>
|
||||
<outlet property="imageViewNginx" destination="ZqW-6d-vpe" id="Wil-Ug-8Kb"/>
|
||||
<outlet property="imageViewPhp" destination="tko-cP-XSz" id="q7L-HK-7Pj"/>
|
||||
<outlet property="textFieldPhp" destination="At1-ch-qv2" id="Guk-hr-f1T"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-64" y="195"/>
|
||||
</customView>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ServiceLoading" width="512" height="512"/>
|
||||
</resources>
|
||||
</document>
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@ -10,11 +10,11 @@
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customView id="c22-O7-iKe" customClass="StatsView" customModule="PHP_Monitor" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="341" height="55"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="340" height="55"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
|
||||
<rect key="frame" x="30" y="6" width="281" height="43"/>
|
||||
<rect key="frame" x="30" y="6" width="280" height="43"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
|
||||
<rect key="frame" x="0.0" y="4" width="87" height="35"/>
|
||||
@ -75,10 +75,10 @@
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
|
||||
<rect key="frame" x="204" y="4" width="77" height="35"/>
|
||||
<rect key="frame" x="204" y="4" width="76" height="35"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
|
||||
<rect key="frame" x="-1" y="21" width="79" height="14"/>
|
||||
<rect key="frame" x="-2" y="21" width="79" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX UPLOAD" id="lGh-MT-TgI">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
@ -86,7 +86,7 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eHT-tr-Kwx">
|
||||
<rect key="frame" x="11" y="0.0" width="55" height="19"/>
|
||||
<rect key="frame" x="10" y="0.0" width="55" height="19"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="1iA-Ri-zYY">
|
||||
<font key="font" metaFont="systemMedium" size="16"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@ -138,7 +138,7 @@
|
||||
<outlet property="titleMaxUpload" destination="Oef-6n-9QI" id="Q61-JI-RJq"/>
|
||||
<outlet property="titleMemLimit" destination="At1-ch-qv2" id="SQT-B9-sWS"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-84.5" y="44"/>
|
||||
<point key="canvasLocation" x="139" y="168"/>
|
||||
</customView>
|
||||
</objects>
|
||||
</document>
|
||||
|
@ -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..<App.shared.availablePhpVersions.count).reversed() {
|
||||
for index in (0..<PhpEnv.shared.availablePhpVersions.count).reversed() {
|
||||
|
||||
// Get the short and long version
|
||||
let shortVersion = App.shared.availablePhpVersions[index]
|
||||
let longVersion = App.shared.cachedPhpInstallations[shortVersion]!.longVersion
|
||||
let shortVersion = PhpEnv.shared.availablePhpVersions[index]
|
||||
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.longVersion
|
||||
|
||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||
let versionString = long ? longVersion : shortVersion
|
||||
let versionString = long ? longVersion.homebrewVersion : shortVersion
|
||||
|
||||
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
||||
let brew = (shortVersion == App.shared.brewPhpVersion) ? "php" : "php@\(shortVersion)"
|
||||
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
|
||||
let menuItem = PhpMenuItem(
|
||||
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
|
||||
action: (shortVersion == App.phpInstall?.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
|
||||
action: (shortVersion == PhpEnv.phpInstall.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
|
||||
)
|
||||
|
||||
menuItem.version = shortVersion
|
||||
|
33
phpmon/Domain/PHP/ActivePhpInstallation+Checks.swift
Normal file
33
phpmon/Domain/PHP/ActivePhpInstallation+Checks.swift
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// ActivePhpInstallation.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 21/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ActivePhpInstallation {
|
||||
|
||||
/**
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
17
phpmon/Domain/Preferences/CustomPrefs.swift
Normal file
17
phpmon/Domain/Preferences/CustomPrefs.swift
Normal file
@ -0,0 +1,17 @@
|
||||
//
|
||||
// CustomPrefs.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 03/01/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct CustomPrefs: Decodable {
|
||||
let scanApps: [String]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case scanApps = "scan_apps"
|
||||
}
|
||||
}
|
@ -8,26 +8,44 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
These are the keys used for every preference in the app.
|
||||
*/
|
||||
enum PreferenceName: String {
|
||||
case wasLaunchedBefore = "launched_before"
|
||||
case shouldDisplayDynamicIcon = "use_dynamic_icon"
|
||||
case shouldDisplayPhpHintInIcon = "add_php_to_icon"
|
||||
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
|
||||
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
|
||||
case useInternalSwitcher = "use_phpmon_switcher"
|
||||
case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch"
|
||||
case allowProtocolForIntegrations = "allow_protocol_for_integrations"
|
||||
case globalHotkey = "global_hotkey"
|
||||
}
|
||||
|
||||
/**
|
||||
These are internal stats. They NEVER get shared.
|
||||
*/
|
||||
enum InternalStats: String {
|
||||
case launchCount = "times_launched"
|
||||
case switchCount = "times_switched_versions"
|
||||
case didSeeSponsorEncouragement = "did_see_sponsor_encouragement"
|
||||
}
|
||||
|
||||
class Preferences {
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
static var shared = Preferences()
|
||||
|
||||
var customPreferences: CustomPrefs
|
||||
|
||||
var cachedPreferences: [PreferenceName: Any?]
|
||||
|
||||
public init() {
|
||||
Preferences.handleFirstTimeLaunch()
|
||||
cachedPreferences = Self.cache()
|
||||
customPreferences = CustomPrefs(scanApps: [])
|
||||
loadCustomPreferences()
|
||||
}
|
||||
|
||||
// MARK: - First Time Run
|
||||
@ -44,16 +62,24 @@ class Preferences {
|
||||
*/
|
||||
static func handleFirstTimeLaunch() {
|
||||
UserDefaults.standard.register(defaults: [
|
||||
/// Preferences
|
||||
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||
PreferenceName.shouldDisplayPhpHintInIcon.rawValue: true,
|
||||
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
|
||||
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true,
|
||||
PreferenceName.useInternalSwitcher.rawValue: false
|
||||
PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue: false,
|
||||
PreferenceName.allowProtocolForIntegrations.rawValue: true,
|
||||
/// Stats
|
||||
InternalStats.switchCount.rawValue: 0,
|
||||
InternalStats.launchCount.rawValue: 0,
|
||||
InternalStats.didSeeSponsorEncouragement.rawValue: false
|
||||
])
|
||||
|
||||
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
|
||||
return
|
||||
}
|
||||
print("Saving first-time preferences!")
|
||||
|
||||
Log.info("Saving first-time preferences!")
|
||||
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
@ -64,15 +90,33 @@ class Preferences {
|
||||
return Self.shared.cachedPreferences
|
||||
}
|
||||
|
||||
static var custom: CustomPrefs {
|
||||
return Self.shared.customPreferences
|
||||
}
|
||||
|
||||
/**
|
||||
Determine whether a particular preference is enabled.
|
||||
- Important: Requires the preference to have a corresponding boolean value, or a fatal error will be thrown.
|
||||
*/
|
||||
static func isEnabled(_ preference: PreferenceName) -> 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.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
116
phpmon/Domain/Preferences/Stats.swift
Normal file
116
phpmon/Domain/Preferences/Stats.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@
|
||||
<constraint firstItem="B8f-nb-Y0A" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="5" id="2Zu-h3-qb0"/>
|
||||
<constraint firstItem="iUx-vA-jg4" firstAttribute="leading" secondItem="gBj-K1-Q2I" secondAttribute="trailing" constant="12" symbolic="YES" id="3fW-pY-HBu"/>
|
||||
<constraint firstItem="gBj-K1-Q2I" firstAttribute="top" secondItem="B8f-nb-Y0A" secondAttribute="top" id="7JI-pU-DnQ"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="iUx-vA-jg4" secondAttribute="trailing" constant="112" id="AVQ-1M-kE4"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="iUx-vA-jg4" secondAttribute="trailing" constant="50" id="AVQ-1M-kE4"/>
|
||||
<constraint firstItem="iUx-vA-jg4" firstAttribute="top" secondItem="gBj-K1-Q2I" secondAttribute="top" id="O2C-aI-XFS"/>
|
||||
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="gBj-K1-Q2I" secondAttribute="bottom" constant="8" id="Sly-aj-yUl"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
|
||||
|
135
phpmon/Domain/Progress/ProgressWindow.storyboard
Normal file
135
phpmon/Domain/Progress/ProgressWindow.storyboard
Normal file
@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="pUZ-6w-gUX">
|
||||
<objects>
|
||||
<windowController storyboardIdentifier="progressWindow" id="LSr-Iw-X1T" customClass="ProgressWindowController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<window key="window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="none" frameAutosaveName="" titlebarAppearsTransparent="YES" titleVisibility="hidden" id="PD9-0p-i0S" customClass="NSPanel">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" rightStrut="YES" topStrut="YES"/>
|
||||
<rect key="contentRect" x="2080" y="1145" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
|
||||
<view key="contentView" id="QOA-qf-m1l">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="LSr-Iw-X1T" id="uEN-dQ-Jv4"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="c01-Tm-OtR" kind="relationship" relationship="window.shadowedContentViewController" id="uNS-tY-qB9"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="aGV-xt-u13" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-313" y="-6"/>
|
||||
</scene>
|
||||
<!--Progress View Controller-->
|
||||
<scene sceneID="XgN-R6-44T">
|
||||
<objects>
|
||||
<viewController id="c01-Tm-OtR" customClass="ProgressViewController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="0fd-qq-0ME">
|
||||
<rect key="frame" x="0.0" y="0.0" width="591" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" horizontalScrollElasticity="none" findBarPosition="aboveHorizontalRuler" translatesAutoresizingMaskIntoConstraints="NO" id="JK7-kL-1L3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="591" height="210"/>
|
||||
<clipView key="contentView" drawsBackground="NO" id="2Mc-oy-AzN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="591" height="210"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" smartInsertDelete="YES" id="d1T-N1-CRe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="591" height="210"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<size key="minSize" width="591" height="210"/>
|
||||
<size key="maxSize" width="757" height="10000000"/>
|
||||
<attributedString key="textStorage">
|
||||
<fragment content="$ ">
|
||||
<attributes>
|
||||
<color key="NSColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<font key="NSFont" size="10" name="Menlo-Regular"/>
|
||||
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0"/>
|
||||
</attributes>
|
||||
</fragment>
|
||||
</attributedString>
|
||||
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="51v-CN-AuA">
|
||||
<rect key="frame" x="-100" y="-100" width="225" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="lSO-JG-QOf">
|
||||
<rect key="frame" x="-100" y="-100" width="15" height="173"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9c0-5U-sVK">
|
||||
<rect key="frame" x="69" y="242" width="504" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="TITLE" id="oyI-D5-kEd">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7Be-eK-EcJ">
|
||||
<rect key="frame" x="69" y="226" width="504" height="14"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="500" id="0kb-4B-ZF3"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" title="DESC" id="V0K-KF-leA">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<imageView horizontalHuggingPriority="750" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="LiM-ZV-F8j">
|
||||
<rect key="frame" x="20" y="224" width="36" height="36"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="36" id="ddK-Ha-wqT"/>
|
||||
<constraint firstAttribute="width" constant="36" id="pHp-9H-nhF"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSInfo" id="GWi-hE-LOJ"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="9c0-5U-sVK" firstAttribute="top" secondItem="LiM-ZV-F8j" secondAttribute="top" constant="2" id="0YJ-RT-kYE"/>
|
||||
<constraint firstAttribute="bottom" secondItem="JK7-kL-1L3" secondAttribute="bottom" id="5Yv-2h-nea"/>
|
||||
<constraint firstItem="9c0-5U-sVK" firstAttribute="leading" secondItem="LiM-ZV-F8j" secondAttribute="trailing" constant="15" id="EUV-hs-udj"/>
|
||||
<constraint firstItem="LiM-ZV-F8j" firstAttribute="top" secondItem="0fd-qq-0ME" secondAttribute="top" constant="10" id="Ovb-Nu-ftR"/>
|
||||
<constraint firstItem="JK7-kL-1L3" firstAttribute="top" secondItem="7Be-eK-EcJ" secondAttribute="bottom" constant="16" id="PYn-gt-Akk"/>
|
||||
<constraint firstAttribute="trailing" secondItem="9c0-5U-sVK" secondAttribute="trailing" constant="20" symbolic="YES" id="TgO-ax-DGp"/>
|
||||
<constraint firstItem="LiM-ZV-F8j" firstAttribute="leading" secondItem="0fd-qq-0ME" secondAttribute="leading" constant="20" symbolic="YES" id="ang-xM-Zmh"/>
|
||||
<constraint firstAttribute="trailing" secondItem="JK7-kL-1L3" secondAttribute="trailing" id="atA-67-BQF"/>
|
||||
<constraint firstItem="JK7-kL-1L3" firstAttribute="leading" secondItem="0fd-qq-0ME" secondAttribute="leading" id="gwR-eH-CmM"/>
|
||||
<constraint firstItem="7Be-eK-EcJ" firstAttribute="top" secondItem="9c0-5U-sVK" secondAttribute="bottom" constant="2" id="jdR-1x-xta"/>
|
||||
<constraint firstItem="7Be-eK-EcJ" firstAttribute="leading" secondItem="LiM-ZV-F8j" secondAttribute="trailing" constant="15" id="loj-L6-5NK"/>
|
||||
<constraint firstAttribute="trailing" secondItem="7Be-eK-EcJ" secondAttribute="trailing" constant="20" symbolic="YES" id="sgd-u4-k0O"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="imageViewType" destination="LiM-ZV-F8j" id="yIU-aW-YFL"/>
|
||||
<outlet property="labelDescription" destination="7Be-eK-EcJ" id="qui-Ub-y5U"/>
|
||||
<outlet property="labelTitle" destination="9c0-5U-sVK" id="Iy3-Ym-pA9"/>
|
||||
<outlet property="textView" destination="d1T-N1-CRe" id="pWp-W1-aus"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="oqT-7w-frK" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="266.5" y="18"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="NSInfo" width="32" height="32"/>
|
||||
</resources>
|
||||
</document>
|
76
phpmon/Domain/Progress/ProgressWindow.swift
Normal file
76
phpmon/Domain/Progress/ProgressWindow.swift
Normal file
@ -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")
|
||||
}
|
||||
|
||||
}
|
139
phpmon/Domain/SiteList/AddSiteVC.swift
Normal file
139
phpmon/Domain/SiteList/AddSiteVC.swift
Normal file
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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!)
|
||||
}
|
||||
}
|
||||
|
29
phpmon/Domain/SwiftUI/PMHeaderView.swift
Normal file
29
phpmon/Domain/SwiftUI/PMHeaderView.swift
Normal file
@ -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) {}
|
||||
}
|
25
phpmon/Domain/SwiftUI/PMServicesView.swift
Normal file
25
phpmon/Domain/SwiftUI/PMServicesView.swift
Normal file
@ -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) {}
|
||||
}
|
29
phpmon/Domain/SwiftUI/PMStatsView.swift
Normal file
29
phpmon/Domain/SwiftUI/PMStatsView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
20
phpmon/Domain/SwiftUI/Preview.swift
Normal file
20
phpmon/Domain/SwiftUI/Preview.swift
Normal file
@ -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!
|
||||
}
|
||||
}
|
47
phpmon/Domain/Watcher/App+ConfigWatch.swift
Normal file
47
phpmon/Domain/Watcher/App+ConfigWatch.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
130
phpmon/Domain/Watcher/PhpConfigWatcher.swift
Normal file
130
phpmon/Domain/Watcher/PhpConfigWatcher.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -18,6 +18,19 @@
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.nicoverbruggen.phpmon</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>phpmon</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
@ -27,7 +40,7 @@
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2021 Nico Verbruggen. All rights reserved.</string>
|
||||
<string>Copyright © 2019-2022 Nico Verbruggen. All rights reserved.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
@ -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" = "<listening for keypress>";
|
||||
"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";
|
||||
|
Reference in New Issue
Block a user