🚀 Version 5.1
Merge branch 'dev/5.x'
14
DEVELOPER.md
@ -13,6 +13,20 @@ Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you
|
||||
|
||||
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
||||
|
||||
## 🚀 Release procedure
|
||||
|
||||
1. Merge into `main`
|
||||
2. Create tag
|
||||
3. Add changes to changelog + update security document
|
||||
4. Archive
|
||||
5. Notarize and prepare for own distribution
|
||||
6. After notarization, export .app
|
||||
7. Create zipped version
|
||||
8. Calculate SHA256: `openssl dgst -sha256 phpmon.zip`
|
||||
9. Upload to GitHub and add to tagged release
|
||||
10. Update Cask with new version + hash
|
||||
11. Check new version can be installed via Cask
|
||||
|
||||
## 🐛 Symbolication of crashes
|
||||
|
||||
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.
|
||||
|
@ -28,10 +28,13 @@
|
||||
C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */; };
|
||||
C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */; };
|
||||
C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */; };
|
||||
C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */; };
|
||||
C4080FF727BD8C6400BF2C6B /* BetterAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */; };
|
||||
C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */; };
|
||||
C4080FFB27BD956700BF2C6B /* BetterAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */; };
|
||||
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
|
||||
C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
|
||||
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
|
||||
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 */; };
|
||||
C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; };
|
||||
@ -62,6 +65,7 @@
|
||||
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 */; };
|
||||
C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */; };
|
||||
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; };
|
||||
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; };
|
||||
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; };
|
||||
@ -112,7 +116,7 @@
|
||||
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */; };
|
||||
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; };
|
||||
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; };
|
||||
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F7C275454A900D44ED0 /* ValetTest.swift */; };
|
||||
C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */; };
|
||||
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 */; };
|
||||
@ -128,6 +132,10 @@
|
||||
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 */; };
|
||||
C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */; };
|
||||
C4C1019627C659B7001FACC2 /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = C4C1019527C659B7001FACC2 /* HotKey */; };
|
||||
C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C1019A27C65C6F001FACC2 /* Process.swift */; };
|
||||
C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C1019A27C65C6F001FACC2 /* Process.swift */; };
|
||||
C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
|
||||
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; };
|
||||
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
|
||||
@ -137,9 +145,9 @@
|
||||
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; };
|
||||
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; };
|
||||
C4CE3BB827B31F2E0086CA49 /* MainMenu+Switcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */; };
|
||||
C4CE3BBA27B31F670086CA49 /* MainMenu+Composer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* MainMenu+Composer.swift */; };
|
||||
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */; };
|
||||
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */; };
|
||||
C4CE3BBC27B324250086CA49 /* MainMenu+Composer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* MainMenu+Composer.swift */; };
|
||||
C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* ComposerWindow.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 */; };
|
||||
@ -147,9 +155,12 @@
|
||||
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
|
||||
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
|
||||
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
|
||||
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; };
|
||||
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; };
|
||||
C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* ValetSite.swift */; };
|
||||
C4E4404727C56F4700D225E1 /* ValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* ValetSite.swift */; };
|
||||
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; };
|
||||
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
|
||||
C4EC1E6D279DF87A0010F296 /* 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 */; };
|
||||
@ -217,6 +228,8 @@
|
||||
C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CheckboxPreferenceView.xib; sourceTree = "<group>"; };
|
||||
C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectPreferenceView.swift; sourceTree = "<group>"; };
|
||||
C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarIcons.swift; sourceTree = "<group>"; };
|
||||
C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlert.swift; sourceTree = "<group>"; };
|
||||
C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlertVC.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@ -240,6 +253,7 @@
|
||||
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.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>"; };
|
||||
C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+FixMyValet.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>"; };
|
||||
@ -273,7 +287,7 @@
|
||||
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>"; };
|
||||
C4AF9F792754499000D44ED0 /* Valet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.swift; sourceTree = "<group>"; };
|
||||
C4AF9F7C275454A900D44ED0 /* ValetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetTest.swift; sourceTree = "<group>"; };
|
||||
C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetVersionExtractorTest.swift; sourceTree = "<group>"; };
|
||||
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@ -282,23 +296,25 @@
|
||||
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>"; };
|
||||
C4C1019A27C65C6F001FACC2 /* Process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Process.swift; sourceTree = "<group>"; };
|
||||
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Startup.swift"; sourceTree = "<group>"; };
|
||||
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Switcher.swift"; sourceTree = "<group>"; };
|
||||
C4CE3BB927B31F670086CA49 /* MainMenu+Composer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Composer.swift"; sourceTree = "<group>"; };
|
||||
C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerWindow.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>"; };
|
||||
C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWindowExtension.swift; sourceTree = "<group>"; };
|
||||
C4E4404527C56F4700D225E1 /* ValetSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetSite.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@ -309,7 +325,6 @@
|
||||
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>"; };
|
||||
C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = "<group>"; };
|
||||
@ -333,6 +348,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C4C1019627C659B7001FACC2 /* HotKey in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -389,6 +405,15 @@
|
||||
path = IAP;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4080FF827BD955900BF2C6B /* Notice */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */,
|
||||
C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */,
|
||||
);
|
||||
path = Notice;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C40C7F1C27720E1400DDDCDC /* Test Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -409,6 +434,7 @@
|
||||
C4B5853D2770FE3900DA4FBE /* Command.swift */,
|
||||
C4B5853B2770FE3900DA4FBE /* Paths.swift */,
|
||||
C4B5853C2770FE3900DA4FBE /* Shell.swift */,
|
||||
C4C1019A27C65C6F001FACC2 /* Process.swift */,
|
||||
C40C7F2F27722E8D00DDDCDC /* Logger.swift */,
|
||||
C417DC73277614690015E6EE /* Helpers.swift */,
|
||||
);
|
||||
@ -420,7 +446,6 @@
|
||||
children = (
|
||||
C4F8C0A522D4FA41002EFE61 /* README.md */,
|
||||
C4E713562570150F00007428 /* SECURITY.md */,
|
||||
C4F7807425D7F7E5000DBC97 /* RELEASE.md */,
|
||||
C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
|
||||
C4E713572570151400007428 /* docs */,
|
||||
C41C1B3522B0097F00E7CF16 /* phpmon */,
|
||||
@ -465,6 +490,7 @@
|
||||
C41E181722CB61EB0072CF09 /* Domain */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4080FF827BD955900BF2C6B /* Notice */,
|
||||
C4AF9F6B275445D300D44ED0 /* Integrations */,
|
||||
C4B13B1D25C4915000548C3A /* App */,
|
||||
C4D9ADBD27761084007277F4 /* PHP */,
|
||||
@ -513,10 +539,10 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */,
|
||||
C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */,
|
||||
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */,
|
||||
C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */,
|
||||
C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */,
|
||||
C4CE3BB927B31F670086CA49 /* MainMenu+Composer.swift */,
|
||||
C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */,
|
||||
C47331A1247093B7009A0597 /* StatusMenu.swift */,
|
||||
C48D0C9525CC80B100CC7490 /* HeaderView.swift */,
|
||||
C48D0C9925CC888B00CC7490 /* HeaderView.xib */,
|
||||
@ -538,7 +564,6 @@
|
||||
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
|
||||
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */,
|
||||
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */,
|
||||
C4EC1E6C279DF87A0010F296 /* Async.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@ -556,6 +581,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4AF9F792754499000D44ED0 /* Valet.swift */,
|
||||
C4E4404527C56F4700D225E1 /* ValetSite.swift */,
|
||||
);
|
||||
path = Valet;
|
||||
sourceTree = "<group>";
|
||||
@ -589,8 +615,8 @@
|
||||
C4811D2322D70A4700B5F6B3 /* App.swift */,
|
||||
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */,
|
||||
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
|
||||
C4D8016522B1584700C6DA1B /* Startup.swift */,
|
||||
C4EED88827A48778006D7272 /* InterAppHandler.swift */,
|
||||
C4D8016522B1584700C6DA1B /* Startup.swift */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
@ -607,6 +633,35 @@
|
||||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4C1019727C65A11001FACC2 /* Parsers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */,
|
||||
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
|
||||
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
|
||||
);
|
||||
path = Parsers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4C1019827C65A1A001FACC2 /* Versions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
|
||||
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */,
|
||||
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */,
|
||||
C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */,
|
||||
);
|
||||
path = Versions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4C1019927C65A4D001FACC2 /* Commands */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4F7809B25D80344000DBC97 /* CommandTest.swift */,
|
||||
);
|
||||
path = Commands;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4C8E81D276F5686003AC782 /* Watcher */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -626,6 +681,7 @@
|
||||
C4D89BC42783C98800A02B68 /* Composer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */,
|
||||
C4D89BC52783C99400A02B68 /* ComposerJson.swift */,
|
||||
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */,
|
||||
);
|
||||
@ -673,16 +729,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
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 */,
|
||||
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */,
|
||||
C40C7F1C27720E1400DDDCDC /* Test Files */,
|
||||
C4C1019927C65A4D001FACC2 /* Commands */,
|
||||
C4C1019827C65A1A001FACC2 /* Versions */,
|
||||
C4C1019727C65A11001FACC2 /* Parsers */,
|
||||
);
|
||||
path = "phpmon-tests";
|
||||
sourceTree = "<group>";
|
||||
@ -694,6 +745,7 @@
|
||||
C46FA23E246C358E00944F05 /* StringExtension.swift */,
|
||||
C48D0C9225CC804200CC7490 /* XibLoadable.swift */,
|
||||
C42759662627662800093CAE /* NSMenuExtension.swift */,
|
||||
C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -735,6 +787,9 @@
|
||||
C4F7807F25D7F84B000DBC97 /* PBXTargetDependency */,
|
||||
);
|
||||
name = "phpmon-tests";
|
||||
packageProductDependencies = (
|
||||
C4C1019527C659B7001FACC2 /* HotKey */,
|
||||
);
|
||||
productName = "phpmon-tests";
|
||||
productReference = C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
@ -754,7 +809,6 @@
|
||||
};
|
||||
C4F7807825D7F84B000DBC97 = {
|
||||
CreatedOnToolsVersion = 12.4;
|
||||
TestTargetID = C41C1B3222B0097F00E7CF16;
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -827,6 +881,7 @@
|
||||
files = (
|
||||
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */,
|
||||
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
|
||||
C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */,
|
||||
C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
|
||||
C4B585412770FE3900DA4FBE /* Shell.swift in Sources */,
|
||||
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */,
|
||||
@ -835,10 +890,13 @@
|
||||
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
|
||||
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
|
||||
C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
|
||||
C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */,
|
||||
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
|
||||
C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */,
|
||||
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */,
|
||||
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
|
||||
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
|
||||
C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */,
|
||||
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */,
|
||||
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
|
||||
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
|
||||
@ -875,8 +933,8 @@
|
||||
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
|
||||
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
|
||||
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */,
|
||||
C4EC1E6D279DF87A0010F296 /* Async.swift in Sources */,
|
||||
C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */,
|
||||
C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */,
|
||||
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */,
|
||||
C4927F0B27B2DFC200C55AFD /* Errors.swift in Sources */,
|
||||
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */,
|
||||
@ -889,8 +947,9 @@
|
||||
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
|
||||
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
|
||||
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
|
||||
C4CE3BBA27B31F670086CA49 /* MainMenu+Composer.swift in Sources */,
|
||||
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */,
|
||||
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */,
|
||||
C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */,
|
||||
C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */,
|
||||
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
|
||||
C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */,
|
||||
@ -919,6 +978,7 @@
|
||||
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
|
||||
C493084B279F331F009C240B /* AddSiteVC.swift in Sources */,
|
||||
C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */,
|
||||
C4080FFB27BD956700BF2C6B /* BetterAlertVC.swift in Sources */,
|
||||
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
|
||||
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
|
||||
C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
|
||||
@ -931,10 +991,11 @@
|
||||
C4F319C927B034A500AFF46F /* Stats.swift in Sources */,
|
||||
C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */,
|
||||
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
|
||||
C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */,
|
||||
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
|
||||
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
|
||||
C40B24F527A3108B0018C7D2 /* Async.swift in Sources */,
|
||||
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
|
||||
C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */,
|
||||
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
|
||||
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
|
||||
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
|
||||
@ -952,9 +1013,10 @@
|
||||
C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */,
|
||||
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
|
||||
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */,
|
||||
C4CE3BBC27B324250086CA49 /* MainMenu+Composer.swift in Sources */,
|
||||
C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */,
|
||||
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */,
|
||||
C417DC75277614690015E6EE /* Helpers.swift in Sources */,
|
||||
C4080FF727BD8C6400BF2C6B /* BetterAlert.swift in Sources */,
|
||||
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
|
||||
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
|
||||
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */,
|
||||
@ -972,18 +1034,20 @@
|
||||
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
|
||||
C40B24F227A310770018C7D2 /* Events.swift in Sources */,
|
||||
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
|
||||
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */,
|
||||
C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */,
|
||||
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
|
||||
C4B585452770FE3900DA4FBE /* Command.swift in Sources */,
|
||||
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */,
|
||||
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
|
||||
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
|
||||
C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */,
|
||||
C4E4404727C56F4700D225E1 /* ValetSite.swift in Sources */,
|
||||
C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */,
|
||||
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
|
||||
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
|
||||
C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */,
|
||||
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */,
|
||||
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
|
||||
C4B585422770FE3900DA4FBE /* Shell.swift in Sources */,
|
||||
C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
|
||||
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */,
|
||||
@ -1143,7 +1207,8 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 610;
|
||||
CURRENT_PROJECT_VERSION = 715;
|
||||
DEBUG = YES;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = phpmon/Info.plist;
|
||||
@ -1152,7 +1217,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 5.0.1;
|
||||
MARKETING_VERSION = 5.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -1168,7 +1233,8 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 610;
|
||||
CURRENT_PROJECT_VERSION = 715;
|
||||
DEBUG = NO;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = phpmon/Info.plist;
|
||||
@ -1177,7 +1243,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 5.0.1;
|
||||
MARKETING_VERSION = 5.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -1188,7 +1254,6 @@
|
||||
C4F7808125D7F84B000DBC97 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
@ -1202,14 +1267,12 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PHP Monitor.app/Contents/MacOS/PHP Monitor";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C4F7808225D7F84B000DBC97 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
@ -1223,7 +1286,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PHP Monitor.app/Contents/MacOS/PHP Monitor";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@ -1276,6 +1338,11 @@
|
||||
package = C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */;
|
||||
productName = HotKey;
|
||||
};
|
||||
C4C1019527C659B7001FACC2 /* HotKey */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */;
|
||||
productName = HotKey;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = C41C1B2B22B0097F00E7CF16 /* Project object */;
|
||||
|
23
README.md
@ -43,7 +43,13 @@ To upgrade your existing installation, run:
|
||||
|
||||
brew upgrade phpmon
|
||||
|
||||
(You may need to run `brew update` first in order to update the cask file if you ran a Homebrew operation recently.)
|
||||
(You may need to run `brew update` or `brew update-reset` first in order to update the cask file if you ran a Homebrew operation recently.)
|
||||
|
||||
## ⚡️ Launchers (Alfred, Raycast)
|
||||
|
||||
If you would like to integrate with your launcher of choice, you can also download an [Alfred workflow](https://github.com/nicoverbruggen/phpmon/raw/main/integrations/phpmon.alfredworkflow) or [Raycast extension](https://www.raycast.com/nicoverbruggen/php-monitor) that works with PHP Monitor.
|
||||
|
||||
The app must be running in the background for these to work, and the _Allow third-party integrations_ checkbox must be enabled in Preferences (it is by default).
|
||||
|
||||
## 🔑 Is the app signed & notarized?
|
||||
|
||||
@ -231,8 +237,15 @@ Since v3.4 all of the loaded .ini files are sourced to determine which extension
|
||||
<details>
|
||||
<summary><strong>I've got two Homebrew installations on my Apple Silicon Mac, can I choose which installation to use with PHP Monitor?</strong></summary>
|
||||
|
||||
Not at this time, no. PHP Monitor will prefer the `/opt/homebrew` installation over the classic installation directory.
|
||||
If you are using PHP Monitor on an Intel machine or on an Apple Silicon machine with Rosetta enabled, PHP Monitor expects the main Homebrew binary in `/usr/local/bin/brew`.
|
||||
|
||||
If you are using PHP Monitor on Apple Silicon without Rosetta, PHP Monitor expects the main Homebrew binary in `/opt/homebrew/bin/brew`.
|
||||
|
||||
If there's an issue here, you'll get an alert at launch.
|
||||
|
||||
Make sure that the version of Homebrew that you are running normally is the same as the one that PHP Monitor expects. If you are on M1 hardware for example, but still using Rosetta for Homebrew, you'll need to run PHP Monitor under Rosetta as well.
|
||||
|
||||
PHP Monitor is a universal app and supports both architectures, so [find out here](https://support.apple.com/en-us/HT211861) how to enable Rosetta with PHP Monitor.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -267,9 +280,11 @@ You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How can the app integrate with third party tools, like Alfred?</strong></summary>
|
||||
<summary><strong>How can the app integrate with third party tools, like Alfred or Raycast?</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`.
|
||||
PHP Monitor supports third party app integrations by default, and this feature is enabled in Preferences unless you disable it.
|
||||
|
||||
You can grab the official [Alfred workflow](https://github.com/nicoverbruggen/phpmon/raw/main/integrations/phpmon.alfredworkflow) or [Raycast extension](https://www.raycast.com/nicoverbruggen/php-monitor).
|
||||
|
||||
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).
|
||||
|
||||
|
13
RELEASE.md
@ -1,13 +0,0 @@
|
||||
# Release Procedure
|
||||
|
||||
1. Merge into `main`
|
||||
2. Create tag
|
||||
3. Add changes to changelog + update security document
|
||||
4. Archive
|
||||
5. Notarize and prepare for own distribution
|
||||
6. After notarization, export .app
|
||||
7. Create zipped version
|
||||
8. Calculate SHA256: `openssl dgst -sha256 phpmon.zip`
|
||||
9. Upload to GitHub and add to tagged release
|
||||
10. Update Cask with new version + hash
|
||||
11. Check new version can be installed via Cask
|
@ -6,7 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
|
||||
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 5.0 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||
| 5 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||
|
||||
## Legacy versions
|
||||
|
||||
|
BIN
assets/affinity/icon.afdesign
Normal file
@ -8,10 +8,10 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
class ValetTest: XCTestCase {
|
||||
class ValetVersionExtractorTest: XCTestCase {
|
||||
|
||||
func testDetermineValetVersion() {
|
||||
let version = valet("--version")
|
||||
let version = valet("--version", sudo: false)
|
||||
XCTAssert(version.contains("Laravel Valet 2."))
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 585 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 134 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_128x128.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_128x128@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_16x16.png
Normal file
After Width: | Height: | Size: 632 B |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_16x16@2x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_256x256.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_256x256@2x.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_32x32.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_32x32@2x.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_512x512.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
phpmon/Assets.xcassets/AppIconDev.appiconset/icon_512x512@2x.png
Normal file
After Width: | Height: | Size: 163 KiB |
@ -116,6 +116,8 @@ class Actions {
|
||||
Detects all currently available PHP versions,
|
||||
and unlinks each and every one of them.
|
||||
|
||||
This all happens in sequence, nothing runs in parallel.
|
||||
|
||||
After this, the brew services are also stopped,
|
||||
the latest PHP version is linked, and php + nginx are restarted.
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Constants {
|
||||
struct Constants {
|
||||
|
||||
/**
|
||||
* The latest PHP version that is considered to be stable at the time of release.
|
||||
@ -50,10 +50,19 @@ class Constants {
|
||||
// dev release. In this case, that means that the version below is detected.
|
||||
"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")!
|
||||
|
||||
struct Urls {
|
||||
|
||||
static let DonationPayment = URL(
|
||||
string: "https://nicoverbruggen.be/sponsor#pay-now"
|
||||
)!
|
||||
static let DonationPage = URL(
|
||||
string: "https://nicoverbruggen.be/sponsor"
|
||||
)!
|
||||
static let FrequentlyAskedQuestions = URL(
|
||||
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
|
||||
)!
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,11 +9,11 @@
|
||||
// MARK: Common Shell Commands
|
||||
|
||||
/**
|
||||
Runs a `valet` command.
|
||||
Runs a `valet` command. Defaults to running as superuser.
|
||||
*/
|
||||
func valet(_ command: String) -> String
|
||||
func valet(_ command: String, sudo: Bool = true) -> String
|
||||
{
|
||||
return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true)
|
||||
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,7 +35,7 @@ func sed(file: String, original: String, replacement: String)
|
||||
|
||||
// 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") {
|
||||
if Filesystem.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)")
|
||||
|
@ -27,25 +27,31 @@ class Log {
|
||||
|
||||
static func err(_ item: Any) {
|
||||
if Verbosity.error.isApplicable() {
|
||||
print("[ERR] \(item)")
|
||||
print("[E] \(item)")
|
||||
}
|
||||
}
|
||||
|
||||
static func warn(_ item: Any) {
|
||||
if Verbosity.warning.isApplicable() {
|
||||
print("[WRN] \(item)")
|
||||
print("[W] \(item)")
|
||||
}
|
||||
}
|
||||
|
||||
static func info(_ item: Any) {
|
||||
if Verbosity.info.isApplicable() {
|
||||
print(item)
|
||||
print("\(item)")
|
||||
}
|
||||
}
|
||||
|
||||
static func perf(_ item: Any) {
|
||||
if Verbosity.performance.isApplicable() {
|
||||
print(item)
|
||||
print("[P] \(item)")
|
||||
}
|
||||
}
|
||||
|
||||
static func separator(as verbosity: Verbosity = .info) {
|
||||
if verbosity.isApplicable() {
|
||||
print("==================================")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,15 +15,19 @@ public class Paths {
|
||||
|
||||
public static let shared = Paths()
|
||||
|
||||
private var baseDir : Paths.HomebrewDir
|
||||
internal var baseDir: Paths.HomebrewDir
|
||||
|
||||
private var userName : String
|
||||
private var userName: String
|
||||
|
||||
init() {
|
||||
baseDir = FileManager.default.fileExists(atPath: "\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr
|
||||
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
||||
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
|
||||
}
|
||||
|
||||
public func detectBinaryPaths() {
|
||||
detectComposerBinary()
|
||||
}
|
||||
|
||||
// - MARK: Binaries
|
||||
|
||||
public static var valet: String {
|
||||
@ -42,6 +46,11 @@ public class Paths {
|
||||
return "\(binPath)/php-config"
|
||||
}
|
||||
|
||||
// - MARK: Detected Binaries
|
||||
|
||||
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
|
||||
public static var composer: String? = nil
|
||||
|
||||
// - MARK: Paths
|
||||
|
||||
public static var whoami: String {
|
||||
@ -64,6 +73,21 @@ public class Paths {
|
||||
return "\(shared.baseDir.rawValue)/etc"
|
||||
}
|
||||
|
||||
// MARK: - Flexible Binaries
|
||||
// (these can be in multiple locations, so we scan common places because)
|
||||
// (PHP Monitor will not use the user's own PATH)
|
||||
|
||||
private func detectComposerBinary() {
|
||||
if Filesystem.fileExists("/usr/local/bin/composer") {
|
||||
Paths.composer = "/usr/local/bin/composer"
|
||||
} else if Filesystem.fileExists("/opt/homebrew/bin/composer") {
|
||||
Paths.composer = "/opt/homebrew/bin/composer"
|
||||
} else {
|
||||
Paths.composer = nil
|
||||
Log.warn("Composer was not found.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Enum
|
||||
|
||||
public enum HomebrewDir: String {
|
||||
|
59
phpmon/Common/Core/Process.swift
Normal file
@ -0,0 +1,59 @@
|
||||
//
|
||||
// Process.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 23/02/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Process {
|
||||
|
||||
/**
|
||||
When a process is running in the background, it can send content to standard
|
||||
output or standard error, just like it would in a terminal. Using `listen`
|
||||
allows us to react whenever data is received by running a particular closure,
|
||||
depending on which type of data is received.
|
||||
*/
|
||||
public func listen(
|
||||
didReceiveStandardOutputData: @escaping (String) -> Void,
|
||||
didReceiveStandardErrorData: @escaping (String) -> Void
|
||||
) {
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
self.standardOutput = outputPipe
|
||||
self.standardError = errorPipe
|
||||
|
||||
[
|
||||
(outputPipe, didReceiveStandardOutputData),
|
||||
(errorPipe, didReceiveStandardErrorData)
|
||||
].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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
After the process is done running, you'll want to stop listening.
|
||||
*/
|
||||
public func haltListening() {
|
||||
if let pipe = self.standardOutput as? Pipe {
|
||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||
}
|
||||
if let pipe = self.standardError as? Pipe {
|
||||
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -104,15 +104,6 @@ public class Shell {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if a file exists at a certain path.
|
||||
Used to be done with a shell command, now uses the native FileManager class instead.
|
||||
*/
|
||||
public static func fileExists(_ path: String) -> Bool {
|
||||
let fullPath = path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
|
||||
return FileManager.default.fileExists(atPath: fullPath)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new process with the correct PATH and shell.
|
||||
*/
|
||||
@ -128,42 +119,6 @@ public class Shell {
|
||||
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
|
||||
|
38
phpmon/Common/Extensions/NSWindowExtension.swift
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// NSWindowExtension.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 17/02/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
extension NSWindow {
|
||||
|
||||
/**
|
||||
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
|
||||
*/
|
||||
func shake(){
|
||||
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
|
||||
|
||||
let frame: CGRect = self.frame
|
||||
let shakeAnimation :CAKeyframeAnimation = CAKeyframeAnimation()
|
||||
|
||||
let shakePath = CGMutablePath()
|
||||
shakePath.move( to: CGPoint(x:NSMinX(frame), y:NSMinY(frame)))
|
||||
|
||||
for _ in 0...numberOfShakes-1 {
|
||||
shakePath.addLine(to: CGPoint(x:NSMinX(frame) - frame.size.width * vigourOfShake, y:NSMinY(frame)))
|
||||
shakePath.addLine(to: CGPoint(x:NSMinX(frame) + frame.size.width * vigourOfShake, y:NSMinY(frame)))
|
||||
}
|
||||
|
||||
shakePath.closeSubpath()
|
||||
shakeAnimation.path = shakePath
|
||||
shakeAnimation.duration = durationOfShake
|
||||
|
||||
self.animations = ["frameOrigin":shakeAnimation]
|
||||
self.animator().setFrameOrigin(self.frame.origin)
|
||||
}
|
||||
}
|
@ -9,24 +9,6 @@ import Cocoa
|
||||
|
||||
class Alert {
|
||||
|
||||
public static func present(
|
||||
messageText: String,
|
||||
informativeText: String,
|
||||
buttonTitle: String = "OK",
|
||||
secondButtonTitle: String = "",
|
||||
style: NSAlert.Style = .informational
|
||||
) -> Bool {
|
||||
let alert = NSAlert.init()
|
||||
alert.alertStyle = style
|
||||
alert.messageText = messageText
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: buttonTitle)
|
||||
if (!secondButtonTitle.isEmpty) {
|
||||
alert.addButton(withTitle: secondButtonTitle)
|
||||
}
|
||||
return alert.runModal() == .alertFirstButtonReturn
|
||||
}
|
||||
|
||||
public static func confirm(
|
||||
onWindow window: NSWindow,
|
||||
messageText: String,
|
||||
@ -36,6 +18,10 @@ class Alert {
|
||||
style: NSAlert.Style = .warning,
|
||||
onFirstButtonPressed: @escaping (() -> Void)
|
||||
) {
|
||||
if !Thread.isMainThread {
|
||||
fatalError("You should always present alerts on the main thread!")
|
||||
}
|
||||
|
||||
let alert = NSAlert.init()
|
||||
alert.alertStyle = style
|
||||
alert.messageText = messageText
|
||||
@ -51,31 +37,4 @@ class Alert {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Notify the user about something by showing an alert.
|
||||
*/
|
||||
public static func notify(message: String, info: String, style: NSAlert.Style = .informational) {
|
||||
_ = present(
|
||||
messageText: message,
|
||||
informativeText: info,
|
||||
buttonTitle: "OK",
|
||||
secondButtonTitle: "",
|
||||
style: style
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Notify the user about a particular error (which must be `Alertable`)
|
||||
by showing an alert.
|
||||
*/
|
||||
public static func notify(about error: Error & AlertableError) {
|
||||
let key = error.getErrorMessageKey()
|
||||
_ = present(
|
||||
messageText: "\(key).title".localized,
|
||||
informativeText: "\(key).description".localized,
|
||||
buttonTitle: "OK",
|
||||
secondButtonTitle: "",
|
||||
style: .critical
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +0,0 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -137,7 +137,7 @@ class ActivePhpInstallation {
|
||||
}
|
||||
|
||||
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
|
||||
return Shell.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
|
||||
return Filesystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
|
||||
}
|
||||
|
||||
// MARK: - Structs
|
||||
|
@ -18,4 +18,18 @@ struct HomebrewService: Decodable, Equatable {
|
||||
let status: String?
|
||||
let log_path: String?
|
||||
let error_log_path: String?
|
||||
|
||||
public static func loadAll(
|
||||
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"]
|
||||
) async -> [HomebrewService] {
|
||||
return try! JSONDecoder().decode(
|
||||
[HomebrewService].self,
|
||||
from: Shell.pipe(
|
||||
"sudo \(Paths.brew) services info --all --json",
|
||||
requiresPath: true
|
||||
).data(using: .utf8)!
|
||||
).filter({ service in
|
||||
return filter.contains(service.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ class PhpEnv {
|
||||
let phpAlias = homebrewPackage.version
|
||||
|
||||
// Avoid inserting a duplicate
|
||||
if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) {
|
||||
if (!versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php")) {
|
||||
versionsOnly.append(phpAlias)
|
||||
}
|
||||
|
||||
@ -134,7 +134,7 @@ class PhpEnv {
|
||||
// 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)
|
||||
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
|
||||
{
|
||||
output.append(version)
|
||||
}
|
||||
|
@ -96,6 +96,12 @@ public struct PhpVersionNumber: Equatable {
|
||||
let minor: Int
|
||||
let patch: Int?
|
||||
|
||||
public func toString() -> String {
|
||||
return self.patch == nil
|
||||
? "\(major).\(minor)"
|
||||
: "\(major).\(minor).\(patch!)"
|
||||
}
|
||||
|
||||
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
|
||||
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
|
||||
}
|
||||
@ -111,7 +117,7 @@ public struct PhpVersionNumber: Equatable {
|
||||
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)
|
||||
// TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
|
||||
/*
|
||||
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
|
@ -21,7 +21,7 @@ class PhpInstallation {
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
||||
self.longVersion = PhpVersionNumber.make(from: version)!
|
||||
|
||||
if Shell.fileExists(phpConfigExecutablePath) {
|
||||
if Filesystem.fileExists(phpConfigExecutablePath) {
|
||||
let longVersionString = Command.execute(
|
||||
path: phpConfigExecutablePath,
|
||||
arguments: ["--version"]
|
||||
@ -29,7 +29,6 @@ class PhpInstallation {
|
||||
|
||||
// The parser should always work, or the string has to be very unusual.
|
||||
// If so, the app SHOULD crash, so that the users report what's up.
|
||||
// TODO: Alert the user that the version number could not be parsed.
|
||||
self.longVersion = try! PhpVersionNumber.parse(longVersionString)
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ import Foundation
|
||||
|
||||
protocol PhpSwitcherDelegate: AnyObject {
|
||||
|
||||
func switcherDidStartSwitching(to: String)
|
||||
func switcherDidStartSwitching(to version: String)
|
||||
|
||||
func switcherDidCompleteSwitch(to: String)
|
||||
func switcherDidCompleteSwitch(to version: String)
|
||||
|
||||
}
|
||||
|
||||
|
@ -22,9 +22,18 @@ class App {
|
||||
return "\(version) (\(build))"
|
||||
}
|
||||
|
||||
/** Whether the app is busy doing something. Used to determine what UI to display. */
|
||||
static var busy: Bool {
|
||||
return PhpEnv.shared.isBusy
|
||||
static var architecture: String {
|
||||
var systeminfo = utsname()
|
||||
uname(&systeminfo)
|
||||
let machine = withUnsafeBytes(of: &systeminfo.machine) {bufPtr->String in
|
||||
let data = Data(bufPtr)
|
||||
if let lastIndex = data.lastIndex(where: {$0 != 0}) {
|
||||
return String(data: data[0...lastIndex], encoding: .isoLatin1)!
|
||||
} else {
|
||||
return String(data: data, encoding: .isoLatin1)!
|
||||
}
|
||||
}
|
||||
return machine
|
||||
}
|
||||
|
||||
// MARK: Variables
|
||||
|
@ -64,10 +64,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
*/
|
||||
override init() {
|
||||
logger.verbosity = .info
|
||||
Log.info("==================================")
|
||||
#if DEBUG
|
||||
logger.verbosity = .performance
|
||||
#endif
|
||||
Log.separator(as: .info)
|
||||
Log.info("PHP MONITOR by Nico Verbruggen")
|
||||
Log.info("Version \(App.version)")
|
||||
Log.info("==================================")
|
||||
Log.separator(as: .info)
|
||||
self.sharedShell = Shell.user
|
||||
self.state = App.shared
|
||||
self.menu = MainMenu.shared
|
||||
@ -91,7 +94,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
// Make sure notifications will work
|
||||
setupNotifications()
|
||||
// Make sure the menu performs its initial checks
|
||||
menu.startup()
|
||||
Task { await menu.startup() }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -462,7 +462,177 @@
|
||||
</windowController>
|
||||
<customObject id="d2k-57-mLZ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-339" y="1147"/>
|
||||
<point key="canvasLocation" x="-409" y="1137"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="BD0-La-ygq">
|
||||
<objects>
|
||||
<windowController storyboardIdentifier="noticeWindow" id="nfT-AN-9ZW" sceneMemberID="viewController">
|
||||
<window key="window" title="Notice" separatorStyle="none" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="alertPanel" frameAutosaveName="" titlebarAppearsTransparent="YES" toolbarStyle="unified" titleVisibility="hidden" id="AoF-SN-xB0">
|
||||
<windowStyleMask key="styleMask" titled="YES" fullSizeContentView="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="Src-7L-4Z4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="nfT-AN-9ZW" id="8dd-JR-bQG"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="hkw-9V-NxP" kind="relationship" relationship="window.shadowedContentViewController" id="eob-YS-ACy"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="i3j-z8-nxv" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-575" y="1624"/>
|
||||
</scene>
|
||||
<!--Better AlertVC-->
|
||||
<scene sceneID="y9E-bB-wIG">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="noticeVC" id="hkw-9V-NxP" customClass="BetterAlertVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="UPH-hV-Naz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<visualEffectView blendingMode="behindWindow" material="popover" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="JVG-5w-fPd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
|
||||
<subviews>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8zu-cF-KCX">
|
||||
<rect key="frame" x="383" y="13" width="104" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="4Uf-fh-jWJ"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Primary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="F26-vf-hFH">
|
||||
<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="primaryButtonAction:" target="hkw-9V-NxP" id="W7d-3b-pZT"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TCp-nS-HN2">
|
||||
<rect key="frame" x="281" y="13" width="104" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="QWZ-BA-0g9"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Secondary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="eCk-FC-9Zr">
|
||||
<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="secondaryButtonAction:" target="hkw-9V-NxP" id="YJs-Hu-lFP"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j">
|
||||
<rect key="frame" x="13" y="13" width="81" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Tertiary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="mzA-Uu-gyf">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="tertiaryButtonAction:" target="hkw-9V-NxP" id="o1C-av-ifx"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="8zu-cF-KCX" secondAttribute="trailing" constant="20" symbolic="YES" id="0DC-rd-xbu"/>
|
||||
<constraint firstItem="8zu-cF-KCX" firstAttribute="leading" secondItem="TCp-nS-HN2" secondAttribute="trailing" constant="12" symbolic="YES" id="8tH-KO-fjY"/>
|
||||
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="L6V-KQ-nj3"/>
|
||||
<constraint firstItem="n5T-nn-k3j" firstAttribute="leading" secondItem="JVG-5w-fPd" secondAttribute="leading" constant="20" symbolic="YES" id="QLS-fE-1PM"/>
|
||||
<constraint firstAttribute="trailing" secondItem="8zu-cF-KCX" secondAttribute="trailing" constant="20" symbolic="YES" id="RRS-WO-6KO"/>
|
||||
<constraint firstAttribute="bottom" secondItem="n5T-nn-k3j" secondAttribute="bottom" constant="20" symbolic="YES" id="Scj-z1-AdN"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TCp-nS-HN2" secondAttribute="bottom" constant="20" symbolic="YES" id="fYa-HG-gmL"/>
|
||||
<constraint firstItem="n5T-nn-k3j" firstAttribute="bottom" secondItem="TCp-nS-HN2" secondAttribute="bottom" id="lOI-ZI-wCd"/>
|
||||
<constraint firstItem="TCp-nS-HN2" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="n5T-nn-k3j" secondAttribute="trailing" constant="12" symbolic="YES" id="rUC-0t-9H9"/>
|
||||
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="wIl-uw-y3p"/>
|
||||
</constraints>
|
||||
</visualEffectView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
|
||||
<rect key="frame" x="98" y="153" width="384" height="19"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="380" id="WgB-hj-d4P"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" title="This is the title of the notice window." id="bzW-MI-jXb">
|
||||
<font key="font" metaFont="systemBold" size="15"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
|
||||
<rect key="frame" x="98" y="127" width="384" height="16"/>
|
||||
<textFieldCell key="cell" selectable="YES" title="This is a slightly more expanded explanation." id="rY3-Nd-Iit">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0rX-Ss-3Xd">
|
||||
<rect key="frame" x="12" y="144" width="48" height="48"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="Uib-R1-GEx">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
</button>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QkM-5D-ZEQ">
|
||||
<rect key="frame" x="20" y="111" width="64" height="64"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="64" id="VhJ-fI-IKC"/>
|
||||
<constraint firstAttribute="width" constant="64" id="a2d-Gn-Oor"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="7eT-Hw-EL9"/>
|
||||
</imageView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
|
||||
<rect key="frame" x="98" y="70" width="384" height="42"/>
|
||||
<textFieldCell key="cell" selectable="YES" id="7iW-Lc-DqO">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<string key="title">Sometimes you need a really long explanation and in that case you can get a really, really long description here, along with, for example, various steps you can take. This allows for a lot of text to be displayed, yay!</string>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="hml-dl-Cah" firstAttribute="leading" secondItem="yI6-qf-htf" secondAttribute="leading" id="1Lh-ve-rwK"/>
|
||||
<constraint firstItem="U1c-qS-cIm" firstAttribute="leading" secondItem="QkM-5D-ZEQ" secondAttribute="trailing" constant="16" id="2xX-Ma-iQZ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="U1c-qS-cIm" secondAttribute="trailing" constant="20" symbolic="YES" id="39u-Uk-TDI"/>
|
||||
<constraint firstItem="8zu-cF-KCX" firstAttribute="top" secondItem="hml-dl-Cah" secondAttribute="bottom" constant="30" id="7fT-EZ-cAO"/>
|
||||
<constraint firstItem="JVG-5w-fPd" firstAttribute="top" secondItem="UPH-hV-Naz" secondAttribute="top" id="CE7-54-G09"/>
|
||||
<constraint firstItem="hml-dl-Cah" firstAttribute="trailing" secondItem="yI6-qf-htf" secondAttribute="trailing" id="DBa-7d-sS3"/>
|
||||
<constraint firstItem="yI6-qf-htf" firstAttribute="trailing" secondItem="U1c-qS-cIm" secondAttribute="trailing" id="NvJ-vf-wEl"/>
|
||||
<constraint firstItem="hml-dl-Cah" firstAttribute="top" secondItem="yI6-qf-htf" secondAttribute="bottom" constant="15" id="Pmz-I9-2up"/>
|
||||
<constraint firstItem="U1c-qS-cIm" firstAttribute="top" secondItem="UPH-hV-Naz" secondAttribute="top" constant="40" id="Uqt-sc-vxn"/>
|
||||
<constraint firstItem="QkM-5D-ZEQ" firstAttribute="top" secondItem="U1c-qS-cIm" secondAttribute="top" constant="-3" id="WAj-rw-srg"/>
|
||||
<constraint firstItem="yI6-qf-htf" firstAttribute="leading" secondItem="U1c-qS-cIm" secondAttribute="leading" id="bng-pH-jSG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="JVG-5w-fPd" secondAttribute="trailing" id="dRt-Pq-6n0"/>
|
||||
<constraint firstItem="JVG-5w-fPd" firstAttribute="leading" secondItem="UPH-hV-Naz" secondAttribute="leading" id="ejC-of-zjN"/>
|
||||
<constraint firstAttribute="bottom" secondItem="JVG-5w-fPd" secondAttribute="bottom" id="hGp-DD-cKr"/>
|
||||
<constraint firstItem="QkM-5D-ZEQ" firstAttribute="leading" secondItem="UPH-hV-Naz" secondAttribute="leading" constant="20" symbolic="YES" id="jG8-dt-l4x"/>
|
||||
<constraint firstItem="yI6-qf-htf" firstAttribute="top" secondItem="U1c-qS-cIm" secondAttribute="bottom" constant="10" id="kqR-yg-zdG"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="buttonPrimary" destination="8zu-cF-KCX" id="MT5-Px-K97"/>
|
||||
<outlet property="buttonSecondary" destination="TCp-nS-HN2" id="nPn-OX-Z4m"/>
|
||||
<outlet property="buttonTertiary" destination="n5T-nn-k3j" id="UnB-8x-s3D"/>
|
||||
<outlet property="imageView" destination="QkM-5D-ZEQ" id="zsW-l7-eH0"/>
|
||||
<outlet property="labelDescription" destination="hml-dl-Cah" id="ehw-zs-EPc"/>
|
||||
<outlet property="labelSubtitle" destination="yI6-qf-htf" id="m7A-bX-HE8"/>
|
||||
<outlet property="labelTitle" destination="U1c-qS-cIm" id="oM3-kl-PL8"/>
|
||||
<outlet property="primaryButtonTopMargin" destination="7fT-EZ-cAO" id="r6u-1l-dbl"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="5Ts-EZ-bJh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="38" y="1624"/>
|
||||
</scene>
|
||||
<!--Add SiteVC-->
|
||||
<scene sceneID="6JC-H6-u4K">
|
||||
@ -472,8 +642,16 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="js9-OW-xzC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
|
||||
<view key="contentView" id="HRC-RT-LxR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="251"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</view>
|
||||
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</box>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
|
||||
<rect key="frame" x="13" y="13" width="103" height="32"/>
|
||||
<rect key="frame" x="364" 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"/>
|
||||
@ -486,7 +664,10 @@ DQ
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
|
||||
<rect key="frame" x="391" y="13" width="76" height="32"/>
|
||||
<rect key="frame" x="13" y="13" width="94" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="qCP-Sp-gxm"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -551,7 +732,7 @@ Gw
|
||||
</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"/>
|
||||
<rect key="frame" x="230" 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"/>
|
||||
@ -562,27 +743,32 @@ Gw
|
||||
<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 firstAttribute="bottom" secondItem="SwS-o8-pbl" secondAttribute="bottom" constant="20" symbolic="YES" id="2pB-nW-NVx"/>
|
||||
<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="900-Z2-tID" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="SwS-o8-pbl" secondAttribute="trailing" constant="8" symbolic="YES" id="IMv-ZD-VXf"/>
|
||||
<constraint firstItem="js9-OW-xzC" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" id="IpM-ot-dBG"/>
|
||||
<constraint firstItem="VzR-5a-cmT" firstAttribute="leading" secondItem="ZX9-s1-23i" secondAttribute="leading" id="UPN-Ad-j3X"/>
|
||||
<constraint firstItem="KZf-b0-9cm" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="Vab-wq-9Nc"/>
|
||||
<constraint firstAttribute="bottom" secondItem="PVw-cM-qAB" secondAttribute="bottom" constant="20" symbolic="YES" id="VsP-Q0-zRW"/>
|
||||
<constraint firstAttribute="trailing" secondItem="PVw-cM-qAB" secondAttribute="trailing" constant="20" symbolic="YES" id="X5z-G4-CBv"/>
|
||||
<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="PVw-cM-qAB" firstAttribute="leading" secondItem="900-Z2-tID" secondAttribute="trailing" constant="15" id="cx5-Gi-XS7"/>
|
||||
<constraint firstItem="mmQ-7e-dlb" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="fKH-1r-MIf"/>
|
||||
<constraint firstItem="js9-OW-xzC" firstAttribute="top" secondItem="JJJ-T9-Yuv" secondAttribute="top" id="ffu-hT-qSK"/>
|
||||
<constraint firstAttribute="bottom" secondItem="js9-OW-xzC" secondAttribute="bottom" id="hLd-Kd-y6k"/>
|
||||
<constraint firstAttribute="trailing" secondItem="mmQ-7e-dlb" secondAttribute="trailing" constant="20" symbolic="YES" id="hjv-Xq-cxV"/>
|
||||
<constraint firstItem="SwS-o8-pbl" firstAttribute="leading" secondItem="JJJ-T9-Yuv" secondAttribute="leading" constant="20" symbolic="YES" id="jkg-UC-GPr"/>
|
||||
<constraint firstItem="6JT-Vt-3q0" firstAttribute="leading" secondItem="P0B-Ht-R8n" secondAttribute="leading" id="jxP-vM-eA9"/>
|
||||
<constraint firstAttribute="bottom" secondItem="PVw-cM-qAB" secondAttribute="bottom" constant="20" symbolic="YES" id="kGp-mI-1Ic"/>
|
||||
<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 firstAttribute="trailing" secondItem="js9-OW-xzC" secondAttribute="trailing" id="rc3-XI-7CY"/>
|
||||
<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"/>
|
||||
@ -602,7 +788,7 @@ Gw
|
||||
</viewController>
|
||||
<customObject id="6XV-bG-0N1" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="277" y="1137.5"/>
|
||||
<point key="canvasLocation" x="191" y="1099"/>
|
||||
</scene>
|
||||
<!--Site ListVC-->
|
||||
<scene sceneID="aZt-6w-TFl">
|
||||
|
@ -56,7 +56,10 @@ class InterApp {
|
||||
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.")
|
||||
BetterAlert().withInformation(
|
||||
title: "Unsupported version",
|
||||
subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
|
||||
).withPrimary(text: "OK").show()
|
||||
}
|
||||
}),
|
||||
]}
|
||||
|
@ -10,88 +10,70 @@ import AppKit
|
||||
|
||||
class Startup {
|
||||
|
||||
public var failed : Bool = false
|
||||
public var failureCallback = {}
|
||||
|
||||
/**
|
||||
Checks the user's environment and checks if PHP Monitor can be used properly.
|
||||
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
|
||||
|
||||
- Parameter success: Callback that is fired if the application can proceed with launch
|
||||
- Parameter failure: Callback that is fired if the application must retry launch
|
||||
If this method returns false, there was a failed check and an alert was displayed.
|
||||
If this method returns true, then all checks succeeded and the app can continue.
|
||||
*/
|
||||
func checkEnvironment(success: () -> Void, failure: @escaping () -> Void)
|
||||
func checkEnvironment() async -> Bool
|
||||
{
|
||||
failureCallback = failure
|
||||
// Do the important system setup checks
|
||||
Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)")
|
||||
|
||||
performEnvironmentCheck(
|
||||
!Shell.fileExists("\(Paths.binPath)/php"),
|
||||
messageText: "startup.errors.php_binary.title".localized,
|
||||
informativeText: "startup.errors.php_binary.desc".localized,
|
||||
breaking: true
|
||||
)
|
||||
for check in self.checks {
|
||||
if await check.succeeds() {
|
||||
Log.info("[OK] \(check.name)")
|
||||
continue
|
||||
}
|
||||
|
||||
// If we get here, something's gone wrong and the check has failed...
|
||||
Log.info("[FAIL] \(check.name)")
|
||||
showAlert(for: check)
|
||||
return false
|
||||
}
|
||||
|
||||
performEnvironmentCheck(
|
||||
!Shell.pipe("ls \(Paths.optPath) | grep php").contains("php"),
|
||||
messageText: "startup.errors.php_opt.title".localized,
|
||||
informativeText: "startup.errors.php_opt.desc".localized,
|
||||
breaking: true
|
||||
)
|
||||
|
||||
performEnvironmentCheck(
|
||||
// Check for Valet; it can be symlinked or in .composer/vendor/bin
|
||||
!(Shell.fileExists("/usr/local/bin/valet")
|
||||
|| Shell.fileExists("/opt/homebrew/bin/valet")
|
||||
|| Shell.fileExists("~/.composer/vendor/bin/valet")
|
||||
),
|
||||
messageText: "startup.errors.valet_executable.title".localized,
|
||||
informativeText: "startup.errors.valet_executable.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,
|
||||
informativeText: "startup.errors.sudoers_brew.desc".localized,
|
||||
breaking: true
|
||||
)
|
||||
|
||||
performEnvironmentCheck(
|
||||
// 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")
|
||||
),
|
||||
messageText: "startup.errors.sudoers_valet.title".localized,
|
||||
informativeText: "startup.errors.sudoers_valet.desc".localized,
|
||||
breaking: true
|
||||
)
|
||||
|
||||
// Determine the Valet version only AFTER confirming the correct permission is in place
|
||||
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
|
||||
)
|
||||
|
||||
if (!failed) {
|
||||
initializeSwitcher()
|
||||
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
|
||||
success()
|
||||
// If we get here, nothing has gone wrong. That's what we want!
|
||||
initializeSwitcher()
|
||||
Log.separator(as: .info)
|
||||
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
Displays an alert for a particular check. There are two types of alerts:
|
||||
- ones that require an app restart, which prompt the user to exit the app
|
||||
- ones that allow the app to continue, which allow the user to retry
|
||||
*/
|
||||
private func showAlert(for check: EnvironmentCheck) {
|
||||
DispatchQueue.main.async {
|
||||
if check.requiresAppRestart {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: check.titleText,
|
||||
subtitle: check.subtitleText,
|
||||
description: check.descriptionText
|
||||
)
|
||||
.withPrimary(text: check.buttonText, action: { _ in
|
||||
exit(1)
|
||||
}).show()
|
||||
}
|
||||
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: check.titleText,
|
||||
subtitle: check.subtitleText,
|
||||
description: check.descriptionText
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Because the Switcher requires various environment guarantees, the switcher is only
|
||||
initialized when it is done working.
|
||||
initialized when it is done working. The switcher must be initialized on the main thread.
|
||||
*/
|
||||
private func initializeSwitcher() {
|
||||
DispatchQueue.main.async {
|
||||
@ -99,35 +81,120 @@ class Startup {
|
||||
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
|
||||
*/
|
||||
private func performEnvironmentCheck(
|
||||
_ condition: Bool,
|
||||
messageText: String,
|
||||
informativeText: String,
|
||||
breaking: Bool
|
||||
) {
|
||||
if (!condition) { return }
|
||||
|
||||
failed = breaking
|
||||
|
||||
DispatchQueue.main.async { [self] in
|
||||
// Present the information to the user
|
||||
Alert.notify(
|
||||
message: messageText,
|
||||
info: informativeText,
|
||||
style: breaking ? .critical : .warning
|
||||
|
||||
// MARK: - Check (List)
|
||||
|
||||
public var checks: [EnvironmentCheck] = [
|
||||
EnvironmentCheck(
|
||||
command: { return !FileManager.default.fileExists(atPath: Paths.brew) },
|
||||
name: "`\(Paths.brew)` exists",
|
||||
titleText: "alert.homebrew_missing.title".localized,
|
||||
subtitleText: "alert.homebrew_missing.subtitle".localized,
|
||||
descriptionText: "alert.homebrew_missing.info".localized(
|
||||
App.architecture
|
||||
.replacingOccurrences(of: "x86_64", with: "Intel")
|
||||
.replacingOccurrences(of: "arm64", with: "Apple Silicon"),
|
||||
Paths.brew
|
||||
),
|
||||
buttonText: "alert.homebrew_missing.quit".localized,
|
||||
requiresAppRestart: true
|
||||
),
|
||||
EnvironmentCheck(
|
||||
command: { return !Filesystem.fileExists(Paths.php) },
|
||||
name: "`\(Paths.php)` exists",
|
||||
titleText: "startup.errors.php_binary.title".localized,
|
||||
subtitleText: "startup.errors.php_binary.subtitle".localized,
|
||||
descriptionText: "startup.errors.php_binary.desc".localized(Paths.php)
|
||||
),
|
||||
EnvironmentCheck(
|
||||
command: { return !Shell.pipe("ls \(Paths.optPath) | grep php").contains("php") },
|
||||
name: "`ls \(Paths.optPath) | grep php` returned php result",
|
||||
titleText: "startup.errors.php_opt.title".localized,
|
||||
subtitleText: "startup.errors.php_opt.subtitle".localized(
|
||||
Paths.optPath
|
||||
),
|
||||
descriptionText: "startup.errors.php_opt.desc".localized
|
||||
),
|
||||
EnvironmentCheck(
|
||||
command: {
|
||||
return !(Filesystem.fileExists(Paths.valet)
|
||||
|| Filesystem.fileExists("~/.composer/vendor/bin/valet"))
|
||||
},
|
||||
name: "`valet` binary exists",
|
||||
titleText: "startup.errors.valet_executable.title".localized,
|
||||
subtitleText: "startup.errors.valet_executable.subtitle".localized,
|
||||
descriptionText: "startup.errors.valet_executable.desc".localized(
|
||||
Paths.valet
|
||||
)
|
||||
// Only breaking issues will throw the extra retry modal
|
||||
breaking ? failureCallback() : ()
|
||||
),
|
||||
EnvironmentCheck(
|
||||
command: { return HomebrewDiagnostics.cannotLoadService() },
|
||||
name: "`sudo \(Paths.brew) services info` JSON loaded",
|
||||
titleText: "startup.errors.services_json_error.title".localized,
|
||||
subtitleText: "startup.errors.services_json_error.subtitle".localized,
|
||||
descriptionText: "startup.errors.services_json_error.desc".localized
|
||||
),
|
||||
EnvironmentCheck(
|
||||
command: { return !Shell.pipe("cat /private/etc/sudoers.d/brew").contains(Paths.brew) },
|
||||
name: "`/private/etc/sudoers.d/brew` contains brew",
|
||||
titleText: "startup.errors.sudoers_brew.title".localized,
|
||||
subtitleText: "startup.errors.sudoers_brew.subtitle".localized
|
||||
),
|
||||
EnvironmentCheck(
|
||||
command: { return !Shell.pipe("cat /private/etc/sudoers.d/valet").contains(Paths.valet) },
|
||||
name: "`/private/etc/sudoers.d/valet` contains valet",
|
||||
titleText: "startup.errors.sudoers_valet.title".localized,
|
||||
subtitleText: "startup.errors.sudoers_valet.subtitle".localized
|
||||
),
|
||||
EnvironmentCheck(
|
||||
command: {
|
||||
// Determine the Valet version only AFTER confirming the correct permission is in place
|
||||
// or otherwise this command will never return a valid version number
|
||||
Valet.shared.version = VersionExtractor.from(valet("--version", sudo: false))
|
||||
return Valet.shared.version == nil
|
||||
},
|
||||
name: "`valet --version` was loaded",
|
||||
titleText: "startup.errors.valet_version_unknown.title".localized,
|
||||
subtitleText: "startup.errors.valet_version_unknown.subtitle".localized,
|
||||
descriptionText: "startup.errors.valet_version_unknown.desc".localized
|
||||
)
|
||||
]
|
||||
|
||||
// MARK: - EnvironmentCheck struct
|
||||
|
||||
/**
|
||||
The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary.
|
||||
Checks that require an app restart will always lead to an alert and app termination shortly after.
|
||||
*/
|
||||
struct EnvironmentCheck {
|
||||
let command: () async -> Bool
|
||||
let name: String
|
||||
let titleText: String
|
||||
let subtitleText: String
|
||||
let descriptionText: String
|
||||
let buttonText: String
|
||||
let requiresAppRestart: Bool
|
||||
|
||||
init(
|
||||
command: @escaping () async -> Bool,
|
||||
name: String,
|
||||
titleText: String,
|
||||
subtitleText: String,
|
||||
descriptionText: String = "",
|
||||
buttonText: String = "OK",
|
||||
requiresAppRestart: Bool = false
|
||||
) {
|
||||
self.command = command
|
||||
self.name = name
|
||||
self.titleText = titleText
|
||||
self.subtitleText = subtitleText
|
||||
self.descriptionText = descriptionText
|
||||
self.buttonText = buttonText
|
||||
self.requiresAppRestart = requiresAppRestart
|
||||
}
|
||||
|
||||
public func succeeds() async -> Bool {
|
||||
return await !self.command()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ struct ComposerJson: Decodable {
|
||||
struct Config: Decodable {
|
||||
let platform: Platform?
|
||||
}
|
||||
|
||||
struct Platform: Decodable {
|
||||
let php: String?
|
||||
}
|
||||
@ -39,19 +40,20 @@ struct ComposerJson: Decodable {
|
||||
Checks what the PHP version constraint is.
|
||||
Returns a tuple (constraint, location of constraint).
|
||||
*/
|
||||
public func getPhpVersion() -> (String, String) {
|
||||
public func getPhpVersion() -> (String, ValetSite.VersionSource)
|
||||
{
|
||||
// Check if in platform
|
||||
if configuration?.platform?.php != nil {
|
||||
return (configuration!.platform!.php!, "platform")
|
||||
return (configuration!.platform!.php!, .platform)
|
||||
}
|
||||
|
||||
// Check if in dependencies
|
||||
if dependencies?["php"] != nil {
|
||||
return (dependencies!["php"]!, "require")
|
||||
return (dependencies!["php"]!, .require)
|
||||
}
|
||||
|
||||
// Unknown!
|
||||
return ("???", "unknown")
|
||||
return ("???", .unknown)
|
||||
}
|
||||
|
||||
/**
|
||||
|
128
phpmon/Domain/Integrations/Composer/ComposerWindow.swift
Normal file
@ -0,0 +1,128 @@
|
||||
//
|
||||
// MainMenu+Composer.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 08/02/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ComposerWindow {
|
||||
|
||||
private var menu: MainMenu? = nil
|
||||
private var shouldNotify: Bool! = nil
|
||||
private var completion: ((Bool) -> Void)! = nil
|
||||
private var window: ProgressWindowController? = nil
|
||||
|
||||
/**
|
||||
Updates the global dependencies and runs the completion callback when done.
|
||||
*/
|
||||
func updateGlobalDependencies(notify: Bool, completion: @escaping (Bool) -> Void) {
|
||||
self.menu = MainMenu.shared
|
||||
self.shouldNotify = notify
|
||||
self.completion = completion
|
||||
|
||||
Paths.shared.detectBinaryPaths()
|
||||
if Paths.composer == nil {
|
||||
presentMissingAlert()
|
||||
return
|
||||
}
|
||||
|
||||
PhpEnv.shared.isBusy = true
|
||||
menu?.setBusyImage()
|
||||
menu?.rebuild()
|
||||
|
||||
window = ProgressWindowController.display(
|
||||
title: "alert.composer_progress.title".localized,
|
||||
description: "alert.composer_progress.info".localized
|
||||
)
|
||||
|
||||
window?.setType(info: true)
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
let task = Shell.user.createTask(
|
||||
for: "\(Paths.composer!) global update", requiresPath: true
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.window?.addToConsole("\(Paths.composer!) global update\n")
|
||||
}
|
||||
|
||||
task.listen(
|
||||
didReceiveStandardOutputData: { string in
|
||||
DispatchQueue.main.async {
|
||||
self.window?.addToConsole(string)
|
||||
}
|
||||
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
|
||||
},
|
||||
didReceiveStandardErrorData: { string in
|
||||
DispatchQueue.main.async {
|
||||
self.window?.addToConsole(string)
|
||||
}
|
||||
// Log.perf("\(string.trimmingCharacters(in: .newlines))")
|
||||
}
|
||||
)
|
||||
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
task.haltListening()
|
||||
|
||||
if task.terminationStatus <= 0 {
|
||||
composerUpdateSucceeded()
|
||||
} else {
|
||||
composerUpdateFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func composerUpdateSucceeded() {
|
||||
// Closing the window should happen after a slight delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in
|
||||
window?.close()
|
||||
if (shouldNotify) {
|
||||
LocalNotification.send(
|
||||
title: "alert.composer_success.title".localized,
|
||||
subtitle: "alert.composer_success.info".localized
|
||||
)
|
||||
}
|
||||
window = nil
|
||||
removeBusyStatus()
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func composerUpdateFailed() {
|
||||
// Showing that something failed should be shown immediately
|
||||
DispatchQueue.main.async { [self] in
|
||||
window?.setType(info: false)
|
||||
window?.progressView?.labelTitle.stringValue = "alert.composer_failure.title".localized
|
||||
window?.progressView?.labelDescription.stringValue = "alert.composer_failure.info".localized
|
||||
window = nil
|
||||
removeBusyStatus()
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Main Menu Update
|
||||
|
||||
private func removeBusyStatus() {
|
||||
PhpEnv.shared.isBusy = false
|
||||
DispatchQueue.main.async { [self] in
|
||||
menu?.updatePhpVersionInStatusBar()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Alert
|
||||
|
||||
private func presentMissingAlert() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.composer_missing.title".localized,
|
||||
subtitle: "alert.composer_missing.subtitle".localized,
|
||||
description: "alert.composer_missing.desc".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
}
|
@ -19,12 +19,12 @@ class Valet {
|
||||
var config: Valet.Configuration!
|
||||
|
||||
/// A cached list of sites that were detected after analyzing the paths set up for Valet.
|
||||
var sites: [Site] = []
|
||||
var sites: [ValetSite] = []
|
||||
|
||||
/// 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.
|
||||
/// When initialising the Valet singleton assume no sites loaded. We will load the version later.
|
||||
init() {
|
||||
self.version = nil
|
||||
self.sites = []
|
||||
@ -87,7 +87,13 @@ class Valet {
|
||||
let version = version
|
||||
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))
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.min_valet_version.title".localized,
|
||||
subtitle:"alert.min_valet_version.info".localized(version!, Constants.MinimumRecommendedValetVersion)
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
Log.info("Valet version \(version!) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
|
||||
@ -168,165 +174,12 @@ class Valet {
|
||||
}
|
||||
|
||||
if type == FileAttributeType.typeSymbolicLink {
|
||||
sites.append(Site(aliasPath: siteDir, tld: tld))
|
||||
sites.append(ValetSite(aliasPath: siteDir, tld: tld))
|
||||
} else if type == FileAttributeType.typeDirectory {
|
||||
sites.append(Site(absolutePath: siteDir, tld: tld))
|
||||
sites.append(ValetSite(absolutePath: siteDir, tld: tld))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Structs
|
||||
|
||||
class Site {
|
||||
/// Name of the site. Does not include the TLD.
|
||||
var name: String!
|
||||
|
||||
/// 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?
|
||||
|
||||
/// Whether the site has been secured.
|
||||
var secured: Bool!
|
||||
|
||||
/// 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) {
|
||||
self.init()
|
||||
self.absolutePath = absolutePath
|
||||
self.name = URL(fileURLWithPath: absolutePath).lastPathComponent
|
||||
self.aliasPath = nil
|
||||
determineSecured(tld)
|
||||
determineComposerPhpVersion()
|
||||
determineDriver()
|
||||
}
|
||||
|
||||
convenience init(aliasPath: String, tld: String) {
|
||||
self.init()
|
||||
self.absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
|
||||
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
|
||||
.replacingOccurrences(of: "This site is served by [", with: "")
|
||||
.replacingOccurrences(of: "ValetDriver].\n", with: "")
|
||||
} else {
|
||||
self.driver = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Configuration: Decodable {
|
||||
/// Top level domain suffix. Usually "test" but can be set to something else.
|
||||
/// - Important: Does not include the actual dot. ("test", not ".test"!)
|
||||
|
185
phpmon/Domain/Integrations/Valet/ValetSite.swift
Normal file
@ -0,0 +1,185 @@
|
||||
//
|
||||
// Valet+Subclasses.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 22/02/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ValetSite {
|
||||
|
||||
/// Name of the site. Does not include the TLD.
|
||||
var name: String!
|
||||
|
||||
/// 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?
|
||||
|
||||
/// Whether the site has been secured.
|
||||
var secured: Bool!
|
||||
|
||||
/// 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` or in .valetphprc.
|
||||
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: VersionSource = .unknown
|
||||
|
||||
enum VersionSource: String {
|
||||
case unknown = "unknown"
|
||||
case require = "require"
|
||||
case platform = "platform"
|
||||
case valetphprc = "valetphprc"
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
convenience init(absolutePath: String, tld: String) {
|
||||
self.init()
|
||||
self.absolutePath = absolutePath
|
||||
self.name = URL(fileURLWithPath: absolutePath).lastPathComponent
|
||||
self.aliasPath = nil
|
||||
determineSecured(tld)
|
||||
determineComposerPhpVersion()
|
||||
determineDriver()
|
||||
}
|
||||
|
||||
convenience init(aliasPath: String, tld: String) {
|
||||
self.init()
|
||||
self.absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
|
||||
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 = Filesystem.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` or via .valetphprc)
|
||||
- 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() {
|
||||
|
||||
self.determineComposerInformation()
|
||||
self.determineValetPhpFileInfo()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks the contents of the composer.json file and determine the notable dependencies,
|
||||
as well as the requested PHP version. If no composer.json file is found, nothing happens.
|
||||
*/
|
||||
private func determineComposerInformation() {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks the contents of the .valetphprc file and determine the version, if possible.
|
||||
*/
|
||||
private func determineValetPhpFileInfo() {
|
||||
let path = "\(absolutePath!)/.valetphprc"
|
||||
|
||||
do {
|
||||
if Filesystem.fileExists(path) {
|
||||
let contents = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
|
||||
if let version = VersionExtractor.from(contents) {
|
||||
self.composerPhp = version
|
||||
self.composerPhpSource = .valetphprc
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Log.err("Something went wrong parsing the .valetphprc file")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
//
|
||||
// MainMenu+Composer.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 08/02/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension MainMenu {
|
||||
|
||||
/**
|
||||
Updates the global dependencies and runs the completion callback when done.
|
||||
This method should probably be broken up into several smaller methods at some point.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
85
phpmon/Domain/Menu/MainMenu+FixMyValet.swift
Normal file
@ -0,0 +1,85 @@
|
||||
//
|
||||
// MainMenu+FixMyValet.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 20/02/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
extension MainMenu {
|
||||
|
||||
@objc func fixMyValet() {
|
||||
let previousVersion = PhpEnv.phpInstall.version.short
|
||||
|
||||
if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
|
||||
presentAlertForMissingFormula()
|
||||
return
|
||||
}
|
||||
|
||||
if !BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_my_valet.title".localized,
|
||||
subtitle: "alert.fix_my_valet.info".localized(PhpEnv.brewPhpVersion)
|
||||
)
|
||||
.withPrimary(text: "alert.fix_my_valet.ok".localized)
|
||||
.withSecondary(text: "alert.fix_my_valet.cancel".localized)
|
||||
.didSelectPrimary()
|
||||
{
|
||||
Log.info("The user has chosen to abort Fix My Valet")
|
||||
return
|
||||
}
|
||||
|
||||
asyncExecution {
|
||||
Actions.fixMyValet()
|
||||
} success: {
|
||||
if previousVersion == PhpEnv.brewPhpVersion {
|
||||
self.presentAlertForSameVersion()
|
||||
} else {
|
||||
self.presentAlertForDifferentVersion(version: previousVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentAlertForMissingFormula() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.php_formula_missing.title".localized,
|
||||
subtitle: "alert.php_formula_missing.info".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
|
||||
private func presentAlertForSameVersion() {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_my_valet_done.title".localized,
|
||||
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
||||
description: "alert.fix_my_valet_done.desc".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
|
||||
private func presentAlertForDifferentVersion(version: String) {
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_my_valet_done.title".localized,
|
||||
subtitle: "alert.fix_my_valet_done.subtitle".localized,
|
||||
description: "alert.fix_my_valet_done.desc".localized
|
||||
)
|
||||
.withPrimary(text: "alert.fix_my_valet_done.switch_back".localized(version), action: { alert in
|
||||
alert.close(with: .alertSecondButtonReturn)
|
||||
MainMenu.shared.switchToPhpVersion(version)
|
||||
})
|
||||
.withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnv.brewPhpVersion))
|
||||
.withTertiary(text: "", action: { alert in
|
||||
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
|
||||
})
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
@ -12,15 +12,16 @@ extension MainMenu {
|
||||
/**
|
||||
Kick off the startup of the rendering of the main menu.
|
||||
*/
|
||||
func startup() {
|
||||
func startup() async {
|
||||
// Start with the icon
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
DispatchQueue.main.async {
|
||||
self.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() }
|
||||
)
|
||||
if await Startup().checkEnvironment() {
|
||||
self.onEnvironmentPass()
|
||||
} else {
|
||||
self.onEnvironmentFail()
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,11 +33,13 @@ extension MainMenu {
|
||||
|
||||
if HomebrewDiagnostics.hasAliasConflict() {
|
||||
DispatchQueue.main.async {
|
||||
Alert.notify(
|
||||
message: "alert.php_alias_conflict.title".localized,
|
||||
info: "alert.php_alias_conflict.info".localized,
|
||||
style: .critical
|
||||
)
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.php_alias_conflict.title".localized,
|
||||
subtitle: "alert.php_alias_conflict.info".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,10 +76,22 @@ extension MainMenu {
|
||||
Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)")
|
||||
}
|
||||
|
||||
Paths.shared.detectBinaryPaths()
|
||||
|
||||
Valet.shared.loadConfiguration()
|
||||
Valet.shared.validateVersion()
|
||||
Valet.shared.startPreloadingSites()
|
||||
|
||||
if (Valet.shared.config.tld != "test") {
|
||||
DispatchQueue.main.async {
|
||||
BetterAlert().withInformation(
|
||||
title: "alert.warnings.tld_issue.title".localized,
|
||||
subtitle: "alert.warnings.tld_issue.subtitle".localized,
|
||||
description: "alert.warnings.tld_issue.description".localized
|
||||
).withPrimary(text: "OK").show()
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
|
||||
|
||||
Log.info("PHP Monitor is ready to serve!")
|
||||
@ -101,18 +116,21 @@ extension MainMenu {
|
||||
*/
|
||||
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)
|
||||
}
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.cannot_start.title".localized,
|
||||
subtitle: "alert.cannot_start.subtitle".localized,
|
||||
description: "alert.cannot_start.description".localized
|
||||
)
|
||||
.withPrimary(text: "alert.cannot_start.retry".localized)
|
||||
.withSecondary(text: "alert.cannot_start.close".localized, action: { vc in
|
||||
vc.close(with: .alertSecondButtonReturn)
|
||||
exit(1)
|
||||
})
|
||||
.show()
|
||||
|
||||
startup()
|
||||
Task { await startup() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,9 +39,13 @@ extension MainMenu {
|
||||
|
||||
// Run composer updates
|
||||
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
|
||||
self.updateGlobalDependencies(notify: false, completion: { _ in
|
||||
self.notifyAboutVersionChange(to: version)
|
||||
})
|
||||
ComposerWindow().updateGlobalDependencies(
|
||||
notify: false,
|
||||
completion: { _ in
|
||||
self.notifyAboutVersionChange(to: version)
|
||||
}
|
||||
)
|
||||
|
||||
} else {
|
||||
self.notifyAboutVersionChange(to: version)
|
||||
}
|
||||
@ -52,12 +56,15 @@ extension MainMenu {
|
||||
}
|
||||
}
|
||||
|
||||
private func suggestFixMyValet(failed version: String) {
|
||||
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)
|
||||
@MainActor private func suggestFixMyValet(failed version: String) {
|
||||
let outcome = BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.php_switch_failed.title".localized(version),
|
||||
subtitle: "alert.php_switch_failed.info".localized(version)
|
||||
)
|
||||
.withPrimary(text: "alert.php_switch_failed.confirm".localized)
|
||||
.withSecondary(text: "alert.php_switch_failed.cancel".localized)
|
||||
.didSelectPrimary()
|
||||
if outcome {
|
||||
MainMenu.shared.fixMyValet()
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
/** Refreshes the icon with the PHP version. */
|
||||
@objc func refreshIcon() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
if (App.busy) {
|
||||
if (PhpEnv.shared.isBusy) {
|
||||
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
} else {
|
||||
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||
@ -170,26 +170,31 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
// MARK: - Actions
|
||||
|
||||
@objc func fixHomebrewPermissions() {
|
||||
if !Alert.present(
|
||||
messageText: "alert.fix_homebrew_permissions.title".localized,
|
||||
informativeText: "alert.fix_homebrew_permissions.info".localized,
|
||||
buttonTitle: "alert.fix_homebrew_permissions.ok".localized,
|
||||
secondButtonTitle: "alert.fix_homebrew_permissions.cancel".localized,
|
||||
style: .warning
|
||||
) {
|
||||
if !BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_homebrew_permissions.title".localized,
|
||||
subtitle: "alert.fix_homebrew_permissions.subtitle".localized,
|
||||
description: "alert.fix_homebrew_permissions.desc".localized
|
||||
)
|
||||
.withPrimary(text: "alert.fix_homebrew_permissions.ok".localized)
|
||||
.withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized)
|
||||
.didSelectPrimary() {
|
||||
return
|
||||
}
|
||||
|
||||
asyncExecution {
|
||||
try Actions.fixHomebrewPermissions()
|
||||
} success: {
|
||||
Alert.notify(
|
||||
message: "alert.fix_homebrew_permissions_done.title".localized,
|
||||
info: "alert.fix_homebrew_permissions_done.info".localized,
|
||||
style: .warning
|
||||
)
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.fix_homebrew_permissions_done.title".localized,
|
||||
subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized,
|
||||
description: "alert.fix_homebrew_permissions_done.desc".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
} failure: { error in
|
||||
Alert.notify(about: error as! HomebrewPermissionError)
|
||||
BetterAlert.show(for: error as! HomebrewPermissionError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,30 +264,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
}
|
||||
|
||||
@objc func fixMyValet() {
|
||||
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
|
||||
) {
|
||||
Log.info("The user has chosen to abort Fix My Valet")
|
||||
return
|
||||
}
|
||||
|
||||
asyncExecution {
|
||||
Actions.fixMyValet()
|
||||
} success: {
|
||||
Alert.notify(
|
||||
message: "alert.fix_my_valet_done.title".localized,
|
||||
info: "alert.fix_my_valet_done.info".localized
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateGlobalComposerDependencies() {
|
||||
self.updateGlobalDependencies(notify: true, completion: { _ in })
|
||||
ComposerWindow().updateGlobalDependencies(
|
||||
notify: true,
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
@objc func openActiveConfigFolder() {
|
||||
@ -342,7 +328,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
|
||||
@objc func openDonate() {
|
||||
NSWorkspace.shared.open(Constants.DonationUrl)
|
||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||
}
|
||||
|
||||
@objc func terminateApp() {
|
||||
|
@ -47,35 +47,18 @@ class ServicesView: NSView, XibLoadable {
|
||||
|
||||
override func viewWillDraw() {
|
||||
super.viewWillDraw()
|
||||
self.loadData()
|
||||
Task { await self.loadData() }
|
||||
}
|
||||
|
||||
@objc func updateInformation() {
|
||||
self.loadData()
|
||||
Task { await self.loadData() }
|
||||
}
|
||||
|
||||
// TODO: (5.1) Move data fetching, caching & retrieval somewhere else
|
||||
func loadData() {
|
||||
// Use stale data
|
||||
func loadData() async {
|
||||
self.applyAllInfoFieldsFromCachedValue()
|
||||
let services = await HomebrewService.loadAll()
|
||||
ServicesView.services = Dictionary(uniqueKeysWithValues: services.map{ ($0.name, $0) })
|
||||
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() {
|
||||
|
@ -22,7 +22,7 @@ class StatusMenu : NSMenu {
|
||||
}
|
||||
|
||||
func addPhpActionMenuItems() {
|
||||
if App.busy {
|
||||
if PhpEnv.shared.isBusy {
|
||||
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
|
||||
return
|
||||
}
|
||||
@ -97,26 +97,15 @@ class StatusMenu : NSMenu {
|
||||
func addFirstAidAndServicesMenuItems() {
|
||||
let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
|
||||
let servicesMenu = NSMenu()
|
||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_first_aid".localized))
|
||||
servicesMenu.addItem(NSMenuItem(
|
||||
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
|
||||
action: #selector(MainMenu.fixMyValet), 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 {
|
||||
servicesMenu.addItem(NSMenuItem(
|
||||
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
|
||||
action: #selector(MainMenu.fixMyValet), keyEquivalent: "")
|
||||
)
|
||||
}
|
||||
|
||||
/* (disabled until v5.1 and further tweaking)
|
||||
servicesMenu.addItem(NSMenuItem(
|
||||
title: "mi_fix_brew_permissions".localized(),
|
||||
action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: "")
|
||||
)
|
||||
*/
|
||||
|
||||
servicesMenu.addItem(NSMenuItem.separator())
|
||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
|
||||
@ -153,7 +142,7 @@ class StatusMenu : NSMenu {
|
||||
let longVersion = PhpEnv.shared.cachedPhpInstallations[shortVersion]!.longVersion
|
||||
|
||||
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||
let versionString = long ? longVersion.homebrewVersion : shortVersion
|
||||
let versionString = long ? longVersion.toString() : shortVersion
|
||||
|
||||
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
||||
let brew = (shortVersion == PhpEnv.brewPhpVersion) ? "php" : "php@\(shortVersion)"
|
||||
|
119
phpmon/Domain/Notice/BetterAlert.swift
Normal file
@ -0,0 +1,119 @@
|
||||
//
|
||||
// Notice.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 16/02/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class BetterAlert {
|
||||
|
||||
var windowController: NSWindowController!
|
||||
|
||||
var noticeVC: BetterAlertVC {
|
||||
return self.windowController.contentViewController as! BetterAlertVC
|
||||
}
|
||||
|
||||
init() {
|
||||
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
|
||||
|
||||
self.windowController = storyboard.instantiateController(
|
||||
withIdentifier: "noticeWindow"
|
||||
) as? NSWindowController
|
||||
}
|
||||
|
||||
public static func make() -> BetterAlert {
|
||||
return BetterAlert()
|
||||
}
|
||||
|
||||
public func withPrimary(
|
||||
text: String,
|
||||
action: @escaping (BetterAlertVC) -> Void = {
|
||||
vc in vc.close(with: .alertFirstButtonReturn)
|
||||
}
|
||||
) -> Self {
|
||||
self.noticeVC.buttonPrimary.title = text
|
||||
self.noticeVC.actionPrimary = action
|
||||
return self
|
||||
}
|
||||
|
||||
public func withSecondary(
|
||||
text: String,
|
||||
action: ((BetterAlertVC) -> Void)? = {
|
||||
vc in vc.close(with: .alertSecondButtonReturn)
|
||||
}
|
||||
) -> Self {
|
||||
self.noticeVC.buttonSecondary.title = text
|
||||
self.noticeVC.actionSecondary = action
|
||||
return self
|
||||
}
|
||||
|
||||
public func withTertiary(
|
||||
text: String = "",
|
||||
action: ((BetterAlertVC) -> Void)? = nil
|
||||
) -> Self {
|
||||
if text == "" {
|
||||
self.noticeVC.buttonTertiary.bezelStyle = .helpButton
|
||||
}
|
||||
self.noticeVC.buttonTertiary.title = text
|
||||
self.noticeVC.actionTertiary = action
|
||||
return self
|
||||
}
|
||||
|
||||
public func withInformation(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
description: String = ""
|
||||
) -> Self {
|
||||
self.noticeVC.labelTitle.stringValue = title
|
||||
self.noticeVC.labelSubtitle.stringValue = subtitle
|
||||
self.noticeVC.labelDescription.stringValue = description
|
||||
|
||||
// If the description is missing, handle the excess space and change the top margin
|
||||
if (description == "") {
|
||||
self.noticeVC.labelDescription.isHidden = true
|
||||
self.noticeVC.primaryButtonTopMargin.constant = 0
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Shows the modal and returns a ModalResponse.
|
||||
If you wish to simply show the alert and disregard the outcome, use `show`.
|
||||
*/
|
||||
public func runModal() -> NSApplication.ModalResponse {
|
||||
if !Thread.isMainThread {
|
||||
fatalError("You should always present alerts on the main thread!")
|
||||
}
|
||||
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
windowController.window?.makeKeyAndOrderFront(nil)
|
||||
return NSApplication.shared.runModal(for: windowController.window!)
|
||||
}
|
||||
|
||||
/** Shows the modal and returns true if the user pressed the primary button. */
|
||||
public func didSelectPrimary() -> Bool {
|
||||
return self.runModal() == .alertFirstButtonReturn
|
||||
}
|
||||
|
||||
/**
|
||||
Shows the modal and does not return anything.
|
||||
*/
|
||||
public func show() {
|
||||
_ = self.runModal()
|
||||
}
|
||||
|
||||
/**
|
||||
Shows the modal for a particular error.
|
||||
*/
|
||||
public static func show(for error: Error & AlertableError) {
|
||||
let key = error.getErrorMessageKey()
|
||||
return BetterAlert().withInformation(
|
||||
title: "\(key).title".localized,
|
||||
subtitle: "\(key).description".localized
|
||||
).withPrimary(text: "OK").show()
|
||||
}
|
||||
}
|
79
phpmon/Domain/Notice/BetterAlertVC.swift
Normal file
@ -0,0 +1,79 @@
|
||||
//
|
||||
// NoticeVC.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 16/02/2022.
|
||||
// Copyright © 2022 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class BetterAlertVC: NSViewController {
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
@IBOutlet weak var labelTitle: NSTextField!
|
||||
@IBOutlet weak var labelSubtitle: NSTextField!
|
||||
@IBOutlet weak var labelDescription: NSTextField!
|
||||
|
||||
@IBOutlet weak var buttonPrimary: NSButton!
|
||||
@IBOutlet weak var buttonSecondary: NSButton!
|
||||
@IBOutlet weak var buttonTertiary: NSButton!
|
||||
|
||||
var actionPrimary: (BetterAlertVC) -> Void = { _ in }
|
||||
var actionSecondary: ((BetterAlertVC) -> Void)?
|
||||
var actionTertiary: ((BetterAlertVC) -> Void)?
|
||||
|
||||
@IBOutlet weak var imageView: NSImageView!
|
||||
|
||||
@IBOutlet weak var primaryButtonTopMargin: NSLayoutConstraint!
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewWillAppear() {
|
||||
imageView.image = NSApp.applicationIconImage
|
||||
|
||||
if actionSecondary == nil {
|
||||
buttonSecondary.isHidden = true
|
||||
}
|
||||
if actionTertiary == nil {
|
||||
buttonTertiary.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear() {
|
||||
view.window?.makeFirstResponder(buttonPrimary)
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
Log.perf("A BetterAlert has been deinitialized.")
|
||||
}
|
||||
|
||||
// MARK: Outlet Actions
|
||||
|
||||
@IBAction func primaryButtonAction(_ sender: Any) {
|
||||
self.actionPrimary(self)
|
||||
}
|
||||
|
||||
@IBAction func secondaryButtonAction(_ sender: Any) {
|
||||
if self.actionSecondary != nil {
|
||||
self.actionSecondary!(self)
|
||||
} else {
|
||||
self.close(with: .alertSecondButtonReturn)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func tertiaryButtonAction(_ sender: Any) {
|
||||
if self.actionSecondary != nil {
|
||||
self.actionTertiary!(self)
|
||||
}
|
||||
}
|
||||
|
||||
public func close(with code: NSApplication.ModalResponse) {
|
||||
self.view.window?.close()
|
||||
NSApplication.shared.stopModal(withCode: code)
|
||||
}
|
||||
|
||||
}
|
@ -21,11 +21,13 @@ extension ActivePhpInstallation {
|
||||
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
|
||||
)
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "alert.php_fpm_broken.title".localized,
|
||||
subtitle: "alert.php_fpm_broken.info".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,7 @@ class Stats {
|
||||
(see `didSeeSponsorEncouragement`)
|
||||
*/
|
||||
public static func evaluateSponsorMessageShouldBeDisplayed() {
|
||||
|
||||
if Bundle.main.bundleIdentifier?.contains("beta") ?? false {
|
||||
return Log.info("Sponsor messages never apply to beta builds.")
|
||||
}
|
||||
@ -99,16 +100,24 @@ class Stats {
|
||||
}
|
||||
|
||||
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)
|
||||
let donate = BetterAlert()
|
||||
.withInformation(
|
||||
title: "startup.sponsor_encouragement.title".localized,
|
||||
subtitle: "startup.sponsor_encouragement.subtitle".localized,
|
||||
description: "startup.sponsor_encouragement.desc".localized
|
||||
)
|
||||
.withPrimary(text: "startup.sponsor_encouragement.accept".localized)
|
||||
.withSecondary(text: "startup.sponsor_encouragement.skip".localized)
|
||||
.withTertiary(text: "", action: { vc in
|
||||
vc.close(with: .alertThirdButtonReturn)
|
||||
NSWorkspace.shared.open(Constants.Urls.DonationPage)
|
||||
}).didSelectPrimary()
|
||||
|
||||
if donate {
|
||||
Log.info("The user is an absolute badass for choosing this option. Thank you.")
|
||||
NSWorkspace.shared.open(Constants.DonationUrl)
|
||||
NSWorkspace.shared.open(Constants.Urls.DonationPayment)
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue)
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import AppKit
|
||||
|
||||
class SiteListCell: NSTableCellView
|
||||
{
|
||||
var site: Valet.Site? = nil
|
||||
var site: ValetSite? = nil
|
||||
|
||||
@IBOutlet weak var labelSiteName: NSTextField!
|
||||
@IBOutlet weak var labelPathName: NSTextField!
|
||||
@ -29,7 +29,7 @@ class SiteListCell: NSTableCellView
|
||||
super.draw(dirtyRect)
|
||||
}
|
||||
|
||||
func populateCell(with site: Valet.Site) {
|
||||
func populateCell(with site: ValetSite) {
|
||||
self.site = site
|
||||
|
||||
// Make sure to show the TLD
|
||||
@ -80,8 +80,8 @@ class SiteListCell: NSTableCellView
|
||||
|
||||
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.informativeText = "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)"
|
||||
.localized
|
||||
|
||||
alert.addButton(withTitle: "site_link.close".localized)
|
||||
|
||||
|
@ -23,10 +23,13 @@ extension SiteListVC {
|
||||
} completion: { [self] in
|
||||
selectedSite.determineSecured(Valet.shared.config.tld)
|
||||
if selectedSite.secured == originalSecureStatus {
|
||||
Alert.notify(
|
||||
message: "site_list.alerts_status_not_changed.title".localized,
|
||||
info: "site_list.alerts_status_not_changed.desc".localized(command)
|
||||
)
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "site_list.alerts_status_not_changed.title".localized,
|
||||
subtitle: "site_list.alerts_status_not_changed.desc".localized(command)
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
} else {
|
||||
let newState = selectedSite.secured ? "secured" : "unsecured"
|
||||
LocalNotification.send(
|
||||
@ -51,10 +54,13 @@ extension SiteListVC {
|
||||
if url != nil {
|
||||
NSWorkspace.shared.open(url!)
|
||||
} else {
|
||||
_ = Alert.present(
|
||||
messageText: "site_list.alert.invalid_folder_name".localized,
|
||||
informativeText: "site_list.alert.invalid_folder_name_desc".localized
|
||||
)
|
||||
BetterAlert()
|
||||
.withInformation(
|
||||
title: "site_list.alert.invalid_folder_name".localized,
|
||||
subtitle: "site_list.alert.invalid_folder_name_desc".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ extension SiteListVC {
|
||||
}
|
||||
}
|
||||
|
||||
private func addUnlink(to menu: NSMenu, with site: Valet.Site) {
|
||||
private func addUnlink(to menu: NSMenu, with site: ValetSite) {
|
||||
if (site.aliasPath != nil) {
|
||||
menu.addItem(
|
||||
withTitle: "site_list.unlink".localized,
|
||||
@ -76,7 +76,7 @@ extension SiteListVC {
|
||||
}
|
||||
}
|
||||
|
||||
private func addToggleSecure(to menu: NSMenu, with site: Valet.Site) {
|
||||
private func addToggleSecure(to menu: NSMenu, with site: ValetSite) {
|
||||
menu.addItem(
|
||||
withTitle: site.secured
|
||||
? "site_list.unsecure".localized
|
||||
|
@ -20,7 +20,7 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||
// MARK: - Variables
|
||||
|
||||
/// List of sites that will be displayed in this view. Originates from the `Valet` object.
|
||||
var sites: [Valet.Site] = []
|
||||
var sites: [ValetSite] = []
|
||||
|
||||
/// Array that contains various apps that might open a particular site directory.
|
||||
var applications: [Application] {
|
||||
@ -32,7 +32,7 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
|
||||
|
||||
// MARK: - Helper Variables
|
||||
|
||||
var selectedSite: Valet.Site? {
|
||||
var selectedSite: ValetSite? {
|
||||
if tableView.selectedRow == -1 {
|
||||
return nil
|
||||
}
|
||||
|
@ -25,9 +25,8 @@
|
||||
"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_fix_brew_permissions" = "Restore Homebrew Permissions";
|
||||
"mi_fix_my_valet" = "Fix My Valet...";
|
||||
"mi_fix_brew_permissions" = "Restore Homebrew Permissions...";
|
||||
"mi_php_refresh" = "Refresh Information";
|
||||
|
||||
"mi_configuration" = "PHP Configuration";
|
||||
@ -181,11 +180,15 @@
|
||||
"notification.services_restarted" = "Valet services restarted";
|
||||
"notification.services_restarted_desc" = "All services have been successfully restarted.";
|
||||
|
||||
// ALERTS
|
||||
|
||||
// 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 /usr/local/bin`.";
|
||||
"alert.composer_missing.subtitle" = "PHP Monitor could not find Composer. Make sure that Composer is installed and try again.";
|
||||
"alert.composer_missing.desc" = "PHP Monitor assumes that Composer is located in either:
|
||||
|
||||
• `/usr/local/bin/composer`
|
||||
• `/opt/homebrew/bin/composer`
|
||||
|
||||
Make sure you have it installed in one of these locations, or make a symlink if you have Composer installed somewhere else.";
|
||||
|
||||
"alert.composer_progress.title" = "Updating global dependencies...";
|
||||
"alert.composer_progress.info" = "You can see the progress in the terminal output below.";
|
||||
@ -199,8 +202,11 @@ problem manually, using your own Terminal app (this just shows you the output)."
|
||||
"alert.composer_success.info" = "Your global Composer dependencies have been successfully updated.";
|
||||
|
||||
// Composer Version
|
||||
"alert.composer_php_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.";
|
||||
"alert.composer_php_requirement.title" = "`%@` has the following PHP requirement: %@.";
|
||||
"alert.composer_php_requirement.type.unknown" = "The required PHP version was determined by an unknown factor.";
|
||||
"alert.composer_php_requirement.type.require" = "This required PHP version was determined by checking the `require` field in the `composer.json` file when the site list was last refreshed.";
|
||||
"alert.composer_php_requirement.type.platform" = "This required PHP version was determined by checking the `platform` field in the `composer.json` file when the site list was last refreshed.";
|
||||
"alert.composer_php_requirement.type.valetphprc" = "This required PHP version was determined by checking the .valetphprc file in your project's directory.";
|
||||
|
||||
// Suggest Fix My Valet
|
||||
"alert.php_switch_failed.title" = "Switching to PHP %@ seems to have failed.";
|
||||
@ -208,6 +214,10 @@ problem manually, using your own Terminal app (this just shows you the output)."
|
||||
"alert.php_switch_failed.confirm" = "Yes, run \"Fix My Valet\"";
|
||||
"alert.php_switch_failed.cancel" = "Do Not Run";
|
||||
|
||||
// PHP Formula Missing
|
||||
"alert.php_formula_missing.title" = "Oops! The `php` formula must be installed for Fix My Valet...";
|
||||
"alert.php_formula_missing.info" = "It seems that you do not have the `php` formula installed, which prevents PHP Monitor from running Fix My Valet. Please install it using `brew install php`, restart PHP Monitor and try again.";
|
||||
|
||||
// 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.)";
|
||||
@ -216,25 +226,31 @@ problem manually, using your own Terminal app (this just shows you the output)."
|
||||
|
||||
// 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.";
|
||||
"alert.fix_my_valet_done.subtitle" = "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.";
|
||||
"alert.fix_my_valet_done.stay" = "Stay on PHP %@";
|
||||
"alert.fix_my_valet_done.switch_back" = "Switch back to PHP %@";
|
||||
"alert.fix_my_valet_done.desc" = "If 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.";
|
||||
|
||||
// Restore Homebrew Permissions
|
||||
"alert.fix_homebrew_permissions.title" = "About \"Restore Homebrew Permissions\"";
|
||||
"alert.fix_homebrew_permissions.info" = "This feature was created so you can run `brew upgrade` or `brew cleanup` without permission issues.\n\nThis will require administrative privileges, because PHP Monitor will restore your ownership of the files and folders that are currently owned by the `root` user, due to Valet services running as root.\n\n(You will be notified when this fix has been applied.)";
|
||||
"alert.fix_homebrew_permissions.subtitle" = "This feature was created so you can run `brew upgrade` or `brew cleanup` without permission issues.\n\n(You will be notified when this fix has been applied.)";
|
||||
"alert.fix_homebrew_permissions.desc" = "This will require administrative privileges, because PHP Monitor will restore your ownership of the files and folders that are currently owned by the `root` user, due to Valet services running as root.";
|
||||
"alert.fix_homebrew_permissions.ok" = "Restore Permissions";
|
||||
"alert.fix_homebrew_permissions.cancel" = "Cancel";
|
||||
|
||||
"alert.fix_homebrew_permissions_done.title" = "Permissions Fixed!";
|
||||
"alert.fix_homebrew_permissions_done.info" = "When you are done with Homebrew, you should open PHP Monitor and restart all services if you want Valet to work again. It is also recommended to restart PHP Monitor after running `brew upgrade`.";
|
||||
"alert.fix_homebrew_permissions_done.title" = "All file and folder permissions for Valet's dependencies have been restored.";
|
||||
"alert.fix_homebrew_permissions_done.subtitle" = "Because of this, all of Valet's services are currently no longer running. You can now interact with Homebrew, but your Valet sites will be unavailable as all services are disabled.";
|
||||
"alert.fix_homebrew_permissions_done.desc" = "When you are done with Homebrew (after running `brew upgrade`, for example, you should restart PHP Monitor and select \"Restart All Services\" if you want Valet to work again. It is always recommended to restart PHP Monitor whenever you upgrade PHP versions with `brew upgrade`, or things might break.";
|
||||
|
||||
// PHP FPM Broken
|
||||
"alert.php_fpm_broken.title" = "PHP-FPM configuration is incorrect";
|
||||
"alert.php_fpm_broken.info" = "PHP Monitor has determined that there are issues with your PHP-FPM config: it's not pointing to the Valet socket. This will result in 502 Bad Gateway if you visit websites linked via Valet.\n\nYou can usually fix this by running\n`valet install`, which updates your\n PHP-FPM configuration.";
|
||||
|
||||
// PHP Monitor Cannot Start
|
||||
"alert.cannot_start.title" = "PHP Monitor cannot start";
|
||||
"alert.cannot_start.info" = "The issue you were just notified about is keeping PHP Monitor from functioning correctly. Please fix the issue and restart PHP Monitor. After clicking on OK, PHP Monitor will close.\n\nIf you have fixed the issue (or don't remember what the exact issue is) you can click on Retry, which will have PHP Monitor retry the startup checks.";
|
||||
"alert.cannot_start.close" = "Close";
|
||||
"alert.cannot_start.title" = "PHP Monitor cannot start due to a configuration problem";
|
||||
"alert.cannot_start.subtitle" = "The issue you were just notified about is keeping PHP Monitor from functioning correctly.";
|
||||
"alert.cannot_start.description" = "You might not need to quit PHP Monitor and restart it. If you have fixed the issue (or don't remember what the exact issue is) you can click on Retry, which will have PHP Monitor retry the startup checks.";
|
||||
"alert.cannot_start.close" = "Quit";
|
||||
"alert.cannot_start.retry" = "Retry";
|
||||
|
||||
// PHP alias issue
|
||||
@ -250,50 +266,71 @@ You can do this by running `composer global update` in your terminal. After that
|
||||
|
||||
// STARTUP
|
||||
|
||||
/// 1. PHP binary not found
|
||||
/// 0. Architecture mismatch
|
||||
|
||||
"alert.homebrew_missing.title" = "PHP Monitor cannot start!";
|
||||
"alert.homebrew_missing.subtitle" = "A working Homebrew binary could not be found in the usual location. Please restart the app after fixing this issue.";
|
||||
"alert.homebrew_missing.info" = "You are running PHP Monitor with the following architecture: %@. As a result, a working Homebrew binary is expected in `%@`, but was not found. This is why PHP Monitor cannot work.\n\nIf you have not installed Homebrew yet, please do so. If you are on Apple Silicon, make sure your Homebrew and PHP Monitor use the same architecture, by enabling or disabling Rosetta where needed.";
|
||||
|
||||
"alert.homebrew_missing.quit" = "Quit";
|
||||
|
||||
/// PHP binary not found
|
||||
"startup.errors.php_binary.title" = "PHP is not correctly installed";
|
||||
"startup.errors.php_binary.desc" = "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php` (or `/opt/homebrew/bin/php`). The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)";
|
||||
"startup.errors.php_binary.subtitle" = "You must install PHP via Homebrew. The app will not work correctly until you resolve this issue.";
|
||||
"startup.errors.php_binary.desc" = "Usually running `brew link php` in your Terminal will resolve this issue.\n\nTo diagnose what is wrong, you can try running `which php` in your Terminal, it should return `%@`.";
|
||||
|
||||
/// 2. PHP not found in /usr/local/opt or /opt/homebrew/opt
|
||||
/// PHP not found in /usr/local/opt or /opt/homebrew/opt
|
||||
"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.";
|
||||
"startup.errors.php_opt.subtitle" = "The PHP alias was not found in `%@`. The app will not work correctly until you resolve this issue.";
|
||||
"startup.errors.php_opt.desc" = "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.";
|
||||
|
||||
/// 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.)";
|
||||
"startup.errors.valet_executable.subtitle" = "You must install Valet with Composer. The app will not work correctly until you resolve this issue.";
|
||||
"startup.errors.valet_executable.desc" = "If you haven't installed Laravel Valet yet, please do so first. If you have it installed but are seeing this message anyway, then try running `which valet` in Terminal, it should return: `%@`.";
|
||||
|
||||
/// 3b. Valet configuration file missing [currently not enabled]
|
||||
/// 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.";
|
||||
/// Valet version not readable
|
||||
"startup.errors.valet_version_unknown.title" = "Your Valet version could not be read";
|
||||
"startup.errors.valet_version_unknown.subtitle" = "Parsing the output of `valet --version` failed. Make sure your Valet installation works and is up-to-date.";
|
||||
"startup.errors.valet_version_unknown.desc" = "Try running `valet --version` in a terminal to find out what's going on.";
|
||||
|
||||
/// 4. Brew & sudoers
|
||||
/// 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.";
|
||||
"startup.errors.sudoers_brew.subtitle" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
|
||||
|
||||
/// 5. Valet & sudoers
|
||||
/// Valet & sudoers
|
||||
"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.";
|
||||
"startup.errors.sudoers_valet.subtitle" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue. If you did this before, please run `sudo valet trust` again.";
|
||||
|
||||
/// 6. Cannot retrieve services
|
||||
/// 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`.";
|
||||
"startup.errors.services_json_error.subtitle" = "PHP Monitor usually queries `brew` using the following command to test if the services can be retrieved: `sudo brew services info nginx --json`.\n\nPHP Monitor could not interpret this response.";
|
||||
"startup.errors.services_json_error.desc" = "This can happen if your Homebrew installation is out of date, in which case Homebrew won't return JSON yet. You can usually fix this by running `brew update`. You can also try running `sudo brew services info nginx --json` in your terminal of choice.";
|
||||
|
||||
/// 7. Multiple services active
|
||||
/// 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 '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";
|
||||
"startup.sponsor_encouragement.subtitle" = "To 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.desc" = "If you have already donated, then YOU are the reason why the app was able to get all these updates. In that case, this is a THANK YOU message to you. I appreciate the support.";
|
||||
|
||||
"startup.sponsor_encouragement.accept" = "Sponsor Now";
|
||||
"startup.sponsor_encouragement.learn_more" = "Learn More";
|
||||
"startup.sponsor_encouragement.skip" = "No Thanks";
|
||||
|
||||
// ERROR MESSAGES (based on AlertableError)
|
||||
|
||||
"alert.errors.homebrew_permissions.applescript_returned_nil.title" = "Restore Homebrew Permissions has been cancelled.";
|
||||
"alert.errors.homebrew_permissions.applescript_returned_nil.description" = "The outcome of the script that is executed to adjust the permissions returned nil, which usually means that you did not grant administrative permissions to PHP Monitor.\n\nIf you clicked on Cancel during the authentication prompt, this is normal. If you did actually authenticate and you are still seeing this message, something probably went wrong.";
|
||||
|
||||
// WARNINGS (hopefully removed in 5.2)
|
||||
|
||||
"alert.warnings.tld_issue.title" = "You are not using `.test` as the TLD for Valet.";
|
||||
"alert.warnings.tld_issue.subtitle" = "Using a non-default TLD is currently not supported in PHP Monitor and there are a few known issues.";
|
||||
"alert.warnings.tld_issue.description" = "PHP Monitor will remain functional, but there might be issues: the app might not correctly show which domains have been secured. For optimal results, go to your Valet configuration file (config.json in the Valet directory) and change the TLD back to `test`. After that, restart PHP Monitor and it should not warn you like this again.";
|
||||
|