diff --git a/DEVELOPER.md b/DEVELOPER.md
new file mode 100644
index 0000000..f06f564
--- /dev/null
+++ b/DEVELOPER.md
@@ -0,0 +1,46 @@
+# DEVELOPER README
+
+## 🔧 Build instructions
+
+
+
+If you'd like to build PHP Monitor yourself, you need:
+
+* Xcode (usually the latest version)
+* The contents of this repository
+
+Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to immediately build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
+
+If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
+
+## 🐛 Symbolication of crashes
+
+If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.
+
+For example, given the following crash (from an .ips file):
+
+```
+Thread 2 Crashed:: Dispatch queue: com.apple.root.user-initiated-qos
+0 libswiftDispatch.dylib 0x7ff82aa3ab8c static OS_dispatch_source.makeProcessSource(identifier:eventMask:queue:) + 28
+1 PHP Monitor 0x1096907d8 0x10965e000 + 206808
+ | |
+ address load address
+2 PHP Monitor 0x1096903ac 0x10965e000 + 205740
+3 PHP Monitor 0x10968f88b 0x10965e000 + 202891
+```
+
+You must use the correct order for the the address and load address in the command below:
+
+```
+$ atos -arch x86_64 -o '/path/to/PHP Monitor.app.dSYM/Contents/Resources/DWARF/PHP Monitor' -l 0x10965e000 0x1096907d8
+ | | | |
+ architecture path to DSYM load address address
+```
+
+This will return the relevant information, for example:
+
+```
+FSWatcher.startMonitoring(_:behaviour:) (in PHP Monitor) (PhpConfigWatcher.swift:95)
+```
+
+For more information, see [Apple's documentation](https://developer.apple.com/documentation/xcode/adding-identifiable-symbol-names-to-a-crash-report).
diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj
index 0c151ef..01baff1 100644
--- a/PHP Monitor.xcodeproj/project.pbxproj
+++ b/PHP Monitor.xcodeproj/project.pbxproj
@@ -9,13 +9,11 @@
/* Begin PBXBuildFile section */
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
- 54AB03262763858F00A29D5F /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AB03252763858F00A29D5F /* Timer.swift */; };
- 54AB03272763858F00A29D5F /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AB03252763858F00A29D5F /* Timer.swift */; };
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
54B48B60275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
- 54FCFD26276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */; };
- 54FCFD27276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */; };
+ 54FCFD26276C883F004CE748 /* SelectPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */; };
+ 54FCFD27276C883F004CE748 /* SelectPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */; };
54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */; };
54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */; };
54FCFD2D276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */; };
@@ -24,37 +22,31 @@
54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */; };
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */; };
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; };
+ C4068CA427B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */; };
+ C4068CA527B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */; };
+ C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */; };
+ 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 */; };
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
- C40B24F327A310780018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
C40B24F527A3108B0018C7D2 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; };
C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
- C40C7F202772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; };
- C40C7F2227721F8200DDDCDC /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
- C40C7F2327721F8200DDDCDC /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
- C40C7F2427721F8200DDDCDC /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
- C40C7F2527721F9800DDDCDC /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
- C40C7F2627721FA200DDDCDC /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; };
C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; };
- C40C7F2B2772201C00DDDCDC /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; };
C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
- C40C7F3227722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; };
C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; };
C415D3B72770F294005EF286 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; };
C415D3B82770F294005EF286 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; };
- C415D3E12770F34D005EF286 /* AllowedArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3DE2770F34D005EF286 /* AllowedArguments.swift */; };
- C415D3E62770F540005EF286 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3E52770F540005EF286 /* main.swift */; };
C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */; };
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */; };
C417DC74277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; };
C417DC75277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; };
- C417DC76277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; };
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; };
C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; };
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
@@ -79,6 +71,10 @@
C44C198E276E3A1C0072762D /* ProgressWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* ProgressWindow.swift */; };
C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C44C1990276E44CB0072762D /* ProgressWindow.storyboard */; };
C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C44C1990276E44CB0072762D /* ProgressWindow.storyboard */; };
+ C44CCD4027AFE2FC00CE40E5 /* AlertableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */; };
+ C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */; };
+ C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; };
+ C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; };
C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */; };
C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */; };
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* SiteListVC.swift */; };
@@ -101,8 +97,9 @@
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */; };
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */; };
- C48D6C72279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */; };
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */; };
+ C4927F0B27B2DFC200C55AFD /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4927F0A27B2DFC200C55AFD /* Errors.swift */; };
+ C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4927F0A27B2DFC200C55AFD /* Errors.swift */; };
C493084A279F331F009C240B /* AddSiteVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4930849279F331F009C240B /* AddSiteVC.swift */; };
C493084B279F331F009C240B /* AddSiteVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4930849279F331F009C240B /* AddSiteVC.swift */; };
C4998F0626175E7200B2526E /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = C4998F0526175E7200B2526E /* HotKey */; };
@@ -121,13 +118,10 @@
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */; };
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; };
C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; };
- C4B585402770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; };
C4B585412770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; };
C4B585422770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; };
- C4B585432770FE3900DA4FBE /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* Shell.swift */; };
C4B585442770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
C4B585452770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
- C4B585462770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; };
C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; };
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */; };
@@ -142,19 +136,20 @@
C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; };
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; };
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; };
+ 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 */; };
+ 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 */; };
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
- C4D9ADC1277610E1007277F4 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
- C4D9ADCA277611A0007277F4 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; };
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C4EC1E6D279DF87A0010F296 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; };
- C4EC1E6E279DF87A0010F296 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E6C279DF87A0010F296 /* Async.swift */; };
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE55A627708B9E001DF387 /* PMHeaderView.swift */; };
@@ -170,12 +165,12 @@
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; };
C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; };
- C4F30B05278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; };
C4F30B07278E195800755FCE /* brew-services.json in Resources */ = {isa = PBXBuildFile; fileRef = C4F30B06278E195800755FCE /* brew-services.json */; };
C4F30B08278E195800755FCE /* brew-services.json in Resources */ = {isa = PBXBuildFile; fileRef = C4F30B06278E195800755FCE /* brew-services.json */; };
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; };
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; };
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; };
+ C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; };
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */; };
@@ -209,39 +204,27 @@
};
/* End PBXContainerItemProxy section */
-/* Begin PBXCopyFilesBuildPhase section */
- C415D3D42770F341005EF286 /* CopyFiles */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = /usr/share/man/man1/;
- dstSubfolderSpec = 0;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 1;
- };
-/* End PBXCopyFilesBuildPhase section */
-
/* Begin PBXFileReference section */
5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = ""; };
5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; };
- 54AB03252763858F00A29D5F /* Timer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timer.swift; sourceTree = ""; };
54B48B5E275F66AE006D90C5 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; };
- 54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CheckboxPreferenceView.xib; sourceTree = ""; };
+ 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SelectPreferenceView.xib; sourceTree = ""; };
54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxPreferenceView.swift; sourceTree = ""; };
54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HotkeyPreferenceView.xib; sourceTree = ""; };
54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotkeyPreferenceView.swift; sourceTree = ""; };
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = ""; };
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = ""; };
+ C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CheckboxPreferenceView.xib; sourceTree = ""; };
+ C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectPreferenceView.swift; sourceTree = ""; };
+ C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarIcons.swift; sourceTree = ""; };
C40C7F1D2772136000DDDCDC /* PhpEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpEnv.swift; sourceTree = ""; };
C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActivePhpInstallation+Checks.swift"; sourceTree = ""; };
C40C7F2F27722E8D00DDDCDC /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; };
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = ""; };
C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFrameworks.swift; sourceTree = ""; };
C415D3B62770F294005EF286 /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = ""; };
- C415D3D62770F341005EF286 /* phpmon-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "phpmon-cli"; sourceTree = BUILT_PRODUCTS_DIR; };
- C415D3DE2770F34D005EF286 /* AllowedArguments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllowedArguments.swift; sourceTree = ""; };
- C415D3E52770F540005EF286 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+InterApp.swift"; sourceTree = ""; };
+ C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPER.md; sourceTree = ""; };
C417DC73277614690015E6EE /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; };
C4188988275FE8CB001EF227 /* Filesystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filesystem.swift; sourceTree = ""; };
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -263,6 +246,8 @@
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewJsonParserTest.swift; sourceTree = ""; };
C44C198C276E3A1C0072762D /* ProgressWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressWindow.swift; sourceTree = ""; };
C44C1990276E44CB0072762D /* ProgressWindow.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ProgressWindow.storyboard; sourceTree = ""; };
+ C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableError.swift; sourceTree = ""; };
+ C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Async.swift"; sourceTree = ""; };
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListWC.swift; sourceTree = ""; };
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListVC.swift; sourceTree = ""; };
C464ADB1275A87CA003FCD53 /* SiteListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListCell.swift; sourceTree = ""; };
@@ -280,6 +265,7 @@
C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = ""; };
C48D6C6F279CD2AC00F26D7E /* PhpVersionNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpVersionNumber.swift; sourceTree = ""; };
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionNumberTest.swift; sourceTree = ""; };
+ C4927F0A27B2DFC200C55AFD /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; };
C4930849279F331F009C240B /* AddSiteVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteVC.swift; sourceTree = ""; };
C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = ""; };
C49E171E27A5736E00787921 /* PMServicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PMServicesView.swift; sourceTree = ""; };
@@ -301,6 +287,8 @@
C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = ""; };
C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpConfigWatcher.swift; sourceTree = ""; };
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = ""; };
+ C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Switcher.swift"; sourceTree = ""; };
+ C4CE3BB927B31F670086CA49 /* MainMenu+Composer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Composer.swift"; sourceTree = ""; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = ""; };
C4D89BC52783C99400A02B68 /* ComposerJson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerJson.swift; sourceTree = ""; };
C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpSwitcher.swift; sourceTree = ""; };
@@ -333,13 +321,6 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
- C415D3D32770F341005EF286 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
C41C1B3022B0097F00E7CF16 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -365,6 +346,7 @@
5420395826135DC100FB00FA /* PrefsVC.swift */,
5420395E2613607600FB00FA /* Preferences.swift */,
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */,
+ C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */,
C4DEB7D327A5D60B00834718 /* Stats.swift */,
C41CD0272628D8E20065BBED /* Keybinds */,
54FCFD28276C88C0004CE748 /* Views */,
@@ -388,8 +370,10 @@
54FCFD28276C88C0004CE748 /* Views */ = {
isa = PBXGroup;
children = (
- 54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */,
+ C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */,
54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */,
+ 54FCFD25276C883F004CE748 /* SelectPreferenceView.xib */,
+ C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */,
54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */,
54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */,
);
@@ -431,26 +415,16 @@
path = Core;
sourceTree = "";
};
- C415D3D72770F341005EF286 /* phpmon-cli */ = {
- isa = PBXGroup;
- children = (
- C415D3E52770F540005EF286 /* main.swift */,
- C415D3DE2770F34D005EF286 /* AllowedArguments.swift */,
- );
- path = "phpmon-cli";
- sourceTree = "";
- };
C41C1B2A22B0097F00E7CF16 = {
isa = PBXGroup;
children = (
C4F8C0A522D4FA41002EFE61 /* README.md */,
C4E713562570150F00007428 /* SECURITY.md */,
C4F7807425D7F7E5000DBC97 /* RELEASE.md */,
+ C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */,
C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */,
C4F7807A25D7F84B000DBC97 /* phpmon-tests */,
- C415D3D72770F341005EF286 /* phpmon-cli */,
- C4B5853A2770FE2500DA4FBE /* phpmon-common */,
C41C1B3422B0097F00E7CF16 /* Products */,
C4D309E72770EF2F00958BCF /* Frameworks */,
);
@@ -461,7 +435,6 @@
children = (
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */,
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */,
- C415D3D62770F341005EF286 /* phpmon-cli */,
);
name = Products;
sourceTree = "";
@@ -469,6 +442,7 @@
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup;
children = (
+ C4B5853A2770FE2500DA4FBE /* Common */,
C41E181722CB61EB0072CF09 /* Domain */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
C4232EE42612526500158FC6 /* Credits.html */,
@@ -492,15 +466,13 @@
isa = PBXGroup;
children = (
C4AF9F6B275445D300D44ED0 /* Integrations */,
- C4B13B1D25C4915000548C3A /* Core */,
+ C4B13B1D25C4915000548C3A /* App */,
C4D9ADBD27761084007277F4 /* PHP */,
C47331A0247093AC009A0597 /* Menu */,
C464ADAA275A7A25003FCD53 /* SiteList */,
5420395726135DB800FB00FA /* Preferences */,
C44C198F276E3A380072762D /* Progress */,
C4C8E81D276F5686003AC782 /* Watcher */,
- C4811D2822D70D9C00B5F6B3 /* Helpers */,
- C4F8C0A222D4F100002EFE61 /* Extensions */,
C4EE55B027708BB2001DF387 /* SwiftUI */,
);
path = Domain;
@@ -515,6 +487,15 @@
path = Progress;
sourceTree = "";
};
+ C44CCD4327AFE93300CE40E5 /* Errors */ = {
+ isa = PBXGroup;
+ children = (
+ C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */,
+ C4927F0A27B2DFC200C55AFD /* Errors.swift */,
+ );
+ path = Errors;
+ sourceTree = "";
+ };
C464ADAA275A7A25003FCD53 /* SiteList */ = {
isa = PBXGroup;
children = (
@@ -532,7 +513,10 @@
isa = PBXGroup;
children = (
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */,
+ C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */,
C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */,
+ C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */,
+ C4CE3BB927B31F670086CA49 /* MainMenu+Composer.swift */,
C47331A1247093B7009A0597 /* StatusMenu.swift */,
C48D0C9525CC80B100CC7490 /* HeaderView.swift */,
C48D0C9925CC888B00CC7490 /* HeaderView.xib */,
@@ -553,7 +537,6 @@
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */,
- 54AB03252763858F00A29D5F /* Timer.swift */,
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */,
C4EC1E6C279DF87A0010F296 /* Async.swift */,
);
@@ -595,7 +578,7 @@
path = Homebrew;
sourceTree = "";
};
- C4B13B1D25C4915000548C3A /* Core */ = {
+ C4B13B1D25C4915000548C3A /* App */ = {
isa = PBXGroup;
children = (
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
@@ -609,16 +592,19 @@
C4D8016522B1584700C6DA1B /* Startup.swift */,
C4EED88827A48778006D7272 /* InterAppHandler.swift */,
);
- path = Core;
+ path = App;
sourceTree = "";
};
- C4B5853A2770FE2500DA4FBE /* phpmon-common */ = {
+ C4B5853A2770FE2500DA4FBE /* Common */ = {
isa = PBXGroup;
children = (
C40C7F2127721F7300DDDCDC /* Core */,
54B20EDF263AA22C00D3250E /* PHP */,
+ C44CCD4327AFE93300CE40E5 /* Errors */,
+ C4F8C0A222D4F100002EFE61 /* Extensions */,
+ C4811D2822D70D9C00B5F6B3 /* Helpers */,
);
- path = "phpmon-common";
+ path = Common;
sourceTree = "";
};
C4C8E81D276F5686003AC782 /* Watcher */ = {
@@ -715,23 +701,6 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
- C415D3D52770F341005EF286 /* phpmon-cli */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = C415D3DA2770F341005EF286 /* Build configuration list for PBXNativeTarget "phpmon-cli" */;
- buildPhases = (
- C415D3D22770F341005EF286 /* Sources */,
- C415D3D32770F341005EF286 /* Frameworks */,
- C415D3D42770F341005EF286 /* CopyFiles */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = "phpmon-cli";
- productName = "phpmon-cli";
- productReference = C415D3D62770F341005EF286 /* phpmon-cli */;
- productType = "com.apple.product-type.tool";
- };
C41C1B3222B0097F00E7CF16 /* PHP Monitor */ = {
isa = PBXNativeTarget;
buildConfigurationList = C41C1B4322B0098000E7CF16 /* Build configuration list for PBXNativeTarget "PHP Monitor" */;
@@ -780,9 +749,6 @@
LastUpgradeCheck = 1320;
ORGANIZATIONNAME = "Nico Verbruggen";
TargetAttributes = {
- C415D3D52770F341005EF286 = {
- CreatedOnToolsVersion = 13.2.1;
- };
C41C1B3222B0097F00E7CF16 = {
CreatedOnToolsVersion = 10.2.1;
};
@@ -810,7 +776,6 @@
targets = (
C41C1B3222B0097F00E7CF16 /* PHP Monitor */,
C4F7807825D7F84B000DBC97 /* phpmon-tests */,
- C415D3D52770F341005EF286 /* phpmon-cli */,
);
};
/* End PBXProject section */
@@ -827,9 +792,10 @@
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */,
C4232EE52612526500158FC6 /* Credits.html in Resources */,
- 54FCFD26276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */,
+ 54FCFD26276C883F004CE748 /* SelectPreferenceView.xib in Resources */,
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
C4F30B07278E195800755FCE /* brew-services.json in Resources */,
+ C4068CA427B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */,
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */,
54FCFD2D276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
@@ -841,9 +807,10 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 54FCFD27276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */,
+ 54FCFD27276C883F004CE748 /* SelectPreferenceView.xib in Resources */,
54FCFD2E276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
+ C4068CA527B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */,
C43A8A2025D9D1D700591B77 /* brew.json in Resources */,
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */,
C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */,
@@ -854,33 +821,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
- C415D3D22770F341005EF286 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- C40C7F2427721F8200DDDCDC /* PhpExtension.swift in Sources */,
- C40C7F2627721FA200DDDCDC /* Constants.swift in Sources */,
- C4B585402770FE3900DA4FBE /* Paths.swift in Sources */,
- C415D3E62770F540005EF286 /* main.swift in Sources */,
- C40B24F327A310780018C7D2 /* Events.swift in Sources */,
- C40C7F2227721F8200DDDCDC /* PhpInstallation.swift in Sources */,
- C4B585432770FE3900DA4FBE /* Shell.swift in Sources */,
- C4D9ADC1277610E1007277F4 /* PhpSwitcher.swift in Sources */,
- C4F30B05278E16BA00755FCE /* HomebrewService.swift in Sources */,
- C4EC1E6E279DF87A0010F296 /* Async.swift in Sources */,
- C40C7F2327721F8200DDDCDC /* ActivePhpInstallation.swift in Sources */,
- C4B585462770FE3900DA4FBE /* Command.swift in Sources */,
- C4D9ADCA277611A0007277F4 /* InternalSwitcher.swift in Sources */,
- C48D6C72279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
- C40C7F2527721F9800DDDCDC /* HomebrewPackage.swift in Sources */,
- C417DC76277614690015E6EE /* Helpers.swift in Sources */,
- C40C7F3227722E8D00DDDCDC /* Logger.swift in Sources */,
- C40C7F2B2772201C00DDDCDC /* Actions.swift in Sources */,
- C415D3E12770F34D005EF286 /* AllowedArguments.swift in Sources */,
- C40C7F202772136000DDDCDC /* PhpEnv.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
C41C1B2F22B0097F00E7CF16 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -894,6 +834,7 @@
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */,
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
+ C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
C49E171F27A5736E00787921 /* PMServicesView.swift in Sources */,
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */,
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
@@ -914,6 +855,7 @@
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */,
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */,
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
+ C4CE3BB827B31F2E0086CA49 /* MainMenu+Switcher.swift in Sources */,
C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */,
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
@@ -926,7 +868,7 @@
C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */,
- 54AB03262763858F00A29D5F /* Timer.swift in Sources */,
+ C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */,
C417DC74277614690015E6EE /* Helpers.swift in Sources */,
C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
@@ -934,9 +876,12 @@
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */,
C4EC1E6D279DF87A0010F296 /* Async.swift in Sources */,
+ C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */,
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */,
+ C4927F0B27B2DFC200C55AFD /* Errors.swift in Sources */,
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
+ C44CCD4027AFE2FC00CE40E5 /* AlertableError.swift in Sources */,
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */,
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */,
@@ -944,6 +889,7 @@
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
+ C4CE3BBA27B31F670086CA49 /* MainMenu+Composer.swift in Sources */,
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */,
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
@@ -967,7 +913,6 @@
C4EE55AE27708B9E001DF387 /* PMStatsView.swift in Sources */,
C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
- 54AB03272763858F00A29D5F /* Timer.swift in Sources */,
54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
C415D3B82770F294005EF286 /* Actions.swift in Sources */,
54B48B60275F66AE006D90C5 /* Application.swift in Sources */,
@@ -976,12 +921,14 @@
C4D9ADC0277610E1007277F4 /* PhpSwitcher.swift in Sources */,
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
+ C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */,
C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */,
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */,
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */,
+ C4F319C927B034A500AFF46F /* Stats.swift in Sources */,
C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */,
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
@@ -1000,15 +947,19 @@
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */,
C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */,
+ C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */,
C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */,
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */,
+ C4CE3BBC27B324250086CA49 /* MainMenu+Composer.swift in Sources */,
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */,
C417DC75277614690015E6EE /* Helpers.swift in Sources */,
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
+ C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */,
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
+ C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */,
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
@@ -1027,6 +978,8 @@
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */,
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
+ C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */,
+ C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */,
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */,
@@ -1065,34 +1018,6 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
- C415D3DB2770F341005EF286 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
- CODE_SIGN_IDENTITY = "-";
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = 8M54J5J787;
- ENABLE_HARDENED_RUNTIME = YES;
- MACOSX_DEPLOYMENT_TARGET = 11.0;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- };
- name = Debug;
- };
- C415D3DC2770F341005EF286 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
- CODE_SIGN_IDENTITY = "-";
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = 8M54J5J787;
- ENABLE_HARDENED_RUNTIME = YES;
- MACOSX_DEPLOYMENT_TARGET = 11.0;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- };
- name = Release;
- };
C41C1B4122B0098000E7CF16 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -1218,7 +1143,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 560;
+ CURRENT_PROJECT_VERSION = 610;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@@ -1227,7 +1152,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
- MARKETING_VERSION = 5.0;
+ MARKETING_VERSION = 5.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1243,7 +1168,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 560;
+ CURRENT_PROJECT_VERSION = 610;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@@ -1252,7 +1177,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
- MARKETING_VERSION = 5.0;
+ MARKETING_VERSION = 5.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1305,15 +1230,6 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
- C415D3DA2770F341005EF286 /* Build configuration list for PBXNativeTarget "phpmon-cli" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- C415D3DB2770F341005EF286 /* Debug */,
- C415D3DC2770F341005EF286 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
C41C1B2E22B0097F00E7CF16 /* Build configuration list for PBXProject "PHP Monitor" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor CLI.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor CLI.xcscheme
deleted file mode 100644
index 41be842..0000000
--- a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor CLI.xcscheme
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/README.md b/README.md
index 19a9653..6a8e202 100644
--- a/README.md
+++ b/README.md
@@ -320,9 +320,9 @@ You can always still ask Valet using the command line, should it be necessary. I
After running PHP Monitor, Homebrew sometimes has issues with `brew upgrade` or `brew cleanup`!
-This is a security feature of Homebrew. When you start a service as an administrator, the root user becomes the owner of relevant binaries. You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder).
+You can now use **First Aid & Services > Restore Homebrew Permissions** to (temporarily) resolve this issue and allow for a clean and painless `brew upgrade` or `brew cleanup` process.
-If you would like to know more, consult [this issue](https://github.com/nicoverbruggen/phpmon/issues/85) for more information.
+If you would like to know more, consult [this issue](https://github.com/nicoverbruggen/phpmon/issues/85) for more information about why this is needed.
@@ -399,15 +399,4 @@ I have done my best to annotate as much as humanly possible, and have avoided us
I also have a few tests for key parts of the application that I found needed to be tested. In the future, I would like to add even more tests for some of the UI stuff, but for now the tests are more unit tests than feature tests.
-## 🔧 Build instructions
-
-
-
-If you'd like to build PHP Monitor yourself, you need:
-
-* Xcode (usually the latest version)
-* The contents of this repository
-
-Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to immediately build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
-
-If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
+For more detailed information for developers, please see [the documentation file for developers](./DEVS.md).
diff --git a/assets/icons.afdesign b/assets/icons.afdesign
new file mode 100644
index 0000000..a78eaf1
Binary files /dev/null and b/assets/icons.afdesign differ
diff --git a/phpmon-cli/AllowedArguments.swift b/phpmon-cli/AllowedArguments.swift
deleted file mode 100644
index e12a279..0000000
--- a/phpmon-cli/AllowedArguments.swift
+++ /dev/null
@@ -1,26 +0,0 @@
-//
-// AllowedArguments.swift
-// phpmon-cli
-//
-// Created by Nico Verbruggen on 20/12/2021.
-// Copyright © 2021 Nico Verbruggen. All rights reserved.
-//
-
-import Foundation
-
-enum AllowedArguments: String, CaseIterable {
- case use = "use"
- case performSwitch = "switch"
- case fix = "fix"
- case help = "help"
-
- static func has(_ string: String) -> Bool {
- return Self.allCases.contains { arg in
- return arg.rawValue == string
- }
- }
-
- static var rawValues: [String] {
- return Self.allCases.map { $0.rawValue }
- }
-}
diff --git a/phpmon-cli/main.swift b/phpmon-cli/main.swift
deleted file mode 100644
index 8b986a2..0000000
--- a/phpmon-cli/main.swift
+++ /dev/null
@@ -1,103 +0,0 @@
-//
-// main.swift
-// phpmon-cli
-//
-// Created by Nico Verbruggen on 20/12/2021.
-// Copyright © 2021 Nico Verbruggen. All rights reserved.
-//
-
-import Foundation
-
-let toolver = "0.1 (early access)"
-
-let log = Log.shared
-log.verbosity = .info
-
-if CommandLine.arguments.contains("-q") || CommandLine.arguments.contains("--quiet") {
- Log.shared.verbosity = .warning
-}
-if CommandLine.arguments.contains("-p") || CommandLine.arguments.contains("--performance") {
- Log.shared.verbosity = .performance
-}
-
-var argument = "help"
-if CommandLine.arguments.count > 1 {
- argument = CommandLine.arguments[1]
-}
-
-if !AllowedArguments.has(argument) {
- Log.err("The supported arguments are: \(AllowedArguments.rawValues)")
- exit(1)
-}
-
-let action = AllowedArguments.init(rawValue: argument)
-
-switch action {
-case .use, .performSwitch:
- if !Shell.fileExists("\(Paths.binPath)/php") {
- Log.err("PHP is currently not linked. Attempting quick fix...")
- _ = Shell.user.executeSynchronously("brew link php", requiresPath: true)
- }
-
- let phpenv = PhpEnv.shared
- PhpEnv.detectPhpVersions()
-
- if CommandLine.arguments.count < 3 {
- Log.err("You must enter at least two additional arguments when using this command.")
- exit(1)
- }
-
- let version = CommandLine.arguments[2].replacingOccurrences(of: "php@", with: "")
- if phpenv.availablePhpVersions.contains(version) {
- Log.info("Switching to PHP \(version)...")
- Actions.switchToPhpVersion(
- version: version,
- availableVersions: phpenv.availablePhpVersions,
- completed: {
- Log.info("The switch has been completed.")
- exit(0)
- }
- )
- } else {
- Log.err("A PHP installation with version \(version) is not installed.")
- Log.err("The installed versions are: \(phpenv.availablePhpVersions.joined(separator: ", ")).")
- Log.err("If this version is available, you may be able to install it by using `brew install php@\(version)`.")
- exit(1)
- }
-case .fix:
- Log.info("Fixing your PHP installation...")
- Actions.fixMyPhp()
- Log.info("All operations completed. You can check which version of PHP is linked by using `php -v`.")
- exit(0)
-case .help:
- print("""
- ===============================================================
- PHP MONITOR CLI \(toolver)
- by Nico Verbruggen
- ===============================================================
-
- Gives access to the quick version switcher from PHP Monitor,
- but without the GUI and 100% of the speed!
-
- SUPPORTED COMMANDS
-
- * use {version}: Switch to a specific version of PHP.
- (e.g. `phpmon-cli use 8.0`)
- * switch {version}: Alias for the `use` command.
- * fix Attempts to unlink all PHP versions,
- and link the latest version of PHP.
- * help: Show this help.
-
- SUPPORTED FLAGS
-
- * `-q / --quiet`: Silences all logs except for warnings and exceptions.
- * `-p / --perf`: Enables performance mode.
-
- """)
- exit(0)
-case .none:
- Log.err("Action not recognized!")
- exit(1)
-}
-
-RunLoop.main.run()
diff --git a/phpmon-tests/PhpVersionNumberTest.swift b/phpmon-tests/PhpVersionNumberTest.swift
index 2a059e3..8ef2ac4 100644
--- a/phpmon-tests/PhpVersionNumberTest.swift
+++ b/phpmon-tests/PhpVersionNumberTest.swift
@@ -11,6 +11,10 @@ import XCTest
class PhpVersionNumberTest: XCTestCase {
func testCanDeconstructPhpVersion() throws {
+ XCTAssertEqual(
+ try! PhpVersionNumber.parse("PHP 8.1.0RC5-dev"),
+ PhpVersionNumber(major: 8, minor: 1, patch: 0)
+ )
XCTAssertEqual(
PhpVersionNumber.make(from: "8.0.11"),
PhpVersionNumber(major: 8, minor: 0, patch: 11)
@@ -29,6 +33,12 @@ class PhpVersionNumberTest: XCTestCase {
)
}
+ func testPhpVersionNumberParse() throws {
+ XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
+ XCTAssertTrue(error is VersionParseError)
+ }
+ }
+
func testCanCheckFixedConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
diff --git a/phpmon/Assets.xcassets/IconLinked.imageset/Contents.json b/phpmon/Assets.xcassets/IconLinked.imageset/Contents.json
index 6cb3d9a..89391b7 100644
--- a/phpmon/Assets.xcassets/IconLinked.imageset/Contents.json
+++ b/phpmon/Assets.xcassets/IconLinked.imageset/Contents.json
@@ -1,11 +1,12 @@
{
"images" : [
{
+ "filename" : "Linked.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "link.svg",
+ "filename" : "Linked@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
diff --git a/phpmon/Assets.xcassets/IconLinked.imageset/Linked.png b/phpmon/Assets.xcassets/IconLinked.imageset/Linked.png
new file mode 100644
index 0000000..a0e0f4d
Binary files /dev/null and b/phpmon/Assets.xcassets/IconLinked.imageset/Linked.png differ
diff --git a/phpmon/Assets.xcassets/IconLinked.imageset/Linked@2x.png b/phpmon/Assets.xcassets/IconLinked.imageset/Linked@2x.png
new file mode 100644
index 0000000..cf1ff87
Binary files /dev/null and b/phpmon/Assets.xcassets/IconLinked.imageset/Linked@2x.png differ
diff --git a/phpmon/Assets.xcassets/IconLinked.imageset/link.svg b/phpmon/Assets.xcassets/IconLinked.imageset/link.svg
deleted file mode 100644
index f0fdbf4..0000000
--- a/phpmon/Assets.xcassets/IconLinked.imageset/link.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/phpmon/Assets.xcassets/IconParked.imageset/Contents.json b/phpmon/Assets.xcassets/IconParked.imageset/Contents.json
index 24b6de7..f07a245 100644
--- a/phpmon/Assets.xcassets/IconParked.imageset/Contents.json
+++ b/phpmon/Assets.xcassets/IconParked.imageset/Contents.json
@@ -1,11 +1,12 @@
{
"images" : [
{
+ "filename" : "Parked.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "car-alt.svg",
+ "filename" : "Parked@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
diff --git a/phpmon/Assets.xcassets/IconParked.imageset/Parked.png b/phpmon/Assets.xcassets/IconParked.imageset/Parked.png
new file mode 100644
index 0000000..810a32e
Binary files /dev/null and b/phpmon/Assets.xcassets/IconParked.imageset/Parked.png differ
diff --git a/phpmon/Assets.xcassets/IconParked.imageset/Parked@2x.png b/phpmon/Assets.xcassets/IconParked.imageset/Parked@2x.png
new file mode 100644
index 0000000..98b6b4c
Binary files /dev/null and b/phpmon/Assets.xcassets/IconParked.imageset/Parked@2x.png differ
diff --git a/phpmon/Assets.xcassets/IconParked.imageset/car-alt.svg b/phpmon/Assets.xcassets/IconParked.imageset/car-alt.svg
deleted file mode 100644
index 90a786e..0000000
--- a/phpmon/Assets.xcassets/IconParked.imageset/car-alt.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/phpmon/Assets.xcassets/Menu Bar Icons/Contents.json b/phpmon/Assets.xcassets/Menu Bar Icons/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/phpmon/Assets.xcassets/Menu Bar Icons/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Contents.json b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Contents.json
new file mode 100644
index 0000000..5c666d4
--- /dev/null
+++ b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Contents.json
@@ -0,0 +1,22 @@
+{
+ "images" : [
+ {
+ "filename" : "Menu Bar Elephant.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "Menu Bar Elephant@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant.png b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant.png
new file mode 100644
index 0000000..8dd923e
Binary files /dev/null and b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant.png differ
diff --git a/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant@2x.png b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant@2x.png
new file mode 100644
index 0000000..86bc7e8
Binary files /dev/null and b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant@2x.png differ
diff --git a/phpmon/Assets.xcassets/StatusBarPHP.imageset/Contents.json b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Contents.json
similarity index 78%
rename from phpmon/Assets.xcassets/StatusBarPHP.imageset/Contents.json
rename to phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Contents.json
index effdb9c..0f8299a 100644
--- a/phpmon/Assets.xcassets/StatusBarPHP.imageset/Contents.json
+++ b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Contents.json
@@ -1,11 +1,12 @@
{
"images" : [
{
+ "filename" : "Menu Bar.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "php@2x.png",
+ "filename" : "Menu Bar@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
diff --git a/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar.png b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar.png
new file mode 100644
index 0000000..179b98e
Binary files /dev/null and b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar.png differ
diff --git a/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar@2x.png b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar@2x.png
new file mode 100644
index 0000000..522077c
Binary files /dev/null and b/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar@2x.png differ
diff --git a/phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json b/phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json
index b7c29f9..b5851cb 100644
--- a/phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json
+++ b/phpmon/Assets.xcassets/ServiceLoading.imageset/Contents.json
@@ -1,11 +1,12 @@
{
"images" : [
{
+ "filename" : "ServiceLoading.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "question-circle.svg",
+ "filename" : "ServiceLoading@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
diff --git a/phpmon/Assets.xcassets/ServiceLoading.imageset/ServiceLoading.png b/phpmon/Assets.xcassets/ServiceLoading.imageset/ServiceLoading.png
new file mode 100644
index 0000000..2967845
Binary files /dev/null and b/phpmon/Assets.xcassets/ServiceLoading.imageset/ServiceLoading.png differ
diff --git a/phpmon/Assets.xcassets/ServiceLoading.imageset/ServiceLoading@2x.png b/phpmon/Assets.xcassets/ServiceLoading.imageset/ServiceLoading@2x.png
new file mode 100644
index 0000000..79ba93a
Binary files /dev/null and b/phpmon/Assets.xcassets/ServiceLoading.imageset/ServiceLoading@2x.png differ
diff --git a/phpmon/Assets.xcassets/ServiceLoading.imageset/question-circle.svg b/phpmon/Assets.xcassets/ServiceLoading.imageset/question-circle.svg
deleted file mode 100644
index df1f089..0000000
--- a/phpmon/Assets.xcassets/ServiceLoading.imageset/question-circle.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json b/phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json
index 5aba3d7..10f5302 100644
--- a/phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json
+++ b/phpmon/Assets.xcassets/ServiceOff.imageset/Contents.json
@@ -1,11 +1,12 @@
{
"images" : [
{
+ "filename" : "ServiceOff.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "times-circle.svg",
+ "filename" : "ServiceOff@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
diff --git a/phpmon/Assets.xcassets/ServiceOff.imageset/ServiceOff.png b/phpmon/Assets.xcassets/ServiceOff.imageset/ServiceOff.png
new file mode 100644
index 0000000..326901b
Binary files /dev/null and b/phpmon/Assets.xcassets/ServiceOff.imageset/ServiceOff.png differ
diff --git a/phpmon/Assets.xcassets/ServiceOff.imageset/ServiceOff@2x.png b/phpmon/Assets.xcassets/ServiceOff.imageset/ServiceOff@2x.png
new file mode 100644
index 0000000..9344277
Binary files /dev/null and b/phpmon/Assets.xcassets/ServiceOff.imageset/ServiceOff@2x.png differ
diff --git a/phpmon/Assets.xcassets/ServiceOff.imageset/times-circle.svg b/phpmon/Assets.xcassets/ServiceOff.imageset/times-circle.svg
deleted file mode 100644
index b5d5eba..0000000
--- a/phpmon/Assets.xcassets/ServiceOff.imageset/times-circle.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json b/phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json
index e3569a7..b1566c6 100644
--- a/phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json
+++ b/phpmon/Assets.xcassets/ServiceOn.imageset/Contents.json
@@ -1,11 +1,12 @@
{
"images" : [
{
+ "filename" : "ServiceOn.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "check-circle.svg",
+ "filename" : "ServiceOn@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
diff --git a/phpmon/Assets.xcassets/ServiceOn.imageset/ServiceOn.png b/phpmon/Assets.xcassets/ServiceOn.imageset/ServiceOn.png
new file mode 100644
index 0000000..5f424a3
Binary files /dev/null and b/phpmon/Assets.xcassets/ServiceOn.imageset/ServiceOn.png differ
diff --git a/phpmon/Assets.xcassets/ServiceOn.imageset/ServiceOn@2x.png b/phpmon/Assets.xcassets/ServiceOn.imageset/ServiceOn@2x.png
new file mode 100644
index 0000000..c40fe32
Binary files /dev/null and b/phpmon/Assets.xcassets/ServiceOn.imageset/ServiceOn@2x.png differ
diff --git a/phpmon/Assets.xcassets/ServiceOn.imageset/check-circle.svg b/phpmon/Assets.xcassets/ServiceOn.imageset/check-circle.svg
deleted file mode 100644
index 5673a79..0000000
--- a/phpmon/Assets.xcassets/ServiceOn.imageset/check-circle.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/phpmon/Assets.xcassets/StatusBarPHP.imageset/php@2x.png b/phpmon/Assets.xcassets/StatusBarPHP.imageset/php@2x.png
deleted file mode 100644
index 497b4a8..0000000
Binary files a/phpmon/Assets.xcassets/StatusBarPHP.imageset/php@2x.png and /dev/null differ
diff --git a/phpmon-common/Core/Actions.swift b/phpmon/Common/Core/Actions.swift
similarity index 72%
rename from phpmon-common/Core/Actions.swift
rename to phpmon/Common/Core/Actions.swift
index fc41ec7..23a677b 100644
--- a/phpmon-common/Core/Actions.swift
+++ b/phpmon/Common/Core/Actions.swift
@@ -34,17 +34,39 @@ class Actions {
brew("services stop dnsmasq", sudo: true)
}
- /**
- Kindly asks Valet to switch to a specific PHP version.
- */
- public static func switchToPhpVersionUsingValet(
- version: String,
- availableVersions: [String],
- completed: @escaping () -> Void
- ) {
- Log.info("Switching to \(version) using Valet")
- Log.info(valet("use php@\(version)"))
- completed()
+ public static func fixHomebrewPermissions() throws
+ {
+ var servicesCommands = [
+ "\(Paths.brew) services stop nginx",
+ "\(Paths.brew) services stop dnsmasq",
+ ]
+ var cellarCommands = [
+ "chown -R \(Paths.whoami):staff \(Paths.cellarPath)/nginx",
+ "chown -R \(Paths.whoami):staff \(Paths.cellarPath)/dnsmasq"
+ ]
+
+ PhpEnv.shared.availablePhpVersions.forEach { version in
+ let formula = version == PhpEnv.brewPhpVersion
+ ? "php"
+ : "php@\(version)"
+ servicesCommands.append("\(Paths.brew) services stop \(formula)")
+ cellarCommands.append("chown -R \(Paths.whoami):staff \(Paths.cellarPath)/\(formula)")
+ }
+
+ let script =
+ servicesCommands.joined(separator: " && ")
+ + " && "
+ + cellarCommands.joined(separator: " && ")
+
+ let appleScript = NSAppleScript(
+ source: "do shell script \"\(script)\" with administrator privileges"
+ )
+
+ let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
+
+ if (eventResult == nil) {
+ throw HomebrewPermissionError(kind: .applescriptNilError)
+ }
}
// MARK: - Finding Config Files
diff --git a/phpmon-common/Core/Command.swift b/phpmon/Common/Core/Command.swift
similarity index 100%
rename from phpmon-common/Core/Command.swift
rename to phpmon/Common/Core/Command.swift
diff --git a/phpmon-common/Core/Constants.swift b/phpmon/Common/Core/Constants.swift
similarity index 100%
rename from phpmon-common/Core/Constants.swift
rename to phpmon/Common/Core/Constants.swift
diff --git a/phpmon-common/Core/Events.swift b/phpmon/Common/Core/Events.swift
similarity index 100%
rename from phpmon-common/Core/Events.swift
rename to phpmon/Common/Core/Events.swift
diff --git a/phpmon-common/Core/Helpers.swift b/phpmon/Common/Core/Helpers.swift
similarity index 100%
rename from phpmon-common/Core/Helpers.swift
rename to phpmon/Common/Core/Helpers.swift
diff --git a/phpmon-common/Core/Logger.swift b/phpmon/Common/Core/Logger.swift
similarity index 93%
rename from phpmon-common/Core/Logger.swift
rename to phpmon/Common/Core/Logger.swift
index 87f2e81..8616528 100644
--- a/phpmon-common/Core/Logger.swift
+++ b/phpmon/Common/Core/Logger.swift
@@ -27,13 +27,13 @@ class Log {
static func err(_ item: Any) {
if Verbosity.error.isApplicable() {
- print(item)
+ print("[ERR] \(item)")
}
}
static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() {
- print(item)
+ print("[WRN] \(item)")
}
}
diff --git a/phpmon-common/Core/Paths.swift b/phpmon/Common/Core/Paths.swift
similarity index 87%
rename from phpmon-common/Core/Paths.swift
rename to phpmon/Common/Core/Paths.swift
index 9083041..a227cc3 100644
--- a/phpmon-common/Core/Paths.swift
+++ b/phpmon/Common/Core/Paths.swift
@@ -20,7 +20,7 @@ public class Paths {
private var userName : String
init() {
- baseDir = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr
+ baseDir = FileManager.default.fileExists(atPath: "\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
}
@@ -48,6 +48,10 @@ public class Paths {
return shared.userName
}
+ public static var cellarPath: String {
+ return "\(shared.baseDir.rawValue)/Cellar"
+ }
+
public static var binPath: String {
return "\(shared.baseDir.rawValue)/bin"
}
diff --git a/phpmon-common/Core/Shell.swift b/phpmon/Common/Core/Shell.swift
similarity index 95%
rename from phpmon-common/Core/Shell.swift
rename to phpmon/Common/Core/Shell.swift
index a1ae6c1..8ea3e2e 100644
--- a/phpmon-common/Core/Shell.swift
+++ b/phpmon/Common/Core/Shell.swift
@@ -105,12 +105,12 @@ public class Shell {
}
/**
- Checks if a file exists at the provided path.
- Uses `/bin/echo` instead of the `builtin` (which does not support `-n`).
+ 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 escapedPath = path.replacingOccurrences(of: " ", with: "\\ ")
- return Shell.pipe("if [ -f \(escapedPath) ]; then /bin/echo -n \"0\"; fi") == "0"
+ let fullPath = path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
+ return FileManager.default.fileExists(atPath: fullPath)
}
/**
diff --git a/phpmon/Common/Errors/AlertableError.swift b/phpmon/Common/Errors/AlertableError.swift
new file mode 100644
index 0000000..f32540b
--- /dev/null
+++ b/phpmon/Common/Errors/AlertableError.swift
@@ -0,0 +1,13 @@
+//
+// Errors.swift
+// PHP Monitor
+//
+// Created by Nico Verbruggen on 06/02/2022.
+// Copyright © 2022 Nico Verbruggen. All rights reserved.
+//
+
+import Foundation
+
+protocol AlertableError {
+ func getErrorMessageKey() -> String
+}
diff --git a/phpmon/Common/Errors/Errors.swift b/phpmon/Common/Errors/Errors.swift
new file mode 100644
index 0000000..1678814
--- /dev/null
+++ b/phpmon/Common/Errors/Errors.swift
@@ -0,0 +1,29 @@
+//
+// VersionParseError.swift
+// PHP Monitor
+//
+// Created by Nico Verbruggen on 08/02/2022.
+// Copyright © 2022 Nico Verbruggen. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Alertable Errors
+// These errors must be resolved by the user.
+
+struct HomebrewPermissionError: Error, AlertableError {
+ enum Kind: String {
+ case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
+ }
+
+ let kind: Kind
+
+ func getErrorMessageKey() -> String {
+ return "alert.errors.\(self.kind.rawValue)"
+ }
+}
+
+// MARK: - Errors that do not have an associated alert message
+// The errors must be resolved by the developer.
+
+struct VersionParseError: Error {}
diff --git a/phpmon/Domain/Extensions/DateExtension.swift b/phpmon/Common/Extensions/DateExtension.swift
similarity index 100%
rename from phpmon/Domain/Extensions/DateExtension.swift
rename to phpmon/Common/Extensions/DateExtension.swift
diff --git a/phpmon/Domain/Extensions/NSMenuExtension.swift b/phpmon/Common/Extensions/NSMenuExtension.swift
similarity index 100%
rename from phpmon/Domain/Extensions/NSMenuExtension.swift
rename to phpmon/Common/Extensions/NSMenuExtension.swift
diff --git a/phpmon/Domain/Extensions/StringExtension.swift b/phpmon/Common/Extensions/StringExtension.swift
similarity index 100%
rename from phpmon/Domain/Extensions/StringExtension.swift
rename to phpmon/Common/Extensions/StringExtension.swift
diff --git a/phpmon/Domain/Extensions/XibLoadable.swift b/phpmon/Common/Extensions/XibLoadable.swift
similarity index 100%
rename from phpmon/Domain/Extensions/XibLoadable.swift
rename to phpmon/Common/Extensions/XibLoadable.swift
diff --git a/phpmon/Domain/Helpers/Alert.swift b/phpmon/Common/Helpers/Alert.swift
similarity index 76%
rename from phpmon/Domain/Helpers/Alert.swift
rename to phpmon/Common/Helpers/Alert.swift
index 897e310..25327a7 100644
--- a/phpmon/Domain/Helpers/Alert.swift
+++ b/phpmon/Common/Helpers/Alert.swift
@@ -51,6 +51,9 @@ 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,
@@ -59,5 +62,20 @@ class Alert {
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
+ )
+ }
}
diff --git a/phpmon/Domain/Helpers/Application.swift b/phpmon/Common/Helpers/Application.swift
similarity index 100%
rename from phpmon/Domain/Helpers/Application.swift
rename to phpmon/Common/Helpers/Application.swift
diff --git a/phpmon/Domain/Helpers/Async.swift b/phpmon/Common/Helpers/Async.swift
similarity index 100%
rename from phpmon/Domain/Helpers/Async.swift
rename to phpmon/Common/Helpers/Async.swift
diff --git a/phpmon/Domain/Helpers/Filesystem.swift b/phpmon/Common/Helpers/Filesystem.swift
similarity index 100%
rename from phpmon/Domain/Helpers/Filesystem.swift
rename to phpmon/Common/Helpers/Filesystem.swift
diff --git a/phpmon/Domain/Helpers/LocalNotification.swift b/phpmon/Common/Helpers/LocalNotification.swift
similarity index 100%
rename from phpmon/Domain/Helpers/LocalNotification.swift
rename to phpmon/Common/Helpers/LocalNotification.swift
diff --git a/phpmon/Domain/Helpers/MenuBarImageGenerator.swift b/phpmon/Common/Helpers/MenuBarImageGenerator.swift
similarity index 58%
rename from phpmon/Domain/Helpers/MenuBarImageGenerator.swift
rename to phpmon/Common/Helpers/MenuBarImageGenerator.swift
index 21621a7..a68d15a 100644
--- a/phpmon/Domain/Helpers/MenuBarImageGenerator.swift
+++ b/phpmon/Common/Helpers/MenuBarImageGenerator.swift
@@ -42,7 +42,7 @@ class MenuBarImageGenerator {
let targetImage: NSImage = NSImage(size: image.size)
- let rep: NSBitmapImageRep = NSBitmapImageRep(
+ let representation: NSBitmapImageRep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(image.size.width),
pixelsHigh: Int(image.size.height),
@@ -55,7 +55,7 @@ class MenuBarImageGenerator {
bitsPerPixel: 0
)!
- targetImage.addRepresentation(rep)
+ targetImage.addRepresentation(representation)
targetImage.lockFocus()
image.draw(in: imageRect)
@@ -69,32 +69,69 @@ class MenuBarImageGenerator {
The same as before, but also attempts to add an icon to the left.
*/
public static func textToImageWithIcon(text: String) -> NSImage {
- let textImage = self.textToImage(text: text)
- let iconImage = NSImage(named: "StatusBarPHP")!
- let iconWidthSize = iconImage.size.width
- let divider = iconWidthSize
+ // We'll start out with the image containing the text
+ let textImage = self.textToImage(text: text)
+
+ // Then we'll fetch the image we want on the left
+ var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
+ if iconType == nil {
+ Log.warn("Invalid icon type found, using the default")
+ iconType = MenuBarIcon.iconPhp.rawValue
+ }
+
+ let iconImage = NSImage(named: "MenuBar_\(iconType!)")!
+
+ // We'll need to reference the width of the icon a bunch of times
+ let iconWidthSize = iconImage.size.width
+
+ // There will also be an additional divider between the image and the text (image)
+ let divider: CGFloat = 3
+
+ // Use a fixed size for the height of the menu bar (18pt)
let imageRect = CGRect(
x: 0,
y: 0,
- width: textImage.size.width + divider,
- height: textImage.size.height
+ width: textImage.size.width + iconWidthSize + divider,
+ height: 18
)
+ // Create a new image, we'll draw the text and our icon in there
let image: NSImage = NSImage(size: imageRect.size)
image.lockFocus()
+
+ // Calculate the offset between the image and the text
+ let offset = imageRect.size.width - textImage.size.width
- let difference = imageRect.size.width - textImage.size.width
+ // Draw the text with a negative x offset (so there is room on the left for the icon)
+ textImage.draw(
+ in: imageRect,
+ from: NSRect(
+ x: -offset,
+ y: 0,
+ width: textImage.size.width + offset,
+ height: textImage.size.height
+ ),
+ operation: .overlay,
+ fraction: 1
+ )
- textImage.draw(in: imageRect, from: NSRect(
- x: -difference,
- y: 0, width: textImage.size.width + difference,
- height: textImage.size.height
- ), operation: .overlay, fraction: 1)
-
- iconImage.draw(in: imageRect, from: NSRect(x: 0, y: 0, width: imageRect.size.width * 1.6, height: imageRect.size.height * 2.0), operation: .overlay, fraction: 1)
+ // Draw the icon directly in the left of the imageRect (where we left space)
+ iconImage.draw(
+ in: imageRect,
+ from: NSRect(
+ x: 0,
+ y: 0,
+ width: imageRect.size.width,
+ height: imageRect.size.height
+ ),
+ operation: .overlay,
+ fraction: 1
+ )
+ // We're done with this image
image.unlockFocus()
+
return image
}
diff --git a/phpmon/Domain/Helpers/PMWindowController.swift b/phpmon/Common/Helpers/PMWindowController.swift
similarity index 100%
rename from phpmon/Domain/Helpers/PMWindowController.swift
rename to phpmon/Common/Helpers/PMWindowController.swift
diff --git a/phpmon/Domain/Helpers/VersionExtractor.swift b/phpmon/Common/Helpers/VersionExtractor.swift
similarity index 92%
rename from phpmon/Domain/Helpers/VersionExtractor.swift
rename to phpmon/Common/Helpers/VersionExtractor.swift
index 69abe9b..7ac49de 100644
--- a/phpmon/Domain/Helpers/VersionExtractor.swift
+++ b/phpmon/Common/Helpers/VersionExtractor.swift
@@ -16,7 +16,7 @@ class VersionExtractor {
public static func from(_ string: String) -> String? {
do {
let regex = try NSRegularExpression(
- pattern: #"Laravel Valet (?(\d+)(.)(\d+)((.)(\d+))?)"#,
+ pattern: #"(?(\d+)(.)(\d+)((.)(\d+))?)"#,
options: []
)
diff --git a/phpmon-common/PHP/ActivePhpInstallation.swift b/phpmon/Common/PHP/ActivePhpInstallation.swift
similarity index 100%
rename from phpmon-common/PHP/ActivePhpInstallation.swift
rename to phpmon/Common/PHP/ActivePhpInstallation.swift
diff --git a/phpmon-common/PHP/Homebrew/HomebrewPackage.swift b/phpmon/Common/PHP/Homebrew/HomebrewPackage.swift
similarity index 100%
rename from phpmon-common/PHP/Homebrew/HomebrewPackage.swift
rename to phpmon/Common/PHP/Homebrew/HomebrewPackage.swift
diff --git a/phpmon-common/PHP/Homebrew/HomebrewService.swift b/phpmon/Common/PHP/Homebrew/HomebrewService.swift
similarity index 100%
rename from phpmon-common/PHP/Homebrew/HomebrewService.swift
rename to phpmon/Common/PHP/Homebrew/HomebrewService.swift
diff --git a/phpmon-common/PHP/PHP Version/PhpEnv.swift b/phpmon/Common/PHP/PHP Version/PhpEnv.swift
similarity index 96%
rename from phpmon-common/PHP/PHP Version/PhpEnv.swift
rename to phpmon/Common/PHP/PHP Version/PhpEnv.swift
index 3d6c159..04230e2 100644
--- a/phpmon-common/PHP/PHP Version/PhpEnv.swift
+++ b/phpmon/Common/PHP/PHP Version/PhpEnv.swift
@@ -8,11 +8,6 @@
import Foundation
-protocol PhpSwitcherDelegate: AnyObject {
- func switcherDidStartSwitching()
- func switcherDidCompleteSwitch()
-}
-
class PhpEnv {
// MARK: - Initializer
@@ -161,7 +156,7 @@ class PhpEnv {
*/
public func validate(_ version: String) -> Bool {
if self.currentInstall.version.short == version {
- print("Switching to version \(version) seems to have succeeded. Validation passed.")
+ Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
return true
}
diff --git a/phpmon-common/PHP/PHP Version/PhpVersionNumber.swift b/phpmon/Common/PHP/PHP Version/PhpVersionNumber.swift
similarity index 96%
rename from phpmon-common/PHP/PHP Version/PhpVersionNumber.swift
rename to phpmon/Common/PHP/PHP Version/PhpVersionNumber.swift
index 624e829..5f4c352 100644
--- a/phpmon-common/PHP/PHP Version/PhpVersionNumber.swift
+++ b/phpmon/Common/PHP/PHP Version/PhpVersionNumber.swift
@@ -118,6 +118,14 @@ public struct PhpVersionNumber: Equatable {
*/
}
+ public static func parse(_ text: String) throws -> Self {
+ guard let versionText = VersionExtractor.from(text) else {
+ throw VersionParseError()
+ }
+
+ return Self.make(from: versionText)!
+ }
+
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
diff --git a/phpmon-common/PHP/PhpExtension.swift b/phpmon/Common/PHP/PhpExtension.swift
similarity index 100%
rename from phpmon-common/PHP/PhpExtension.swift
rename to phpmon/Common/PHP/PhpExtension.swift
diff --git a/phpmon-common/PHP/PhpInstallation.swift b/phpmon/Common/PHP/PhpInstallation.swift
similarity index 73%
rename from phpmon-common/PHP/PhpInstallation.swift
rename to phpmon/Common/PHP/PhpInstallation.swift
index 4ecc010..f36fde1 100644
--- a/phpmon-common/PHP/PhpInstallation.swift
+++ b/phpmon/Common/PHP/PhpInstallation.swift
@@ -27,9 +27,10 @@ class PhpInstallation {
arguments: ["--version"]
).trimmingCharacters(in: .whitespacesAndNewlines)
- self.longVersion = PhpVersionNumber.make(
- from: String(longVersionString.split(separator: "-")[0])
- )!
+ // 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)
}
}
diff --git a/phpmon-common/PHP/Switcher/InternalSwitcher.swift b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift
similarity index 100%
rename from phpmon-common/PHP/Switcher/InternalSwitcher.swift
rename to phpmon/Common/PHP/Switcher/InternalSwitcher.swift
diff --git a/phpmon-common/PHP/Switcher/PhpSwitcher.swift b/phpmon/Common/PHP/Switcher/PhpSwitcher.swift
similarity index 65%
rename from phpmon-common/PHP/Switcher/PhpSwitcher.swift
rename to phpmon/Common/PHP/Switcher/PhpSwitcher.swift
index e167e15..324bcde 100644
--- a/phpmon-common/PHP/Switcher/PhpSwitcher.swift
+++ b/phpmon/Common/PHP/Switcher/PhpSwitcher.swift
@@ -8,6 +8,14 @@
import Foundation
+protocol PhpSwitcherDelegate: AnyObject {
+
+ func switcherDidStartSwitching(to: String)
+
+ func switcherDidCompleteSwitch(to: String)
+
+}
+
protocol PhpSwitcher {
func performSwitch(to version: String, completion: @escaping () -> Void)
diff --git a/phpmon/Domain/Core/App+ActivationPolicy.swift b/phpmon/Domain/App/App+ActivationPolicy.swift
similarity index 100%
rename from phpmon/Domain/Core/App+ActivationPolicy.swift
rename to phpmon/Domain/App/App+ActivationPolicy.swift
diff --git a/phpmon/Domain/Core/App+GlobalHotkey.swift b/phpmon/Domain/App/App+GlobalHotkey.swift
similarity index 100%
rename from phpmon/Domain/Core/App+GlobalHotkey.swift
rename to phpmon/Domain/App/App+GlobalHotkey.swift
diff --git a/phpmon/Domain/Core/App.swift b/phpmon/Domain/App/App.swift
similarity index 83%
rename from phpmon/Domain/Core/App.swift
rename to phpmon/Domain/App/App.swift
index c41fbd2..ae76d89 100644
--- a/phpmon/Domain/Core/App.swift
+++ b/phpmon/Domain/App/App.swift
@@ -8,7 +8,7 @@
import Cocoa
import HotKey
-class App: PhpSwitcherDelegate {
+class App {
// MARK: Static Vars
@@ -73,20 +73,4 @@ class App: PhpSwitcherDelegate {
The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder.
*/
var watcher: PhpConfigWatcher!
-
- // MARK: - PhpSwitcherDelegate
-
- func switcherDidStartSwitching() {
- }
-
- func switcherDidCompleteSwitch() {
- PhpEnv.shared.currentInstall = ActivePhpInstallation()
- handlePhpConfigWatcher()
-
- if let window = siteListWindowController {
- DispatchQueue.main.async {
- window.contentVC.reloadSites()
- }
- }
- }
}
diff --git a/phpmon/Domain/Core/AppDelegate+InterApp.swift b/phpmon/Domain/App/AppDelegate+InterApp.swift
similarity index 100%
rename from phpmon/Domain/Core/AppDelegate+InterApp.swift
rename to phpmon/Domain/App/AppDelegate+InterApp.swift
diff --git a/phpmon/Domain/Core/AppDelegate+MenuOutlets.swift b/phpmon/Domain/App/AppDelegate+MenuOutlets.swift
similarity index 100%
rename from phpmon/Domain/Core/AppDelegate+MenuOutlets.swift
rename to phpmon/Domain/App/AppDelegate+MenuOutlets.swift
diff --git a/phpmon/Domain/Core/AppDelegate+Notifications.swift b/phpmon/Domain/App/AppDelegate+Notifications.swift
similarity index 100%
rename from phpmon/Domain/Core/AppDelegate+Notifications.swift
rename to phpmon/Domain/App/AppDelegate+Notifications.swift
diff --git a/phpmon/Domain/Core/AppDelegate.swift b/phpmon/Domain/App/AppDelegate.swift
similarity index 88%
rename from phpmon/Domain/Core/AppDelegate.swift
rename to phpmon/Domain/App/AppDelegate.swift
index 3b50045..fbca312 100644
--- a/phpmon/Domain/Core/AppDelegate.swift
+++ b/phpmon/Domain/App/AppDelegate.swift
@@ -45,12 +45,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
let valet: Valet
/**
- The PhpSwitcher singleton that handles PHP version
+ The PhpEnv singleton that handles PHP version
detection, as well as switching. It is initialized
when the app is ready and passed all checks.
*/
- var switcher: PhpEnv! = nil
+ var phpEnvironment: PhpEnv! = nil
+ /**
+ The logger is responsible for different levels of logging.
+ You can tweak the verbosity in the `init` method here.
+ */
var logger = Log.shared
// MARK: - Initializer
@@ -59,7 +63,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
When the application initializes, create all singletons.
*/
override init() {
- logger.verbosity = .performance
+ logger.verbosity = .info
Log.info("==================================")
Log.info("PHP MONITOR by Nico Verbruggen")
Log.info("Version \(App.version)")
@@ -73,8 +77,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
}
func initializeSwitcher() {
- self.switcher = PhpEnv.shared
- self.switcher.delegate = self.state
+ self.phpEnvironment = PhpEnv.shared
}
// MARK: - Lifecycle
diff --git a/phpmon/Domain/Core/Base.lproj/Main.storyboard b/phpmon/Domain/App/Base.lproj/Main.storyboard
similarity index 99%
rename from phpmon/Domain/Core/Base.lproj/Main.storyboard
rename to phpmon/Domain/App/Base.lproj/Main.storyboard
index e65e6f7..bebb3da 100644
--- a/phpmon/Domain/Core/Base.lproj/Main.storyboard
+++ b/phpmon/Domain/App/Base.lproj/Main.storyboard
@@ -771,7 +771,7 @@ Gw
-
+
@@ -808,7 +808,7 @@ Gw
-
+
diff --git a/phpmon/Domain/Core/InterAppHandler.swift b/phpmon/Domain/App/InterAppHandler.swift
similarity index 100%
rename from phpmon/Domain/Core/InterAppHandler.swift
rename to phpmon/Domain/App/InterAppHandler.swift
diff --git a/phpmon/Domain/Core/Startup.swift b/phpmon/Domain/App/Startup.swift
similarity index 93%
rename from phpmon/Domain/Core/Startup.swift
rename to phpmon/Domain/App/Startup.swift
index 9fff546..5f4bbdf 100644
--- a/phpmon/Domain/Core/Startup.swift
+++ b/phpmon/Domain/App/Startup.swift
@@ -49,14 +49,6 @@ class Startup {
breaking: true
)
- Valet.shared.version = VersionExtractor.from(valet("--version"))
- performEnvironmentCheck(
- Valet.shared.version == nil,
- messageText: "startup.errors.valet_version_unknown.title".localized,
- informativeText: "startup.errors.valet_version_unknown.desc".localized,
- breaking: true
- )
-
performEnvironmentCheck(
HomebrewDiagnostics.cannotLoadService(),
messageText: "startup.errors.services_json_error.title".localized,
@@ -81,12 +73,13 @@ class Startup {
breaking: true
)
- let services = Shell.pipe("\(Paths.brew) services list | grep php")
+ // Determine the Valet version only AFTER confirming the correct permission is in place
+ Valet.shared.version = VersionExtractor.from(valet("--version"))
performEnvironmentCheck(
- (services.countInstances(of: "started") > 1),
- messageText: "startup.errors.services.title".localized,
- informativeText: "startup.errors.services.desc".localized,
- breaking: false
+ Valet.shared.version == nil,
+ messageText: "startup.errors.valet_version_unknown.title".localized,
+ informativeText: "startup.errors.valet_version_unknown.desc".localized,
+ breaking: true
)
if (!failed) {
diff --git a/phpmon/Domain/Helpers/Timer.swift b/phpmon/Domain/Helpers/Timer.swift
deleted file mode 100644
index f3b2ce3..0000000
--- a/phpmon/Domain/Helpers/Timer.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-//
-// BenchmarkTimer.swift
-// PHP Monitor
-//
-// Created by Nico Verbruggen on 10/12/2021.
-// Copyright © 2021 Nico Verbruggen. All rights reserved.
-//
-
-import Foundation
-
-class BenchmarkTimer {
- let startTime: CFAbsoluteTime
- var endTime: CFAbsoluteTime?
-
- init() {
- startTime = CFAbsoluteTimeGetCurrent()
- }
-
- func stop() -> CFAbsoluteTime {
- endTime = CFAbsoluteTimeGetCurrent()
-
- return duration!
- }
-
- var duration: CFAbsoluteTime? {
- if let endTime = endTime {
- return endTime - startTime
- } else {
- return nil
- }
- }
-}
diff --git a/phpmon/Domain/Integrations/Valet/Valet.swift b/phpmon/Domain/Integrations/Valet/Valet.swift
index 8001ef9..3ab6390 100644
--- a/phpmon/Domain/Integrations/Valet/Valet.swift
+++ b/phpmon/Domain/Integrations/Valet/Valet.swift
@@ -163,7 +163,7 @@ class Valet {
// We should also check that we can interpret the path correctly
if URL(fileURLWithPath: siteDir).lastPathComponent == "" {
- print("Warning: could not parse the site: \(siteDir), skipping!")
+ Log.warn("Could not parse the site: \(siteDir), skipping!")
return
}
diff --git a/phpmon/Domain/Menu/HeaderView.xib b/phpmon/Domain/Menu/HeaderView.xib
index c765e7d..b0dbbe9 100644
--- a/phpmon/Domain/Menu/HeaderView.xib
+++ b/phpmon/Domain/Menu/HeaderView.xib
@@ -1,8 +1,8 @@
-
+
-
+
@@ -10,7 +10,7 @@
-
+
@@ -29,7 +29,7 @@
-
+
diff --git a/phpmon/Domain/Menu/MainMenu+Async.swift b/phpmon/Domain/Menu/MainMenu+Async.swift
new file mode 100644
index 0000000..a3f2094
--- /dev/null
+++ b/phpmon/Domain/Menu/MainMenu+Async.swift
@@ -0,0 +1,96 @@
+//
+// MainMenu+Async.swift
+// PHP Monitor
+//
+// Created by Nico Verbruggen on 06/02/2022.
+// Copyright © 2022 Nico Verbruggen. All rights reserved.
+//
+
+import Foundation
+
+extension MainMenu {
+
+ // MARK: - Nicer callbacks
+
+ enum AsyncBehaviour {
+ case setsBusyUI
+ case reloadsPhpInstallation
+ case updatesMenuBarContents
+ case broadcastServicesUpdate
+ }
+
+ /**
+ Attempts asynchronous execution of a callback that may throw an `Error`.
+ While the callback is being executed, the UI will be marked as busy.
+
+ (Preferably, if an `Error` is thrown, it should also be an `AlertableError`,
+ which will make presenting errors easier.)
+
+ - Parameter execute: Required callback of the work that needs to happen.
+
+ - Parameter success: Optional callback that is fired when all was OK.
+ - Parameter failure: Optional callback that is fired when an `Error` was thrown.
+ - Parameter behaviours: Various behaviours that can be tweaked, but usually best left to the default.
+ The default will set the UI to busy, reload PHP info, update the menu bar,
+ and broadcast to the services view that the list has been updated.
+ */
+ func asyncExecution(
+ _ execute: @escaping () throws -> Void,
+ success: @escaping () -> Void = {},
+ failure: @escaping (Error) -> Void = { _ in },
+ behaviours: [AsyncBehaviour] = [
+ .setsBusyUI,
+ .reloadsPhpInstallation,
+ .updatesMenuBarContents,
+ .broadcastServicesUpdate
+ ]
+ ) {
+ if behaviours.contains(.reloadsPhpInstallation) {
+ PhpEnv.shared.isBusy = true
+ }
+
+ if behaviours.contains(.setsBusyUI) {
+ setBusyImage()
+ }
+
+ DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
+ var error: Error? = nil
+
+ do { try execute() } catch let e { error = e }
+
+ if behaviours.contains(.setsBusyUI) {
+ PhpEnv.shared.isBusy = false
+ }
+
+ DispatchQueue.main.async { [self] in
+ if behaviours.contains(.reloadsPhpInstallation) {
+ PhpEnv.shared.currentInstall = ActivePhpInstallation()
+ }
+
+ if behaviours.contains(.updatesMenuBarContents) {
+ updatePhpVersionInStatusBar()
+ } else if behaviours.contains(.setsBusyUI) {
+ refreshIcon()
+ }
+
+ if behaviours.contains(.broadcastServicesUpdate) {
+ NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
+ }
+
+ error == nil ? success() : failure(error!)
+ }
+ }
+ }
+
+ func asyncWithBusyUI(
+ _ execute: @escaping () throws -> Void,
+ completion: @escaping () -> Void = {}
+ ) {
+ asyncExecution({
+ try! execute()
+ }, success: {
+ completion()
+ }, behaviours: [.setsBusyUI])
+ }
+
+}
diff --git a/phpmon/Domain/Menu/MainMenu+Composer.swift b/phpmon/Domain/Menu/MainMenu+Composer.swift
new file mode 100644
index 0000000..c24f73c
--- /dev/null
+++ b/phpmon/Domain/Menu/MainMenu+Composer.swift
@@ -0,0 +1,98 @@
+//
+// 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)
+ }
+ }
+ }
+ }
+}
diff --git a/phpmon/Domain/Menu/MainMenu+Switcher.swift b/phpmon/Domain/Menu/MainMenu+Switcher.swift
new file mode 100644
index 0000000..85f85b9
--- /dev/null
+++ b/phpmon/Domain/Menu/MainMenu+Switcher.swift
@@ -0,0 +1,84 @@
+//
+// MainMenu+PhpSwitcherDelegate.swift
+// PHP Monitor
+//
+// Created by Nico Verbruggen on 08/02/2022.
+// Copyright © 2022 Nico Verbruggen. All rights reserved.
+//
+
+import Foundation
+
+extension MainMenu {
+
+ // MARK: - PhpSwitcherDelegate
+
+ func switcherDidStartSwitching(to version: String) {}
+
+ func switcherDidCompleteSwitch(to version: String) {
+ // Update the PHP version
+ PhpEnv.shared.currentInstall = ActivePhpInstallation()
+
+ // Ensure the config watcher gets reloaded
+ App.shared.handlePhpConfigWatcher()
+
+ // Mark as no longer busy
+ PhpEnv.shared.isBusy = false
+
+ // Reload the site list
+ self.reloadSiteListData()
+
+ // Perform UI updates on main thread
+ DispatchQueue.main.async { [self] in
+ updatePhpVersionInStatusBar()
+ rebuild()
+
+ if !PhpEnv.shared.validate(version) {
+ self.suggestFixMyValet(failed: version)
+ return
+ }
+
+ // Run composer updates
+ if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
+ self.updateGlobalDependencies(notify: false, completion: { _ in
+ self.notifyAboutVersionChange(to: version)
+ })
+ } else {
+ self.notifyAboutVersionChange(to: version)
+ }
+
+ // Update stats
+ Stats.incrementSuccessfulSwitchCount()
+ Stats.evaluateSponsorMessageShouldBeDisplayed()
+ }
+ }
+
+ 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)
+ if outcome {
+ MainMenu.shared.fixMyValet()
+ }
+ }
+
+ private func reloadSiteListData() {
+ if let window = App.shared.siteListWindowController {
+ DispatchQueue.main.async {
+ window.contentVC.reloadSites()
+ }
+ } else {
+ Valet.shared.reloadSites()
+ }
+ }
+
+ private func notifyAboutVersionChange(to version: String) {
+ LocalNotification.send(
+ title: String(format: "notification.version_changed_title".localized, version),
+ subtitle: String(format: "notification.version_changed_desc".localized, version)
+ )
+
+ PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
+ }
+}
diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift
index 7f09176..55ec3c1 100644
--- a/phpmon/Domain/Menu/MainMenu.swift
+++ b/phpmon/Domain/Menu/MainMenu.swift
@@ -7,7 +7,7 @@
import Cocoa
-class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
+class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
static let shared = MainMenu()
@@ -23,52 +23,67 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
// MARK: - UI related
/**
- Update the menu's contents, based on what's going on.
- This will rebuild the entire menu, so this can take a few moments.
+ Rebuilds the menu (either asynchronously or synchronously).
+ Defaults to rebuilding the menu asynchronously.
*/
- func rebuild() {
+ func rebuild(async: Bool = true) {
+ if !async {
+ self.rebuildMenu()
+ return
+ }
+
// Update the menu item on the main thread
DispatchQueue.main.async { [self] in
- // Create a new menu
- let menu = StatusMenu()
-
- // Add the PHP versions (or error messages)
- menu.addPhpVersionMenuItems()
- menu.addItem(NSMenuItem.separator())
-
- // Add the possible actions
- menu.addPhpActionMenuItems()
- menu.addItem(NSMenuItem.separator())
-
- // Add Valet interactions
- menu.addValetMenuItems()
- menu.addItem(NSMenuItem.separator())
-
- // Add services
- menu.addPhpConfigurationMenuItems()
- menu.addItem(NSMenuItem.separator())
-
- // Add about & quit menu items
- menu.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(openPrefs), keyEquivalent: ","))
- menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(openAbout), keyEquivalent: ""))
- menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(terminateApp), keyEquivalent: "q"))
-
- // Make sure every item can be interacted with
- menu.items.forEach({ (item) in
- item.target = self
- })
-
- statusItem.menu = menu
- statusItem.menu?.delegate = self
+ self.rebuildMenu()
}
}
+ /**
+ Update the menu's contents, based on what's going on.
+ This will rebuild the entire menu, so this can take a few moments.
+
+ Use `rebuild(async:)` to ensure the rebuilding happens in the background.
+ */
+ private func rebuildMenu() {
+ // Create a new menu
+ let menu = StatusMenu()
+
+ // Add the PHP versions (or error messages)
+ menu.addPhpVersionMenuItems()
+ menu.addItem(NSMenuItem.separator())
+
+ // Add the possible actions
+ menu.addPhpActionMenuItems()
+ menu.addItem(NSMenuItem.separator())
+
+ // Add Valet interactions
+ menu.addValetMenuItems()
+ menu.addItem(NSMenuItem.separator())
+
+ // Add services
+ menu.addPhpConfigurationMenuItems()
+ menu.addItem(NSMenuItem.separator())
+
+ // Add about & quit menu items
+ menu.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(openPrefs), keyEquivalent: ","))
+ menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(openAbout), keyEquivalent: ""))
+ menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(terminateApp), keyEquivalent: "q"))
+
+ // Make sure every item can be interacted with
+ menu.items.forEach({ (item) in
+ item.target = self
+ })
+
+ statusItem.menu = menu
+ statusItem.menu?.delegate = self
+ }
+
/**
Sets the status bar image based on a version string.
*/
func setStatusBarImage(version: String) {
setStatusBar(
- image: Preferences.isEnabled(.shouldDisplayPhpHintInIcon)
+ image: (Preferences.preferences[.iconTypeToDisplay] as! String != MenuBarIcon.noIcon.rawValue)
? MenuBarImageGenerator.textToImageWithIcon(text: version)
: MenuBarImageGenerator.textToImage(text: version)
)
@@ -85,35 +100,9 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
}
}
- // MARK: - Nicer callbacks
-
- /**
- Executes a specific callback and fires the completion callback,
- while updating the UI as required. As long as the completion callback
- does not fire, the app is presumed to be busy and the UI reflects this.
-
- - Parameter execute: Callback of the work that needs to happen.
- - Parameter completion: Callback that is fired when the work is done.
- */
- private func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
- {
- PhpEnv.shared.isBusy = true
- setBusyImage()
- DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
- execute()
- PhpEnv.shared.isBusy = false
-
- DispatchQueue.main.async { [self] in
- PhpEnv.shared.currentInstall = ActivePhpInstallation()
- updatePhpVersionInStatusBar()
- NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
- completion()
- }
- }
- }
-
// MARK: - User Interface
+ /** Reloads which PHP versions is currently active. */
@objc func refreshActiveInstallation() {
if !PhpEnv.shared.isBusy {
PhpEnv.shared.currentInstall = ActivePhpInstallation()
@@ -123,12 +112,38 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
}
}
+ /** Updates the icon (refresh icon) and rebuilds the menu. */
@objc func updatePhpVersionInStatusBar() {
refreshIcon()
rebuild()
}
- func refreshIcon() {
+ /**
+ Reloads the menu in the foreground.
+ This mimics the exact behaviours of `asyncExecution` as set in the method below.
+ */
+ @objc func reloadPhpMonitorMenuInForeground() {
+ refreshActiveInstallation()
+ refreshIcon()
+ rebuild(async: false)
+ NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil)
+ }
+
+ /** Reloads the menu in the background, using `asyncExecution`. */
+ @objc func reloadPhpMonitorMenuInBackground() {
+ asyncExecution({
+ // This automatically reloads the menu
+ Log.info("Reloading information about the PHP installation (in the background)...")
+ }, behaviours: [
+ .setsBusyUI,
+ .reloadsPhpInstallation,
+ .broadcastServicesUpdate,
+ .updatesMenuBarContents
+ ])
+ }
+
+ /** Refreshes the icon with the PHP version. */
+ @objc func refreshIcon() {
DispatchQueue.main.async { [self] in
if (App.busy) {
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
@@ -145,20 +160,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
}
}
- @objc func reloadPhpMonitorMenuInBackground() {
- waitAndExecute {
- // This automatically reloads the menu
- Log.info("Reloading information about the PHP installation (in the background)...")
- }
- }
-
- @objc func reloadPhpMonitorMenu() {
- waitAndExecute {
- // This automatically reloads the menu
- Log.info("Reloading information about the PHP installation...")
- }
- }
-
+ /** Updates the icon to be displayed as busy. */
@objc func setBusyImage() {
DispatchQueue.main.async { [self] in
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
@@ -167,18 +169,42 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
// 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
+ ) {
+ 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
+ )
+ } failure: { error in
+ Alert.notify(about: error as! HomebrewPermissionError)
+ }
+ }
+
@objc func restartPhpFpm() {
- waitAndExecute {
+ asyncExecution {
Actions.restartPhpFpm()
}
}
@objc func restartAllServices() {
- waitAndExecute {
+ asyncExecution {
Actions.restartDnsMasq()
Actions.restartPhpFpm()
Actions.restartNginx()
- } completion: {
+ } success: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_restarted".localized,
@@ -189,9 +215,9 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
}
@objc func stopAllServices() {
- waitAndExecute {
+ asyncExecution {
Actions.stopAllServices()
- } completion: {
+ } success: {
DispatchQueue.main.async {
LocalNotification.send(
title: "notification.services_stopped".localized,
@@ -202,19 +228,19 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
}
@objc func restartNginx() {
- waitAndExecute {
+ asyncExecution {
Actions.restartNginx()
}
}
@objc func restartDnsMasq() {
- waitAndExecute {
+ asyncExecution {
Actions.restartDnsMasq()
}
}
@objc func toggleExtension(sender: ExtensionMenuItem) {
- waitAndExecute {
+ asyncExecution {
sender.phpExtension?.toggle()
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
@@ -226,34 +252,32 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
@objc func openPhpInfo() {
var url: URL? = nil
- waitAndExecute {
+ asyncWithBusyUI {
url = Actions.createTempPhpInfoFile()
} completion: {
- // When this has been completed, open the URL to the file in the browser
- NSWorkspace.shared.open(url!)
+ if url != nil { NSWorkspace.shared.open(url!) }
}
}
@objc func fixMyValet() {
- // Tell the user the switch is about to occur
- if Alert.present(
+ if !Alert.present(
messageText: "alert.fix_my_valet.title".localized,
informativeText: "alert.fix_my_valet.info".localized(PhpEnv.brewPhpVersion),
buttonTitle: "alert.fix_my_valet.ok".localized,
secondButtonTitle: "alert.fix_my_valet.cancel".localized,
style: .warning
) {
- // Start the fix
- waitAndExecute {
- Actions.fixMyValet()
- } completion: {
- Alert.notify(
- message: "alert.fix_my_valet_done.title".localized,
- info: "alert.fix_my_valet_done.info".localized
- )
- }
- } else {
Log.info("The user has chosen to abort Fix My Valet")
+ return
+ }
+
+ asyncExecution {
+ Actions.fixMyValet()
+ } success: {
+ Alert.notify(
+ message: "alert.fix_my_valet_done.title".localized,
+ info: "alert.fix_my_valet_done.info".localized
+ )
}
}
@@ -284,70 +308,26 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
self.switchToPhpVersion(sender.version)
}
- // TODO (5.1): Investigate if `waitAndExecute` cannot be used here
@objc func switchToPhpVersion(_ version: String) {
setBusyImage()
PhpEnv.shared.isBusy = true
+ PhpEnv.shared.delegate = self
+ PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version)
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
- // Update the PHP version in the status bar
updatePhpVersionInStatusBar()
-
- // Update the menu
rebuild()
-
- let sendLocalNotification = {
- LocalNotification.send(
- title: String(format: "notification.version_changed_title".localized, version),
- subtitle: String(format: "notification.version_changed_desc".localized, version)
- )
- PhpEnv.phpInstall.notifyAboutBrokenPhpFpm()
- }
-
- let completion = {
- // Fire off the delegate method
- PhpEnv.shared.delegate?.switcherDidCompleteSwitch()
-
- // Mark as no longer busy
- PhpEnv.shared.isBusy = false
-
- // Perform UI updates on main thread
- DispatchQueue.main.async { [self] in
- updatePhpVersionInStatusBar()
- rebuild()
-
- if !PhpEnv.shared.validate(version) {
- let outcome = Alert.present(
- messageText: "alert.php_switch_failed.title".localized(version),
- informativeText: "alert.php_switch_failed.info".localized(version),
- buttonTitle: "alert.php_switch_failed.confirm".localized,
- secondButtonTitle: "alert.php_switch_failed.cancel".localized, style: .informational)
- if outcome {
- MainMenu.shared.fixMyValet()
- }
- return
- }
-
- // Run composer updates
- if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
- self.updateGlobalDependencies(notify: false, completion: { _ in sendLocalNotification() })
- } else {
- sendLocalNotification()
- }
-
- // Update stats
- Stats.incrementSuccessfulSwitchCount()
- Stats.evaluateSponsorMessageShouldBeDisplayed()
- }
- }
-
PhpEnv.switcher.performSwitch(
to: version,
- completion: completion
+ completion: {
+ PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version)
+ }
)
}
}
+ // MARK: - Menu Item Functionality
+
@objc func openAbout() {
NSApplication.shared.activate(ignoringOtherApps: true)
NSApplication.shared.orderFrontStandardAboutPanel()
@@ -380,91 +360,4 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
// When the menu is closed, allow the shortcut to work again
App.shared.shortcutHotkey?.isPaused = false
}
-
- // MARK: - Private Methods
-
- /**
- Updates the global dependencies and runs the completion callback when done.
- */
- private func updateGlobalDependencies(notify: Bool, completion: @escaping (Bool) -> Void) {
- if !Shell.fileExists("/usr/local/bin/composer") {
- Alert.notify(
- message: "alert.composer_missing.title".localized,
- info: "alert.composer_missing.info".localized
- )
- return
- }
-
- PhpEnv.shared.isBusy = true
- setBusyImage()
- self.rebuild()
-
- let noLongerBusy = {
- PhpEnv.shared.isBusy = false
- DispatchQueue.main.async { [self] in
- self.updatePhpVersionInStatusBar()
- self.rebuild()
- }
- }
-
- var window: ProgressWindowController? = ProgressWindowController.display(
- title: "alert.composer_progress.title".localized,
- description: "alert.composer_progress.info".localized
- )
- window?.setType(info: true)
-
- DispatchQueue.global(qos: .userInitiated).async {
- let task = Shell.user.createTask(
- for: "/usr/local/bin/composer global update", requiresPath: true
- )
-
- DispatchQueue.main.async {
- window?.addToConsole("/usr/local/bin/composer global update\n")
- }
-
- Shell.captureOutput(
- task,
- didReceiveStdOutData: { string in
- DispatchQueue.main.async {
- window?.addToConsole(string)
- }
- Log.perf("\(string.trimmingCharacters(in: .newlines))")
- },
- didReceiveStdErrData: { string in
- DispatchQueue.main.async {
- window?.addToConsole(string)
- }
- Log.perf("\(string.trimmingCharacters(in: .newlines))")
- }
- )
-
- task.launch()
- task.waitUntilExit()
- Shell.haltCapturingOutput(task)
-
- DispatchQueue.main.async {
- if task.terminationStatus <= 0 {
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
- window?.close()
- if (notify) {
- LocalNotification.send(
- title: "alert.composer_success.title".localized,
- subtitle: "alert.composer_success.info".localized
- )
- }
- window = nil
- noLongerBusy()
- completion(true)
- }
- } else {
- window?.setType(info: false)
- window?.progressView?.labelTitle.stringValue = "alert.composer_failure.title".localized
- window?.progressView?.labelDescription.stringValue = "alert.composer_failure.info".localized
- window = nil
- noLongerBusy()
- completion(false)
- }
- }
- }
- }
}
diff --git a/phpmon/Domain/Menu/ServicesView.xib b/phpmon/Domain/Menu/ServicesView.xib
index c1371ff..964d4b2 100644
--- a/phpmon/Domain/Menu/ServicesView.xib
+++ b/phpmon/Domain/Menu/ServicesView.xib
@@ -9,18 +9,18 @@
-
-
+
+
-
+
-
+
-
+
@@ -28,7 +28,7 @@
-
+
@@ -47,10 +47,10 @@
-
+
-
+
@@ -58,7 +58,7 @@
-
+
@@ -77,10 +77,10 @@
-
+
-
+
@@ -88,7 +88,7 @@
-
+
@@ -145,6 +145,6 @@
-
+
diff --git a/phpmon/Domain/Menu/StatsView.xib b/phpmon/Domain/Menu/StatsView.xib
index fd6bca0..0f83537 100644
--- a/phpmon/Domain/Menu/StatsView.xib
+++ b/phpmon/Domain/Menu/StatsView.xib
@@ -10,11 +10,11 @@
-
+
-
+
@@ -46,10 +46,10 @@
-
+
-
+
@@ -57,7 +57,7 @@
-
+
@@ -75,7 +75,7 @@
-
+
diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift
index 56ae048..880e293 100644
--- a/phpmon/Domain/Menu/StatusMenu.swift
+++ b/phpmon/Domain/Menu/StatusMenu.swift
@@ -8,6 +8,7 @@
import Cocoa
class StatusMenu : NSMenu {
+
func addPhpVersionMenuItems() {
if PhpEnv.phpInstall.version.error {
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
@@ -37,47 +38,6 @@ class StatusMenu : NSMenu {
self.addItem(NSMenuItem.separator())
}
- func addOtherMenuItems() {
- let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
- let servicesMenu = NSMenu()
-
- servicesMenu.addItem(NSMenuItem(title: "mi_help".localized, action: nil, keyEquivalent: ""))
-
- if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpVersion) {
- servicesMenu.addItem(NSMenuItem(
- title: "mi_fix_my_valet_unavailable".localized(PhpEnv.brewPhpVersion),
- action: nil, keyEquivalent: "f"
- ))
- } else {
- servicesMenu.addItem(NSMenuItem(
- title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
- action: #selector(MainMenu.fixMyValet), keyEquivalent: "f"))
- }
-
- servicesMenu.addItem(NSMenuItem(title: "mi_services".localized, action: nil, keyEquivalent: ""))
-
- servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
- servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
- servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
-
- servicesMenu.addItem(
- NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
- withKeyModifier: [.command, .shift])
-
- servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
-
- servicesMenu.addItem(NSMenuItem(title: "mi_manual_actions".localized, action: nil, keyEquivalent: ""))
-
- servicesMenu.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r"))
-
- for item in servicesMenu.items {
- item.target = MainMenu.shared
- }
-
- self.setSubmenu(servicesMenu, for: services)
- self.addItem(services)
- }
-
func addValetMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_valet".localized))
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
@@ -131,9 +91,59 @@ class StatusMenu : NSMenu {
// Other
self.addItem(NSMenuItem.separator())
- self.addOtherMenuItems()
+ self.addFirstAidAndServicesMenuItems()
}
+ func addFirstAidAndServicesMenuItems() {
+ let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "")
+ let servicesMenu = NSMenu()
+ servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_first_aid".localized))
+
+ 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))
+
+ servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
+ servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
+ servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
+ servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
+ servicesMenu.addItem(
+ NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
+ withKeyModifier: [.command, .shift])
+
+ servicesMenu.addItem(NSMenuItem.separator())
+ servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized))
+
+ servicesMenu.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r"))
+
+ for item in servicesMenu.items {
+ item.target = MainMenu.shared
+ }
+
+ self.setSubmenu(servicesMenu, for: services)
+ self.addItem(services)
+ }
+
+ // PRIVATE HELPERS
+
private func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..
-
+
diff --git a/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift b/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift
new file mode 100644
index 0000000..33afae0
--- /dev/null
+++ b/phpmon/Domain/Preferences/Views/SelectPreferenceView.swift
@@ -0,0 +1,82 @@
+//
+// SelectPreferenceView.swift
+// PHP Monitor
+//
+// Created by Nico Verbruggen on 06/02/2022.
+// Copyright © 2022 Nico Verbruggen. All rights reserved.
+//
+
+import Foundation
+import Cocoa
+
+class SelectPreferenceView: NSView, XibLoadable {
+
+ @IBOutlet weak var labelSection: NSTextField!
+ @IBOutlet weak var labelDescription: NSTextField!
+ @IBOutlet weak var popupButton: NSPopUpButton!
+
+ var localizationPrefix: String = ""
+ var imagePrefix: String? = nil
+
+ var options: [String] = [] {
+ didSet {
+ self.popupButton.removeAllItems()
+ self.options.forEach { value in
+ self.popupButton.addItem(
+ withTitle: "\(localizationPrefix).\(value)".localized
+ )
+ }
+
+ if imagePrefix == nil {
+ return
+ }
+
+ self.popupButton.itemArray.enumerated().forEach { item in
+ item.element.image = NSImage(named: "\(imagePrefix!)_\(self.options[item.offset])")
+ }
+ }
+ }
+
+ var action: (() -> Void)!
+
+ var preference: PreferenceName! {
+ didSet {
+ let value = Preferences.preferences[preference] as! String
+ self.options.enumerated().forEach { option in
+ if option.element == value {
+ self.popupButton.selectItem(at: option.offset)
+ }
+ }
+ }
+ }
+
+ static func make(
+ sectionText: String,
+ descriptionText: String,
+ options: [String],
+ localizationPrefix: String,
+ imagePrefix: String? = nil,
+ preference: PreferenceName,
+ action: @escaping () -> Void) -> NSView
+ {
+ let view = Self.createFromXib()!
+
+ view.labelSection.stringValue = sectionText
+ view.labelDescription.stringValue = descriptionText
+
+ view.localizationPrefix = localizationPrefix
+ view.imagePrefix = imagePrefix
+ view.options = options
+ view.preference = preference
+ view.action = action
+
+ return view
+ }
+
+ @IBAction func valueChanged(_ sender: Any) {
+ let index = self.popupButton.indexOfSelectedItem
+ Preferences.update(.iconTypeToDisplay, value: self.options[index])
+ self.action()
+ }
+
+}
diff --git a/phpmon/Domain/Preferences/Views/SelectPreferenceView.xib b/phpmon/Domain/Preferences/Views/SelectPreferenceView.xib
new file mode 100644
index 0000000..0e5dbbc
--- /dev/null
+++ b/phpmon/Domain/Preferences/Views/SelectPreferenceView.xib
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpmon/Domain/SiteList/SiteListCell.swift b/phpmon/Domain/SiteList/SiteListCell.swift
index bf95985..8a24f50 100644
--- a/phpmon/Domain/SiteList/SiteListCell.swift
+++ b/phpmon/Domain/SiteList/SiteListCell.swift
@@ -47,7 +47,6 @@ class SiteListCell: NSTableCellView
imageViewType.contentTintColor = NSColor.tertiaryLabelColor
// Show the green or red lock based on whether the site was secured
- // imageViewLock.image = NSImage(named: site.secured ? "Lock" : "LockUnlocked")
imageViewLock.contentTintColor = site.secured ?
NSColor(named: "IconColorGreen") // green
: NSColor(named: "IconColorRed")
@@ -103,7 +102,7 @@ class SiteListCell: NSTableCellView
if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue {
if map.keys.contains(response.rawValue) {
let version = map[response.rawValue]!
- print("Pressed button to switch to \(version)")
+ Log.info("Pressed button to switch to \(version)")
MainMenu.shared.switchToPhpVersion(version)
}
}
diff --git a/phpmon/Domain/SiteList/SiteListVC.swift b/phpmon/Domain/SiteList/SiteListVC.swift
index b441153..642cdbd 100644
--- a/phpmon/Domain/SiteList/SiteListVC.swift
+++ b/phpmon/Domain/SiteList/SiteListVC.swift
@@ -39,6 +39,8 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
return sites[tableView.selectedRow]
}
+ var timer: Timer? = nil
+
// MARK: - Display
public static func create(delegate: NSWindowDelegate?) {
@@ -90,7 +92,11 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
Also shows a spinner to indicate that we're busy.
*/
private func setUIBusy() {
- progressIndicator.startAnimation(nil)
+ // If it takes more than 0.5s to set the UI to not busy, show a spinner
+ timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in
+ self.progressIndicator.startAnimation(true)
+ })
+
tableView.alphaValue = 0.3
tableView.isEnabled = false
tableView.selectRowIndexes([], byExtendingSelection: true)
@@ -100,6 +106,7 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
Re-enables the UI so the user can interact with it.
*/
private func setUINotBusy() {
+ timer?.invalidate()
progressIndicator.stopAnimation(nil)
tableView.alphaValue = 1.0
tableView.isEnabled = true
@@ -118,7 +125,9 @@ class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
setUIBusy()
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
execute()
- DispatchQueue.main.async { [self] in
+
+ // For a smoother animation, expect at least a 0.2 second delay
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
completion()
setUINotBusy()
}
diff --git a/phpmon/Domain/Watcher/App+ConfigWatch.swift b/phpmon/Domain/Watcher/App+ConfigWatch.swift
index 82a60e1..fa1af7b 100644
--- a/phpmon/Domain/Watcher/App+ConfigWatch.swift
+++ b/phpmon/Domain/Watcher/App+ConfigWatch.swift
@@ -30,17 +30,22 @@ extension App {
func handlePhpConfigWatcher(forceReload: Bool = false) {
let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(PhpEnv.phpInstall.version.short)")
- // Watcher needs to be created
- if self.watcher == nil {
- startWatcher(url)
- }
-
- // Watcher needs to be updated
- if self.watcher.url != url || forceReload {
- self.watcher.disable()
- self.watcher = nil
- Log.info("Watcher has stopped watching files. Starting new one...")
- startWatcher(url)
+ // Check whether the watcher exists and schedule on the main thread
+ // if we don't consistently do this, the app will create duplicate watchers
+ // due to timing issues, which creates retain cycles.
+ DispatchQueue.main.async {
+ // Watcher needs to be created
+ if self.watcher == nil {
+ self.startWatcher(url)
+ }
+
+ // Watcher needs to be updated
+ if self.watcher.url != url || forceReload {
+ self.watcher.disable()
+ self.watcher = nil
+ Log.info("Watcher has stopped watching files. Starting new one...")
+ self.startWatcher(url)
+ }
}
}
diff --git a/phpmon/Domain/Watcher/PhpConfigWatcher.swift b/phpmon/Domain/Watcher/PhpConfigWatcher.swift
index e97cfe5..f03fa04 100644
--- a/phpmon/Domain/Watcher/PhpConfigWatcher.swift
+++ b/phpmon/Domain/Watcher/PhpConfigWatcher.swift
@@ -40,19 +40,24 @@ class PhpConfigWatcher {
self.addWatcher(for: self.url.appendingPathComponent("conf.d/\(file)"), eventMask: .write)
}
- Log.info("A watcher exists for the following config paths:")
- Log.info(self.watchers.map({ watcher in
+ Log.perf("A watcher exists for the following config paths:")
+ Log.perf(self.watchers.map({ watcher in
return watcher.url.relativePath
}))
}
func addWatcher(for url: URL, eventMask: DispatchSource.FileSystemEvent, behaviour: FSWatcherBehaviour = .reloadsMenu) {
+ if !Filesystem.fileExists(url.path) {
+ Log.warn("No watcher was created for \(url.path) because the requested file does not exist.")
+ return
+ }
+
let watcher = FSWatcher(for: url, eventMask: eventMask, parent: self, behaviour: behaviour)
self.watchers.append(watcher)
}
func disable() {
- Log.info("Turning off existing watchers...")
+ Log.perf("Turning off all individual existing watchers...")
self.watchers.forEach { (watcher) in
watcher.stopMonitoring()
}
diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings
index a1c6136..3b1ec19 100644
--- a/phpmon/Localizable.strings
+++ b/phpmon/Localizable.strings
@@ -27,6 +27,7 @@
"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_php_refresh" = "Refresh Information";
"mi_configuration" = "PHP Configuration";
@@ -36,11 +37,11 @@
"mi_upload_max_filesize" = "Max Upload";
"mi_manual_actions" = "Manual Actions";
"mi_services" = "Services";
-"mi_help" = "First Aid";
"mi_other" = "First Aid & Services";
+"mi_first_aid" = "First Aid";
"mi_composer" = "Composer";
-"mi_valet_config" = "Locate Valet folder (.config/valet)";
+"mi_valet_config" = "Locate Valet Folder (.config/valet)";
"mi_php_config" = "Locate PHP Configuration File (php.ini)";
"mi_global_composer" = "Locate Global Composer File (.composer)";
"mi_phpinfo" = "Show Current Configuration (phpinfo)";
@@ -132,22 +133,26 @@
"prefs.subtitle" = "Preferences";
"prefs.close" = "Close";
-"prefs.global_shortcut" = "Global shortcut:";
-"prefs.dynamic_icon" = "Icon customization:";
-"prefs.info_density" = "Info density:";
+"prefs.global_shortcut" = "Global Shortcut:";
+"prefs.dynamic_icon" = "Dynamic Icon:";
+"prefs.dynamic_icon" = "Icon Type:";
+"prefs.info_density" = "Info Density:";
"prefs.services" = "Services:";
"prefs.switcher" = "Switcher:";
"prefs.integrations" = "Integrations:";
+"prefs.icon_options.php" = "Display PHP Icon";
+"prefs.icon_options.elephant" = "Display Elephant Icon";
+"prefs.icon_options.none" = "Do Not Display Icon";
+
+"prefs.icon_options_desc" = "This option decides which icon will be displayed next to the version number of the currently linked PHP version. If the Dynamic Icon option has been disabled, this will have no effect.";
+
"prefs.auto_restart_services_title" = "Auto-restart PHP-FPM";
"prefs.auto_restart_services_desc" = "When checked, will automatically restart PHP-FPM when you check or uncheck an extension. Slightly slower when enabled, but this applies the extension change immediately for all sites you're serving, no need to restart PHP-FPM manually.";
"prefs.dynamic_icon_title" = "Display dynamic icon in menu bar";
"prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible. If checked, it will display the major version number of the currently linked PHP version.";
-"prefs.icon_hint_title" = "Display PHP hint next to version number";
-"prefs.icon_hint_desc" = "If you uncheck this box, the icon in the menu bar will only show the version number and not the PHP hint. This preference does not do anything if the dynamic icon has been disabled.";
-
"prefs.display_full_php_version" = "Display full PHP version everywhere";
"prefs.display_full_php_version_desc" = "Display the full version instead of the major version displayed in the menu bar and the dropdown menu. (This may be undesirable on smaller displays, so this is disabled by default.)";
@@ -180,7 +185,7 @@
// Composer Update
"alert.composer_missing.title" = "Composer not found!";
-"alert.composer_missing.info" = "Make sure you have Composer available in `/usr/local/bin/composer`. If Composer is located somewhere else, please create a symlink, like so (make sure to use the correct path):\n\n`ln -s /path/to/composer /user/local/bin`.";
+"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_progress.title" = "Updating global dependencies...";
"alert.composer_progress.info" = "You can see the progress in the terminal output below.";
@@ -213,6 +218,15 @@ problem manually, using your own Terminal app (this just shows you the output)."
"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.";
+// 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.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`.";
+
// 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.";
@@ -278,3 +292,8 @@ You can do this by running `composer global update` in your terminal. After that
"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";
+
+// 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.";