1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-08 04:20:07 +02:00

🔀 Merge branch 'dev/4.x'

This commit is contained in:
2021-12-22 23:49:33 +01:00
71 changed files with 2900 additions and 635 deletions

View File

@ -9,10 +9,24 @@
/* 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 */; };
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 */; };
54FCFD2E276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */; };
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */; };
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 */; };
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; };
C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4188988275FE8CB001EF227 /* Filesystem.swift */; };
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
@ -21,13 +35,23 @@
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; };
C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; };
C42295DD2358D02000E263B2 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; };
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; };
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; };
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; };
C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; };
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.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 */; };
C464ADB0275A7A6A003FCD53 /* SiteListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* SiteListVC.swift */; };
C464ADB2275A87CA003FCD53 /* SiteListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* SiteListCell.swift */; };
C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* SiteListCell.swift */; };
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
@ -47,6 +71,23 @@
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4AF9F71275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; };
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */; };
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; };
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; };
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F7C275454A900D44ED0 /* ValetTest.swift */; };
C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; };
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; };
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */; };
C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; };
C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; };
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */; };
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */; };
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; };
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; };
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; };
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; };
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
@ -94,9 +135,16 @@
/* Begin PBXFileReference section */
5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = "<group>"; };
5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
54AB03252763858F00A29D5F /* Timer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timer.swift; sourceTree = "<group>"; };
54B48B5E275F66AE006D90C5 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CheckboxPreferenceView.xib; sourceTree = "<group>"; };
54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxPreferenceView.swift; sourceTree = "<group>"; };
54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HotkeyPreferenceView.xib; sourceTree = "<group>"; };
54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotkeyPreferenceView.swift; sourceTree = "<group>"; };
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = "<group>"; };
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = "<group>"; };
C4188988275FE8CB001EF227 /* Filesystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filesystem.swift; sourceTree = "<group>"; };
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; };
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -108,12 +156,17 @@
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = "<group>"; };
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = "<group>"; };
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.swift"; sourceTree = "<group>"; };
C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = "<group>"; };
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.swift"; sourceTree = "<group>"; };
C43A8A1925D9CD1000591B77 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = "<group>"; };
C43A8A1F25D9D1D700591B77 /* brew.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = brew.json; sourceTree = "<group>"; };
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewJsonParserTest.swift; sourceTree = "<group>"; };
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListWC.swift; sourceTree = "<group>"; };
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListVC.swift; sourceTree = "<group>"; };
C464ADB1275A87CA003FCD53 /* SiteListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListCell.swift; sourceTree = "<group>"; };
C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = "<group>"; };
@ -129,6 +182,16 @@
C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = "<group>"; };
C49EAB45259FC305007F6C3B /* Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
C4AF9F70275445FF00D44ED0 /* valet-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "valet-config.json"; sourceTree = "<group>"; };
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigParserTest.swift; sourceTree = "<group>"; };
C4AF9F792754499000D44ED0 /* Valet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.swift; sourceTree = "<group>"; };
C4AF9F7C275454A900D44ED0 /* ValetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetTest.swift; sourceTree = "<group>"; };
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.swift; sourceTree = "<group>"; };
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionExtractorTest.swift; sourceTree = "<group>"; };
C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MenuOutlets.swift"; sourceTree = "<group>"; };
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+ActivationPolicy.swift"; sourceTree = "<group>"; };
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+GlobalHotkey.swift"; sourceTree = "<group>"; };
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = "<group>"; };
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
@ -172,6 +235,7 @@
5420395826135DC100FB00FA /* PrefsVC.swift */,
5420395E2613607600FB00FA /* Preferences.swift */,
C41CD0272628D8E20065BBED /* Keybinds */,
54FCFD28276C88C0004CE748 /* Views */,
);
path = Preferences;
sourceTree = "<group>";
@ -186,6 +250,17 @@
path = PHP;
sourceTree = "<group>";
};
54FCFD28276C88C0004CE748 /* Views */ = {
isa = PBXGroup;
children = (
54FCFD25276C883F004CE748 /* CheckboxPreferenceView.xib */,
54FCFD29276C8AA4004CE748 /* CheckboxPreferenceView.swift */,
54FCFD2C276C8D67004CE748 /* HotkeyPreferenceView.xib */,
54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */,
);
path = Views;
sourceTree = "<group>";
};
C405A4CD24B9B9070062FAFA /* IAP */ = {
isa = PBXGroup;
children = (
@ -220,7 +295,6 @@
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup;
children = (
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
C4EE188322D3386B00E126E5 /* Constants.swift */,
C41E181722CB61EB0072CF09 /* Domain */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
@ -244,10 +318,12 @@
C41E181722CB61EB0072CF09 /* Domain */ = {
isa = PBXGroup;
children = (
C4AF9F6B275445D300D44ED0 /* Integrations */,
C4B13B1D25C4915000548C3A /* Core */,
54B20EDF263AA22C00D3250E /* PHP */,
C4F7808A25D7F918000DBC97 /* Terminal */,
C47331A0247093AC009A0597 /* Menu */,
C464ADAA275A7A25003FCD53 /* SiteList */,
5420395726135DB800FB00FA /* Preferences */,
C4811D2822D70D9C00B5F6B3 /* Helpers */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
@ -255,6 +331,17 @@
path = Domain;
sourceTree = "<group>";
};
C464ADAA275A7A25003FCD53 /* SiteList */ = {
isa = PBXGroup;
children = (
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */,
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */,
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */,
C464ADB1275A87CA003FCD53 /* SiteListCell.swift */,
);
path = SiteList;
sourceTree = "<group>";
};
C47331A0247093AC009A0597 /* Menu */ = {
isa = PBXGroup;
children = (
@ -272,19 +359,53 @@
isa = PBXGroup;
children = (
C476FF9722B0DD830098105B /* Alert.swift */,
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
54B48B5E275F66AE006D90C5 /* Application.swift */,
C4188988275FE8CB001EF227 /* Filesystem.swift */,
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */,
54AB03252763858F00A29D5F /* Timer.swift */,
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
C4AF9F6A275445C900D44ED0 /* Valet */ = {
isa = PBXGroup;
children = (
C4AF9F792754499000D44ED0 /* Valet.swift */,
);
path = Valet;
sourceTree = "<group>";
};
C4AF9F6B275445D300D44ED0 /* Integrations */ = {
isa = PBXGroup;
children = (
C4AF9F6C275445D900D44ED0 /* Homebrew */,
C4AF9F6A275445C900D44ED0 /* Valet */,
);
path = Integrations;
sourceTree = "<group>";
};
C4AF9F6C275445D900D44ED0 /* Homebrew */ = {
isa = PBXGroup;
children = (
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */,
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
);
path = Helpers;
path = Homebrew;
sourceTree = "<group>";
};
C4B13B1D25C4915000548C3A /* Core */ = {
isa = PBXGroup;
children = (
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */,
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */,
C4811D2322D70A4700B5F6B3 /* App.swift */,
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */,
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
C4D8016522B1584700C6DA1B /* Startup.swift */,
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
);
@ -294,6 +415,7 @@
C4F7807A25D7F84B000DBC97 /* phpmon-tests */ = {
isa = PBXGroup;
children = (
C4AF9F70275445FF00D44ED0 /* valet-config.json */,
C43A8A1F25D9D1D700591B77 /* brew.json */,
C4F780A725D80AE8000DBC97 /* php.ini */,
C4F7807D25D7F84B000DBC97 /* Info.plist */,
@ -302,6 +424,9 @@
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
C43A8A1925D9CD1000591B77 /* Utility.swift */,
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */,
C4AF9F7C275454A900D44ED0 /* ValetTest.swift */,
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */,
);
path = "phpmon-tests";
sourceTree = "<group>";
@ -416,10 +541,13 @@
files = (
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
C4AF9F71275445FF00D44ED0 /* valet-config.json in Resources */,
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */,
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
C4232EE52612526500158FC6 /* Credits.html in Resources */,
54FCFD26276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */,
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
54FCFD2D276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */,
);
@ -429,8 +557,11 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54FCFD27276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */,
54FCFD2E276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
C43A8A2025D9D1D700591B77 /* brew.json in Resources */,
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -445,29 +576,45 @@
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */,
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */,
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */,
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */,
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */,
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
C42295DD2358D02000E263B2 /* Command.swift in Sources */,
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */,
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
5420395F2613607600FB00FA /* Preferences.swift in Sources */,
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
54AB03262763858F00A29D5F /* Timer.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */,
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */,
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */,
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */,
C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C464ADAC275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
C464ADB2275A87CA003FCD53 /* SiteListCell.swift in Sources */,
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -478,6 +625,9 @@
files = (
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
54AB03272763858F00A29D5F /* Timer.swift in Sources */,
54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
54B48B60275F66AE006D90C5 /* Application.swift in Sources */,
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
@ -485,27 +635,43 @@
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */,
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */,
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */,
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */,
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */,
C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
C464ADB3275A87CA003FCD53 /* SiteListCell.swift in Sources */,
C4AF9F78275447F100D44ED0 /* ValetConfigParserTest.swift in Sources */,
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */,
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */,
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */,
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */,
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */,
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */,
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */,
C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */,
C464ADB0275A7A6A003FCD53 /* SiteListVC.swift in Sources */,
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */,
C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */,
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */,
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */,
C4F780B425D80B51000DBC97 /* Actions.swift in Sources */,
@ -586,7 +752,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -642,7 +808,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
@ -659,7 +825,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 81;
CURRENT_PROJECT_VERSION = 135;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -667,7 +833,8 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 4.0.1;
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 4.1;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -683,7 +850,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 81;
CURRENT_PROJECT_VERSION = 135;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -691,7 +858,8 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 4.0.1;
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 4.1;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -712,7 +880,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 11.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@ -733,7 +901,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 11.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;

View File

@ -7,7 +7,7 @@
**PHP Monitor** (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so you need to have it set up before you can use this.
<img src="./docs/screenshot34.png" width="412px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot41.jpg" width="800px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: A menu showing all of the functionality of PHP Monitor.</i></small>
@ -21,12 +21,12 @@ PHP Monitor also gives you quick access to various useful functionality (like ac
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
* macOS 10.14 Mojave or higher (works on macOS 11 Big Sur and macOS 12 Monterey)
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* The brew formula `php` has to be installed (which version is detected)
* Laravel Valet 2.13 or higher
* Laravel Valet 2.16.2 or higher (older versions might be compatible but are not supported)
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released._
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`._
## 🚀 How to install
@ -231,18 +231,30 @@ PHP Monitor itself doesn't do any network requests. Feel free to check the sourc
</details>
<details>
<summary><strong>After running PHP Monitor, Homebrew sometimes has issues with `brew upgrade`!</strong></summary>
<summary><strong>How do I get various applications to show up in the domain list's right-click menu?</strong></summary>
This is a security feature of Brew. When you start a service as an administrator, the root user becomes the owner of relevant binaries.
When you select and right-click on a domain, you can open these directories with various applications. This can help speed up your workflow. However, for these apps to show up, they must be detected first.
You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder).
The supported apps are: <i>PhpStorm, Visual Studio Code, Sublime Text, Sublime Merge, iTerm</i>.
All of these apps should just be detected correctly, no matter their location on your system. If you can open it using `open -a "appname"`, the app should be detected and work. If you have renamed the app, there might be an issue getting it detected.
To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift).
</details>
<details>
<summary><strong>After running PHP Monitor, Homebrew sometimes has issues with `brew upgrade` or `brew cleanup`!</strong></summary>
<strike>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).</strike>
**Update**: If you are using the Valet switcher (currently not available in the latest stable build) you will not encounter this issue. For more information on this, see [this issue](https://github.com/nicoverbruggen/phpmon/issues/17) and also [this issue about switching to Valet's switcher](https://github.com/nicoverbruggen/phpmon/issues/34).
</details>
<details>
<summary><strong>The app has crashed!</strong></summary>
Please get in touch and open an issue. PHP Monitor shouldn't crash :)
Please get in touch and open an issue. PHP Monitor shouldn't crash... (unless you are actually removing PHP *while* the app is running, thats considered normal behaviour!)
</details>
@ -283,16 +295,7 @@ In order to save power, this only happens once every 60 seconds.
This utility will detect which PHP versions you have installed via Homebrew, and then allows you to switch between them.
This means:
- You have at least the latest version of PHP installed (`php`)
- You have installed Laravel Valet (`which valet` returns `/usr/local/bin/valet`)
- You ran `valet trust`, which means Valet commands can be run without using sudo
The utility runs the following commands:
- Unlink all detected PHP versions & stop the respective `php@X.X` services
- Link the desired version of PHP, and start the associated service
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be much faster than Valets switcher.
### Want to know more?

View File

@ -2,17 +2,25 @@
## Supported versions
Generally speaking, only the latest version of **PHP Monitor** is supported:
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions |
| ------- | ------------- | ------------------ | ----- | ----- | ----- |
| 4.0 | ✅ Universal binary | ✅ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 |
| 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 |
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 |
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable |
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable |
| < 2.4 | Intel binary<br/>`/usr/local/homebrew` installations only | ❌ | Catalina (10.15) | macOS 10.14+ | not applicable |
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 4.1 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
## Legacy versions
These versions of PHP Monitor are no longer supported, but if youre using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0) and Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.0—3.4 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.1 | 2.13 |
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ | PHP 5.6—PHP 8.0 | 2.13 |
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |
| < 2.4 | Intel binary<br/>`/usr/local/homebrew` installations only | ❌ | Catalina (10.15) | macOS 10.14+ | not applicable | not applicable |
## Reporting a vulnerability

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

BIN
docs/screenshot41.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

View File

@ -0,0 +1,38 @@
//
// ValetConfigParserTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class ValetConfigParserTest: XCTestCase {
static var jsonConfigFileUrl: URL {
return Bundle(for: Self.self).url(
forResource: "valet-config",
withExtension: "json"
)!
}
func testCanLoadConfigFile() throws {
let json = try? String(
contentsOf: Self.jsonConfigFileUrl,
encoding: .utf8
)
let config = try! JSONDecoder().decode(
Valet.Configuration.self,
from: json!.data(using: .utf8)!
)
XCTAssertEqual(config.tld, "test")
XCTAssertEqual(config.paths, [
"/Users/username/.config/valet/Sites",
"/Users/username/Sites"
])
XCTAssertEqual(config.loopback, "127.0.0.1")
}
}

View File

@ -0,0 +1,18 @@
//
// ValetTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class ValetTest: XCTestCase {
func testDetermineValetVersion() {
let version = Actions.valet("--version")
XCTAssert(version.contains("Laravel Valet 2."))
}
}

View File

@ -0,0 +1,25 @@
//
// VersionExtractorTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import XCTest
class VersionExtractorTest: XCTestCase {
func testExtractVersion() {
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
}
func testVersionComparison() {
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)
XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending)
}
}

View File

@ -0,0 +1,8 @@
{
"tld": "test",
"paths": [
"/Users/username/.config/valet/Sites",
"/Users/username/Sites"
],
"loopback": "127.0.0.1"
}

View File

@ -0,0 +1,68 @@
{
"images" : [
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "link.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "car-alt.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M438.66 212.33l-11.24-28.1-19.93-49.83C390.38 91.63 349.57 64 303.5 64h-127c-46.06 0-86.88 27.63-103.99 70.4l-19.93 49.83-11.24 28.1C17.22 221.5 0 244.66 0 272v48c0 16.12 6.16 30.67 16 41.93V416c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-32h256v32c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-54.07c9.84-11.25 16-25.8 16-41.93v-48c0-27.34-17.22-50.5-41.34-59.67zm-306.73-54.16c7.29-18.22 24.94-30.17 44.57-30.17h127c19.63 0 37.28 11.95 44.57 30.17L368 208H112l19.93-49.83zM80 319.8c-19.2 0-32-12.76-32-31.9S60.8 256 80 256s48 28.71 48 47.85-28.8 15.95-48 15.95zm320 0c-19.2 0-48 3.19-48-15.95S380.8 256 400 256s32 12.76 32 31.9-12.8 31.9-32 31.9z"/></svg>

After

Width:  |  Height:  |  Size: 918 B

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Locked.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect id="Locked" x="0" y="0" width="30" height="30" style="fill:none;"/>
<g id="Locked1" serif:id="Locked">
<g transform="matrix(0.0468317,0,0,0.0468317,4.50971,3.01112)">
<path d="M400,256L152,256L152,152.9C152,113.3 183.7,80.4 223.3,80C263.3,79.6 296,112.1 296,152L296,266.079C296,279.379 376,279.137 376,265.837L376,152C376,68 307.5,-0.3 223.5,0C139.5,0.3 72,69.5 72,153.5L72,256L48,256C21.5,256 0,277.5 0,304L0,464C0,490.5 21.5,512 48,512L400,512C426.5,512 448,490.5 448,464L448,304C448,277.5 426.5,256 400,256Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Unlocked.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect id="Locked" x="0" y="0" width="30" height="30" style="fill:none;"/>
<g id="Locked1" serif:id="Locked">
<g transform="matrix(0.0468317,0,0,0.0468317,4.50971,3.01112)">
<path d="M400,256L152,256L152,152.9C152,113.3 183.7,80.4 223.3,80C263.3,79.6 296,112.1 296,152L296,168C296,181.3 322.386,192 322.386,192L352,192C365.3,192 376,181.3 376,168L376,152C376,68 307.5,-0.3 223.5,0C139.5,0.3 72,69.5 72,153.5L72,256L48,256C21.5,256 0,277.5 0,304L0,464C0,490.5 21.5,512 48,512L400,512C426.5,512 448,490.5 448,464L448,304C448,277.5 426.5,256 400,256Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 353 B

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "php@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

View File

@ -15,6 +15,16 @@ class Constants {
*/
static let LatestStablePhpVersion = "8.1"
/**
The minimum version of Valet that is recommended.
If the installed version is older, a notification will be shown
every time the app launches (with a recommendation to upgrade).
The minimum requirement is currently synced to PHP 8.1 compatibility.
See also: https://github.com/laravel/valet/releases/tag/v2.16.2
*/
static let MinimumRecommendedValetVersion = "2.16.2"
/**
* The PHP versions supported by this application.
* Versions that do not appear in this array are omitted from the list.

View File

@ -23,7 +23,7 @@ class Actions {
let phpAlias = App.shared.brewPhpVersion
// Avoid inserting a duplicate
if (!versionsOnly.contains(phpAlias)) {
if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) {
versionsOnly.append(phpAlias);
}
@ -102,6 +102,19 @@ 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
) {
print("Switching to \(version) using Valet")
print(valet("use php@\(version)"))
completed()
}
/**
Switching to a new PHP version involves:
- unlinking the current version
@ -178,9 +191,14 @@ class Actions {
// MARK: - Quick Fix
/**
Detects all currently available PHP versions, and unlinks each and every one of them.
After this, the brew services are also stopped, the latest PHP version is linked, and php + nginx are restarted.
If this does not solve the issue, the user may need to install additional extensions and/or run `composer global update`.
Detects all currently available PHP versions,
and unlinks each and every one of them.
After this, the brew services are also stopped,
the latest PHP version is linked, and php + nginx are restarted.
If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`.
*/
public static func fixMyPhp()
{
@ -203,6 +221,14 @@ class Actions {
// MARK: Common Shell Commands
/**
Runs a `valet` command.
*/
public static func valet(_ command: String) -> String
{
return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true)
}
/**
Runs a `brew` command. Can run as superuser.
*/
@ -220,7 +246,8 @@ class Actions {
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
// Check if gsed exists; it is able to follow symlinks, which we want to do to toggle the extension
// Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension
if Shell.fileExists("\(Paths.binPath)/gsed") {
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else {

View File

@ -0,0 +1,44 @@
//
// App+ActivationPolicy.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
import Foundation
extension App {
// MARK: - Application State
/**
Registers a window as currently open.
*/
public func register(window name: String) {
if !openWindows.contains(name) {
openWindows.append(name)
}
updateActivationPolicy()
}
/**
Removes a window, assuming it was closed.
*/
public func remove(window name: String) {
openWindows.removeAll { window in
window == name
}
updateActivationPolicy()
}
/**
If there are any open windows, the app will be a regular app.
If there are no windows open, the app will be an accessory (toolbar) app.
*/
public func updateActivationPolicy() {
NSApp.setActivationPolicy(openWindows.count > 0 ? .regular : .accessory)
}
}

View File

@ -0,0 +1,55 @@
//
// App+GlobalHotkey.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import HotKey
import Cocoa
extension App {
// MARK: - Methods
/**
On startup, the preferences should be loaded from the .plist,
and we'll enable the shortcut if it is set.
*/
func loadGlobalHotkey() {
// Make sure we can retrieve the hotkey from preferences
guard let hotkey = Preferences.preferences[.globalHotkey] as? String else {
print("No global hotkey was saved in preferences. None set.")
return
}
// Make sure we can parse the JSON into the desired format
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
print("No global hotkey loaded, could not be parsed!")
shortcutHotkey = nil
return
}
shortcutHotkey = HotKey(keyCombo: KeyCombo(
carbonKeyCode: keybindPref.keyCode,
carbonModifiers: keybindPref.carbonFlags
))
}
/**
Sets up the action that needs to occur when the shortcut key is pressed
(opens the menu).
*/
func setupGlobalHotkeyListener() {
guard let hotkey = shortcutHotkey else {
return
}
hotkey.keyDownHandler = {
MainMenu.shared.statusItem.button?.performClick(nil)
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
}

View File

@ -10,10 +10,16 @@ import HotKey
class App {
// MARK: Static Vars
/** The static app instance. Accessible at any time. */
static let shared = App()
init() {
loadGlobalHotkey()
/** Retrieve the version number from the main info dictionary, Info.plist. */
static var version: String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
return "\(version) (\(build))"
}
/** Information about the currently linked PHP installation. */
@ -26,42 +32,36 @@ class App {
return App.shared.busy
}
// MARK: Variables
/** The list of preferences that are currently active. */
var preferences: [PreferenceName: Bool]!
/**
The window controller of the currently active window.
*/
var windowController: NSWindowController? = nil
/** The window controller of the currently active preferences window. */
var preferencesWindowController: PrefsWC? = nil
/**
Whether the application is busy switching versions.
*/
/** The window controller of the currently active site list window. */
var siteListWindowController: SiteListWC? = nil
/** Whether the application is busy switching versions. */
var busy: Bool = false
/**
The currently active installation of PHP.
*/
/** The currently active installation of PHP. */
var currentInstall: ActivePhpInstallation? = nil
/**
All available versions of PHP.
*/
/** All available versions of PHP. */
var availablePhpVersions: [String] = []
/**
Cached information about the PHP installations; contains only the full version number at this point.
*/
/** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:]
/**
The timer that will periodically fetch the PHP version that is currently active.
*/
/** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = []
/** Timer that will periodically reload info about the user's PHP installation. */
var timer: Timer?
/**
Information we were able to discern from the Homebrew info command (as JSON).
*/
/** Information we were able to discern from the Homebrew info command (as JSON). */
var brewPhpPackage: HomebrewPackage! = nil {
didSet {
brewPhpVersion = brewPhpPackage!.version
@ -79,52 +79,27 @@ class App {
*/
var brewPhpVersion: String = Constants.LatestStablePhpVersion
// MARK: - Global Hotkey
/**
The shortcut the user has requested.
*/
var shortcutHotkey: HotKey? = nil {
didSet {
self.setupGlobalHotkeyListener()
setupGlobalHotkeyListener()
}
}
// MARK: - Methods
// MARK: - Activation Policy
/**
On startup, the preferences should be loaded from the .plist, and we'll enable the shortcut if it is set.
Variable that keeps track of which windows are currently open.
(Please note that window controllers remain open in memory once opened.)
When this list is updated, the app activation policy is re-evaluated.
The app activation policy dictates how the app runs
(as a normal app or as a toolbar app).
*/
private func loadGlobalHotkey() {
// Make sure we can retrieve the hotkey from preferences; if we cannot, no hotkey is set
guard let hotkey = Preferences.preferences[.globalHotkey] as? String else {
print("No global hotkey loaded")
return
}
// Make sure we can parse the JSON into the desired format; if we cannot, no hotkey is set
guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else {
print("No global hotkey loaded, could not be parsed!")
self.shortcutHotkey = nil
return
}
self.shortcutHotkey = HotKey(keyCombo: KeyCombo(
carbonKeyCode: keybindPref.keyCode,
carbonModifiers: keybindPref.carbonFlags
))
}
/**
Sets up the action that needs to occur when the shortcut key is pressed (open the menu).
*/
private func setupGlobalHotkeyListener() {
guard let hotkey = self.shortcutHotkey else {
return
}
hotkey.keyDownHandler = {
MainMenu.shared.statusItem.button?.performClick(nil)
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
var openWindows: [String] = []
}

View File

@ -0,0 +1,40 @@
//
// AppDelegate+MenuOutlets.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
Any outlets connected to the app's main menu (not the menu that shows when the icon in
the menu bar is clicked, but the regular app's main menu) are configured here.
Default interactions like copy/paste, select all, close window etc. are wired up by
default in the storyboard and do not need to be manually added.
Extra functionality (like the menu item to reload the list of sites) does, however.
- Note: This menu is only displayed when the app is NOT running in accessory mode.
For more information about this, please see the ActivationPolicy-related extension.
*/
extension AppDelegate {
// MARK: - Menu Interactions
@IBAction func reloadSiteListPressed(_ sender: Any) {
let vc = App.shared.siteListWindowController?
.window?.contentViewController as? SiteListVC
if vc != nil {
// If the view exists, directly reload the list of sites
vc!.reloadSites()
} else {
// If the view does not exist, reload the cached data that was populated when the app initially launched.
Valet.shared.reloadSites()
}
}
}

View File

@ -0,0 +1,42 @@
//
// AppDelegate+Notifications.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 06/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import UserNotifications
extension AppDelegate {
// MARK: - Notifications
public func setupNotifications() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
notificationCenter.requestAuthorization(options: [.alert], completionHandler: { granted, error in
if !granted {
print("PHP Monitor does not have permission to show notifications.")
}
if let error = error {
print("PHP Monitor encounted an error determining notification permissions:")
print(error)
}
})
}
/**
Ensure that the application displays notifications even when the app is active.
*/
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler:
@escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner])
}
}

View File

@ -9,7 +9,7 @@ import Cocoa
import UserNotifications
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
// MARK: - Variables
@ -38,16 +38,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
*/
let paths: Paths
/**
The Valet singleton that determines all information
about Valet and its current configuration.
*/
let valet: Valet
// MARK: - Initializer
/**
When the application initializes, create all singletons.
*/
override init() {
print("==================================")
print("PHP MONITOR by Nico Verbruggen")
print("Version \(App.version)")
print("==================================")
self.sharedShell = Shell.user
self.state = App.shared
self.menu = MainMenu.shared
self.paths = Paths.shared
self.valet = Valet.shared
super.init()
}
@ -55,27 +66,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
/**
When the application has finished launching, we'll want to set up
the user notification center delegate, and kickoff the menu
the user notification center permissions, and kickoff the menu
startup procedure.
*/
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSUserNotificationCenter.default.delegate = self
self.menu.startup()
}
// MARK: - NSUserNotificationCenterDelegate
/**
When a notification is sent, the delegate of the notification center
is asked whether the notification should be presented or not. Since
the user can now disable notifications per application since macOS
Catalina, any and all notifications should be displayed.
*/
func userNotificationCenter(
_ center: NSUserNotificationCenter,
shouldPresent notification: NSUserNotification
) -> Bool {
return true
// Make sure notifications will work
setupNotifications()
// Make sure the menu performs its initial checks
menu.startup()
}
}

View File

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19455"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -31,6 +33,249 @@
</items>
</menu>
</menuItem>
<menuItem title="File" id="XRy-v5-KNb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="File" id="zA7-mh-f1x">
<items>
<menuItem title="Close" keyEquivalent="w" id="2FI-pQ-tuO">
<connections>
<action selector="performClose:" target="Ady-hI-5gd" id="ZHq-so-Sba"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Sites" id="9gy-d3-Pos">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Sites" id="YTZ-bb-TOG">
<items>
<menuItem title="Reload Site List" keyEquivalent="r" id="Ema-AU-Nbr">
<connections>
<action selector="reloadSiteListPressed:" target="Voe-Tx-rLC" id="geC-Ld-haX"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="r2Z-pR-umI">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="8Pm-83-BlM">
<items>
<menuItem title="Undo" keyEquivalent="z" id="jCt-Yf-FSE">
<connections>
<action selector="undo:" target="Ady-hI-5gd" id="O3z-27-Ug0"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="fCh-1M-Qyg">
<connections>
<action selector="redo:" target="Ady-hI-5gd" id="utE-Bv-fdY"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="7Ja-wX-Yyy"/>
<menuItem title="Cut" keyEquivalent="x" id="wud-nd-1nZ">
<connections>
<action selector="cut:" target="Ady-hI-5gd" id="C3e-e7-Z50"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="V42-o1-WHL">
<connections>
<action selector="copy:" target="Ady-hI-5gd" id="ec3-KB-YgV"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="aBF-dz-Blf">
<connections>
<action selector="paste:" target="Ady-hI-5gd" id="BHd-PO-XsH"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="EgA-GE-99p">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="ls4-pp-hcL"/>
</connections>
</menuItem>
<menuItem title="Delete" id="smI-vK-hCc">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="Ady-hI-5gd" id="iNe-gC-rFo"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="29b-s6-UmK">
<connections>
<action selector="selectAll:" target="Ady-hI-5gd" id="b6J-NN-IIc"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uFh-RS-XNP"/>
<menuItem title="Find" id="Dvh-pB-nbE">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="QlO-5L-pAZ">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="m08-yq-ZGg">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="W9P-aN-Jes"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="pjr-Fe-SEl">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="XVP-he-TQd"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="Zpc-8S-9bB">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="oRy-fc-1aa"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="GDM-nF-rG0">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="x6a-fg-4qv"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="6fa-55-D8I">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="EGI-VW-wxB"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="H8e-pj-DLt">
<connections>
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="oI9-dt-1tg"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="RMo-NJ-dGJ">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="4PN-Vd-GBg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="2c8-04-pLg">
<connections>
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="hyy-YK-6Bw"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="ZBj-z6-5YX">
<connections>
<action selector="checkSpelling:" target="Ady-hI-5gd" id="21B-wo-C7b"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="APF-Br-Trc"/>
<menuItem title="Check Spelling While Typing" id="knZ-NA-0Jb">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="32z-g2-SCz"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="v6M-1d-el3">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="1YL-19-eUI"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="qg8-Mm-AiQ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="zdy-r0-ioM"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="SW4-hB-QOQ">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="EmO-8n-AsV">
<items>
<menuItem title="Show Substitutions" id="rvM-Vq-p0Y">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="SjT-fP-U8q"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="TTo-oL-4Pj"/>
<menuItem title="Smart Copy/Paste" id="op9-oC-x65">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="82G-c7-eEX"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="Sg4-Dr-IyH">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="tf9-2j-dbm"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="Uop-B5-hKQ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="2jO-5h-PhN"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="G9f-Tv-imo">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="ryX-Py-Jan"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="9sq-LY-oWc">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="ps3-Vn-32V"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="AQ0-Wh-nkQ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="nEj-vL-yg2"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="BLU-2S-dqL">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="lFI-Ry-XFg">
<items>
<menuItem title="Make Upper Case" id="bx6-aZ-THy">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="tyN-SK-Cgt"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="Ks8-z7-N7j">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="0fo-Fo-xfq"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="Lv4-Up-dyv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="Bqs-0x-WzX"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="cTl-lQ-Mg9">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="4c5-we-5Vo">
<items>
<menuItem title="Start Speaking" id="YPC-zf-2Xh">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="Ady-hI-5gd" id="VRy-Kb-4cG"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="4YM-9V-tLE">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="KHB-GE-En3"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
@ -49,11 +294,11 @@
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="-343" y="-16"/>
<point key="canvasLocation" x="-484" y="32"/>
</scene>
<!--Window Controller-->
<scene sceneID="PQa-AT-b2a">
@ -68,6 +313,10 @@
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<toolbar key="toolbar" implicitIdentifier="611E3485-DC7F-46A0-8528-11CF9366370C" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconAndLabel" sizeMode="regular" id="fcq-wR-7iv">
<allowedToolbarItems/>
<defaultToolbarItems/>
</toolbar>
<connections>
<outlet property="delegate" destination="hLJ-Fd-wRr" id="6HE-8Y-aCO"/>
</connections>
@ -84,205 +333,252 @@
<scene sceneID="iyi-IS-7Ps">
<objects>
<viewController title="Preferences" storyboardIdentifier="preferences" showSeguePresentationStyle="single" id="AW2-rV-rbS" customClass="PrefsVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" wantsLayer="YES" misplaced="YES" id="Pf1-A5-3Xz">
<rect key="frame" x="0.0" y="0.0" width="574" height="311"/>
<view key="view" wantsLayer="YES" id="Pf1-A5-3Xz">
<rect key="frame" x="0.0" y="0.0" width="574" height="498"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="GSr-K5-3yw">
<rect key="frame" x="485" y="13" width="76" height="32"/>
<buttonCell key="cell" type="push" title="CLOSE" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ocw-Rx-gyh">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="pressed:" target="AW2-rV-rbS" id="8dA-y4-voq"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MEf-MN-oXt">
<rect key="frame" x="148" y="274" width="406" height="18"/>
<buttonCell key="cell" type="check" title="DYN_ICON" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="m5s-qp-Iaj">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="toggledDynamicIcon:" target="AW2-rV-rbS" id="cuJ-mt-agf"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JrH-aa-AzL">
<rect key="frame" x="148" y="253" width="408" height="14"/>
<textFieldCell key="cell" title="DYN_ICON_DESC" id="MHA-Xt-xgF">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="V7b-jv-oCB">
<rect key="frame" x="143" y="75" width="184" height="32"/>
<constraints>
<constraint firstAttribute="width" constant="170" id="9jD-Bf-T2M"/>
</constraints>
<backgroundFilters>
<ciFilter name="CIDotScreen">
<configuration>
<real key="inputAngle" value="0.0"/>
<ciVector key="inputCenter">
<real value="150"/>
<real value="150"/>
</ciVector>
<null key="inputImage"/>
<real key="inputSharpness" value="0.69999999999999996"/>
<real key="inputWidth" value="6"/>
</configuration>
</ciFilter>
</backgroundFilters>
<buttonCell key="cell" type="push" title="SET_SHORTCUT" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="R63-tN-KVQ">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="register:" target="AW2-rV-rbS" id="4Mj-eM-4eW"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YsQ-AZ-Aei">
<rect key="frame" x="325" y="75" width="138" height="32"/>
<buttonCell key="cell" type="push" title="CLEAR_SHORTCUT" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nvE-5d-VOS">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystem"/>
</buttonCell>
<connections>
<action selector="unregister:" target="AW2-rV-rbS" id="2RI-4w-6Td"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5ZK-BG-o1t">
<rect key="frame" x="42" y="85" width="100" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="PREF_GLOSHO:" id="xiD-8H-p5s">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="31d-gd-auR">
<rect key="frame" x="18" y="275" width="124" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="120" id="8dt-Pg-wFI"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="PREF_DYN_ICON:" id="E10-ss-Cdz">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1TO-9H-z2d">
<rect key="frame" x="148" y="60" width="101" height="14"/>
<textFieldCell key="cell" title="SHORTCUT_DESC" id="nYP-yi-DBf">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="vSc-oQ-NC5">
<rect key="frame" x="148" y="220" width="121" height="18"/>
<buttonCell key="cell" type="check" title="FULL_PHP_VER" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="eCd-ja-EwE">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="toggledFullPhpVersion:" target="AW2-rV-rbS" id="RCY-Ah-sLM"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="t24-LR-wKz">
<rect key="frame" x="148" y="199" width="123" height="14"/>
<textFieldCell key="cell" title="FULL_PHP_VER_DESC" id="8gG-qs-mHR">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ogC-wz-ZfO">
<rect key="frame" x="18" y="153" width="124" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="120" id="i9O-6m-Gr9"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="PREF_SERVICES:" id="bm4-rf-kCF">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="47u-9B-eDu">
<rect key="frame" x="148" y="152" width="126" height="18"/>
<buttonCell key="cell" type="check" title="AUTO_RESTART" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="n1d-l4-inL">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="toggledAutoRestartServices:" target="AW2-rV-rbS" id="THn-nu-IiJ"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ObP-GE-ejZ">
<rect key="frame" x="148" y="131" width="126" height="14"/>
<textFieldCell key="cell" title="AUTO_RESTART_DESC" id="F9P-iQ-gBk">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<stackView distribution="fillEqually" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="k57-O3-Yyj">
<rect key="frame" x="0.0" y="15" width="574" height="468"/>
</stackView>
</subviews>
<constraints>
<constraint firstItem="ogC-wz-ZfO" firstAttribute="trailing" secondItem="31d-gd-auR" secondAttribute="trailing" id="2Lr-Ht-qKI"/>
<constraint firstItem="t24-LR-wKz" firstAttribute="leading" secondItem="vSc-oQ-NC5" secondAttribute="leading" id="3tK-kp-q5R"/>
<constraint firstItem="t24-LR-wKz" firstAttribute="top" secondItem="vSc-oQ-NC5" secondAttribute="bottom" constant="8" symbolic="YES" id="4Ft-lN-vwA"/>
<constraint firstAttribute="trailing" secondItem="JrH-aa-AzL" secondAttribute="trailing" constant="20" symbolic="YES" id="8iM-Xf-ShU"/>
<constraint firstItem="ObP-GE-ejZ" firstAttribute="leading" secondItem="47u-9B-eDu" secondAttribute="leading" id="ASF-WR-A3X"/>
<constraint firstAttribute="trailing" secondItem="GSr-K5-3yw" secondAttribute="trailing" constant="20" symbolic="YES" id="AT9-5F-6g1"/>
<constraint firstItem="YsQ-AZ-Aei" firstAttribute="leading" secondItem="V7b-jv-oCB" secondAttribute="trailing" constant="12" symbolic="YES" id="Bk6-4V-GLk"/>
<constraint firstItem="31d-gd-auR" firstAttribute="top" secondItem="Pf1-A5-3Xz" secondAttribute="top" constant="20" symbolic="YES" id="C3K-NX-BBl"/>
<constraint firstItem="YsQ-AZ-Aei" firstAttribute="top" secondItem="V7b-jv-oCB" secondAttribute="top" id="DY5-za-saX"/>
<constraint firstItem="vSc-oQ-NC5" firstAttribute="leading" secondItem="JrH-aa-AzL" secondAttribute="leading" id="FVa-vu-VGJ"/>
<constraint firstItem="MEf-MN-oXt" firstAttribute="leading" secondItem="31d-gd-auR" secondAttribute="trailing" constant="10" id="G5S-JV-re3"/>
<constraint firstItem="V7b-jv-oCB" firstAttribute="firstBaseline" secondItem="5ZK-BG-o1t" secondAttribute="firstBaseline" id="H5D-2D-DLH"/>
<constraint firstItem="1TO-9H-z2d" firstAttribute="leading" secondItem="V7b-jv-oCB" secondAttribute="leading" id="Imk-o0-2fS"/>
<constraint firstItem="ObP-GE-ejZ" firstAttribute="top" secondItem="47u-9B-eDu" secondAttribute="bottom" constant="8" symbolic="YES" id="JqR-Jd-SoR"/>
<constraint firstItem="JrH-aa-AzL" firstAttribute="leading" secondItem="MEf-MN-oXt" secondAttribute="leading" id="K2H-Af-2qK"/>
<constraint firstItem="5ZK-BG-o1t" firstAttribute="top" secondItem="ObP-GE-ejZ" secondAttribute="bottom" constant="30" id="LO4-8j-ihp"/>
<constraint firstItem="47u-9B-eDu" firstAttribute="top" secondItem="ogC-wz-ZfO" secondAttribute="top" id="T9j-v2-fSW"/>
<constraint firstItem="JrH-aa-AzL" firstAttribute="top" secondItem="MEf-MN-oXt" secondAttribute="bottom" constant="8" symbolic="YES" id="Vf8-fx-H50"/>
<constraint firstItem="MEf-MN-oXt" firstAttribute="firstBaseline" secondItem="31d-gd-auR" secondAttribute="firstBaseline" id="W36-bE-iAT"/>
<constraint firstItem="1TO-9H-z2d" firstAttribute="firstBaseline" secondItem="V7b-jv-oCB" secondAttribute="baseline" constant="25" id="bJG-ed-pch"/>
<constraint firstItem="V7b-jv-oCB" firstAttribute="leading" secondItem="JrH-aa-AzL" secondAttribute="leading" id="bUY-uH-N7A"/>
<constraint firstItem="5ZK-BG-o1t" firstAttribute="trailing" secondItem="31d-gd-auR" secondAttribute="trailing" id="c4g-jO-JUm"/>
<constraint firstAttribute="bottom" secondItem="GSr-K5-3yw" secondAttribute="bottom" constant="20" symbolic="YES" id="dAS-yW-vua"/>
<constraint firstItem="vSc-oQ-NC5" firstAttribute="top" secondItem="JrH-aa-AzL" secondAttribute="bottom" constant="16" id="hQf-4s-iHn"/>
<constraint firstItem="GSr-K5-3yw" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="mTE-WD-54L"/>
<constraint firstItem="47u-9B-eDu" firstAttribute="leading" secondItem="MEf-MN-oXt" secondAttribute="leading" id="n8B-C8-dXs"/>
<constraint firstItem="31d-gd-auR" firstAttribute="leading" secondItem="Pf1-A5-3Xz" secondAttribute="leading" constant="20" symbolic="YES" id="o0J-yT-TDX"/>
<constraint firstItem="ogC-wz-ZfO" firstAttribute="top" secondItem="t24-LR-wKz" secondAttribute="bottom" constant="30" id="oXh-LE-sRS"/>
<constraint firstAttribute="trailing" secondItem="MEf-MN-oXt" secondAttribute="trailing" constant="20" symbolic="YES" id="pJg-zj-cBs"/>
<constraint firstItem="GSr-K5-3yw" firstAttribute="top" secondItem="1TO-9H-z2d" secondAttribute="bottom" constant="20" id="pMZ-Gx-Jmm"/>
<constraint firstAttribute="bottom" secondItem="k57-O3-Yyj" secondAttribute="bottom" constant="15" id="ECF-1q-1zc"/>
<constraint firstItem="k57-O3-Yyj" firstAttribute="top" secondItem="Pf1-A5-3Xz" secondAttribute="top" constant="15" id="HwH-HC-MSf"/>
<constraint firstAttribute="trailing" secondItem="k57-O3-Yyj" secondAttribute="trailing" id="M7l-W4-EDv"/>
<constraint firstItem="k57-O3-Yyj" firstAttribute="leading" secondItem="Pf1-A5-3Xz" secondAttribute="leading" id="ctd-MO-fe1"/>
</constraints>
</view>
<connections>
<outlet property="buttonAutoRestartServices" destination="47u-9B-eDu" id="kyg-BX-PQK"/>
<outlet property="buttonClearShortcut" destination="YsQ-AZ-Aei" id="1xo-hk-HgM"/>
<outlet property="buttonClose" destination="GSr-K5-3yw" id="d4I-Cf-gXD"/>
<outlet property="buttonDisplayFullPhpVersion" destination="vSc-oQ-NC5" id="ZLa-Vf-4Dq"/>
<outlet property="buttonDynamicIcon" destination="MEf-MN-oXt" id="qEN-Vg-EZS"/>
<outlet property="buttonSetShortcut" destination="V7b-jv-oCB" id="2aS-S4-cKR"/>
<outlet property="labelAutoRestartServices" destination="ObP-GE-ejZ" id="uwY-D7-Uve"/>
<outlet property="labelDisplayFullPhpVersion" destination="t24-LR-wKz" id="wYj-Z0-a3h"/>
<outlet property="labelDynamicIcon" destination="JrH-aa-AzL" id="CFc-fF-oPq"/>
<outlet property="labelShortcut" destination="1TO-9H-z2d" id="paF-hK-78x"/>
<outlet property="leftLabelDynamicIcon" destination="31d-gd-auR" id="ANZ-Zs-4d7"/>
<outlet property="leftLabelGlobalShortcut" destination="5ZK-BG-o1t" id="73E-9i-cg8"/>
<outlet property="leftLabelServices" destination="ogC-wz-ZfO" id="BYx-Gv-N1p"/>
<outlet property="stackView" destination="k57-O3-Yyj" id="fF8-8n-bc9"/>
</connections>
</viewController>
<customObject id="eQC-8B-FkX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="264" y="457"/>
<point key="canvasLocation" x="251" y="205"/>
</scene>
<!--Window Controller-->
<scene sceneID="4XS-kY-YIS">
<objects>
<windowController storyboardIdentifier="siteListWindow" id="8Ec-9q-82s" customClass="SiteListWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" title="Domains" subtitle="Linked &amp; Parked" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="raw-02-3Q1">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="425" y="461" width="550" height="263"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="uVx-Da-x4I">
<rect key="frame" x="0.0" y="0.0" width="550" height="263"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<toolbar key="toolbar" implicitIdentifier="594015E3-8428-4926-9341-4B8CE4C7E373" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconOnly" sizeMode="regular" id="OOz-oZ-vlN">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="B734CDE2-70E9-45A8-B1B3-5A5DE156621D" label="Reload" paletteLabel="Reload" tag="-1" bordered="YES" sizingBehavior="auto" id="YtK-vM-5y7">
<imageReference key="image" image="arrow.clockwise" catalog="system" symbolScale="medium"/>
<connections>
<action selector="pressedReload:" target="8Ec-9q-82s" id="fLc-bD-oYQ"/>
</connections>
</toolbarItem>
<searchToolbarItem implicitItemIdentifier="629F0782-3C5F-4CD0-9396-3A054A422180" label="Search" paletteLabel="Search" visibilityPriority="1001" id="Q7Z-fw-lB9">
<nil key="toolTip"/>
<searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="oWA-TH-Pm7">
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
<autoresizingMask key="autoresizingMask"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="3NO-6x-aLc">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</searchFieldCell>
</searchField>
</searchToolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="YtK-vM-5y7"/>
<searchToolbarItem reference="Q7Z-fw-lB9"/>
</defaultToolbarItems>
</toolbar>
<connections>
<outlet property="delegate" destination="8Ec-9q-82s" id="xEM-aj-eHL"/>
</connections>
</window>
<connections>
<outlet property="searchToolbarItem" destination="Q7Z-fw-lB9" id="J5o-oh-VhO"/>
<segue destination="JZI-Vd-9oq" kind="relationship" relationship="window.shadowedContentViewController" id="9Gy-Gw-hPH"/>
</connections>
</windowController>
<customObject id="VCP-dF-cqM" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="773.5"/>
</scene>
<!--Site ListVC-->
<scene sceneID="aZt-6w-TFl">
<objects>
<viewController storyboardIdentifier="siteList" id="JZI-Vd-9oq" customClass="SiteListVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="rIZ-4U-bhj">
<rect key="frame" x="0.0" y="0.0" width="550" height="309"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView autohidesScrollers="YES" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
<rect key="frame" x="0.0" y="0.0" width="550" height="309"/>
<clipView key="contentView" id="6IL-DW-37w">
<rect key="frame" x="1" y="1" width="548" height="307"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" rowHeight="54" rowSizeStyle="automatic" viewBased="YES" id="cp3-34-pQj">
<rect key="frame" x="0.0" y="0.0" width="548" height="307"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn width="536" minWidth="40" maxWidth="10000" id="oeH-B2-0rA">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="Ith-sv-3bo">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<tableCellView identifier="siteItem" wantsLayer="YES" id="5GY-nN-BWd" customClass="SiteListCell" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="531" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
<rect key="frame" x="38" y="26" width="145" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
<font key="font" metaFont="systemSemibold" size="13"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
<rect key="frame" x="38" y="12" width="75" height="14"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QPX-eu-eV8">
<rect key="frame" x="10" y="22" width="20" height="20"/>
<constraints>
<constraint firstAttribute="width" constant="20" id="Bmk-CN-Yyn"/>
<constraint firstAttribute="height" constant="20" id="d4z-lb-Ww0"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Lock" id="aJ0-ia-YrZ"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jKi-Ls-7FZ">
<rect key="frame" x="459" y="28" width="64" height="11"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="DRIVER TYPE" id="fjd-eb-itv">
<font key="font" metaFont="miniSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TbX-e2-3QL">
<rect key="frame" x="459" y="15" width="36" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Driver" id="GMt-SG-vFl">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="syz-LF-l6P">
<rect key="frame" x="0.0" y="-2" width="531" height="5"/>
</box>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0NQ-ZD-CqD">
<rect key="frame" x="435" y="18" width="18" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="18" id="Suw-gm-AEi"/>
<constraint firstAttribute="height" constant="18" id="qO6-vg-5nC"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="IconLinked" id="2ng-pK-kvv"/>
<color key="contentTintColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/>
</imageView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="TbX-e2-3QL" secondAttribute="trailing" constant="20" symbolic="YES" id="3vE-LR-S7N"/>
<constraint firstItem="TbX-e2-3QL" firstAttribute="leading" secondItem="0NQ-ZD-CqD" secondAttribute="trailing" constant="8" symbolic="YES" id="4cb-D9-8d1"/>
<constraint firstItem="XJL-Uw-frD" firstAttribute="leading" secondItem="QPX-eu-eV8" secondAttribute="trailing" constant="10" id="55y-3V-RYt"/>
<constraint firstItem="syz-LF-l6P" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" id="8QK-nf-Fiw"/>
<constraint firstItem="QPX-eu-eV8" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="top" id="9QB-jZ-k1V"/>
<constraint firstItem="QPX-eu-eV8" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" constant="10" id="GOj-sw-ZlZ"/>
<constraint firstItem="TbX-e2-3QL" firstAttribute="top" secondItem="jKi-Ls-7FZ" secondAttribute="bottom" constant="-1" id="J29-wT-Uex"/>
<constraint firstItem="CXK-Q9-CpO" firstAttribute="leading" secondItem="XJL-Uw-frD" secondAttribute="leading" id="Ojw-VZ-3EG"/>
<constraint firstAttribute="trailing" secondItem="syz-LF-l6P" secondAttribute="trailing" id="PWd-5k-AlD"/>
<constraint firstItem="XJL-Uw-frD" firstAttribute="top" secondItem="5GY-nN-BWd" secondAttribute="top" constant="12" id="QeE-c7-I9U"/>
<constraint firstAttribute="trailing" secondItem="jKi-Ls-7FZ" secondAttribute="trailing" constant="10" id="Uhk-Dy-c65"/>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="Utr-aa-tqX"/>
<constraint firstItem="CXK-Q9-CpO" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="bottom" id="VKg-Vq-sYa"/>
<constraint firstItem="TbX-e2-3QL" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" constant="5" id="cN8-zO-fnc"/>
<constraint firstAttribute="bottom" secondItem="syz-LF-l6P" secondAttribute="bottom" id="gj7-cJ-Lle"/>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="CXK-Q9-CpO" secondAttribute="trailing" constant="8" symbolic="YES" id="iEd-Y3-zhp"/>
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="8" symbolic="YES" id="lLA-Jx-Q4W"/>
<constraint firstItem="jKi-Ls-7FZ" firstAttribute="leading" secondItem="TbX-e2-3QL" secondAttribute="leading" id="zjN-s3-2Ww"/>
</constraints>
<connections>
<outlet property="imageViewLock" destination="QPX-eu-eV8" id="Nnh-kB-adG"/>
<outlet property="imageViewType" destination="0NQ-ZD-CqD" id="Cph-FN-LaY"/>
<outlet property="labelDriver" destination="TbX-e2-3QL" id="qJh-Ak-Dge"/>
<outlet property="labelPathName" destination="CXK-Q9-CpO" id="iVZ-cL-azB"/>
<outlet property="labelSiteName" destination="XJL-Uw-frD" id="f0t-vd-W68"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
<connections>
<outlet property="dataSource" destination="JZI-Vd-9oq" id="sbf-YF-ENF"/>
<outlet property="delegate" destination="JZI-Vd-9oq" id="kal-o7-c23"/>
</connections>
</tableView>
</subviews>
</clipView>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="300" id="R3Z-g3-tYQ"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="550" id="iRQ-sz-oyv"/>
</constraints>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
<rect key="frame" x="1" y="293" width="548" height="15"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
<rect key="frame" x="558" y="29" width="15" height="225"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<progressIndicator maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="ZiS-Gq-TLQ">
<rect key="frame" x="260" y="150" width="30" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="30" id="XK3-AR-Oc0"/>
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
</constraints>
</progressIndicator>
</subviews>
<constraints>
<constraint firstItem="p0j-eB-I2i" firstAttribute="leading" secondItem="rIZ-4U-bhj" secondAttribute="leading" id="2Tx-yb-xrv"/>
<constraint firstItem="p0j-eB-I2i" firstAttribute="top" secondItem="rIZ-4U-bhj" secondAttribute="top" id="Pst-5A-dI0"/>
<constraint firstAttribute="bottom" secondItem="p0j-eB-I2i" secondAttribute="bottom" id="QEw-5m-u1s"/>
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerY" secondItem="rIZ-4U-bhj" secondAttribute="centerY" constant="-10" id="XqX-Tf-8ck"/>
<constraint firstItem="ZiS-Gq-TLQ" firstAttribute="centerX" secondItem="rIZ-4U-bhj" secondAttribute="centerX" id="eD8-TV-7dF"/>
<constraint firstAttribute="trailing" secondItem="p0j-eB-I2i" secondAttribute="trailing" id="zWH-TD-RZv"/>
</constraints>
</view>
<connections>
<outlet property="progressIndicator" destination="ZiS-Gq-TLQ" id="Ylb-Vk-uub"/>
<outlet property="tableView" destination="cp3-34-pQj" id="sdw-Ac-27X"/>
</connections>
</viewController>
<customObject id="HgD-aB-bQb" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="288" y="765"/>
</scene>
</scenes>
<resources>
<image name="IconLinked" width="512" height="512"/>
<image name="Lock" width="30" height="30"/>
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
</resources>
</document>

View File

@ -26,7 +26,7 @@ class Startup {
performEnvironmentCheck(
!Shell.fileExists("\(Paths.binPath)/php"),
messageText: "startup.errors.php_binary.title".localized,
informativeText: "startup.errors.php_binary_desc".localized,
informativeText: "startup.errors.php_binary.desc".localized,
breaking: true
)

View File

@ -12,6 +12,10 @@ extension String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}
func localized(_ args: CVarArg...) -> String {
String(format: self.localized, arguments: args)
}
func countInstances(of stringToFind: String) -> Int {
if (stringToFind.isEmpty) {
return 0
@ -34,4 +38,37 @@ extension String {
return String(self[start ..< end])
}
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
/*
<1> We split the version by period (.).
<2> Then, we find the difference of digit that we will zero pad.
<3> If there are no differences, we don't need to do anything and use simple .compare.
<4> We populate an array of missing zero.
<5> We add zero pad array to a version with a fewer period and zero.
<6> We user array components to build back our versions from components and compare them.
This time it will have the same period and number of digit.
*/
func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "."
var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
if zeroDiff == 0 { // <3>
// Same format, compare normally
return self.compare(otherVersion, options: .numeric)
} else {
let zeros = Array(repeating: "0", count: abs(zeroDiff)) // <4>
if zeroDiff > 0 {
otherVersionComponents.append(contentsOf: zeros) // <5>
} else {
versionComponents.append(contentsOf: zeros)
}
return versionComponents.joined(separator: versionDelimiter)
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
}
}
}

View File

@ -27,8 +27,38 @@ class Alert {
return alert.runModal() == .alertFirstButtonReturn
}
public static func confirm(
onWindow window: NSWindow,
messageText: String,
informativeText: String,
buttonTitle: String = "OK",
secondButtonTitle: String = "Cancel",
style: NSAlert.Style = .warning,
onFirstButtonPressed: @escaping (() -> Void)
) {
let alert = NSAlert.init()
alert.alertStyle = style
alert.messageText = messageText
alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) {
alert.addButton(withTitle: secondButtonTitle)
}
alert.beginSheetModal(for: window) { response in
if response == .alertFirstButtonReturn {
onFirstButtonPressed()
}
}
}
public static func notify(message: String, info: String, style: NSAlert.Style = .informational) {
_ = self.present(messageText: message, informativeText: info, buttonTitle: "OK", secondButtonTitle: "", style: style)
_ = present(
messageText: message,
informativeText: info,
buttonTitle: "OK",
secondButtonTitle: "",
style: style
)
}
}

View File

@ -0,0 +1,64 @@
//
// Editor.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
/// An application that is capable of opening a particular directory (usually of a PHP project).
/// In most cases this is going to be a code editor, but it could also be another application
/// that supports opening those directories, like a visual Git client or a terminal app.
class Application {
enum AppType {
case editor, browser, git_gui, terminal
}
/// Name of the app. Used for display purposes and to determine `name.app` exists.
let name: String
/// Application type. Depending on the type, a different action might occur.
let type: AppType
/// Initializer. Used to detect a specific app of a specific type.
init(_ name: String, _ type: AppType) {
self.name = name
self.type = type
}
/**
Attempt to open a specific directory in the app of choice.
(This will open the app if it isn't open yet.)
*/
@objc public func openDirectory(file: String) {
return Shell.run("/usr/bin/open -a \"\(name)\" \(file)")
}
/** Checks if the app is installed. */
func isInstalled() -> Bool {
// If this script does not complain, the app exists!
return Shell.user.execute(
"/usr/bin/open -Ra \"\(name)\"",
requiresPath: false,
waitUntilExit: true
).task.terminationStatus == 0
}
/**
Detect which apps are available to open a specific directory.
*/
static public func detectPresetApplications() -> [Application] {
return [
Application("PhpStorm", .editor),
Application("Visual Studio Code", .editor),
Application("Sublime Text", .editor),
Application("Sublime Merge", .git_gui),
Application("iTerm", .terminal)
].filter {
return $0.isInstalled()
}
}
}

View File

@ -0,0 +1,23 @@
//
// FileSystem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Filesystem {
/**
Checks if a file exists at the provided path.
Uses `FileManager`.
*/
public static func fileExists(_ path: String) -> Bool {
return FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
)
}
}

View File

@ -6,14 +6,28 @@
//
import Foundation
import UserNotifications
class LocalNotification {
public static func send(title: String, subtitle: String) {
let notification = NSUserNotification()
notification.title = title
notification.subtitle = subtitle
NSUserNotificationCenter.default.deliver(notification)
let content = UNMutableNotificationContent()
content.title = title
content.body = subtitle
let uuidString = UUID().uuidString
let request = UNNotificationRequest(
identifier: uuidString,
content: content,
trigger: nil
)
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) { (error) in
if error != nil {
print(error!)
}
}
}
}

View File

@ -41,6 +41,7 @@ class MenuBarImageGenerator {
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
let targetImage: NSImage = NSImage(size: image.size)
let rep: NSBitmapImageRep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(image.size.width),
@ -64,4 +65,34 @@ class MenuBarImageGenerator {
return targetImage
}
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
let imageRect = CGRect(
x: 0,
y: 0,
width: textImage.size.width + divider,
height: textImage.size.height
)
let image: NSImage = NSImage(size: imageRect.size)
image.lockFocus()
let difference = imageRect.size.width - textImage.size.width
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)
image.unlockFocus()
return image
}
}

View File

@ -0,0 +1,48 @@
//
// PMWindowController.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
/**
This window class keeps track of which windows are currently visible, and reports this info back to the App class.
For more information, check the `windows` property on `App`.
- Note: This class does make a simple assumption: each window controller corresponds to a single view.
*/
class PMWindowController: NSWindowController, NSWindowDelegate {
public var windowName: String {
fatalError("Please specify a window name")
}
override func showWindow(_ sender: Any?) {
super.showWindow(sender)
App.shared.register(window: windowName)
}
public func positionWindowInTopLeftCorner() {
guard let frame = NSScreen.main?.frame else { return }
guard let window = self.window else { return }
window.setFrame(NSRect(
x: frame.size.width - window.frame.size.width - 20,
y: frame.size.height - window.frame.size.height - 40,
width: window.frame.width,
height: window.frame.height
), display: true)
}
func windowWillClose(_ notification: Notification) {
App.shared.remove(window: windowName)
}
deinit {
print("Window controller '\(windowName)' was deinitialized")
}
}

View File

@ -0,0 +1,32 @@
//
// 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
}
}
}

View File

@ -0,0 +1,37 @@
//
// VersionExtractor.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class VersionExtractor {
public static func from(_ string: String) -> String? {
let regex = try! NSRegularExpression(
pattern: #"Laravel Valet (?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
options: []
)
let match = regex.matches(
in: string,
options: [],
range: NSMakeRange(0, string.count)
).first
guard let match = match else {
return nil
}
let range = Range(
match.range(withName: "version"),
in: string
)!
return String(string[range])
}
}

View File

@ -18,8 +18,8 @@ class HomebrewDiagnostics {
var errors: [HomebrewDiagnostics.Errors] = []
init() {
if self.determineAliasConflicts() {
self.errors.append(.aliasConflict)
if determineAliasConflicts() {
errors.append(.aliasConflict)
}
}

View File

@ -16,7 +16,8 @@ struct HomebrewPackage: Decodable {
let linked_keg: String?
public var version: String {
return aliases.first!.replacingOccurrences(of: "php@", with: "")
return aliases.first!
.replacingOccurrences(of: "php@", with: "")
}
}

View File

@ -0,0 +1,168 @@
//
// Valet.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class Valet {
static let shared = Valet()
/// The version of Valet that was detected.
var version: String
/// The Valet configuration file.
var config: Valet.Configuration
/// A cached list of sites that were detected after analyzing the paths set up for Valet.
var sites: [Site] = []
init() {
version = VersionExtractor.from(Actions.valet("--version"))
?? "UNKNOWN"
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet/config.json")
config = try! JSONDecoder().decode(
Valet.Configuration.self,
from: try! String(contentsOf: file, encoding: .utf8).data(using: .utf8)!
)
self.sites = []
}
public func startPreloadingSites() {
if self.sites.count <= 10 {
// Preload the sites and their drivers
print("Fewer than or 11 sites found, preloading list of sites...")
self.reloadSites()
}
}
public func reloadSites() {
resolvePaths(tld: config.tld)
}
public func validateVersion() -> Void {
if version == "UNKNOWN" {
return print("The Valet version could not be extracted... that does not bode well.")
}
if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending {
let version = version
print("Valet version \(version) is too old! (recommended: \(Constants.MinimumRecommendedValetVersion))")
DispatchQueue.main.async {
Alert.notify(message: "alert.min_valet_version.title".localized, info: "alert.min_valet_version.info".localized(version, Constants.MinimumRecommendedValetVersion))
}
} else {
print("Valet version \(version) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
}
}
private func resolvePaths(tld: String) {
sites = []
for path in config.paths {
let entries = try! FileManager.default.contentsOfDirectory(atPath: path)
for entry in entries {
resolvePath(entry, forPath: path, tld: tld)
}
}
}
private func resolvePath(_ entry: String, forPath path: String, tld: String) {
let siteDir = path + "/" + entry
// See if the file is a symlink, if so, resolve it
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
// We can also determine whether the thing at the path is a directory, too
let type = attrs[FileAttributeKey.type] as! FileAttributeType
if type == FileAttributeType.typeSymbolicLink {
sites.append(Site(aliasPath: siteDir, tld: tld))
} else if type == FileAttributeType.typeDirectory {
sites.append(Site(absolutePath: siteDir, tld: tld))
}
}
// MARK: - Structs
class Site {
/// Name of the site. Does not include the TLD.
var name: String!
/// The absolute path to the directory that is served.
var absolutePath: String!
/// Location of the alias. If set, this is a linked domain.
var aliasPath: String?
/// Whether the site has been secured.
var secured: Bool!
/// What driver is currently in use. If not detected, defaults to nil.
var driver: String? = nil
init() {}
convenience init(absolutePath: String, tld: String) {
self.init()
self.absolutePath = absolutePath
self.name = URL(string: absolutePath)!.lastPathComponent
self.aliasPath = nil
determineSecured(tld)
determineDriver()
}
convenience init(aliasPath: String, tld: String) {
self.init()
self.absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
self.name = URL(string: aliasPath)!.lastPathComponent
self.aliasPath = aliasPath
determineSecured(tld)
determineDriver()
}
public func determineSecured(_ tld: String) {
secured = Shell.fileExists("~/.config/valet/Certificates/\(self.name!).\(tld).key")
}
public func determineDriver() {
let driver = Shell.pipe("cd \(absolutePath!) && valet which", requiresPath: true)
if driver.contains("This site is served by") {
self.driver = driver
// TODO: Use a regular expression to retrieve the driver instead?
.replacingOccurrences(of: "This site is served by [", with: "")
.replacingOccurrences(of: "ValetDriver].\n", with: "")
} else {
self.driver = nil
}
}
}
struct Configuration: Decodable {
/// Top level domain suffix. Usually "test" but can be set to something else.
/// - Important: Does not include the actual dot. ("test", not ".test"!)
let tld: String
/// The paths that need to be checked.
let paths: [String]
/// The loopback address.
let loopback: String
/// The default site that is served if the domain is not found. Optional.
let defaultSite: String?
private enum CodingKeys: String, CodingKey {
case tld, paths, loopback, defaultSite = "default"
}
}
}

View File

@ -55,9 +55,28 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
updatePhpVersionInStatusBar()
print("Determining broken PHP-FPM...")
// Attempt to find out if PHP-FPM is broken
let installation = App.phpInstall!
installation.notifyAboutBrokenPhpFpm()
print("Detecting applications...")
// Attempt to load list of applications
App.shared.detectedApplications = Application.detectPresetApplications()
let appNames = App.shared.detectedApplications.map { app in
return app.name
}
print("Detected applications: \(appNames)")
// Load the global hotkey
App.shared.loadGlobalHotkey()
// Attempt to find out more info about Valet
print("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version)")
Valet.shared.validateVersion()
Valet.shared.startPreloadingSites()
print("PHP Monitor is ready to serve!")
// Schedule a request to fetch the PHP version every 60 seconds
DispatchQueue.main.async { [self] in
App.shared.timer = Timer.scheduledTimer(
@ -107,6 +126,14 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
menu.addPhpActionMenuItems()
menu.addItem(NSMenuItem.separator())
// Add Valet interactions
menu.addValetMenuItems()
menu.addItem(NSMenuItem.separator())
// Add services
menu.addServicesMenuItems()
menu.addItem(NSMenuItem.separator())
// Add information about services & actions
menu.addPhpConfigurationMenuItems()
menu.addItem(NSMenuItem.separator())
@ -131,7 +158,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
*/
func setStatusBarImage(version: String) {
setStatusBar(
image: MenuBarImageGenerator.textToImage(text: version)
image: MenuBarImageGenerator.textToImageWithIcon(text: version)
)
}
@ -349,12 +376,24 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
}
}
// Switch the PHP version
/* DISABLED UNTIL VALET SWITCHING IS OK (see #34)
if Preferences.preferences[.useInternalSwitcher] as! Bool == false {
// 1. Default switcher using Valet
// Will cause less issues, but is slower
Actions.switchToPhpVersionUsingValet(
version: sender.version,
availableVersions: App.shared.availablePhpVersions,
completed: completion
)
} else { */
// 2. Custom switcher (internal)
// Will cause more issues with Homebrew and is faster
Actions.switchToPhpVersion(
version: sender.version,
availableVersions: App.shared.availablePhpVersions,
completed: completion
)
/* } */
}
}
@ -367,6 +406,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
PrefsVC.show()
}
@objc func openSiteList() {
SiteListVC.show()
}
@objc func terminateApp() {
NSApplication.shared.terminate(nil)
}

View File

@ -36,35 +36,9 @@ class StatusMenu : NSMenu {
self.addSwitchToPhpMenuItems()
self.addItem(NSMenuItem.separator())
self.addServicesMenuItems()
}
private func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
// Get the short and long version
let shortVersion = App.shared.availablePhpVersions[index]
let longVersion = App.shared.cachedPhpInstallations[shortVersion]!.longVersion
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
let versionString = long ? longVersion : shortVersion
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (shortVersion == App.shared.brewPhpVersion) ? "php" : "php@\(shortVersion)"
let menuItem = PhpMenuItem(
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
action: (shortVersion == App.phpInstall?.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
)
menuItem.version = shortVersion
shortcutKey = shortcutKey + 1
self.addItem(menuItem)
}
}
private func addServicesMenuItems() {
func addServicesMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_active_services".localized))
let services = NSMenuItem(title: "mi_manage_services".localized, action: nil, keyEquivalent: "")
@ -74,16 +48,36 @@ class StatusMenu : NSMenu {
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]
)
withKeyModifier: [.command, .shift])
servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
for item in servicesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(servicesMenu, for: services)
self.addItem(NSMenuItem(title: "mi_force_load_latest".localized, action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: "f"))
self.addForceLoadLatestVersion()
self.addItem(services)
self.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
}
func addForceLoadLatestVersion() {
if !App.shared.availablePhpVersions.contains(App.shared.brewPhpVersion) {
self.addItem(NSMenuItem(
title: "mi_force_load_latest_unavailable".localized(App.shared.brewPhpVersion),
action: nil, keyEquivalent: "f"
))
} else {
self.addItem(NSMenuItem(
title: "mi_force_load_latest".localized(App.shared.brewPhpVersion),
action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: "f"))
}
}
func addValetMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_valet".localized))
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
self.addItem(NSMenuItem(title: "mi_sitelist".localized, action: #selector(MainMenu.openSiteList), keyEquivalent: "l"))
self.addItem(NSMenuItem.separator())
}
func addPhpConfigurationMenuItems() {
@ -93,7 +87,6 @@ class StatusMenu : NSMenu {
// Configuration
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
self.addItem(NSMenuItem(title: "mi_global_composer".localized, action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g"))
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
@ -102,7 +95,7 @@ class StatusMenu : NSMenu {
return
}
let stats = App.phpInstall!.configuration
let stats = App.phpInstall!.limits
// Stats
self.addItem(NSMenuItem.separator())
@ -131,6 +124,31 @@ class StatusMenu : NSMenu {
self.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r"))
}
private func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
// Get the short and long version
let shortVersion = App.shared.availablePhpVersions[index]
let longVersion = App.shared.cachedPhpInstallations[shortVersion]!.longVersion
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
let versionString = long ? longVersion : shortVersion
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (shortVersion == App.shared.brewPhpVersion) ? "php" : "php@\(shortVersion)"
let menuItem = PhpMenuItem(
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
action: (shortVersion == App.phpInstall?.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
)
menuItem.version = shortVersion
shortcutKey = shortcutKey + 1
self.addItem(menuItem)
}
}
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
@ -160,3 +178,7 @@ class PhpMenuItem: NSMenuItem {
class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension? = nil
}
class EditorMenuItem: NSMenuItem {
var editor: Application? = nil
}

View File

@ -13,12 +13,13 @@ import Foundation
When initialized, that version's .ini files are also scanned (for active or inactive extensions).
Integrity checks can be performed to determine whether PHP-FPM is configured correctly.
- Note: Each installation has a separate version number. Using `version.short` is advisable if you want to interact with Homebrew.
- Note: Each installation has a separate version number.
Using `version.short` is advisable if you want to interact with Homebrew.
*/
class ActivePhpInstallation {
var version: Version!
var configuration: Configuration!
var limits: Limits!
var extensions: [PhpExtension]!
// MARK: - Computed
@ -31,11 +32,11 @@ class ActivePhpInstallation {
init() {
// Show information about the current version
self.getVersion()
getVersion()
// If an error occurred, exit early
if (version.error) {
configuration = Configuration()
limits = Limits()
extensions = []
return
}
@ -45,10 +46,10 @@ class ActivePhpInstallation {
extensions = PhpExtension.load(from: path)
// Get configuration values
configuration = Configuration(
memory_limit: self.getByteCount(key: "memory_limit"),
upload_max_filesize: self.getByteCount(key: "upload_max_filesize"),
post_max_size: self.getByteCount(key: "post_max_size")
limits = Limits(
memory_limit: getByteCount(key: "memory_limit"),
upload_max_filesize: getByteCount(key: "upload_max_filesize"),
post_max_size: getByteCount(key: "post_max_size")
)
// Return a list of .ini files parsed after php.ini
@ -59,9 +60,9 @@ class ActivePhpInstallation {
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
let extensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if extensions.count > 0 {
self.extensions.append(contentsOf: extensions)
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if exts.count > 0 {
extensions.append(contentsOf: exts)
}
}
}
@ -100,8 +101,10 @@ class ActivePhpInstallation {
* 10000: an integer = amount of bytes
* 1K, 1M, 1G = shorthand for kilobytes, megabytes and gigabytes
If none of these notations are used, the _fallback_ value is used. We'll show an emoji to indicate something has gone wrong here.
To clarify, B gets appended to valid values. As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes .
If none of these notations are used, the _fallback_ value is used.
We'll show an emoji to indicate something has gone wrong here.
To clarify, B gets appended to valid values.
As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes .
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
*/
@ -159,13 +162,23 @@ class ActivePhpInstallation {
// MARK: - Structs
/**
Struct containing information about the version number of the current PHP installation.
Also includes information about whether the install is considered "broken" or not.
If an error was found in the terminal output, `error` is set to `true` and the installation
can be considered broken. (The app will display this as well.)
*/
struct Version {
var short = "???"
var long = "???"
var error = false
}
struct Configuration {
/**
Struct containing information about the limits of the current PHP installation.
Includes: memory limit, max upload size and max post size.
*/
struct Limits {
var memory_limit = "???"
var upload_max_filesize = "???"
var post_max_size = "???"

View File

@ -1,5 +1,5 @@
//
// BrewPhpInstallation.swift
// PhpInstallation.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 28/11/2021.
@ -11,8 +11,11 @@ import Foundation
class PhpInstallation {
var longVersion: String
var homebrewInfo: HomebrewPackage?
/**
In order to determine details about a PHP installation, well simply run `php-config --version`
in the relevant directory.
*/
init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
self.longVersion = version
@ -22,19 +25,6 @@ class PhpInstallation {
arguments: ["--version"]
)
}
let info = Shell.pipe("\(Paths.brew) info php@\(version) --json")
do {
let data = try JSONDecoder().decode(
[HomebrewPackage].self,
from: info.data(using: .utf8)!
)
self.homebrewInfo = data.first!
} catch {
print("There was an issue parsing Homebrew info for PHP \(version)")
self.homebrewInfo = nil
}
}
}

View File

@ -13,6 +13,7 @@ enum PreferenceName: String {
case shouldDisplayDynamicIcon = "use_dynamic_icon"
case fullPhpVersionDynamicIcon = "full_php_in_menu_bar"
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
case useInternalSwitcher = "use_phpmon_switcher"
case globalHotkey = "global_hotkey"
}
@ -26,7 +27,7 @@ class Preferences {
public init() {
Preferences.handleFirstTimeLaunch()
self.cachedPreferences = Self.cache()
cachedPreferences = Self.cache()
}
// MARK: - First Time Run
@ -45,7 +46,8 @@ class Preferences {
UserDefaults.standard.register(defaults: [
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true,
PreferenceName.useInternalSwitcher.rawValue: false
])
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
@ -70,6 +72,7 @@ class Preferences {
.shouldDisplayDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.shouldDisplayDynamicIcon.rawValue) as Any,
.fullPhpVersionDynamicIcon: UserDefaults.standard.bool(forKey: PreferenceName.fullPhpVersionDynamicIcon.rawValue) as Any,
.autoServiceRestartAfterExtensionToggle: UserDefaults.standard.bool(forKey: PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue) as Any,
.useInternalSwitcher: UserDefaults.standard.bool(forKey: PreferenceName.useInternalSwitcher.rawValue) as Any,
// Part 2: Always Strings
.globalHotkey: UserDefaults.standard.string(forKey: PreferenceName.globalHotkey.rawValue) as Any,

View File

@ -12,213 +12,92 @@ import Carbon
class PrefsVC: NSViewController {
// Labels on the left
@IBOutlet weak var leftLabelDynamicIcon: NSTextField!
@IBOutlet weak var leftLabelServices: NSTextField!
@IBOutlet weak var leftLabelGlobalShortcut: NSTextField!
// MARK: - Window Identifier
// Dynamic icon
@IBOutlet weak var buttonDynamicIcon: NSButton!
@IBOutlet weak var labelDynamicIcon: NSTextField!
// Full PHP version
@IBOutlet weak var buttonDisplayFullPhpVersion: NSButton!
@IBOutlet weak var labelDisplayFullPhpVersion: NSTextField!
// Auto-restart services
@IBOutlet weak var buttonAutoRestartServices: NSButton!
@IBOutlet weak var labelAutoRestartServices: NSTextField!
// Shortcut
@IBOutlet weak var buttonSetShortcut: NSButton!
@IBOutlet weak var buttonClearShortcut: NSButton!
@IBOutlet weak var labelShortcut: NSTextField!
// Close button (bottom right)
@IBOutlet weak var buttonClose: NSButton!
@IBOutlet weak var stackView: NSStackView!
// MARK: - Display
public static func show(delegate: NSWindowDelegate? = nil) {
if (App.shared.windowController == nil) {
let vc = NSStoryboard(name: "Main", bundle: nil)
.instantiateController(withIdentifier: "preferences") as! PrefsVC
let window = NSWindow(contentViewController: vc)
public static func create(delegate: NSWindowDelegate?) {
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
window.title = "prefs.title".localized
window.delegate = delegate
window.styleMask = [.titled, .closable]
let windowController = storyboard.instantiateController(
withIdentifier: "preferencesWindow"
) as! PrefsWC
App.shared.windowController = PrefsWC(window: window)
windowController.window!.title = "prefs.title".localized
windowController.window!.subtitle = "prefs.subtitle".localized
windowController.window!.delegate = delegate
windowController.window!.styleMask = [.titled, .closable, .miniaturizable]
windowController.window!.delegate = windowController
windowController.positionWindowInTopLeftCorner()
App.shared.preferencesWindowController = windowController
}
App.shared.windowController!.showWindow(self)
public static func show(delegate: NSWindowDelegate? = nil) {
if (App.shared.preferencesWindowController == nil) {
Self.create(delegate: delegate)
}
App.shared.preferencesWindowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
}
// MARK: - Lifecycle
override func viewWillAppear() {
loadLocalization()
loadDynamicIconFromPreferences()
loadFullPhpVersionFromPreferences()
loadGlobalKeybindFromPreferences()
}
override func viewWillDisappear() {
if self.listeningForGlobalHotkey {
listeningForGlobalHotkey = false
}
}
private func loadLocalization() {
// Dynamic icon
leftLabelDynamicIcon.stringValue = "prefs.dynamic_icon".localized
labelDynamicIcon.stringValue = "prefs.dynamic_icon_desc".localized
buttonDynamicIcon.title = "prefs.dynamic_icon_title".localized
// Full PHP version
buttonDisplayFullPhpVersion.title = "prefs.display_full_php_version".localized
labelDisplayFullPhpVersion.stringValue = "prefs.display_full_php_version_desc".localized
// Services
leftLabelServices.stringValue = "prefs.services".localized
buttonAutoRestartServices.title = "prefs.auto_restart_services_title".localized
labelAutoRestartServices.stringValue = "prefs_auto_restart_services_desc".localized
// Global Shortcut
leftLabelGlobalShortcut.stringValue = "prefs.global_shortcut".localized
labelShortcut.stringValue = "prefs.shortcut_desc".localized
buttonSetShortcut.title = "prefs.shortcut_set".localized
buttonClearShortcut.title = "prefs.shortcut_clear".localized
// Close button
buttonClose.title = "prefs.close".localized
}
// MARK: - Loading Preferences
func loadDynamicIconFromPreferences() {
let shouldDisplay = Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == true
self.buttonDynamicIcon.state = shouldDisplay ? .on : .off
}
func loadFullPhpVersionFromPreferences() {
let shouldDisplay = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool == true
self.buttonDisplayFullPhpVersion.state = shouldDisplay ? .on : .off
}
func loadAutoRestartServicesFromPreferences() {
let shouldDisplay = Preferences.preferences[.autoServiceRestartAfterExtensionToggle] as! Bool == true
self.buttonAutoRestartServices.state = shouldDisplay ? .on : .off
}
// MARK: - Actions
@IBAction func toggledDynamicIcon(_ sender: Any) {
Preferences.update(.shouldDisplayDynamicIcon, value: buttonDynamicIcon.state == .on)
override func viewDidLoad() {
[
CheckboxPreferenceView.make(
sectionText: "prefs.dynamic_icon".localized,
descriptionText: "prefs.dynamic_icon_desc".localized,
checkboxText: "prefs.dynamic_icon_title".localized,
preference: .shouldDisplayDynamicIcon,
action: {
MainMenu.shared.refreshIcon()
}
@IBAction func toggledFullPhpVersion(_ sender: Any) {
Preferences.update(.fullPhpVersionDynamicIcon, value: buttonDisplayFullPhpVersion.state == .on)
),
CheckboxPreferenceView.make(
sectionText: "",
descriptionText: "prefs.display_full_php_version_desc".localized,
checkboxText: "prefs.display_full_php_version".localized,
preference: .fullPhpVersionDynamicIcon,
action: {
MainMenu.shared.refreshIcon()
MainMenu.shared.update()
}
@IBAction func toggledAutoRestartServices(_ sender: Any) {
Preferences.update(.autoServiceRestartAfterExtensionToggle, value: buttonAutoRestartServices.state == .on)
}
// MARK: - Shortcut Preference
// Adapted from: https://dev.to/mitchartemis/creating-a-global-configurable-shortcut-for-macos-apps-in-swift-25e9
var listeningForGlobalHotkey = false {
didSet {
if listeningForGlobalHotkey {
DispatchQueue.main.async { [weak self] in
self?.buttonSetShortcut.highlight(true)
self?.buttonSetShortcut.title = "prefs.shortcut_listening".localized
}
} else {
DispatchQueue.main.async { [weak self] in
self?.buttonSetShortcut.highlight(false)
self?.loadGlobalKeybindFromPreferences()
}
}
}
}
func loadGlobalKeybindFromPreferences() {
let globalKeybind = GlobalKeybindPreference.fromJson(Preferences.preferences[.globalHotkey] as! String?)
if (globalKeybind != nil) {
updateKeybindButton(globalKeybind!)
} else {
buttonSetShortcut.title = "prefs.shortcut_set".localized
}
buttonClearShortcut.isEnabled = globalKeybind != nil
}
func updateGlobalShortcut(_ event : NSEvent) {
self.listeningForGlobalHotkey = false
if let characters = event.charactersIgnoringModifiers {
let newGlobalKeybind = GlobalKeybindPreference.init(
function: event.modifierFlags.contains(.function),
control: event.modifierFlags.contains(.control),
command: event.modifierFlags.contains(.command),
shift: event.modifierFlags.contains(.shift),
option: event.modifierFlags.contains(.option),
capsLock: event.modifierFlags.contains(.capsLock),
carbonFlags: event.modifierFlags.carbonFlags,
characters: characters,
keyCode: UInt32(event.keyCode)
),
CheckboxPreferenceView.make(
sectionText: "prefs.services".localized,
descriptionText: "prefs.auto_restart_services_desc".localized,
checkboxText: "prefs.auto_restart_services_title".localized,
preference: .autoServiceRestartAfterExtensionToggle,
action: {}
),
/* DISABLED UNTIL VALET SWITCHING IS OK (see #34)
CheckboxPreferenceView.make(
sectionText: "",
descriptionText: "prefs.use_internal_switcher_desc".localized,
checkboxText: "prefs.use_internal_switcher".localized,
preference: .useInternalSwitcher,
action: {}
), */
HotkeyPreferenceView.make(
sectionText: "prefs.global_shortcut".localized,
descriptionText: "prefs.shortcut_desc".localized,
self
)
Preferences.update(.globalHotkey, value: newGlobalKeybind.toJson())
updateKeybindButton(newGlobalKeybind)
buttonClearShortcut.isEnabled = true
App.shared.shortcutHotkey = HotKey(
keyCombo: KeyCombo(
carbonKeyCode: UInt32(event.keyCode),
carbonModifiers: event.modifierFlags.carbonFlags
)
)
}
].forEach({ self.stackView.addArrangedSubview($0) })
}
@IBAction func register(_ sender: Any) {
unregister(nil)
listeningForGlobalHotkey = true
view.window?.makeFirstResponder(nil)
}
// MARK: - Listening for hotkey dleegate
@IBAction func unregister(_ sender: Any?) {
listeningForGlobalHotkey = false
App.shared.shortcutHotkey = nil
buttonSetShortcut.title = ""
var listeningForHotkeyView: HotkeyPreferenceView? = nil
Preferences.update(.globalHotkey, value: nil)
override func viewWillDisappear() {
if listeningForHotkeyView !== nil {
listeningForHotkeyView = nil
}
func updateClearButton(_ globalKeybindPreference: GlobalKeybindPreference?) {
if globalKeybindPreference != nil {
buttonClearShortcut.isEnabled = true
} else {
buttonClearShortcut.isEnabled = false
}
}
func updateKeybindButton(_ globalKeybindPreference: GlobalKeybindPreference) {
buttonSetShortcut.title = globalKeybindPreference.description
}
@IBAction func pressed(_ sender: Any) {
self.view.window?.windowController?.close()
}
// MARK: - Deinitialization

View File

@ -13,22 +13,32 @@ struct Keys {
static let Space = 49
}
class PrefsWC: NSWindowController {
class PrefsWC: PMWindowController {
// MARK: - Window Identifier
override var windowName: String {
return "Preferences"
}
// MARK: - Window Lifecycle
override func windowDidLoad() {
super.windowDidLoad()
}
// MARK: - Key Interaction
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)
if let vc = self.contentViewController as? PrefsVC {
if vc.listeningForGlobalHotkey {
if let vc = contentViewController as? PrefsVC {
if vc.listeningForHotkeyView != nil {
if event.keyCode == Keys.Escape || event.keyCode == Keys.Space {
print("A blacklisted key was pressed, canceling listen")
vc.listeningForGlobalHotkey = false
vc.listeningForHotkeyView = nil
} else {
vc.updateGlobalShortcut(event)
vc.listeningForHotkeyView!.updateShortcut(event)
}
}
}

View File

@ -0,0 +1,44 @@
//
// CheckboxPreferenceView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 17/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import Foundation
import Cocoa
class CheckboxPreferenceView: NSView, XibLoadable {
@IBOutlet weak var labelSection: NSTextField!
@IBOutlet weak var labelDescription: NSTextField!
@IBOutlet weak var buttonCheckbox: NSButton!
var action: (() -> Void)!
var preference: PreferenceName! {
didSet {
let shouldDisplay = Preferences.preferences[self.preference] as! Bool == true
self.buttonCheckbox.state = shouldDisplay ? .on : .off
}
}
static func make(sectionText: String, descriptionText: String, checkboxText: String, preference: PreferenceName, action: @escaping () -> Void) -> NSView {
let view = Self.createFromXib()!
view.labelSection.stringValue = sectionText
view.labelDescription.stringValue = descriptionText
view.buttonCheckbox.title = checkboxText
view.preference = preference
view.action = action
return view
}
@IBAction func toggled(_ sender: Any) {
Preferences.update(self.preference, value: buttonCheckbox.state == .on)
self.action()
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="CheckboxPreferenceView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="596" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Wbz-5A-DqE">
<rect key="frame" x="168" y="26" width="408" height="18"/>
<buttonCell key="cell" type="check" title="CHECKBOX" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="Roe-uj-mHb">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="toggled:" target="c22-O7-iKe" id="c9y-JM-TdE"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
<rect key="frame" x="168" y="5" width="410" height="14"/>
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
<rect key="frame" x="-2" y="27" width="154" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="SECTION" id="46w-Sv-y21">
<font key="font" metaFont="systemMedium" size="13"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="B8f-nb-Y0A" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="5" id="2Zu-h3-qb0"/>
<constraint firstAttribute="trailing" secondItem="Wbz-5A-DqE" secondAttribute="trailing" constant="20" symbolic="YES" id="RwX-EM-dum"/>
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="Wbz-5A-DqE" secondAttribute="bottom" constant="8" symbolic="YES" id="W4U-SA-N2v"/>
<constraint firstItem="Wbz-5A-DqE" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="5" id="Wff-2b-K6W"/>
<constraint firstItem="Wbz-5A-DqE" firstAttribute="leading" secondItem="B8f-nb-Y0A" secondAttribute="trailing" constant="20" id="YCZ-tC-TCi"/>
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" id="Ztd-uk-4aw"/>
<constraint firstItem="Wbz-5A-DqE" firstAttribute="firstBaseline" secondItem="B8f-nb-Y0A" secondAttribute="firstBaseline" id="cdO-YW-08I"/>
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="Wbz-5A-DqE" secondAttribute="bottom" constant="8" symbolic="YES" id="cvb-Is-ZlF"/>
<constraint firstItem="Bcg-X1-qca" firstAttribute="leading" secondItem="Wbz-5A-DqE" secondAttribute="leading" id="goU-3A-lTq"/>
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
</constraints>
<connections>
<outlet property="buttonCheckbox" destination="Wbz-5A-DqE" id="jZ3-Tf-ncG"/>
<outlet property="labelDescription" destination="Bcg-X1-qca" id="T23-ag-AUf"/>
<outlet property="labelSection" destination="B8f-nb-Y0A" id="i61-ls-yM0"/>
</connections>
<point key="canvasLocation" x="149" y="-114.5"/>
</customView>
</objects>
</document>

View File

@ -0,0 +1,97 @@
//
// HotkeyPreferenceView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 17/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import Foundation
import HotKey
import Cocoa
class HotkeyPreferenceView: NSView, XibLoadable {
weak var delegate: PrefsVC?
@IBOutlet weak var labelSection: NSTextField!
@IBOutlet weak var labelDescription: NSTextField!
@IBOutlet weak var buttonSetShortcut: NSButton!
@IBOutlet weak var buttonClearShortcut: NSButton!
static func make(sectionText: String, descriptionText: String, _ prefsVC: PrefsVC) -> NSView {
let view = Self.createFromXib()!
view.labelSection.stringValue = sectionText
view.labelDescription.stringValue = descriptionText
view.buttonClearShortcut.title = "prefs.shortcut_clear".localized
view.delegate = prefsVC
view.loadGlobalKeybindFromPreferences()
return view
}
// MARK: - Shortcut Functionality
// Adapted from: https://dev.to/mitchartemis/creating-a-global-configurable-shortcut-for-macos-apps-in-swift-25e9
func updateShortcut(_ event: NSEvent) {
guard let characters = event.charactersIgnoringModifiers else { return }
let newGlobalKeybind = GlobalKeybindPreference.init(
function: event.modifierFlags.contains(.function),
control: event.modifierFlags.contains(.control),
command: event.modifierFlags.contains(.command),
shift: event.modifierFlags.contains(.shift),
option: event.modifierFlags.contains(.option),
capsLock: event.modifierFlags.contains(.capsLock),
carbonFlags: event.modifierFlags.carbonFlags,
characters: characters,
keyCode: UInt32(event.keyCode)
)
Preferences.update(.globalHotkey, value: newGlobalKeybind.toJson())
updateKeybindButton(newGlobalKeybind)
buttonClearShortcut.isEnabled = true
App.shared.shortcutHotkey = HotKey(
keyCombo: KeyCombo(
carbonKeyCode: UInt32(event.keyCode),
carbonModifiers: event.modifierFlags.carbonFlags
)
)
}
func loadGlobalKeybindFromPreferences() {
let globalKeybind = GlobalKeybindPreference.fromJson(Preferences.preferences[.globalHotkey] as! String?)
if (globalKeybind != nil) {
updateKeybindButton(globalKeybind!)
} else {
buttonSetShortcut.title = "prefs.shortcut_set".localized
}
buttonClearShortcut.isEnabled = globalKeybind != nil
}
func updateKeybindButton(_ globalKeybindPreference: GlobalKeybindPreference) {
buttonSetShortcut.title = globalKeybindPreference.description
}
@IBAction func register(_ sender: Any) {
unregister(nil)
delegate?.listeningForHotkeyView = self
delegate?.view.window?.makeFirstResponder(nil)
buttonSetShortcut.title = "prefs.shortcut_listening".localized
}
@IBAction func unregister(_ sender: Any?) {
delegate?.listeningForHotkeyView = nil
App.shared.shortcutHotkey = nil
buttonSetShortcut.title = "prefs.shortcut_set".localized
Preferences.update(.globalHotkey, value: nil)
}
}

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="HotkeyPreferenceView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="596" height="52"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="B8f-nb-Y0A">
<rect key="frame" x="-2" y="31" width="154" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="150" id="euj-t0-xv4"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="SECTION" id="46w-Sv-y21">
<font key="font" metaFont="systemMedium" size="13"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gBj-K1-Q2I">
<rect key="frame" x="163" y="20" width="184" height="32"/>
<constraints>
<constraint firstAttribute="width" constant="170" id="U5r-ZA-RFy"/>
</constraints>
<buttonCell key="cell" type="push" title="SET_SHORTCUT" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="H49-35-Mca">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="register:" target="c22-O7-iKe" id="RSp-Go-nhA"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="iUx-vA-jg4">
<rect key="frame" x="345" y="20" width="138" height="32"/>
<constraints>
<constraint firstAttribute="width" constant="124" id="pAc-6D-sMp"/>
</constraints>
<buttonCell key="cell" type="push" title="CLEAR_SHORTCUT" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="fGz-4W-JTL">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystem"/>
</buttonCell>
<connections>
<action selector="unregister:" target="c22-O7-iKe" id="zEw-uN-BFM"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bcg-X1-qca">
<rect key="frame" x="168" y="5" width="410" height="14"/>
<textFieldCell key="cell" title="DESCRIPTION" id="9fH-up-Sob">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="B8f-nb-Y0A" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="5" id="2Zu-h3-qb0"/>
<constraint firstItem="iUx-vA-jg4" firstAttribute="leading" secondItem="gBj-K1-Q2I" secondAttribute="trailing" constant="12" symbolic="YES" id="3fW-pY-HBu"/>
<constraint firstItem="gBj-K1-Q2I" firstAttribute="top" secondItem="B8f-nb-Y0A" secondAttribute="top" id="7JI-pU-DnQ"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="iUx-vA-jg4" secondAttribute="trailing" constant="112" id="AVQ-1M-kE4"/>
<constraint firstItem="iUx-vA-jg4" firstAttribute="top" secondItem="gBj-K1-Q2I" secondAttribute="top" id="O2C-aI-XFS"/>
<constraint firstItem="Bcg-X1-qca" firstAttribute="top" secondItem="gBj-K1-Q2I" secondAttribute="bottom" constant="8" id="Sly-aj-yUl"/>
<constraint firstAttribute="trailing" secondItem="Bcg-X1-qca" secondAttribute="trailing" constant="20" symbolic="YES" id="UPo-Il-l81"/>
<constraint firstItem="B8f-nb-Y0A" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" id="Ztd-uk-4aw"/>
<constraint firstItem="Bcg-X1-qca" firstAttribute="leading" secondItem="gBj-K1-Q2I" secondAttribute="leading" id="fuY-6S-QGB"/>
<constraint firstAttribute="bottom" secondItem="Bcg-X1-qca" secondAttribute="bottom" constant="5" id="hNE-mU-jcu"/>
<constraint firstItem="gBj-K1-Q2I" firstAttribute="leading" secondItem="B8f-nb-Y0A" secondAttribute="trailing" constant="20" id="wnL-4n-cDh"/>
</constraints>
<connections>
<outlet property="buttonClearShortcut" destination="iUx-vA-jg4" id="Xtu-zg-m0z"/>
<outlet property="buttonSetShortcut" destination="gBj-K1-Q2I" id="T8h-4s-c34"/>
<outlet property="labelDescription" destination="Bcg-X1-qca" id="hOs-y6-gDq"/>
<outlet property="labelSection" destination="B8f-nb-Y0A" id="Fbc-eW-CXF"/>
</connections>
<point key="canvasLocation" x="149" y="-111"/>
</customView>
</objects>
</document>

View File

@ -0,0 +1,51 @@
//
// SiteListCell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 03/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
import AppKit
class SiteListCell: NSTableCellView
{
@IBOutlet weak var labelSiteName: NSTextField!
@IBOutlet weak var labelPathName: NSTextField!
@IBOutlet weak var imageViewLock: NSImageView!
@IBOutlet weak var imageViewType: NSImageView!
@IBOutlet weak var labelDriver: NSTextField!
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
func populateCell(with site: Valet.Site) {
// Make sure to show the TLD
labelSiteName.stringValue = "\(site.name!).\(Valet.shared.config.tld)"
// Show the absolute path, except make sure to replace the /Users/username segment with ~ for readability
labelPathName.stringValue = site.absolutePath
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
// If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked).
imageViewType.image = NSImage(
named: site.aliasPath == nil
? "IconParked"
: "IconLinked"
)
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.init(red: 63/255, green: 195/255, blue: 128/255, alpha: 1.0) // green
: NSColor.init(red: 246/255, green: 71/255, blue: 71/255, alpha: 1.0) // red
// Show the current driver
labelDriver.stringValue = site.driver ?? "???"
}
}

View File

@ -0,0 +1,93 @@
//
// SiteListVC+ContextMenu.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension SiteListVC {
internal func reloadContextMenu() {
guard let site = selectedSite else {
tableView.menu = nil
return
}
let menu = NSMenu()
addSystemApps(to: menu)
addSeparator(to: menu)
addDetectedApps(to: menu)
addSeparator(to: menu)
addUnlink(to: menu, with: site)
addToggleSecure(to: menu, with: site)
tableView.menu = menu
}
private func addSystemApps(to menu: NSMenu) {
menu.addItem(withTitle: "site_list.system_apps".localized, action: nil, keyEquivalent: "")
menu.addItem(
withTitle: "site_list.open_in_finder".localized,
action: #selector(self.openInFinder),
keyEquivalent: "F"
)
menu.addItem(
withTitle: "site_list.open_in_terminal".localized,
action: #selector(self.openInTerminal),
keyEquivalent: "T"
)
menu.addItem(
withTitle: "site_list.open_in_browser".localized,
action: #selector(self.openInBrowser),
keyEquivalent: "B"
)
}
private func addDetectedApps(to menu: NSMenu) {
if (applications.count > 0) {
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: "site_list.detected_apps".localized, action: nil, keyEquivalent: "")
for (_, editor) in applications.enumerated() {
let editorMenuItem = EditorMenuItem(
title: "Open with \(editor.name)",
action: #selector(self.openWithEditor(sender:)),
keyEquivalent: ""
)
editorMenuItem.editor = editor
menu.addItem(editorMenuItem)
}
}
}
private func addUnlink(to menu: NSMenu, with site: Valet.Site) {
if (site.aliasPath != nil) {
menu.addItem(
withTitle: "site_list.unlink".localized,
action: #selector(self.unlinkSite),
keyEquivalent: ""
)
menu.addItem(NSMenuItem.separator())
}
}
private func addToggleSecure(to menu: NSMenu, with site: Valet.Site) {
menu.addItem(
withTitle: site.secured
? "site_list.unsecure".localized
: "site_list.secure".localized,
action: #selector(toggleSecure),
keyEquivalent: ""
)
}
private func addSeparator(to menu: NSMenu) {
menu.addItem(NSMenuItem.separator())
}
}

View File

@ -0,0 +1,273 @@
//
// SiteListVC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
import HotKey
import Carbon
class SiteListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
// MARK: - Outlets
@IBOutlet weak var tableView: NSTableView!
@IBOutlet weak var progressIndicator: NSProgressIndicator!
// MARK: - Variables
/// List of sites that will be displayed in this view. Originates from the `Valet` object.
var sites: [Valet.Site] = []
/// Array that contains various apps that might open a particular site directory.
var applications: [Application] {
return App.shared.detectedApplications
}
/// String that was last searched for. Empty by default.
var lastSearchedFor = ""
// MARK: - Helper Variables
var selectedSite: Valet.Site? {
if tableView.selectedRow == -1 {
return nil
}
return sites[tableView.selectedRow]
}
// MARK: - Display
public static func create(delegate: NSWindowDelegate?) {
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
let windowController = storyboard.instantiateController(
withIdentifier: "siteListWindow"
) as! SiteListWC
windowController.window!.title = "site_list.title".localized
windowController.window!.subtitle = "site_list.subtitle".localized
windowController.window!.delegate = delegate
windowController.window!.styleMask = [
.titled, .closable, .resizable, .miniaturizable
]
windowController.window!.minSize = NSSize(width: 550, height: 200)
windowController.window!.delegate = windowController
windowController.positionWindowInTopLeftCorner()
App.shared.siteListWindowController = windowController
}
public static func show(delegate: NSWindowDelegate? = nil) {
if (App.shared.siteListWindowController == nil) {
Self.create(delegate: delegate)
}
App.shared.siteListWindowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
}
// MARK: - Lifecycle
override func viewDidLoad() {
tableView.doubleAction = #selector(self.doubleClicked(sender:))
if !Valet.shared.sites.isEmpty {
// Preloaded list
sites = Valet.shared.sites
searchedFor(text: lastSearchedFor)
} else {
reloadSites()
}
}
// MARK: - Async Operations
/**
Disables the UI so the user cannot interact with it.
Also shows a spinner to indicate that we're busy.
*/
private func setUIBusy() {
progressIndicator.startAnimation(nil)
tableView.alphaValue = 0.3
tableView.isEnabled = false
}
/**
Re-enables the UI so the user can interact with it.
*/
private func setUINotBusy() {
progressIndicator.stopAnimation(nil)
tableView.alphaValue = 1.0
tableView.isEnabled = true
}
/**
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 = {})
{
setUIBusy()
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
execute()
DispatchQueue.main.async { [self] in
completion()
setUINotBusy()
}
}
}
// MARK: - Site Data Loading
func reloadSites() {
waitAndExecute {
Valet.shared.reloadSites()
} completion: { [self] in
sites = Valet.shared.sites
searchedFor(text: lastSearchedFor)
}
}
// MARK: - Table View Delegate
func numberOfRows(in tableView: NSTableView) -> Int {
return sites.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let userCell = tableView.makeView(
withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "siteItem"), owner: self
) as? SiteListCell else { return nil }
userCell.populateCell(with: sites[row])
return userCell
}
func tableViewSelectionDidChange(_ notification: Notification) {
reloadContextMenu()
}
@objc func doubleClicked(sender: Any) {
guard self.selectedSite != nil else {
return
}
self.openInBrowser()
}
// MARK: Secure & Unsecure
@objc public func toggleSecure() {
let rowToReload = tableView.selectedRow
let originalSecureStatus = selectedSite!.secured
let action = selectedSite!.secured ? "unsecure" : "secure"
let selectedSite = selectedSite!
let command = "cd \(selectedSite.absolutePath!) && sudo \(Paths.valet) \(action) && exit;"
waitAndExecute {
Shell.run(command, requiresPath: true)
} completion: { [self] in
selectedSite.determineSecured(Valet.shared.config.tld)
if selectedSite.secured == originalSecureStatus {
Alert.notify(
message: "site_list.alerts_status_not_changed.title".localized,
info: "site_list.alerts_status_not_changed.desc".localized(command)
)
} else {
let newState = selectedSite.secured ? "secured" : "unsecured"
LocalNotification.send(
title: "site_list.alerts_status_changed.title".localized,
subtitle: "site_list.alerts_status_changed.desc"
.localized(
"\(selectedSite.name!).\(Valet.shared.config.tld)",
newState
)
)
}
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0])
tableView.deselectRow(rowToReload)
tableView.selectRowIndexes([rowToReload], byExtendingSelection: true)
}
}
// MARK: Open in Browser & Finder
@objc public func openInBrowser() {
let prefix = selectedSite!.secured ? "https://" : "http://"
let url = "\(prefix)\(selectedSite!.name!).\(Valet.shared.config.tld)"
NSWorkspace.shared.open(URL(string: url)!)
}
@objc public func openInFinder() {
Shell.run("open \(selectedSite!.absolutePath!)")
}
@objc public func openInTerminal() {
Shell.run("open -b com.apple.terminal \(selectedSite!.absolutePath!)")
}
@objc public func unlinkSite() {
guard let site = selectedSite else {
return
}
if site.aliasPath == nil {
return
}
Alert.confirm(
onWindow: view.window!,
messageText: "site_list.confirm_unlink".localized(site.name),
informativeText: "site_link.confirm_link".localized,
buttonTitle: "site_list.unlink".localized,
secondButtonTitle: "Cancel",
style: .critical,
onFirstButtonPressed: {
Shell.run("valet unlink \(site.name!)", requiresPath: true)
self.reloadSites()
}
)
}
// MARK: - (Search) Text Field Delegate
func searchedFor(text: String) {
lastSearchedFor = text
let searchString = text.lowercased()
if searchString.isEmpty {
sites = Valet.shared.sites
tableView.reloadData()
return
}
sites = Valet.shared.sites.filter({ site in
return site.name.lowercased().contains(searchString)
})
tableView.reloadData()
}
// MARK: - Context Menu
@objc func openWithEditor(sender: EditorMenuItem) {
guard let editor = sender.editor else { return }
editor.openDirectory(file: selectedSite!.absolutePath!)
}
// MARK: - Deinitialization
deinit {
print("VC deallocated")
}
}

View File

@ -0,0 +1,50 @@
//
// SiteListWC.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 03/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class SiteListWC: PMWindowController, NSSearchFieldDelegate, NSToolbarDelegate {
// MARK: - Window Identifier
override var windowName: String {
return "SiteList"
}
// MARK: - Outlets
@IBOutlet weak var searchToolbarItem: NSSearchToolbarItem!
// MARK: - Window Lifecycle
override func windowDidLoad() {
super.windowDidLoad()
self.searchToolbarItem.searchField.delegate = self
self.searchToolbarItem.searchField.becomeFirstResponder()
}
// MARK: - Search functionality
var contentVC: SiteListVC {
return self.contentViewController as! SiteListVC
}
func controlTextDidChange(_ notification: Notification) {
guard let searchField = notification.object as? NSSearchField else {
return
}
contentVC.searchedFor(text: searchField.stringValue)
}
// MARK: - Reload functionality
@IBAction func pressedReload(_ sender: Any) {
contentVC.reloadSites()
}
}

View File

@ -33,12 +33,14 @@ class Paths {
print("This usually means we're in trouble... (no Homebrew?)")
baseDir = .usr
}
print("Homebrew directory: \(baseDir)")
}
// - MARK: Binaries
public static var valet: String {
return "/Users/\(whoami)/.composer/vendor/bin/valet"
}
public static var brew: String {
return "\(binPath)/brew"
}
@ -53,6 +55,10 @@ class Paths {
// - MARK: Paths
public static var whoami: String {
return String(Shell.pipe("whoami").split(separator: "\n")[0])
}
public static var binPath: String {
return "\(shared.baseDir.rawValue)/bin"
}

View File

@ -11,33 +11,26 @@ class Shell {
// MARK: - Invoke static functions
public static func run(_ command: String) {
Shell.user.run(command)
public static func run(
_ command: String,
requiresPath: Bool = false
) {
Shell.user.run(command, requiresPath: requiresPath)
}
public static func pipe(_ command: String) -> String {
return Shell.user.pipe(command)
public static func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
return Shell.user.pipe(command, requiresPath: requiresPath)
}
// MARK: - Singleton
var shell: String
init() {
// Determine if we're using macOS Catalina or newer (that support /bin/zsh as default shell)
let at_least_10_15 = ProcessInfo.processInfo.isOperatingSystemAtLeast(
.init(majorVersion: 10, minorVersion: 15, patchVersion: 0))
// If macOS Mojave is being used, we'll default to /bin/bash
shell = at_least_10_15
? "/bin/sh"
: "/bin/bash"
print(at_least_10_15
? "Detected recent macOS (> 10.15): defaulting to /bin/sh"
: "Detected older macOS (< 10.15): defaulting to /bin/bash"
)
}
/**
We now require macOS 11, so no need to detect which terminal to use.
*/
var shell: String = "/bin/sh"
/**
Singleton to access a user shell (with --login)
@ -49,37 +42,73 @@ class Shell {
Uses the default shell.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
func run(_ command: String) {
func run(
_ command: String,
requiresPath: Bool = false
) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = pipe(command)
_ = Shell.pipe(command, requiresPath: requiresPath)
}
/**
Runs a shell command and returns the output.
- Parameter command: The command to run
- Parameter shell: Path to the shell to invoke
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
func pipe(_ command: String) -> String {
func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
let shellOutput = self.execute(command, requiresPath: requiresPath)
let hasError = (
shellOutput.standardOutput == ""
&& shellOutput.errorOutput.lengthOfBytes(using: .utf8) > 0
)
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
}
/**
Runs the command and returns a `ShellOutput` object, which contains info about the process.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
- Parameter waitUntilExit: Waits for the command to complete before returning the `ShellOutput`
*/
func execute(
_ command: String,
requiresPath: Bool = false,
waitUntilExit: Bool = false
) -> ShellOutput {
let task = Process()
let outputPipe = Pipe()
let errorPipe = Pipe()
let tailoredCommand = requiresPath
? "export PATH=\(Paths.binPath):$PATH && \(command)"
: command
task.launchPath = self.shell
task.arguments = ["--login", "-c", command]
task.arguments = ["--login", "-c", tailoredCommand]
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
let error = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
let output = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
if (output == "" && error.lengthOfBytes(using: .utf8) > 0) {
return error
if waitUntilExit {
task.waitUntilExit()
}
return output
return ShellOutput(
standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8
)!,
errorOutput: String(
data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8
)!,
task: task
)
}
/**
@ -89,5 +118,18 @@ class Shell {
public static func fileExists(_ path: String) -> Bool {
return Shell.pipe("if [ -f \(path) ]; then /bin/echo -n \"0\"; fi") == "0"
}
}
class ShellOutput {
let standardOutput: String
let errorOutput: String
let task: Process
init(standardOutput: String,
errorOutput: String,
task: Process) {
self.standardOutput = standardOutput
self.errorOutput = errorOutput
self.task = task
}
}

View File

@ -25,7 +25,8 @@
"mi_manage_services" = "Manage services";
"mi_restart_all_services" = "Restart all services";
"mi_stop_all_services" = "Stop all services";
"mi_force_load_latest" = "Force load latest PHP version";
"mi_force_load_latest" = "Force load (latest) PHP %@";
"mi_force_load_latest_unavailable" = "Force load unavailable (PHP %@ not installed)";
"mi_php_refresh" = "Refresh information";
"mi_configuration" = "Configuration";
@ -41,13 +42,47 @@
"mi_detected_extensions" = "Detected Extensions";
"mi_no_extensions_detected" = "No additional extensions detected.";
"mi_valet" = "Laravel Valet";
"mi_sitelist" = "View linked & parked domains...";
"mi_preferences" = "Preferences...";
"mi_quit" = "Quit PHP Monitor";
"mi_about" = "About PHP Monitor";
// SITE LIST
"site_list.title" = "Domains";
"site_list.subtitle" = "Linked & Parked";
"site_list.alerts_status_not_changed.title" = "SSL Status Not Changed";
"site_list.alerts_status_not_changed.desc" = "Something went wrong. Try running the command in your terminal manually: %@";
"site_list.alerts_status_changed.title" = "SSL Status Changed";
"site_list.alerts_status_changed.desc" = "The domain '%@' is now %@.";
"site_list.confirm_unlink" = "Are you sure you want to unlink '%@'?";
"site_link.confirm_link" = "No files will be removed. If needed, the site will need to be relinked via the command line.";
// SITE LIST ACTIONS
"site_list.unlink" = "Unlink Directory";
"site_list.secure" = "Secure Domain";
"site_list.unsecure" = "Unsecure Domain";
"site_list.open_in_finder" = "Open in Finder";
"site_list.open_in_browser" = "Open in Browser";
"site_list.open_in_terminal" = "Open in Terminal";
"site_list.detected_apps" = "Detected Applications";
"site_list.system_apps" = "System Applications";
// EDITORS
"editors.alert.try_again" = "Try Again";
"editors.alert.cancel" = "Cancel";
// PREFERENCES
"prefs.title" = "PHP Monitor";
"prefs.subtitle" = "Preferences";
"prefs.close" = "Close";
"prefs.global_shortcut" = "Global shortcut:";
@ -55,7 +90,7 @@
"prefs.services" = "Services:";
"prefs.auto_restart_services_title" = "Auto-restart PHP-FPM";
"prefs_auto_restart_services_desc" = "When checked, will automatically restart PHP-FPM when\nyou check or uncheck an extension. Slightly slower when enabled, \nbut this applies the extension change immediately for all sites \nyou're serving, no need to restart PHP-FPM manually.";
"prefs.auto_restart_services_desc" = "When checked, will automatically restart PHP-FPM when\nyou check or uncheck an extension. Slightly slower when enabled, \nbut this applies the extension change immediately for all sites \nyou're serving, no need to restart PHP-FPM manually.";
"prefs.dynamic_icon_title" = "Display dynamic icon in menu bar";
"prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible.\nIf checked, it will display the major version number of the\ncurrently linked PHP version.";
@ -63,6 +98,15 @@
"prefs.display_full_php_version" = "Display full PHP version in menu bar";
"prefs.display_full_php_version_desc" = "Display the full version instead of the major version only.\n(This may be undesirable on smaller displays,\nso this is disabled by default.)";
"prefs.use_internal_switcher" = "Use PHP Monitors own version switcher";
"prefs.use_internal_switcher_desc" =
"By default, PHP Monitor will attempt to use Laravel Valet
in order to switch PHP versions. If you prefer a different
switcher or are having issues with `valet use php`, you may
use PHP Monitor's own switcher which is slightly faster,
but might cause issues with permissions in your Homebrew
directories, since PHP Monitor controls the services.";
"prefs.shortcut_set" = "Set global shortcut";
"prefs.shortcut_listening" = "<listening for keypress>";
"prefs.shortcut_clear" = "Clear";
@ -106,11 +150,18 @@
"alert.php_alias_conflict.title" = "Homebrew `php` formula alias conflict detected";
"alert.php_alias_conflict.info" = "PHP Monitor has detected conflicting `php` aliases in your Homebrew setup, both of which have been detected as installed.\n\nThis will likely result in failed linking when switching PHP versions, and will break PHP Monitor functionality.\n\nFor more information, please visit: https://github.com/nicoverbruggen/phpmon/issues/54";
"alert.min_valet_version.title" = "Your version of Valet is outdated, please upgrade!";
"alert.min_valet_version.info" = "You are currently running Valet %@.
For optimal support of the latest versions of PHP and proper version switching, it is recommended that you update to version %@.
You can do this by running `composer global update` in your terminal. After that, run `valet install` again. For best results, restart PHP Monitor after that.";
// STARTUP
/// 1. PHP binary not found
"startup.errors.php_binary.title" = "PHP is not correctly installed";
"startup.errors.php_binary_desc" = "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php` (or `/opt/homebrew/bin/php`). The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)";
"startup.errors.php_binary.desc" = "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php` (or `/opt/homebrew/bin/php`). The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)";
/// 2. PHP not found in /usr/local/opt or /opt/homebrew/opt
"startup.errors.php_opt.title" = "PHP is not correctly installed";